feat: Add event information to verifyUserEmails, preventLoginWithUnverifiedEmail to identify invoking signup / login action and auth provider (#9963)

This commit is contained in:
Palixir
2026-02-06 04:48:35 +01:00
committed by GitHub
parent 617de9989b
commit ed98c15f90
12 changed files with 350 additions and 27 deletions

View File

@@ -288,7 +288,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return false;
},
};
@@ -349,7 +357,15 @@ describe('Email Verification Token Expiration:', () => {
};
const verifyUserEmails = {
method(req) {
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
expect(Object.keys(req)).toEqual([
'original',
'object',
'master',
'ip',
'installationId',
'createdWith',
]);
expect(req.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
if (req.object.get('username') === 'no_email') {
return false;
}
@@ -384,6 +400,144 @@ describe('Email Verification Token Expiration:', () => {
expect(verifySpy).toHaveBeenCalledTimes(5);
});
it('provides createdWith on signup when verification blocks session creation', async () => {
const verifyUserEmails = {
method: params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'signup', authProvider: 'password' });
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const user = new Parse.User();
user.setUsername('signup_created_with');
user.setPassword('pass');
user.setEmail('signup@example.com');
const res = await user.signUp().catch(e => e);
expect(res.message).toBe('User email is not verified.');
expect(user.getSessionToken()).toBeUndefined();
expect(verifySpy).toHaveBeenCalledTimes(2); // before signup completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on login verification', async () => {
const user = new Parse.User();
user.setUsername('user_created_with_login');
user.setPassword('pass');
user.set('email', 'login@example.com');
await user.signUp();
const verifyUserEmails = {
method: async params => {
expect(params.object).toBeInstanceOf(Parse.User);
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_created_with_login', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); // before login completion and on preventLoginWithUnverifiedEmail
});
it('provides createdWith with auth provider on signup verification', async () => {
const createdWithValues = [];
const verifyUserEmails = {
method: params => {
createdWithValues.push(params.createdWith);
return true;
},
};
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
await reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: verifyUserEmails.method,
preventLoginWithUnverifiedEmail: true,
preventSignupWithUnverifiedEmail: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1',
});
const provider = {
authData: { id: '8675309', access_token: 'jenny' },
shouldError: false,
authenticate(options) {
options.success(this, this.authData);
},
restoreAuthentication() {
return true;
},
getAuthType() {
return 'facebook';
},
deauthenticate() {},
};
Parse.User._registerAuthenticationProvider(provider);
const res = await Parse.User._logInWith('facebook').catch(e => e);
expect(res.message).toBe('User email is not verified.');
// Called once in createSessionTokenIfNeeded (no email set, so _validateEmail skips)
expect(verifySpy).toHaveBeenCalledTimes(1);
expect(createdWithValues[0]).toEqual({ action: 'signup', authProvider: 'facebook' });
});
it('provides createdWith for preventLoginWithUnverifiedEmail function', async () => {
const user = new Parse.User();
user.setUsername('user_prevent_login_fn');
user.setPassword('pass');
user.set('email', 'preventlogin@example.com');
await user.signUp();
const preventLoginCreatedWith = [];
await reconfigureServer({
appName: 'emailVerifyToken',
publicServerURL: 'http://localhost:8378/1',
verifyUserEmails: true,
preventLoginWithUnverifiedEmail: params => {
preventLoginCreatedWith.push(params.createdWith);
return true;
},
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
});
const res = await Parse.User.logIn('user_prevent_login_fn', 'pass').catch(e => e);
expect(res.code).toBe(205);
expect(preventLoginCreatedWith.length).toBe(1);
expect(preventLoginCreatedWith[0]).toEqual({ action: 'login', authProvider: 'password' });
});
it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => {
const emailAdapter = {
sendVerificationEmail: () => {},
@@ -779,6 +933,7 @@ describe('Email Verification Token Expiration:', () => {
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.resendRequest).toBeTrue();
expect(params.createdWith).toBeUndefined();
return true;
},
};

View File

@@ -284,6 +284,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
expect(params.ip).toBeDefined();
expect(params.master).toBeDefined();
expect(params.installationId).toBeDefined();
expect(params.createdWith).toEqual({ action: 'login', authProvider: 'password' });
return true;
},
};

View File

@@ -133,6 +133,72 @@ describe('buildConfigDefinitions', () => {
expect(result.property.name).toBe('arrayParser');
});
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return booleanOrFunctionParser for UnionTypeAnnotation containing boolean (non-nullable)', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'FunctionTypeAnnotation' },
],
};
const result = mapperFor(mockElement, t);
expect(t.isMemberExpression(result)).toBe(true);
expect(result.object.name).toBe('parsers');
expect(result.property.name).toBe('booleanOrFunctionParser');
});
it('should return undefined for UnionTypeAnnotation without boolean', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'StringTypeAnnotation' },
{ type: 'NumberTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return undefined for UnionTypeAnnotation with boolean but without function', () => {
const mockElement = {
type: 'UnionTypeAnnotation',
typeAnnotation: {
types: [
{ type: 'BooleanTypeAnnotation' },
{ type: 'VoidTypeAnnotation' },
],
},
};
const result = mapperFor(mockElement, t);
expect(result).toBeUndefined();
});
it('should return objectParser for unknown GenericTypeAnnotation', () => {
const mockElement = {
type: 'GenericTypeAnnotation',

View File

@@ -3,6 +3,7 @@ const {
numberOrBoolParser,
numberOrStringParser,
booleanParser,
booleanOrFunctionParser,
objectParser,
arrayParser,
moduleOrObjectParser,
@@ -48,6 +49,23 @@ describe('parsers', () => {
expect(parser(2)).toEqual(false);
});
it('parses correctly with booleanOrFunctionParser', () => {
const parser = booleanOrFunctionParser;
// Preserves functions
const fn = () => true;
expect(parser(fn)).toBe(fn);
const asyncFn = async () => false;
expect(parser(asyncFn)).toBe(asyncFn);
// Parses booleans and string booleans like booleanParser
expect(parser(true)).toEqual(true);
expect(parser(false)).toEqual(false);
expect(parser('true')).toEqual(true);
expect(parser('false')).toEqual(false);
expect(parser('1')).toEqual(true);
expect(parser(1)).toEqual(true);
expect(parser(0)).toEqual(false);
});
it('parses correctly with objectParser', () => {
const parser = objectParser;
expect(parser({ hello: 'world' })).toEqual({ hello: 'world' });