chore: build
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user