fix: Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) (#9668)
This commit is contained in:
182
spec/Adapters/Auth/BaseCodeAdapter.spec.js
Normal file
182
spec/Adapters/Auth/BaseCodeAdapter.spec.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
|
||||
|
||||
describe('BaseAuthCodeAdapter', function () {
|
||||
let adapter;
|
||||
const adapterName = 'TestAdapter';
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
|
||||
async getUserFromAccessToken(accessToken) {
|
||||
if (accessToken === 'validAccessToken') {
|
||||
return { id: 'validUserId' };
|
||||
}
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
|
||||
async getAccessTokenFromCode(authData) {
|
||||
if (authData.code === 'validCode') {
|
||||
return 'validAccessToken';
|
||||
}
|
||||
throw new Error('Invalid code');
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TestAuthCodeAdapter(adapterName);
|
||||
});
|
||||
|
||||
describe('validateOptions', function () {
|
||||
it('should throw error if options are missing', function () {
|
||||
expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientId is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientSecret: 'validClientSecret' })
|
||||
).toThrowError(`${adapterName} clientId is required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientSecret is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientId: 'validClientId' })
|
||||
).toThrowError(`${adapterName} clientSecret is required.`);
|
||||
});
|
||||
|
||||
it('should not throw error for valid options', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
expect(adapter.clientId).toBe('validClientId');
|
||||
expect(adapter.clientSecret).toBe('validClientSecret');
|
||||
expect(adapter.enableInsecureAuth).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow insecure mode without clientId or clientSecret', function () {
|
||||
const options = { enableInsecureAuth: true };
|
||||
expect(() => adapter.validateOptions(options)).not.toThrow();
|
||||
expect(adapter.enableInsecureAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeFind', function () {
|
||||
it('should throw error if code is missing in secure mode', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} code is required.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if access token is missing in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = {};
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if user ID does not match in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should process valid secure payload and update authData', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validAccessToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process valid insecure payload', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'validUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
|
||||
'getUserFromAccessToken is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid user for valid access token', async function () {
|
||||
const user = await adapter.getUserFromAccessToken('validAccessToken', {});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid access token', async function () {
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
|
||||
'Invalid access token'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
|
||||
'getAccessTokenFromCode is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid access token for valid code', async function () {
|
||||
const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
|
||||
expect(accessToken).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid code', async function () {
|
||||
await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
|
||||
'Invalid code'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLogin', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSetUp', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUpdate', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
});
|
||||
220
spec/Adapters/Auth/gcenter.spec.js
Normal file
220
spec/Adapters/Auth/gcenter.spec.js
Normal file
@@ -0,0 +1,220 @@
|
||||
const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
|
||||
const { pki } = require('node-forge');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('GameCenterAuth Adapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GameCenterAuth.constructor();
|
||||
|
||||
const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
|
||||
const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map(),
|
||||
arrayBuffer: () => Promise.resolve(
|
||||
gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
|
||||
text: () => Promise.resolve(digicertPem),
|
||||
},
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Test config failing due to missing params or wrong types', function () {
|
||||
it('should throw error for invalid options', async function () {
|
||||
const invalidOptions = [
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
{ bundleId: '' },
|
||||
{ enableInsecureAuth: false }, // Missing bundleId in secure mode
|
||||
];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload failing due to missing params or wrong types', function () {
|
||||
it('should throw error for missing authData fields', async function () {
|
||||
await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
|
||||
'AuthData id is missing.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload fails due to incorrect appId / certificate', function () {
|
||||
it('should throw error for invalid publicKeyUrl', async function () {
|
||||
const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.throwError(
|
||||
new Error('Invalid publicKeyUrl')
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAppleCertificate(invalidPublicKeyUrl)
|
||||
).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
|
||||
});
|
||||
|
||||
it('should throw error for invalid signature verification', async function () {
|
||||
const fakePublicKey = 'invalid-key';
|
||||
const fakeAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'invalidSignature',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
|
||||
spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
|
||||
|
||||
await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
|
||||
'Invalid signature.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload passing', function () {
|
||||
it('should successfully process valid payload and save auth data', async function () {
|
||||
const validAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'validSignature',
|
||||
bundleId: 'com.valid.app',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
|
||||
spyOn(adapter, 'verifySignature').and.returnValue(true);
|
||||
|
||||
await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate and Signature Validation', function () {
|
||||
it('should fetch and validate Apple certificate', async function () {
|
||||
const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
|
||||
const mockCertificate = 'mockCertificate';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.returnValue(
|
||||
Promise.resolve({ certificate: mockCertificate, headers: new Map() })
|
||||
);
|
||||
spyOn(pki, 'certificateFromPem').and.returnValue({});
|
||||
|
||||
adapter.cache[certUrl] = mockCertificate;
|
||||
|
||||
const cert = await adapter.getAppleCertificate(certUrl);
|
||||
expect(cert).toBe(mockCertificate);
|
||||
});
|
||||
|
||||
it('should verify signature successfully', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in secure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('can use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
285
spec/Adapters/Auth/github.spec.js
Normal file
285
spec/Adapters/Auth/github.spec.js
Normal file
@@ -0,0 +1,285 @@
|
||||
const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
|
||||
|
||||
describe('GitHubAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GitHubAdapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const token = await adapter.getAccessTokenFromCode(code);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid GitHub user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using GitHub adapter successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'GitHub code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
356
spec/Adapters/Auth/gpgames.spec.js
Normal file
356
spec/Adapters/Auth/gpgames.spec.js
Normal file
@@ -0,0 +1,356 @@
|
||||
const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GooglePlayGamesServicesAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(code, authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if playerId does not match the provided user ID', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
gpgames: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('gpgames', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when the token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'gpgames code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
258
spec/Adapters/Auth/instagram.spec.js
Normal file
258
spec/Adapters/Auth/instagram.spec.js
Normal file
@@ -0,0 +1,258 @@
|
||||
const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
|
||||
|
||||
describe('InstagramAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new InstagramAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
adapter.redirectUri = 'https://example.com/callback';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match authData', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstagramAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
instagram: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('instagram', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when access token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
309
spec/Adapters/Auth/line.spec.js
Normal file
309
spec/Adapters/Auth/line.spec.js
Normal file
@@ -0,0 +1,309 @@
|
||||
const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
|
||||
describe('LineAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LineAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw an error if code is missing in authData', async function () {
|
||||
const authData = { redirect_uri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Line auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if response contains an error object', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
line: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('line', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Line code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
312
spec/Adapters/Auth/linkedIn.spec.js
Normal file
312
spec/Adapters/Auth/linkedIn.spec.js
Normal file
@@ -0,0 +1,312 @@
|
||||
|
||||
const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
|
||||
describe('LinkedInAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LinkedInAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test beforeFind', function () {
|
||||
it('should throw error for invalid payload', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
const payloads = [{}, { access_token: null }];
|
||||
|
||||
for (const payload of payloads) {
|
||||
await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should process secure payload and set auth data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(
|
||||
Promise.resolve('validToken')
|
||||
);
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com', is_mobile_sdk: false };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', false);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer validToken`,
|
||||
'x-li-format': 'json',
|
||||
'x-li-src': undefined,
|
||||
},
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
|
||||
new Error('LinkedIn API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
})
|
||||
);
|
||||
|
||||
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(tokenResponse).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
|
||||
).toBeRejectedWith(new Error('LinkedIn API request failed.'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test validate methods', function () {
|
||||
const authData = { id: 'validUserId', access_token: 'validToken' };
|
||||
|
||||
it('validateLogin should return user id', function () {
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateSetUp should return user id', function () {
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateUpdate should return user id', function () {
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('afterFind should return user id', function () {
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkedInAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using LinkedIn adapter successfully (secure)', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when LinkedIn returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn API request failed.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('secure does not support insecure payload if not enabled', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('insecure mode supports insecure payload if enabled', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
307
spec/Adapters/Auth/microsoft.spec.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||
|
||||
describe('MicrosoftAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new MicrosoftAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||
headers: {
|
||||
Authorization: 'Bearer validToken',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test secure authentication flow', function () {
|
||||
it('should exchange code for access token and fetch user data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should throw error if user data cannot be fetched', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test insecure authentication flow', function () {
|
||||
beforeEach(function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MicrosoftAdapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with Microsoft auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false, statusText: 'Invalid code' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false, statusText: 'Unauthorized' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'user123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'incorrectUser',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
305
spec/Adapters/Auth/oauth2.spec.js
Normal file
305
spec/Adapters/Auth/oauth2.spec.js
Normal file
@@ -0,0 +1,305 @@
|
||||
const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
|
||||
|
||||
describe('OAuth2Adapter', () => {
|
||||
let adapter;
|
||||
|
||||
const validOptions = {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new OAuth2Adapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('validateAppId', () => {
|
||||
it('should validate app ID successfully', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'valid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error if app ID is invalid', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'invalid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeRejectedWithError('OAuth2: Invalid app ID.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuthData', () => {
|
||||
it('should validate auth data successfully', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeResolvedTo({});
|
||||
});
|
||||
|
||||
it('should throw an error if the token is inactive', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = { active: false };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'different-user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestTokenInfo', () => {
|
||||
it('should fetch token info successfully', async () => {
|
||||
const mockResponse = { active: true };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.requestTokenInfo(
|
||||
'validAccessToken',
|
||||
validOptions
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if the introspection endpoint URL is missing', async () => {
|
||||
const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
|
||||
|
||||
expect(
|
||||
() => adapter.validateOptions(options)
|
||||
).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.requestTokenInfo('invalidAccessToken')
|
||||
).toBeRejectedWithError('OAuth2 token introspection request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2Adapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with OAuth2 auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate and authenticate user successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.get('authData').mockOauth.id).toEqual('user123');
|
||||
});
|
||||
|
||||
it('should reject authentication for inactive token', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'inactiveToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for mismatched user ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'different-user',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for invalid app ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'invalid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
|
||||
'OAuth2: Invalid app ID.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when token introspection endpoint is missing', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: null,
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
252
spec/Adapters/Auth/qq.spec.js
Normal file
252
spec/Adapters/Auth/qq.spec.js
Normal file
@@ -0,0 +1,252 @@
|
||||
const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
|
||||
|
||||
describe('QqAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new QqAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getUserFromAccessToken('invalidAccessToken')
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode({
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
});
|
||||
|
||||
expect(result).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode({
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
})
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponseData', () => {
|
||||
it('should parse valid callback response data', () => {
|
||||
const response = `callback({"key":"value"})`;
|
||||
const result = adapter.parseResponseData(response);
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response data is invalid', () => {
|
||||
const response = 'invalid response';
|
||||
|
||||
expect(() => adapter.parseResponseData(response)).toThrowError(
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QqAdapter E2E Test', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using Qq adapter successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid code', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid user data', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async () => {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
113
spec/Adapters/Auth/spotify.spec.js
Normal file
113
spec/Adapters/Auth/spotify.spec.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
|
||||
|
||||
describe('SpotifyAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new SpotifyAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = {
|
||||
id: 'spotifyUser123',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ id: 'spotifyUser123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'validAccessToken',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refreshToken',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'validCodeVerifier',
|
||||
};
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if authData is missing required fields', async () => {
|
||||
const authData = {
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'invalidCodeVerifier',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
120
spec/Adapters/Auth/twitter.spec.js
Normal file
120
spec/Adapters/Auth/twitter.spec.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
|
||||
|
||||
describe('TwitterAuthAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
consumer_secret: 'validConsumerSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TwitterAuthAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw an error when options are missing', function () {
|
||||
expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
|
||||
});
|
||||
|
||||
it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
|
||||
const options = { enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(options)).toThrowError(
|
||||
'Consumer key and secret are required for secure Twitter auth.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw an error when valid options are provided', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validate Insecure Auth', function () {
|
||||
it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
|
||||
const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate insecure auth successfully when data matches', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error when user ID does not match', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'invalidUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Tests', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
twitter: validOptions,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using validateAuthData', async function () {
|
||||
spyOn(adapter, 'exchangeAccessToken').and.returnValue(
|
||||
Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = {
|
||||
oauth_token: 'validToken',
|
||||
oauth_verifier: 'validVerifier',
|
||||
};
|
||||
await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.auth_token).toBe('validToken');
|
||||
});
|
||||
|
||||
it('should handle multiple configurations and validate successfully', async function () {
|
||||
const authData = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
|
||||
const optionsArray = [
|
||||
{ consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
|
||||
validOptions,
|
||||
];
|
||||
|
||||
const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
|
||||
expect(selectedOption).toEqual(validOptions);
|
||||
});
|
||||
|
||||
it('should throw an error when no matching configuration is found', function () {
|
||||
const authData = { consumer_key: 'missingKey' };
|
||||
const optionsArray = [validOptions];
|
||||
|
||||
expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
234
spec/Adapters/Auth/wechat.spec.js
Normal file
234
spec/Adapters/Auth/wechat.spec.js
Normal file
@@ -0,0 +1,234 @@
|
||||
const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
|
||||
|
||||
describe('WeChatAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeChatAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
|
||||
);
|
||||
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeChatAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeChatAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
204
spec/Adapters/Auth/weibo.spec.js
Normal file
204
spec/Adapters/Auth/weibo.spec.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
|
||||
|
||||
describe('WeiboAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeiboAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error if code or redirect_uri is missing', async function () {
|
||||
const invalidAuthData = [
|
||||
{},
|
||||
{ code: 'validCode' },
|
||||
{ redirect_uri: 'http://example.com/callback' },
|
||||
];
|
||||
|
||||
for (const authData of invalidAuthData) {
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth requires code and redirect_uri to be sent.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'validUserId' };
|
||||
const user = await adapter.getUserFromAccessToken('validToken', authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/get_token_info',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'invalidUserId' };
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/access_token',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeiboAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
weibo: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeiboAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('weibo', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user