Skip to content

Commit

Permalink
move spys to beforeAll, add another test user, other cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
alanb2718 committed Nov 6, 2023
1 parent 6d9b37e commit c1a954a
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 3.0.6 (UNRELEASED)
* The `weekdays` filter parameter for the `GetSearchResults` API now accept a comma separated string (e.g. `weekdays=1,-2`) in addition to the standard array syntax.
* Added unit tests for login in the new UI. (These are also intended to be prototypes for other UI unit tests.)

## 3.0.5 (June 3, 2023)
* Change name `Disabled User` to `Deactivated User`.
Expand Down
125 changes: 80 additions & 45 deletions src/resources/js/test/loginForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ResponseError, Token, User } from 'bmlt-root-server-client';
import { describe, test, vi } from 'vitest';
import { afterAll, beforeAll, beforeEach, describe, test, vi } from 'vitest';

import App from '../App';
import ApiClientWrapper from '../RootServerApi';

// define a mock authToken that expires 1 hour from now
// (the token uses PHP's time rather than Javascript's time, so seconds from the epoch instead of milliseconds)
const now: number = Math.round(new Date().valueOf() / 1000);
const mockedToken: Token = {
accessToken: 'mysteryString42',
expiresAt: now + 60 * 60,
tokenType: 'bearer',
userId: 1,
};

