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) } }) }