Refactor Docker and Makefile configurations for improved build processes

- 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.
This commit is contained in:
2026-03-04 13:59:00 +03:00
parent fc42d86ca0
commit 761fbaed55
20 changed files with 352 additions and 165 deletions

View File

@@ -1,7 +1,10 @@
# Backend tools image: Go + golangci-lint for test, fmt, lint.
# Source is mounted at /hnh-map at run time via docker-compose.tools.yml.
# Source is mounted at /src at run time; this WORKDIR is only for build-time go mod download.
FROM golang:1.24-alpine
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
# v1.64+ required for Go 1.24 (export data format); see https://github.com/golangci/golangci-lint/issues/5225
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.0
WORKDIR /hnh-map
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download

View File

@@ -11,17 +11,23 @@ build:
test: test-backend test-frontend
test-backend:
$(TOOLS_COMPOSE) run --rm backend-tools go test ./...
$(TOOLS_COMPOSE) build backend-tools
$(TOOLS_COMPOSE) run --rm backend-tools sh -c "go mod download && go test ./..."
test-frontend:
$(TOOLS_COMPOSE) build frontend-tools
$(TOOLS_COMPOSE) run --rm frontend-tools sh -c "npm ci && npm test"
lint:
$(TOOLS_COMPOSE) run --rm backend-tools golangci-lint run
$(TOOLS_COMPOSE) build backend-tools
$(TOOLS_COMPOSE) build frontend-tools
$(TOOLS_COMPOSE) run --rm backend-tools sh -c "go mod download && golangci-lint run"
$(TOOLS_COMPOSE) run --rm frontend-tools sh -c "npm ci && npm run lint"
fmt:
$(TOOLS_COMPOSE) run --rm backend-tools go fmt ./...
$(TOOLS_COMPOSE) build backend-tools
$(TOOLS_COMPOSE) build frontend-tools
$(TOOLS_COMPOSE) run --rm backend-tools sh -c "go mod download && go fmt ./..."
$(TOOLS_COMPOSE) run --rm frontend-tools sh -c "npm ci && npm run format"
generate-frontend:

View File

@@ -1,13 +1,18 @@
# One-off tools: test, lint, fmt. Use with: docker compose -f docker-compose.tools.yml run --rm <service> <cmd>
# Source is mounted so commands run against current code.
# Backend: mount at /src so the image's /go module cache (from build) is not overwritten.
services:
backend-tools:
build:
context: .
dockerfile: Dockerfile.tools
working_dir: /src
environment:
GOPATH: /go
GOMODCACHE: /go/pkg/mod
volumes:
- .:/hnh-map
- .:/src
# Default command; override when running (e.g. go test ./..., golangci-lint run).
command: ["go", "test", "./..."]

View File

@@ -1,11 +1,14 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/consistent-type-imports': ['warn', { prefer: 'type-imports' }],
'@typescript-eslint/no-require-imports': 'off',
export default withNuxt(
{ ignores: ['eslint.config.mjs'] },
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/consistent-type-imports': ['warn', { prefer: 'type-imports' }],
'@typescript-eslint/no-require-imports': 'off',
},
},
})
)

View File

