Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Canner PAT authenticator #181

Merged
merged 7 commits into from
Jun 13, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ it('Data source should export successfully', async () => {

it('Data source should throw when fail to export data', async () => {
// Arrange
// TODO: refactor to avoid stubbing private method
// stub the private function to manipulate getting error from the remote server
sinon.default
.stub(CannerAdapter.prototype, 'createAsyncQueryResultUrls')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ServeConfig, VulcanServer } from '@vulcan-sql/serve';
import * as supertest from 'supertest';
import * as md5 from 'md5';
import defaultConfig from './projectConfig';
import faker from '@faker-js/faker';

let server: VulcanServer;

Expand All @@ -23,29 +24,6 @@ const users = [
},
];

const projectConfig: ServeConfig & IBuildOptions = {
...defaultConfig,
auth: {
enabled: true,
options: {
basic: {
'users-list': [
{
name: users[0].name,
md5Password: md5(users[0].password),
attr: users[0].attr,
},
{
name: users[1].name,
md5Password: md5(users[1].password),
attr: users[1].attr,
},
],
},
},
},
};

afterEach(async () => {
await server.close();
});
Expand All @@ -54,6 +32,28 @@ it.each([...users])(
'Example 2: authenticate user identity by POST /auth/token API',
async ({ name, password }) => {
// Arrange
const projectConfig: ServeConfig & IBuildOptions = {
...defaultConfig,
auth: {
enabled: true,
options: {
basic: {
'users-list': [
{
name: users[0].name,
md5Password: md5(users[0].password),
attr: users[0].attr,
},
{
name: users[1].name,
md5Password: md5(users[1].password),
attr: users[1].attr,
},
],
},
},
},
};
const expected = Buffer.from(`${name}:${password}`).toString('base64');
const builder = new VulcanBuilder(projectConfig);
await builder.build();
Expand All @@ -76,3 +76,39 @@ it.each([...users])(
},
10000
);

it('Example 2: authenticate user identity by POST /auth/token API using PAT should get 400', async () => {
// Arrange
const projectConfig: ServeConfig & IBuildOptions = {
...defaultConfig,
auth: {
enabled: true,
options: {
'canner-pat': {
host: 'mockhost',
port: faker.datatype.number({ min: 20000, max: 30000 }),
ssl: false,
},
},
},
};
const builder = new VulcanBuilder(projectConfig);
await builder.build();
server = new VulcanServer(projectConfig);
const httpServer = (await server.start())['http'];
// Act
const agent = supertest(httpServer);

// Assert
const result = await agent
.post('/auth/token')
.send({
type: 'canner-pat',
})
.set('Accept', 'application/json')
.set('Authorization', 'Canner-PAT mocktoken');
expect(result.status).toBe(400);
expect(JSON.parse(result.text).message).toBe(
'canner-pat does not support token generate.'
);
}, 10000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build';
import {
ServeConfig,
VulcanServer,
CannerPATAuthenticator,
} from '@vulcan-sql/serve';
import * as supertest from 'supertest';
import defaultConfig from './projectConfig';
import * as sinon from 'ts-sinon';

describe('Example3: get user profile by GET /auth/user-profile API with Authorization', () => {
let server: VulcanServer;
let projectConfig: ServeConfig & IBuildOptions;
const mockUser = {
username: 'apple Hey',
firstName: 'Hey',
lastName: 'apple',
accountRole: 'admin',

attributes: {
attr1: 100 * 10000,
attr2: 'Los Angeles',
},
createdAt: '2023-03-27T12:48:15.882Z',
email: 'Alvina_Farrell82@yahoo.com',
groups: [{ id: 1, name: 'group1' }],
};
const mockToken = `Canner-PAT myPATToken`;
const mockCannerUserResponse = {
status: 200,
data: {
data: {
userMe: mockUser,
},
},
};
const expectedUserProfile = {
name: mockUser.username,
attr: {
firstName: 'Hey',
lastName: 'apple',
accountRole: 'admin',

attributes: {
attr1: 100 * 10000,
attr2: 'Los Angeles',
},
createdAt: '2023-03-27T12:48:15.882Z',
email: 'Alvina_Farrell82@yahoo.com',
groups: [{ id: 1, name: 'group1' }],
},
};
// stub the private function to manipulate getting user info from remote server
const stubFetchCannerUser = (user: any) => {
const stub = sinon.default.stub(
CannerPATAuthenticator.prototype,
<any>'fetchCannerUser'
Copy link
Contributor

@kokokuo kokokuo Jun 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding the comment to describe what reason make here should use the prototype to stub the method because it's an unregular way to stub the method of a class.

Copy link
Contributor

@kokokuo kokokuo Jun 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, here you could also have another way to prevent using the prototype to stub private method, the solution ( Depends on you, two way both work, just need to add the comment if you use the unregular solution):

  1. Could create a folder e.g: canner/
  2. Move the CannerPATAuthenticator class to the folder canner/
  3. Make fetchCannerUser private method to a function expression object, and put it in a helpers.ts file, move the helpers.ts under the canner/, it means the helpers.ts only used in the related file under the canner/.
  4. import the fetchCannerUser function from helpers and call it in the CannerPATAuthenticator, then you could stub the function in the test cases, and prevent stubbing the private method.

PS: if your private method could be a common method and make multiple places to use, then you could create a folder utils/ under the libs/ and make the private common method a function and put it in a file under the utils/, and write the test case for the function under the test

You could refer to the flattenElements.ts and https://github.com/Canner/vulcan-sql/blob/2b5e282aa047cfaff1c80471e2e4bca119bde769/packages/core/src/lib/utils/normalizedStringValue.ts under the core package, we also write the tests case for them under the test/utils :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the solution!
I'll add some comments for now, and I think we can encapsulate how to make requests to Canner and move it to utils

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you forget to add the // TODO: refactor to avoid stubbing private method ?

);
stub.resolves(user);
return stub;
};
beforeEach(async () => {
projectConfig = {
...defaultConfig,
auth: {
enabled: true,
options: {
'canner-pat': {
host: 'mockhost',
port: 3000,
ssl: false,
},
},
},
};
});

afterEach(async () => {
sinon.default.restore();
await server?.close();
});

it('Example 3-1: set Authorization in header with default options', async () => {
stubFetchCannerUser(mockCannerUserResponse);
const builder = new VulcanBuilder(projectConfig);
await builder.build();
server = new VulcanServer(projectConfig);
const httpServer = (await server.start())['http'];

const agent = supertest(httpServer);
const result = await agent
.get('/auth/user-profile')
.set('Authorization', mockToken);
expect(result.body).toEqual(expectedUserProfile);
}, 10000);

it('Example 3-2: set Authorization in querying with default options', async () => {
projectConfig['auth-source'] = {
options: {
key: 'x-auth',
},
};
stubFetchCannerUser(mockCannerUserResponse);
const builder = new VulcanBuilder(projectConfig);
await builder.build();
server = new VulcanServer(projectConfig);
const httpServer = (await server.start())['http'];

const agent = supertest(httpServer);
const auth = Buffer.from(
JSON.stringify({
Authorization: `Canner-PAT ${Buffer.from(mockToken).toString(
'base64'
)}`,
})
).toString('base64');
const result = await agent.get(`/auth/user-profile?x-auth=${auth}`);

expect(result.body).toEqual(expectedUserProfile);
}, 10000);

it('Example 3-3: set Authorization in querying with specific auth "key" options', async () => {
projectConfig['auth-source'] = {
options: {
key: 'x-auth',
},
};
stubFetchCannerUser(mockCannerUserResponse);
const builder = new VulcanBuilder(projectConfig);
await builder.build();
server = new VulcanServer(projectConfig);
const httpServer = (await server.start())['http'];

const agent = supertest(httpServer);
const auth = Buffer.from(
JSON.stringify({
Authorization: `Canner-PAT ${Buffer.from(mockToken).toString(
'base64'
)}`,
})
).toString('base64');
const result = await agent.get(`/auth/user-profile?x-auth=${auth}`);

expect(result.body).toEqual(expectedUserProfile);
}, 10000);

it('Example 3-4: set Authorization in json payload specific auth "x-key" options', async () => {
projectConfig['auth-source'] = {
options: {
key: 'x-auth',
in: 'payload',
},
};
stubFetchCannerUser(mockCannerUserResponse);
const builder = new VulcanBuilder(projectConfig);
await builder.build();
server = new VulcanServer(projectConfig);
const httpServer = (await server.start())['http'];

const auth = Buffer.from(
JSON.stringify({
Authorization: `Canner-PAT ${Buffer.from(mockToken).toString(
'base64'
)}`,
})
).toString('base64');

const agent = supertest(httpServer);

const result = await agent
.get('/auth/user-profile')
.send({
['x-auth']: auth,
})
.set('Accept', 'application/json');

expect(result.body).toEqual(expectedUserProfile);
}, 10000);
});
Loading