test: fix tests and bump various dependencies
This commit is contained in:
parent
29c7f98a69
commit
79f1da00d0
17 changed files with 300 additions and 315 deletions
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest'],
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'next/core-web-vitals',
|
||||
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
@ -28,7 +29,7 @@ module.exports = {
|
|||
'react/jsx-props-no-spreading': 0,
|
||||
'react/no-unused-prop-types': 0,
|
||||
'react/button-has-type': 0,
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
|
||||
'no-underscore-dangle': 0,
|
||||
},
|
||||
globals: {
|
||||
|
|
|
@ -1,120 +1,107 @@
|
|||
import path from 'path';
|
||||
|
||||
const fs: {
|
||||
__createMockFiles: typeof createMockFiles;
|
||||
__resetAllMocks: typeof resetAllMocks;
|
||||
readFileSync: typeof readFileSync;
|
||||
existsSync: typeof existsSync;
|
||||
writeFileSync: typeof writeFileSync;
|
||||
mkdirSync: typeof mkdirSync;
|
||||
rmSync: typeof rmSync;
|
||||
readdirSync: typeof readdirSync;
|
||||
copyFileSync: typeof copyFileSync;
|
||||
copySync: typeof copyFileSync;
|
||||
createFileSync: typeof createFileSync;
|
||||
unlinkSync: typeof unlinkSync;
|
||||
} = jest.genMockFromModule('fs-extra');
|
||||
class FsMock {
|
||||
private static instance: FsMock;
|
||||
|
||||
let mockFiles = Object.create(null);
|
||||
private mockFiles = Object.create(null);
|
||||
|
||||
const createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
mockFiles = Object.create(null);
|
||||
// private constructor() {}
|
||||
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!mockFiles[dir]) {
|
||||
mockFiles[dir] = [];
|
||||
static getInstance(): FsMock {
|
||||
if (!FsMock.instance) {
|
||||
FsMock.instance = new FsMock();
|
||||
}
|
||||
|
||||
mockFiles[dir].push(path.basename(file));
|
||||
mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
const readFileSync = (p: string) => mockFiles[p];
|
||||
|
||||
const existsSync = (p: string) => mockFiles[p] !== undefined;
|
||||
|
||||
const writeFileSync = (p: string, data: string | string[]) => {
|
||||
mockFiles[p] = data;
|
||||
};
|
||||
|
||||
const mkdirSync = (p: string) => {
|
||||
mockFiles[p] = Object.create(null);
|
||||
};
|
||||
|
||||
const rmSync = (p: string) => {
|
||||
if (mockFiles[p] instanceof Array) {
|
||||
mockFiles[p].forEach((file: string) => {
|
||||
delete mockFiles[path.join(p, file)];
|
||||
});
|
||||
return FsMock.instance;
|
||||
}
|
||||
|
||||
delete mockFiles[p];
|
||||
};
|
||||
__createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
this.mockFiles = Object.create(null);
|
||||
|
||||
const readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
const depth = p.split('/').length;
|
||||
|
||||
Object.keys(mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(file));
|
||||
this.mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
__resetAllMocks = () => {
|
||||
this.mockFiles = Object.create(null);
|
||||
};
|
||||
|
||||
readFileSync = (p: string) => this.mockFiles[p];
|
||||
|
||||
existsSync = (p: string) => this.mockFiles[p] !== undefined;
|
||||
|
||||
writeFileSync = (p: string, data: string | string[]) => {
|
||||
this.mockFiles[p] = data;
|
||||
};
|
||||
|
||||
mkdirSync = (p: string) => {
|
||||
this.mockFiles[p] = Object.create(null);
|
||||
};
|
||||
|
||||
rmSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
};
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
const copyFileSync = (source: string, destination: string) => {
|
||||
mockFiles[destination] = mockFiles[source];
|
||||
};
|
||||
readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
|
||||
const copySync = (source: string, destination: string) => {
|
||||
mockFiles[destination] = mockFiles[source];
|
||||
const depth = p.split('/').length;
|
||||
|
||||
if (mockFiles[source] instanceof Array) {
|
||||
mockFiles[source].forEach((file: string) => {
|
||||
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
|
||||
Object.keys(this.mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFileSync = (p: string) => {
|
||||
mockFiles[p] = '';
|
||||
};
|
||||
return files;
|
||||
};
|
||||
|
||||
const resetAllMocks = () => {
|
||||
mockFiles = Object.create(null);
|
||||
};
|
||||
copyFileSync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
};
|
||||
|
||||
const unlinkSync = (p: string) => {
|
||||
if (mockFiles[p] instanceof Array) {
|
||||
mockFiles[p].forEach((file: string) => {
|
||||
delete mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete mockFiles[p];
|
||||
};
|
||||
copySync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
|
||||
fs.unlinkSync = unlinkSync;
|
||||
fs.readdirSync = readdirSync;
|
||||
fs.existsSync = existsSync;
|
||||
fs.readFileSync = readFileSync;
|
||||
fs.writeFileSync = writeFileSync;
|
||||
fs.mkdirSync = mkdirSync;
|
||||
fs.rmSync = rmSync;
|
||||
fs.copyFileSync = copyFileSync;
|
||||
fs.copySync = copySync;
|
||||
fs.createFileSync = createFileSync;
|
||||
fs.__createMockFiles = createMockFiles;
|
||||
fs.__resetAllMocks = resetAllMocks;
|
||||
if (this.mockFiles[source] instanceof Array) {
|
||||
this.mockFiles[source].forEach((file: string) => {
|
||||
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default fs;
|
||||
createFileSync = (p: string) => {
|
||||
this.mockFiles[p] = '';
|
||||
};
|
||||
|
||||
unlinkSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
getMockFiles = () => this.mockFiles;
|
||||
}
|
||||
|
||||
export default FsMock.getInstance();
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@tabler/core": "1.0.0-beta16",
|
||||
"@tabler/icons": "^1.109.0",
|
||||
"@tanstack/react-query": "^4.20.4",
|
||||
"@tanstack/react-query": "^4.24.4",
|
||||
"@trpc/client": "^10.7.0",
|
||||
"@trpc/next": "^10.7.0",
|
||||
"@trpc/react-query": "^10.7.0",
|
||||
"@trpc/server": "^10.7.0",
|
||||
"@trpc/next": "^10.9.1",
|
||||
"@trpc/react-query": "^10.9.1",
|
||||
"@trpc/server": "^10.9.1",
|
||||
"argon2": "^0.29.1",
|
||||
"clsx": "^1.1.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
|
@ -90,12 +90,13 @@
|
|||
"eslint-config-next": "13.1.1",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^27.1.7",
|
||||
"eslint-plugin-jsdoc": "^39.6.9",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"msw": "^0.49.2",
|
||||
"msw": "^1.0.0",
|
||||
"next-router-mock": "^0.8.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"prisma": "^4.8.0",
|
||||
|
|
|
@ -6,10 +6,6 @@ import { server } from '../../../../mocks/server';
|
|||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { SettingsContainer } from './SettingsContainer';
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.removeItem('token');
|
||||
});
|
||||
|
||||
describe('Test: SettingsContainer', () => {
|
||||
describe('UI', () => {
|
||||
it('renders without crashing', () => {
|
||||
|
@ -58,8 +54,7 @@ describe('Test: SettingsContainer', () => {
|
|||
it('should remove token from local storage on success', async () => {
|
||||
const current = '0.0.1';
|
||||
const latest = faker.system.semver();
|
||||
localStorage.setItem('token', 'token');
|
||||
|
||||
const removeItem = jest.spyOn(localStorage, 'removeItem');
|
||||
render(<SettingsContainer data={{ current, latest }} />);
|
||||
|
||||
const updateButton = screen.getByText('Update');
|
||||
|
@ -67,11 +62,9 @@ describe('Test: SettingsContainer', () => {
|
|||
fireEvent.click(updateButton);
|
||||
});
|
||||
|
||||
// wait 500 ms because localStore cannot be awaited in tests
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(localStorage.getItem('token')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(removeItem).toBeCalledWith('token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast on error', async () => {
|
||||
|
@ -98,7 +91,7 @@ describe('Test: SettingsContainer', () => {
|
|||
describe('Restart', () => {
|
||||
it('should remove token from local storage on success', async () => {
|
||||
const current = faker.system.semver();
|
||||
localStorage.setItem('token', 'token');
|
||||
const removeItem = jest.spyOn(localStorage, 'removeItem');
|
||||
|
||||
render(<SettingsContainer data={{ current }} />);
|
||||
const restartButton = screen.getByTestId('settings-modal-restart-button');
|
||||
|
@ -106,11 +99,9 @@ describe('Test: SettingsContainer', () => {
|
|||
fireEvent.click(restartButton);
|
||||
});
|
||||
|
||||
// wait 500 ms because localStore cannot be awaited in tests
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(localStorage.getItem('token')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(removeItem).toBeCalledWith('token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast on error', async () => {
|
||||
|
|
|
@ -17,7 +17,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
const { setDarkMode } = useUIStore();
|
||||
const { setStatus, setVersion } = useSystemStore();
|
||||
|
||||
// trpc.system.status.useQuery(undefined, { refetchInterval: 1000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
|
||||
trpc.system.status.useQuery(undefined, { refetchInterval: 1000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
|
||||
const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;
|
|
@ -2,7 +2,7 @@
|
|||
* @jest-environment node
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import { EventDispatcher, EventTypes } from '.';
|
||||
import { EventDispatcher } from '.';
|
||||
|
||||
const WATCH_FILE = '/runtipi/state/events';
|
||||
|
||||
|
@ -19,18 +19,18 @@ beforeEach(() => {
|
|||
|
||||
describe('EventDispatcher - dispatchEvent', () => {
|
||||
it('should dispatch an event', () => {
|
||||
const event = EventDispatcher.dispatchEvent(EventTypes.APP);
|
||||
const event = EventDispatcher.dispatchEvent('app');
|
||||
expect(event.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should dispatch an event with args', () => {
|
||||
const event = EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
const event = EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
expect(event.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should put events into queue', async () => {
|
||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
|
||||
// @ts-expect-error - private method
|
||||
const { queue } = EventDispatcher;
|
||||
|
@ -39,8 +39,8 @@ describe('EventDispatcher - dispatchEvent', () => {
|
|||
});
|
||||
|
||||
it('Should put first event into lock after 1 sec', async () => {
|
||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
EventDispatcher.dispatchEvent(EventTypes.UPDATE, ['--help']);
|
||||
EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
EventDispatcher.dispatchEvent('update', ['--help']);
|
||||
|
||||
// @ts-expect-error - private method
|
||||
const { queue } = EventDispatcher;
|
||||
|
@ -52,13 +52,13 @@ describe('EventDispatcher - dispatchEvent', () => {
|
|||
|
||||
expect(queue.length).toBe(2);
|
||||
expect(lock).toBeDefined();
|
||||
expect(lock?.type).toBe(EventTypes.APP);
|
||||
expect(lock?.type).toBe('app');
|
||||
});
|
||||
|
||||
it('Should clear event once its status is success', async () => {
|
||||
// @ts-expect-error - private method
|
||||
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('success');
|
||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
|
||||
await wait(1050);
|
||||
|
||||
|
@ -71,7 +71,7 @@ describe('EventDispatcher - dispatchEvent', () => {
|
|||
it('Should clear event once its status is error', async () => {
|
||||
// @ts-expect-error - private method
|
||||
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('error');
|
||||
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
|
||||
EventDispatcher.dispatchEvent('app', ['--help']);
|
||||
|
||||
await wait(1050);
|
||||
|
||||
|
@ -86,7 +86,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
|
|||
it('Should dispatch an event and wait for it to finish', async () => {
|
||||
// @ts-expect-error - private method
|
||||
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('success');
|
||||
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['--help']);
|
||||
const { success } = await EventDispatcher.dispatchEventAsync('app', ['--help']);
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
@ -95,7 +95,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
|
|||
// @ts-expect-error - private method
|
||||
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('error');
|
||||
|
||||
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['--help']);
|
||||
const { success } = await EventDispatcher.dispatchEventAsync('app', ['--help']);
|
||||
|
||||
expect(success).toBe(false);
|
||||
});
|
||||
|
@ -104,7 +104,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
|
|||
describe('EventDispatcher - runEvent', () => {
|
||||
it('Should do nothing if there is a lock', async () => {
|
||||
// @ts-expect-error - private method
|
||||
EventDispatcher.lock = { id: '123', type: EventTypes.APP, args: [] };
|
||||
EventDispatcher.lock = { id: '123', type: 'app', args: [] };
|
||||
// @ts-expect-error - private method
|
||||
await EventDispatcher.runEvent();
|
||||
|
||||
|
@ -136,7 +136,7 @@ describe('EventDispatcher - getEventStatus', () => {
|
|||
it('Should return error if event is expired', async () => {
|
||||
const dateFiveMinutesAgo = new Date(new Date().getTime() - 5 * 60 * 10000);
|
||||
// @ts-expect-error - private method
|
||||
EventDispatcher.queue = [{ id: '123', type: EventTypes.APP, args: [], creationDate: dateFiveMinutesAgo }];
|
||||
EventDispatcher.queue = [{ id: '123', type: 'app', args: [], creationDate: dateFiveMinutesAgo }];
|
||||
// @ts-expect-error - private method
|
||||
const status = EventDispatcher.getEventStatus('123');
|
||||
|
||||
|
@ -145,7 +145,7 @@ describe('EventDispatcher - getEventStatus', () => {
|
|||
|
||||
it('Should be waiting if line is not found in the file', async () => {
|
||||
// @ts-expect-error - private method
|
||||
EventDispatcher.queue = [{ id: '123', type: EventTypes.APP, args: [], creationDate: new Date() }];
|
||||
EventDispatcher.queue = [{ id: '123', type: 'app', args: [], creationDate: new Date() }];
|
||||
// @ts-expect-error - private method
|
||||
const status = EventDispatcher.getEventStatus('123');
|
||||
|
||||
|
@ -155,7 +155,7 @@ describe('EventDispatcher - getEventStatus', () => {
|
|||
|
||||
describe('EventDispatcher - clearEvent', () => {
|
||||
it('Should clear event', async () => {
|
||||
const event = { id: '123', type: EventTypes.APP, args: [], creationDate: new Date() };
|
||||
const event = { id: '123', type: 'app', args: [], creationDate: new Date() };
|
||||
// @ts-expect-error - private method
|
||||
EventDispatcher.queue = [event];
|
||||
// @ts-expect-error - private method
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
/* eslint-disable vars-on-top */
|
||||
import fs from 'fs-extra';
|
||||
import { Logger } from '../Logger';
|
||||
import { getConfig } from '../TipiConfig';
|
||||
|
||||
export enum EventTypes {
|
||||
// System events
|
||||
RESTART = 'restart',
|
||||
UPDATE = 'update',
|
||||
CLONE_REPO = 'clone_repo',
|
||||
UPDATE_REPO = 'update_repo',
|
||||
APP = 'app',
|
||||
SYSTEM_INFO = 'system_info',
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var EventDispatcher: EventDispatcher | undefined;
|
||||
}
|
||||
|
||||
export const EVENT_TYPES = {
|
||||
// System events
|
||||
RESTART: 'restart',
|
||||
UPDATE: 'update',
|
||||
CLONE_REPO: 'clone_repo',
|
||||
UPDATE_REPO: 'update_repo',
|
||||
APP: 'app',
|
||||
SYSTEM_INFO: 'system_info',
|
||||
} as const;
|
||||
|
||||
export type EventType = typeof EVENT_TYPES[keyof typeof EVENT_TYPES];
|
||||
|
||||
type SystemEvent = {
|
||||
id: string;
|
||||
type: EventTypes;
|
||||
type: EventType;
|
||||
args: string[];
|
||||
creationDate: Date;
|
||||
};
|
||||
|
@ -27,6 +36,8 @@ const WATCH_FILE = '/runtipi/state/events';
|
|||
class EventDispatcher {
|
||||
private static instance: EventDispatcher | null;
|
||||
|
||||
private dispatcherId = EventDispatcher.generateId();
|
||||
|
||||
private queue: SystemEvent[] = [];
|
||||
|
||||
private lock: SystemEvent | null = null;
|
||||
|
@ -77,7 +88,7 @@ class EventDispatcher {
|
|||
* Poll queue and run events
|
||||
*/
|
||||
private pollQueue() {
|
||||
Logger.info('EventDispatcher: Polling queue...');
|
||||
Logger.info(`EventDispatcher(${this.dispatcherId}): Polling queue...`);
|
||||
|
||||
if (!this.interval) {
|
||||
const id = setInterval(() => {
|
||||
|
@ -148,7 +159,7 @@ class EventDispatcher {
|
|||
* @param args - Event arguments
|
||||
* @returns - Event object
|
||||
*/
|
||||
public dispatchEvent(type: EventTypes, args?: string[]): SystemEvent {
|
||||
public dispatchEvent(type: EventType, args?: string[]): SystemEvent {
|
||||
const event: SystemEvent = {
|
||||
id: EventDispatcher.generateId(),
|
||||
type,
|
||||
|
@ -185,7 +196,7 @@ class EventDispatcher {
|
|||
* @param args - Event arguments
|
||||
* @returns - Promise that resolves when the event is done
|
||||
*/
|
||||
public async dispatchEventAsync(type: EventTypes, args?: string[]): Promise<{ success: boolean; stdout?: string }> {
|
||||
public async dispatchEventAsync(type: EventType, args?: string[]): Promise<{ success: boolean; stdout?: string }> {
|
||||
const event = this.dispatchEvent(type, args);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
@ -222,4 +233,8 @@ class EventDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
export const EventDispatcherInstance = EventDispatcher.getInstance();
|
||||
export const EventDispatcherInstance = global.EventDispatcher || EventDispatcher.getInstance();
|
||||
|
||||
if (getConfig().NODE_ENV !== 'production') {
|
||||
global.EventDispatcher = EventDispatcherInstance;
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { EventDispatcherInstance as EventDispatcher } from './EventDispatcher';
|
||||
export { EventTypes } from './EventDispatcher';
|
||||
export type { EventType } from './EventDispatcher';
|
||||
export { EVENT_TYPES } from './EventDispatcher';
|
||||
|
|
|
@ -3,13 +3,12 @@ import fs from 'fs-extra';
|
|||
import { getConfig, setConfig, TipiConfig } from '.';
|
||||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
// @ts-expect-error - We are mocking fs
|
||||
fs.__resetAllMocks();
|
||||
jest.mock('fs-extra');
|
||||
});
|
||||
|
||||
describe('Test: getConfig', () => {
|
||||
|
|
|
@ -4,11 +4,12 @@ import nextConfig from 'next/config';
|
|||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
import { Logger } from '../Logger';
|
||||
|
||||
enum AppSupportedArchitecturesEnum {
|
||||
ARM = 'arm',
|
||||
ARM64 = 'arm64',
|
||||
AMD64 = 'amd64',
|
||||
}
|
||||
export const ARCHITECTURES = {
|
||||
ARM: 'arm',
|
||||
ARM64: 'arm64',
|
||||
AMD64: 'amd64',
|
||||
} as const;
|
||||
export type Architecture = typeof ARCHITECTURES[keyof typeof ARCHITECTURES];
|
||||
|
||||
const {
|
||||
NODE_ENV,
|
||||
|
@ -27,13 +28,13 @@ const {
|
|||
POSTGRES_PASSWORD,
|
||||
POSTGRES_PORT = 5432,
|
||||
} = nextConfig()?.serverRuntimeConfig || process.env;
|
||||
// Use process.env if nextConfig is not available (e.g. in in server-preload.ts)
|
||||
// Use process.env if nextConfig is not available
|
||||
|
||||
const configSchema = z.object({
|
||||
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
|
||||
REDIS_HOST: z.string(),
|
||||
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
|
||||
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
|
||||
architecture: z.nativeEnum(ARCHITECTURES),
|
||||
dnsIp: z.string(),
|
||||
rootFolder: z.string(),
|
||||
internalIp: z.string(),
|
||||
|
|
|
@ -6,11 +6,11 @@ import { EventDispatcher } from '../../core/EventDispatcher';
|
|||
import { setConfig } from '../../core/TipiConfig';
|
||||
import TipiCache from '../../core/TipiCache';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('axios');
|
||||
jest.mock('redis');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mock('fs-extra');
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import semver from 'semver';
|
||||
import { z } from 'zod';
|
||||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
import { EventDispatcher, EventTypes } from '../../core/EventDispatcher';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { Logger } from '../../core/Logger';
|
||||
import TipiCache from '../../core/TipiCache';
|
||||
import { getConfig, setConfig } from '../../core/TipiConfig';
|
||||
|
@ -66,7 +66,7 @@ const restart = async (): Promise<boolean> => {
|
|||
}
|
||||
|
||||
setConfig('status', 'RESTARTING');
|
||||
EventDispatcher.dispatchEventAsync(EventTypes.RESTART);
|
||||
EventDispatcher.dispatchEventAsync('restart');
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -91,12 +91,12 @@ const update = async (): Promise<boolean> => {
|
|||
}
|
||||
|
||||
if (semver.major(current) !== semver.major(latest)) {
|
||||
throw new Error('The major version has changed. Please update manually');
|
||||
throw new Error('The major version has changed. Please update manually (instructions on GitHub)');
|
||||
}
|
||||
|
||||
setConfig('status', 'UPDATING');
|
||||
|
||||
EventDispatcher.dispatchEventAsync(EventTypes.UPDATE);
|
||||
EventDispatcher.dispatchEventAsync('update');
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createTRPCReact, httpLink, loggerLink } from '@trpc/react-query';
|
||||
import SuperJSON from 'superjson';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import type { AppRouter } from '../src/server/routers/_app';
|
||||
|
||||
|
@ -17,28 +17,36 @@ export const trpc = createTRPCReact<AppRouter>({
|
|||
},
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const trpcClient = trpc.createClient({
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: () => false,
|
||||
}),
|
||||
httpLink({
|
||||
url: 'http://localhost:3000/api/trpc',
|
||||
headers() {
|
||||
return {};
|
||||
},
|
||||
fetch: async (input, init?) =>
|
||||
fetch(input, {
|
||||
...init,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
transformer: SuperJSON,
|
||||
});
|
||||
|
||||
export function TRPCTestClientProvider(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
transformer: superjson,
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: () => false,
|
||||
}),
|
||||
httpLink({
|
||||
url: 'http://localhost:3000/api/trpc',
|
||||
fetch: async (input, init?) =>
|
||||
fetch(input, {
|
||||
...init,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import 'whatwg-fetch';
|
||||
import { server } from '../../src/client/mocks/server';
|
||||
import { mockApolloClient } from '../test-utils';
|
||||
import { useToastStore } from '../../src/client/state/toastStore';
|
||||
|
||||
// Mock next/router
|
||||
|
@ -18,6 +16,27 @@ jest.mock('remark-mdx', () => () => ({}));
|
|||
|
||||
console.error = jest.fn();
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem(key: string) {
|
||||
return store[key] || null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem(key: string) {
|
||||
delete store[key];
|
||||
},
|
||||
clear() {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
beforeAll(() => {
|
||||
// Enable the mocking in tests.
|
||||
server.listen();
|
||||
|
@ -25,10 +44,6 @@ beforeAll(() => {
|
|||
|
||||
beforeEach(async () => {
|
||||
useToastStore.getState().clearToasts();
|
||||
// Ensure Apollo cache is cleared between tests.
|
||||
// https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.clearStore
|
||||
await mockApolloClient.clearStore();
|
||||
await mockApolloClient.cache.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -1,27 +1,8 @@
|
|||
import React, { FC, ReactElement } from 'react';
|
||||
import { render, RenderOptions, renderHook } from '@testing-library/react';
|
||||
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import { TRPCTestClientProvider } from './TRPCTestClientProvider';
|
||||
|
||||
const link = new HttpLink({
|
||||
uri: 'http://localhost:3000/graphql',
|
||||
// Use explicit `window.fetch` so tha outgoing requests
|
||||
// are captured and deferred until the Service Worker is ready.
|
||||
fetch: (...args) => fetch(...args),
|
||||
});
|
||||
|
||||
// create a mock of Apollo Client
|
||||
export const mockApolloClient = new ApolloClient({
|
||||
cache: new InMemoryCache({}),
|
||||
link,
|
||||
});
|
||||
|
||||
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<TRPCTestClientProvider>
|
||||
<ApolloProvider client={mockApolloClient}>{children}</ApolloProvider>
|
||||
</TRPCTestClientProvider>
|
||||
);
|
||||
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => <TRPCTestClientProvider>{children}</TRPCTestClientProvider>;
|
||||
|
||||
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => render(ui, { wrapper: AllTheProviders, ...options });
|
||||
const customRenderHook = (callback: () => any, options?: Omit<RenderOptions, 'wrapper'>) => renderHook(callback, { wrapper: AllTheProviders, ...options });
|
||||
|
|
|
@ -1,121 +1,105 @@
|
|||
import path from 'path';
|
||||
|
||||
const fs: {
|
||||
__createMockFiles: typeof createMockFiles;
|
||||
__resetAllMocks: typeof resetAllMocks;
|
||||
readFileSync: typeof readFileSync;
|
||||
existsSync: typeof existsSync;
|
||||
writeFileSync: typeof writeFileSync;
|
||||
mkdirSync: typeof mkdirSync;
|
||||
rmSync: typeof rmSync;
|
||||
readdirSync: typeof readdirSync;
|
||||
copyFileSync: typeof copyFileSync;
|
||||
copySync: typeof copyFileSync;
|
||||
createFileSync: typeof createFileSync;
|
||||
unlinkSync: typeof unlinkSync;
|
||||
} = jest.genMockFromModule('fs-extra');
|
||||
class FsMock {
|
||||
private static instance: FsMock;
|
||||
|
||||
let mockFiles = Object.create(null);
|
||||
private mockFiles = Object.create(null);
|
||||
|
||||
const createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
mockFiles = Object.create(null);
|
||||
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!mockFiles[dir]) {
|
||||
mockFiles[dir] = [];
|
||||
static getInstance(): FsMock {
|
||||
if (!FsMock.instance) {
|
||||
FsMock.instance = new FsMock();
|
||||
}
|
||||
|
||||
mockFiles[dir].push(path.basename(file));
|
||||
mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
const readFileSync = (p: string) => mockFiles[p];
|
||||
|
||||
const existsSync = (p: string) => mockFiles[p] !== undefined;
|
||||
|
||||
const writeFileSync = (p: string, data: string | string[]) => {
|
||||
mockFiles[p] = data;
|
||||
};
|
||||
|
||||
const mkdirSync = (p: string) => {
|
||||
mockFiles[p] = Object.create(null);
|
||||
};
|
||||
|
||||
const rmSync = (p: string) => {
|
||||
if (mockFiles[p] instanceof Array) {
|
||||
mockFiles[p].forEach((file: string) => {
|
||||
delete mockFiles[path.join(p, file)];
|
||||
});
|
||||
return FsMock.instance;
|
||||
}
|
||||
|
||||
delete mockFiles[p];
|
||||
};
|
||||
__createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
this.mockFiles = Object.create(null);
|
||||
|
||||
const readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
const depth = p.split('/').length;
|
||||
|
||||
Object.keys(mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(file));
|
||||
this.mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
__resetAllMocks = () => {
|
||||
this.mockFiles = Object.create(null);
|
||||
};
|
||||
|
||||
readFileSync = (p: string) => this.mockFiles[p];
|
||||
|
||||
existsSync = (p: string) => this.mockFiles[p] !== undefined;
|
||||
|
||||
writeFileSync = (p: string, data: string | string[]) => {
|
||||
this.mockFiles[p] = data;
|
||||
};
|
||||
|
||||
mkdirSync = (p: string) => {
|
||||
this.mockFiles[p] = Object.create(null);
|
||||
};
|
||||
|
||||
rmSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
};
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
const copyFileSync = (source: string, destination: string) => {
|
||||
mockFiles[destination] = mockFiles[source];
|
||||
};
|
||||
readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
|
||||
const copySync = (source: string, destination: string) => {
|
||||
mockFiles[destination] = mockFiles[source];
|
||||
const depth = p.split('/').length;
|
||||
|
||||
if (mockFiles[source] instanceof Array) {
|
||||
mockFiles[source].forEach((file: string) => {
|
||||
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
|
||||
Object.keys(this.mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFileSync = (p: string) => {
|
||||
mockFiles[p] = '';
|
||||
};
|
||||
return files;
|
||||
};
|
||||
|
||||
const resetAllMocks = () => {
|
||||
mockFiles = Object.create(null);
|
||||
};
|
||||
copyFileSync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
};
|
||||
|
||||
const unlinkSync = (p: string) => {
|
||||
if (mockFiles[p] instanceof Array) {
|
||||
mockFiles[p].forEach((file: string) => {
|
||||
delete mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete mockFiles[p];
|
||||
};
|
||||
copySync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
|
||||
fs.unlinkSync = unlinkSync;
|
||||
fs.readdirSync = readdirSync;
|
||||
fs.existsSync = existsSync;
|
||||
fs.readFileSync = readFileSync;
|
||||
fs.writeFileSync = writeFileSync;
|
||||
fs.mkdirSync = mkdirSync;
|
||||
fs.rmSync = rmSync;
|
||||
fs.copyFileSync = copyFileSync;
|
||||
fs.copySync = copySync;
|
||||
fs.createFileSync = createFileSync;
|
||||
fs.__createMockFiles = createMockFiles;
|
||||
fs.__resetAllMocks = resetAllMocks;
|
||||
if (this.mockFiles[source] instanceof Array) {
|
||||
this.mockFiles[source].forEach((file: string) => {
|
||||
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default fs;
|
||||
// module.exports = fs;
|
||||
createFileSync = (p: string) => {
|
||||
this.mockFiles[p] = '';
|
||||
};
|
||||
|
||||
unlinkSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
getMockFiles = () => this.mockFiles;
|
||||
}
|
||||
|
||||
export default FsMock.getInstance();
|
||||
|
|
Loading…
Add table
Reference in a new issue