chore: build
This commit is contained in:
@@ -8,9 +8,14 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"backend/internal/modules/users"
|
"backend/internal/modules/users"
|
||||||
|
"backend/internal/platform/config"
|
||||||
"backend/internal/platform/security"
|
"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
|
// fakeRepository is an in-memory users.Repository used to unit-test
|
||||||
// users.Service without a real database.
|
// users.Service without a real database.
|
||||||
type fakeRepository struct {
|
type fakeRepository struct {
|
||||||
@@ -79,6 +84,16 @@ func (r *fakeRepository) Update(_ context.Context, u *users.User) error {
|
|||||||
return nil
|
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 {
|
func (r *fakeRepository) Delete(_ context.Context, id uuid.UUID) error {
|
||||||
u, ok := r.byID[id]
|
u, ok := r.byID[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -100,7 +115,7 @@ func (g fakeAccountsGate) CustomerAccountsAvailable(context.Context) (bool, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Create_HashesPasswordAndPersists(t *testing.T) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, err := svc.Create(ctx, "admin", "super-strong-password", users.RoleAdmin)
|
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) {
|
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 {
|
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")
|
t.Fatal("Create() error = nil, want error for an invalid role")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Create_DuplicateUsernameRejected(t *testing.T) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
if _, err := svc.Create(ctx, "dup-user", "super-strong-password", users.RoleAdmin); err != nil {
|
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) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, err := svc.Create(ctx, "user1", "first-strong-password", users.RoleAdmin)
|
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) {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, err := svc.Create(ctx, "todelete", "super-strong-password", users.RoleAdmin)
|
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) {
|
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) {
|
if err := svc.Delete(context.Background(), uuid.New()); !errors.Is(err, users.ErrNotFound) {
|
||||||
t.Fatalf("Delete() error = %v, want ErrNotFound", err)
|
t.Fatalf("Delete() error = %v, want ErrNotFound", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Create_CustomerRoleRejectedWhenAccountsDisabled(t *testing.T) {
|
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)
|
_, err := svc.Create(context.Background(), "some-customer", "super-strong-password", users.RoleCustomer)
|
||||||
if !errors.Is(err, users.ErrCustomerAccountsDisabled) {
|
if !errors.Is(err, users.ErrCustomerAccountsDisabled) {
|
||||||
t.Fatalf("Create() error = %v, want ErrCustomerAccountsDisabled", err)
|
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) {
|
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)
|
user, err := svc.Create(context.Background(), "some-customer", "super-strong-password", users.RoleCustomer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Create() error = %v", err)
|
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)
|
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 {
|
export class ApiError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
constructor(message: string, status: number) {
|
constructor(message: string, status: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user