mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Audits what actually has tests, then closes the gaps it found. Full write-up with before/after numbers in docs/audit-test-coverage-2026-07-25.md. Measurement first: `go test ./... -coverprofile` (what CI runs) instruments each package only for itself, so code exercised through another package's tests reads as uncovered — `service` reported 36.7% against a real 85%. All analysis here uses -coverpkg=./..., and both views now have Makefile targets. Features that had zero coverage at every layer: - user blocking (db + service + the /api/v1/blocks routes) - auth lockout persistence — the DB round-trip that survives a restart - plugin install/enable/disable/uninstall and the plugin KV namespace - event replay bounds (GetMaxEventSeq, PruneEventsOlderThan) - LiveKit participant_joined webhook (replayed-token guard), the room-service client, and proxyWebSocket/copyWS - ws_proxy.rs and livekit_proxy.rs — pure helpers extracted, matching the existing tofu.rs pattern, so cert-pin and header-injection checks are testable Gaps that were hidden rather than absent: - Server/admin reported 0.3% coverage with 307 tests passing. TestSpawnDetached_* re-execs the test binary; the child inherited GOCOVERDIR and the parent's stdout, clobbering the profile and printing "[no tests to run]". Now 71.4%, and CI's uploaded artifact is correct. - vitest.config.ts excluded 2.2k LOC unexplained, including two files that already had tests. Trimmed to three entries, each justified inline. - api.HandleLiveKitHealthForTest re-implemented the handler it claimed to expose, so eight call sites tested a copy. Added a hook to the real one. Two bugs found and pinned rather than silently patched: logctx.WithGroup nests req_id under the group, and drag-reorder.ts takes one listener ref per channel but releases one per sidebar, so the count never reaches zero. Coverage: client 92.93% -> 94.87% statements (3371 -> 3572 tests) even after un-excluding hidden files; Rust 47 -> 74 tests; Go zero-coverage functions ~70 -> 21, with plugin 61->77%, admin 67->86%, db 76->84%, service 85->91%. Verified: go vet, all four build-tag variants, go test -race, -tags deadlock, vitest --coverage, cargo test --lib, cargo clippy --all-targets, playwright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEETs3Vh6sAHHb1jMBL75g
174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/owncord/server/db"
|
|
)
|
|
|
|
// BlockService had no coverage at all. Its job is the validation layer above
|
|
// db.BlockUser — which is INSERT OR IGNORE and therefore silently accepts a
|
|
// self-block — so these tests concentrate on the rejections.
|
|
|
|
func newBlockService(t *testing.T) (*BlockService, *db.DB) {
|
|
t.Helper()
|
|
database := newTestDB(t)
|
|
seedUser(t, database, &db.User{ID: 1, Username: "alice"})
|
|
seedUser(t, database, &db.User{ID: 2, Username: "bob"})
|
|
return NewBlockService(database), database
|
|
}
|
|
|
|
func TestBlockService_BlockUser(t *testing.T) {
|
|
svc, database := newBlockService(t)
|
|
ctx := context.Background()
|
|
|
|
if err := svc.BlockUser(ctx, 1, 2); err != nil {
|
|
t.Fatalf("BlockUser: %v", err)
|
|
}
|
|
|
|
blocked, err := database.IsBlocked(ctx, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("IsBlocked: %v", err)
|
|
}
|
|
if !blocked {
|
|
t.Error("block was not persisted")
|
|
}
|
|
}
|
|
|
|
func TestBlockService_BlockUser_Rejections(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
blockerID int64
|
|
targetID int64
|
|
wantErr error
|
|
}{
|
|
{"zero target", 1, 0, ErrBadRequest},
|
|
{"negative target", 1, -5, ErrBadRequest},
|
|
{"self block", 1, 1, ErrBadRequest},
|
|
{"unknown target", 1, 9999, ErrNotFound},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, database := newBlockService(t)
|
|
ctx := context.Background()
|
|
|
|
err := svc.BlockUser(ctx, tt.blockerID, tt.targetID)
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Fatalf("BlockUser(%d, %d) = %v, want %v",
|
|
tt.blockerID, tt.targetID, err, tt.wantErr)
|
|
}
|
|
|
|
// A rejected block must not write anything.
|
|
ids, listErr := database.ListBlockedUsers(ctx, tt.blockerID)
|
|
if listErr != nil {
|
|
t.Fatalf("ListBlockedUsers: %v", listErr)
|
|
}
|
|
if len(ids) != 0 {
|
|
t.Errorf("a rejected block still wrote %v", ids)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBlockService_BlockUser_Idempotent(t *testing.T) {
|
|
svc, database := newBlockService(t)
|
|
ctx := context.Background()
|
|
|
|
for i := range 2 {
|
|
if err := svc.BlockUser(ctx, 1, 2); err != nil {
|
|
t.Fatalf("BlockUser call %d: %v", i+1, err)
|
|
}
|
|
}
|
|
|
|
ids, err := database.ListBlockedUsers(ctx, 1)
|
|
if err != nil {
|
|
t.Fatalf("ListBlockedUsers: %v", err)
|
|
}
|
|
if len(ids) != 1 {
|
|
t.Errorf("ListBlockedUsers = %v after blocking twice, want one entry", ids)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_UnblockUser(t *testing.T) {
|
|
svc, database := newBlockService(t)
|
|
ctx := context.Background()
|
|
|
|
if err := svc.BlockUser(ctx, 1, 2); err != nil {
|
|
t.Fatalf("BlockUser: %v", err)
|
|
}
|
|
if err := svc.UnblockUser(ctx, 1, 2); err != nil {
|
|
t.Fatalf("UnblockUser: %v", err)
|
|
}
|
|
|
|
blocked, err := database.IsBlocked(ctx, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("IsBlocked: %v", err)
|
|
}
|
|
if blocked {
|
|
t.Error("block survived UnblockUser")
|
|
}
|
|
}
|
|
|
|
func TestBlockService_UnblockUser_Rejections(t *testing.T) {
|
|
svc, _ := newBlockService(t)
|
|
ctx := context.Background()
|
|
|
|
for _, targetID := range []int64{0, -1} {
|
|
if err := svc.UnblockUser(ctx, 1, targetID); !errors.Is(err, ErrBadRequest) {
|
|
t.Errorf("UnblockUser(1, %d) = %v, want ErrBadRequest", targetID, err)
|
|
}
|
|
}
|
|
|
|
// Unlike BlockUser, UnblockUser does not verify the target exists — an
|
|
// unblock of a never-blocked (or deleted) user is a successful no-op.
|
|
if err := svc.UnblockUser(ctx, 1, 9999); err != nil {
|
|
t.Errorf("UnblockUser on an unknown user = %v, want nil (no-op)", err)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ListBlocked(t *testing.T) {
|
|
svc, database := newBlockService(t)
|
|
ctx := context.Background()
|
|
seedUser(t, database, &db.User{ID: 3, Username: "carol"})
|
|
|
|
// Never nil — the REST layer serializes this straight to JSON, and a nil
|
|
// slice would emit `null` where clients expect `[]`.
|
|
got, err := svc.ListBlocked(ctx, 1)
|
|
if err != nil {
|
|
t.Fatalf("ListBlocked: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("ListBlocked returned nil; want an empty slice so it marshals as []")
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("ListBlocked = %v on a fresh user, want empty", got)
|
|
}
|
|
|
|
if err := svc.BlockUser(ctx, 1, 2); err != nil {
|
|
t.Fatalf("BlockUser: %v", err)
|
|
}
|
|
if err := svc.BlockUser(ctx, 1, 3); err != nil {
|
|
t.Fatalf("BlockUser: %v", err)
|
|
}
|
|
|
|
got, err = svc.ListBlocked(ctx, 1)
|
|
if err != nil {
|
|
t.Fatalf("ListBlocked: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("ListBlocked = %v, want 2 entries", got)
|
|
}
|
|
|
|
// Another user's blocks must not appear.
|
|
other, err := svc.ListBlocked(ctx, 2)
|
|
if err != nil {
|
|
t.Fatalf("ListBlocked for user 2: %v", err)
|
|
}
|
|
if len(other) != 0 {
|
|
t.Errorf("ListBlocked(2) = %v, want empty", other)
|
|
}
|
|
}
|