Files
OwnCord/Server/admin/logstream_test.go
T
jevb 90b4f268e2 feat: TOTP 2FA settings UI, server hardening, full validation pass
Client:
- Add TOTP enrollment/disable UI in Settings > Account (AccountTab.ts)
- Fix api.ts enableTotp/confirmTotp/disableTotp to require password param
- Add totp_enabled field to UserWithRole type
- Wire SettingsOverlay TOTP callbacks through MainPage and ConnectPage
- 27 new tests: totp-settings (18), api TOTP methods (6), auth store (3)

Server:
- Fix targetBoolSetting to default false on ErrNotFound (fresh DB compat)
- Fix admin settings test: boolean keys use valid values, not "testvalue"
- Add require_2fa validation to settings handler (normalizeSettingUpdates)
- Remove unused authenticateAdmin from logstream.go

Docs:
- Mark DOCUMENTATION_AUDIT Critical Finding #1 as RESOLVED
- Update CLAUDE.md Key Features with 2FA/TOTP bullet
- Update CLIENT-ARCHITECTURE.md with TOTP components
- Update CHATSERVER.md login flow and rate limiting table
- Create session log, update task tracking (T-192–T-201)
2026-03-29 21:31:18 +02:00

116 lines
2.7 KiB
Go

package admin
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
)
type revokingSSEWriter struct {
header http.Header
statusCode int
writeCount int
revoke func()
cancel func()
buffer bytes.Buffer
}
func (w *revokingSSEWriter) Header() http.Header {
if w.header == nil {
w.header = make(http.Header)
}
return w.header
}
func (w *revokingSSEWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}
func (w *revokingSSEWriter) Write(data []byte) (int, error) {
_, _ = w.buffer.Write(data)
if bytes.Contains(data, []byte("data: ")) {
w.writeCount++
switch w.writeCount {
case 1:
if w.revoke != nil {
w.revoke()
}
case 2:
if w.cancel != nil {
w.cancel()
}
}
}
return len(data), nil
}
func (w *revokingSSEWriter) Flush() {}
func newLogStreamTestDB(t *testing.T) *db.DB {
t.Helper()
database, err := db.Open(":memory:")
if err != nil {
t.Fatalf("db.Open: %v", err)
}
t.Cleanup(func() { _ = database.Close() })
if err := db.Migrate(database); err != nil {
t.Fatalf("db.Migrate: %v", err)
}
return database
}
func TestHandleLogStream_BackfillStopsAfterSessionRevocation(t *testing.T) {
database := newLogStreamTestDB(t)
logBuf := NewRingBuffer(8)
logBuf.Write(LogEntry{Timestamp: "2026-03-29T10:00:00Z", Level: "info", Message: "first", Source: "test"})
logBuf.Write(LogEntry{Timestamp: "2026-03-29T10:00:01Z", Level: "info", Message: "second", Source: "test"})
userID, err := database.CreateUser("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(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)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
req := httptest.NewRequest(http.MethodGet, "/logs/stream?ticket="+ticket, nil).WithContext(ctx)
writer := &revokingSSEWriter{
header: make(http.Header),
revoke: func() {
_ = database.DeleteSession(tokenHash)
},
cancel: cancel,
}
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 writer.writeCount != 1 {
t.Fatalf("expected backfill to stop after first entry once session was revoked, wrote %d entries; body = %s", writer.writeCount, writer.buffer.String())
}
}