Files
OwnCord/Server/api/constants_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

108 lines
3.9 KiB
Go

package api
import (
"os"
"strings"
"testing"
)
// I-7: loginRateLimitPerMinute must be 5 (not 60).
func TestLoginRateLimit_Value(t *testing.T) {
if loginRateLimitPerMinute != 5 {
t.Errorf("loginRateLimitPerMinute = %d, want 5", loginRateLimitPerMinute)
}
}
// The rate-limiter reaper deletes any window entry whose timestamps are all
// older than rateLimiterCleanupMaxWindow, which is only safe for windows no
// longer than that horizon (auth/ratelimit.go). Slow mode uses the limiter
// with windows up to admin's maxSlowModeSeconds (21600 s = 6 h), so a shorter
// horizon silently resets long slow modes after ~15 minutes.
func TestRateLimiterCleanupHorizon_CoversMaxSlowMode(t *testing.T) {
const maxSlowMode = 21600 // admin/handlers_channels.go maxSlowModeSeconds
if rateLimiterCleanupMaxWindow.Seconds() < maxSlowMode {
t.Errorf("rateLimiterCleanupMaxWindow = %v, must cover the %ds slow-mode cap",
rateLimiterCleanupMaxWindow, maxSlowMode)
}
}
// setAuthRateScale/scaledAuthLimit gate every per-IP auth limit
// (auth_handler.go:107-136) and the per-IP login failure threshold that arms
// the lockout (auth_handler.go:514,537). The multiplier is operator-supplied
// via security.auth_rate_limit_multiplier and config validates nothing, so
// this clamp is all that stands between a typo and brute-force protection
// disappearing.
func TestSetAuthRateScale_ClampsMultiplier(t *testing.T) {
t.Cleanup(func() { setAuthRateScale(1.0) })
tests := []struct {
name string
mult float64
limit int
want int
}{
{"unset config means 1x", 0, loginRateLimitPerMinute, 5},
{"negative means 1x", -3.5, loginRateLimitPerMinute, 5},
{"1x leaves the limit alone", 1, registerRateLimitPerMinute, 3},
{"above the cap clamps to 100x", 1e9, loginRateLimitPerMinute, 500},
{"at the cap is 100x", 100, loginRateLimitPerMinute, 500},
{"below the floor clamps to 0.1x", 1e-9, verifyTOTPRateLimitPerMinute, 1},
{"at the floor is 0.1x", 0.1, verifyTOTPRateLimitPerMinute, 1},
{"in range scales and rounds", 0.5, loginRateLimitPerMinute, 3},
{"in range scales the failure threshold", 2, loginFailureThreshold, 18},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setAuthRateScale(tt.mult)
if got := scaledAuthLimit(tt.limit); got != tt.want {
t.Errorf("setAuthRateScale(%v); scaledAuthLimit(%d) = %d, want %d",
tt.mult, tt.limit, got, tt.want)
}
})
}
}
// A limit of 0 lets nothing through: on the login failure threshold
// (auth_handler.go:514) that locks every IP out on its first attempt. The
// smallest allowed multiplier must still leave every scaled limit usable.
func TestScaledAuthLimit_NeverBelowOne(t *testing.T) {
t.Cleanup(func() { setAuthRateScale(1.0) })
setAuthRateScale(0.1)
for _, n := range []int{
1,
registerRateLimitPerMinute,
loginRateLimitPerMinute,
verifyTOTPRateLimitPerMinute,
sensitiveEndpointRateLimitPerMinute,
loginFailureThreshold,
} {
if got := scaledAuthLimit(n); got < 1 {
t.Errorf("scaledAuthLimit(%d) = %d at the 0.1x floor, want >= 1", n, got)
}
}
}
// The multiplier exists for shared-NAT *per-IP* limits. The per-user caps are
// the only cross-IP brute-force defence, so scaling them would hand a
// distributed attacker up to 100x the guesses (totp_handler.go:76-80). Those
// caps are only observable through a limiter key inside the handler, so this
// pins the call site instead.
func TestPerUserFailureCapsStayUnscaled(t *testing.T) {
for file, constants := range map[string][]string{
"totp_handler.go": {"totpFailureRateLimit"},
"auth_handler.go": {"loginUserFailureThreshold"},
} {
src, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read %s: %v", file, err)
}
for _, c := range constants {
if strings.Contains(string(src), "scaledAuthLimit("+c) {
t.Errorf("%s scales %s with the per-IP auth multiplier; per-user caps must stay unscaled",
file, c)
}
}
}
}