338 lines
11 KiB
Go
338 lines
11 KiB
Go
package rtpconn
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/skald/hall"
|
|
)
|
|
|
|
func newWaitingTestHall(t *testing.T, maxClients int) (*hall.Hall, string) {
|
|
t.Helper()
|
|
name := "waiting-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name())
|
|
oldDirectory := hall.Directory
|
|
hall.Directory = t.TempDir()
|
|
t.Cleanup(func() { hall.Directory = oldDirectory })
|
|
password := testPassword()
|
|
desc := &hall.Description{
|
|
DisplayName: "Stormux", MaxClients: maxClients,
|
|
Users: map[string]hall.UserDescription{
|
|
"Operator": {Password: password, Permissions: makePermission(t, "op")},
|
|
"Operator2": {Password: password, Permissions: makePermission(t, "op")},
|
|
"Alice": {Password: password, Permissions: makePermission(t, "present")},
|
|
"Bob": {Password: password, Permissions: makePermission(t, "present")},
|
|
},
|
|
}
|
|
data, err := json.Marshal(desc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(hall.Directory, name+".json"), data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, err := hall.Add(name, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
cancelScheduledHallUnlock(g)
|
|
cancelWaiting(g, "test cleanup")
|
|
for _, client := range g.GetClients(nil) {
|
|
hall.DelClient(client)
|
|
}
|
|
hall.Delete(name)
|
|
})
|
|
return g, name
|
|
}
|
|
|
|
func joinWaitingTestClient(t *testing.T, name, username string) *webClient {
|
|
t.Helper()
|
|
c := testWebClient(username + "-waiting-id")
|
|
err := handleClientMessage(c, clientMessage{
|
|
Type: "join", Kind: "join", Hall: name,
|
|
Username: &username, Password: "pw",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("waiting join: %v", err)
|
|
}
|
|
drainActions(t, c)
|
|
return c
|
|
}
|
|
|
|
func TestWaitingClientAuthenticatedWithoutMembership(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
|
|
waiter := joinWaitingTestClient(t, name, "Alice")
|
|
if status := g.Status(true, nil); !status.WaitingRoom {
|
|
t.Fatalf("locked hall with operator did not report waiting-room availability: %#v", status)
|
|
}
|
|
if waiter.hall != nil || waiter.waiting == nil {
|
|
t.Fatalf("waiting client membership: hall=%v waiting=%v", waiter.hall, waiter.waiting)
|
|
}
|
|
if waiter.Username() != "Alice" || waiter.waiting.auth.Username != "Alice" {
|
|
t.Fatalf("authenticated identity was not retained: %#v", waiter.waiting.auth)
|
|
}
|
|
if g.GetClient(waiter.Id()) != nil {
|
|
t.Fatal("waiting client was visible as a hall member")
|
|
}
|
|
if err := handleClientMessage(waiter, clientMessage{Type: "chat", Value: "private"}); err != nil {
|
|
t.Fatalf("isolated chat attempt returned fatal error: %v", err)
|
|
}
|
|
if len(g.GetChatHistory()) != 0 {
|
|
t.Fatal("waiting client wrote to hall chat")
|
|
}
|
|
duplicate := testWebClient("direct-duplicate-id")
|
|
username := "Alice"
|
|
if _, err := hall.AddClient(name, duplicate, hall.ClientCredentials{
|
|
Username: &username, Password: "pw",
|
|
}); err == nil {
|
|
t.Fatal("waiting username was not reserved against direct admission")
|
|
}
|
|
messages := drainMessages(waiter)
|
|
foundEnter := false
|
|
for _, message := range messages {
|
|
if message.Type == "waiting" && message.Kind == "enter" {
|
|
foundEnter = true
|
|
}
|
|
if message.Type == "user" || message.Type == "chat" || message.Type == "chathistory" || message.Type == "offer" ||
|
|
(message.Type == "usermessage" && (message.Kind == "recording" || message.Kind == "chalkboard")) {
|
|
t.Fatalf("waiting client received hall-only message: %#v", message)
|
|
}
|
|
}
|
|
if !foundEnter {
|
|
t.Fatalf("missing waiting enter event: %#v", messages)
|
|
}
|
|
}
|
|
|
|
func TestWaitingClientLivenessMessagesDoNotReportAdmissionError(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
|
|
waiter := joinWaitingTestClient(t, name, "Alice")
|
|
for _, messageType := range []string{"ping", "pong"} {
|
|
if err := handleClientMessage(waiter, clientMessage{Type: messageType}); err != nil {
|
|
t.Fatalf("waiting %s: %v", messageType, err)
|
|
}
|
|
}
|
|
for _, message := range drainMessages(waiter) {
|
|
if message.Type == "usermessage" && message.Kind == "error" {
|
|
t.Fatalf("waiting liveness message produced error: %#v", message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWaitingManualAdmissionAndLastOperatorCancellation(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
waiter := joinWaitingTestClient(t, name, "Alice")
|
|
|
|
if err := admitWaitingClient(operator, "Alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
drainActions(t, waiter)
|
|
if waiter.hall != g || waiter.waiting != nil {
|
|
t.Fatalf("operator admission did not join: hall=%v waiting=%v", waiter.hall, waiter.waiting)
|
|
}
|
|
|
|
second := joinWaitingTestClient(t, name, "Bob")
|
|
leaveHall(operator)
|
|
drainActions(t, second)
|
|
if second.waiting != nil {
|
|
t.Fatal("waiting entry remained after last operator left")
|
|
}
|
|
if second.Username() != "" || len(second.Permissions()) != 0 {
|
|
t.Fatal("cancelled waiting client retained its authenticated identity")
|
|
}
|
|
foundCancel := false
|
|
for _, message := range drainMessages(second) {
|
|
if message.Type == "waiting" && message.Kind == "cancel" {
|
|
foundCancel = true
|
|
}
|
|
}
|
|
if !foundCancel {
|
|
t.Fatal("waiting client did not receive cancellation")
|
|
}
|
|
}
|
|
|
|
func TestWaitingAutomaticAdmissionRetriesInOrderAtCapacity(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 2)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
bob := joinWaitingTestClient(t, name, "Bob")
|
|
if err := updateWaitingPreference(alice, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := updateWaitingPreference(bob, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
setHallLockState(g, false, "")
|
|
drainActions(t, alice)
|
|
drainActions(t, bob)
|
|
if alice.hall != g || bob.hall != nil || bob.waiting == nil {
|
|
t.Fatalf("unexpected first admission: alice=%v bob=%v bobWaiting=%v", alice.hall, bob.hall, bob.waiting)
|
|
}
|
|
leaveHall(alice)
|
|
drainActions(t, bob)
|
|
if bob.hall != g || bob.waiting != nil {
|
|
t.Fatalf("capacity retry did not admit Bob: hall=%v waiting=%v", bob.hall, bob.waiting)
|
|
}
|
|
}
|
|
|
|
func TestWaitingAdmissionRequiresOperator(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
if err := admitWaitingClient(alice, "Alice"); err == nil {
|
|
t.Fatal("waiting user admitted themself")
|
|
}
|
|
}
|
|
|
|
func TestWaitingRequiresOperatorAndReservesUsername(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
g.SetLocked(true, "")
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
if alice.waiting != nil {
|
|
t.Fatal("client waited without an active operator")
|
|
}
|
|
foundFailure := false
|
|
for _, message := range drainMessages(alice) {
|
|
if message.Type == "joined" && message.Kind == "fail" {
|
|
foundFailure = true
|
|
}
|
|
}
|
|
if !foundFailure {
|
|
t.Fatal("locked hall without an operator did not reject the join")
|
|
}
|
|
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
drainActions(t, operator)
|
|
first := joinWaitingTestClient(t, name, "Alice")
|
|
second := joinWaitingTestClient(t, name, "Alice")
|
|
if first.waiting == nil || second.waiting != nil {
|
|
t.Fatalf("username reservation failed: first=%v second=%v", first.waiting, second.waiting)
|
|
}
|
|
removeWaitingClient(first, true)
|
|
third := joinWaitingTestClient(t, name, "Alice")
|
|
if third.waiting == nil {
|
|
t.Fatal("username was not released after waiting client departed")
|
|
}
|
|
}
|
|
|
|
func TestWaitingRelockDisablesManualAdmission(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 1)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
setHallLockState(g, false, "")
|
|
setHallLockState(g, true, "")
|
|
if err := requestWaitingAdmission(alice); err == nil {
|
|
t.Fatal("manual admission succeeded after the hall was relocked")
|
|
}
|
|
if alice.hall != nil || alice.waiting == nil {
|
|
t.Fatalf("relocked client left waiting state: hall=%v waiting=%v", alice.hall, alice.waiting)
|
|
}
|
|
}
|
|
|
|
func TestWaitingManualJoinAfterOpening(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
setHallLockState(g, false, "")
|
|
if err := requestWaitingAdmission(alice); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
drainActions(t, alice)
|
|
if alice.hall != g || alice.waiting != nil {
|
|
t.Fatalf("manual join did not complete: hall=%v waiting=%v", alice.hall, alice.waiting)
|
|
}
|
|
}
|
|
|
|
func TestWaitingApprovalSurvivesCapacityAndRelock(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 2)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
bob := joinWaitingTestClient(t, name, "Bob")
|
|
if err := admitWaitingClient(operator, "Alice"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
drainActions(t, alice)
|
|
if alice.hall != g {
|
|
t.Fatal("first approved user did not fill available capacity")
|
|
}
|
|
if err := admitWaitingClient(operator, "Bob"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bob.waiting == nil || !bob.waiting.approved {
|
|
t.Fatal("approval was not retained while the hall was full")
|
|
}
|
|
setHallLockState(g, false, "")
|
|
setHallLockState(g, true, "")
|
|
leaveHall(alice)
|
|
drainActions(t, bob)
|
|
if bob.hall != g || bob.waiting != nil {
|
|
t.Fatalf("approved user was not admitted after capacity opened: hall=%v waiting=%v", bob.hall, bob.waiting)
|
|
}
|
|
}
|
|
|
|
func TestWaitingCancelledWhenLastOperatorLosesPermission(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
operator := addTestWebClient(t, name, "Operator", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, operator)
|
|
alice := joinWaitingTestClient(t, name, "Alice")
|
|
if err := handleAction(operator, changePermissionsAction{kind: "unop"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Exercise the window before permissionsChangedAction is processed.
|
|
WaitingCapacityChanged(g)
|
|
drainActions(t, operator)
|
|
drainActions(t, alice)
|
|
if alice.waiting != nil {
|
|
t.Fatal("waiting continued after the last operator lost permission")
|
|
}
|
|
}
|
|
|
|
func TestWaitingScheduleRemovedWhenIssuerLeaves(t *testing.T) {
|
|
g, name := newWaitingTestHall(t, 0)
|
|
issuer := addTestWebClient(t, name, "Operator", "op")
|
|
other := addTestWebClient(t, name, "Operator2", "op")
|
|
g.SetLocked(true, "")
|
|
drainActions(t, issuer)
|
|
drainActions(t, other)
|
|
waiter := joinWaitingTestClient(t, name, "Alice")
|
|
now := unlockNow()
|
|
if err := scheduleHallUnlock(g, issuer, now.Add(10*time.Minute), now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := scheduledUnlockDeadline(g); !ok {
|
|
t.Fatal("scheduled deadline was not recorded")
|
|
}
|
|
leaveHall(issuer)
|
|
if _, ok := scheduledUnlockDeadline(g); ok {
|
|
t.Fatal("scheduled deadline remained after issuer departure")
|
|
}
|
|
if waiter.waiting == nil {
|
|
t.Fatal("waiter was cancelled even though another operator remained")
|
|
}
|
|
}
|