Skip to content
This repository was archived by the owner on Jun 2, 2026. It is now read-only.

Commit 045d8d1

Browse files
committed
test: Phase 3 — comprehensive test coverage for 6 untested packages
Add test coverage to all previously untested packages: Repository tests (10 files, ~90 methods covered): - records_test.go: Insert/BatchInsert/Get/Delete/Stats/TimeSeries/Iterate - config_test.go: Get/Set/AdminDIDs/URLDefaults/InitializeDefaults - actors_test.go: Upsert/BatchUpsert/GetByDID/GetByHandle/Exists - lexicons_test.go: Upsert/GetByID/GetAll/Delete/Exists - jetstream_activity_test.go: Log/UpdateStatus/Buckets/Cleanup - labels_test.go: Insert/Negation/Takedown/Pagination - label_definitions_test.go: CRUD + ValidateVisibility/ValidateSeverity - reports_test.go: Insert/Resolve/Pagination + ValidateReasonType/Status - oauth_test.go: Clients/AccessTokens/RefreshTokens/AuthCodes/DPoPJTI - label_preferences_test.go: covered via labels_test.go Other package tests (5 files): - jetstream/event_test.go: ParseEvent, IsCommit/Create/Update/Delete, URI - server/handlers_test.go: DPoPNonce, ClientMetadata, GraphiQL, ConnectDB - graphql/types/types_test.go: Mapper primitives, ObjectBuilder, caching - workers/workers_test.go: BackfillState lifecycle, ActivityCleanupWorker - database/migrations/migrations_test.go: Run/Idempotent/Rollback Infrastructure: - internal/testutil/db.go: Shared SetupTestDB helper (in-memory SQLite) Bug fix discovered via tests: - labels.go: Fix negation query from timestamp comparison (cts > cts) to ID comparison (id > id) — SQLite second-precision timestamps caused negations within the same second to be invisible Previously 6 packages had zero test files. Now only cmd/hypergoat and graphql/query remain without tests (both are trivially simple). All tests pass: go build ./... && go test ./...
1 parent 38f6bc7 commit 045d8d1

16 files changed

