package rtpconn import ( "encoding/json" "os" "path/filepath" "reflect" "strings" "testing" "git.stormux.org/storm/skald/diskwriter" "git.stormux.org/storm/skald/hall" "git.stormux.org/storm/skald/token" "git.stormux.org/storm/skald/unbounded" ) var tokens = []string{ `{ "token": "a", "hall": "h", "username": "u", "permissions":["present"], "expires": "2023-05-03T20:24:47.616624532+02:00" }`, `{ "token": "a", "hall": "h" }`, `{ "token": "a", "hall": "h", "username":"" }`, } func TestParseStatefulToken(t *testing.T) { for i, tok := range tokens { var t1 *token.Stateful err := json.Unmarshal([]byte(tok), &t1) if err != nil { t.Errorf("Unmarshal %v: %v", i, err) continue } var m map[string]interface{} err = json.Unmarshal([]byte(tok), &m) if err != nil { t.Errorf("Unmarshal (map) %v: %v", i, err) continue } t2, err := parseStatefulToken(m) if err != nil { t.Errorf("parseStatefulToken %v: %v", i, err) } if !reflect.DeepEqual(t1, t2) { t.Errorf("Mismatch: %v, %v", t1, t2) } } } func TestHallRecording(t *testing.T) { oldDirectory := hall.Directory hall.Directory = t.TempDir() defer func() { hall.Directory = oldDirectory }() if err := os.WriteFile( filepath.Join(hall.Directory, "recording-status-test.json"), []byte("{}"), 0o600, ); err != nil { t.Fatalf("Write hall description: %v", err) } g, err := hall.Add("recording-status-test", nil) if err != nil { t.Fatalf("Add hall: %v", err) } defer hall.Delete(g.Name()) if hallRecording(g) { t.Fatal("empty hall reported active recording") } disk := diskwriter.New(g) if _, err := hall.AddClient(g.Name(), disk, hall.ClientCredentials{}); err != nil { t.Fatalf("Add recording client: %v", err) } defer hall.DelClient(disk) if !hallRecording(g) { t.Fatal("hall did not report active recording") } } func testWebClient(id string) *webClient { return &webClient{ id: id, actions: unbounded.New[any](), done: make(chan struct{}), writeCh: make(chan interface{}, 100), writerDone: make(chan struct{}), } } func makePermission(t *testing.T, name string) hall.Permissions { t.Helper() p, err := hall.NewPermissions(name) if err != nil { t.Fatalf("NewPermissions(%q): %v", name, err) } return p } func testPassword() hall.Password { key := "pw" return hall.Password{ Type: "plain", Key: &key, } } func addTestWebClient(t *testing.T, hallName string, username string, permission string) *webClient { t.Helper() c := testWebClient(username + "-id") _, err := hall.AddClient(hallName, c, hall.ClientCredentials{ Username: &username, Password: "pw", }) if err != nil { t.Fatalf("AddClient(%q): %v", username, err) } c.hall = hall.Get(hallName) drainActions(t, c) drainMessages(c) if !member(permission, c.permissions) { t.Fatalf("%q permissions %v, missing %q", username, c.permissions, permission) } return c } func newChalkboardTestHall(t *testing.T) string { t.Helper() name := "chalkboard-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name()) oldDirectory := hall.Directory hall.Directory = t.TempDir() t.Cleanup(func() { hall.Directory = oldDirectory }) password := testPassword() desc := &hall.Description{ Users: map[string]hall.UserDescription{ "Operator": { Password: password, Permissions: makePermission(t, "op"), }, "Admin": { Password: password, Permissions: makePermission(t, "admin"), }, "Participant": { Password: password, Permissions: makePermission(t, "present"), }, }, } data, err := json.Marshal(desc) if err != nil { t.Fatalf("Marshal hall description: %v", err) } if err := os.WriteFile(filepath.Join(hall.Directory, name+".json"), data, 0o666); err != nil { t.Fatalf("Write hall description: %v", err) } if _, err := hall.Add(name, nil); err != nil { t.Fatalf("Add hall: %v", err) } t.Cleanup(func() { if g := hall.Get(name); g != nil { for _, c := range g.GetClients(nil) { hall.DelClient(c) } } hall.Delete(name) }) return name } func drainActions(t *testing.T, c *webClient) { t.Helper() for { select { case <-c.actions.Ch: for _, a := range c.actions.Get() { if err := handleAction(c, a); err != nil { t.Fatalf("handleAction: %v", err) } } default: return } } } func drainMessages(c *webClient) []clientMessage { var messages []clientMessage for { select { case m := <-c.writeCh: switch m := m.(type) { case clientMessage: messages = append(messages, m) case []byte: var cm clientMessage if err := json.Unmarshal(m, &cm); err == nil { messages = append(messages, cm) } } default: return messages } } } func latestChalkboardMessage(messages []clientMessage) (hall.ChalkboardState, bool) { for i := len(messages) - 1; i >= 0; i-- { if messages[i].Type != "usermessage" || messages[i].Kind != "chalkboard" { continue } b, err := json.Marshal(messages[i].Value) if err != nil { return hall.ChalkboardState{}, false } var state hall.ChalkboardState if err := json.Unmarshal(b, &state); err != nil { return hall.ChalkboardState{}, false } return state, true } return hall.ChalkboardState{}, false } func TestChalkboardEditRequiresPermission(t *testing.T) { hallName := newChalkboardTestHall(t) participant := addTestWebClient(t, hallName, "Participant", "present") operator := addTestWebClient(t, hallName, "Operator", "op") err := handleClientMessage(participant, clientMessage{ Type: "hallaction", Kind: "chalkboard", Value: map[string]any{ "text": "should not save", "revision": float64(0), }, }) if err != nil { t.Fatalf("unauthorised chalkboard edit returned error: %v", err) } if got := participant.hall.Chalkboard().Text; got != "" { t.Fatalf("unauthorised edit changed text to %q", got) } err = handleClientMessage(operator, clientMessage{ Type: "hallaction", Kind: "chalkboard", Value: map[string]any{ "text": "line 1\nline 2", "revision": float64(0), }, }) if err != nil { t.Fatalf("operator chalkboard edit: %v", err) } state := operator.hall.Chalkboard() if state.Text != "line 1\nline 2" || state.Revision != 1 { t.Fatalf("chalkboard state = %#v", state) } for _, c := range []*webClient{participant, operator} { got, ok := latestChalkboardMessage(drainMessages(c)) if !ok { t.Fatalf("%v did not receive chalkboard broadcast", c.id) } if got != state { t.Fatalf("%v broadcast = %#v, want %#v", c.id, got, state) } } } func TestChalkboardGrantIsSessionPermission(t *testing.T) { hallName := newChalkboardTestHall(t) admin := addTestWebClient(t, hallName, "Admin", "admin") participant := addTestWebClient(t, hallName, "Participant", "present") err := handleClientMessage(admin, clientMessage{ Type: "useraction", Kind: "chalkboard", Dest: participant.id, }) if err != nil { t.Fatalf("admin chalkboard grant: %v", err) } drainActions(t, participant) if !member("chalkboard", participant.permissions) { t.Fatalf("grant did not add chalkboard permission: %v", participant.permissions) } err = handleClientMessage(participant, clientMessage{ Type: "hallaction", Kind: "chalkboard", Value: map[string]any{ "text": "granted edit", "revision": float64(0), }, }) if err != nil { t.Fatalf("granted chalkboard edit: %v", err) } if got := participant.hall.Chalkboard().Text; got != "granted edit" { t.Fatalf("chalkboard text = %q", got) } err = handleClientMessage(admin, clientMessage{ Type: "useraction", Kind: "unchalkboard", Dest: participant.id, }) if err != nil { t.Fatalf("admin chalkboard revoke: %v", err) } drainActions(t, participant) if member("chalkboard", participant.permissions) { t.Fatalf("revoke left chalkboard permission: %v", participant.permissions) } }