Refactor frontend components and enhance API integration
- Updated frontend-nuxt.mdc to specify usage of composables for API calls. - Added new AuthCard and ConfirmModal components for improved UI consistency. - Introduced UserAvatar component for user profile display, replacing previous Gravatar implementation. - Implemented useFormSubmit composable for handling form submissions with loading and error states. - Enhanced vitest.config.ts to include coverage reporting for composables and components. - Removed deprecated useAdminApi and useAuth composables to streamline API interactions. - Updated login and setup pages to utilize new components and composables for better user experience.
This commit is contained in:
@@ -20,6 +20,7 @@ type AdminService struct {
|
||||
}
|
||||
|
||||
// NewAdminService creates an AdminService with the given store and map service.
|
||||
// Uses direct args (two dependencies) rather than a deps struct.
|
||||
func NewAdminService(st *store.Store, mapSvc *MapService) *AdminService {
|
||||
return &AdminService{st: st, mapSvc: mapSvc}
|
||||
}
|
||||
@@ -37,19 +38,21 @@ func (s *AdminService) ListUsers(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
|
||||
// GetUser returns a user's permissions by username.
|
||||
func (s *AdminService) GetUser(ctx context.Context, username string) (auths app.Auths, found bool) {
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
func (s *AdminService) GetUser(ctx context.Context, username string) (auths app.Auths, found bool, err error) {
|
||||
err = s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
var u app.User
|
||||
json.Unmarshal(raw, &u)
|
||||
if err := json.Unmarshal(raw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
auths = u.Auths
|
||||
found = true
|
||||
return nil
|
||||
})
|
||||
return auths, found
|
||||
return auths, found, err
|
||||
}
|
||||
|
||||
// CreateOrUpdateUser creates or updates a user.
|
||||
@@ -60,7 +63,9 @@ func (s *AdminService) CreateOrUpdateUser(ctx context.Context, username string,
|
||||
u := app.User{}
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
if err := json.Unmarshal(raw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if pass != "" {
|
||||
hash, e := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
@@ -88,7 +93,9 @@ func (s *AdminService) DeleteUser(ctx context.Context, username string) error {
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
if uRaw != nil {
|
||||
var u app.User
|
||||
json.Unmarshal(uRaw, &u)
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tok := range u.Tokens {
|
||||
s.st.DeleteToken(tx, tok)
|
||||
}
|
||||
@@ -140,7 +147,9 @@ func (s *AdminService) ListMaps(ctx context.Context) ([]app.MapInfo, error) {
|
||||
err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
return s.st.ForEachMap(tx, func(k, v []byte) error {
|
||||
mi := app.MapInfo{}
|
||||
json.Unmarshal(v, &mi)
|
||||
if err := json.Unmarshal(v, &mi); err != nil {
|
||||
return err
|
||||
}
|
||||
if id, err := strconv.Atoi(string(k)); err == nil {
|
||||
mi.ID = id
|
||||
}
|
||||
@@ -152,18 +161,23 @@ func (s *AdminService) ListMaps(ctx context.Context) ([]app.MapInfo, error) {
|
||||
}
|
||||
|
||||
// GetMap returns a map by ID.
|
||||
func (s *AdminService) GetMap(ctx context.Context, id int) (*app.MapInfo, bool) {
|
||||
func (s *AdminService) GetMap(ctx context.Context, id int) (*app.MapInfo, bool, error) {
|
||||
var mi *app.MapInfo
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetMap(tx, id)
|
||||
if raw != nil {
|
||||
mi = &app.MapInfo{}
|
||||
json.Unmarshal(raw, mi)
|
||||
mi.ID = id
|
||||
return json.Unmarshal(raw, mi)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return mi, mi != nil
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if mi != nil {
|
||||
mi.ID = id
|
||||
}
|
||||
return mi, mi != nil, nil
|
||||
}
|
||||
|
||||
// UpdateMap updates a map's name, hidden, and priority fields.
|
||||
@@ -172,7 +186,9 @@ func (s *AdminService) UpdateMap(ctx context.Context, id int, name string, hidde
|
||||
mi := app.MapInfo{}
|
||||
raw := s.st.GetMap(tx, id)
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &mi)
|
||||
if err := json.Unmarshal(raw, &mi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mi.ID = id
|
||||
mi.Name = name
|
||||
@@ -190,7 +206,9 @@ func (s *AdminService) ToggleMapHidden(ctx context.Context, id int) (*app.MapInf
|
||||
raw := s.st.GetMap(tx, id)
|
||||
mi = &app.MapInfo{}
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, mi)
|
||||
if err := json.Unmarshal(raw, mi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mi.ID = id
|
||||
mi.Hidden = !mi.Hidden
|
||||
@@ -340,7 +358,9 @@ func (s *AdminService) HideMarker(ctx context.Context, markerID string) error {
|
||||
return nil
|
||||
}
|
||||
m := app.Marker{}
|
||||
json.Unmarshal(raw, &m)
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Hidden = true
|
||||
raw, _ = json.Marshal(m)
|
||||
grid.Put(key, raw)
|
||||
|
||||
@@ -52,9 +52,9 @@ func TestAdminGetUser_Found(t *testing.T) {
|
||||
admin, st := newTestAdmin(t)
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP, app.AUTH_UPLOAD})
|
||||
|
||||
auths, found := admin.GetUser(context.Background(), "alice")
|
||||
if !found {
|
||||
t.Fatal("expected found")
|
||||
auths, found, err := admin.GetUser(context.Background(), "alice")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("expected found, err=%v", err)
|
||||
}
|
||||
if !auths.Has(app.AUTH_MAP) {
|
||||
t.Fatal("expected map auth")
|
||||
@@ -63,7 +63,10 @@ func TestAdminGetUser_Found(t *testing.T) {
|
||||
|
||||
func TestAdminGetUser_NotFound(t *testing.T) {
|
||||
admin, _ := newTestAdmin(t)
|
||||
_, found := admin.GetUser(context.Background(), "ghost")
|
||||
_, found, err := admin.GetUser(context.Background(), "ghost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found {
|
||||
t.Fatal("expected not found")
|
||||
}
|
||||
@@ -78,9 +81,9 @@ func TestCreateOrUpdateUser_New(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auths, found := admin.GetUser(ctx, "bob")
|
||||
if !found {
|
||||
t.Fatal("expected user to exist")
|
||||
auths, found, err := admin.GetUser(ctx, "bob")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("expected user to exist, err=%v", err)
|
||||
}
|
||||
if !auths.Has(app.AUTH_MAP) {
|
||||
t.Fatal("expected map auth")
|
||||
@@ -97,9 +100,9 @@ func TestCreateOrUpdateUser_Update(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auths, found := admin.GetUser(ctx, "alice")
|
||||
if !found {
|
||||
t.Fatal("expected user")
|
||||
auths, found, err := admin.GetUser(ctx, "alice")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("expected user, err=%v", err)
|
||||
}
|
||||
if !auths.Has(app.AUTH_ADMIN) {
|
||||
t.Fatal("expected admin auth after update")
|
||||
@@ -139,9 +142,9 @@ func TestDeleteUser(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, found := admin.GetUser(ctx, "alice")
|
||||
if found {
|
||||
t.Fatal("expected user to be deleted")
|
||||
_, found, err := admin.GetUser(ctx, "alice")
|
||||
if err != nil || found {
|
||||
t.Fatalf("expected user to be deleted, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,9 +213,9 @@ func TestMapCRUD(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mi, found := admin.GetMap(ctx, 1)
|
||||
if !found || mi == nil {
|
||||
t.Fatal("expected map")
|
||||
mi, found, err := admin.GetMap(ctx, 1)
|
||||
if err != nil || !found || mi == nil {
|
||||
t.Fatalf("expected map, err=%v", err)
|
||||
}
|
||||
if mi.Name != "world" {
|
||||
t.Fatalf("expected world, got %s", mi.Name)
|
||||
@@ -285,7 +288,10 @@ func TestWipe(t *testing.T) {
|
||||
|
||||
func TestGetMap_NotFound(t *testing.T) {
|
||||
admin, _ := newTestAdmin(t)
|
||||
_, found := admin.GetMap(context.Background(), 999)
|
||||
_, found, err := admin.GetMap(context.Background(), 999)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found {
|
||||
t.Fatal("expected not found")
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ type AuthService struct {
|
||||
}
|
||||
|
||||
// NewAuthService creates an AuthService with the given store.
|
||||
// Uses direct args (single dependency) rather than a deps struct.
|
||||
func NewAuthService(st *store.Store) *AuthService {
|
||||
return &AuthService{st: st}
|
||||
}
|
||||
@@ -121,12 +122,14 @@ func (s *AuthService) CreateSession(ctx context.Context, username string, tempAd
|
||||
// GetUser returns user if username/password match.
|
||||
func (s *AuthService) GetUser(ctx context.Context, username, pass string) *app.User {
|
||||
var u *app.User
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
if err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
json.Unmarshal(raw, &u)
|
||||
if err := json.Unmarshal(raw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Pass == nil {
|
||||
u = nil
|
||||
return nil
|
||||
@@ -136,20 +139,26 @@ func (s *AuthService) GetUser(ctx context.Context, username, pass string) *app.U
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// GetUserByUsername returns user without password check (for OAuth-only check).
|
||||
func (s *AuthService) GetUserByUsername(ctx context.Context, username string) *app.User {
|
||||
var u *app.User
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
if err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw != nil {
|
||||
json.Unmarshal(raw, &u)
|
||||
if err := json.Unmarshal(raw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -205,11 +214,13 @@ func GetBootstrapPassword() string {
|
||||
|
||||
// GetUserTokensAndPrefix returns tokens and config prefix for a user.
|
||||
func (s *AuthService) GetUserTokensAndPrefix(ctx context.Context, username string) (tokens []string, prefix string) {
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
_ = s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
if uRaw != nil {
|
||||
var u app.User
|
||||
json.Unmarshal(uRaw, &u)
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
tokens = u.Tokens
|
||||
}
|
||||
if p := s.st.GetConfig(tx, "prefix"); p != nil {
|
||||
@@ -232,7 +243,9 @@ func (s *AuthService) GenerateTokenForUser(ctx context.Context, username string)
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
u := app.User{}
|
||||
if uRaw != nil {
|
||||
json.Unmarshal(uRaw, &u)
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
u.Tokens = append(u.Tokens, token)
|
||||
tokens = u.Tokens
|
||||
@@ -252,7 +265,9 @@ func (s *AuthService) SetUserPassword(ctx context.Context, username, pass string
|
||||
uRaw := s.st.GetUser(tx, username)
|
||||
u := app.User{}
|
||||
if uRaw != nil {
|
||||
json.Unmarshal(uRaw, &u)
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -297,7 +312,9 @@ func (s *AuthService) ValidateClientToken(ctx context.Context, token string) (st
|
||||
return apperr.ErrUnauthorized
|
||||
}
|
||||
var u app.User
|
||||
json.Unmarshal(uRaw, &u)
|
||||
if err := json.Unmarshal(uRaw, &u); err != nil {
|
||||
return err
|
||||
}
|
||||
if !u.Auths.Has(app.AUTH_UPLOAD) {
|
||||
return apperr.ErrForbidden
|
||||
}
|
||||
@@ -401,7 +418,9 @@ func (s *AuthService) OAuthHandleCallback(ctx context.Context, provider, code, s
|
||||
if raw == nil {
|
||||
return apperr.ErrBadRequest
|
||||
}
|
||||
json.Unmarshal(raw, &st)
|
||||
if err := json.Unmarshal(raw, &st); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.st.DeleteOAuthState(tx, state)
|
||||
})
|
||||
if err != nil || st.Provider == "" {
|
||||
@@ -479,8 +498,8 @@ func (s *AuthService) findOrCreateOAuthUser(ctx context.Context, provider, sub,
|
||||
err := s.st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
_ = s.st.ForEachUser(tx, func(k, v []byte) error {
|
||||
user := app.User{}
|
||||
if json.Unmarshal(v, &user) != nil {
|
||||
return nil
|
||||
if err := json.Unmarshal(v, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
if user.OAuthLinks != nil && user.OAuthLinks[provider] == sub {
|
||||
username = string(k)
|
||||
@@ -491,7 +510,9 @@ func (s *AuthService) findOrCreateOAuthUser(ctx context.Context, provider, sub,
|
||||
raw := s.st.GetUser(tx, username)
|
||||
if raw != nil {
|
||||
user := app.User{}
|
||||
json.Unmarshal(raw, &user)
|
||||
if err := json.Unmarshal(raw, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
if user.OAuthLinks == nil {
|
||||
user.OAuthLinks = map[string]string{provider: sub}
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/apperr"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
@@ -63,7 +64,7 @@ func (s *ClientService) Locate(ctx context.Context, gridID string) (string, erro
|
||||
err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetGrid(tx, gridID)
|
||||
if raw == nil {
|
||||
return fmt.Errorf("grid not found")
|
||||
return apperr.ErrNotFound
|
||||
}
|
||||
cur := app.GridData{}
|
||||
if err := json.Unmarshal(raw, &cur); err != nil {
|
||||
@@ -110,7 +111,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
gridRaw := grids.Get([]byte(grid))
|
||||
if gridRaw != nil {
|
||||
gd := app.GridData{}
|
||||
json.Unmarshal(gridRaw, &gd)
|
||||
if err := json.Unmarshal(gridRaw, &gd); err != nil {
|
||||
return err
|
||||
}
|
||||
maps[gd.Map] = struct{ X, Y int }{gd.Coord.X - x, gd.Coord.Y - y}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +155,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
mi := app.MapInfo{}
|
||||
mraw := mapB.Get([]byte(strconv.Itoa(id)))
|
||||
if mraw != nil {
|
||||
json.Unmarshal(mraw, &mi)
|
||||
if err := json.Unmarshal(mraw, &mi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mi.Priority {
|
||||
mapid = id
|
||||
@@ -171,7 +176,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
for y, grid := range row {
|
||||
cur := app.GridData{}
|
||||
if curRaw := grids.Get([]byte(grid)); curRaw != nil {
|
||||
json.Unmarshal(curRaw, &cur)
|
||||
if err := json.Unmarshal(curRaw, &cur); err != nil {
|
||||
return err
|
||||
}
|
||||
if time.Now().After(cur.NextUpdate) {
|
||||
greq.GridRequests = append(greq.GridRequests, grid)
|
||||
}
|
||||
@@ -192,7 +199,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
if len(grup.Grids) >= 2 && len(grup.Grids[1]) >= 2 {
|
||||
if curRaw := grids.Get([]byte(grup.Grids[1][1])); curRaw != nil {
|
||||
cur := app.GridData{}
|
||||
json.Unmarshal(curRaw, &cur)
|
||||
if err := json.Unmarshal(curRaw, &cur); err != nil {
|
||||
return err
|
||||
}
|
||||
greq.Map = cur.Map
|
||||
greq.Coords = cur.Coord
|
||||
}
|
||||
@@ -200,7 +209,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
if len(maps) > 1 {
|
||||
grids.ForEach(func(k, v []byte) error {
|
||||
gd := app.GridData{}
|
||||
json.Unmarshal(v, &gd)
|
||||
if err := json.Unmarshal(v, &gd); err != nil {
|
||||
return err
|
||||
}
|
||||
if gd.Map == mapid {
|
||||
return nil
|
||||
}
|
||||
@@ -216,7 +227,9 @@ func (s *ClientService) ProcessGridUpdate(ctx context.Context, grup GridUpdate)
|
||||
}
|
||||
tileraw := zoom.Get([]byte(gd.Coord.Name()))
|
||||
if tileraw != nil {
|
||||
json.Unmarshal(tileraw, &td)
|
||||
if err := json.Unmarshal(tileraw, &td); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gd.Map = mapid
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package services_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func TestFixMultipartContentType_NeedsQuoting(t *testing.T) {
|
||||
@@ -30,3 +35,57 @@ func TestFixMultipartContentType_Normal(t *testing.T) {
|
||||
t.Fatalf("expected unchanged, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClientService(t *testing.T) (*services.ClientService, *store.Store) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
st := store.New(db)
|
||||
mapSvc := services.NewMapService(services.MapServiceDeps{
|
||||
Store: st,
|
||||
GridStorage: t.TempDir(),
|
||||
GridUpdates: &app.Topic[app.TileData]{},
|
||||
})
|
||||
client := services.NewClientService(services.ClientServiceDeps{
|
||||
Store: st,
|
||||
MapSvc: mapSvc,
|
||||
WithChars: func(fn func(chars map[string]app.Character)) { fn(map[string]app.Character{}) },
|
||||
})
|
||||
return client, st
|
||||
}
|
||||
|
||||
func TestClientLocate_Found(t *testing.T) {
|
||||
client, st := newTestClientService(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 {
|
||||
return st.PutGrid(tx, "g1", raw)
|
||||
})
|
||||
result, err := client.Locate(ctx, "g1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result != "1;2;3" {
|
||||
t.Fatalf("expected 1;2;3, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientLocate_NotFound(t *testing.T) {
|
||||
client, _ := newTestClientService(t)
|
||||
_, err := client.Locate(context.Background(), "ghost")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown grid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientProcessGridUpdate_EmptyGrids(t *testing.T) {
|
||||
client, _ := newTestClientService(t)
|
||||
ctx := context.Background()
|
||||
result, err := client.ProcessGridUpdate(ctx, services.GridUpdate{Grids: [][]string{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type ExportService struct {
|
||||
}
|
||||
|
||||
// NewExportService creates an ExportService with the given store and map service.
|
||||
// Uses direct args (two dependencies) rather than a deps struct.
|
||||
func NewExportService(st *store.Store, mapSvc *MapService) *ExportService {
|
||||
return &ExportService{st: st, mapSvc: mapSvc}
|
||||
}
|
||||
@@ -190,7 +191,9 @@ func (s *ExportService) Merge(ctx context.Context, zr *zip.Reader) error {
|
||||
gridRaw := grids.Get([]byte(gid))
|
||||
if gridRaw != nil {
|
||||
gd := app.GridData{}
|
||||
json.Unmarshal(gridRaw, &gd)
|
||||
if err := json.Unmarshal(gridRaw, &gd); err != nil {
|
||||
return err
|
||||
}
|
||||
ops = append(ops, TileOp{
|
||||
MapID: gd.Map,
|
||||
X: gd.Coord.X,
|
||||
@@ -265,7 +268,9 @@ func (s *ExportService) processMergeJSON(
|
||||
gridRaw := grids.Get([]byte(v))
|
||||
if gridRaw != nil {
|
||||
gd := app.GridData{}
|
||||
json.Unmarshal(gridRaw, &gd)
|
||||
if err := json.Unmarshal(gridRaw, &gd); err != nil {
|
||||
return err
|
||||
}
|
||||
existingMaps[gd.Map] = struct{ X, Y int }{gd.Coord.X - c.X, gd.Coord.Y - c.Y}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +306,9 @@ func (s *ExportService) processMergeJSON(
|
||||
mi := app.MapInfo{}
|
||||
mraw := mapB.Get([]byte(strconv.Itoa(id)))
|
||||
if mraw != nil {
|
||||
json.Unmarshal(mraw, &mi)
|
||||
if err := json.Unmarshal(mraw, &mi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mi.Priority {
|
||||
mapid = id
|
||||
@@ -333,7 +340,9 @@ func (s *ExportService) processMergeJSON(
|
||||
if len(existingMaps) > 1 {
|
||||
grids.ForEach(func(k, v []byte) error {
|
||||
gd := app.GridData{}
|
||||
json.Unmarshal(v, &gd)
|
||||
if err := json.Unmarshal(v, &gd); err != nil {
|
||||
return err
|
||||
}
|
||||
if gd.Map == mapid {
|
||||
return nil
|
||||
}
|
||||
@@ -349,7 +358,9 @@ func (s *ExportService) processMergeJSON(
|
||||
}
|
||||
tileraw := zoom.Get([]byte(gd.Coord.Name()))
|
||||
if tileraw != nil {
|
||||
json.Unmarshal(tileraw, &td)
|
||||
if err := json.Unmarshal(tileraw, &td); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gd.Map = mapid
|
||||
|
||||
64
internal/app/services/export_test.go
Normal file
64
internal/app/services/export_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package services_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func newTestExportService(t *testing.T) (*services.ExportService, *store.Store) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
st := store.New(db)
|
||||
mapSvc := services.NewMapService(services.MapServiceDeps{
|
||||
Store: st,
|
||||
GridStorage: t.TempDir(),
|
||||
GridUpdates: &app.Topic[app.TileData]{},
|
||||
})
|
||||
return services.NewExportService(st, mapSvc), st
|
||||
}
|
||||
|
||||
func TestExport_EmptyDB(t *testing.T) {
|
||||
export, _ := newTestExportService(t)
|
||||
var buf bytes.Buffer
|
||||
err := export.Export(context.Background(), &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
t.Fatal("expected non-empty zip output")
|
||||
}
|
||||
// ZIP magic number
|
||||
if buf.Len() < 4 || buf.Bytes()[0] != 0x50 || buf.Bytes()[1] != 0x4b {
|
||||
t.Fatal("expected valid zip header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExport_WithGrid(t *testing.T) {
|
||||
export, st := newTestExportService(t)
|
||||
ctx := context.Background()
|
||||
gd := app.GridData{ID: "g1", Map: 1, Coord: app.Coord{X: 0, Y: 0}}
|
||||
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.PutGrid(tx, "g1", gdRaw); err != nil {
|
||||
return err
|
||||
}
|
||||
return st.PutMap(tx, 1, miRaw)
|
||||
})
|
||||
var buf bytes.Buffer
|
||||
err := export.Export(ctx, &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if buf.Len() < 4 {
|
||||
t.Fatal("expected zip data")
|
||||
}
|
||||
}
|
||||
@@ -77,13 +77,17 @@ func (s *MapService) GetMarkers(ctx context.Context) ([]app.FrontendMarker, erro
|
||||
}
|
||||
return grid.ForEach(func(k, v []byte) error {
|
||||
marker := app.Marker{}
|
||||
json.Unmarshal(v, &marker)
|
||||
if err := json.Unmarshal(v, &marker); err != nil {
|
||||
return err
|
||||
}
|
||||
graw := grids.Get([]byte(marker.GridID))
|
||||
if graw == nil {
|
||||
return nil
|
||||
}
|
||||
g := app.GridData{}
|
||||
json.Unmarshal(graw, &g)
|
||||
if err := json.Unmarshal(graw, &g); err != nil {
|
||||
return err
|
||||
}
|
||||
markers = append(markers, app.FrontendMarker{
|
||||
Image: marker.Image,
|
||||
Hidden: marker.Hidden,
|
||||
@@ -111,7 +115,9 @@ func (s *MapService) GetMaps(ctx context.Context, showHidden bool) (map[int]*app
|
||||
return nil
|
||||
}
|
||||
mi := &app.MapInfo{}
|
||||
json.Unmarshal(v, mi)
|
||||
if err := json.Unmarshal(v, mi); err != nil {
|
||||
return err
|
||||
}
|
||||
if mi.Hidden && !showHidden {
|
||||
return nil
|
||||
}
|
||||
@@ -165,14 +171,16 @@ func (s *MapService) GetGrid(ctx context.Context, id string) (*app.GridData, err
|
||||
// GetTile returns a tile by map ID, coordinate, and zoom level.
|
||||
func (s *MapService) GetTile(ctx context.Context, mapID int, c app.Coord, zoom int) *app.TileData {
|
||||
var td *app.TileData
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
if err := s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
raw := s.st.GetTile(tx, mapID, zoom, c.Name())
|
||||
if raw != nil {
|
||||
td = &app.TileData{}
|
||||
json.Unmarshal(raw, td)
|
||||
return json.Unmarshal(raw, td)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return nil
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
@@ -259,7 +267,9 @@ func (s *MapService) RebuildZooms(ctx context.Context) error {
|
||||
}
|
||||
b.ForEach(func(k, v []byte) error {
|
||||
grid := app.GridData{}
|
||||
json.Unmarshal(v, &grid)
|
||||
if err := json.Unmarshal(v, &grid); err != nil {
|
||||
return err
|
||||
}
|
||||
needProcess[zoomproc{grid.Coord.Parent(), grid.Map}] = struct{}{}
|
||||
saveGrid[zoomproc{grid.Coord, grid.Map}] = grid.ID
|
||||
return nil
|
||||
@@ -318,7 +328,9 @@ func (s *MapService) GetAllTileCache(ctx context.Context) []TileCache {
|
||||
s.st.View(ctx, func(tx *bbolt.Tx) error {
|
||||
return s.st.ForEachTile(tx, func(mapK, zoomK, coordK, v []byte) error {
|
||||
td := app.TileData{}
|
||||
json.Unmarshal(v, &td)
|
||||
if err := json.Unmarshal(v, &td); err != nil {
|
||||
return err
|
||||
}
|
||||
cache = append(cache, TileCache{
|
||||
M: td.MapID,
|
||||
X: td.Coord.X,
|
||||
|
||||
Reference in New Issue
Block a user