package service import ( "context" "errors" "testing" "wucher/internal/domain/auth" ) type roleServiceRepoMock struct { createCalled bool updateCalled bool deleteCalled bool getByIDCalled bool getByNameCalled bool listCalled bool getPermByIDCalled bool getPermByKeyCalled bool updatePermCalled bool assignPermCalled bool removePermCalled bool listPermCalled bool lastCreate *auth.Role lastUpdate *auth.Role lastDelete []byte lastListFilter string lastListSort string lastListLimit int lastListOffset int lastGetPermID []byte lastGetPermKey string lastAssignRoleID []byte lastAssignPermissionID []byte lastRemoveRoleID []byte lastRemovePermissionID []byte lastPermFilter string lastPermSort string lastPermLimit int lastPermOffset int roleByID *auth.Role roleByName *auth.Role listRoles []auth.Role listTotal int64 permissionByID *auth.Permission permissionByKey *auth.Permission assignedPermission *auth.RolePermission listPermissions []auth.Permission listPermissionsTotal int64 createErr error updateErr error deleteErr error getByIDErr error getByNameErr error listErr error getPermByIDErr error getPermByKeyErr error updatePermErr error assignPermErr error removePermErr error listPermErr error } func (m *roleServiceRepoMock) CreateRole(_ context.Context, role *auth.Role) error { m.createCalled = true m.lastCreate = role return m.createErr } func (m *roleServiceRepoMock) UpdateRole(_ context.Context, role *auth.Role) error { m.updateCalled = true m.lastUpdate = role return m.updateErr } func (m *roleServiceRepoMock) DeleteRole(_ context.Context, id []byte) error { m.deleteCalled = true m.lastDelete = id return m.deleteErr } func (m *roleServiceRepoMock) GetRoleByID(_ context.Context, _ []byte) (*auth.Role, error) { m.getByIDCalled = true return m.roleByID, m.getByIDErr } func (m *roleServiceRepoMock) GetRoleByName(_ context.Context, _ string) (*auth.Role, error) { m.getByNameCalled = true return m.roleByName, m.getByNameErr } func (m *roleServiceRepoMock) ListRoles(_ context.Context, filterName string, sort string, limit, offset int) ([]auth.Role, int64, error) { m.listCalled = true m.lastListFilter = filterName m.lastListSort = sort m.lastListLimit = limit m.lastListOffset = offset return m.listRoles, m.listTotal, m.listErr } func (m *roleServiceRepoMock) GetPermissionByID(_ context.Context, id []byte) (*auth.Permission, error) { m.getPermByIDCalled = true m.lastGetPermID = id return m.permissionByID, m.getPermByIDErr } func (m *roleServiceRepoMock) GetPermissionByKey(_ context.Context, key string) (*auth.Permission, error) { m.getPermByKeyCalled = true m.lastGetPermKey = key return m.permissionByKey, m.getPermByKeyErr } func (m *roleServiceRepoMock) UpdatePermission(_ context.Context, permission *auth.Permission) error { m.updatePermCalled = true m.permissionByID = permission return m.updatePermErr } func (m *roleServiceRepoMock) ListPermissions(_ context.Context, filter, sort string, limit, offset int) ([]auth.Permission, int64, error) { m.listPermCalled = true m.lastPermFilter = filter m.lastPermSort = sort m.lastPermLimit = limit m.lastPermOffset = offset return m.listPermissions, m.listPermissionsTotal, m.listPermErr } func (m *roleServiceRepoMock) ListPermissionsByRoleID(_ context.Context, _ []byte) ([]auth.Permission, error) { return []auth.Permission{}, nil } func (m *roleServiceRepoMock) HasRolePermission(_ context.Context, _ []byte, _ string) (bool, error) { return true, nil } func (m *roleServiceRepoMock) AssignPermission(_ context.Context, roleID, permissionID []byte) (*auth.RolePermission, error) { m.assignPermCalled = true m.lastAssignRoleID = roleID m.lastAssignPermissionID = permissionID if m.assignedPermission == nil { m.assignedPermission = &auth.RolePermission{RoleID: roleID, PermissionID: permissionID} } return m.assignedPermission, m.assignPermErr } func (m *roleServiceRepoMock) RemovePermission(_ context.Context, roleID []byte, permissionID []byte) error { m.removePermCalled = true m.lastRemoveRoleID = roleID m.lastRemovePermissionID = permissionID return m.removePermErr } func TestRoleService_Create_Validation(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) if err := svc.Create(context.Background(), &auth.Role{Name: " "}); err == nil { t.Fatalf("expected validation error") } if repo.createCalled { t.Fatalf("expected repo.CreateRole not called") } } func TestRoleService_Create_RepoError(t *testing.T) { repo := &roleServiceRepoMock{createErr: errors.New("db error")} svc := NewRoleService(repo) err := svc.Create(context.Background(), &auth.Role{Name: "admin"}) if err == nil { t.Fatalf("expected error") } if !repo.createCalled { t.Fatalf("expected repo.CreateRole called") } } func TestRoleService_Update_Validation(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) if err := svc.Update(context.Background(), &auth.Role{Name: ""}); err == nil { t.Fatalf("expected validation error") } if repo.updateCalled { t.Fatalf("expected repo.UpdateRole not called") } } func TestRoleService_Update_Success(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) err := svc.Update(context.Background(), &auth.Role{Name: "admin"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !repo.updateCalled { t.Fatalf("expected repo.UpdateRole called") } } func TestRoleService_Delete_Success(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) id := []byte("id") err := svc.Delete(context.Background(), id) if err != nil { t.Fatalf("unexpected error: %v", err) } if !repo.deleteCalled { t.Fatalf("expected repo.DeleteRole called") } if string(repo.lastDelete) != string(id) { t.Fatalf("expected delete id passed to repo") } } func TestRoleService_GetByID_Success(t *testing.T) { repo := &roleServiceRepoMock{roleByID: &auth.Role{Name: "admin"}} svc := NewRoleService(repo) role, err := svc.GetByID(context.Background(), []byte("id")) if err != nil { t.Fatalf("unexpected error: %v", err) } if role == nil || role.Name != "admin" { t.Fatalf("unexpected role") } if !repo.getByIDCalled { t.Fatalf("expected repo.GetRoleByID called") } } func TestRoleService_GetByName_Success(t *testing.T) { repo := &roleServiceRepoMock{roleByName: &auth.Role{Name: "admin"}} svc := NewRoleService(repo) role, err := svc.GetByName(context.Background(), "admin") if err != nil { t.Fatalf("unexpected error: %v", err) } if role == nil || role.Name != "admin" { t.Fatalf("unexpected role") } if !repo.getByNameCalled { t.Fatalf("expected repo.GetRoleByName called") } } func TestRoleService_GetPermissionByID(t *testing.T) { t.Run("success", func(t *testing.T) { perm := &auth.Permission{Name: "Users Read", Key: "users.read"} repo := &roleServiceRepoMock{permissionByID: perm} svc := NewRoleService(repo) id := []byte("perm-id") got, err := svc.GetPermissionByID(context.Background(), id) if err != nil { t.Fatalf("unexpected error: %v", err) } if got == nil || got.Key != "users.read" { t.Fatalf("unexpected permission: %+v", got) } if !repo.getPermByIDCalled { t.Fatalf("expected repo.GetPermissionByID called") } if string(repo.lastGetPermID) != string(id) { t.Fatalf("expected id forwarded to repo") } }) t.Run("repo error", func(t *testing.T) { repo := &roleServiceRepoMock{getPermByIDErr: errors.New("db error")} svc := NewRoleService(repo) if _, err := svc.GetPermissionByID(context.Background(), []byte("perm-id")); err == nil { t.Fatalf("expected error") } }) } func TestRoleService_GetPermissionByKey(t *testing.T) { repo := &roleServiceRepoMock{ permissionByKey: &auth.Permission{Name: "Delete User", Key: "user.delete"}, } svc := NewRoleService(repo) got, err := svc.GetPermissionByKey(context.Background(), "user.delete") if err != nil { t.Fatalf("unexpected error: %v", err) } if got == nil || got.Key != "user.delete" { t.Fatalf("unexpected permission: %+v", got) } if !repo.getPermByKeyCalled { t.Fatalf("expected repo.GetPermissionByKey called") } if repo.lastGetPermKey != "user.delete" { t.Fatalf("expected key forwarded to repo") } } func TestRoleService_UpdatePermission(t *testing.T) { t.Run("validation", func(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) if err := svc.UpdatePermission(context.Background(), &auth.Permission{Name: " ", Key: "user.delete"}); err == nil { t.Fatalf("expected validation error for empty name") } if err := svc.UpdatePermission(context.Background(), &auth.Permission{Name: "Delete User", Key: " "}); err == nil { t.Fatalf("expected validation error for empty key") } if repo.updatePermCalled { t.Fatalf("expected repo.UpdatePermission not called") } }) t.Run("success", func(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) perm := &auth.Permission{Name: "Delete User", Key: "user.delete", RequiresPIN: true} if err := svc.UpdatePermission(context.Background(), perm); err != nil { t.Fatalf("unexpected error: %v", err) } if !repo.updatePermCalled { t.Fatalf("expected repo.UpdatePermission called") } if repo.permissionByID == nil || !repo.permissionByID.RequiresPIN { t.Fatalf("expected permission forwarded to repo") } }) } func TestRoleService_AssignPermission(t *testing.T) { t.Run("success", func(t *testing.T) { roleID := []byte("role-id") permID := []byte("perm-id") repo := &roleServiceRepoMock{} svc := NewRoleService(repo) rp, err := svc.AssignPermission(context.Background(), roleID, permID) if err != nil { t.Fatalf("unexpected error: %v", err) } if rp == nil { t.Fatalf("expected role permission") } if !repo.assignPermCalled { t.Fatalf("expected repo.AssignPermission called") } if string(repo.lastAssignRoleID) != string(roleID) || string(repo.lastAssignPermissionID) != string(permID) { t.Fatalf("expected roleID/permissionID forwarded to repo") } }) t.Run("repo error", func(t *testing.T) { repo := &roleServiceRepoMock{assignPermErr: errors.New("assign error")} svc := NewRoleService(repo) if _, err := svc.AssignPermission(context.Background(), []byte("role-id"), []byte("perm-id")); err == nil { t.Fatalf("expected error") } }) } func TestRoleService_RemovePermission(t *testing.T) { t.Run("success", func(t *testing.T) { roleID := []byte("role-id") permID := []byte("perm-id") repo := &roleServiceRepoMock{} svc := NewRoleService(repo) err := svc.RemovePermission(context.Background(), roleID, permID) if err != nil { t.Fatalf("unexpected error: %v", err) } if !repo.removePermCalled { t.Fatalf("expected repo.RemovePermission called") } if string(repo.lastRemoveRoleID) != string(roleID) || string(repo.lastRemovePermissionID) != string(permID) { t.Fatalf("expected roleID/permissionID forwarded to repo") } }) t.Run("repo error", func(t *testing.T) { repo := &roleServiceRepoMock{removePermErr: errors.New("remove error")} svc := NewRoleService(repo) if err := svc.RemovePermission(context.Background(), []byte("role-id"), []byte("perm-id")); err == nil { t.Fatalf("expected error") } }) } func TestRoleService_ListPermissions(t *testing.T) { t.Run("normalize sort and forward args", func(t *testing.T) { repo := &roleServiceRepoMock{ listPermissions: []auth.Permission{{Name: "Users Read", Key: "users.read"}}, listPermissionsTotal: 1, } svc := NewRoleService(repo) perms, total, err := svc.ListPermissions(context.Background(), "users", "-key", 10, 20) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(perms) != 1 || perms[0].Key != "users.read" || total != 1 { t.Fatalf("unexpected list result: perms=%+v total=%d", perms, total) } if !repo.listPermCalled { t.Fatalf("expected repo.ListPermissions called") } if repo.lastPermFilter != "users" { t.Fatalf("expected filter forwarded") } if repo.lastPermSort != "`key` DESC" { t.Fatalf("expected normalized permission sort, got %q", repo.lastPermSort) } if repo.lastPermLimit != 10 || repo.lastPermOffset != 20 { t.Fatalf("expected limit/offset forwarded") } }) t.Run("repo error", func(t *testing.T) { repo := &roleServiceRepoMock{listPermErr: errors.New("list error")} svc := NewRoleService(repo) if _, _, err := svc.ListPermissions(context.Background(), "", "bad", 10, 0); err == nil { t.Fatalf("expected error") } if repo.lastPermSort != "" { t.Fatalf("unexpected sort for invalid input, got %q", repo.lastPermSort) } }) } func TestRoleService_List_NormalizeSort(t *testing.T) { repo := &roleServiceRepoMock{} svc := NewRoleService(repo) _, _, err := svc.List(context.Background(), "adm", "-created_at", 10, 20) if err != nil { t.Fatalf("unexpected error: %v", err) } if !repo.listCalled { t.Fatalf("expected repo.ListRoles called") } if repo.lastListFilter != "adm" { t.Fatalf("expected filter passed to repo") } if repo.lastListSort != "created_at DESC" { t.Fatalf("expected normalized sort, got %q", repo.lastListSort) } if repo.lastListLimit != 10 || repo.lastListOffset != 20 { t.Fatalf("expected limit/offset passed to repo") } } func TestNormalizeSort(t *testing.T) { cases := map[string]string{ "name": "name ASC", "-name": "name DESC", "created_at": "created_at ASC", "-created_at": "created_at DESC", "updated_at": "updated_at ASC", "-updated_at": "updated_at DESC", "": "", "bad": "", } for in, want := range cases { if got := normalizeSort(in); got != want { t.Fatalf("normalizeSort(%q) = %q, want %q", in, got, want) } } } func TestNormalizePermissionSort(t *testing.T) { cases := map[string]string{ "name": "name ASC", "-name": "name DESC", "key": "`key` ASC", "-key": "`key` DESC", "created_at": "created_at ASC", "-created_at": "created_at DESC", "updated_at": "updated_at ASC", "-updated_at": "updated_at DESC", "": "", "bad": "", } for in, want := range cases { if got := normalizePermissionSort(in); got != want { t.Fatalf("normalizePermissionSort(%q) = %q, want %q", in, got, want) } } }