- Updated docker-compose.tools.yml to mount source code at /src and set working directory for backend tools, ensuring proper Go module caching. - Modified Dockerfile.tools to install the latest golangci-lint version compatible with Go 1.24 and adjusted working directory for build-time operations. - Enhanced Makefile to build backend tools before running tests and linting, ensuring dependencies are up-to-date and improving overall workflow efficiency. - Refactored test and handler files to include error handling for database operations, enhancing reliability and debugging capabilities.
583 lines
13 KiB
Go
583 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
|