package store_test import ( "context" "path/filepath" "testing" "github.com/andyleap/hnh-map/internal/app/store" "go.etcd.io/bbolt" ) func newTestStore(t *testing.T) *store.Store { t.Helper() dir := t.TempDir() db, err := bbolt.Open(filepath.Join(dir, "test.db"), 0600, nil) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return store.New(db) } func TestUserCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() // Verify user doesn't exist on empty DB. if err := st.View(ctx, func(tx *bbolt.Tx) error { if got := st.GetUser(tx, "alice"); got != nil { t.Fatal("expected nil user before creation") } return nil }); err != nil { t.Fatal(err) } // Create user. if err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.PutUser(tx, "alice", []byte(`{"pass":"hash"}`)) }); err != nil { t.Fatal(err) } // Verify user exists and count is correct (separate transaction for accurate Stats). if err := st.View(ctx, func(tx *bbolt.Tx) error { got := st.GetUser(tx, "alice") if got == nil || string(got) != `{"pass":"hash"}` { t.Fatalf("expected user data, got %s", got) } if c := st.UserCount(tx); c != 1 { t.Fatalf("expected 1 user, got %d", c) } return nil }); err != nil { t.Fatal(err) } // Delete user. if err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteUser(tx, "alice") }); err != nil { t.Fatal(err) } if err := st.View(ctx, func(tx *bbolt.Tx) error { if got := st.GetUser(tx, "alice"); got != nil { t.Fatal("expected nil user after deletion") } return nil }); err != nil { t.Fatal(err) } } func TestForEachUser(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if err := st.PutUser(tx, "alice", []byte("1")); err != nil { return err } if err := st.PutUser(tx, "bob", []byte("2")); err != nil { return err } return nil }); err != nil { t.Fatal(err) } var names []string if err := st.View(ctx, func(tx *bbolt.Tx) error { return st.ForEachUser(tx, func(k, _ []byte) error { names = append(names, string(k)) return nil }) }); err != nil { t.Fatal(err) } if len(names) != 2 { t.Fatalf("expected 2 users, got %d", len(names)) } } func TestUserCountEmptyBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.View(ctx, func(tx *bbolt.Tx) error { if c := st.UserCount(tx); c != 0 { t.Fatalf("expected 0 users on empty db, got %d", c) } return nil }); err != nil { t.Fatal(err) } } func TestSessionCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetSession(tx, "sess1"); got != nil { t.Fatal("expected nil session") } if err := st.PutSession(tx, "sess1", []byte(`{"user":"alice"}`)); err != nil { return err } got := st.GetSession(tx, "sess1") if string(got) != `{"user":"alice"}` { t.Fatalf("unexpected session data: %s", got) } return st.DeleteSession(tx, "sess1") }) if err != nil { t.Fatal(err) } } func TestTokenCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetTokenUser(tx, "tok"); got != nil { t.Fatal("expected nil token") } if err := st.PutToken(tx, "tok", "alice"); err != nil { return err } got := st.GetTokenUser(tx, "tok") if string(got) != "alice" { t.Fatalf("expected alice, got %s", got) } return st.DeleteToken(tx, "tok") }) if err != nil { t.Fatal(err) } } func TestConfigCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetConfig(tx, "title"); got != nil { t.Fatal("expected nil config") } if err := st.PutConfig(tx, "title", []byte("My Map")); err != nil { return err } got := st.GetConfig(tx, "title") if string(got) != "My Map" { t.Fatalf("expected My Map, got %s", got) } return st.DeleteConfig(tx, "title") }) if err != nil { t.Fatal(err) } } func TestMapCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetMap(tx, 1); got != nil { t.Fatal("expected nil map") } if err := st.PutMap(tx, 1, []byte(`{"name":"world"}`)); err != nil { return err } got := st.GetMap(tx, 1) if string(got) != `{"name":"world"}` { t.Fatalf("unexpected map data: %s", got) } seq, err := st.MapsNextSequence(tx) if err != nil { return err } if seq == 0 { t.Fatal("expected non-zero sequence") } return st.DeleteMap(tx, 1) }) if err != nil { t.Fatal(err) } } func TestForEachMap(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if err := st.PutMap(tx, 1, []byte("a")); err != nil { return err } if err := st.PutMap(tx, 2, []byte("b")); err != nil { return err } return nil }); err != nil { t.Fatal(err) } var count int if err := st.View(ctx, func(tx *bbolt.Tx) error { return st.ForEachMap(tx, func(_, _ []byte) error { count++ return nil }) }); err != nil { t.Fatal(err) } if count != 2 { t.Fatalf("expected 2 maps, got %d", count) } } func TestGridCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetGrid(tx, "g1"); got != nil { t.Fatal("expected nil grid") } if err := st.PutGrid(tx, "g1", []byte(`{"map":1}`)); err != nil { return err } got := st.GetGrid(tx, "g1") if string(got) != `{"map":1}` { t.Fatalf("unexpected grid data: %s", got) } return st.DeleteGrid(tx, "g1") }) if err != nil { t.Fatal(err) } } func TestTileCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetTile(tx, 1, 0, "0_0"); got != nil { t.Fatal("expected nil tile") } if err := st.PutTile(tx, 1, 0, "0_0", []byte("png")); err != nil { return err } got := st.GetTile(tx, 1, 0, "0_0") if string(got) != "png" { t.Fatalf("unexpected tile data: %s", got) } if got := st.GetTile(tx, 1, 0, "1_1"); got != nil { t.Fatal("expected nil for different coord") } if got := st.GetTile(tx, 2, 0, "0_0"); got != nil { t.Fatal("expected nil for different map") } return nil }) if err != nil { t.Fatal(err) } } func TestForEachTile(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if err := st.PutTile(tx, 1, 0, "0_0", []byte("a")); err != nil { return err } if err := st.PutTile(tx, 1, 1, "0_0", []byte("b")); err != nil { return err } if err := st.PutTile(tx, 2, 0, "1_1", []byte("c")); err != nil { return err } return nil }); err != nil { t.Fatal(err) } var count int if err := st.View(ctx, func(tx *bbolt.Tx) error { return st.ForEachTile(tx, func(_, _, _, _ []byte) error { count++ return nil }) }); err != nil { t.Fatal(err) } if count != 3 { t.Fatalf("expected 3 tiles, got %d", count) } } func TestTilesMapBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if b := st.GetTilesMapBucket(tx, 1); b != nil { t.Fatal("expected nil bucket before creation") } b, err := st.CreateTilesMapBucket(tx, 1) if err != nil { return err } if b == nil { t.Fatal("expected non-nil bucket") } if b2 := st.GetTilesMapBucket(tx, 1); b2 == nil { t.Fatal("expected non-nil after create") } return st.DeleteTilesMapBucket(tx, 1) }); err != nil { t.Fatal(err) } } func TestDeleteTilesBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if err := st.PutTile(tx, 1, 0, "0_0", []byte("a")); err != nil { return err } return st.DeleteTilesBucket(tx) }); err != nil { t.Fatal(err) } if err := st.View(ctx, func(tx *bbolt.Tx) error { if got := st.GetTile(tx, 1, 0, "0_0"); got != nil { t.Fatal("expected nil after bucket deletion") } return nil }); err != nil { t.Fatal(err) } } func TestMarkerBuckets(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if b := st.GetMarkersGridBucket(tx); b != nil { t.Fatal("expected nil grid bucket before creation") } if b := st.GetMarkersIDBucket(tx); b != nil { t.Fatal("expected nil id bucket before creation") } grid, idB, err := st.CreateMarkersBuckets(tx) if err != nil { return err } if grid == nil || idB == nil { t.Fatal("expected non-nil marker buckets") } seq, err := st.MarkersNextSequence(tx) if err != nil { return err } if seq == 0 { t.Fatal("expected non-zero sequence") } return nil }); err != nil { t.Fatal(err) } } func TestOAuthStateCRUD(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { if got := st.GetOAuthState(tx, "state1"); got != nil { t.Fatal("expected nil") } if err := st.PutOAuthState(tx, "state1", []byte(`{"provider":"google"}`)); err != nil { return err } got := st.GetOAuthState(tx, "state1") if string(got) != `{"provider":"google"}` { t.Fatalf("unexpected state data: %s", got) } return st.DeleteOAuthState(tx, "state1") }) if err != nil { t.Fatal(err) } } func TestBucketExistsAndDelete(t *testing.T) { st := newTestStore(t) ctx := context.Background() if err := st.Update(ctx, func(tx *bbolt.Tx) error { if st.BucketExists(tx, store.BucketUsers) { t.Fatal("expected bucket to not exist") } if err := st.PutUser(tx, "alice", []byte("x")); err != nil { return err } if !st.BucketExists(tx, store.BucketUsers) { t.Fatal("expected bucket to exist") } return st.DeleteBucket(tx, store.BucketUsers) }); err != nil { t.Fatal(err) } if err := st.View(ctx, func(tx *bbolt.Tx) error { if st.BucketExists(tx, store.BucketUsers) { t.Fatal("expected bucket to be deleted") } return nil }); err != nil { t.Fatal(err) } } func TestDeleteBucketNonExistent(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteBucket(tx, store.BucketUsers) }) if err != nil { t.Fatalf("deleting non-existent bucket should not error: %v", err) } } func TestViewCancelledContext(t *testing.T) { st := newTestStore(t) ctx, cancel := context.WithCancel(context.Background()) cancel() err := st.View(ctx, func(_ *bbolt.Tx) error { t.Fatal("should not execute") return nil }) if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestUpdateCancelledContext(t *testing.T) { st := newTestStore(t) ctx, cancel := context.WithCancel(context.Background()) cancel() err := st.Update(ctx, func(_ *bbolt.Tx) error { t.Fatal("should not execute") return nil }) if err != context.Canceled { t.Fatalf("expected context.Canceled, got %v", err) } } func TestDeleteSessionNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteSession(tx, "nonexistent") }) if err != nil { t.Fatal(err) } } func TestDeleteTokenNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteToken(tx, "nonexistent") }) if err != nil { t.Fatal(err) } } func TestDeleteUserNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteUser(tx, "nonexistent") }) if err != nil { t.Fatal(err) } } func TestDeleteConfigNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteConfig(tx, "nonexistent") }) if err != nil { t.Fatal(err) } } func TestDeleteMapNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteMap(tx, 1) }) if err != nil { t.Fatal(err) } } func TestDeleteGridNoBucket(t *testing.T) { st := newTestStore(t) ctx := context.Background() err := st.Update(ctx, func(tx *bbolt.Tx) error { return st.DeleteGrid(tx, "g1") }) if err != nil { t.Fatal(err) } }