2026-08-07 21:20:48 +02:00
|
|
|
package admin
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-08-28 06:54:32 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/auth"
|
2026-08-07 21:20:48 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// handleLogStream's backfill loop does a DB round-trip per entry between
|
|
|
|
|
// taking the snapshot and registering the live subscription — Snapshot()
|
|
|
|
|
// followed by a separate Subscribe() call left a window where a write in
|
|
|
|
|
// between landed in neither (v059). SnapshotAndSubscribe closes that window
|
|
|
|
|
// by doing both under one lock acquisition.
|
|
|
|
|
func TestRingBuffer_SnapshotAndSubscribe_NoGap(t *testing.T) {
|
|
|
|
|
buf := NewRingBuffer(10)
|
|
|
|
|
buf.Write(LogEntry{Message: "before"})
|
|
|
|
|
|
|
|
|
|
snap, ch, unsub := buf.SnapshotAndSubscribe()
|
|
|
|
|
defer unsub()
|
|
|
|
|
|
|
|
|
|
if len(snap) != 1 || snap[0].Message != "before" {
|
|
|
|
|
t.Fatalf("snapshot = %+v, want [{Message: before}]", snap)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A write that lands after the atomic call returns must reach the
|
|
|
|
|
// channel — it is neither in the snapshot nor lost.
|
|
|
|
|
buf.Write(LogEntry{Message: "after"})
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case e := <-ch:
|
|
|
|
|
if e.Message != "after" {
|
|
|
|
|
t.Fatalf("got %+v, want Message=after", e)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
t.Fatal("expected the post-subscribe write to be delivered on the channel, got nothing")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The snapshot and the subscription returned by SnapshotAndSubscribe must
|
|
|
|
|
// still behave like independently-called Snapshot/Subscribe: entries already
|
|
|
|
|
// in the ring appear in the snapshot, not replayed on the channel.
|
|
|
|
|
func TestRingBuffer_SnapshotAndSubscribe_SnapshotExcludedFromChannel(t *testing.T) {
|
|
|
|
|
buf := NewRingBuffer(10)
|
|
|
|
|
buf.Write(LogEntry{Message: "already-in-ring"})
|
|
|
|
|
|
|
|
|
|
snap, ch, unsub := buf.SnapshotAndSubscribe()
|
|
|
|
|
defer unsub()
|
|
|
|
|
|
|
|
|
|
if len(snap) != 1 {
|
|
|
|
|
t.Fatalf("snapshot = %+v, want 1 entry", snap)
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case e := <-ch:
|
|
|
|
|
t.Fatalf("channel should not replay pre-existing entries, got %+v", e)
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// gapProbeSSEWriter drives the handler from inside its own writes: the first
|
|
|
|
|
// backfilled entry triggers onFirstBackfill (which writes a fresh log line,
|
|
|
|
|
// i.e. exactly the interleaving the gap loses), and once wantData entries have
|
|
|
|
|
// been written the request context is cancelled so the handler returns.
|
|
|
|
|
type gapProbeSSEWriter struct {
|
|
|
|
|
header http.Header
|
|
|
|
|
statusCode int
|
|
|
|
|
dataWrites int
|
|
|
|
|
wantData int
|
|
|
|
|
buffer bytes.Buffer
|
|
|
|
|
onFirstBackfill func()
|
|
|
|
|
cancel func()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *gapProbeSSEWriter) Header() http.Header {
|
|
|
|
|
if w.header == nil {
|
|
|
|
|
w.header = make(http.Header)
|
|
|
|
|
}
|
|
|
|
|
return w.header
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *gapProbeSSEWriter) WriteHeader(statusCode int) { w.statusCode = statusCode }
|
|
|
|
|
func (w *gapProbeSSEWriter) Flush() {}
|
|
|
|
|
|
|
|
|
|
func (w *gapProbeSSEWriter) Write(data []byte) (int, error) {
|
|
|
|
|
_, _ = w.buffer.Write(data)
|
|
|
|
|
if bytes.Contains(data, []byte("data: ")) {
|
|
|
|
|
w.dataWrites++
|
|
|
|
|
if w.dataWrites == 1 && w.onFirstBackfill != nil {
|
|
|
|
|
w.onFirstBackfill()
|
|
|
|
|
}
|
|
|
|
|
if w.dataWrites >= w.wantData && w.cancel != nil {
|
|
|
|
|
w.cancel()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return len(data), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The end-to-end shape of v059: a log line written *while the backfill loop is
|
|
|
|
|
// running* must still reach the stream. Under the old Snapshot()-then-
|
|
|
|
|
// Subscribe() ordering it was in neither — the snapshot predated it and the
|
|
|
|
|
// subscription did not exist yet — and this test hits that window
|
|
|
|
|
// deterministically by doing the write from inside the first backfill entry's
|
|
|
|
|
// Write call.
|
|
|
|
|
func TestHandleLogStream_EntryWrittenDuringBackfillIsDelivered(t *testing.T) {
|
|
|
|
|
database := newLogStreamTestDB(t)
|
|
|
|
|
logBuf := NewRingBuffer(8)
|
|
|
|
|
logBuf.Write(LogEntry{Timestamp: "2026-08-07T10:00:00Z", Level: "info", Message: "backfill-one", Source: "test"})
|
|
|
|
|
logBuf.Write(LogEntry{Timestamp: "2026-08-07T10:00:01Z", Level: "info", Message: "backfill-two", Source: "test"})
|
|
|
|
|
|
|
|
|
|
userID, err := database.CreateUser(context.Background(), "owner", "hash", 1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateUser: %v", err)
|
|
|
|
|
}
|
|
|
|
|
token, err := auth.GenerateToken()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GenerateToken: %v", err)
|
|
|
|
|
}
|
|
|
|
|
tokenHash := auth.HashToken(token)
|
|
|
|
|
if _, err := database.CreateSession(context.Background(), userID, tokenHash, "test", "127.0.0.1"); err != nil {
|
|
|
|
|
t.Fatalf("CreateSession: %v", err)
|
|
|
|
|
}
|
|
|
|
|
ticket, err := logTickets.issue(tokenHash)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("issue ticket: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The timeout is the failure path only: with the gap open the third entry
|
|
|
|
|
// never arrives, so nothing would ever cancel the stream.
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/logs/stream?ticket="+ticket, nil).WithContext(ctx)
|
|
|
|
|
writer := &gapProbeSSEWriter{
|
|
|
|
|
header: make(http.Header),
|
|
|
|
|
wantData: 3,
|
|
|
|
|
cancel: cancel,
|
|
|
|
|
onFirstBackfill: func() {
|
|
|
|
|
logBuf.Write(LogEntry{Timestamp: "2026-08-07T10:00:02Z", Level: "warn", Message: "written-during-backfill", Source: "test"})
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleLogStream(database, logBuf).ServeHTTP(writer, req)
|
|
|
|
|
|
|
|
|
|
if writer.statusCode != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body = %s", writer.statusCode, writer.buffer.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(writer.buffer.String(), "written-during-backfill") {
|
|
|
|
|
t.Fatalf("entry written during the backfill was lost from both the backfill and the live feed; body = %s", writer.buffer.String())
|
|
|
|
|
}
|
|
|
|
|
}
|