test: re-test previously decreased coverage because of new implementation
This commit is contained in:
parent
8f18a76120
commit
10f3c9efcf
11 changed files with 259 additions and 25 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ jest.mock('next/router', () => {
|
|||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
pushFn.mockClear();
|
||||
});
|
||||
|
||||
describe('Test: LoginContainer', () => {
|
||||
it('should render without error', () => {
|
||||
// Arrange
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue