Files
skald/rtpconn/webclient_test.go

333 lines
7.8 KiB
Go

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