Skip to content

Commit 33c3dda

Browse files
committed
Refactor authData validation tests and helpers: introduce dynamic token resolvers, centralize mock logic, and streamline user/session creation
1 parent f04be45 commit 33c3dda

File tree

6 files changed

+751
-777
lines changed

6 files changed

+751
-777
lines changed

spec/Users.authdata.helpers.js

Lines changed: 227 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,26 @@ function setupAuthConfig(options = {}) {
7676

7777
/**
7878
* Create mock for Google Play Games token exchange
79-
* @param {string} accessToken - Access token to return
79+
* @param {string|Function} accessTokenOrResolver - Access token to return, or function(code, body) => accessToken
8080
* @param {Function} onCall - Optional callback when mock is called
8181
* @returns {Object} Mock response object
8282
*/
83-
function mockGpgamesTokenExchange(accessToken = MOCK_ACCESS_TOKEN, onCall = null) {
83+
function mockGpgamesTokenExchange(accessTokenOrResolver = MOCK_ACCESS_TOKEN, onCall = null) {
8484
return {
8585
url: GOOGLE_TOKEN_URL,
8686
method: 'POST',
8787
response: {
8888
ok: true,
8989
json: (options) => {
9090
if (onCall) onCall(options);
91+
const body = JSON.parse(options.body);
92+
const code = body.code;
93+
let accessToken;
94+
if (typeof accessTokenOrResolver === 'function') {
95+
accessToken = accessTokenOrResolver(code, body);
96+
} else {
97+
accessToken = accessTokenOrResolver;
98+
}
9199
return Promise.resolve({ access_token: accessToken });
92100
},
93101
},
@@ -116,18 +124,27 @@ function mockGpgamesPlayerInfo(userId = MOCK_USER_ID, onCall = null) {
116124

117125
/**
118126
* Create mock for Instagram token exchange
119-
* @param {string} accessToken - Access token to return
127+
* @param {string|Function} accessTokenOrResolver - Access token to return, or function(code, body) => accessToken
120128
* @param {Function} onCall - Optional callback when mock is called
121129
* @returns {Object} Mock response object
122130
*/
123-
function mockInstagramTokenExchange(accessToken = 'ig_token_1', onCall = null) {
131+
function mockInstagramTokenExchange(accessTokenOrResolver = 'ig_token_1', onCall = null) {
124132
return {
125133
url: IG_TOKEN_URL,
126134
method: 'POST',
127135
response: {
128136
ok: true,
129137
json: (options) => {
130138
if (onCall) onCall(options);
139+
// Instagram uses URLSearchParams, not JSON
140+
const body = new URLSearchParams(options.body);
141+
const code = body.get('code');
142+
let accessToken;
143+
if (typeof accessTokenOrResolver === 'function') {
144+
accessToken = accessTokenOrResolver(code, body);
145+
} else {
146+
accessToken = accessTokenOrResolver;
147+
}
131148
return Promise.resolve({ access_token: accessToken });
132149
},
133150
},
@@ -136,20 +153,34 @@ function mockInstagramTokenExchange(accessToken = 'ig_token_1', onCall = null) {
136153

137154
/**
138155
* Create mock for Instagram user info
139-
* @param {string} accessToken - Access token used in URL
156+
* @param {string|Function} accessTokenOrResolver - Access token or function(accessToken) => userId for dynamic responses
140157
* @param {string} userId - User ID to return (default: 'I1')
141158
* @param {Function} onCall - Optional callback when mock is called
142-
* @returns {Object} Mock response object
159+
* @returns {Object} Mock response object with dynamic URL matching
143160
*/
144-
function mockInstagramUserInfo(accessToken = 'ig_token_1', userId = 'I1', onCall = null) {
161+
function mockInstagramUserInfo(accessTokenOrResolver = 'ig_token_1', userId = 'I1', onCall = null) {
162+
// For dynamic access tokens, use a function that matches any IG_ME_URL pattern
163+
const urlPattern = typeof accessTokenOrResolver === 'function'
164+
? (url) => url && url.startsWith('https://graph.instagram.com/me?fields=id&access_token=')
165+
: IG_ME_URL(accessTokenOrResolver);
166+
145167
return {
146-
url: IG_ME_URL(accessToken),
168+
url: urlPattern,
147169
method: 'GET',
148170
response: {
149171
ok: true,
150172
json: (options) => {
151173
if (onCall) onCall(options);
152-
return Promise.resolve({ id: userId });
174+
// Extract accessToken from URL if resolver is a function
175+
let resolvedUserId = userId;
176+
if (typeof accessTokenOrResolver === 'function' && options && options.url) {
177+
const urlMatch = options.url.match(/access_token=([^&]+)/);
178+
if (urlMatch) {
179+
const token = urlMatch[1];
180+
resolvedUserId = accessTokenOrResolver(token) || userId;
181+
}
182+
}
183+
return Promise.resolve({ id: resolvedUserId });
153184
},
154185
},
155186
};
@@ -159,7 +190,7 @@ function mockInstagramUserInfo(accessToken = 'ig_token_1', userId = 'I1', onCall
159190
* Create complete mock for gpgames login flow
160191
* @param {Object} options - Configuration options
161192
* @param {string} options.userId - User ID (default: MOCK_USER_ID)
162-
* @param {string} options.accessToken - Access token (default: MOCK_ACCESS_TOKEN)
193+
* @param {string|Function} options.accessToken - Access token or function(code, body) => accessToken for dynamic responses
163194
* @param {Function} options.onTokenExchange - Callback for token exchange
164195
* @param {Function} options.onPlayerInfo - Callback for player info
165196
* @returns {Array} Array of mock responses
@@ -182,7 +213,7 @@ function mockGpgamesLogin(options = {}) {
182213
* Create complete mock for instagram login flow
183214
* @param {Object} options - Configuration options
184215
* @param {string} options.userId - User ID (default: 'I1')
185-
* @param {string} options.accessToken - Access token (default: 'ig_token_1')
216+
* @param {string|Function} options.accessToken - Access token or function(code, body) => accessToken for dynamic responses
186217
* @param {Function} options.onTokenExchange - Callback for token exchange
187218
* @param {Function} options.onUserInfo - Callback for user info
188219
* @returns {Array} Array of mock responses
@@ -195,9 +226,13 @@ function mockInstagramLogin(options = {}) {
195226
onUserInfo = null,
196227
} = options;
197228

229+
// If accessToken is a function, we need to handle dynamic URL generation for IG_ME_URL
230+
// For now, use first token or default for URL construction
231+
const urlToken = typeof accessToken === 'function' ? 'ig_token_1' : accessToken;
232+
198233
return [
199234
mockInstagramTokenExchange(accessToken, onTokenExchange),
200-
mockInstagramUserInfo(accessToken, userId, onUserInfo),
235+
mockInstagramUserInfo(urlToken, userId, onUserInfo),
201236
];
202237
}
203238

@@ -236,6 +271,32 @@ async function createUserWithGpgames(options = {}) {
236271
});
237272
}
238273

274+
/**
275+
* Create user with gpgames authData and return session token
276+
* @param {Object} options - Configuration options
277+
* @param {string} options.userId - User ID (default: MOCK_USER_ID)
278+
* @param {string} options.code - Auth code (default: 'C1')
279+
* @param {boolean} options.fetch - Whether to fetch user after creation (default: true)
280+
* @returns {Promise<{user: Parse.User, sessionToken: string}>} User and session token
281+
*/
282+
async function createUserWithGpgamesAndSession(options = {}) {
283+
const { userId = MOCK_USER_ID, code = 'C1', fetch = true } = options;
284+
285+
mockFetch(mockGpgamesLogin({ userId }));
286+
287+
const user = await Parse.User.logInWith('gpgames', {
288+
authData: { id: userId, code },
289+
});
290+
291+
const sessionToken = user.getSessionToken();
292+
293+
if (fetch) {
294+
await user.fetch({ sessionToken });
295+
}
296+
297+
return { user, sessionToken };
298+
}
299+
239300
/**
240301
* Create user with password auth
241302
* @param {Object} options - Configuration options
@@ -249,6 +310,152 @@ async function createUserWithPassword(options = {}) {
249310
return await Parse.User.signUp(username, password);
250311
}
251312

313+
/**
314+
* Create user with password auth and return session token
315+
* @param {Object} options - Configuration options
316+
* @param {string} options.username - Username (default: TEST_USERNAME)
317+
* @param {string} options.password - Password (default: TEST_PASSWORD)
318+
* @param {boolean} options.fetch - Whether to fetch user after creation (default: true)
319+
* @returns {Promise<{user: Parse.User, sessionToken: string}>} User and session token
320+
*/
321+
async function createUserWithPasswordAndSession(options = {}) {
322+
const { username = TEST_USERNAME, password = TEST_PASSWORD, fetch = true } = options;
323+
324+
const user = await Parse.User.signUp(username, password);
325+
const sessionToken = user.getSessionToken();
326+
327+
if (fetch) {
328+
await user.fetch({ sessionToken });
329+
}
330+
331+
return { user, sessionToken };
332+
}
333+
334+
// ============================================
335+
// AuthData Assertion Helpers
336+
// ============================================
337+
338+
/**
339+
* Assert that authData contains expected providers
340+
* @param {Parse.User} user - User object
341+
* @param {Object} expectedProviders - Object with provider names as keys and expected data as values
342+
* @param {Object} options - Options
343+
* @param {boolean} options.useMasterKey - Whether to fetch with master key (default: false)
344+
* @param {string} options.sessionToken - Session token for fetch (default: undefined)
345+
* @returns {Promise<Object>} The authData object
346+
*/
347+
async function assertAuthDataProviders(user, expectedProviders, options = {}) {
348+
const { useMasterKey = false, sessionToken } = options;
349+
350+
if (useMasterKey) {
351+
await user.fetch({ useMasterKey: true });
352+
} else if (sessionToken) {
353+
await user.fetch({ sessionToken });
354+
}
355+
356+
const authData = user.get('authData');
357+
expect(authData).toBeDefined();
358+
359+
for (const [provider, expectedData] of Object.entries(expectedProviders)) {
360+
if (expectedData === null) {
361+
expect(authData[provider]).toBeUndefined();
362+
} else if (typeof expectedData === 'object') {
363+
expect(authData[provider]).toBeDefined();
364+
for (const [key, value] of Object.entries(expectedData)) {
365+
expect(authData[provider][key]).toBe(value);
366+
}
367+
} else {
368+
expect(authData[provider]).toBe(expectedData);
369+
}
370+
}
371+
372+
return authData;
373+
}
374+
375+
/**
376+
* Update user authData with session token
377+
* @param {Parse.User} user - User object
378+
* @param {Object} authDataUpdate - AuthData to update
379+
* @param {string} sessionToken - Session token
380+
* @param {boolean} fetchAfter - Whether to fetch user after update (default: true)
381+
* @returns {Promise<Parse.User>} Updated user
382+
*/
383+
async function updateUserAuthData(user, authDataUpdate, sessionToken, fetchAfter = true) {
384+
await user.save({ authData: authDataUpdate }, { sessionToken });
385+
if (fetchAfter) {
386+
await user.fetch({ sessionToken });
387+
}
388+
return user;
389+
}
390+
391+
/**
392+
* Setup mocks for gpgames and instagram with dynamic responses
393+
* @param {Object} options - Configuration options
394+
* @param {string|Function} options.gpgamesResolver - Access token or function (code) => accessToken for gpgames
395+
* @param {string|Function} options.instagramResolver - Access token or function (code) => accessToken for instagram
396+
* @param {Function} options.onGpgamesTokenExchange - Callback for gpgames token exchange
397+
* @param {Function} options.onInstagramTokenExchange - Callback for instagram token exchange
398+
* @returns {Array} Array of mock responses
399+
*/
400+
function setupGpgamesAndInstagramMocks(options = {}) {
401+
const {
402+
gpgamesResolver = MOCK_ACCESS_TOKEN,
403+
instagramResolver = 'ig_token_1',
404+
onGpgamesTokenExchange = null,
405+
onInstagramTokenExchange = null,
406+
} = options;
407+
408+
return [
409+
...mockGpgamesLogin({
410+
accessToken: typeof gpgamesResolver === 'function'
411+
? gpgamesResolver
412+
: () => gpgamesResolver,
413+
onTokenExchange: onGpgamesTokenExchange,
414+
}),
415+
...mockInstagramLogin({
416+
accessToken: typeof instagramResolver === 'function'
417+
? instagramResolver
418+
: () => instagramResolver,
419+
onTokenExchange: onInstagramTokenExchange,
420+
}),
421+
];
422+
}
423+
424+
/**
425+
* Create validation tracker for a provider
426+
* @param {string} provider - Provider name ('gpgames' or 'instagram')
427+
* @param {Object} options - Configuration options
428+
* @param {Function} options.onValidation - Callback when validation is called
429+
* @returns {Object} Mock setup with validation tracker
430+
*/
431+
function createValidationTracker(provider, options = {}) {
432+
const { onValidation } = options;
433+
let validated = false;
434+
435+
const tracker = {
436+
get validated() { return validated; },
437+
reset() { validated = false; },
438+
};
439+
440+
if (provider === 'gpgames') {
441+
mockFetch(mockGpgamesLogin({
442+
onTokenExchange: () => {
443+
validated = true;
444+
if (onValidation) onValidation();
445+
},
446+
}));
447+
} else if (provider === 'instagram') {
448+
mockFetch(mockInstagramLogin({
449+
onTokenExchange: () => {
450+
validated = true;
451+
if (onValidation) onValidation();
452+
},
453+
}));
454+
}
455+
456+
return tracker;
457+
}
458+
252459
// ============================================
253460
// Exports
254461
// ============================================
@@ -282,6 +489,14 @@ module.exports = {
282489

283490
// User Creation
284491
createUserWithGpgames,
492+
createUserWithGpgamesAndSession,
285493
createUserWithPassword,
494+
createUserWithPasswordAndSession,
495+
496+
// AuthData Assertion Helpers
497+
assertAuthDataProviders,
498+
updateUserAuthData,
499+
setupGpgamesAndInstagramMocks,
500+
createValidationTracker,
286501
};
287502

0 commit comments

Comments
 (0)