|
|
@@ -6,6 +6,7 @@ import {
|
|
|
CreateRole,
|
|
|
LoginMutationArgs,
|
|
|
Permission,
|
|
|
+ RegisterCustomerInput,
|
|
|
UpdateProductMutationArgs,
|
|
|
} from 'shared/generated-types';
|
|
|
import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from 'shared/shared-constants';
|
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
GET_PRODUCT_LIST,
|
|
|
UPDATE_PRODUCT,
|
|
|
} from '../../admin-ui/src/app/data/definitions/product-definitions';
|
|
|
+import { NoopEmailGenerator } from '../src/config/email/noop-email-generator';
|
|
|
+import { defaultEmailTypes } from '../src/email/default-email-types';
|
|
|
|
|
|
import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
|
|
|
import { TestClient } from './test-client';
|
|
|
@@ -28,12 +31,25 @@ import { TestServer } from './test-server';
|
|
|
describe('Authorization & permissions', () => {
|
|
|
const client = new TestClient();
|
|
|
const server = new TestServer();
|
|
|
+ let sendEmailFn: jest.Mock;
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
- const token = await server.init({
|
|
|
- productCount: 1,
|
|
|
- customerCount: 1,
|
|
|
- });
|
|
|
+ const token = await server.init(
|
|
|
+ {
|
|
|
+ productCount: 1,
|
|
|
+ customerCount: 1,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ emailOptions: {
|
|
|
+ emailTypes: defaultEmailTypes,
|
|
|
+ generator: new NoopEmailGenerator(),
|
|
|
+ transport: {
|
|
|
+ type: 'testing',
|
|
|
+ onSend: ctx => sendEmailFn(ctx),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ );
|
|
|
await client.init();
|
|
|
}, TEST_SETUP_TIMEOUT_MS);
|
|
|
|
|
|
@@ -41,87 +57,212 @@ describe('Authorization & permissions', () => {
|
|
|
await server.destroy();
|
|
|
});
|
|
|
|
|
|
- describe('Anonymous user', () => {
|
|
|
- beforeAll(async () => {
|
|
|
- await client.asAnonymousUser();
|
|
|
+ beforeEach(() => {
|
|
|
+ sendEmailFn = jest.fn();
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('admin permissions', () => {
|
|
|
+ describe('Anonymous user', () => {
|
|
|
+ beforeAll(async () => {
|
|
|
+ await client.asAnonymousUser();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('can attempt login', async () => {
|
|
|
+ await assertRequestAllowed<LoginMutationArgs>(ATTEMPT_LOGIN, {
|
|
|
+ username: SUPER_ADMIN_USER_IDENTIFIER,
|
|
|
+ password: SUPER_ADMIN_USER_PASSWORD,
|
|
|
+ rememberMe: false,
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
- it('can attempt login', async () => {
|
|
|
- await assertRequestAllowed<LoginMutationArgs>(ATTEMPT_LOGIN, {
|
|
|
- username: SUPER_ADMIN_USER_IDENTIFIER,
|
|
|
- password: SUPER_ADMIN_USER_PASSWORD,
|
|
|
- rememberMe: false,
|
|
|
+ describe('ReadCatalog', () => {
|
|
|
+ beforeAll(async () => {
|
|
|
+ await client.asSuperAdmin();
|
|
|
+ const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
|
|
|
+ Permission.ReadCatalog,
|
|
|
+ ]);
|
|
|
+ await client.asUserWithCredentials(identifier, password);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('can read', async () => {
|
|
|
+ await assertRequestAllowed(GET_PRODUCT_LIST);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('cannot uppdate', async () => {
|
|
|
+ await assertRequestForbidden<UpdateProductMutationArgs>(UPDATE_PRODUCT, {
|
|
|
+ input: {
|
|
|
+ id: '1',
|
|
|
+ translations: [],
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('cannot create', async () => {
|
|
|
+ await assertRequestForbidden<CreateProductMutationArgs>(CREATE_PRODUCT, {
|
|
|
+ input: {
|
|
|
+ translations: [],
|
|
|
+ },
|
|
|
+ });
|
|
|
});
|
|
|
});
|
|
|
- });
|
|
|
|
|
|
- describe('ReadCatalog', () => {
|
|
|
- beforeAll(async () => {
|
|
|
- await client.asSuperAdmin();
|
|
|
- const { identifier, password } = await createAdministratorWithPermissions('ReadCatalog', [
|
|
|
- Permission.ReadCatalog,
|
|
|
- ]);
|
|
|
- await client.asUserWithCredentials(identifier, password);
|
|
|
+ describe('CRUD on Customers', () => {
|
|
|
+ beforeAll(async () => {
|
|
|
+ await client.asSuperAdmin();
|
|
|
+ const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
|
|
|
+ Permission.CreateCustomer,
|
|
|
+ Permission.ReadCustomer,
|
|
|
+ Permission.UpdateCustomer,
|
|
|
+ Permission.DeleteCustomer,
|
|
|
+ ]);
|
|
|
+ await client.asUserWithCredentials(identifier, password);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('can create', async () => {
|
|
|
+ await assertRequestAllowed(
|
|
|
+ gql`
|
|
|
+ mutation CreateCustomer($input: CreateCustomerInput!) {
|
|
|
+ createCustomer(input: $input) {
|
|
|
+ id
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `,
|
|
|
+ { input: { emailAddress: '', firstName: '', lastName: '' } },
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it('can read', async () => {
|
|
|
+ await assertRequestAllowed(gql`
|
|
|
+ query {
|
|
|
+ customers {
|
|
|
+ totalItems
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `);
|
|
|
+ });
|
|
|
});
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('customer account creation', () => {
|
|
|
+ const password = 'password';
|
|
|
+ const emailAddress = 'test1@test.com';
|
|
|
+ let verificationToken: string;
|
|
|
+
|
|
|
+ it('register a new account', async () => {
|
|
|
+ const verificationTokenPromise = getVerificationTokenPromise();
|
|
|
+ const input: RegisterCustomerInput = {
|
|
|
+ firstName: 'Sean',
|
|
|
+ lastName: 'Tester',
|
|
|
+ emailAddress,
|
|
|
+ };
|
|
|
+ const result = await client.query(REGISTER_ACCOUNT, { input });
|
|
|
+
|
|
|
+ verificationToken = await verificationTokenPromise;
|
|
|
|
|
|
- it('can read', async () => {
|
|
|
- await assertRequestAllowed(GET_PRODUCT_LIST);
|
|
|
+ expect(result.registerCustomerAccount).toBe(true);
|
|
|
+ expect(sendEmailFn).toHaveBeenCalled();
|
|
|
+ expect(verificationToken).toBeDefined();
|
|
|
});
|
|
|
|
|
|
- it('cannot uppdate', async () => {
|
|
|
- await assertRequestForbidden<UpdateProductMutationArgs>(UPDATE_PRODUCT, {
|
|
|
- input: {
|
|
|
- id: '1',
|
|
|
- translations: [],
|
|
|
- },
|
|
|
+ it('issues a new token if attempting to register a second time', async () => {
|
|
|
+ const sendEmail = new Promise<string>(resolve => {
|
|
|
+ sendEmailFn.mockImplementation(ctx => {
|
|
|
+ resolve(ctx.event.user.verificationToken);
|
|
|
+ });
|
|
|
});
|
|
|
+ const input: RegisterCustomerInput = {
|
|
|
+ firstName: 'Sean',
|
|
|
+ lastName: 'Tester',
|
|
|
+ emailAddress,
|
|
|
+ };
|
|
|
+ const result = await client.query(REGISTER_ACCOUNT, { input });
|
|
|
+
|
|
|
+ const newVerificationToken = await sendEmail;
|
|
|
+
|
|
|
+ expect(result.registerCustomerAccount).toBe(true);
|
|
|
+ expect(sendEmailFn).toHaveBeenCalled();
|
|
|
+ expect(newVerificationToken).not.toBe(verificationToken);
|
|
|
+
|
|
|
+ verificationToken = newVerificationToken;
|
|
|
});
|
|
|
|
|
|
- it('cannot create', async () => {
|
|
|
- await assertRequestForbidden<CreateProductMutationArgs>(CREATE_PRODUCT, {
|
|
|
- input: {
|
|
|
- translations: [],
|
|
|
- },
|
|
|
+ it('refreshCustomerVerification issues a new token', async () => {
|
|
|
+ const sendEmail = new Promise<string>(resolve => {
|
|
|
+ sendEmailFn.mockImplementation(ctx => {
|
|
|
+ resolve(ctx.event.user.verificationToken);
|
|
|
+ });
|
|
|
});
|
|
|
+ const result = await client.query(REFRESH_TOKEN, { emailAddress });
|
|
|
+
|
|
|
+ const newVerificationToken = await sendEmail;
|
|
|
+
|
|
|
+ expect(result.refreshCustomerVerification).toBe(true);
|
|
|
+ expect(sendEmailFn).toHaveBeenCalled();
|
|
|
+ expect(newVerificationToken).not.toBe(verificationToken);
|
|
|
+
|
|
|
+ verificationToken = newVerificationToken;
|
|
|
});
|
|
|
- });
|
|
|
+ it('refreshCustomerVerification does nothing with an unrecognized emailAddress', async () => {
|
|
|
+ const result = await client.query(REFRESH_TOKEN, {
|
|
|
+ emailAddress: 'never-been-registered@test.com',
|
|
|
+ });
|
|
|
|
|
|
- describe('CRUD on Customers', () => {
|
|
|
- beforeAll(async () => {
|
|
|
- await client.asSuperAdmin();
|
|
|
- const { identifier, password } = await createAdministratorWithPermissions('CRUDCustomer', [
|
|
|
- Permission.CreateCustomer,
|
|
|
- Permission.ReadCustomer,
|
|
|
- Permission.UpdateCustomer,
|
|
|
- Permission.DeleteCustomer,
|
|
|
- ]);
|
|
|
- await client.asUserWithCredentials(identifier, password);
|
|
|
+ expect(result.refreshCustomerVerification).toBe(true);
|
|
|
+ expect(sendEmailFn).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- it('can create', async () => {
|
|
|
- await assertRequestAllowed(
|
|
|
- gql`
|
|
|
- mutation CreateCustomer($input: CreateCustomerInput!) {
|
|
|
- createCustomer(input: $input) {
|
|
|
- id
|
|
|
- }
|
|
|
- }
|
|
|
- `,
|
|
|
- { input: { emailAddress: '', firstName: '', lastName: '' } },
|
|
|
- );
|
|
|
+ it('login fails before verification', async () => {
|
|
|
+ try {
|
|
|
+ await client.asUserWithCredentials(emailAddress, '');
|
|
|
+ fail('should have thrown');
|
|
|
+ } catch (err) {
|
|
|
+ expect(getErrorStatusCode(err)).toBe(401);
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
- it('can read', async () => {
|
|
|
- await assertRequestAllowed(gql`
|
|
|
- query {
|
|
|
- customers {
|
|
|
- totalItems
|
|
|
- }
|
|
|
- }
|
|
|
- `);
|
|
|
+ it('verification fails with wrong token', async () => {
|
|
|
+ try {
|
|
|
+ await client.query(VERIFY_EMAIL, {
|
|
|
+ password,
|
|
|
+ token: 'bad-token',
|
|
|
+ });
|
|
|
+ fail('should have thrown');
|
|
|
+ } catch (err) {
|
|
|
+ expect(err.message).toEqual(expect.stringContaining(`Verification token not recognized`));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('verification succeeds with correct token', async () => {
|
|
|
+ const result = await client.query(VERIFY_EMAIL, {
|
|
|
+ password,
|
|
|
+ token: verificationToken,
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.verifyCustomerAccount.user.identifier).toBe('test1@test.com');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('verification fails if attempted a second time', async () => {
|
|
|
+ try {
|
|
|
+ await client.query(VERIFY_EMAIL, {
|
|
|
+ password,
|
|
|
+ token: verificationToken,
|
|
|
+ });
|
|
|
+ fail('should have thrown');
|
|
|
+ } catch (err) {
|
|
|
+ expect(err.message).toEqual(expect.stringContaining(`Verification token not recognized`));
|
|
|
+ }
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ function getVerificationTokenPromise(): Promise<string> {
|
|
|
+ return new Promise<string>(resolve => {
|
|
|
+ sendEmailFn.mockImplementation(ctx => {
|
|
|
+ resolve(ctx.event.user.verificationToken);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
async function assertRequestAllowed<V>(operation: DocumentNode, variables?: V) {
|
|
|
try {
|
|
|
const status = await client.queryStatus(operation, variables);
|
|
|
@@ -187,4 +328,27 @@ describe('Authorization & permissions', () => {
|
|
|
password,
|
|
|
};
|
|
|
}
|
|
|
+
|
|
|
+ const REGISTER_ACCOUNT = gql`
|
|
|
+ mutation Register($input: RegisterCustomerInput!) {
|
|
|
+ registerCustomerAccount(input: $input)
|
|
|
+ }
|
|
|
+ `;
|
|
|
+
|
|
|
+ const VERIFY_EMAIL = gql`
|
|
|
+ mutation Verify($password: String!, $token: String!) {
|
|
|
+ verifyCustomerAccount(password: $password, token: $token) {
|
|
|
+ user {
|
|
|
+ id
|
|
|
+ identifier
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `;
|
|
|
+
|
|
|
+ const REFRESH_TOKEN = gql`
|
|
|
+ mutation RefreshToken($emailAddress: String!) {
|
|
|
+ refreshCustomerVerification(emailAddress: $emailAddress)
|
|
|
+ }
|
|
|
+ `;
|
|
|
});
|