mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
M1 — TOTP secrets are now AES-256-GCM encrypted before being stored in the database. Key is auto-generated on first run (data/totp.key) or set via OWNCORD_TOTP_KEY env var. Existing plaintext secrets are detected and returned as-is for backwards compatibility. M3 — Replay buffer events are now tagged with their channel ID. On reconnect, the server computes the user's current accessible channels and only replays events from those channels. Global broadcasts (presence, voice state, member updates) are always replayed. Falls back to full ready payload if permission computation fails.
463 lines
12 KiB
Go
463 lines
12 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/owncord/server/ws"
|
|
)
|
|
|
|
// ─── Push ────────────────────────────────────────────────────────────────────
|
|
|
|
func TestPush_SingleEntry(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
rb.Push(1, 0, []byte("hello"))
|
|
|
|
// afterSeq=0 is before the oldest seq (1), so EventsSince returns nil
|
|
// (the buffer can't confirm it covers everything the caller missed).
|
|
// Verify via OldestSeq and a valid afterSeq instead.
|
|
if got := rb.OldestSeq(); got != 1 {
|
|
t.Fatalf("expected oldest seq 1, got %d", got)
|
|
}
|
|
|
|
// afterSeq = oldestSeq means "give me everything after seq 1" = nothing newer.
|
|
// But we can use EventsSince with afterSeq matching oldest to get items > oldest.
|
|
// There's only seq 1, and 1 > 1 is false, so we get 0 events.
|
|
got := rb.EventsSince(1)
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected 0 events when afterSeq = only seq, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestPush_MultipleInOrder(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
for i := uint64(1); i <= 5; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("msg-%d", i)))
|
|
}
|
|
|
|
// afterSeq == oldestSeq (1) → nil (BUG-085: conservative boundary).
|
|
if got := rb.EventsSince(1); got != nil {
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq one past oldest → replay seq 3..5.
|
|
got := rb.EventsSince(2)
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected 3 events after seq 2, got %d", len(got))
|
|
}
|
|
for i, ev := range got {
|
|
want := fmt.Sprintf("msg-%d", i+3)
|
|
if string(ev) != want {
|
|
t.Errorf("event[%d]: expected %q, got %q", i, want, string(ev))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPush_WrapsAround(t *testing.T) {
|
|
const cap = 4
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
// Push 6 events into a buffer with capacity 4 — first two are evicted.
|
|
for i := uint64(1); i <= 6; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("e%d", i)))
|
|
}
|
|
|
|
got := rb.EventsSince(0)
|
|
// afterSeq=0 is older than oldest (seq 3), so EventsSince returns nil.
|
|
if got != nil {
|
|
t.Fatalf("expected nil (afterSeq too old), got %d events", len(got))
|
|
}
|
|
|
|
// Ask for events after seq 2 — still too old.
|
|
got = rb.EventsSince(2)
|
|
if got != nil {
|
|
t.Fatalf("expected nil (afterSeq 2 still evicted), got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq == oldestSeq (3) → nil (BUG-085).
|
|
got = rb.EventsSince(3)
|
|
if got != nil {
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq (3), got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq one past oldest → replay seq 5, 6.
|
|
got = rb.EventsSince(4)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 events after seq 4, got %d", len(got))
|
|
}
|
|
for i, want := range []string{"e5", "e6"} {
|
|
if string(got[i]) != want {
|
|
t.Errorf("event[%d]: expected %q, got %q", i, want, string(got[i]))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPush_OverwritesOldest(t *testing.T) {
|
|
const cap = 3
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
rb.Push(1, 0, []byte("a"))
|
|
rb.Push(2, 0, []byte("b"))
|
|
rb.Push(3, 0, []byte("c"))
|
|
|
|
if oldest := rb.OldestSeq(); oldest != 1 {
|
|
t.Fatalf("expected oldest seq 1, got %d", oldest)
|
|
}
|
|
|
|
// Overwrite seq 1.
|
|
rb.Push(4, 0, []byte("d"))
|
|
if oldest := rb.OldestSeq(); oldest != 2 {
|
|
t.Fatalf("expected oldest seq 2 after overwrite, got %d", oldest)
|
|
}
|
|
|
|
// afterSeq == oldestSeq (2) → nil (BUG-085).
|
|
if got := rb.EventsSince(2); got != nil {
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq one past oldest → replay seq 4 only.
|
|
got := rb.EventsSince(3)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(got))
|
|
}
|
|
if string(got[0]) != "d" {
|
|
t.Errorf("expected [d], got [%s]", got[0])
|
|
}
|
|
}
|
|
|
|
// ─── EventsSince ─────────────────────────────────────────────────────────────
|
|
|
|
func TestEventsSince_EmptyBuffer(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
got := rb.EventsSince(0)
|
|
if got != nil {
|
|
t.Fatalf("expected nil for empty buffer, got %d events", len(got))
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_AfterSpecificSeq(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
for i := uint64(1); i <= 5; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("m%d", i)))
|
|
}
|
|
|
|
got := rb.EventsSince(3)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 events after seq 3, got %d", len(got))
|
|
}
|
|
if string(got[0]) != "m4" || string(got[1]) != "m5" {
|
|
t.Errorf("expected [m4, m5], got [%s, %s]", got[0], got[1])
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_TooOld(t *testing.T) {
|
|
const cap = 4
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
for i := uint64(1); i <= 6; i++ {
|
|
rb.Push(i, 0, []byte("x"))
|
|
}
|
|
|
|
// Oldest is seq 3. Requesting seq 1 should return nil.
|
|
got := rb.EventsSince(1)
|
|
if got != nil {
|
|
t.Fatalf("expected nil for evicted seq, got %d events", len(got))
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_AtLatestSeq(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
for i := uint64(1); i <= 5; i++ {
|
|
rb.Push(i, 0, []byte("x"))
|
|
}
|
|
|
|
got := rb.EventsSince(5)
|
|
// afterSeq equals latest — nothing newer exists.
|
|
if len(got) != 0 {
|
|
t.Fatalf("expected 0 events when afterSeq = latest, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_WraparoundOrder(t *testing.T) {
|
|
const cap = 4
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
// Fill past capacity to force wrap.
|
|
for i := uint64(1); i <= 7; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("v%d", i)))
|
|
}
|
|
|
|
// afterSeq == oldestSeq (4) → nil (BUG-085).
|
|
if got := rb.EventsSince(4); got != nil {
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq one past oldest → replay seq 6, 7.
|
|
got := rb.EventsSince(5)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 events, got %d", len(got))
|
|
}
|
|
for i, want := range []string{"v6", "v7"} {
|
|
if string(got[i]) != want {
|
|
t.Errorf("event[%d]: expected %q, got %q", i, want, string(got[i]))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_AfterSeqZero_ReturnsBehavior(t *testing.T) {
|
|
// afterSeq=0 is below the oldest seq in the buffer (seq starts at 1),
|
|
// so EventsSince treats it as "too old" and returns nil. This is correct:
|
|
// the server can't confirm the buffer covers everything the client missed.
|
|
rb := ws.NewEventRingBuffer(8)
|
|
for i := uint64(1); i <= 3; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("a%d", i)))
|
|
}
|
|
|
|
got := rb.EventsSince(0)
|
|
if got != nil {
|
|
t.Fatalf("expected nil for afterSeq=0 (before oldest), got %d events", len(got))
|
|
}
|
|
|
|
// If we start seqs from 0, afterSeq=0 equals oldest → nil (BUG-085).
|
|
rb2 := ws.NewEventRingBuffer(8)
|
|
rb2.Push(0, 0, []byte("z0"))
|
|
rb2.Push(1, 0, []byte("z1"))
|
|
rb2.Push(2, 0, []byte("z2"))
|
|
|
|
got = rb2.EventsSince(0)
|
|
if got != nil {
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq (0), got %d events", len(got))
|
|
}
|
|
|
|
// afterSeq one past oldest → replay seq 2 only.
|
|
got = rb2.EventsSince(1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1 event after seq 1, got %d", len(got))
|
|
}
|
|
if string(got[0]) != "z2" {
|
|
t.Errorf("expected [z2], got [%s]", got[0])
|
|
}
|
|
}
|
|
|
|
func TestEventsSince_AfterSeqEqualsOldest_ReturnsNil(t *testing.T) {
|
|
// BUG-085: When afterSeq == oldestSeq, the client's last event is the
|
|
// oldest in the buffer. We can't guarantee nothing was missed between
|
|
// the evicted event before oldest and oldest itself, so EventsSince
|
|
// must return nil to trigger a full ready payload.
|
|
const cap = 4
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
// Push 6 events: buffer holds seq 3,4,5,6. Oldest = 3.
|
|
for i := uint64(1); i <= 6; i++ {
|
|
rb.Push(i, 0, []byte(fmt.Sprintf("e%d", i)))
|
|
}
|
|
|
|
if oldest := rb.OldestSeq(); oldest != 3 {
|
|
t.Fatalf("expected oldest seq 3, got %d", oldest)
|
|
}
|
|
|
|
// afterSeq == oldestSeq (3): must return nil, not empty slice.
|
|
got := rb.EventsSince(3)
|
|
if got == nil {
|
|
// This is the CORRECT behavior after the fix.
|
|
return
|
|
}
|
|
// Before the fix, this returns a non-nil slice [e4, e5, e6].
|
|
// That's wrong because the client at seq 3 might have missed events
|
|
// between the evicted seq 2 and seq 3.
|
|
t.Fatalf("expected nil when afterSeq == oldestSeq, got %d events", len(got))
|
|
}
|
|
|
|
// ─── OldestSeq ───────────────────────────────────────────────────────────────
|
|
|
|
func TestOldestSeq_Empty(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
if got := rb.OldestSeq(); got != 0 {
|
|
t.Fatalf("expected 0 for empty buffer, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestOldestSeq_AfterInitialPushes(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(8)
|
|
rb.Push(10, 0, []byte("x"))
|
|
rb.Push(11, 0, []byte("y"))
|
|
|
|
if got := rb.OldestSeq(); got != 10 {
|
|
t.Fatalf("expected oldest seq 10, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestOldestSeq_AfterWraparound(t *testing.T) {
|
|
const cap = 3
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
rb.Push(10, 0, []byte("a"))
|
|
rb.Push(20, 0, []byte("b"))
|
|
rb.Push(30, 0, []byte("c"))
|
|
rb.Push(40, 0, []byte("d")) // evicts seq 10
|
|
|
|
if got := rb.OldestSeq(); got != 20 {
|
|
t.Fatalf("expected oldest seq 20 after wraparound, got %d", got)
|
|
}
|
|
}
|
|
|
|
// ─── Concurrency ─────────────────────────────────────────────────────────────
|
|
|
|
func TestConcurrent_PushAndEventsSince(t *testing.T) {
|
|
const (
|
|
cap = 64
|
|
writers = 4
|
|
pushes = 500
|
|
readers = 4
|
|
reads = 500
|
|
)
|
|
rb := ws.NewEventRingBuffer(cap)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent writers.
|
|
for w := 0; w < writers; w++ {
|
|
wg.Add(1)
|
|
go func(base uint64) {
|
|
defer wg.Done()
|
|
for i := uint64(0); i < pushes; i++ {
|
|
rb.Push(base+i, 0, []byte("data"))
|
|
}
|
|
}(uint64(w) * pushes)
|
|
}
|
|
|
|
// Concurrent readers.
|
|
for r := 0; r < readers; r++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < reads; i++ {
|
|
_ = rb.EventsSince(0)
|
|
_ = rb.OldestSeq()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// If we get here without a race detector complaint, the mutex is working.
|
|
// Sanity: buffer should have events.
|
|
if rb.OldestSeq() == 0 {
|
|
t.Fatal("expected non-zero oldest seq after concurrent pushes")
|
|
}
|
|
}
|
|
|
|
// ─── Table-driven: capacity boundary ─────────────────────────────────────────
|
|
|
|
func TestEventsSince_CapacityBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cap int
|
|
pushes int
|
|
afterSeq uint64
|
|
wantLen int // -1 means nil
|
|
wantFirst string
|
|
}{
|
|
{
|
|
name: "exactly at capacity, afterSeq=0 too old",
|
|
cap: 4,
|
|
pushes: 4,
|
|
afterSeq: 0,
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "exactly at capacity, afterSeq == oldest → nil",
|
|
cap: 4,
|
|
pushes: 4,
|
|
afterSeq: 1, // oldest=1, BUG-085: == returns nil
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "exactly at capacity, afterSeq one past oldest",
|
|
cap: 4,
|
|
pushes: 4,
|
|
afterSeq: 2,
|
|
wantLen: 2,
|
|
wantFirst: "e3",
|
|
},
|
|
{
|
|
name: "one past capacity",
|
|
cap: 4,
|
|
pushes: 5,
|
|
afterSeq: 1, // evicted
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "one past capacity, afterSeq == oldest → nil",
|
|
cap: 4,
|
|
pushes: 5,
|
|
afterSeq: 2, // oldest=2, BUG-085: == returns nil
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "one past capacity, afterSeq one past oldest",
|
|
cap: 4,
|
|
pushes: 5,
|
|
afterSeq: 3,
|
|
wantLen: 2,
|
|
wantFirst: "e4",
|
|
},
|
|
{
|
|
name: "double capacity, afterSeq == oldest → nil",
|
|
cap: 4,
|
|
pushes: 8,
|
|
afterSeq: 5, // oldest=5, BUG-085: == returns nil
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "double capacity, afterSeq one past oldest",
|
|
cap: 4,
|
|
pushes: 8,
|
|
afterSeq: 6,
|
|
wantLen: 2,
|
|
wantFirst: "e7",
|
|
},
|
|
{
|
|
name: "capacity 1, afterSeq == oldest → nil",
|
|
cap: 1,
|
|
pushes: 3,
|
|
afterSeq: 3, // oldest=3, BUG-085: == returns nil
|
|
wantLen: -1,
|
|
},
|
|
{
|
|
name: "capacity 1, afterSeq too old",
|
|
cap: 1,
|
|
pushes: 3,
|
|
afterSeq: 2,
|
|
wantLen: -1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rb := ws.NewEventRingBuffer(tc.cap)
|
|
for i := 1; i <= tc.pushes; i++ {
|
|
rb.Push(uint64(i), 0, []byte(fmt.Sprintf("e%d", i)))
|
|
}
|
|
|
|
got := rb.EventsSince(tc.afterSeq)
|
|
|
|
if tc.wantLen == -1 {
|
|
if got != nil {
|
|
t.Fatalf("expected nil, got %d events", len(got))
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(got) != tc.wantLen {
|
|
t.Fatalf("expected %d events, got %d", tc.wantLen, len(got))
|
|
}
|
|
|
|
if tc.wantLen > 0 && string(got[0]) != tc.wantFirst {
|
|
t.Errorf("first event: expected %q, got %q", tc.wantFirst, string(got[0]))
|
|
}
|
|
})
|
|
}
|
|
}
|