Files
unibus/pkg/membership/store_test.go
T

143 lines
4.0 KiB
Go

package membership
import (
"bytes"
"path/filepath"
"testing"
)
func openTestStore(t *testing.T) *Store {
t.Helper()
path := filepath.Join(t.TempDir(), "test.db")
s, err := Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { s.Close() })
return s
}
func TestMigrationsCreateSchema(t *testing.T) {
s := openTestStore(t)
// Verify the three tables exist by querying sqlite_master.
for _, tbl := range []string{"rooms", "members", "room_keys"} {
var name string
err := s.db.QueryRow(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, tbl,
).Scan(&name)
if err != nil {
t.Fatalf("table %q not created: %v", tbl, err)
}
}
// Re-applying migrations must be idempotent (no error on a populated db).
if err := s.applyMigrations(); err != nil {
t.Fatalf("re-apply migrations: %v", err)
}
}
func TestRoomMemberKeyRoundTrip(t *testing.T) {
s := openTestStore(t)
owner := "owner-ep"
roomID := "room-1"
info := RoomInfo{
RoomID: roomID,
Subject: "room.test",
Encrypt: true,
Persist: true,
SignMsgs: true,
OwnerEndpoint: owner,
}
ownerSealed := []byte("owner-sealed-key-epoch1")
if err := s.CreateRoom(info, []byte("owner-sign"), []byte("owner-kex"), ownerSealed); err != nil {
t.Fatalf("CreateRoom: %v", err)
}
// GetRoom returns epoch 1 and the policy.
got, err := s.GetRoom(roomID)
if err != nil {
t.Fatalf("GetRoom: %v", err)
}
if got.Epoch != 1 || !got.Encrypt || !got.Persist || !got.SignMsgs || got.OwnerEndpoint != owner {
t.Fatalf("GetRoom mismatch: %+v", got)
}
// Owner sealed key at epoch 1 (latest).
ep, sealed, err := s.GetSealedKey(roomID, owner, 0)
if err != nil {
t.Fatalf("GetSealedKey owner: %v", err)
}
if ep != 1 || !bytes.Equal(sealed, ownerSealed) {
t.Fatalf("owner sealed key mismatch: epoch=%d sealed=%q", ep, sealed)
}
// Add member at epoch 1.
member := Member{Endpoint: "member-ep", Role: "member", SignPub: []byte("m-sign"), KexPub: []byte("m-kex")}
memberSealed := []byte("member-sealed-epoch1")
if err := s.AddMember(roomID, member, 1, memberSealed); err != nil {
t.Fatalf("AddMember: %v", err)
}
gotMember, err := s.GetMember(roomID, "member-ep")
if err != nil {
t.Fatalf("GetMember: %v", err)
}
if gotMember.Role != "member" || !bytes.Equal(gotMember.SignPub, []byte("m-sign")) {
t.Fatalf("GetMember mismatch: %+v", gotMember)
}
members, err := s.ListMembers(roomID)
if err != nil {
t.Fatalf("ListMembers: %v", err)
}
if len(members) != 2 {
t.Fatalf("expected 2 members, got %d", len(members))
}
// Bump to epoch 2 and store new keys only for the owner (simulating a kick of member-ep).
if err := s.BumpEpoch(roomID, 2); err != nil {
t.Fatalf("BumpEpoch: %v", err)
}
newKeys := map[string][]byte{owner: []byte("owner-sealed-epoch2")}
if err := s.PutSealedKeys(roomID, 2, newKeys); err != nil {
t.Fatalf("PutSealedKeys: %v", err)
}
if err := s.RemoveMember(roomID, "member-ep"); err != nil {
t.Fatalf("RemoveMember: %v", err)
}
got, err = s.GetRoom(roomID)
if err != nil {
t.Fatalf("GetRoom after bump: %v", err)
}
if got.Epoch != 2 {
t.Fatalf("expected epoch 2, got %d", got.Epoch)
}
// Owner now has a fresh sealed key at epoch 2 (latest).
ep, sealed, err = s.GetSealedKey(roomID, owner, 0)
if err != nil {
t.Fatalf("GetSealedKey owner epoch2: %v", err)
}
if ep != 2 || !bytes.Equal(sealed, []byte("owner-sealed-epoch2")) {
t.Fatalf("owner epoch2 key mismatch: epoch=%d sealed=%q", ep, sealed)
}
// The removed member is gone.
if _, err := s.GetMember(roomID, "member-ep"); err == nil {
t.Fatalf("expected error getting removed member")
}
// The kicked member has no key at epoch 2 (was excluded from the rekey).
if _, _, err := s.GetSealedKey(roomID, "member-ep", 2); err == nil {
t.Fatalf("kicked member should have no key at epoch 2")
}
members, err = s.ListMembers(roomID)
if err != nil {
t.Fatalf("ListMembers after remove: %v", err)
}
if len(members) != 1 || members[0].Endpoint != owner {
t.Fatalf("expected only owner remaining, got %+v", members)
}
}