// for now just define a serveradmin user
const mockUser: User = {
const mockServerAdmin: User = {
description: 'Main Server Administrator',
displayName: 'Server Administrator',
email: 'nobody@bmlt.app',
Expand All @@ -27,9 +16,34 @@ const mockUser: User = {
username: 'serveradmin',
};

const mockAreaAdmin: User = {
description: 'River City Area Administrator',
displayName: 'River City Area',
email: 'nobody@bmlt.app',
id: 6,
ownerId: '5',
type: 'serviceBodyAdmin',
username: 'RiverCityArea',
};

const allMockUsers = [mockServerAdmin, mockAreaAdmin];
const allMockPasswords = ['serveradmin-password', 'rivercity-password'];

// mocked access token
let savedAccessToken: Token | null;

// define a mock authToken that expires 1 hour from now
function generateMockToken(u: User): Token {
// the token uses PHP's time rather than Javascript's time, so seconds from the epoch instead of milliseconds
const now: number = Math.round(new Date().valueOf() / 1000);
return {
accessToken: 'mysteryString42',
expiresAt: now + 60 * 60,
tokenType: 'bearer',
userId: u.id,
};
}

function mockGetToken(): Token | null {
return savedAccessToken;
}
Expand All @@ -39,47 +53,56 @@ function mockSetToken(token: Token | null): void {
}

async function mockGetUser(params: { userId: number }): Promise<User> {
if (params.userId == mockUser.id) {
const mockUser = allMockUsers.find((u) => u.id === params.userId);
if (mockUser) {
return mockUser;
}
throw new Error('no user found with the given userId');
throw new Error('unknown user -- something went wrong');
}

function mockIsLoggedIn(): boolean {
return Boolean(savedAccessToken);
}

async function mockAuthToken(authTokenRequest: { tokenCredentials: { username: string; password: string } }): Promise<Token> {
if (authTokenRequest.tokenCredentials.username === 'serveradmin' && authTokenRequest.tokenCredentials.password === 'good-password') {
return mockedToken;
} else {
const msg = '{ "message": "The provided credentials are incorrect." }';
const unicodeMsg = Uint8Array.from(Array.from(msg).map((x) => x.charCodeAt(0)));
const strm = new ReadableStream({
start(controller) {
controller.enqueue(unicodeMsg);
controller.close();
},
});
const r: Response = new Response(strm, { status: 401, statusText: 'Unauthorized' });
throw new ResponseError(r, 'Response returned an error code');
const n = authTokenRequest.tokenCredentials.username;
const p = authTokenRequest.tokenCredentials.password;
for (let i = 0; i < allMockUsers.length; i++) {
if (allMockUsers[i].username === n && allMockPasswords[i] === p) {
return generateMockToken(allMockUsers[i]);
}
}
const msg = '{ "message": "The provided credentials are incorrect." }';
const unicodeMsg = Uint8Array.from(Array.from(msg).map((x) => x.charCodeAt(0)));
const strm = new ReadableStream({
start(controller) {
controller.enqueue(unicodeMsg);
controller.close();
},
});
const r = new Response(strm, { status: 401, statusText: 'Unauthorized' });
throw new ResponseError(r, 'Response returned an error code');
}

async function mockAuthLogout(): Promise<void> {
savedAccessToken = null;
}

vi.spyOn(ApiClientWrapper.api, 'token', 'get').mockImplementation(mockGetToken);
vi.spyOn(ApiClientWrapper.api, 'token', 'set').mockImplementation(mockSetToken);
vi.spyOn(ApiClientWrapper.api, 'isLoggedIn', 'get').mockImplementation(mockIsLoggedIn);
vi.spyOn(ApiClientWrapper.api, 'getUser').mockImplementation(mockGetUser);
vi.spyOn(ApiClientWrapper.api, 'authToken').mockImplementation(mockAuthToken);
vi.spyOn(ApiClientWrapper.api, 'authLogout').mockImplementation(mockAuthLogout);
beforeAll(async () => {
vi.spyOn(ApiClientWrapper.api, 'token', 'get').mockImplementation(mockGetToken);
vi.spyOn(ApiClientWrapper.api, 'token', 'set').mockImplementation(mockSetToken);
vi.spyOn(ApiClientWrapper.api, 'isLoggedIn', 'get').mockImplementation(mockIsLoggedIn);
vi.spyOn(ApiClientWrapper.api, 'getUser').mockImplementation(mockGetUser);
vi.spyOn(ApiClientWrapper.api, 'authToken').mockImplementation(mockAuthToken);
vi.spyOn(ApiClientWrapper.api, 'authLogout').mockImplementation(mockAuthLogout);
});

beforeEach(async () => {
savedAccessToken = null;
});

describe('Login', () => {
test('check login page before logging in', () => {
savedAccessToken = null;
render(<App />);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Root Server');
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Login');
Expand All @@ -89,16 +112,14 @@ describe('Login', () => {
});

test('missing username', async () => {
savedAccessToken = null;
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByLabelText(/password/i), 'good-password');
await user.type(screen.getByLabelText(/password/i), 'serveradmin-password');
await user.click(screen.getByRole('button', { name: 'Log In' }));
expect(screen.getByText(/Username is required/)).toBeInTheDocument();
});

test('missing password', async () => {
savedAccessToken = null;
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole('textbox', { name: 'Username' }), 'serveradmin');
Expand All @@ -107,7 +128,6 @@ describe('Login', () => {
});

test('invalid password', async () => {
savedAccessToken = null;
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole('textbox', { name: 'Username' }), 'serveradmin');
Expand All @@ -118,19 +138,34 @@ describe('Login', () => {
await screen.findByText(/The provided credentials are incorrect./);
});

test('log in with valid username and password, then log out again', async () => {
savedAccessToken = null;
test('log in with valid username and password for the server administrator, then log out', async () => {
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole('textbox', { name: 'Username' }), 'serveradmin');
await user.type(screen.getByLabelText(/password/i), 'good-password');
await user.type(screen.getByLabelText(/password/i), 'serveradmin-password');
expect(screen.getByRole('textbox', { name: 'Username' })).toHaveDisplayValue('serveradmin');
expect(screen.getByLabelText(/password/i)).toHaveDisplayValue('good-password');
expect(screen.getByLabelText(/password/i)).toHaveDisplayValue('serveradmin-password');
await user.click(screen.getByRole('button', { name: 'Log In' }));
// after a successful login, we should see the Dashboard
await screen.findByText(/Dashboard/i);
// after a successful login, we should see the dashboard, including the word 'Dashboard' and the user name
await screen.findByText(/Dashboard/);
await screen.findByText(/Server Administrator/);
// log out, and make sure we're back at the login screen
await user.click(screen.getByRole('button', { name: 'Sign Out' }));
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Login');
});

test('log in with valid username and password for an area administrator', async () => {
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole('textbox', { name: 'Username' }), 'RiverCityArea');
await user.type(screen.getByLabelText(/password/i), 'rivercity-password');
await user.click(screen.getByRole('button', { name: 'Log In' }));
// after a successful login, we should see the dashboard, including the word 'Dashboard' and the user name
await screen.findByText(/Dashboard/);
await screen.findByText(/River City Area/);
});
});

afterAll(async () => {
vi.restoreAllMocks();
});

0 comments on commit c1a954a

Please sign in to comment.