@rebasepro/server-core 0.6.1 → 0.8.0
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/api/graphql/graphql-schema-generator.d.ts +9 -0
- package/dist/api/rest/api-generator.d.ts +2 -14
- package/dist/api/rest/query-parser.d.ts +2 -5
- package/dist/api/types.d.ts +8 -2
- package/dist/auth/adapter-middleware.d.ts +7 -1
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-types.d.ts +8 -0
- package/dist/auth/auth-hooks.d.ts +17 -0
- package/dist/auth/builtin-auth-adapter.d.ts +3 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/interfaces.d.ts +32 -2
- package/dist/auth/magic-link-routes.d.ts +30 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +2 -1
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/routes.d.ts +3 -1
- package/dist/auth/session-routes.d.ts +2 -0
- package/dist/backend-CIxN4FVm.js.map +1 -1
- package/dist/base64-js-C_frYBkI.js +1687 -0
- package/dist/base64-js-C_frYBkI.js.map +1 -0
- package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
- package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/templates.d.ts +8 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/functions/define-function.d.ts +50 -0
- package/dist/functions/index.d.ts +2 -0
- package/dist/index.d.ts +16 -18
- package/dist/index.es.js +1947 -2356
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +40615 -10203
- package/dist/index.umd.js.map +1 -1
- package/dist/init.d.ts +68 -12
- package/dist/jws-BqRRaK11.js +616 -0
- package/dist/jws-BqRRaK11.js.map +1 -0
- package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
- package/dist/jwt-BETC8a1J.js.map +1 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
- package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
- package/dist/src-DjzOT1kG.js +29746 -0
- package/dist/src-DjzOT1kG.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/index.d.ts +5 -2
- package/dist/storage/routes.d.ts +33 -1
- package/dist/storage/tus-handler.d.ts +3 -1
- package/dist/storage/types.d.ts +28 -3
- package/dist/utils/dev-port.d.ts +2 -0
- package/package.json +13 -5
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +50 -15
- package/src/api/openapi-generator.ts +2 -2
- package/src/api/rest/api-generator.ts +44 -123
- package/src/api/rest/query-parser.ts +6 -23
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +8 -2
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +29 -6
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +38 -2
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +14 -5
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +78 -10
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/functions/define-function.ts +59 -0
- package/src/functions/function-loader.ts +16 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +70 -37
- package/src/init.ts +214 -25
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/storage/GCSStorageController.ts +334 -0
- package/src/storage/index.ts +9 -3
- package/src/storage/routes.ts +191 -23
- package/src/storage/tus-handler.ts +16 -4
- package/src/storage/types.ts +25 -3
- package/src/utils/dev-port.ts +13 -7
- package/test/api-generator.test.ts +1 -1
- package/test/auth-config-types.test.ts +40 -0
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/define-function.test.ts +45 -0
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/storage-routes.test.ts +160 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/test/backend-hooks-data.test.ts +0 -477
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transformAuthResponse Hook — Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the `transformAuthResponse` hook across login, register, refresh,
|
|
5
|
+
* and error handling scenarios. Services are mocked following the same
|
|
6
|
+
* pattern as auth-routes.test.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import type { HonoEnv } from "../src/api/types";
|
|
11
|
+
import { errorHandler } from "../src/api/errors";
|
|
12
|
+
import { createAuthRoutes, AuthModuleConfig } from "../src/auth/routes";
|
|
13
|
+
import type { AuthRepository } from "../src/auth/interfaces";
|
|
14
|
+
import { configureJwt, hashRefreshToken } from "../src/auth/jwt";
|
|
15
|
+
import type { AuthHooks } from "../src/auth/auth-hooks";
|
|
16
|
+
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
17
|
+
|
|
18
|
+
// ── Mocks ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
jest.mock("../src/auth/password");
|
|
21
|
+
|
|
22
|
+
jest.mock("../src/utils/logger", () => {
|
|
23
|
+
return {
|
|
24
|
+
logger: {
|
|
25
|
+
info: jest.fn(),
|
|
26
|
+
warn: jest.fn(),
|
|
27
|
+
error: jest.fn(),
|
|
28
|
+
debug: jest.fn(),
|
|
29
|
+
child: jest.fn().mockReturnThis()
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Bypass rate limiters — they share state across tests and cause 429s
|
|
35
|
+
jest.mock("../src/auth/rate-limiter", () => {
|
|
36
|
+
const passthrough = async (_c: unknown, next: () => Promise<void>) => next();
|
|
37
|
+
return {
|
|
38
|
+
createRateLimiter: () => passthrough,
|
|
39
|
+
defaultAuthLimiter: passthrough,
|
|
40
|
+
strictAuthLimiter: passthrough
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
import { validatePasswordStrength, hashPassword, verifyPassword } from "../src/auth/password";
|
|
45
|
+
import { logger } from "../src/utils/logger";
|
|
46
|
+
|
|
47
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const TEST_SECRET = "integration-test-secret-key-that-is-definitely-32-chars-long!!";
|
|
50
|
+
|
|
51
|
+
function mockUser(overrides: Partial<{ id: string; email: string; passwordHash: string | null; displayName: string | null; photoUrl: string | null; provider: string; emailVerified: boolean; emailVerificationToken: string | null }> = {}) {
|
|
52
|
+
return {
|
|
53
|
+
id: overrides.id ?? "user-1",
|
|
54
|
+
email: overrides.email ?? "test@example.com",
|
|
55
|
+
passwordHash: "passwordHash" in overrides ? overrides.passwordHash : "salt:hash",
|
|
56
|
+
displayName: overrides.displayName ?? "Test User",
|
|
57
|
+
photoUrl: overrides.photoUrl ?? null,
|
|
58
|
+
emailVerified: overrides.emailVerified ?? false,
|
|
59
|
+
emailVerificationToken: overrides.emailVerificationToken ?? null,
|
|
60
|
+
emailVerificationSentAt: null,
|
|
61
|
+
createdAt: new Date(),
|
|
62
|
+
updatedAt: new Date()
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockRole(id: string, isAdmin = false) {
|
|
67
|
+
return { id,
|
|
68
|
+
name: id.charAt(0).toUpperCase() + id.slice(1),
|
|
69
|
+
isAdmin,
|
|
70
|
+
defaultPermissions: null,
|
|
71
|
+
collectionPermissions: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let mockAuthRepo: jest.Mocked<AuthRepository>;
|
|
75
|
+
|
|
76
|
+
function createApp(opts: { authHooks?: AuthHooks } = {}) {
|
|
77
|
+
mockAuthRepo = {
|
|
78
|
+
getUserByEmail: jest.fn().mockResolvedValue(null),
|
|
79
|
+
getUserByIdentity: jest.fn().mockResolvedValue(null),
|
|
80
|
+
linkUserIdentity: jest.fn().mockResolvedValue(undefined),
|
|
81
|
+
getUserIdentities: jest.fn().mockResolvedValue([]),
|
|
82
|
+
getUserById: jest.fn().mockResolvedValue(null),
|
|
83
|
+
createUser: jest.fn().mockImplementation((data) =>
|
|
84
|
+
Promise.resolve(mockUser({ email: data.email,
|
|
85
|
+
displayName: data.displayName,
|
|
86
|
+
passwordHash: data.passwordHash }))
|
|
87
|
+
),
|
|
88
|
+
listUsers: jest.fn().mockResolvedValue([]),
|
|
89
|
+
getUserRoles: jest.fn().mockResolvedValue([mockRole("editor")]),
|
|
90
|
+
getUserRoleIds: jest.fn().mockResolvedValue(["editor"]),
|
|
91
|
+
assignDefaultRole: jest.fn().mockResolvedValue(undefined),
|
|
92
|
+
setUserRoles: jest.fn().mockResolvedValue(undefined),
|
|
93
|
+
updateUser: jest.fn().mockImplementation((id, data) =>
|
|
94
|
+
Promise.resolve(mockUser({ id,
|
|
95
|
+
...data }))
|
|
96
|
+
),
|
|
97
|
+
deleteUser: jest.fn().mockResolvedValue(undefined),
|
|
98
|
+
updatePassword: jest.fn().mockResolvedValue(undefined),
|
|
99
|
+
setEmailVerified: jest.fn().mockResolvedValue(undefined),
|
|
100
|
+
setVerificationToken: jest.fn().mockResolvedValue(undefined),
|
|
101
|
+
getUserByVerificationToken: jest.fn().mockResolvedValue(null),
|
|
102
|
+
getUserWithRoles: jest.fn().mockImplementation(async (userId) => {
|
|
103
|
+
const user = mockUser({ id: userId });
|
|
104
|
+
return { user,
|
|
105
|
+
roles: [mockRole("editor")] };
|
|
106
|
+
}),
|
|
107
|
+
createRefreshToken: jest.fn().mockResolvedValue(undefined),
|
|
108
|
+
findRefreshTokenByHash: jest.fn().mockResolvedValue(null),
|
|
109
|
+
deleteRefreshToken: jest.fn().mockResolvedValue(undefined),
|
|
110
|
+
deleteAllRefreshTokensForUser: jest.fn().mockResolvedValue(undefined),
|
|
111
|
+
listRefreshTokensForUser: jest.fn().mockResolvedValue([]),
|
|
112
|
+
deleteRefreshTokenById: jest.fn().mockResolvedValue(undefined),
|
|
113
|
+
createPasswordResetToken: jest.fn().mockResolvedValue(undefined),
|
|
114
|
+
findValidPasswordResetToken: jest.fn().mockResolvedValue(null),
|
|
115
|
+
markPasswordResetTokenUsed: jest.fn().mockResolvedValue(undefined),
|
|
116
|
+
deleteExpiredPasswordResetTokens: jest.fn().mockResolvedValue(undefined)
|
|
117
|
+
} as unknown as jest.Mocked<AuthRepository>;
|
|
118
|
+
|
|
119
|
+
// Password mocks
|
|
120
|
+
(validatePasswordStrength as jest.Mock).mockReturnValue({ valid: true,
|
|
121
|
+
errors: [] });
|
|
122
|
+
(hashPassword as jest.Mock).mockResolvedValue("hashed-pw");
|
|
123
|
+
(verifyPassword as jest.Mock).mockResolvedValue(true);
|
|
124
|
+
|
|
125
|
+
const config: AuthModuleConfig = {
|
|
126
|
+
authRepo: mockAuthRepo,
|
|
127
|
+
allowRegistration: true,
|
|
128
|
+
authHooks: opts.authHooks
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const app = new Hono<HonoEnv>();
|
|
132
|
+
app.onError(errorHandler);
|
|
133
|
+
app.route("/auth", createAuthRoutes(config));
|
|
134
|
+
return app;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function json(body: Record<string, unknown>) {
|
|
138
|
+
return {
|
|
139
|
+
method: "POST" as const,
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify(body)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// TESTS
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
148
|
+
|
|
149
|
+
describe("transformAuthResponse hook", () => {
|
|
150
|
+
beforeAll(() => {
|
|
151
|
+
configureJwt({ secret: TEST_SECRET,
|
|
152
|
+
accessExpiresIn: "1h" });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
jest.clearAllMocks();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── Registration ────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe("POST /auth/register", () => {
|
|
162
|
+
it("calls transformAuthResponse with the response and 'register' method", async () => {
|
|
163
|
+
const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
|
|
164
|
+
...response,
|
|
165
|
+
tokens: { ...response.tokens, firebaseToken: "custom-firebase-token" }
|
|
166
|
+
}));
|
|
167
|
+
const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
|
|
168
|
+
|
|
169
|
+
const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
|
|
170
|
+
expect(res.status).toBe(201);
|
|
171
|
+
const body = await res.json() as Record<string, unknown>;
|
|
172
|
+
const tokens = body.tokens as Record<string, unknown>;
|
|
173
|
+
|
|
174
|
+
// Verify the hook was called
|
|
175
|
+
expect(transformHook).toHaveBeenCalledTimes(1);
|
|
176
|
+
|
|
177
|
+
// Verify context
|
|
178
|
+
const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
|
|
179
|
+
expect(context.method).toBe("register");
|
|
180
|
+
expect(context.userId).toBeTruthy();
|
|
181
|
+
expect(context.request).toBeInstanceOf(Request);
|
|
182
|
+
|
|
183
|
+
// Verify the custom field was injected
|
|
184
|
+
expect(tokens.firebaseToken).toBe("custom-firebase-token");
|
|
185
|
+
// Original fields still present
|
|
186
|
+
expect(tokens.accessToken).toBeTruthy();
|
|
187
|
+
expect(tokens.refreshToken).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── Login ────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
describe("POST /auth/login", () => {
|
|
194
|
+
it("calls transformAuthResponse with 'login' method", async () => {
|
|
195
|
+
const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
|
|
196
|
+
...response,
|
|
197
|
+
tokens: { ...response.tokens, externalToken: "ext-123" }
|
|
198
|
+
}));
|
|
199
|
+
const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
|
|
200
|
+
|
|
201
|
+
// Setup: user exists
|
|
202
|
+
mockAuthRepo.getUserByEmail.mockResolvedValueOnce(mockUser());
|
|
203
|
+
|
|
204
|
+
const res = await app.request("/auth/login", json({ email: "test@example.com", password: "StrongPass1" }));
|
|
205
|
+
expect(res.status).toBe(200);
|
|
206
|
+
const body = await res.json() as Record<string, unknown>;
|
|
207
|
+
const tokens = body.tokens as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
expect(transformHook).toHaveBeenCalledTimes(1);
|
|
210
|
+
const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
|
|
211
|
+
expect(context.method).toBe("login");
|
|
212
|
+
expect(tokens.externalToken).toBe("ext-123");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── Refresh ──────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("POST /auth/refresh", () => {
|
|
219
|
+
it("calls transformAuthResponse with 'refresh' method (tokens-only response)", async () => {
|
|
220
|
+
const transformHook = jest.fn(async (response: AuthResponsePayload) => ({
|
|
221
|
+
...response,
|
|
222
|
+
tokens: { ...response.tokens, refreshedExternalToken: "refreshed-ext" }
|
|
223
|
+
}));
|
|
224
|
+
const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
|
|
225
|
+
|
|
226
|
+
// Setup: valid refresh token exists
|
|
227
|
+
const rawRefreshToken = "a".repeat(80);
|
|
228
|
+
const hashedToken = hashRefreshToken(rawRefreshToken);
|
|
229
|
+
mockAuthRepo.findRefreshTokenByHash.mockResolvedValueOnce({
|
|
230
|
+
id: "rt-1",
|
|
231
|
+
userId: "user-1",
|
|
232
|
+
tokenHash: hashedToken,
|
|
233
|
+
expiresAt: new Date(Date.now() + 86400000), // 1 day from now
|
|
234
|
+
createdAt: new Date(),
|
|
235
|
+
userAgent: "test",
|
|
236
|
+
ipAddress: "127.0.0.1"
|
|
237
|
+
});
|
|
238
|
+
mockAuthRepo.getUserRoleIds.mockResolvedValueOnce(["editor"]);
|
|
239
|
+
|
|
240
|
+
const res = await app.request("/auth/refresh", json({ refreshToken: rawRefreshToken }));
|
|
241
|
+
expect(res.status).toBe(200);
|
|
242
|
+
const body = await res.json() as Record<string, unknown>;
|
|
243
|
+
const tokens = body.tokens as Record<string, unknown>;
|
|
244
|
+
|
|
245
|
+
expect(transformHook).toHaveBeenCalledTimes(1);
|
|
246
|
+
|
|
247
|
+
// Verify refresh context
|
|
248
|
+
const hookResponse = transformHook.mock.calls[0][0] as AuthResponsePayload;
|
|
249
|
+
const context = transformHook.mock.calls[0][1] as TransformAuthResponseContext;
|
|
250
|
+
expect(context.method).toBe("refresh");
|
|
251
|
+
expect(hookResponse.user).toBeUndefined(); // refresh has no user
|
|
252
|
+
expect(tokens.refreshedExternalToken).toBe("refreshed-ext");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── Error handling ───────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
describe("error handling", () => {
|
|
259
|
+
it("returns untransformed response when hook throws", async () => {
|
|
260
|
+
const transformHook = jest.fn(async () => {
|
|
261
|
+
throw new Error("External service down");
|
|
262
|
+
});
|
|
263
|
+
const app = createApp({ authHooks: { transformAuthResponse: transformHook } });
|
|
264
|
+
|
|
265
|
+
const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
|
|
266
|
+
expect(res.status).toBe(201);
|
|
267
|
+
const body = await res.json() as Record<string, unknown>;
|
|
268
|
+
const tokens = body.tokens as Record<string, unknown>;
|
|
269
|
+
const user = body.user as Record<string, unknown>;
|
|
270
|
+
|
|
271
|
+
// Response is still valid (untransformed fallback)
|
|
272
|
+
expect(tokens.accessToken).toBeTruthy();
|
|
273
|
+
expect(tokens.refreshToken).toBeTruthy();
|
|
274
|
+
expect(user.email).toBe("new@test.com");
|
|
275
|
+
|
|
276
|
+
// Error was logged
|
|
277
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
278
|
+
"[AuthHooks] transformAuthResponse error",
|
|
279
|
+
expect.objectContaining({ error: "External service down" })
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Opt-in behavior ──────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("opt-in behavior", () => {
|
|
287
|
+
it("does not alter response when hook is not provided", async () => {
|
|
288
|
+
const app = createApp(); // no hooks
|
|
289
|
+
|
|
290
|
+
const res = await app.request("/auth/register", json({ email: "new@test.com", password: "StrongPass1" }));
|
|
291
|
+
expect(res.status).toBe(201);
|
|
292
|
+
const body = await res.json() as Record<string, unknown>;
|
|
293
|
+
const tokens = body.tokens as Record<string, unknown>;
|
|
294
|
+
const user = body.user as Record<string, unknown>;
|
|
295
|
+
|
|
296
|
+
// Standard response, no extra fields
|
|
297
|
+
expect(tokens.accessToken).toBeTruthy();
|
|
298
|
+
expect(tokens.refreshToken).toBeTruthy();
|
|
299
|
+
expect(user.email).toBe("new@test.com");
|
|
300
|
+
expect(Object.keys(tokens as object)).toEqual(
|
|
301
|
+
expect.arrayContaining(["accessToken", "refreshToken", "accessTokenExpiresAt"])
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|