@htlkg/data 0.0.21 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/index.d.ts +601 -94
- package/dist/hooks/index.js +682 -73
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +691 -82
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.js +4 -4
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.js +5 -5
- package/dist/queries/index.js.map +1 -1
- package/package.json +11 -12
- package/src/hooks/accounts/index.ts +2 -0
- package/src/hooks/{useAccounts.ts → accounts/useAccounts.ts} +48 -5
- package/src/hooks/accounts/usePaginatedAccounts.ts +166 -0
- package/src/hooks/brands/index.ts +2 -0
- package/src/hooks/{useBrands.ts → brands/useBrands.ts} +1 -1
- package/src/hooks/brands/usePaginatedBrands.ts +206 -0
- package/src/hooks/createPaginatedDataHook.ts +359 -0
- package/src/hooks/data-hook-errors.property.test.ts +4 -4
- package/src/hooks/data-hook-filters.property.test.ts +4 -4
- package/src/hooks/data-hooks.property.test.ts +4 -4
- package/src/hooks/index.ts +96 -8
- package/src/hooks/productInstances/index.ts +1 -0
- package/src/hooks/{useProductInstances.ts → productInstances/useProductInstances.ts} +9 -6
- package/src/hooks/products/index.ts +1 -0
- package/src/hooks/{useProducts.ts → products/useProducts.ts} +4 -5
- package/src/hooks/reservations/index.ts +2 -0
- package/src/hooks/reservations/usePaginatedReservations.ts +258 -0
- package/src/hooks/{useReservations.ts → reservations/useReservations.ts} +65 -10
- package/src/hooks/users/index.ts +2 -0
- package/src/hooks/users/usePaginatedUsers.ts +213 -0
- package/src/hooks/{useUsers.ts → users/useUsers.ts} +1 -1
- package/src/mutations/accounts/accounts.test.ts +287 -0
- package/src/mutations/{accounts.ts → accounts/accounts.ts} +2 -2
- package/src/mutations/accounts/index.ts +1 -0
- package/src/mutations/brands/brands.test.ts +292 -0
- package/src/mutations/{brands.ts → brands/brands.ts} +2 -2
- package/src/mutations/brands/index.ts +1 -0
- package/src/mutations/reservations/index.ts +1 -0
- package/src/mutations/{reservations.test.ts → reservations/reservations.test.ts} +1 -1
- package/src/mutations/{reservations.ts → reservations/reservations.ts} +2 -2
- package/src/mutations/users/index.ts +1 -0
- package/src/mutations/users/users.test.ts +289 -0
- package/src/mutations/{users.ts → users/users.ts} +2 -2
- package/src/queries/accounts/accounts.test.ts +228 -0
- package/src/queries/accounts/index.ts +1 -0
- package/src/queries/brands/brands.test.ts +288 -0
- package/src/queries/brands/index.ts +1 -0
- package/src/queries/products/index.ts +1 -0
- package/src/queries/products/products.test.ts +347 -0
- package/src/queries/reservations/index.ts +1 -0
- package/src/queries/users/index.ts +1 -0
- package/src/queries/users/users.test.ts +301 -0
- /package/src/queries/{accounts.ts → accounts/accounts.ts} +0 -0
- /package/src/queries/{brands.ts → brands/brands.ts} +0 -0
- /package/src/queries/{products.ts → products/products.ts} +0 -0
- /package/src/queries/{reservations.test.ts → reservations/reservations.test.ts} +0 -0
- /package/src/queries/{reservations.ts → reservations/reservations.ts} +0 -0
- /package/src/queries/{users.ts → users/users.ts} +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand Mutation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for brand CRUD operations including soft delete and restoration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
createBrand,
|
|
10
|
+
updateBrand,
|
|
11
|
+
softDeleteBrand,
|
|
12
|
+
restoreBrand,
|
|
13
|
+
deleteBrand,
|
|
14
|
+
} from "./brands";
|
|
15
|
+
|
|
16
|
+
// Mock the systemSettings query functions
|
|
17
|
+
vi.mock("../../queries/systemSettings", () => ({
|
|
18
|
+
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
19
|
+
if (!deletedAt) {
|
|
20
|
+
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
21
|
+
}
|
|
22
|
+
const deletedDate = new Date(deletedAt);
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const diffMs = now.getTime() - deletedDate.getTime();
|
|
25
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
26
|
+
|
|
27
|
+
if (diffDays > retentionDays) {
|
|
28
|
+
return {
|
|
29
|
+
canRestore: false,
|
|
30
|
+
daysRemaining: 0,
|
|
31
|
+
daysExpired: Math.floor(diffDays - retentionDays),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
canRestore: true,
|
|
37
|
+
daysRemaining: Math.ceil(retentionDays - diffDays),
|
|
38
|
+
daysExpired: 0,
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS: 30,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("Brand Mutations", () => {
|
|
45
|
+
describe("createBrand", () => {
|
|
46
|
+
const validInput = {
|
|
47
|
+
accountId: "account-123",
|
|
48
|
+
name: "Test Brand",
|
|
49
|
+
timezone: "America/New_York",
|
|
50
|
+
status: "active" as const,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
it("should create a brand with valid input", async () => {
|
|
54
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
55
|
+
data: { id: "brand-001", ...validInput },
|
|
56
|
+
errors: null,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const mockClient = {
|
|
60
|
+
models: {
|
|
61
|
+
Brand: { create: mockCreate },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const result = await createBrand(mockClient, validInput);
|
|
66
|
+
|
|
67
|
+
expect(result).toBeTruthy();
|
|
68
|
+
expect(result?.id).toBe("brand-001");
|
|
69
|
+
expect(mockCreate).toHaveBeenCalledWith(validInput);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
73
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
74
|
+
data: null,
|
|
75
|
+
errors: [{ message: "Validation error" }],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mockClient = {
|
|
79
|
+
models: {
|
|
80
|
+
Brand: { create: mockCreate },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await createBrand(mockClient, validInput);
|
|
85
|
+
|
|
86
|
+
expect(result).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should throw on unexpected error", async () => {
|
|
90
|
+
const mockCreate = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
91
|
+
|
|
92
|
+
const mockClient = {
|
|
93
|
+
models: {
|
|
94
|
+
Brand: { create: mockCreate },
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await expect(createBrand(mockClient, validInput)).rejects.toThrow("Network error");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("updateBrand", () => {
|
|
103
|
+
it("should update a brand", async () => {
|
|
104
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
105
|
+
data: { id: "brand-001", name: "Updated Brand" },
|
|
106
|
+
errors: null,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mockClient = {
|
|
110
|
+
models: {
|
|
111
|
+
Brand: { update: mockUpdate },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await updateBrand(mockClient, {
|
|
116
|
+
id: "brand-001",
|
|
117
|
+
name: "Updated Brand",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result).toBeTruthy();
|
|
121
|
+
expect(result?.name).toBe("Updated Brand");
|
|
122
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
123
|
+
id: "brand-001",
|
|
124
|
+
name: "Updated Brand",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should return null on GraphQL error", async () => {
|
|
129
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
130
|
+
data: null,
|
|
131
|
+
errors: [{ message: "Update failed" }],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const mockClient = {
|
|
135
|
+
models: {
|
|
136
|
+
Brand: { update: mockUpdate },
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = await updateBrand(mockClient, { id: "brand-001" });
|
|
141
|
+
|
|
142
|
+
expect(result).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("softDeleteBrand", () => {
|
|
147
|
+
it("should set deletedAt and deletedBy", async () => {
|
|
148
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
149
|
+
data: { id: "brand-001" },
|
|
150
|
+
errors: null,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const mockClient = {
|
|
154
|
+
models: {
|
|
155
|
+
Brand: { update: mockUpdate },
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const result = await softDeleteBrand(mockClient, "brand-001", "admin@example.com");
|
|
160
|
+
|
|
161
|
+
expect(result).toBe(true);
|
|
162
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
163
|
+
expect(callArgs.id).toBe("brand-001");
|
|
164
|
+
expect(callArgs.status).toBe("deleted");
|
|
165
|
+
expect(callArgs.deletedAt).toBeDefined();
|
|
166
|
+
expect(callArgs.deletedBy).toBe("admin@example.com");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should return false on GraphQL error", async () => {
|
|
170
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
171
|
+
data: null,
|
|
172
|
+
errors: [{ message: "Error" }],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const mockClient = {
|
|
176
|
+
models: {
|
|
177
|
+
Brand: { update: mockUpdate },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const result = await softDeleteBrand(mockClient, "brand-001", "admin@example.com");
|
|
182
|
+
|
|
183
|
+
expect(result).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("restoreBrand", () => {
|
|
188
|
+
it("should restore a recently deleted brand", async () => {
|
|
189
|
+
const recentDeletedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString();
|
|
190
|
+
|
|
191
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
192
|
+
data: { id: "brand-001", deletedAt: recentDeletedAt },
|
|
193
|
+
errors: null,
|
|
194
|
+
});
|
|
195
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
196
|
+
data: { id: "brand-001" },
|
|
197
|
+
errors: null,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const mockClient = {
|
|
201
|
+
models: {
|
|
202
|
+
Brand: { get: mockGet, update: mockUpdate },
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = await restoreBrand(mockClient, "brand-001");
|
|
207
|
+
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
210
|
+
id: "brand-001",
|
|
211
|
+
status: "active",
|
|
212
|
+
deletedAt: null,
|
|
213
|
+
deletedBy: null,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should reject restoration after retention period expires", async () => {
|
|
218
|
+
const oldDeletedAt = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
219
|
+
|
|
220
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
221
|
+
data: { id: "brand-001", deletedAt: oldDeletedAt },
|
|
222
|
+
errors: null,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const mockClient = {
|
|
226
|
+
models: {
|
|
227
|
+
Brand: { get: mockGet, update: vi.fn() },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const result = await restoreBrand(mockClient, "brand-001", 30);
|
|
232
|
+
|
|
233
|
+
expect(result.success).toBe(false);
|
|
234
|
+
expect(result.error).toContain("Retention period");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should return error when brand not found", async () => {
|
|
238
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
239
|
+
data: null,
|
|
240
|
+
errors: [{ message: "Not found" }],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const mockClient = {
|
|
244
|
+
models: {
|
|
245
|
+
Brand: { get: mockGet, update: vi.fn() },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const result = await restoreBrand(mockClient, "nonexistent");
|
|
250
|
+
|
|
251
|
+
expect(result.success).toBe(false);
|
|
252
|
+
expect(result.error).toBe("Brand not found");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("deleteBrand", () => {
|
|
257
|
+
it("should permanently delete a brand", async () => {
|
|
258
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
259
|
+
data: { id: "brand-001" },
|
|
260
|
+
errors: null,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const mockClient = {
|
|
264
|
+
models: {
|
|
265
|
+
Brand: { delete: mockDelete },
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await deleteBrand(mockClient, "brand-001");
|
|
270
|
+
|
|
271
|
+
expect(result).toBe(true);
|
|
272
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: "brand-001" });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should return false on GraphQL error", async () => {
|
|
276
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
277
|
+
data: null,
|
|
278
|
+
errors: [{ message: "Cannot delete" }],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const mockClient = {
|
|
282
|
+
models: {
|
|
283
|
+
Brand: { delete: mockDelete },
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const result = await deleteBrand(mockClient, "brand-001");
|
|
288
|
+
|
|
289
|
+
expect(result).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -8,8 +8,8 @@ import type { Brand } from "@htlkg/core/types";
|
|
|
8
8
|
import {
|
|
9
9
|
checkRestoreEligibility,
|
|
10
10
|
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
-
} from "
|
|
12
|
-
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "
|
|
11
|
+
} from "../../queries/systemSettings";
|
|
12
|
+
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "../common";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Input type for creating a brand
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./brands";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./reservations";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "./reservations";
|
|
19
19
|
|
|
20
20
|
// Mock the systemSettings query functions
|
|
21
|
-
vi.mock("
|
|
21
|
+
vi.mock("../../queries/systemSettings", () => ({
|
|
22
22
|
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
23
23
|
if (!deletedAt) {
|
|
24
24
|
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import {
|
|
9
9
|
checkRestoreEligibility,
|
|
10
10
|
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
-
} from "
|
|
12
|
-
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "
|
|
11
|
+
} from "../../queries/systemSettings";
|
|
12
|
+
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "../common";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Reservation status type
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./users";
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Mutation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for user CRUD operations including soft delete and restoration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
createUser,
|
|
10
|
+
updateUser,
|
|
11
|
+
softDeleteUser,
|
|
12
|
+
restoreUser,
|
|
13
|
+
deleteUser,
|
|
14
|
+
} from "./users";
|
|
15
|
+
|
|
16
|
+
// Mock the systemSettings query functions
|
|
17
|
+
vi.mock("../../queries/systemSettings", () => ({
|
|
18
|
+
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
19
|
+
if (!deletedAt) {
|
|
20
|
+
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
21
|
+
}
|
|
22
|
+
const deletedDate = new Date(deletedAt);
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const diffMs = now.getTime() - deletedDate.getTime();
|
|
25
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
26
|
+
|
|
27
|
+
if (diffDays > retentionDays) {
|
|
28
|
+
return {
|
|
29
|
+
canRestore: false,
|
|
30
|
+
daysRemaining: 0,
|
|
31
|
+
daysExpired: Math.floor(diffDays - retentionDays),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
canRestore: true,
|
|
37
|
+
daysRemaining: Math.ceil(retentionDays - diffDays),
|
|
38
|
+
daysExpired: 0,
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS: 30,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("User Mutations", () => {
|
|
45
|
+
describe("createUser", () => {
|
|
46
|
+
const validInput = {
|
|
47
|
+
cognitoId: "cognito-123",
|
|
48
|
+
email: "user@example.com",
|
|
49
|
+
accountId: "account-123",
|
|
50
|
+
roles: ["BRAND_ADMIN"],
|
|
51
|
+
status: "active" as const,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
it("should create a user with valid input", async () => {
|
|
55
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
56
|
+
data: { id: "user-001", ...validInput },
|
|
57
|
+
errors: null,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const mockClient = {
|
|
61
|
+
models: {
|
|
62
|
+
User: { create: mockCreate },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = await createUser(mockClient, validInput);
|
|
67
|
+
|
|
68
|
+
expect(result).toBeTruthy();
|
|
69
|
+
expect(result?.id).toBe("user-001");
|
|
70
|
+
expect(mockCreate).toHaveBeenCalledWith(validInput);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
74
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
75
|
+
data: null,
|
|
76
|
+
errors: [{ message: "Validation error" }],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const mockClient = {
|
|
80
|
+
models: {
|
|
81
|
+
User: { create: mockCreate },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await createUser(mockClient, validInput);
|
|
86
|
+
|
|
87
|
+
expect(result).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should throw on unexpected error", async () => {
|
|
91
|
+
const mockCreate = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
92
|
+
|
|
93
|
+
const mockClient = {
|
|
94
|
+
models: {
|
|
95
|
+
User: { create: mockCreate },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await expect(createUser(mockClient, validInput)).rejects.toThrow("Network error");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("updateUser", () => {
|
|
104
|
+
it("should update a user", async () => {
|
|
105
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
106
|
+
data: { id: "user-001", email: "updated@example.com" },
|
|
107
|
+
errors: null,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const mockClient = {
|
|
111
|
+
models: {
|
|
112
|
+
User: { update: mockUpdate },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = await updateUser(mockClient, {
|
|
117
|
+
id: "user-001",
|
|
118
|
+
email: "updated@example.com",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result).toBeTruthy();
|
|
122
|
+
expect(result?.email).toBe("updated@example.com");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should return null on GraphQL error", async () => {
|
|
126
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
127
|
+
data: null,
|
|
128
|
+
errors: [{ message: "Update failed" }],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const mockClient = {
|
|
132
|
+
models: {
|
|
133
|
+
User: { update: mockUpdate },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = await updateUser(mockClient, { id: "user-001" });
|
|
138
|
+
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("softDeleteUser", () => {
|
|
144
|
+
it("should set deletedAt and deletedBy", async () => {
|
|
145
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
146
|
+
data: { id: "user-001" },
|
|
147
|
+
errors: null,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mockClient = {
|
|
151
|
+
models: {
|
|
152
|
+
User: { update: mockUpdate },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = await softDeleteUser(mockClient, "user-001", "admin@example.com");
|
|
157
|
+
|
|
158
|
+
expect(result).toBe(true);
|
|
159
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
160
|
+
expect(callArgs.id).toBe("user-001");
|
|
161
|
+
expect(callArgs.status).toBe("deleted");
|
|
162
|
+
expect(callArgs.deletedAt).toBeDefined();
|
|
163
|
+
expect(callArgs.deletedBy).toBe("admin@example.com");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should return false on GraphQL error", async () => {
|
|
167
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
168
|
+
data: null,
|
|
169
|
+
errors: [{ message: "Error" }],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const mockClient = {
|
|
173
|
+
models: {
|
|
174
|
+
User: { update: mockUpdate },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = await softDeleteUser(mockClient, "user-001", "admin@example.com");
|
|
179
|
+
|
|
180
|
+
expect(result).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("restoreUser", () => {
|
|
185
|
+
it("should restore a recently deleted user", async () => {
|
|
186
|
+
const recentDeletedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString();
|
|
187
|
+
|
|
188
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
189
|
+
data: { id: "user-001", deletedAt: recentDeletedAt },
|
|
190
|
+
errors: null,
|
|
191
|
+
});
|
|
192
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
193
|
+
data: { id: "user-001" },
|
|
194
|
+
errors: null,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const mockClient = {
|
|
198
|
+
models: {
|
|
199
|
+
User: { get: mockGet, update: mockUpdate },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await restoreUser(mockClient, "user-001");
|
|
204
|
+
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
207
|
+
id: "user-001",
|
|
208
|
+
status: "active",
|
|
209
|
+
deletedAt: null,
|
|
210
|
+
deletedBy: null,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should reject restoration after retention period expires", async () => {
|
|
215
|
+
const oldDeletedAt = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
216
|
+
|
|
217
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
218
|
+
data: { id: "user-001", deletedAt: oldDeletedAt },
|
|
219
|
+
errors: null,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const mockClient = {
|
|
223
|
+
models: {
|
|
224
|
+
User: { get: mockGet, update: vi.fn() },
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = await restoreUser(mockClient, "user-001", 30);
|
|
229
|
+
|
|
230
|
+
expect(result.success).toBe(false);
|
|
231
|
+
expect(result.error).toContain("Retention period");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should return error when user not found", async () => {
|
|
235
|
+
const mockGet = vi.fn().mockResolvedValue({
|
|
236
|
+
data: null,
|
|
237
|
+
errors: [{ message: "Not found" }],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const mockClient = {
|
|
241
|
+
models: {
|
|
242
|
+
User: { get: mockGet, update: vi.fn() },
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result = await restoreUser(mockClient, "nonexistent");
|
|
247
|
+
|
|
248
|
+
expect(result.success).toBe(false);
|
|
249
|
+
expect(result.error).toBe("User not found");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("deleteUser", () => {
|
|
254
|
+
it("should permanently delete a user", async () => {
|
|
255
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
256
|
+
data: { id: "user-001" },
|
|
257
|
+
errors: null,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const mockClient = {
|
|
261
|
+
models: {
|
|
262
|
+
User: { delete: mockDelete },
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = await deleteUser(mockClient, "user-001");
|
|
267
|
+
|
|
268
|
+
expect(result).toBe(true);
|
|
269
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: "user-001" });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should return false on GraphQL error", async () => {
|
|
273
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
274
|
+
data: null,
|
|
275
|
+
errors: [{ message: "Cannot delete" }],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const mockClient = {
|
|
279
|
+
models: {
|
|
280
|
+
User: { delete: mockDelete },
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const result = await deleteUser(mockClient, "user-001");
|
|
285
|
+
|
|
286
|
+
expect(result).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -8,8 +8,8 @@ import type { User } from "@htlkg/core/types";
|
|
|
8
8
|
import {
|
|
9
9
|
checkRestoreEligibility,
|
|
10
10
|
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
11
|
-
} from "
|
|
12
|
-
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "
|
|
11
|
+
} from "../../queries/systemSettings";
|
|
12
|
+
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "../common";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Input type for creating a user
|