chore: update test fixtures and frontend for UUID identity (refs #217)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s

Backend test fixtures:
- Replace auth0|xxx format with UUID in all test userId values
- Update admin tests for new id/userProfileId schema
- Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks
- Fix admin integration test supertest usage (app.server)

Frontend:
- AdminUser type: auth0Sub -> id + userProfileId
- admin.api.ts: all user management methods use userId (UUID) params
- useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations
- AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id
- Remove encodeURIComponent (UUIDs don't need encoding)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-16 10:21:18 -06:00
parent 3b1112a9fe
commit 754639c86d
20 changed files with 316 additions and 263 deletions

View File

@@ -48,7 +48,8 @@ describe('AdminUsersPage', () => {
mockUseAdminAccess.mockReturnValue({
isAdmin: true,
adminRecord: {
auth0Sub: 'auth0|123',
id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin@example.com',
role: 'admin',
createdAt: '2024-01-01',

View File

@@ -55,7 +55,8 @@ describe('useAdminAccess', () => {
mockAdminApi.verifyAccess.mockResolvedValue({
isAdmin: true,
adminRecord: {
auth0Sub: 'auth0|123',
id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin@example.com',
role: 'admin',
createdAt: '2024-01-01',

View File

@@ -42,7 +42,8 @@ describe('Admin user management hooks', () => {
it('should fetch admin users', async () => {
const mockAdmins = [
{
auth0Sub: 'auth0|123',
id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin1@example.com',
role: 'admin',
createdAt: '2024-01-01',
@@ -68,11 +69,12 @@ describe('Admin user management hooks', () => {
describe('useCreateAdmin', () => {
it('should create admin and show success toast', async () => {
const newAdmin = {
auth0Sub: 'auth0|456',
id: 'admin-uuid-456',
userProfileId: 'user-uuid-456',
email: 'newadmin@example.com',
role: 'admin',
createdAt: '2024-01-01',
createdBy: 'auth0|123',
createdBy: 'admin-uuid-123',
revokedAt: null,
updatedAt: '2024-01-01',
};
@@ -131,11 +133,11 @@ describe('Admin user management hooks', () => {
wrapper: createWrapper(),
});
result.current.mutate('auth0|123');
result.current.mutate('admin-uuid-123');
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('auth0|123');
expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('admin-uuid-123');
expect(toast.success).toHaveBeenCalledWith('Admin revoked successfully');
});
});
@@ -148,11 +150,11 @@ describe('Admin user management hooks', () => {
wrapper: createWrapper(),
});
result.current.mutate('auth0|123');
result.current.mutate('admin-uuid-123');
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('auth0|123');
expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('admin-uuid-123');
expect(toast.success).toHaveBeenCalledWith('Admin reinstated successfully');
});
});

View File

@@ -101,12 +101,12 @@ export const adminApi = {
return response.data;
},
revokeAdmin: async (auth0Sub: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${auth0Sub}/revoke`);
revokeAdmin: async (id: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${id}/revoke`);
},
reinstateAdmin: async (auth0Sub: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${auth0Sub}/reinstate`);
reinstateAdmin: async (id: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${id}/reinstate`);
},
// Audit logs
@@ -328,62 +328,62 @@ export const adminApi = {
return response.data;
},
get: async (auth0Sub: string): Promise<ManagedUser> => {
get: async (userId: string): Promise<ManagedUser> => {
const response = await apiClient.get<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}`
`/admin/users/${userId}`
);
return response.data;
},
getVehicles: async (auth0Sub: string): Promise<AdminUserVehiclesResponse> => {
getVehicles: async (userId: string): Promise<AdminUserVehiclesResponse> => {
const response = await apiClient.get<AdminUserVehiclesResponse>(
`/admin/users/${encodeURIComponent(auth0Sub)}/vehicles`
`/admin/users/${userId}/vehicles`
);
return response.data;
},
updateTier: async (auth0Sub: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
updateTier: async (userId: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/tier`,
`/admin/users/${userId}/tier`,
data
);
return response.data;
},
deactivate: async (auth0Sub: string, data?: DeactivateUserRequest): Promise<ManagedUser> => {
deactivate: async (userId: string, data?: DeactivateUserRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/deactivate`,
`/admin/users/${userId}/deactivate`,
data || {}
);
return response.data;
},
reactivate: async (auth0Sub: string): Promise<ManagedUser> => {
reactivate: async (userId: string): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/reactivate`
`/admin/users/${userId}/reactivate`
);
return response.data;
},
updateProfile: async (auth0Sub: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => {
updateProfile: async (userId: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/profile`,
`/admin/users/${userId}/profile`,
data
);
return response.data;
},
promoteToAdmin: async (auth0Sub: string, data?: PromoteToAdminRequest): Promise<AdminUser> => {
promoteToAdmin: async (userId: string, data?: PromoteToAdminRequest): Promise<AdminUser> => {
const response = await apiClient.patch<AdminUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/promote`,
`/admin/users/${userId}/promote`,
data || {}
);
return response.data;
},
hardDelete: async (auth0Sub: string, reason?: string): Promise<{ message: string }> => {
hardDelete: async (userId: string, reason?: string): Promise<{ message: string }> => {
const response = await apiClient.delete<{ message: string }>(
`/admin/users/${encodeURIComponent(auth0Sub)}`,
`/admin/users/${userId}`,
{ params: reason ? { reason } : undefined }
);
return response.data;

View File

@@ -51,7 +51,7 @@ export const useRevokeAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (auth0Sub: string) => adminApi.revokeAdmin(auth0Sub),
mutationFn: (id: string) => adminApi.revokeAdmin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] });
toast.success('Admin revoked successfully');
@@ -66,7 +66,7 @@ export const useReinstateAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (auth0Sub: string) => adminApi.reinstateAdmin(auth0Sub),
mutationFn: (id: string) => adminApi.reinstateAdmin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] });
toast.success('Admin reinstated successfully');

View File

@@ -29,8 +29,8 @@ interface ApiError {
export const userQueryKeys = {
all: ['admin-users'] as const,
list: (params: ListUsersParams) => [...userQueryKeys.all, 'list', params] as const,
detail: (auth0Sub: string) => [...userQueryKeys.all, 'detail', auth0Sub] as const,
vehicles: (auth0Sub: string) => [...userQueryKeys.all, 'vehicles', auth0Sub] as const,
detail: (userId: string) => [...userQueryKeys.all, 'detail', userId] as const,
vehicles: (userId: string) => [...userQueryKeys.all, 'vehicles', userId] as const,
};
// Query keys for admin stats
@@ -58,13 +58,13 @@ export const useUsers = (params: ListUsersParams = {}) => {
/**
* Hook to get a single user's details
*/
export const useUser = (auth0Sub: string) => {
export const useUser = (userId: string) => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: userQueryKeys.detail(auth0Sub),
queryFn: () => adminApi.users.get(auth0Sub),
enabled: isAuthenticated && !isLoading && !!auth0Sub,
queryKey: userQueryKeys.detail(userId),
queryFn: () => adminApi.users.get(userId),
enabled: isAuthenticated && !isLoading && !!userId,
staleTime: 2 * 60 * 1000,
gcTime: 5 * 60 * 1000,
retry: 1,
@@ -78,8 +78,8 @@ export const useUpdateUserTier = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserTierRequest }) =>
adminApi.users.updateTier(auth0Sub, data),
mutationFn: ({ userId, data }: { userId: string; data: UpdateUserTierRequest }) =>
adminApi.users.updateTier(userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('Subscription tier updated');
@@ -101,8 +101,8 @@ export const useDeactivateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: DeactivateUserRequest }) =>
adminApi.users.deactivate(auth0Sub, data),
mutationFn: ({ userId, data }: { userId: string; data?: DeactivateUserRequest }) =>
adminApi.users.deactivate(userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User deactivated');
@@ -124,7 +124,7 @@ export const useReactivateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (auth0Sub: string) => adminApi.users.reactivate(auth0Sub),
mutationFn: (userId: string) => adminApi.users.reactivate(userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User reactivated');
@@ -146,8 +146,8 @@ export const useUpdateUserProfile = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserProfileRequest }) =>
adminApi.users.updateProfile(auth0Sub, data),
mutationFn: ({ userId, data }: { userId: string; data: UpdateUserProfileRequest }) =>
adminApi.users.updateProfile(userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User profile updated');
@@ -169,8 +169,8 @@ export const usePromoteToAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: PromoteToAdminRequest }) =>
adminApi.users.promoteToAdmin(auth0Sub, data),
mutationFn: ({ userId, data }: { userId: string; data?: PromoteToAdminRequest }) =>
adminApi.users.promoteToAdmin(userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User promoted to admin');
@@ -192,8 +192,8 @@ export const useHardDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ auth0Sub, reason }: { auth0Sub: string; reason?: string }) =>
adminApi.users.hardDelete(auth0Sub, reason),
mutationFn: ({ userId, reason }: { userId: string; reason?: string }) =>
adminApi.users.hardDelete(userId, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User permanently deleted');
@@ -228,13 +228,13 @@ export const useAdminStats = () => {
/**
* Hook to get a user's vehicles (admin view - year, make, model only)
*/
export const useUserVehicles = (auth0Sub: string) => {
export const useUserVehicles = (userId: string) => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: userQueryKeys.vehicles(auth0Sub),
queryFn: () => adminApi.users.getVehicles(auth0Sub),
enabled: isAuthenticated && !isLoading && !!auth0Sub,
queryKey: userQueryKeys.vehicles(userId),
queryFn: () => adminApi.users.getVehicles(userId),
enabled: isAuthenticated && !isLoading && !!userId,
staleTime: 2 * 60 * 1000,
gcTime: 5 * 60 * 1000,
retry: 1,

View File

@@ -104,8 +104,8 @@ const VehicleCountBadge: React.FC<{ count: number; onClick?: () => void }> = ({
);
// Expandable vehicle list component
const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(auth0Sub);
const UserVehiclesList: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(userId);
if (!isOpen) return null;
@@ -215,7 +215,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
(newTier: SubscriptionTier) => {
if (selectedUser) {
updateTierMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { subscriptionTier: newTier } },
{ userId: selectedUser.id, data: { subscriptionTier: newTier } },
{
onSuccess: () => {
setShowTierPicker(false);
@@ -232,7 +232,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleDeactivate = useCallback(() => {
if (selectedUser) {
deactivateMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } },
{ userId: selectedUser.id, data: { reason: deactivateReason || undefined } },
{
onSuccess: () => {
setShowDeactivateConfirm(false);
@@ -247,7 +247,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleReactivate = useCallback(() => {
if (selectedUser) {
reactivateMutation.mutate(selectedUser.auth0Sub, {
reactivateMutation.mutate(selectedUser.id, {
onSuccess: () => {
setShowUserActions(false);
setSelectedUser(null);
@@ -276,7 +276,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
}
if (Object.keys(updates).length > 0) {
updateProfileMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: updates },
{ userId: selectedUser.id, data: updates },
{
onSuccess: () => {
setShowEditModal(false);
@@ -306,7 +306,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handlePromoteConfirm = useCallback(() => {
if (selectedUser) {
promoteToAdminMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } },
{ userId: selectedUser.id, data: { role: promoteRole } },
{
onSuccess: () => {
setShowPromoteModal(false);
@@ -332,7 +332,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleHardDeleteConfirm = useCallback(() => {
if (selectedUser && hardDeleteConfirmText === 'DELETE') {
hardDeleteMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined },
{ userId: selectedUser.id, reason: hardDeleteReason || undefined },
{
onSuccess: () => {
setShowHardDeleteModal(false);
@@ -509,7 +509,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
{users.length > 0 && (
<div className="space-y-3">
{users.map((user) => (
<GlassCard key={user.auth0Sub} padding="md">
<GlassCard key={user.id} padding="md">
<button
onClick={() => handleUserClick(user)}
className="w-full text-left min-h-[44px]"
@@ -526,7 +526,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
<VehicleCountBadge
count={user.vehicleCount}
onClick={user.vehicleCount > 0 ? () => setExpandedUserId(
expandedUserId === user.auth0Sub ? null : user.auth0Sub
expandedUserId === user.id ? null : user.id
) : undefined}
/>
<StatusBadge active={!user.deactivatedAt} />
@@ -543,8 +543,8 @@ export const AdminUsersMobileScreen: React.FC = () => {
</div>
</button>
<UserVehiclesList
auth0Sub={user.auth0Sub}
isOpen={expandedUserId === user.auth0Sub}
userId={user.id}
isOpen={expandedUserId === user.id}
/>
</GlassCard>
))}

View File

@@ -5,7 +5,8 @@
// Admin user types
export interface AdminUser {
auth0Sub: string;
id: string;
userProfileId: string;
email: string;
role: string;
createdAt: string;