@@ -66,7 +66,7 @@ func (h *Handlers) clientLocate(rw http.ResponseWriter, req *http.Request) {
}
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(result))
_, _ = rw.Write([]byte(result))
}
func (h *Handlers) clientGridUpdate(rw http.ResponseWriter, req *http.Request) {
@@ -85,7 +85,7 @@ func (h *Handlers) clientGridUpdate(rw http.ResponseWriter, req *http.Request) {
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(result.Response)
_ = json.NewEncoder(rw).Encode(result.Response)
}
func (h *Handlers) clientGridUpload(rw http.ResponseWriter, req *http.Request) {

View File

@@ -64,9 +64,11 @@ func (env *testEnv) createUser(t *testing.T, username, password string, auths ap
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
u := app.User{Pass: hash, Auths: auths}
raw, _ := json.Marshal(u)
env.st.Update(context.Background(), func(tx *bbolt.Tx) error {
if err := env.st.Update(context.Background(), func(tx *bbolt.Tx) error {
return env.st.PutUser(tx, username, raw)
})
}); err != nil {
t.Fatal(err)
}
}
func (env *testEnv) loginSession(t *testing.T, username string) string {
@@ -90,7 +92,7 @@ func TestAPISetup_NoUsers(t *testing.T) {
}
var resp struct{ SetupRequired bool }
json.NewDecoder(rr.Body).Decode(&resp)
_ = json.NewDecoder(rr.Body).Decode(&resp)
if !resp.SetupRequired {
t.Fatal("expected setupRequired=true")
}
@@ -105,7 +107,7 @@ func TestAPISetup_WithUsers(t *testing.T) {
env.h.APISetup(rr, req)
var resp struct{ SetupRequired bool }
json.NewDecoder(rr.Body).Decode(&resp)
_ = json.NewDecoder(rr.Body).Decode(&resp)
if resp.SetupRequired {
t.Fatal("expected setupRequired=false with users")
}
@@ -196,7 +198,7 @@ func TestAPIMe_Authenticated(t *testing.T) {
Username string `json:"username"`
Auths []string `json:"auths"`
}
json.NewDecoder(rr.Body).Decode(&resp)
_ = json.NewDecoder(rr.Body).Decode(&resp)
if resp.Username != "alice" {
t.Fatalf("expected alice, got %s", resp.Username)
}
@@ -245,7 +247,7 @@ func TestAPIMeTokens_Authenticated(t *testing.T) {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var resp struct{ Tokens []string }
json.NewDecoder(rr.Body).Decode(&resp)
_ = json.NewDecoder(rr.Body).Decode(&resp)
if len(resp.Tokens) != 1 {
t.Fatalf("expected 1 token, got %d", len(resp.Tokens))
}
@@ -396,7 +398,7 @@ func TestAdminSettings(t *testing.T) {
DefaultHide bool `json:"defaultHide"`
Title string `json:"title"`
}
json.NewDecoder(rr3.Body).Decode(&resp)
_ = json.NewDecoder(rr3.Body).Decode(&resp)
if resp.Prefix != "pfx" || !resp.DefaultHide || resp.Title != "New Title" {
t.Fatalf("unexpected settings: %+v", resp)
}
@@ -493,7 +495,7 @@ func TestAPIGetChars_NoMarkersAuth(t *testing.T) {
t.Fatalf("expected 200, got %d", rr.Code)
}
var chars []interface{}
json.NewDecoder(rr.Body).Decode(&chars)
_ = json.NewDecoder(rr.Body).Decode(&chars)
if len(chars) != 0 {
t.Fatal("expected empty array without markers auth")
}
@@ -511,7 +513,7 @@ func TestAPIGetMarkers_NoMarkersAuth(t *testing.T) {
t.Fatalf("expected 200, got %d", rr.Code)
}
var markers []interface{}
json.NewDecoder(rr.Body).Decode(&markers)
_ = json.NewDecoder(rr.Body).Decode(&markers)
if len(markers) != 0 {
t.Fatal("expected empty array without markers auth")
}
@@ -583,7 +585,7 @@ func TestAdminUserByName(t *testing.T) {
Username string `json:"username"`
Auths []string `json:"auths"`
}
json.NewDecoder(rr.Body).Decode(&resp)
_ = json.NewDecoder(rr.Body).Decode(&resp)
if resp.Username != "bob" {
t.Fatalf("expected bob, got %s", resp.Username)
}
@@ -613,9 +615,11 @@ func TestClientRouter_Locate(t *testing.T) {
}
gd := app.GridData{ID: "g1", Map: 1, Coord: app.Coord{X: 2, Y: 3}}
raw, _ := json.Marshal(gd)
env.st.Update(ctx, func(tx *bbolt.Tx) error {
if err := env.st.Update(ctx, func(tx *bbolt.Tx) error {
return env.st.PutGrid(tx, "g1", raw)
})
}); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/client/"+tokens[0]+"/locate?gridID=g1", nil)
rr := httptest.NewRecorder()

View File

@@ -55,7 +55,7 @@ func (h *Handlers) WatchGridUpdates(rw http.ResponseWriter, req *http.Request) {
tileCache := []services.TileCache{}
raw, _ := json.Marshal(tileCache)
fmt.Fprint(rw, "data: ")
rw.Write(raw)
_, _ = rw.Write(raw)
fmt.Fprint(rw, "\n\n")
flusher.Flush()
@@ -93,13 +93,13 @@ func (h *Handlers) WatchGridUpdates(rw http.ResponseWriter, req *http.Request) {
}
fmt.Fprint(rw, "event: merge\n")
fmt.Fprint(rw, "data: ")
rw.Write(raw)
_, _ = rw.Write(raw)
fmt.Fprint(rw, "\n\n")
flusher.Flush()
case <-ticker.C:
raw, _ := json.Marshal(tileCache)
fmt.Fprint(rw, "data: ")
rw.Write(raw)
_, _ = rw.Write(raw)
fmt.Fprint(rw, "\n\n")
tileCache = tileCache[:0]
flusher.Flush()
@@ -152,7 +152,7 @@ func (h *Handlers) GridTile(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "image/png")
rw.Header().Set("Cache-Control", "private, max-age=3600")
rw.WriteHeader(http.StatusOK)
rw.Write(transparentPNG)
_, _ = rw.Write(transparentPNG)
return
}

View File

@@ -172,7 +172,9 @@ var migrations = []func(tx *bbolt.Tx) error{
if err != nil {
return err
}
users.Put(k, raw)
if err := users.Put(k, raw); err != nil {
return err
}
}
return nil
})

View File

@@ -26,7 +26,7 @@ func TestRunMigrations_FreshDB(t *testing.T) {
t.Fatalf("migrations failed on fresh DB: %v", err)
}
db.View(func(tx *bbolt.Tx) error {
if err := db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(store.BucketConfig)
if b == nil {
t.Fatal("expected config bucket after migrations")
@@ -40,13 +40,15 @@ func TestRunMigrations_FreshDB(t *testing.T) {
t.Fatalf("expected default title, got %s", title)
}
return nil
})
}); err != nil {
t.Fatal(err)
}
if tx, _ := db.Begin(false); tx != nil {
if tx.Bucket(store.BucketOAuthStates) == nil {
t.Fatal("expected oauth_states bucket after migrations")
}
tx.Rollback()
_ = tx.Rollback()
}
}
@@ -61,7 +63,7 @@ func TestRunMigrations_Idempotent(t *testing.T) {
t.Fatalf("second run failed: %v", err)
}
db.View(func(tx *bbolt.Tx) error {
if err := db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(store.BucketConfig)
if b == nil {
t.Fatal("expected config bucket")
@@ -71,7 +73,9 @@ func TestRunMigrations_Idempotent(t *testing.T) {
t.Fatal("expected version key")
}
return nil
})
}); err != nil {
t.Fatal(err)
}
}
func TestRunMigrations_SetsVersion(t *testing.T) {
@@ -81,11 +85,13 @@ func TestRunMigrations_SetsVersion(t *testing.T) {
}
var version string
db.View(func(tx *bbolt.Tx) error {
if err := db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(store.BucketConfig)
version = string(b.Get([]byte("version")))
return nil
})
}); err != nil {
t.Fatal(err)
}
if version == "" || version == "0" {
t.Fatalf("expected non-zero version, got %q", version)

View File

@@ -101,7 +101,9 @@ func (s *AdminService) DeleteUser(ctx context.Context, username string) error {
return err
}
for _, tok := range u.Tokens {
s.st.DeleteToken(tx, tok)
if err := s.st.DeleteToken(tx, tok); err != nil {
return err
}
}
}
return s.st.DeleteUser(tx, username)
@@ -129,17 +131,25 @@ func (s *AdminService) GetSettings(ctx context.Context) (prefix string, defaultH
func (s *AdminService) UpdateSettings(ctx context.Context, prefix *string, defaultHide *bool, title *string) error {
return s.st.Update(ctx, func(tx *bbolt.Tx) error {
if prefix != nil {
s.st.PutConfig(tx, "prefix", []byte(*prefix))
if err := s.st.PutConfig(tx, "prefix", []byte(*prefix)); err != nil {
return err
}
}
if defaultHide != nil {
if *defaultHide {
s.st.PutConfig(tx, "defaultHide", []byte("1"))
if err := s.st.PutConfig(tx, "defaultHide", []byte("1")); err != nil {
return err
}
} else {
s.st.DeleteConfig(tx, "defaultHide")
if err := s.st.DeleteConfig(tx, "defaultHide"); err != nil {
return err
}
}
}
if title != nil {
s.st.PutConfig(tx, "title", []byte(*title))
if err := s.st.PutConfig(tx, "title", []byte(*title)); err != nil {
return err
}
}
return nil
})
@@ -264,7 +274,9 @@ func (s *AdminService) WipeTile(ctx context.Context, mapid, x, y int) error {
return err
}
for _, id := range ids {
grids.Delete(id)
if err := grids.Delete(id); err != nil {
return err
}
}
return nil
}); err != nil {
@@ -310,7 +322,9 @@ func (s *AdminService) SetCoords(ctx context.Context, mapid, fx, fy, tx2, ty int
g.Coord.X += diff.X
g.Coord.Y += diff.Y
raw, _ := json.Marshal(g)
grids.Put(k, raw)
if err := grids.Put(k, raw); err != nil {
return err
}
}
return nil
}); err != nil {
@@ -367,7 +381,9 @@ func (s *AdminService) HideMarker(ctx context.Context, markerID string) error {
}
m.Hidden = true
raw, _ = json.Marshal(m)
grid.Put(key, raw)
if err := grid.Put(key, raw); err != nil {
return err
}
return nil
})
}

