chore: build
ci-api / test (push) Successful in 21m6s
ci-web / test (push) Successful in 11m27s

This commit is contained in:
Xor290
2026-09-20 13:42:46 +02:00
parent e004b4edf7
commit 178652ef14
2 changed files with 53 additions and 18 deletions
+53 -8
View File
@@ -8,9 +8,14 @@ import (
"github.com/google/uuid"
"backend/internal/modules/users"
"backend/internal/platform/config"
"backend/internal/platform/security"
)
// testSeedConfig lets the service create several admins in one test. The
// limit itself (ADMIN_NUMBER) is covered by TestService_Create_AdminLimit.
var testSeedConfig = config.SeedConfig{AdminNumber: 10}
// fakeRepository is an in-memory users.Repository used to unit-test
// users.Service without a real database.
type fakeRepository struct {
@@ -79,6 +84,16 @@ func (r *fakeRepository) Update(_ context.Context, u *users.User) error {
return nil
}
func (r *fakeRepository) CountByRole(_ context.Context, role string) (int64, error) {
var n int64
for _, u := range r.byID {
if u.Role == role {
n++
}
}
return n, nil
}
func (r *fakeRepository) Delete(_ context.Context, id uuid.UUID) error {
u, ok := r.byID[id]
if !ok {
@@ -100,7 +115,7 @@ func (g fakeAccountsGate) CustomerAccountsAvailable(context.Context) (bool, erro
}
func TestService_Create_HashesPasswordAndPersists(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
ctx := context.Background()
user, err := svc.Create(ctx, "admin", "super-strong-password", users.RoleAdmin)
@@ -118,14 +133,14 @@ func TestService_Create_HashesPasswordAndPersists(t *testing.T) {
}
func TestService_Create_RejectsInvalidRole(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
if _, err := svc.Create(context.Background(), "user-a", "super-strong-password", "superadmin"); err == nil {
t.Fatal("Create() error = nil, want error for an invalid role")
}
}
func TestService_Create_DuplicateUsernameRejected(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
ctx := context.Background()
if _, err := svc.Create(ctx, "dup-user", "super-strong-password", users.RoleAdmin); err != nil {
@@ -138,7 +153,7 @@ func TestService_Create_DuplicateUsernameRejected(t *testing.T) {
}
func TestService_SetPassword_ChangesHash(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
ctx := context.Background()
user, err := svc.Create(ctx, "user1", "first-strong-password", users.RoleAdmin)
@@ -169,7 +184,7 @@ func TestService_SetPassword_ChangesHash(t *testing.T) {
}
func TestService_Delete_RemovesUser(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
ctx := context.Background()
user, err := svc.Create(ctx, "todelete", "super-strong-password", users.RoleAdmin)
@@ -186,14 +201,14 @@ func TestService_Delete_RemovesUser(t *testing.T) {
}
func TestService_Delete_UnknownUserReturnsNotFound(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
if err := svc.Delete(context.Background(), uuid.New()); !errors.Is(err, users.ErrNotFound) {
t.Fatalf("Delete() error = %v, want ErrNotFound", err)
}
}
func TestService_Create_CustomerRoleRejectedWhenAccountsDisabled(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: false})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: false}, testSeedConfig)
_, err := svc.Create(context.Background(), "some-customer", "super-strong-password", users.RoleCustomer)
if !errors.Is(err, users.ErrCustomerAccountsDisabled) {
t.Fatalf("Create() error = %v, want ErrCustomerAccountsDisabled", err)
@@ -201,7 +216,7 @@ func TestService_Create_CustomerRoleRejectedWhenAccountsDisabled(t *testing.T) {
}
func TestService_Create_CustomerRoleAllowedWhenAccountsEnabled(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true})
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, testSeedConfig)
user, err := svc.Create(context.Background(), "some-customer", "super-strong-password", users.RoleCustomer)
if err != nil {
t.Fatalf("Create() error = %v", err)
@@ -210,3 +225,33 @@ func TestService_Create_CustomerRoleAllowedWhenAccountsEnabled(t *testing.T) {
t.Fatalf("Create() role = %q, want %q", user.Role, users.RoleCustomer)
}
}
func TestService_Create_AdminLimit(t *testing.T) {
ctx := context.Background()
cfg := config.SeedConfig{AdminNumber: 2}
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, cfg)
for _, name := range []string{"admin-a", "admin-b"} {
if _, err := svc.Create(ctx, name, "super-strong-password", users.RoleAdmin); err != nil {
t.Fatalf("Create(%q) error = %v, want nil while under the limit", name, err)
}
}
_, err := svc.Create(ctx, "admin-c", "super-strong-password", users.RoleAdmin)
if !errors.Is(err, users.ErrAdminLimitReached) {
t.Fatalf("Create() error = %v, want ErrAdminLimitReached once ADMIN_NUMBER admins exist", err)
}
// The limit only concerns admins: customers are not counted against it.
if _, err := svc.Create(ctx, "customer-a", "super-strong-password", users.RoleCustomer); err != nil {
t.Fatalf("Create(customer) error = %v, want nil (customer accounts are outside ADMIN_NUMBER)", err)
}
}
func TestService_Create_AdminLimitZeroRejectsEveryAdmin(t *testing.T) {
svc := users.NewService(newFakeRepository(), fakeAccountsGate{available: true}, config.SeedConfig{})
_, err := svc.Create(context.Background(), "admin", "super-strong-password", users.RoleAdmin)
if !errors.Is(err, users.ErrAdminLimitReached) {
t.Fatalf("Create() error = %v, want ErrAdminLimitReached when ADMIN_NUMBER is 0", err)
}
}
-10
View File
@@ -1,13 +1,3 @@
// Central fetch wrapper: the access token is kept in memory only (never
// localStorage/sessionStorage, per the auth design), and a 401 triggers a
// single silent refresh-and-retry via the httpOnly refresh cookie.
//
// Admin and customer sessions are two entirely separate spaces (separate
// cookies, separate JWT audience -- see backend/internal/modules/auth) so
// each gets its own token store and its own refresh endpoint here too: a
// customer's expired access token must never be "refreshed" against the
// admin endpoint (it would just fail, since that reads the admin cookie).
export class ApiError extends Error {
status: number;
constructor(message: string, status: number) {