test: re-test previously decreased coverage because of new implementation

This commit is contained in:
Nicolas Meienberger 2023-05-05 08:45:34 +02:00 committed by Nicolas Meienberger
parent 8f18a76120
commit 10f3c9efcf
11 changed files with 259 additions and 25 deletions

View file

@ -12,8 +12,6 @@ module.exports = {
'plugin:react/recommended',
'plugin:jsdoc/recommended',
'plugin:jsx-a11y/recommended',
'plugin:testing-library/react',
'plugin:jest-dom/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -39,6 +37,12 @@ module.exports = {
'class-methods-use-this': 0,
'jsdoc/require-returns': 0,
},
overrides: [
{
files: ['*.test.ts', '*.test.tsx'],
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
},
],
globals: {
JSX: true,
NodeJS: true,

View file

@ -14,10 +14,11 @@ export const createClient = jest.fn(() => {
ttl: (key: string) => expirations.get(key),
on: jest.fn(),
keys: (key: string) => {
const keyprefix = key.substring(0, key.length - 1);
const keys = [];
// eslint-disable-next-line no-restricted-syntax
for (const [k] of values) {
if (k.startsWith(key)) {
if (k.startsWith(keyprefix)) {
keys.push(k);
}
}

View file

@ -18,6 +18,10 @@ jest.mock('next/router', () => {
};
});
beforeEach(() => {
pushFn.mockClear();
});
describe('Test: LoginContainer', () => {
it('should render without error', () => {
// Arrange

View file

@ -18,6 +18,10 @@ jest.mock('next/router', () => {
};
});
beforeEach(() => {
pushFn.mockClear();
});
describe('Test: RegisterContainer', () => {
it('should render without error', () => {
render(<RegisterContainer />);
@ -25,7 +29,7 @@ describe('Test: RegisterContainer', () => {
expect(screen.getByText('Register')).toBeInTheDocument();
});
it.only('should redirect to / upon successful registration', async () => {
it('should redirect to / upon successful registration', async () => {
// Arrange
const email = faker.internet.email();
const password = faker.internet.password();

View file

@ -1,6 +1,9 @@
import React from 'react';
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
import { server } from '@/client/mocks/server';
import { StatusProvider } from '@/components/hoc/StatusProvider';
import { renderHook } from '@testing-library/react';
import { useSystemStore } from '@/client/state/systemStore';
import { GeneralActions } from './GeneralActions';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
@ -32,9 +35,40 @@ describe('Test: GeneralActions', () => {
});
});
it('should set poll status to true if update mutation succeeds', async () => {
// arrange
server.use(getTRPCMock({ path: ['system', 'getVersion'], response: { current: '1.0.0', latest: '2.0.0', body: '' } }));
server.use(getTRPCMock({ path: ['system', 'update'], type: 'mutation', response: true }));
const { result } = renderHook(() => useSystemStore());
result.current.setStatus('RUNNING');
render(
<StatusProvider>
<GeneralActions />
</StatusProvider>,
);
await waitFor(() => {
expect(screen.getByText('Update to 2.0.0')).toBeInTheDocument();
});
const updateButton = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButton);
// act
const updateButtonModal = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButtonModal);
result.current.setStatus('UPDATING');
// assert
await waitFor(() => {
expect(screen.getByText('Your system is updating...')).toBeInTheDocument();
});
expect(result.current.pollStatus).toBe(true);
});
it('should show toast if restart mutation fails', async () => {
// arrange
server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went badly' }));
render(<GeneralActions />);
const restartButton = screen.getByRole('button', { name: /Restart/i });
@ -45,7 +79,35 @@ describe('Test: GeneralActions', () => {
// assert
await waitFor(() => {
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
expect(screen.getByText(/Something went badly/)).toBeInTheDocument();
});
});
it('should set poll status to true if restart mutation succeeds', async () => {
// arrange
server.use(getTRPCMock({ path: ['system', 'restart'], type: 'mutation', response: true }));
const { result } = renderHook(() => useSystemStore());
result.current.setStatus('RUNNING');
render(
<StatusProvider>
<GeneralActions />
</StatusProvider>,
);
const restartButton = screen.getByRole('button', { name: /Restart/i });
// act
fireEvent.click(restartButton);
const restartButtonModal = screen.getByRole('button', { name: /Restart/i });
fireEvent.click(restartButtonModal);
result.current.setStatus('RESTARTING');
// assert
await waitFor(() => {
expect(screen.getByText('Your system is restarting...')).toBeInTheDocument();
});
expect(result.current.pollStatus).toBe(true);
});
});

View file

@ -68,20 +68,6 @@ class TipiCache {
return Promise.all(promises);
}
public async delByValue(value: string, prefix: string) {
const client = await this.getClient();
const keys = await client.keys(`${prefix}*`);
const promises = keys.map(async (key) => {
const val = await client.get(key);
if (val === value) {
await client.del(key);
}
});
return Promise.all(promises);
}
public async close() {
return this.client.quit();
}

View file

@ -1,5 +1,137 @@
import { fromPartial } from '@total-typescript/shoehorn';
import { authRouter } from './auth.router';
import { TestDatabase, clearDatabase, closeDatabase, setupTestSuite } from '@/server/tests/test-utils';
import { createUser } from '@/server/tests/user.factory';
import { AuthRouter } from './auth.router';
let db: TestDatabase;
let authRouter: AuthRouter;
const TEST_SUITE = 'authrouter';
jest.mock('fs-extra');
beforeAll(async () => {
const testSuite = await setupTestSuite(TEST_SUITE);
db = testSuite;
authRouter = (await import('./auth.router')).authRouter;
});
beforeEach(async () => {
await clearDatabase(db);
});
afterAll(async () => {
await closeDatabase(db);
});
describe('Test: login', () => {
it('should be accessible without an account', async () => {
// arrange
const caller = authRouter.createCaller(fromPartial({ req: { session: {} } }));
let error;
// act
try {
await caller.login({ password: '123456', username: 'test' });
} catch (e) {
error = e as { code: string };
}
// assert
expect(error?.code).not.toBe('UNAUTHORIZED');
});
});
describe('Test: logout', () => {
it('should not be accessible without an account', async () => {
// arrange
// @ts-expect-error - we're testing the error case
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456, destroy: (cb) => cb() } } }));
let error;
// act
try {
await caller.logout();
} catch (e) {
error = e as { code: string };
}
// assert
expect(error?.code).toBe('UNAUTHORIZED');
});
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 123456 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } }));
let error;
// act
try {
await caller.logout();
} catch (e) {
error = e as { code: string };
}
// assert
expect(error?.code).not.toBe('UNAUTHORIZED');
});
});
describe('Test: register', () => {
it('should be accessible without an account', async () => {
// arrange
const caller = authRouter.createCaller(fromPartial({ req: { session: {} } }));
let error;
// act
try {
await caller.register({ username: 'test@test.com', password: '123' });
} catch (e) {
error = e as { code: string };
}
// assert
expect(error?.code).not.toBe('UNAUTHORIZED');
});
});
describe('Test: me', () => {
it('should be accessible without an account', async () => {
// arrange
const caller = authRouter.createCaller(fromPartial({ req: { session: {} } }));
// act
const result = await caller.me();
// assert
expect(result).toBe(null);
});
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 123456 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } }));
// act
const result = await caller.me();
// assert
expect(result).not.toBe(null);
expect(result?.id).toBe(123456);
});
});
describe('Test: isConfigured', () => {
it('should be accessible without an account', async () => {
// arrange
const caller = authRouter.createCaller(fromPartial({ req: { session: {} } }));
// act
const result = await caller.isConfigured();
// assert
expect(result).toBe(false);
});
});
describe('Test: verifyTotp', () => {
it('should be accessible without an account', async () => {
@ -40,6 +172,7 @@ describe('Test: getTotpUri', () => {
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 123456 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } }));
let error;
@ -74,6 +207,7 @@ describe('Test: setupTotp', () => {
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 123456 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } }));
let error;
@ -108,6 +242,7 @@ describe('Test: disableTotp', () => {
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 123456 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 123456 } } }));
let error;
@ -177,6 +312,7 @@ describe('Test: resetPassword', () => {
it('should be accessible with an account', async () => {
// arrange
await createUser({ id: 122 }, db);
const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 122 } } }));
let error;