View File

@@ -234,7 +234,7 @@ func TestToggleMapHidden(t *testing.T) {
admin, _ := newTestAdmin(t)
ctx := context.Background()
admin.UpdateMap(ctx, 1, "world", false, false)
_ = admin.UpdateMap(ctx, 1, "world", false, false)
mi, err := admin.ToggleMapHidden(ctx, 1)
if err != nil {
@@ -257,19 +257,27 @@ func TestWipe(t *testing.T) {
admin, st := newTestAdmin(t)
ctx := context.Background()
st.Update(ctx, func(tx *bbolt.Tx) error {
st.PutGrid(tx, "g1", []byte("data"))
st.PutMap(tx, 1, []byte("data"))
st.PutTile(tx, 1, 0, "0_0", []byte("data"))
st.CreateMarkersBuckets(tx)
return nil
})
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.PutGrid(tx, "g1", []byte("data")); err != nil {
return err
}
if err := st.PutMap(tx, 1, []byte("data")); err != nil {
return err
}
if err := st.PutTile(tx, 1, 0, "0_0", []byte("data")); err != nil {
return err
}
_, _, err := st.CreateMarkersBuckets(tx)
return err
}); err != nil {
t.Fatal(err)
}
if err := admin.Wipe(ctx); err != nil {
t.Fatal(err)
}
st.View(ctx, func(tx *bbolt.Tx) error {
if err := st.View(ctx, func(tx *bbolt.Tx) error {
if st.GetGrid(tx, "g1") != nil {
t.Fatal("expected grids wiped")
}
@@ -283,7 +291,9 @@ func TestWipe(t *testing.T) {
t.Fatal("expected markers wiped")
}
return nil
})
}); err != nil {
t.Fatal(err)
}
}
func TestGetMap_NotFound(t *testing.T) {

View File

@@ -53,7 +53,7 @@ func (s *AuthService) GetSession(ctx context.Context, req *http.Request) *app.Se
return nil
}
var sess *app.Session
s.st.View(ctx, func(tx *bbolt.Tx) error {
if err := s.st.View(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetSession(tx, c.Value)
if raw == nil {
return nil
@@ -77,7 +77,9 @@ func (s *AuthService) GetSession(ctx context.Context, req *http.Request) *app.Se
}
sess.Auths = u.Auths
return nil
})
}); err != nil {
return nil
}
return sess
}
@@ -165,7 +167,7 @@ func (s *AuthService) GetUserByUsername(ctx context.Context, username string) *a
// SetupRequired returns true if no users exist (first run).
func (s *AuthService) SetupRequired(ctx context.Context) bool {
var required bool
s.st.View(ctx, func(tx *bbolt.Tx) error {
_ = s.st.View(ctx, func(tx *bbolt.Tx) error {
if s.st.UserCount(tx) == 0 {
required = true
}
@@ -181,7 +183,7 @@ func (s *AuthService) BootstrapAdmin(ctx context.Context, username, pass, bootst
}
var created bool
var u *app.User
s.st.Update(ctx, func(tx *bbolt.Tx) error {
if err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
if s.st.GetUser(tx, "admin") != nil {
return nil
}
@@ -200,7 +202,9 @@ func (s *AuthService) BootstrapAdmin(ctx context.Context, username, pass, bootst
created = true
u = &user
return nil
})
}); err != nil {
return nil
}
if created {
return u
}
@@ -239,7 +243,7 @@ func (s *AuthService) GenerateTokenForUser(ctx context.Context, username string)
}
token := hex.EncodeToString(tokenRaw)
var tokens []string
s.st.Update(ctx, func(tx *bbolt.Tx) error {
_ = s.st.Update(ctx, func(tx *bbolt.Tx) error {
uRaw := s.st.GetUser(tx, username)
u := app.User{}
if uRaw != nil {
@@ -250,7 +254,9 @@ func (s *AuthService) GenerateTokenForUser(ctx context.Context, username string)
u.Tokens = append(u.Tokens, token)
tokens = u.Tokens
buf, _ := json.Marshal(u)
s.st.PutUser(tx, username, buf)
if err := s.st.PutUser(tx, username, buf); err != nil {
return err
}
return s.st.PutToken(tx, token, username)
})
return tokens
@@ -522,7 +528,9 @@ func (s *AuthService) findOrCreateOAuthUser(ctx context.Context, provider, sub,
user.Email = email
}
raw, _ = json.Marshal(user)
s.st.PutUser(tx, username, raw)
if err := s.st.PutUser(tx, username, raw); err != nil {
return err
}
}
return nil
}

View File

@@ -41,9 +41,11 @@ func createUser(t *testing.T, st *store.Store, username, password string, auths
}
u := app.User{Pass: hash, Auths: auths}
raw, _ := json.Marshal(u)
st.Update(context.Background(), func(tx *bbolt.Tx) error {
if err := st.Update(context.Background(), func(tx *bbolt.Tx) error {
return st.PutUser(tx, username, raw)
})
}); err != nil {
t.Fatal(err)
}
}
func TestSetupRequired_EmptyDB(t *testing.T) {
@@ -246,9 +248,11 @@ func TestGetUserTokensAndPrefix(t *testing.T) {
ctx := context.Background()
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutConfig(tx, "prefix", []byte("myprefix"))
})
}); err != nil {
t.Fatal(err)
}
auth.GenerateTokenForUser(ctx, "alice")
tokens, prefix := auth.GetUserTokensAndPrefix(ctx, "alice")
@@ -288,9 +292,11 @@ func TestValidateClientToken_NoUploadPerm(t *testing.T) {
ctx := context.Background()
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP})
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutToken(tx, "tok123", "alice")
})
}); err != nil {
t.Fatal(err)
}
_, err := auth.ValidateClientToken(ctx, "tok123")
if err == nil {

View File

@@ -141,7 +141,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
if err := grids.Put([]byte(grid), raw); err != nil {
return err
}
greq.GridRequests = append(greq.GridRequests, grid)
}
}
@@ -192,7 +194,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
if err := grids.Put([]byte(grid), raw); err != nil {
return err
}
greq.GridRequests = append(greq.GridRequests, grid)
}
}
@@ -207,7 +211,7 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
}
}
if len(maps) > 1 {
grids.ForEach(func(k, v []byte) error {
if err := grids.ForEach(func(k, v []byte) error {
gd := app.GridData{}
if err := json.Unmarshal(v, &gd); err != nil {
return err
@@ -244,16 +248,22 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
File: td.File,
})
}
grids.Put(k, raw)
if err := grids.Put(k, raw); err != nil {
return err
}
}
return nil
})
}); err != nil {
return err
}
}
for mergeid, merge := range maps {
if mapid == mergeid {
continue
}
mapB.Delete([]byte(strconv.Itoa(mergeid)))
if err := mapB.Delete([]byte(strconv.Itoa(mergeid))); err != nil {
return err
}
slog.Info("reporting merge", "from", mergeid, "to", mapid)
s.mapSvc.ReportMerge(mergeid, mapid, app.Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
}
@@ -271,10 +281,10 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
func (s *ClientService) ProcessGridUpload(ctx context.Context, id string, extraData string, fileReader io.Reader) error {
if extraData != "" {
ed := ExtraData{}
json.Unmarshal([]byte(extraData), &ed)
_ = json.Unmarshal([]byte(extraData), &ed)
if ed.Season == 3 {
needTile := false
s.st.Update(ctx, func(tx *bbolt.Tx) error {
if err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetGrid(tx, id)
if raw == nil {
return fmt.Errorf("unknown grid id: %s", id)
@@ -301,7 +311,9 @@ func (s *ClientService) ProcessGridUpload(ctx context.Context, id string, extraD
}
raw, _ = json.Marshal(cur)
return s.st.PutGrid(tx, id, raw)
})
}); err != nil {
return err
}
if !needTile {
slog.Debug("ignoring tile upload: winter")
return nil
@@ -316,7 +328,7 @@ func (s *ClientService) ProcessGridUpload(ctx context.Context, id string, extraD
cur := app.GridData{}
mapid := 0
s.st.Update(ctx, func(tx *bbolt.Tx) error {
if err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
raw := s.st.GetGrid(tx, id)
if raw == nil {
return fmt.Errorf("unknown grid id: %s", id)
@@ -331,7 +343,9 @@ func (s *ClientService) ProcessGridUpload(ctx context.Context, id string, extraD
}
raw, _ = json.Marshal(cur)
return s.st.PutGrid(tx, id, raw)
})
}); err != nil {
return err
}
if updateTile {
gridDir := fmt.Sprintf("%s/grids", s.mapSvc.GridStorage())
@@ -374,7 +388,7 @@ func (s *ClientService) UpdatePositions(ctx context.Context, data []byte) error
}
gridDataByID := make(map[string]app.GridData)
s.st.View(ctx, func(tx *bbolt.Tx) error {
if err := s.st.View(ctx, func(tx *bbolt.Tx) error {
for _, craw := range craws {
raw := s.st.GetGrid(tx, craw.GridID)
if raw != nil {
@@ -385,7 +399,9 @@ func (s *ClientService) UpdatePositions(ctx context.Context, data []byte) error
}
}
return nil
})
}); err != nil {
return err
}
username, _ := ctx.Value(app.ClientUsernameKey).(string)
@@ -478,8 +494,12 @@ func (s *ClientService) UploadMarkers(ctx context.Context, data []byte) error {
Image: img,
}
raw, _ := json.Marshal(m)
grid.Put(key, raw)
idB.Put(idKey, key)
if err := grid.Put(key, raw); err != nil {
return err
}
if err := idB.Put(idKey, key); err != nil {
return err
}
}
return nil
})

View File

@@ -58,9 +58,11 @@ func TestClientLocate_Found(t *testing.T) {
ctx := context.Background()
gd := app.GridData{ID: "g1", Map: 1, Coord: app.Coord{X: 2, Y: 3}}
raw, _ := json.Marshal(gd)
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutGrid(tx, "g1", raw)
})
}); err != nil {
t.Fatal(err)
}
result, err := client.Locate(ctx, "g1")
if err != nil {
t.Fatal(err)

View File

@@ -111,7 +111,7 @@ func (s *ExportService) Export(ctx context.Context, w io.Writer) error {
if markersb != nil {
markersgrid := markersb.Bucket(store.BucketMarkersGrid)
if markersgrid != nil {
markersgrid.ForEach(func(k, v []byte) error {
if err := markersgrid.ForEach(func(k, v []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
@@ -125,7 +125,9 @@ func (s *ExportService) Export(ctx context.Context, w io.Writer) error {
maps[gridMap[marker.GridID]].Markers[marker.GridID] = append(maps[gridMap[marker.GridID]].Markers[marker.GridID], marker)
}
return nil
})
}); err != nil {
return err
}
}
}
return nil
@@ -218,7 +220,11 @@ func (s *ExportService) Merge(ctx context.Context, zr *zip.Reader) error {
f.Close()
return err
}
io.Copy(f, r)
if _, err := io.Copy(f, r); err != nil {
r.Close()
f.Close()
return err
}
r.Close()
f.Close()
newTiles[strings.TrimSuffix(filepath.Base(fhdr.Name), ".png")] = struct{}{}
@@ -290,8 +296,12 @@ func (s *ExportService) processMergeJSON(
Image: img,
}
raw, _ := json.Marshal(m)
mgrid.Put(key, raw)
idB.Put(idKey, key)
if err := mgrid.Put(key, raw); err != nil {
return err
}
if err := idB.Put(idKey, key); err != nil {
return err
}
}
}
@@ -333,7 +343,9 @@ func (s *ExportService) processMergeJSON(
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
if err := grids.Put([]byte(grid), raw); err != nil {
return err
}
}
return nil
}
@@ -372,11 +384,13 @@ func (s *ExportService) processMergeJSON(
if err != nil {
return err
}
grids.Put([]byte(grid), raw)
if err := grids.Put([]byte(grid), raw); err != nil {
return err
}
}
if len(existingMaps) > 1 {
grids.ForEach(func(k, v []byte) error {
if err := grids.ForEach(func(k, v []byte) error {
gd := app.GridData{}
if err := json.Unmarshal(v, &gd); err != nil {
return err
@@ -413,16 +427,22 @@ func (s *ExportService) processMergeJSON(
File: td.File,
})
}
grids.Put(k, raw)
if err := grids.Put(k, raw); err != nil {
return err
}
}
return nil
})
}); err != nil {
return err
}
}
for mergeid, merge := range existingMaps {
if mapid == mergeid {
continue
}
mapB.Delete([]byte(strconv.Itoa(mergeid)))
if err := mapB.Delete([]byte(strconv.Itoa(mergeid))); err != nil {
return err
}
slog.Info("reporting merge", "from", mergeid, "to", mapid)
s.mapSvc.ReportMerge(mergeid, mapid, app.Coord{X: offset.X - merge.X, Y: offset.Y - merge.Y})
}

