643 lines
15 KiB
Go
643 lines
15 KiB
Go
package hall
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/skald/conn"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
func TestConstantTimeCompare(t *testing.T) {
|
|
tests := []struct {
|
|
a, b string
|
|
}{
|
|
{"", ""},
|
|
{"a", "a"},
|
|
{"a", "b"},
|
|
{"foo", "foo"},
|
|
{"foo", "bar"},
|
|
{"foo", "foo1"},
|
|
{"foo1", "foo"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
e := ConstantTimeCompare(test.a, test.b)
|
|
if e != (test.a == test.b) {
|
|
t.Errorf("constantTimeCompare(%v, %v): got %v",
|
|
test.a, test.b, e,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHall(t *testing.T) {
|
|
halls.halls = nil
|
|
Add("hall", &Description{})
|
|
Add("hall/subhall", &Description{Public: true})
|
|
if len(halls.halls) != 2 {
|
|
t.Errorf("Expected 2, got %v", len(halls.halls))
|
|
}
|
|
g := Get("hall")
|
|
g2 := Get("hall/subhall")
|
|
if g == nil {
|
|
t.Fatalf("Couldn't get hall")
|
|
}
|
|
if g2 == nil {
|
|
t.Fatalf("Couldn't get hall/subhall")
|
|
}
|
|
if name := g.Name(); name != "hall" {
|
|
t.Errorf("Name: expected hall, got %v", name)
|
|
}
|
|
if locked, _ := g.Locked(); locked {
|
|
t.Errorf("Locked: expected false, got %v", locked)
|
|
}
|
|
api, err := g.API()
|
|
if err != nil || api == nil {
|
|
t.Errorf("Couldn't get API: %v", err)
|
|
}
|
|
|
|
if names := GetNames(); len(names) != 2 {
|
|
t.Errorf("Expected 2, got %v", names)
|
|
}
|
|
|
|
if subs := GetSubHalls("hall"); len(subs) != 0 {
|
|
t.Errorf("Expected [], got %v", subs)
|
|
}
|
|
|
|
if public := GetPublic(nil); len(public) != 1 || public[0].Name != "hall/subhall" {
|
|
t.Errorf("Expected hall/subhall, got %v", public)
|
|
}
|
|
}
|
|
|
|
func TestChatHistory(t *testing.T) {
|
|
g := Hall{
|
|
description: &Description{},
|
|
}
|
|
user := "user"
|
|
for i := 0; i < 2*maxChatHistory; i++ {
|
|
g.AddToChatHistory(
|
|
fmt.Sprintf("id-%v", i),
|
|
fmt.Sprintf("source-%v", i%4),
|
|
&user, time.Now(), "",
|
|
fmt.Sprintf("%v", i),
|
|
)
|
|
}
|
|
h := g.GetChatHistory()
|
|
if len(h) != maxChatHistory {
|
|
t.Errorf("Expected %v, got %v", maxChatHistory, len(g.history))
|
|
}
|
|
for i, s := range h {
|
|
j := i + maxChatHistory
|
|
if s.Id != fmt.Sprintf("id-%v", j) {
|
|
t.Errorf("Expected %v, got %v", j, s.Id)
|
|
}
|
|
if s.Source != fmt.Sprintf("source-%v", j%4) {
|
|
t.Errorf("Expected %v, got %v", j%4, s.Id)
|
|
}
|
|
if s.Value.(string) != fmt.Sprintf("%v", j) {
|
|
t.Errorf("Expected %v, got %v", j, s.Value)
|
|
}
|
|
}
|
|
|
|
l := len(h)
|
|
j := maxChatHistory + 4
|
|
g.ClearChatHistory(
|
|
fmt.Sprintf("id-%v", j), fmt.Sprintf("source-%v", j%4),
|
|
)
|
|
if lh := len(g.GetChatHistory()); lh != l-1 {
|
|
t.Errorf("Expected %v, got %v", l-1, lh)
|
|
}
|
|
g.ClearChatHistory("", fmt.Sprintf("source-%v", j%4))
|
|
if lh := len(g.GetChatHistory()); lh != l*3/4 {
|
|
t.Errorf("Expected %v, got %v", l*3/4, lh)
|
|
}
|
|
g.ClearChatHistory("", "")
|
|
if lh := len(g.GetChatHistory()); lh != 0 {
|
|
t.Errorf("Expected 0, got %v", lh)
|
|
}
|
|
}
|
|
|
|
type testClient struct {
|
|
id string
|
|
username string
|
|
permissions []string
|
|
pushed []pushedClient
|
|
}
|
|
|
|
type pushedClient struct {
|
|
kind string
|
|
id string
|
|
username string
|
|
permissions []string
|
|
}
|
|
|
|
func (c *testClient) Hall() *Hall { return nil }
|
|
func (c *testClient) Addr() net.Addr { return nil }
|
|
func (c *testClient) Id() string { return c.id }
|
|
func (c *testClient) Username() string { return c.username }
|
|
func (c *testClient) SetUsername(username string) { c.username = username }
|
|
func (c *testClient) Permissions() []string { return c.permissions }
|
|
func (c *testClient) SetPermissions(permissions []string) { c.permissions = permissions }
|
|
func (c *testClient) Data() map[string]interface{} { return nil }
|
|
func (c *testClient) PushConn(*Hall, string, conn.Up, []conn.UpTrack, string) error {
|
|
return nil
|
|
}
|
|
func (c *testClient) RequestConns(Client, *Hall, string) error { return nil }
|
|
func (c *testClient) Joined(string, string) error { return nil }
|
|
func (c *testClient) PushClient(_ string, kind string, id string, username string, permissions []string, _ map[string]interface{}) error {
|
|
c.pushed = append(c.pushed, pushedClient{
|
|
kind: kind,
|
|
id: id,
|
|
username: username,
|
|
permissions: permissions,
|
|
})
|
|
return nil
|
|
}
|
|
func (c *testClient) Kick(string, *string, string) error { return nil }
|
|
|
|
func TestSystemClientIsNotAdvertisedToUsers(t *testing.T) {
|
|
halls.halls = nil
|
|
oldDirectory := Directory
|
|
Directory = t.TempDir()
|
|
t.Cleanup(func() {
|
|
Directory = oldDirectory
|
|
halls.halls = nil
|
|
})
|
|
|
|
err := os.WriteFile(
|
|
filepath.Join(Directory, "system-hidden-test.json"),
|
|
[]byte(`{
|
|
"users": {
|
|
"user1": {"password": "secret", "permissions": "present"},
|
|
"user2": {"password": "secret", "permissions": "present"}
|
|
}
|
|
}`),
|
|
0600,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
user1 := &testClient{id: "user1-client"}
|
|
g, err := AddClient("system-hidden-test", user1, ClientCredentials{
|
|
Username: strPtr("user1"),
|
|
Password: "secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
user1.pushed = nil
|
|
|
|
recorder := &testClient{
|
|
id: "recorder-client",
|
|
username: "RECORDING",
|
|
permissions: []string{"system"},
|
|
}
|
|
_, err = AddClient("system-hidden-test", recorder, ClientCredentials{
|
|
System: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(user1.pushed) != 0 {
|
|
t.Fatalf("user saw system recorder client: %#v", user1.pushed)
|
|
}
|
|
if count := g.ClientCount(); count != 1 {
|
|
t.Fatalf("client count with recorder = %d, expected 1", count)
|
|
}
|
|
|
|
user2 := &testClient{id: "user2-client"}
|
|
_, err = AddClient("system-hidden-test", user2, ClientCredentials{
|
|
Username: strPtr("user2"),
|
|
Password: "secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, pushed := range user2.pushed {
|
|
if pushed.id == recorder.id {
|
|
t.Fatalf("joining user saw system recorder client: %#v", user2.pushed)
|
|
}
|
|
}
|
|
for _, pushed := range user1.pushed {
|
|
if pushed.id == recorder.id {
|
|
t.Fatalf("existing user saw system recorder client: %#v", user1.pushed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func TestEmptyUsernameIsRejected(t *testing.T) {
|
|
halls.halls = nil
|
|
oldDirectory := Directory
|
|
Directory = t.TempDir()
|
|
t.Cleanup(func() {
|
|
Directory = oldDirectory
|
|
halls.halls = nil
|
|
})
|
|
|
|
err := os.WriteFile(
|
|
filepath.Join(Directory, "empty-username-test.json"),
|
|
[]byte(`{
|
|
"wildcard-user": {"password": "secret", "permissions": "present"}
|
|
}`),
|
|
0600,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = AddClient("empty-username-test", &testClient{id: "user1"}, ClientCredentials{
|
|
Username: strPtr(""),
|
|
Password: "secret",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("empty username unexpectedly joined")
|
|
}
|
|
}
|
|
|
|
func TestDuplicateUsernameIsRejected(t *testing.T) {
|
|
halls.halls = nil
|
|
oldDirectory := Directory
|
|
Directory = t.TempDir()
|
|
t.Cleanup(func() {
|
|
Directory = oldDirectory
|
|
halls.halls = nil
|
|
})
|
|
|
|
err := os.WriteFile(
|
|
filepath.Join(Directory, "duplicate-username-test.json"),
|
|
[]byte(`{
|
|
"wildcard-user": {"password": "secret", "permissions": "present"}
|
|
}`),
|
|
0600,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = AddClient("duplicate-username-test", &testClient{id: "user1"}, ClientCredentials{
|
|
Username: strPtr("bob1"),
|
|
Password: "secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = AddClient("duplicate-username-test", &testClient{id: "user2"}, ClientCredentials{
|
|
Username: strPtr("bob1"),
|
|
Password: "secret",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("duplicate username unexpectedly joined")
|
|
}
|
|
if got, want := err.Error(), "username already in use"; got != want {
|
|
t.Fatalf("duplicate username error = %q, want %q", got, want)
|
|
}
|
|
|
|
_, err = AddClient("duplicate-username-test", &testClient{id: "user3"}, ClientCredentials{
|
|
Username: strPtr("bob2"),
|
|
Password: "secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("different username should join: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLockedHallJoinMessage(t *testing.T) {
|
|
halls.halls = nil
|
|
oldDirectory := Directory
|
|
Directory = t.TempDir()
|
|
t.Cleanup(func() {
|
|
Directory = oldDirectory
|
|
halls.halls = nil
|
|
})
|
|
|
|
err := os.WriteFile(
|
|
filepath.Join(Directory, "locked-message-test.json"),
|
|
[]byte(`{
|
|
"users": {
|
|
"user1": {"password": "secret", "permissions": "present"},
|
|
"admin": {"password": "secret", "permissions": "op"}
|
|
}
|
|
}`),
|
|
0644,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
g, err := Add("locked-message-test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
g.SetLocked(true, "")
|
|
|
|
username := "user1"
|
|
_, err = AddClient(g.Name(), &testClient{id: "user1"}, ClientCredentials{
|
|
Username: &username,
|
|
Password: "secret",
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("AddClient unexpectedly succeeded")
|
|
}
|
|
if got, want := err.Error(), "Hall is locked."; got != want {
|
|
t.Fatalf("locked join message: got %q, want %q", got, want)
|
|
}
|
|
|
|
adminUsername := "admin"
|
|
_, err = AddClient(g.Name(), &testClient{id: "admin"}, ClientCredentials{
|
|
Username: &adminUsername,
|
|
Password: "secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("operator join should bypass lock: %v", err)
|
|
}
|
|
}
|
|
|
|
func permissionsEqual(a, b []string) bool {
|
|
// nil case
|
|
if len(a) == 0 && len(b) == 0 {
|
|
return true
|
|
}
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
aa := append([]string(nil), a...)
|
|
sort.Slice(aa, func(i, j int) bool {
|
|
return aa[i] < aa[j]
|
|
})
|
|
bb := append([]string(nil), b...)
|
|
sort.Slice(bb, func(i, j int) bool {
|
|
return bb[i] < bb[j]
|
|
})
|
|
for i := range aa {
|
|
if aa[i] != bb[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var jch = "jch"
|
|
var john = "john"
|
|
var james = "james"
|
|
var paul = "paul"
|
|
var peter = "peter"
|
|
var admin = "admin"
|
|
var badClients = []ClientCredentials{
|
|
{Username: &jch, Password: "foo"},
|
|
{Username: &john, Password: "foo"},
|
|
{Username: &james, Password: "foo"},
|
|
}
|
|
|
|
type credPerm struct {
|
|
c ClientCredentials
|
|
p []string
|
|
}
|
|
|
|
var desc2JSON = `
|
|
{
|
|
"max-history-age": 10,
|
|
"auto-subhalls": true,
|
|
"users": {
|
|
"jch": {"password": "topsecret", "permissions": "op"},
|
|
"john": {"password": "secret", "permissions": "present"},
|
|
"james": {"password": "secret2", "permissions": "message"},
|
|
"peter": {"password": "secret4"},
|
|
"admin": {"password": "admin", "permissions": "admin"}
|
|
},
|
|
"wildcard-user":
|
|
{"permissions": "message", "password": {"type":"wildcard"}}
|
|
}`
|
|
|
|
var goodClients = []credPerm{
|
|
{
|
|
ClientCredentials{Username: &jch, Password: "topsecret"},
|
|
[]string{"op", "present", "message", "token"},
|
|
},
|
|
{
|
|
ClientCredentials{Username: &john, Password: "secret"},
|
|
[]string{"present", "message"},
|
|
},
|
|
{
|
|
ClientCredentials{Username: &james, Password: "secret2"},
|
|
[]string{"message"},
|
|
},
|
|
{
|
|
ClientCredentials{Username: &paul, Password: "secret3"},
|
|
[]string{"message"},
|
|
},
|
|
{
|
|
ClientCredentials{Username: &peter, Password: "secret4"},
|
|
[]string{},
|
|
},
|
|
{
|
|
ClientCredentials{Username: &admin, Password: "admin"},
|
|
[]string{"admin"},
|
|
},
|
|
}
|
|
|
|
func TestPermissions(t *testing.T) {
|
|
var g Hall
|
|
err := json.Unmarshal([]byte(desc2JSON), &g.description)
|
|
if err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
for _, c := range badClients {
|
|
t.Run("bad "+*c.Username, func(t *testing.T) {
|
|
var autherr *NotAuthorisedError
|
|
_, p, err := g.GetPermission(c)
|
|
if !errors.As(err, &autherr) {
|
|
t.Errorf("GetPermission %v: %v %v", c, err, p)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, cp := range goodClients {
|
|
t.Run("good "+*cp.c.Username, func(t *testing.T) {
|
|
u, p, err := g.GetPermission(cp.c)
|
|
if err != nil {
|
|
t.Errorf("GetPermission %v: %v", cp.c, err)
|
|
} else if u != *cp.c.Username ||
|
|
!permissionsEqual(p, cp.p) {
|
|
t.Errorf("%v: got %v %v, expected %v",
|
|
cp.c, u, p, cp.p)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestExtraPermissions(t *testing.T) {
|
|
j := `
|
|
{
|
|
"users": {
|
|
"jch": {"password": "topsecret", "permissions": "op"},
|
|
"john": {"password": "secret", "permissions": "present"},
|
|
"james": {"password": "secret2", "permissions": "observe"}
|
|
}
|
|
}`
|
|
|
|
var d Description
|
|
err := json.Unmarshal([]byte(j), &d)
|
|
if err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
doit := func(u string, p []string) {
|
|
t.Helper()
|
|
pu := d.Users[u].Permissions.Permissions(&d)
|
|
if !permissionsEqual(pu, p) {
|
|
t.Errorf("%v: expected %v, got %v", u, p, pu)
|
|
}
|
|
}
|
|
|
|
doit("jch", []string{"op", "token", "present", "message"})
|
|
doit("john", []string{"present", "message"})
|
|
doit("james", []string{})
|
|
|
|
d.AllowRecording = true
|
|
d.UnrestrictedTokens = false
|
|
|
|
doit("jch", []string{
|
|
"op", "record", "token", "present", "message",
|
|
})
|
|
doit("john", []string{"present", "message"})
|
|
doit("james", []string{})
|
|
|
|
d.AllowRecording = false
|
|
d.UnrestrictedTokens = true
|
|
|
|
doit("jch", []string{"op", "token", "present", "message"})
|
|
doit("john", []string{"token", "present", "message"})
|
|
doit("james", []string{})
|
|
|
|
d.AllowRecording = true
|
|
d.UnrestrictedTokens = true
|
|
|
|
doit("jch", []string{
|
|
"op", "record", "token", "present", "message",
|
|
})
|
|
doit("john", []string{"token", "present", "message"})
|
|
doit("james", []string{})
|
|
}
|
|
|
|
func TestUsernameTaken(t *testing.T) {
|
|
var g Hall
|
|
err := json.Unmarshal([]byte(desc2JSON), &g.description)
|
|
if err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
if g.UserExists("") {
|
|
t.Error("UserExists(\"\") is true, expected false")
|
|
}
|
|
if !g.UserExists("john") {
|
|
t.Error("UserExists(john) is false")
|
|
}
|
|
if !g.UserExists("john") {
|
|
t.Error("UserExists(james) is false")
|
|
}
|
|
if g.UserExists("paul") {
|
|
t.Error("UserExists(paul) is true")
|
|
}
|
|
}
|
|
|
|
func TestFmtpValue(t *testing.T) {
|
|
type fmtpTest struct {
|
|
fmtp string
|
|
key string
|
|
value string
|
|
}
|
|
fmtpTests := []fmtpTest{
|
|
{"", "foo", ""},
|
|
{"profile-id=0", "profile-id", "0"},
|
|
{"profile-id=0", "foo", ""},
|
|
{"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", "profile-level-id", "42001f"},
|
|
{"foo=1;bar=2;quux=3", "foo", "1"},
|
|
{"foo=1;bar=2;quux=3", "bar", "2"},
|
|
{"foo=1;bar=2;quux=3", "fu", ""},
|
|
}
|
|
|
|
for _, test := range fmtpTests {
|
|
v := fmtpValue(test.fmtp, test.key)
|
|
if v != test.value {
|
|
t.Errorf("fmtpValue(%v, %v) = %v, expected %v",
|
|
test.fmtp, test.key, v, test.value,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidHallName(t *testing.T) {
|
|
type nameTest struct {
|
|
name string
|
|
result bool
|
|
}
|
|
tests := []nameTest{
|
|
{"", false},
|
|
{"/", false},
|
|
{"/foo", false},
|
|
{"foo/", false},
|
|
{"./foo", false},
|
|
{"foo/.", false},
|
|
{"../foo", false},
|
|
{"foo/..", false},
|
|
{"foo/./bar", false},
|
|
{"foo/../bar", false},
|
|
{"foo\\bar", false},
|
|
{"foo", true},
|
|
{"foo/bar", true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
r := validHallName(test.name)
|
|
if r != test.result {
|
|
t.Errorf("Valid %v: got %v, expected %v",
|
|
test.name, r, test.result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPayloadTypeDistinct(t *testing.T) {
|
|
names := []string{
|
|
"opus", "g722", "pcmu", "pcma",
|
|
}
|
|
|
|
m := make(map[webrtc.PayloadType]string)
|
|
|
|
for _, n := range names {
|
|
codec, err := codecsFromName(n)
|
|
if err != nil {
|
|
t.Errorf("%v: %v", n, err)
|
|
continue
|
|
}
|
|
pt, err := CodecPayloadType(codec[0].RTPCodecCapability)
|
|
if err != nil {
|
|
t.Errorf("%v: %v", codec, err)
|
|
continue
|
|
}
|
|
if other, ok := m[pt]; ok {
|
|
t.Errorf(
|
|
"Duplicate ptype %v: %v and %v",
|
|
pt, n, other,
|
|
)
|
|
continue
|
|
}
|
|
m[pt] = n
|
|
}
|
|
}
|