View file

@ -24,3 +24,5 @@ export const authRouter = router({
setupTotp: protectedProcedure.input(z.object({ totpCode: z.string() })).mutation(({ input, ctx }) => AuthService.setupTotp({ userId: Number(ctx.req.session.userId), totpCode: input.totpCode })),
disableTotp: protectedProcedure.input(z.object({ password: z.string() })).mutation(({ input, ctx }) => AuthService.disableTotp({ userId: Number(ctx.req.session.userId), password: input.password })),
});
export type AuthRouter = typeof authRouter;

View file

@ -672,4 +672,20 @@ describe('Test: changePassword', () => {
// act & assert
await expect(AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'password' })).rejects.toThrowError('Changing password is not allowed in demo mode');
});
it('should delete all sessions for the user', async () => {
// arrange
const email = faker.internet.email();
const user = await createUser({ email }, database);
const newPassword = faker.internet.password();
await TipiCache.set(`session:${user.id}:${faker.random.word()}`, 'test');
// act
await AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'password' });
// assert
// eslint-disable-next-line testing-library/no-await-sync-query
const sessions = await TipiCache.getByPrefix(`session:${user.id}:`);
expect(sessions).toHaveLength(0);
});
});

View file

@ -7,6 +7,7 @@ import { AuthQueries } from '@/server/queries/auth/auth.queries';
import { Context } from '@/server/context';
import { getConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache';
import { Logger } from '../../core/Logger';
import { fileExists, unlinkFile } from '../../common/fs.helpers';
import { decrypt, encrypt } from '../../utils/encryption';
@ -345,10 +346,12 @@ export class AuthServiceClass {
* @param {number} userId - The user ID
*/
private destroyAllSessionsByUserId = async (userId: number) => {
const sessions = await TipiCache.getByPrefix(`session:${userId}:`);
for (const session of sessions) {
await TipiCache.del(session.key);
}
await TipiCache.getByPrefix(`session:${userId}:`).then((sessions) => {
sessions.forEach((session) => {
TipiCache.del(session.key).then(() => Logger.info('Session deleted'));
TipiCache.del(`tipi:${session.val}`).then(() => Logger.info('Session key deleted'));
});
});
};
public changePassword = async (params: { currentPassword: string; newPassword: string; userId: number }) => {

View file

@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax */
import pg, { Pool } from 'pg';
import { NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres';
import { runPostgresMigrations } from '../run-migration';
@ -53,4 +54,19 @@ const closeDatabase = async (database: TestDatabase) => {
await database.client.end();
};
/**
* Setup a test suite by mocking the database.
*
* @param {string} testSuite - name of the test suite
*/
export async function setupTestSuite(testSuite: string) {
const db = await createDatabase(testSuite);
jest.mock('../db', () => {
return { db: db.db };
});
return { db: db.db, client: db.client };
}
export { createDatabase, clearDatabase, closeDatabase };