View File

@@ -47,12 +47,14 @@ func TestExport_WithGrid(t *testing.T) {
gdRaw, _ := json.Marshal(gd)
mi := app.MapInfo{ID: 1, Name: "test", Hidden: false}
miRaw, _ := json.Marshal(mi)
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.PutGrid(tx, "g1", gdRaw); err != nil {
return err
}
return st.PutMap(tx, 1, miRaw)
})
}); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
err := export.Export(ctx, &buf)
if err != nil {

View File

@@ -218,7 +218,7 @@ func (s *MapService) getSubTiles(ctx context.Context, mapid int, c app.Coord, z
// SaveTile persists a tile and broadcasts the update.
func (s *MapService) SaveTile(ctx context.Context, mapid int, c app.Coord, z int, f string, t int64) {
s.st.Update(ctx, func(tx *bbolt.Tx) error {
_ = s.st.Update(ctx, func(tx *bbolt.Tx) error {
td := &app.TileData{
MapID: mapid,
Coord: c,
@@ -293,7 +293,7 @@ func (s *MapService) RebuildZooms(ctx context.Context) error {
if b == nil {
return nil
}
b.ForEach(func(k, v []byte) error {
if err := b.ForEach(func(k, v []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
@@ -306,8 +306,12 @@ func (s *MapService) RebuildZooms(ctx context.Context) error {
needProcess[zoomproc{grid.Coord.Parent(), grid.Map}] = struct{}{}
saveGrid[zoomproc{grid.Coord, grid.Map}] = grid.ID
return nil
})
tx.DeleteBucket(store.BucketTiles)
}); err != nil {
return err
}
if err := tx.DeleteBucket(store.BucketTiles); err != nil {
return err
}
return nil
}); err != nil {
slog.Error("RebuildZooms: failed to update store", "error", err)
@@ -364,7 +368,7 @@ func (s *MapService) WatchMerges() chan *app.Merge {
// GetAllTileCache returns all tiles for the initial SSE cache dump.
func (s *MapService) GetAllTileCache(ctx context.Context) []TileCache {
var cache []TileCache
s.st.View(ctx, func(tx *bbolt.Tx) error {
_ = s.st.View(ctx, func(tx *bbolt.Tx) error {
return s.st.ForEachTile(tx, func(mapK, zoomK, coordK, v []byte) error {
select {
case <-ctx.Done():

View File

@@ -57,9 +57,11 @@ func TestGetConfig(t *testing.T) {
svc, st := newTestMapService(t)
ctx := context.Background()
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutConfig(tx, "title", []byte("Test Map"))
})
}); err != nil {
t.Fatal(err)
}
config, err := svc.GetConfig(ctx, app.Auths{app.AUTH_MAP})
if err != nil {
@@ -93,9 +95,11 @@ func TestGetConfig_Empty(t *testing.T) {
func TestGetPage(t *testing.T) {
svc, st := newTestMapService(t)
ctx := context.Background()
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutConfig(tx, "title", []byte("Map Page"))
})
}); err != nil {
t.Fatal(err)
}
page, err := svc.GetPage(ctx)
if err != nil {
@@ -112,9 +116,11 @@ func TestGetGrid(t *testing.T) {
gd := app.GridData{ID: "g1", Map: 1, Coord: app.Coord{X: 5, Y: 10}}
raw, _ := json.Marshal(gd)
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutGrid(tx, "g1", raw)
})
}); err != nil {
t.Fatal(err)
}
got, err := svc.GetGrid(ctx, "g1")
if err != nil {
@@ -158,11 +164,17 @@ func TestGetMaps_HiddenFilter(t *testing.T) {
mi2 := app.MapInfo{ID: 2, Name: "hidden", Hidden: true}
raw1, _ := json.Marshal(mi1)
raw2, _ := json.Marshal(mi2)
st.Update(ctx, func(tx *bbolt.Tx) error {
st.PutMap(tx, 1, raw1)
st.PutMap(tx, 2, raw2)
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.PutMap(tx, 1, raw1); err != nil {
return err
}
if err := st.PutMap(tx, 2, raw2); err != nil {
return err
}
return nil
})
}); err != nil {
t.Fatal(err)
}
maps, err := svc.GetMaps(ctx, false)
if err != nil {
@@ -201,14 +213,18 @@ func TestGetMarkers_WithData(t *testing.T) {
m := app.Marker{Name: "Tower", ID: 1, GridID: "g1", Position: app.Position{X: 10, Y: 20}, Image: "gfx/terobjs/mm/tower"}
mRaw, _ := json.Marshal(m)
st.Update(ctx, func(tx *bbolt.Tx) error {
st.PutGrid(tx, "g1", gdRaw)
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.PutGrid(tx, "g1", gdRaw); err != nil {
return err
}
grid, _, err := st.CreateMarkersBuckets(tx)
if err != nil {
return err
}
return grid.Put([]byte("g1_10_20"), mRaw)
})
}); err != nil {
t.Fatal(err)
}
markers, err := svc.GetMarkers(ctx)
if err != nil {
@@ -233,9 +249,11 @@ func TestGetTile(t *testing.T) {
td := app.TileData{MapID: 1, Coord: app.Coord{X: 0, Y: 0}, Zoom: 0, File: "grids/g1.png", Cache: 12345}
raw, _ := json.Marshal(td)
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutTile(tx, 1, 0, "0_0", raw)
})
}); err != nil {
t.Fatal(err)
}
got := svc.GetTile(ctx, 1, app.Coord{X: 0, Y: 0}, 0)
if got == nil {
@@ -287,9 +305,11 @@ func TestGetAllTileCache_WithData(t *testing.T) {
td := app.TileData{MapID: 1, Coord: app.Coord{X: 1, Y: 2}, Zoom: 0, Cache: 999}
raw, _ := json.Marshal(td)
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutTile(tx, 1, 0, "1_2", raw)
})
}); err != nil {
t.Fatal(err)
}
cache := svc.GetAllTileCache(ctx)
if len(cache) != 1 {

View File

@@ -25,12 +25,14 @@ func TestUserCRUD(t *testing.T) {
ctx := context.Background()
// Verify user doesn't exist on empty DB.
st.View(ctx, func(tx *bbolt.Tx) error {
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 {
@@ -40,7 +42,7 @@ func TestUserCRUD(t *testing.T) {
}
// Verify user exists and count is correct (separate transaction for accurate Stats).
st.View(ctx, func(tx *bbolt.Tx) error {
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)
@@ -49,7 +51,9 @@ func TestUserCRUD(t *testing.T) {
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 {
@@ -58,31 +62,41 @@ func TestUserCRUD(t *testing.T) {
t.Fatal(err)
}
st.View(ctx, func(tx *bbolt.Tx) error {
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()
st.Update(ctx, func(tx *bbolt.Tx) error {
st.PutUser(tx, "alice", []byte("1"))
st.PutUser(tx, "bob", []byte("2"))
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
st.View(ctx, func(tx *bbolt.Tx) error {
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))
}
@@ -91,12 +105,14 @@ func TestForEachUser(t *testing.T) {
func TestUserCountEmptyBucket(t *testing.T) {
st := newTestStore(t)
ctx := context.Background()
st.View(ctx, func(tx *bbolt.Tx) error {
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) {
@@ -211,19 +227,27 @@ 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"))
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
st.View(ctx, func(tx *bbolt.Tx) error {
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)
}
@@ -290,20 +314,30 @@ 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"))
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
st.View(ctx, func(tx *bbolt.Tx) error {
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)
}
@@ -313,7 +347,7 @@ func TestTilesMapBucket(t *testing.T) {
st := newTestStore(t)
ctx := context.Background()
st.Update(ctx, func(tx *bbolt.Tx) error {
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")
}
@@ -328,31 +362,39 @@ func TestTilesMapBucket(t *testing.T) {
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()
st.Update(ctx, func(tx *bbolt.Tx) error {
st.PutTile(tx, 1, 0, "0_0", []byte("a"))
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)
}
st.View(ctx, func(tx *bbolt.Tx) error {
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()
st.Update(ctx, func(tx *bbolt.Tx) error {
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")
}
@@ -376,7 +418,9 @@ func TestMarkerBuckets(t *testing.T) {
t.Fatal("expected non-zero sequence")
}
return nil
})
}); err != nil {
t.Fatal(err)
}
}
func TestOAuthStateCRUD(t *testing.T) {
@@ -408,23 +452,29 @@ func TestBucketExistsAndDelete(t *testing.T) {
st := newTestStore(t)
ctx := context.Background()
st.Update(ctx, func(tx *bbolt.Tx) error {
if err := 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 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)
}
st.View(ctx, func(tx *bbolt.Tx) error {
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) {