Files
OwnCord/Server/admin/export_test.go
T
J3vbandClaude Fable 5 d880b64d64 test: audit 2026-08-19 — fix stale tests, close coverage gaps (#1397)
* test(server): admin/handlers/channels — test-audit 2026-08-19 fixes

* test(server): api/constants — test-audit 2026-08-19 fixes

* test(server): api/middleware — test-audit 2026-08-19 fixes

* test(server): api/waf — test-audit 2026-08-19 fixes

* test(server): auth/totp/encrypt — test-audit 2026-08-19 fixes

* test(server): db/session/expiry/test — test-audit 2026-08-19 fixes

* test(server): migrations/030/attachments/unlink/on/message/delete — test-audit 2026-08-19 fixes

* test(server): updater/download — test-audit 2026-08-19 fixes

* test(server): ws/handlers_command — test-audit 2026-08-19 fixes

* test(server): ws/hub/broadcast — test-audit 2026-08-19 fixes

* test(server): ws/hub/events — test-audit 2026-08-19 fixes

* test(server): ws/livekit/webhook — test-audit 2026-08-19 fixes

* test(server): ws/voice/controls — test-audit 2026-08-19 fixes

* test(server): ws/voice/join — test-audit 2026-08-19 fixes

* test(server): ws/voice/moderation — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/commands.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/secret_store.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/update_commands.rs — test-audit 2026-08-19 fixes

* test(client): src/components/ChannelSidebar.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/ws.ts — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/credentials.rs — test-audit 2026-08-19 fixes

* test(rust): src-tauri/src/tofu.rs — test-audit 2026-08-19 fixes

* test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 fixes

* test(client): src/pages/connect-page/LoginForm.ts — test-audit 2026-08-19 fixes

* test(client): src/pages/main-page/SidebarArea.ts — test-audit 2026-08-19 fixes

* test(client): src/stores/voice.store.ts — test-audit 2026-08-19 fixes

* test(client): tests/browser/smoke.test.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/media.test.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/renderers.test.ts — test-audit 2026-08-19 fixes

* test(client): src/components/UserProfilePopup.ts — test-audit 2026-08-19 fixes

* test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 fixes

* test(client): tests/unit/log-persistence.test.ts — test-audit 2026-08-19 fixes

* test(client): keep tests/browser out of the jsdom suite and run it in CI

* test(server): ws/hub_broadcast_test.go — bytes.Equal payload compare (gocritic)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(client): src/lib/credentials.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/dispatcher.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/permissions.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/messages.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/ws.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/identity.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/lib/livekitE2EE.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/auth.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* test(client): src/stores/voice.store.ts — test-audit 2026-08-19 round 2 (Stryker)

* docs: test audit 2026-08-19 — findings, fixes, measured baselines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(graph): refresh the knowledge graph after the 2026-08-19 test audit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 19:29:31 +02:00

197 lines
7.0 KiB
Go

package admin
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"time"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
)
// BroadcastRolesForTest exposes broadcastRoles for external tests. It builds
// a bare *http.Request carrying ctx, since broadcastRoles's only use of its
// *http.Request argument is r.Context().
func BroadcastRolesForTest(ctx context.Context, database *db.DB, hub HubBroadcaster) {
r := httptest.NewRequest(http.MethodPost, "/roles", nil).WithContext(ctx)
broadcastRoles(r, database, hub)
}
// CaptureSetupLimiter installs h so the next NewAdminAPI call reports the
// *auth.RateLimiter it creates for the /setup endpoint. NewAdminAPI returns
// only an http.Handler, so this is the only way tests can reach that limiter
// to check whether its stale entries get reaped.
func CaptureSetupLimiter(h func(*auth.RateLimiter)) (restore func()) {
prev := setupLimiterHook
setupLimiterHook = h
return func() { setupLimiterHook = prev }
}
// SetSetupLimiterReapTiming overrides the interval and max-window the setup
// endpoint's rate-limiter reaper uses, so tests don't wait on the real
// 5-minute interval.
func SetSetupLimiterReapTiming(interval, maxWindow time.Duration) (restore func()) {
prevI, prevW := setupLimiterReapInterval, setupLimiterReapMaxWindow
setupLimiterReapInterval = interval
setupLimiterReapMaxWindow = maxWindow
return func() {
setupLimiterReapInterval = prevI
setupLimiterReapMaxWindow = prevW
}
}
// SetBackupBaseDir overrides backupBaseDir so tests can point backup handlers
// at a temp dir. Lives here so it stays out of the production binary.
func SetBackupBaseDir(dir string) { backupBaseDir = dir }
// SetPatchChannelPostCommitHook installs h to run synchronously right after
// handlePatchChannel's AdminUpdateChannel commit, before the post-commit
// re-read and hub fan-out — the only way to deterministically land a caller
// cancellation in that exact window (OC-0158) instead of racing wall-clock
// timing.
func SetPatchChannelPostCommitHook(h func()) (restore func()) {
prev := patchChannelPostCommitHook
patchChannelPostCommitHook = h
return func() { patchChannelPostCommitHook = prev }
}
// SetCreateChannelPostCommitHook installs h to run synchronously right after
// handleCreateChannel's AdminCreateChannel commit, before the post-commit
// re-read and hub fan-out — the create-side twin of
// SetPatchChannelPostCommitHook (OC-0158).
func SetCreateChannelPostCommitHook(h func()) (restore func()) {
prev := createChannelPostCommitHook
createChannelPostCommitHook = h
return func() { createChannelPostCommitHook = prev }
}
// StubCopyBackup swaps the restore path's file-copy hook so tests can inject
// mid-copy failures that pass the pre-copy integrity gate. CopyBackupForTest
// is the real implementation, for stubs that only want to fail once.
func StubCopyBackup(fn func(src, dst string) error) (restore func()) {
prev := copyBackupFile
copyBackupFile = fn
return func() { copyBackupFile = prev }
}
// CopyBackupForTest exposes the real copyFile for StubCopyBackup delegates.
var CopyBackupForTest = copyFile
// StubCloseError makes the next handleRestoreBackup call's database.Close()
// return err instead of actually closing the pools, so tests can exercise the
// Close-failure branch without a genuine driver-level close error (see
// dbCloser's doc comment for why that's not otherwise reachable in a test).
func StubCloseError(msg string) (restore func()) {
closeMu.Lock()
prev := dbCloser
dbCloser = func(*db.DB) error { return errors.New(msg) }
closeMu.Unlock()
return func() {
closeMu.Lock()
dbCloser = prev
closeMu.Unlock()
}
}
// ApplyStagedUpdate exposes applyStagedUpdate (the on-disk swap behind
// POST /updates/apply's background goroutine) so tests can drive it directly
// with fake filesystem paths, instead of exercising the full HTTP handler —
// which resolves exePath via os.Executable() and would rename/replace the
// running test binary itself. The swap has no process side effects anymore
// (no spawn, no signal, no exit), so the success path is testable too.
var ApplyStagedUpdate = applyStagedUpdate
// ApplyAndRestart exposes the whole background tail (countdown broadcast →
// swap → restart request / guard release) for tests, combined with
// StubRestart and SetApplyRestartDelay.
var ApplyAndRestart = applyAndRestart
// SetApplyRestartDelay shrinks the client-facing restart countdown so tests
// of applyAndRestart don't sleep the real 5 seconds.
func SetApplyRestartDelay(d time.Duration) (restore func()) {
prev := applyRestartDelay
applyRestartDelay = d
return func() { applyRestartDelay = prev }
}
// ResetRestartState returns the restart-serialization guard to idle. Tests
// that drive a restart-committing path (restore, apply success, setup
// restart) must call it afterwards — the state is process-global and would
// otherwise 409 every later test's request.
func ResetRestartState() { restartState.Store(restartStateIdle) }
// ForceRestartState pins the guard for conflict-path tests: busy=false sets
// restart-pending, busy=true sets an in-flight exclusive operation.
func ForceRestartState(busy bool) {
if busy {
restartState.Store(restartStateBusy)
} else {
restartState.Store(restartStatePending)
}
}
// CurrentRestartState reports the guard state by name for assertions.
func CurrentRestartState() string {
switch restartState.Load() {
case restartStateBusy:
return "busy"
case restartStatePending:
return "pending"
default:
return "idle"
}
}
// RequestRestartForTest exposes requestRestart so the unwired default hook's
// inertness (log, no exit, no spawn) is directly testable.
var RequestRestartForTest = requestRestart
// StubRestart replaces the process-restart hook for the duration of a test and
// returns a func reporting whether a restart was requested. Without this the
// restore handler's request would hit the unwired-hook error log; with it the
// test can assert the request happened. The restore func also returns the
// restart-serialization guard to idle, since any test that triggered the hook
// has necessarily left it in restart-pending.
func StubRestart() (restarted func() bool, restore func()) {
restartMu.Lock()
prev := restartSelf
called := &atomic.Bool{}
restartSelf = func(string) { called.Store(true) }
restartMu.Unlock()
return called.Load, func() {
restartMu.Lock()
restartSelf = prev
restartMu.Unlock()
ResetRestartState()
}
}
// StubRestartCapture is StubRestart recording the reasons passed to the hook,
// for tests that assert which path requested the restart.
func StubRestartCapture() (reasons func() []string, restore func()) {
restartMu.Lock()
prev := restartSelf
var mu sync.Mutex
var got []string
restartSelf = func(reason string) {
mu.Lock()
got = append(got, reason)
mu.Unlock()
}
restartMu.Unlock()
return func() []string {
mu.Lock()
defer mu.Unlock()
return append([]string(nil), got...)
}, func() {
restartMu.Lock()
restartSelf = prev
restartMu.Unlock()
ResetRestartState()
}
}