mayhem-party/src/device/input/parse/v01/server_test.go

593 lines
16 KiB
Go

package v01
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"strconv"
"strings"
"sync"
"testing"
"time"
"gopkg.in/yaml.v2"
)
func TestPatchConfig(t *testing.T) {
dir := t.TempDir()
p := path.Join(dir, t.Name()+".yaml")
cases := map[string]struct {
was config
patch string
want config
}{
"replace entire doc": {
was: config{
Feedback: configFeedback{Addr: "a", TTSURL: "a"},
Users: map[string]configUser{"a": configUser{State: configUserState{Player: 1, Message: "a"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"a": "a"}}},
Quiet: true,
},
patch: `[{"op": "replace", "path": "", "value": {
"Feedback": {"Addr": "b", "TTSURL": "b"},
"Users": {"b": {"State":{"Player": 2, "Message": "b"}}},
"Players": [{"Transformation": {"b": "b"}}],
"Quiet": false
}}]`,
want: config{
Feedback: configFeedback{Addr: "b", TTSURL: "b"},
Users: map[string]configUser{"b": configUser{State: configUserState{Player: 2, Message: "b"}}},
Players: []configPlayer{configPlayer{Transformation: transformation{"b": "b"}}},
Quiet: false,
},
},
}
for name, d := range cases {
c := d
for _, usesdisk := range []bool{false, true} {
t.Run(fmt.Sprintf("%s disk=%v", name, usesdisk), func(t *testing.T) {
b, _ := yaml.Marshal(c.was)
os.WriteFile(p, b, os.ModePerm)
FlagParseV01Config = ""
if usesdisk {
FlagParseV01Config = p
}
v01 := &V01{cfg: c.was}
v01.cfg.lock = &sync.Mutex{}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPatch, "/config", strings.NewReader(c.patch))
v01.servePatchConfig(w, r)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
if usesdisk {
b, _ := os.ReadFile(p)
var got config
yaml.Unmarshal(b, &got)
if fmt.Sprintf("%+v", c.want) != fmt.Sprintf("%+v", v01.cfg) {
t.Errorf("want \n\t%+v, got \n\t%+v", c.want, v01.cfg)
}
}
})
}
}
}
func TestServeGM(t *testing.T) {
ctx, can := context.WithCancel(context.Background())
defer can()
do := func(v01 *V01, path, body string, method ...string) *httptest.ResponseRecorder {
m := http.MethodPost
if len(method) > 0 {
m = method[0]
}
w := httptest.NewRecorder()
r := httptest.NewRequest(m, path, strings.NewReader(body))
v01.ServeHTTP(w, r)
return w
}
t.Run("status", func(t *testing.T) {
v01 := NewV01(ctx, nil)
var result struct {
Players int `yaml:"Players"`
Users map[string]string `yaml:"Users"`
}
t.Run("empty", func(t *testing.T) {
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 0 {
t.Error(result.Players)
}
if len(result.Users) != 0 {
t.Error(result.Users)
}
})
t.Run("full", func(t *testing.T) {
v01.cfg.Players = []configPlayer{
{},
{},
{},
{},
}
v01.cfg.Users = map[string]configUser{
"bel": configUser{
State: configUserState{Player: 3},
Meta: configUserMeta{
LastTSMS: time.Now().Add(-1*time.Minute).UnixNano() / int64(time.Millisecond),
LastLag: int64(time.Second / time.Millisecond),
},
},
"zach": configUser{},
"chase": configUser{},
"mason": configUser{},
"nat": configUser{},
"roxy": configUser{},
"bill": configUser{},
}
resp := do(v01, "/gm/rpc/status", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
t.Log(string(resp.Body.Bytes()))
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.Players != 4 {
t.Error(result.Players)
}
if len(result.Users) != 7 {
t.Error(result.Users)
}
if result.Users["bel"] == "" || result.Users["bel"] == "..." {
t.Error(result.Users["bel"])
}
})
})
t.Run("broadcastSomeoneSaidAlias to hotword tap", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.GM = configGM{
Hotwords: map[string]configGMHotword{
"hotword": configGMHotword{
Call: "tap",
Args: []string{"a"},
},
},
}
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
for i := 0; i < 2; i++ {
select {
case btn := <-v01.alt:
if len(btn) != 1 {
t.Error(btn)
} else if btn[0].Down != (i == 0) {
t.Error(btn[0])
} else if btn[0].Char != 'a' {
t.Error(btn[0].Char)
}
case <-time.After(time.Second):
t.Fatal("nothing in alt")
}
}
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias?message=hotword", "")
time.Sleep(time.Millisecond * 150)
if got := v01.Read(); len(got) != 1 || !got[0].Down || got[0].Char != 'a' {
t.Error(got)
} else if got := v01.Read(); len(got) != 1 || got[0].Down || got[0].Char != 'a' {
t.Error(got)
}
})
t.Run("broadcastSomeoneSaidAlias", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = false
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{
GM: configUserStateGM{
Alias: "driver",
},
Message: "if someone else says 'driver', then you get to play",
}},
}
v01.cfg.Broadcast.Message = ":)"
do(v01, "/gm/rpc/broadcastSomeoneSaidAlias", "")
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.GM.LastAlias != "driver" {
t.Error(v.State.GM.LastAlias)
}
if bc := v01.cfg.Broadcast.Message; bc == ":)" {
t.Error(bc)
}
})
t.Run("fillNonPlayerAliases", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if resp.Code != http.StatusNoContent {
t.Error(resp.Code)
}
})
t.Run("not enough", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"zach": configUser{State: configUserState{Player: 0}},
}
resp := do(v01, "/gm/rpc/fillNonPlayerAliases", "[]")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 0}},
}
do(v01, "/gm/rpc/fillNonPlayerAliases", "[qt]")
if v := v01.cfg.Users["bel"]; v.State.GM.Alias != "" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 1 {
t.Error(v.State.Player)
}
if v := v01.cfg.Users["zach"]; v.State.GM.Alias != "qt" {
t.Error(v.State.GM.Alias)
} else if v.State.Player != 0 {
t.Error(v.State.Player)
}
})
})
t.Run("vote", func(t *testing.T) {
type result map[string]string
t.Run("cast bad vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote?user=bel&payload=?", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp)
}
if v01.cfg.Users["bel"].State.Message != "" {
t.Error(v01.cfg.Users["bel"].State.Message)
}
})
t.Run("cast vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}, "zach": {}}
do(v01, "/gm/rpc/vote?user=bel&payload=zach", "")
if v01.cfg.Users["bel"].State.GM.Vote != "zach" {
t.Error(v01.cfg.Users["bel"].State.GM.Vote)
}
})
t.Run("get non vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voting" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get mid vote", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{"bel": {State: configUserState{GM: configUserStateGM{Vote: "zach"}, Message: "driver"}}}
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 1 {
t.Error(result)
}
if result["bel"] != "voted" {
t.Error(result)
}
t.Logf("%+v", result)
})
t.Run("get empty", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = nil
resp := do(v01, "/gm/rpc/vote", "", "GET")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 0 {
t.Error(result)
}
t.Logf("%+v", result)
})
})
t.Run("elect", func(t *testing.T) {
type result map[string]int
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}, Player: 2}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Errorf("%s => %v", resp.Body.Bytes(), err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if v01.cfg.Users["bel"].State.Player != 0 {
t.Error(v01.cfg.Users["bel"].State.Player)
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"].State.Player)
}
if v01.cfg.Broadcast.Message != `bel is now player 0 and zach is now player 1` {
t.Error(v01.cfg.Broadcast)
}
})
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
"bill": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=driver", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 2 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if !strings.HasSuffix(v01.cfg.Broadcast.Message, `is now player 1`) || strings.Contains(v01.cfg.Broadcast.Message, ",") {
t.Error(v01.cfg.Broadcast.Message)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 2 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
t.Run("tie", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Players = []configPlayer{{}}
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{GM: configUserStateGM{Vote: "zach", LastAlias: "driver"}, Player: 1}},
"zach": configUser{State: configUserState{GM: configUserStateGM{Vote: "bel", LastAlias: "pizza"}}},
}
resp := do(v01, "/gm/rpc/elect?alias=pizza", "")
var result result
if err := yaml.Unmarshal(resp.Body.Bytes(), &result); err != nil {
t.Error(err)
}
if len(result) != 2 {
t.Error(result)
} else if result["bel"] != 1 {
t.Error(result)
} else if result["zach"] != 1 {
t.Error(result)
}
if bc := v01.cfg.Broadcast.Message; !strings.HasSuffix(bc, `is now player 1`) || strings.Contains(bc, ",") {
t.Error(bc)
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
if len(assignments) != 2 {
t.Error(assignments)
} else if assignments[0] != 1 {
t.Error(assignments[0])
} else if assignments[1] != 1 {
t.Error(assignments[1])
}
})
})
t.Run("shuffle", func(t *testing.T) {
t.Run("many 2u 2p", func(t *testing.T) {
v01 := NewV01(ctx, nil)
for i := 0; i < 100; i++ {
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
v01.cfg.Players = []configPlayer{{}, {}}
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != 2 {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != 2 {
t.Error(v01.cfg.Users)
} else if bp := v01.cfg.Users["bel"].State.Player; bp != 1 && bp != 2 {
t.Error(bp)
} else if zp := v01.cfg.Users["zach"].State.Player; zp != 1 && zp != 2 {
t.Error(zp)
} else if bp == zp {
t.Error(bp, zp)
}
}
})
cases := map[string]struct {
users int
usersAssigned int
players int
}{
"empty": {},
"just users": {users: 2},
"just players": {players: 2},
"2 unassigned users and 2 players": {users: 2, players: 2},
"2 users and 2 players": {users: 2, usersAssigned: 2, players: 2},
"1 users and 2 players": {users: 1, usersAssigned: 1, players: 2},
"1 unassigned users and 2 players": {users: 1, players: 2},
"4 players for 7 users 0 assigned": {users: 7, players: 4},
"4 players for 7 users 4 assigned": {users: 7, players: 4, usersAssigned: 4},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{}
for i := 0; i < c.users; i++ {
v01.cfg.Users[strconv.Itoa(i)] = configUser{}
if i < c.usersAssigned {
v01.cfg.Users[strconv.Itoa(i)] = configUser{State: configUserState{Player: i}}
}
}
v01.cfg.Players = make([]configPlayer, c.players)
do(v01, "/gm/rpc/shuffle", "")
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if len(v01.cfg.Users) != c.users {
t.Error(v01.cfg.Users)
} else if len(v01.cfg.Players) != c.players {
t.Error(v01.cfg.Users)
}
for i := 0; i < c.users; i++ {
if _, ok := v01.cfg.Users[strconv.Itoa(i)]; !ok {
t.Error(i)
}
}
assignments := map[int]int{}
for _, v := range v01.cfg.Users {
if v.State.Player > 0 {
assignments[v.State.Player] = assignments[v.State.Player] + 1
}
}
lesser := c.users
if c.players < lesser {
lesser = c.players
}
if len(assignments) != lesser {
t.Error(assignments)
}
for _, v := range assignments {
if v != 1 {
t.Error(v)
}
}
})
}
})
t.Run("swap", func(t *testing.T) {
t.Run("self", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=bel", "")
if resp.Code != http.StatusConflict {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("who", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
resp := do(v01, "/gm/rpc/swap?a=bel", "")
if resp.Code != http.StatusBadRequest {
t.Error(resp.Code)
}
if !v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
})
t.Run("happy", func(t *testing.T) {
v01 := NewV01(ctx, nil)
v01.cfg.Quiet = true
v01.cfg.Users = map[string]configUser{
"bel": configUser{State: configUserState{Player: 1}},
"zach": configUser{State: configUserState{Player: 2}},
}
resp := do(v01, "/gm/rpc/swap?a=bel&b=zach", "")
if resp.Code != http.StatusOK {
t.Error(resp.Code)
}
if v01.cfg.Quiet {
t.Error(v01.cfg.Quiet)
}
if v01.cfg.Users["bel"].State.Player != 2 {
t.Error(v01.cfg.Users["bel"])
} else if v01.cfg.Users["zach"].State.Player != 1 {
t.Error(v01.cfg.Users["zach"])
}
})
})
t.Run("404", func(t *testing.T) {
v01 := NewV01(ctx, nil)
resp := do(v01, "/gm/teehee", "")
if resp.Code != http.StatusNotFound {
t.Error(resp.Code)
}
})
}