package services_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "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" "golang.org/x/crypto/bcrypt" ) func newTestDB(t *testing.T) *bbolt.DB { 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 db } func newTestAuth(t *testing.T) (*services.AuthService, *store.Store) { t.Helper() db := newTestDB(t) st := store.New(db) return services.NewAuthService(st), st } func createUser(t *testing.T, st *store.Store, username, password string, auths app.Auths) { t.Helper() hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost) if err != nil { t.Fatal(err) } u := app.User{Pass: hash, Auths: auths} raw, _ := json.Marshal(u) st.Update(context.Background(), func(tx *bbolt.Tx) error { return st.PutUser(tx, username, raw) }) } func TestSetupRequired_EmptyDB(t *testing.T) { auth, _ := newTestAuth(t) if !auth.SetupRequired(context.Background()) { t.Fatal("expected setup required on empty DB") } } func TestSetupRequired_WithUsers(t *testing.T) { auth, st := newTestAuth(t) createUser(t, st, "admin", "pass", app.Auths{app.AUTH_ADMIN}) if auth.SetupRequired(context.Background()) { t.Fatal("expected setup not required when users exist") } } func TestGetUser_ValidPassword(t *testing.T) { auth, st := newTestAuth(t) createUser(t, st, "alice", "secret", app.Auths{app.AUTH_MAP}) u := auth.GetUser(context.Background(), "alice", "secret") if u == nil { t.Fatal("expected user with correct password") } if !u.Auths.Has(app.AUTH_MAP) { t.Fatal("expected map auth") } } func TestGetUser_InvalidPassword(t *testing.T) { auth, st := newTestAuth(t) createUser(t, st, "alice", "secret", nil) u := auth.GetUser(context.Background(), "alice", "wrong") if u != nil { t.Fatal("expected nil with wrong password") } } func TestGetUser_NonExistent(t *testing.T) { auth, _ := newTestAuth(t) u := auth.GetUser(context.Background(), "ghost", "pass") if u != nil { t.Fatal("expected nil for non-existent user") } } func TestGetUserByUsername(t *testing.T) { auth, st := newTestAuth(t) createUser(t, st, "alice", "secret", app.Auths{app.AUTH_MAP}) u := auth.GetUserByUsername(context.Background(), "alice") if u == nil { t.Fatal("expected user") } } func TestGetUserByUsername_NonExistent(t *testing.T) { auth, _ := newTestAuth(t) u := auth.GetUserByUsername(context.Background(), "ghost") if u != nil { t.Fatal("expected nil") } } func TestCreateSession_GetSession(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP, app.AUTH_UPLOAD}) sid := auth.CreateSession(ctx, "alice", false) if sid == "" { t.Fatal("expected non-empty session ID") } req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: sid}) sess := auth.GetSession(ctx, req) if sess == nil { t.Fatal("expected session") } if sess.Username != "alice" { t.Fatalf("expected alice, got %s", sess.Username) } if !sess.Auths.Has(app.AUTH_MAP) { t.Fatal("expected map auth from user") } } func TestGetSession_NoCookie(t *testing.T) { auth, _ := newTestAuth(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess := auth.GetSession(context.Background(), req) if sess != nil { t.Fatal("expected nil session without cookie") } } func TestGetSession_InvalidSession(t *testing.T) { auth, _ := newTestAuth(t) req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "invalid"}) sess := auth.GetSession(context.Background(), req) if sess != nil { t.Fatal("expected nil for invalid session") } } func TestGetSession_TempAdmin(t *testing.T) { auth, _ := newTestAuth(t) ctx := context.Background() sid := auth.CreateSession(ctx, "temp", true) if sid == "" { t.Fatal("expected session ID") } req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: sid}) sess := auth.GetSession(ctx, req) if sess == nil { t.Fatal("expected session") } if !sess.TempAdmin { t.Fatal("expected temp admin") } if !sess.Auths.Has(app.AUTH_ADMIN) { t.Fatal("expected admin auth for temp admin") } } func TestDeleteSession(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", nil) sid := auth.CreateSession(ctx, "alice", false) sess := &app.Session{ID: sid, Username: "alice"} auth.DeleteSession(ctx, sess) req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: sid}) if auth.GetSession(ctx, req) != nil { t.Fatal("expected nil after deletion") } } func TestSetUserPassword(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "old", app.Auths{app.AUTH_MAP}) if err := auth.SetUserPassword(ctx, "alice", "new"); err != nil { t.Fatal(err) } u := auth.GetUser(ctx, "alice", "new") if u == nil { t.Fatal("expected user with new password") } if auth.GetUser(ctx, "alice", "old") != nil { t.Fatal("old password should not work") } } func TestSetUserPassword_EmptyIsNoop(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", nil) if err := auth.SetUserPassword(ctx, "alice", ""); err != nil { t.Fatal(err) } if auth.GetUser(ctx, "alice", "pass") == nil { t.Fatal("password should be unchanged") } } func TestGenerateTokenForUser(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD}) tokens := auth.GenerateTokenForUser(ctx, "alice") if len(tokens) != 1 { t.Fatalf("expected 1 token, got %d", len(tokens)) } tokens2 := auth.GenerateTokenForUser(ctx, "alice") if len(tokens2) != 2 { t.Fatalf("expected 2 tokens, got %d", len(tokens2)) } } func TestGetUserTokensAndPrefix(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD}) st.Update(ctx, func(tx *bbolt.Tx) error { return st.PutConfig(tx, "prefix", []byte("myprefix")) }) auth.GenerateTokenForUser(ctx, "alice") tokens, prefix := auth.GetUserTokensAndPrefix(ctx, "alice") if len(tokens) != 1 { t.Fatalf("expected 1 token, got %d", len(tokens)) } if prefix != "myprefix" { t.Fatalf("expected myprefix, got %s", prefix) } } func TestValidateClientToken_Valid(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD}) tokens := auth.GenerateTokenForUser(ctx, "alice") username, err := auth.ValidateClientToken(ctx, tokens[0]) if err != nil { t.Fatal(err) } if username != "alice" { t.Fatalf("expected alice, got %s", username) } } func TestValidateClientToken_Invalid(t *testing.T) { auth, _ := newTestAuth(t) _, err := auth.ValidateClientToken(context.Background(), "bad-token") if err == nil { t.Fatal("expected error for invalid token") } } func TestValidateClientToken_NoUploadPerm(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP}) st.Update(ctx, func(tx *bbolt.Tx) error { return st.PutToken(tx, "tok123", "alice") }) _, err := auth.ValidateClientToken(ctx, "tok123") if err == nil { t.Fatal("expected error without upload permission") } } func TestBootstrapAdmin_Success(t *testing.T) { auth, _ := newTestAuth(t) ctx := context.Background() u := auth.BootstrapAdmin(ctx, "admin", "bootstrap123", "bootstrap123") if u == nil { t.Fatal("expected user creation") } if !u.Auths.Has(app.AUTH_ADMIN) { t.Fatal("expected admin auth") } } func TestBootstrapAdmin_WrongUsername(t *testing.T) { auth, _ := newTestAuth(t) u := auth.BootstrapAdmin(context.Background(), "notadmin", "pass", "pass") if u != nil { t.Fatal("expected nil for non-admin username") } } func TestBootstrapAdmin_MismatchPassword(t *testing.T) { auth, _ := newTestAuth(t) u := auth.BootstrapAdmin(context.Background(), "admin", "pass", "different") if u != nil { t.Fatal("expected nil for mismatched password") } } func TestBootstrapAdmin_AlreadyExists(t *testing.T) { auth, st := newTestAuth(t) ctx := context.Background() createUser(t, st, "admin", "existing", app.Auths{app.AUTH_ADMIN}) u := auth.BootstrapAdmin(ctx, "admin", "pass", "pass") if u != nil { t.Fatal("expected nil when admin already exists") } }