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