Lines changed: 5541 additions & 3 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package migrations_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/GainForest/hypergoat/internal/database/migrations"
8+
"github.com/GainForest/hypergoat/internal/database/sqlite"
9+
)
10+
11+
// newTestExecutor creates an in-memory SQLite executor for testing.
12+
func newTestExecutor(t *testing.T) *sqlite.Executor {
13+
t.Helper()
14+
15+
exec, err := sqlite.NewExecutor("sqlite::memory:")
16+
if err != nil {
17+
t.Fatalf("failed to create SQLite executor: %v", err)
18+
}
19+
t.Cleanup(func() { exec.Close() })
20+
21+
return exec
22+
}
23+
24+
func TestMigrations_Run(t *testing.T) {
25+
exec := newTestExecutor(t)
26+
ctx := context.Background()
27+
28+
if err := migrations.Run(ctx, exec); err != nil {
29+
t.Fatalf("Run() returned error: %v", err)
30+
}
31+
32+
// Verify key tables exist by querying sqlite_master.
33+
expectedTables := []string{
34+
"record",
35+
"actor",
36+
"config",
37+
"lexicon",
38+
"jetstream_activity",
39+
"label",
40+
"report",
41+
"label_definition",
42+
"actor_label_preference",
43+
}
44+
45+
for _, table := range expectedTables {
46+
var name string
47+
err := exec.DB().QueryRowContext(ctx,
48+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", table,
49+
).Scan(&name)
50+
if err != nil {
51+
t.Errorf("expected table %q to exist, but got error: %v", table, err)
52+
}
53+
}
54+
}
55+
56+
func TestMigrations_RunIdempotent(t *testing.T) {
57+
exec := newTestExecutor(t)
58+
ctx := context.Background()
59+
60+
if err := migrations.Run(ctx, exec); err != nil {
61+
t.Fatalf("first Run() returned error: %v", err)
62+
}
63+
64+
// Running a second time should be a no-op (all migrations already applied).
65+
if err := migrations.Run(ctx, exec); err != nil {
66+
t.Fatalf("second Run() returned error: %v", err)
67+
}
68+
69+
// Verify tables still present after second run.
70+
var count int
71+
err := exec.DB().QueryRowContext(ctx,
72+
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='record'",
73+
).Scan(&count)
74+
if err != nil {
75+
t.Fatalf("failed to query sqlite_master: %v", err)
76+
}
77+
if count != 1 {
78+
t.Errorf("expected 1 record table, got %d", count)
79+
}
80+
}
81+
82+
func TestMigrations_Rollback(t *testing.T) {
83+
exec := newTestExecutor(t)
84+
ctx := context.Background()
85+
86+
// Apply all migrations first.
87+
if err := migrations.Run(ctx, exec); err != nil {
88+
t.Fatalf("Run() returned error: %v", err)
89+
}
90+
91+
// Count applied migrations before rollback.
92+
var countBefore int
93+
err := exec.DB().QueryRowContext(ctx,
94+
"SELECT COUNT(*) FROM schema_migrations",
95+
).Scan(&countBefore)
96+
if err != nil {
97+
t.Fatalf("failed to count migrations: %v", err)
98+
}
99+
100+
if countBefore == 0 {
101+
t.Fatal("expected at least one applied migration before rollback")
102+
}
103+
104+
// Rollback the last migration.
105+
if err := migrations.Rollback(ctx, exec); err != nil {
106+
t.Fatalf("Rollback() returned error: %v", err)
107+
}
108+
109+
// Verify one fewer migration is recorded.
110+
var countAfter int
111+
err = exec.DB().QueryRowContext(ctx,
112+
"SELECT COUNT(*) FROM schema_migrations",
113+
).Scan(&countAfter)
114+
if err != nil {
115+
t.Fatalf("failed to count migrations after rollback: %v", err)
116+
}
117+
118+
if countAfter != countBefore-1 {
119+
t.Errorf("expected %d migrations after rollback, got %d", countBefore-1, countAfter)
120+
}
121+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package repositories_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"testing"
8+
9+
"github.com/GainForest/hypergoat/internal/database/repositories"
10+
"github.com/GainForest/hypergoat/internal/testutil"
11+
)
12+
13+
func setupActorsTest(t *testing.T) *repositories.ActorsRepository {
14+
t.Helper()
15+
db := testutil.SetupTestDB(t)
16+
return db.Actors
17+
}
18+
19+
func TestActorsRepository_Upsert(t *testing.T) {
20+
repo := setupActorsTest(t)
21+
ctx := context.Background()
22+
23+
// Insert new actor
24+
err := repo.Upsert(ctx, "did:plc:testactor1", "alice.bsky.social")
25+
if err != nil {
26+
t.Fatalf("failed to insert actor: %v", err)
27+
}
28+
29+
actor, err := repo.GetByDID(ctx, "did:plc:testactor1")
30+
if err != nil {
31+
t.Fatalf("failed to get actor after insert: %v", err)
32+
}
33+
if actor.DID != "did:plc:testactor1" {
34+
t.Errorf("DID = %q, want %q", actor.DID, "did:plc:testactor1")
35+
}
36+
if actor.Handle != "alice.bsky.social" {
37+
t.Errorf("Handle = %q, want %q", actor.Handle, "alice.bsky.social")
38+
}
39+
40+
// Update handle via upsert
41+
err = repo.Upsert(ctx, "did:plc:testactor1", "alice-new.bsky.social")
42+
if err != nil {
43+
t.Fatalf("failed to upsert actor: %v", err)
44+
}
45+
46+
actor, err = repo.GetByDID(ctx, "did:plc:testactor1")
47+
if err != nil {
48+
t.Fatalf("failed to get actor after upsert: %v", err)
49+
}
50+
if actor.Handle != "alice-new.bsky.social" {
51+
t.Errorf("Handle after upsert = %q, want %q", actor.Handle, "alice-new.bsky.social")
52+
}
53+
}
54+
55+
func TestActorsRepository_BatchUpsert(t *testing.T) {
56+
tests := []struct {
57+
name string
58+
actors []repositories.ActorData
59+
want int64
60+
}{
61+
{
62+
name: "empty slice",
63+
actors: []repositories.ActorData{},
64+
want: 0,
65+
},
66+
{
67+
name: "single actor",
68+
actors: []repositories.ActorData{
69+
{DID: "did:plc:testactor1", Handle: "alice.bsky.social"},
70+
},
71+
want: 1,
72+
},
73+
{
74+
name: "multiple actors",
75+
actors: []repositories.ActorData{
76+
{DID: "did:plc:testactor1", Handle: "alice.bsky.social"},
77+
{DID: "did:plc:testactor2", Handle: "bob.bsky.social"},
78+
{DID: "did:plc:testactor3", Handle: "carol.bsky.social"},
79+
},
80+
want: 3,
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
repo := setupActorsTest(t)
87+
ctx := context.Background()
88+
89+
err := repo.BatchUpsert(ctx, tt.actors)
90+
if err != nil {
91+
t.Fatalf("BatchUpsert() error = %v", err)
92+
}
93+
94+
count, err := repo.GetCount(ctx)
95+
if err != nil {
96+
t.Fatalf("GetCount() error = %v", err)
97+
}
98+
if count != tt.want {
99+
t.Errorf("GetCount() = %d, want %d", count, tt.want)
100+
}
101+
})
102+
}
103+
}
104+
105+
func TestActorsRepository_GetByDID(t *testing.T) {
106+
repo := setupActorsTest(t)
107+
ctx := context.Background()
108+
109+
// Setup: insert an actor
110+
err := repo.Upsert(ctx, "did:plc:testactor1", "alice.bsky.social")
111+
if err != nil {
112+
t.Fatalf("failed to insert actor: %v", err)
113+
}
114+
115+
t.Run("found", func(t *testing.T) {
116+
actor, err := repo.GetByDID(ctx, "did:plc:testactor1")
117+
if err != nil {
118+
t.Fatalf("GetByDID() error = %v", err)
119+
}
120+
if actor.DID != "did:plc:testactor1" {
121+
t.Errorf("DID = %q, want %q", actor.DID, "did:plc:testactor1")
122+
}
123+
if actor.Handle != "alice.bsky.social" {
124+
t.Errorf("Handle = %q, want %q", actor.Handle, "alice.bsky.social")
125+
}
126+
})
127+
128+
t.Run("not found", func(t *testing.T) {
129+
_, err := repo.GetByDID(ctx, "did:plc:nonexistent")
130+
if err == nil {
131+
t.Fatal("GetByDID() expected error for non-existing DID, got nil")
132+
}
133+
if !errors.Is(err, sql.ErrNoRows) {
134+
t.Errorf("GetByDID() error = %v, want sql.ErrNoRows", err)
135+
}
136+
})
137+
}
138+
139+
func TestActorsRepository_GetByHandle(t *testing.T) {
140+
repo := setupActorsTest(t)
141+
ctx := context.Background()
142+
143+
// Setup: insert an actor
144+
err := repo.Upsert(ctx, "did:plc:testactor1", "alice.bsky.social")
145+
if err != nil {
146+
t.Fatalf("failed to insert actor: %v", err)
147+
}
148+
149+
t.Run("found", func(t *testing.T) {
150+
actor, err := repo.GetByHandle(ctx, "alice.bsky.social")
151+
if err != nil {
152+
t.Fatalf("GetByHandle() error = %v", err)
153+
}
154+
if actor.DID != "did:plc:testactor1" {
155+
t.Errorf("DID = %q, want %q", actor.DID, "did:plc:testactor1")
156+
}
157+
if actor.Handle != "alice.bsky.social" {
158+
t.Errorf("Handle = %q, want %q", actor.Handle, "alice.bsky.social")
159+
}
160+
})
161+
162+
t.Run("not found", func(t *testing.T) {
163+
_, err := repo.GetByHandle(ctx, "nobody.bsky.social")
164+
if err == nil {
165+
t.Fatal("GetByHandle() expected error for non-existing handle, got nil")
166+
}
167+
if !errors.Is(err, sql.ErrNoRows) {
168+
t.Errorf("GetByHandle() error = %v, want sql.ErrNoRows", err)
169+
}
170+
})
171+
}
172+
173+
func TestActorsRepository_GetCount(t *testing.T) {
174+
repo := setupActorsTest(t)
175+
ctx := context.Background()
176+
177+
// Empty database
178+
count, err := repo.GetCount(ctx)
179+
if err != nil {
180+
t.Fatalf("GetCount() error = %v", err)
181+
}
182+
if count != 0 {
183+
t.Errorf("GetCount() on empty db = %d, want 0", count)
184+
}
185+
186+
// After inserts
187+
err = repo.Upsert(ctx, "did:plc:testactor1", "alice.bsky.social")
188+
if err != nil {
189+
t.Fatalf("failed to insert actor: %v", err)
190+
}
191+
err = repo.Upsert(ctx, "did:plc:testactor2", "bob.bsky.social")
192+
if err != nil {
193+
t.Fatalf("failed to insert actor: %v", err)
194+
}
195+
196+
count, err = repo.GetCount(ctx)
197+
if err != nil {
198+
t.Fatalf("GetCount() error = %v", err)
199+
}
200+
if count != 2 {
201+
t.Errorf("GetCount() after 2 inserts = %d, want 2", count)
202+
}
203+
}
204+
205+
func TestActorsRepository_DeleteAll(t *testing.T) {
206+
repo := setupActorsTest(t)
207+
ctx := context.Background()
208+
209+
// Insert some actors
210+
err := repo.BatchUpsert(ctx, []repositories.ActorData{
211+
{DID: "did:plc:testactor1", Handle: "alice.bsky.social"},
212+
{DID: "did:plc:testactor2", Handle: "bob.bsky.social"},
213+
})
214+
if err != nil {
215+
t.Fatalf("BatchUpsert() error = %v", err)
216+
}
217+
218+
// Verify they exist
219+
count, err := repo.GetCount(ctx)
220+
if err != nil {
221+
t.Fatalf("GetCount() error = %v", err)
222+
}
223+
if count != 2 {
224+
t.Fatalf("GetCount() = %d, want 2 before delete", count)
225+
}
226+
227+
// Delete all
228+
err = repo.DeleteAll(ctx)
229+
if err != nil {
230+
t.Fatalf("DeleteAll() error = %v", err)
231+
}
232+
233+
// Verify empty
234+
count, err = repo.GetCount(ctx)
235+
if err != nil {
236+
t.Fatalf("GetCount() error = %v", err)
237+
}
238+
if count != 0 {
239+
t.Errorf("GetCount() after DeleteAll = %d, want 0", count)
240+
}
241+
}
242+
243+
func TestActorsRepository_Exists(t *testing.T) {
244+
repo := setupActorsTest(t)
245+
ctx := context.Background()
246+
247+
// Insert an actor
248+
err := repo.Upsert(ctx, "did:plc:testactor1", "alice.bsky.social")
249+
if err != nil {
250+
t.Fatalf("failed to insert actor: %v", err)
251+
}
252+
253+
t.Run("existing actor", func(t *testing.T) {
254+
exists, err := repo.Exists(ctx, "did:plc:testactor1")
255+
if err != nil {
256+
t.Fatalf("Exists() error = %v", err)
257+
}
258+
if !exists {
259+
t.Error("Exists() = false, want true for existing actor")
260+
}
261+
})
262+
263+
t.Run("non-existing actor", func(t *testing.T) {
264+
exists, err := repo.Exists(ctx, "did:plc:nonexistent")
265+
if err != nil {
266+
t.Fatalf("Exists() error = %v", err)
267+
}
268+
if exists {
269+
t.Error("Exists() = true, want false for non-existing actor")
270+
}
271+
})
272+
}

0 commit comments

Comments
 (0)