CommunitySolidServer/test/unit/init/AppRunner.test.ts
2022-05-16 10:15:37 +02:00

491 lines
20 KiB
TypeScript

import { ComponentsManager } from 'componentsjs';
import type { ClusterManager } from '../../../src';
import type { App } from '../../../src/init/App';
import { AppRunner } from '../../../src/init/AppRunner';
import type { CliExtractor } from '../../../src/init/cli/CliExtractor';
import type { SettingsResolver } from '../../../src/init/variables/SettingsResolver';
import { joinFilePath } from '../../../src/util/PathUtil';
import { flushPromises } from '../../util/Util';
const defaultParameters = {
port: 3000,
logLevel: 'info',
};
const cliExtractor: jest.Mocked<CliExtractor> = {
handleSafe: jest.fn().mockResolvedValue(defaultParameters),
} as any;
const defaultVariables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
};
const settingsResolver: jest.Mocked<SettingsResolver> = {
handleSafe: jest.fn().mockResolvedValue(defaultVariables),
} as any;
const mockLogger = {
info: jest.fn(),
warn: jest.fn(),
silly: jest.fn(),
error: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
};
const clusterManager: jest.Mocked<ClusterManager> = {
isSingleThreaded: jest.fn().mockReturnValue(false),
spawnWorkers: jest.fn(),
isPrimary: jest.fn().mockReturnValue(true),
isWorker: jest.fn().mockReturnValue(false),
logger: mockLogger,
workers: 1,
clusterMode: 1,
} as any;
const app: jest.Mocked<App> = {
start: jest.fn(),
clusterManager,
} as any;
const manager: jest.Mocked<ComponentsManager<App>> = {
instantiate: jest.fn(async(iri: string): Promise<any> => {
switch (iri) {
case 'urn:solid-server-app-setup:default:CliResolver': return { cliExtractor, settingsResolver };
case 'urn:solid-server:default:App': return app;
default: throw new Error('unknown iri');
}
}),
configRegistry: {
register: jest.fn(),
},
} as any;
const listSingleThreadedComponentsMock = jest.fn().mockResolvedValue([]);
jest.mock('../../../src/init/cluster/SingleThreaded', (): any => ({
listSingleThreadedComponents: (): any => listSingleThreadedComponentsMock(),
}));
jest.mock('componentsjs', (): any => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
ComponentsManager: {
build: jest.fn(async(): Promise<ComponentsManager<App>> => manager),
},
}));
jest.spyOn(process, 'cwd').mockReturnValue('/var/cwd');
const write = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());
const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any);
describe('AppRunner', (): void => {
afterEach((): void => {
jest.clearAllMocks();
});
describe('create', (): void => {
it('creates an App with the provided settings.', async(): Promise<void> => {
const variables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
};
const createdApp = await new AppRunner().create(
{
mainModulePath: joinFilePath(__dirname, '../../../'),
dumpErrorState: true,
logLevel: 'info',
},
joinFilePath(__dirname, '../../../config/default.json'),
variables,
);
expect(createdApp).toBe(app);
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'info',
mainModulePath: joinFilePath(__dirname, '../../../'),
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register)
.toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json'));
expect(manager.instantiate).toHaveBeenCalledTimes(1);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server:default:App', { variables });
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(0);
expect(app.start).toHaveBeenCalledTimes(0);
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
});
it('throws an error if threading issues are detected with 1 class.', async(): Promise<void> => {
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass' ]);
const variables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
};
let caughtError: Error | undefined;
try {
await new AppRunner().create(
{
mainModulePath: joinFilePath(__dirname, '../../../'),
dumpErrorState: true,
logLevel: 'info',
},
joinFilePath(__dirname, '../../../config/default.json'),
variables,
);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError?.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
expect(caughtError?.message).toMatch(
/\[ViolatingClass\] is not threadsafe and should not be run in multithreaded setups!/mu,
);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
it('throws an error if threading issues are detected with 2 class.', async(): Promise<void> => {
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass1', 'ViolatingClass2' ]);
const variables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
};
let caughtError: Error | undefined;
try {
await new AppRunner().create(
{
mainModulePath: joinFilePath(__dirname, '../../../'),
dumpErrorState: true,
logLevel: 'info',
},
joinFilePath(__dirname, '../../../config/default.json'),
variables,
);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError?.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
expect(caughtError?.message).toMatch(
/\[ViolatingClass1, ViolatingClass2\] are not threadsafe and should not be run in multithreaded setups!/mu,
);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
});
describe('run', (): void => {
it('starts the server with provided settings.', async(): Promise<void> => {
const variables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:seededPodConfigJson': '/var/cwd/seeded-pod-config.json',
};
await new AppRunner().run(
{
mainModulePath: joinFilePath(__dirname, '../../../'),
dumpErrorState: true,
logLevel: 'info',
},
joinFilePath(__dirname, '../../../config/default.json'),
variables,
);
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'info',
mainModulePath: joinFilePath(__dirname, '../../../'),
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register)
.toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json'));
expect(manager.instantiate).toHaveBeenCalledTimes(1);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server:default:App', { variables });
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(0);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(0);
expect(app.start).toHaveBeenCalledTimes(1);
expect(app.start).toHaveBeenCalledWith();
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
});
});
describe('createCli', (): void => {
it('creates the server with default settings.', async(): Promise<void> => {
await expect(new AppRunner().createCli([ 'node', 'script' ])).resolves.toBe(app);
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'info',
mainModulePath: joinFilePath(__dirname, '../../../'),
typeChecking: false,
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register)
.toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json'));
expect(manager.instantiate).toHaveBeenCalledTimes(2);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(cliExtractor.handleSafe).toHaveBeenCalledWith([ 'node', 'script' ]);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(1);
expect(settingsResolver.handleSafe).toHaveBeenCalledWith(defaultParameters);
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
'urn:solid-server:default:App',
{ variables: defaultVariables });
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
expect(app.start).toHaveBeenCalledTimes(0);
});
it('uses the default process.argv in case none are provided.', async(): Promise<void> => {
const { argv } = process;
const argvParameters = [
'node', 'script',
'-b', 'http://pod.example/',
'-c', 'myconfig.json',
'-f', '/root',
'-l', 'debug',
'-m', 'module/path',
'-p', '4000',
'-s', 'http://localhost:5000/sparql',
'-t',
'--podConfigJson', '/different-path.json',
'--seededPodConfigJson', '/different-path.json',
'-w', '1',
];
process.argv = argvParameters;
await expect(new AppRunner().createCli()).resolves.toBe(app);
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'debug',
mainModulePath: '/var/cwd/module/path',
typeChecking: false,
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json');
expect(manager.instantiate).toHaveBeenCalledTimes(2);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(cliExtractor.handleSafe).toHaveBeenCalledWith(argvParameters);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(1);
expect(settingsResolver.handleSafe).toHaveBeenCalledWith(defaultParameters);
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
'urn:solid-server:default:App',
{ variables: defaultVariables });
expect(app.start).toHaveBeenCalledTimes(0);
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
process.argv = argv;
});
it('checks for threading issues when starting in multithreaded mode.', async(): Promise<void> => {
const createdApp = await new AppRunner().createCli();
expect(createdApp).toBe(app);
expect(listSingleThreadedComponentsMock).toHaveBeenCalled();
});
it('throws an error if there are threading issues detected.', async(): Promise<void> => {
listSingleThreadedComponentsMock.mockImplementationOnce((): string[] => [ 'ViolatingClass' ]);
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().createCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Cannot run a singlethreaded-only component in a multithreaded setup!/mu);
expect(caughtError?.message).toMatch(
/\[ViolatingClass\] is not threadsafe and should not be run in multithreaded setups!/mu,
);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
it('throws an error if creating a ComponentsManager fails.', async(): Promise<void> => {
(manager.configRegistry.register as jest.Mock).mockRejectedValueOnce(new Error('Fatal'));
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().createCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Could not build the config files from .*default\.json/mu);
expect(caughtError.message).toMatch(/^Cause: Fatal/mu);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
it('throws an error if instantiating the CliResolver fails.', async(): Promise<void> => {
manager.instantiate.mockRejectedValueOnce(new Error('Fatal'));
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().createCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Could not load the config variables/mu);
expect(caughtError.message).toMatch(/^Cause: Fatal/mu);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
it('throws an error if instantiating the server fails.', async(): Promise<void> => {
// We want the second call to fail
manager.instantiate
.mockResolvedValueOnce({ cliExtractor, settingsResolver })
.mockRejectedValueOnce(new Error('Fatal'));
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().createCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Could not create the server/mu);
expect(caughtError.message).toMatch(/^Cause: Fatal/mu);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
it('throws an error if non-error objects get thrown.', async(): Promise<void> => {
(manager.configRegistry.register as jest.Mock).mockRejectedValueOnce('NotAnError');
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().createCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Cause: Unknown error: NotAnError$/mu);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
});
describe('runCli', (): void => {
it('runs the server.', async(): Promise<void> => {
await expect(new AppRunner().runCli([ 'node', 'script' ])).resolves.toBeUndefined();
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'info',
mainModulePath: joinFilePath(__dirname, '../../../'),
typeChecking: false,
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register)
.toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json'));
expect(manager.instantiate).toHaveBeenCalledTimes(2);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(cliExtractor.handleSafe).toHaveBeenCalledWith([ 'node', 'script' ]);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(1);
expect(settingsResolver.handleSafe).toHaveBeenCalledWith(defaultParameters);
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
'urn:solid-server:default:App',
{ variables: defaultVariables });
expect(app.start).toHaveBeenCalledTimes(1);
expect(app.start).toHaveBeenLastCalledWith();
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
});
it('throws an error if the server could not start.', async(): Promise<void> => {
app.start.mockRejectedValueOnce(new Error('Fatal'));
let caughtError: Error = new Error('should disappear');
try {
await new AppRunner().runCli([ 'node', 'script' ]);
} catch (error: unknown) {
caughtError = error as Error;
}
expect(caughtError.message).toMatch(/^Could not start the server/mu);
expect(caughtError.message).toMatch(/^Cause: Fatal/mu);
expect(app.start).toHaveBeenCalledTimes(1);
expect(write).toHaveBeenCalledTimes(0);
expect(exit).toHaveBeenCalledTimes(0);
});
});
describe('runCliSync', (): void => {
it('starts the server.', async(): Promise<void> => {
// eslint-disable-next-line no-sync
new AppRunner().runCliSync({ argv: [ 'node', 'script' ]});
// Wait until app.start has been called, because we can't await AppRunner.run.
await flushPromises();
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
expect(ComponentsManager.build).toHaveBeenCalledWith({
dumpErrorState: true,
logLevel: 'info',
mainModulePath: joinFilePath(__dirname, '../../../'),
typeChecking: false,
});
expect(manager.configRegistry.register).toHaveBeenCalledTimes(1);
expect(manager.configRegistry.register)
.toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json'));
expect(manager.instantiate).toHaveBeenCalledTimes(2);
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1);
expect(cliExtractor.handleSafe).toHaveBeenCalledWith([ 'node', 'script' ]);
expect(settingsResolver.handleSafe).toHaveBeenCalledTimes(1);
expect(settingsResolver.handleSafe).toHaveBeenCalledWith(defaultParameters);
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
'urn:solid-server:default:App',
{ variables: defaultVariables });
expect(app.start).toHaveBeenCalledTimes(1);
expect(app.start).toHaveBeenLastCalledWith();
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
});
it('exits the process and writes to stderr if there was an error.', async(): Promise<void> => {
manager.instantiate.mockRejectedValueOnce(new Error('Fatal'));
// eslint-disable-next-line no-sync
new AppRunner().runCliSync({ argv: [ 'node', 'script' ]});
// Wait until app.start has been called, because we can't await AppRunner.runCli.
await flushPromises();
expect(write).toHaveBeenCalledTimes(1);
expect(write).toHaveBeenLastCalledWith(expect.stringMatching(/Cause: Fatal/mu));
expect(exit).toHaveBeenCalledTimes(1);
expect(exit).toHaveBeenLastCalledWith(1);
});
});
});