Add configuration files and update project documentation
- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
@@ -16,19 +17,29 @@ func New(db *bbolt.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
// View runs fn in a read-only transaction.
|
||||
func (s *Store) View(fn func(tx *bbolt.Tx) error) error {
|
||||
return s.db.View(fn)
|
||||
// View runs fn in a read-only transaction. Checks context before starting.
|
||||
func (s *Store) View(ctx context.Context, fn func(tx *bbolt.Tx) error) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return s.db.View(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Update runs fn in a read-write transaction.
|
||||
func (s *Store) Update(fn func(tx *bbolt.Tx) error) error {
|
||||
return s.db.Update(fn)
|
||||
// Update runs fn in a read-write transaction. Checks context before starting.
|
||||
func (s *Store) Update(ctx context.Context, fn func(tx *bbolt.Tx) error) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return s.db.Update(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Users ---
|
||||
|
||||
// GetUser returns raw user bytes by username, or nil if not found.
|
||||
// GetUser returns the raw JSON for a user, or nil if not found.
|
||||
func (s *Store) GetUser(tx *bbolt.Tx, username string) []byte {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
@@ -37,7 +48,7 @@ func (s *Store) GetUser(tx *bbolt.Tx, username string) []byte {
|
||||
return b.Get([]byte(username))
|
||||
}
|
||||
|
||||
// PutUser stores user bytes by username.
|
||||
// PutUser stores a user (creates the bucket if needed).
|
||||
func (s *Store) PutUser(tx *bbolt.Tx, username string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketUsers)
|
||||
if err != nil {
|
||||
@@ -46,7 +57,7 @@ func (s *Store) PutUser(tx *bbolt.Tx, username string, raw []byte) error {
|
||||
return b.Put([]byte(username), raw)
|
||||
}
|
||||
|
||||
// DeleteUser removes a user.
|
||||
// DeleteUser removes a user by username.
|
||||
func (s *Store) DeleteUser(tx *bbolt.Tx, username string) error {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
@@ -55,7 +66,7 @@ func (s *Store) DeleteUser(tx *bbolt.Tx, username string) error {
|
||||
return b.Delete([]byte(username))
|
||||
}
|
||||
|
||||
// ForEachUser calls fn for each user key.
|
||||
// ForEachUser iterates over all users.
|
||||
func (s *Store) ForEachUser(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
@@ -64,7 +75,7 @@ func (s *Store) ForEachUser(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
return b.ForEach(fn)
|
||||
}
|
||||
|
||||
// UserCount returns the number of users.
|
||||
// UserCount returns the number of users in the database.
|
||||
func (s *Store) UserCount(tx *bbolt.Tx) int {
|
||||
b := tx.Bucket(BucketUsers)
|
||||
if b == nil {
|
||||
@@ -75,7 +86,7 @@ func (s *Store) UserCount(tx *bbolt.Tx) int {
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
// GetSession returns raw session bytes by ID, or nil if not found.
|
||||
// GetSession returns the raw JSON for a session, or nil if not found.
|
||||
func (s *Store) GetSession(tx *bbolt.Tx, id string) []byte {
|
||||
b := tx.Bucket(BucketSessions)
|
||||
if b == nil {
|
||||
@@ -84,7 +95,7 @@ func (s *Store) GetSession(tx *bbolt.Tx, id string) []byte {
|
||||
return b.Get([]byte(id))
|
||||
}
|
||||
|
||||
// PutSession stores session bytes.
|
||||
// PutSession stores a session.
|
||||
func (s *Store) PutSession(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketSessions)
|
||||
if err != nil {
|
||||
@@ -93,7 +104,7 @@ func (s *Store) PutSession(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
return b.Put([]byte(id), raw)
|
||||
}
|
||||
|
||||
// DeleteSession removes a session.
|
||||
// DeleteSession removes a session by ID.
|
||||
func (s *Store) DeleteSession(tx *bbolt.Tx, id string) error {
|
||||
b := tx.Bucket(BucketSessions)
|
||||
if b == nil {
|
||||
@@ -104,7 +115,7 @@ func (s *Store) DeleteSession(tx *bbolt.Tx, id string) error {
|
||||
|
||||
// --- Tokens ---
|
||||
|
||||
// GetTokenUser returns username for token, or nil if not found.
|
||||
// GetTokenUser returns the username associated with a token, or nil.
|
||||
func (s *Store) GetTokenUser(tx *bbolt.Tx, token string) []byte {
|
||||
b := tx.Bucket(BucketTokens)
|
||||
if b == nil {
|
||||
@@ -113,7 +124,7 @@ func (s *Store) GetTokenUser(tx *bbolt.Tx, token string) []byte {
|
||||
return b.Get([]byte(token))
|
||||
}
|
||||
|
||||
// PutToken stores token -> username mapping.
|
||||
// PutToken associates a token with a username.
|
||||
func (s *Store) PutToken(tx *bbolt.Tx, token, username string) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketTokens)
|
||||
if err != nil {
|
||||
@@ -133,7 +144,7 @@ func (s *Store) DeleteToken(tx *bbolt.Tx, token string) error {
|
||||
|
||||
// --- Config ---
|
||||
|
||||
// GetConfig returns config value by key.
|
||||
// GetConfig returns a config value by key, or nil.
|
||||
func (s *Store) GetConfig(tx *bbolt.Tx, key string) []byte {
|
||||
b := tx.Bucket(BucketConfig)
|
||||
if b == nil {
|
||||
@@ -142,7 +153,7 @@ func (s *Store) GetConfig(tx *bbolt.Tx, key string) []byte {
|
||||
return b.Get([]byte(key))
|
||||
}
|
||||
|
||||
// PutConfig stores config value.
|
||||
// PutConfig stores a config key-value pair.
|
||||
func (s *Store) PutConfig(tx *bbolt.Tx, key string, value []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketConfig)
|
||||
if err != nil {
|
||||
@@ -162,7 +173,7 @@ func (s *Store) DeleteConfig(tx *bbolt.Tx, key string) error {
|
||||
|
||||
// --- Maps ---
|
||||
|
||||
// GetMap returns raw MapInfo bytes by ID.
|
||||
// GetMap returns the raw JSON for a map, or nil if not found.
|
||||
func (s *Store) GetMap(tx *bbolt.Tx, id int) []byte {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
@@ -171,7 +182,7 @@ func (s *Store) GetMap(tx *bbolt.Tx, id int) []byte {
|
||||
return b.Get([]byte(strconv.Itoa(id)))
|
||||
}
|
||||
|
||||
// PutMap stores MapInfo.
|
||||
// PutMap stores a map entry.
|
||||
func (s *Store) PutMap(tx *bbolt.Tx, id int, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketMaps)
|
||||
if err != nil {
|
||||
@@ -180,7 +191,7 @@ func (s *Store) PutMap(tx *bbolt.Tx, id int, raw []byte) error {
|
||||
return b.Put([]byte(strconv.Itoa(id)), raw)
|
||||
}
|
||||
|
||||
// DeleteMap removes a map.
|
||||
// DeleteMap removes a map by ID.
|
||||
func (s *Store) DeleteMap(tx *bbolt.Tx, id int) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
@@ -189,7 +200,7 @@ func (s *Store) DeleteMap(tx *bbolt.Tx, id int) error {
|
||||
return b.Delete([]byte(strconv.Itoa(id)))
|
||||
}
|
||||
|
||||
// MapsNextSequence returns next map ID sequence.
|
||||
// MapsNextSequence returns the next auto-increment ID for maps.
|
||||
func (s *Store) MapsNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketMaps)
|
||||
if err != nil {
|
||||
@@ -198,7 +209,7 @@ func (s *Store) MapsNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
return b.NextSequence()
|
||||
}
|
||||
|
||||
// MapsSetSequence sets map bucket sequence.
|
||||
// MapsSetSequence sets the maps bucket sequence counter.
|
||||
func (s *Store) MapsSetSequence(tx *bbolt.Tx, v uint64) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
@@ -207,7 +218,7 @@ func (s *Store) MapsSetSequence(tx *bbolt.Tx, v uint64) error {
|
||||
return b.SetSequence(v)
|
||||
}
|
||||
|
||||
// ForEachMap calls fn for each map.
|
||||
// ForEachMap iterates over all maps.
|
||||
func (s *Store) ForEachMap(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketMaps)
|
||||
if b == nil {
|
||||
@@ -218,7 +229,7 @@ func (s *Store) ForEachMap(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
|
||||
// --- Grids ---
|
||||
|
||||
// GetGrid returns raw GridData bytes by ID.
|
||||
// GetGrid returns the raw JSON for a grid, or nil if not found.
|
||||
func (s *Store) GetGrid(tx *bbolt.Tx, id string) []byte {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
@@ -227,7 +238,7 @@ func (s *Store) GetGrid(tx *bbolt.Tx, id string) []byte {
|
||||
return b.Get([]byte(id))
|
||||
}
|
||||
|
||||
// PutGrid stores GridData.
|
||||
// PutGrid stores a grid entry.
|
||||
func (s *Store) PutGrid(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketGrids)
|
||||
if err != nil {
|
||||
@@ -236,7 +247,7 @@ func (s *Store) PutGrid(tx *bbolt.Tx, id string, raw []byte) error {
|
||||
return b.Put([]byte(id), raw)
|
||||
}
|
||||
|
||||
// DeleteGrid removes a grid.
|
||||
// DeleteGrid removes a grid by ID.
|
||||
func (s *Store) DeleteGrid(tx *bbolt.Tx, id string) error {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
@@ -245,7 +256,7 @@ func (s *Store) DeleteGrid(tx *bbolt.Tx, id string) error {
|
||||
return b.Delete([]byte(id))
|
||||
}
|
||||
|
||||
// ForEachGrid calls fn for each grid.
|
||||
// ForEachGrid iterates over all grids.
|
||||
func (s *Store) ForEachGrid(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
b := tx.Bucket(BucketGrids)
|
||||
if b == nil {
|
||||
@@ -256,7 +267,7 @@ func (s *Store) ForEachGrid(tx *bbolt.Tx, fn func(k, v []byte) error) error {
|
||||
|
||||
// --- Tiles (nested: mapid -> zoom -> coord) ---
|
||||
|
||||
// GetTile returns raw TileData bytes.
|
||||
// GetTile returns the raw JSON for a tile at the given map/zoom/coord, or nil.
|
||||
func (s *Store) GetTile(tx *bbolt.Tx, mapID, zoom int, coordKey string) []byte {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
@@ -273,7 +284,7 @@ func (s *Store) GetTile(tx *bbolt.Tx, mapID, zoom int, coordKey string) []byte {
|
||||
return zoomB.Get([]byte(coordKey))
|
||||
}
|
||||
|
||||
// PutTile stores TileData.
|
||||
// PutTile stores a tile entry (creates nested buckets as needed).
|
||||
func (s *Store) PutTile(tx *bbolt.Tx, mapID, zoom int, coordKey string, raw []byte) error {
|
||||
tiles, err := tx.CreateBucketIfNotExists(BucketTiles)
|
||||
if err != nil {
|
||||
@@ -290,7 +301,7 @@ func (s *Store) PutTile(tx *bbolt.Tx, mapID, zoom int, coordKey string, raw []by
|
||||
return zoomB.Put([]byte(coordKey), raw)
|
||||
}
|
||||
|
||||
// DeleteTilesBucket removes the tiles bucket (for wipe).
|
||||
// DeleteTilesBucket removes the entire tiles bucket.
|
||||
func (s *Store) DeleteTilesBucket(tx *bbolt.Tx) error {
|
||||
if tx.Bucket(BucketTiles) == nil {
|
||||
return nil
|
||||
@@ -298,7 +309,7 @@ func (s *Store) DeleteTilesBucket(tx *bbolt.Tx) error {
|
||||
return tx.DeleteBucket(BucketTiles)
|
||||
}
|
||||
|
||||
// ForEachTile calls fn for each tile in the nested structure.
|
||||
// ForEachTile iterates over all tiles across all maps and zoom levels.
|
||||
func (s *Store) ForEachTile(tx *bbolt.Tx, fn func(mapK, zoomK, coordK, v []byte) error) error {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
@@ -321,7 +332,7 @@ func (s *Store) ForEachTile(tx *bbolt.Tx, fn func(mapK, zoomK, coordK, v []byte)
|
||||
})
|
||||
}
|
||||
|
||||
// GetTilesMapBucket returns the bucket for a map's tiles, or nil.
|
||||
// GetTilesMapBucket returns the tiles sub-bucket for a specific map, or nil.
|
||||
func (s *Store) GetTilesMapBucket(tx *bbolt.Tx, mapID int) *bbolt.Bucket {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
@@ -330,7 +341,7 @@ func (s *Store) GetTilesMapBucket(tx *bbolt.Tx, mapID int) *bbolt.Bucket {
|
||||
return tiles.Bucket([]byte(strconv.Itoa(mapID)))
|
||||
}
|
||||
|
||||
// CreateTilesMapBucket creates and returns the bucket for a map's tiles.
|
||||
// CreateTilesMapBucket returns or creates the tiles sub-bucket for a specific map.
|
||||
func (s *Store) CreateTilesMapBucket(tx *bbolt.Tx, mapID int) (*bbolt.Bucket, error) {
|
||||
tiles, err := tx.CreateBucketIfNotExists(BucketTiles)
|
||||
if err != nil {
|
||||
@@ -339,18 +350,22 @@ func (s *Store) CreateTilesMapBucket(tx *bbolt.Tx, mapID int) (*bbolt.Bucket, er
|
||||
return tiles.CreateBucketIfNotExists([]byte(strconv.Itoa(mapID)))
|
||||
}
|
||||
|
||||
// DeleteTilesMapBucket removes a map's tile bucket.
|
||||
// DeleteTilesMapBucket removes the tiles sub-bucket for a specific map.
|
||||
func (s *Store) DeleteTilesMapBucket(tx *bbolt.Tx, mapID int) error {
|
||||
tiles := tx.Bucket(BucketTiles)
|
||||
if tiles == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.DeleteBucket([]byte(strconv.Itoa(mapID)))
|
||||
key := []byte(strconv.Itoa(mapID))
|
||||
if tiles.Bucket(key) == nil {
|
||||
return nil
|
||||
}
|
||||
return tiles.DeleteBucket(key)
|
||||
}
|
||||
|
||||
// --- Markers (nested: grid bucket, id bucket) ---
|
||||
|
||||
// GetMarkersGridBucket returns the markers-by-grid bucket.
|
||||
// GetMarkersGridBucket returns the markers grid sub-bucket, or nil.
|
||||
func (s *Store) GetMarkersGridBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
@@ -359,7 +374,7 @@ func (s *Store) GetMarkersGridBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
return mb.Bucket(BucketMarkersGrid)
|
||||
}
|
||||
|
||||
// GetMarkersIDBucket returns the markers-by-id bucket.
|
||||
// GetMarkersIDBucket returns the markers ID sub-bucket, or nil.
|
||||
func (s *Store) GetMarkersIDBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
@@ -368,7 +383,7 @@ func (s *Store) GetMarkersIDBucket(tx *bbolt.Tx) *bbolt.Bucket {
|
||||
return mb.Bucket(BucketMarkersID)
|
||||
}
|
||||
|
||||
// CreateMarkersBuckets creates markers, grid, and id buckets.
|
||||
// CreateMarkersBuckets returns or creates both markers sub-buckets (grid and id).
|
||||
func (s *Store) CreateMarkersBuckets(tx *bbolt.Tx) (*bbolt.Bucket, *bbolt.Bucket, error) {
|
||||
mb, err := tx.CreateBucketIfNotExists(BucketMarkers)
|
||||
if err != nil {
|
||||
@@ -385,7 +400,7 @@ func (s *Store) CreateMarkersBuckets(tx *bbolt.Tx) (*bbolt.Bucket, *bbolt.Bucket
|
||||
return grid, idB, nil
|
||||
}
|
||||
|
||||
// MarkersNextSequence returns next marker ID.
|
||||
// MarkersNextSequence returns the next auto-increment ID for markers.
|
||||
func (s *Store) MarkersNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
mb := tx.Bucket(BucketMarkers)
|
||||
if mb == nil {
|
||||
@@ -400,7 +415,7 @@ func (s *Store) MarkersNextSequence(tx *bbolt.Tx) (uint64, error) {
|
||||
|
||||
// --- OAuth states ---
|
||||
|
||||
// GetOAuthState returns raw state bytes.
|
||||
// GetOAuthState returns the raw JSON for an OAuth state, or nil.
|
||||
func (s *Store) GetOAuthState(tx *bbolt.Tx, state string) []byte {
|
||||
b := tx.Bucket(BucketOAuthStates)
|
||||
if b == nil {
|
||||
@@ -409,7 +424,7 @@ func (s *Store) GetOAuthState(tx *bbolt.Tx, state string) []byte {
|
||||
return b.Get([]byte(state))
|
||||
}
|
||||
|
||||
// PutOAuthState stores state.
|
||||
// PutOAuthState stores an OAuth state entry.
|
||||
func (s *Store) PutOAuthState(tx *bbolt.Tx, state string, raw []byte) error {
|
||||
b, err := tx.CreateBucketIfNotExists(BucketOAuthStates)
|
||||
if err != nil {
|
||||
@@ -418,7 +433,7 @@ func (s *Store) PutOAuthState(tx *bbolt.Tx, state string, raw []byte) error {
|
||||
return b.Put([]byte(state), raw)
|
||||
}
|
||||
|
||||
// DeleteOAuthState removes state.
|
||||
// DeleteOAuthState removes an OAuth state entry.
|
||||
func (s *Store) DeleteOAuthState(tx *bbolt.Tx, state string) error {
|
||||
b := tx.Bucket(BucketOAuthStates)
|
||||
if b == nil {
|
||||
@@ -429,16 +444,15 @@ func (s *Store) DeleteOAuthState(tx *bbolt.Tx, state string) error {
|
||||
|
||||
// --- Bucket existence (for wipe) ---
|
||||
|
||||
// BucketExists returns true if the bucket exists.
|
||||
// BucketExists returns true if a top-level bucket with the given name exists.
|
||||
func (s *Store) BucketExists(tx *bbolt.Tx, name []byte) bool {
|
||||
return tx.Bucket(name) != nil
|
||||
}
|
||||
|
||||
// DeleteBucket removes a bucket.
|
||||
// DeleteBucket removes a top-level bucket (no-op if it doesn't exist).
|
||||
func (s *Store) DeleteBucket(tx *bbolt.Tx, name []byte) error {
|
||||
if tx.Bucket(name) == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(name)
|
||||
}
|
||||
|
||||
|
||||
532
internal/app/store/db_test.go
Normal file
532
internal/app/store/db_test.go
Normal file
@@ -0,0 +1,532 @@
|
||||
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.
|
||||
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
|
||||
})
|
||||
|
||||
// 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).
|
||||
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
|
||||
})
|
||||
|
||||
// Delete user.
|
||||
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.DeleteUser(tx, "alice")
|
||||
}); err != nil {
|
||||
t.Fatal(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
|
||||
})
|
||||
}
|
||||
|
||||
func TestForEachUser(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
st.PutUser(tx, "alice", []byte("1"))
|
||||
st.PutUser(tx, "bob", []byte("2"))
|
||||
return nil
|
||||
})
|
||||
|
||||
var names []string
|
||||
st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.ForEachUser(tx, func(k, _ []byte) error {
|
||||
names = append(names, string(k))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("expected 2 users, got %d", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCountEmptyBucket(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
st.PutMap(tx, 1, []byte("a"))
|
||||
st.PutMap(tx, 2, []byte("b"))
|
||||
return nil
|
||||
})
|
||||
|
||||
var count int
|
||||
st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.ForEachMap(tx, func(_, _ []byte) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
})
|
||||
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()
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
st.PutTile(tx, 1, 0, "0_0", []byte("a"))
|
||||
st.PutTile(tx, 1, 1, "0_0", []byte("b"))
|
||||
st.PutTile(tx, 2, 0, "1_1", []byte("c"))
|
||||
return nil
|
||||
})
|
||||
|
||||
var count int
|
||||
st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.ForEachTile(tx, func(_, _, _, _ []byte) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if count != 3 {
|
||||
t.Fatalf("expected 3 tiles, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTilesMapBucket(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTilesBucket(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
st.PutTile(tx, 1, 0, "0_0", []byte("a"))
|
||||
return st.DeleteTilesBucket(tx)
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarkerBuckets(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
if st.BucketExists(tx, store.BucketUsers) {
|
||||
t.Fatal("expected bucket to not exist")
|
||||
}
|
||||
st.PutUser(tx, "alice", []byte("x"))
|
||||
if !st.BucketExists(tx, store.BucketUsers) {
|
||||
t.Fatal("expected bucket to exist")
|
||||
}
|
||||
return st.DeleteBucket(tx, store.BucketUsers)
|
||||
})
|
||||
|
||||
st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
if st.BucketExists(tx, store.BucketUsers) {
|
||||
t.Fatal("expected bucket to be deleted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user