Chalkboard feature added.
This commit is contained in:
@@ -2,10 +2,15 @@ package rtpconn
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/skald/hall"
|
||||
"git.stormux.org/storm/skald/token"
|
||||
"git.stormux.org/storm/skald/unbounded"
|
||||
)
|
||||
|
||||
var tokens = []string{
|
||||
@@ -50,3 +55,243 @@ func TestParseStatefulToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user