diff --git a/config/init/README.md b/config/app/README.md similarity index 63% rename from config/init/README.md rename to config/app/README.md index c2d35e3f9..2a1debee4 100644 --- a/config/init/README.md +++ b/config/app/README.md @@ -1,8 +1,12 @@ -# Init -Options related to the server initialization. -This is the entry point to the main server setup. +# App +Options related to the server startup. -## Handler +## Base +This is the entry point to the main server setup. +* *default*: The main application. This should only be changed/replaced + if you want to start from a different kind of class. + +## Init Contains a list of initializer that need to be run when starting the server. For example, when acl authorization is used, an initializer will be added that makes sure there is an acl file in the root. diff --git a/config/app/app/default.json b/config/app/app/default.json new file mode 100644 index 000000000..8d218fb17 --- /dev/null +++ b/config/app/app/default.json @@ -0,0 +1,19 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "This is the entry point to the application. It can be used to both start and stop the server.", + "@id": "urn:solid-server:default:App", + "@type": "App", + "initializer": { "@id": "urn:solid-server:default:Initializer" }, + "finalizer": { + "comment": "This is going to contain the list of finalizers that need to be called. These should be added in the configs where such classes are configured.", + "@id": "urn:solid-server:default:Finalizer", + "@type": "ParallelFinalizer", + "finalizers": [ + { "@id": "urn:solid-server:default:ServerInitializer" } + ] + } + } + ] +} diff --git a/config/init/handler/base/init.json b/config/app/init/base/init.json similarity index 82% rename from config/init/handler/base/init.json rename to config/app/init/base/init.json index 95a710be1..f2d1467a2 100644 --- a/config/init/handler/base/init.json +++ b/config/app/init/base/init.json @@ -1,8 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ - "files-scs:config/init/handler/initializers/logger.json", - "files-scs:config/init/handler/initializers/server.json" + "files-scs:config/app/init/initializers/logger.json", + "files-scs:config/app/init/initializers/server.json" ], "@graph": [ { diff --git a/config/init/handler/default.json b/config/app/init/default.json similarity index 81% rename from config/init/handler/default.json rename to config/app/init/default.json index 96ba030bd..88cf74038 100644 --- a/config/init/handler/default.json +++ b/config/app/init/default.json @@ -1,8 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ - "files-scs:config/init/handler/base/init.json", - "files-scs:config/init/handler/initializers/root-container.json" + "files-scs:config/app/init/base/init.json", + "files-scs:config/app/init/initializers/root-container.json" ], "@graph": [ { diff --git a/config/init/handler/initializers/logger.json b/config/app/init/initializers/logger.json similarity index 100% rename from config/init/handler/initializers/logger.json rename to config/app/init/initializers/logger.json diff --git a/config/init/handler/initializers/root-container.json b/config/app/init/initializers/root-container.json similarity index 100% rename from config/init/handler/initializers/root-container.json rename to config/app/init/initializers/root-container.json diff --git a/config/init/handler/initializers/server.json b/config/app/init/initializers/server.json similarity index 100% rename from config/init/handler/initializers/server.json rename to config/app/init/initializers/server.json diff --git a/config/default.json b/config/default.json index 94de69f3b..9c270b5e6 100644 --- a/config/default.json +++ b/config/default.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/dynamic.json b/config/dynamic.json index ea70123b2..1feab9e32 100644 --- a/config/dynamic.json +++ b/config/dynamic.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/dynamic.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/example-https-file.json b/config/example-https-file.json index 4d5915dc1..7ddcfad3e 100644 --- a/config/example-https-file.json +++ b/config/example-https-file.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", @@ -8,7 +10,6 @@ "files-scs:config/identity/email/default.json", "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/file.json b/config/file.json index 0b1419e89..506ae6e41 100644 --- a/config/file.json +++ b/config/file.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/identity/handler/key-value/resource-store.json b/config/identity/handler/key-value/resource-store.json index 6d8f87dbf..952362fd7 100644 --- a/config/identity/handler/key-value/resource-store.json +++ b/config/identity/handler/key-value/resource-store.json @@ -14,6 +14,11 @@ "@id": "urn:solid-server:default:ExpiringIdpStorage", "@type": "WrappedExpiringStorage", "source": { "@id": "urn:solid-server:default:IdpStorage" } + }, + { + "comment": "Makes sure the expiring storage cleanup timer is stopped when the application needs to stop.", + "@id": "urn:solid-server:default:Finalizer", + "ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:ExpiringIdpStorage" } ] } ] } diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json index 2b4ca9e33..93edc64f6 100644 --- a/config/memory-subdomains.json +++ b/config/memory-subdomains.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/path-routing.json b/config/path-routing.json index b94f4df9e..ca714da2b 100644 --- a/config/path-routing.json +++ b/config/path-routing.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json index 8149a9351..b453f8b3d 100644 --- a/config/sparql-endpoint.json +++ b/config/sparql-endpoint.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", diff --git a/config/util/resource-locker/redis.json b/config/util/resource-locker/redis.json index 5f5b3ee88..1ee708a63 100644 --- a/config/util/resource-locker/redis.json +++ b/config/util/resource-locker/redis.json @@ -18,6 +18,11 @@ "suffixes_write": "write" }, "expiration": 3000 + }, + { + "comment": "Makes sure the redis connection is closed when the application needs to stop.", + "@id": "urn:solid-server:default:Finalizer", + "ParallelFinalizer:_finalizers": [ { "@id": "urn:solid-server:default:RedisResourceLocker" } ] } ] } diff --git a/src/index.ts b/src/index.ts index a748f87f6..5e0ad9aad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,8 +66,13 @@ export * from './identity/storage/WrappedFetchAdapterFactory'; export * from './identity/IdentityProviderFactory'; export * from './identity/IdentityProviderHttpHandler'; +// Init/Final +export * from './init/final/Finalizable'; +export * from './init/final/ParallelFinalizer'; + // Init export * from './init/AclInitializer'; +export * from './init/App'; export * from './init/AppRunner'; export * from './init/ConfigPodInitializer'; export * from './init/Initializer'; diff --git a/src/init/App.ts b/src/init/App.ts new file mode 100644 index 000000000..2d112ac10 --- /dev/null +++ b/src/init/App.ts @@ -0,0 +1,29 @@ +import type { Finalizable } from './final/Finalizable'; +import type { Initializer } from './Initializer'; + +/** + * Entry point for the entire Solid server. + */ +export class App { + private readonly initializer: Initializer; + private readonly finalizer: Finalizable; + + public constructor(initializer: Initializer, finalizer: Finalizable) { + this.initializer = initializer; + this.finalizer = finalizer; + } + + /** + * Initializes and starts the application. + */ + public async start(): Promise { + await this.initializer.handleSafe(); + } + + /** + * Stops the application and handles cleanup. + */ + public async stop(): Promise { + await this.finalizer.finalize(); + } +} diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 77401794c..74e38bdb8 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -6,11 +6,25 @@ import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; import { absoluteFilePath, ensureTrailingSlash, joinFilePath } from '../util/PathUtil'; -import type { Initializer } from './Initializer'; - +import type { App } from './App'; export class AppRunner { private readonly logger = getLoggerFor(this); + /** + * Generic function for getting the app object to start the server from JavaScript for a given config. + * @param loaderProperties - Components.js loader properties. + * @param configFile - Path to the server config file. + * @param variableParams - Variables to pass into the config file. + */ + public async getApp( + loaderProperties: IComponentsManagerBuilderOptions, + configFile: string, + variableParams: ConfigVariables, + ): Promise { + const variables = this.createVariables(variableParams); + return this.createApp(loaderProperties, configFile, variables); + } + /** * Generic run function for starting the server from JavaScript for a given config. * @param loaderProperties - Components.js loader properties. @@ -18,13 +32,12 @@ export class AppRunner { * @param variableParams - Variables to pass into the config file. */ public async run( - loaderProperties: IComponentsManagerBuilderOptions, + loaderProperties: IComponentsManagerBuilderOptions, configFile: string, variableParams: ConfigVariables, ): Promise { - const variables = this.createVariables(variableParams); - const initializer = await this.createInitializer(loaderProperties, configFile, variables); - await initializer.handleSafe(); + const app = await this.getApp(loaderProperties, configFile, variableParams); + await app.start(); } /** @@ -74,18 +87,17 @@ export class AppRunner { .help(); // Gather settings for instantiating the server - const loaderProperties: IComponentsManagerBuilderOptions = { + const loaderProperties: IComponentsManagerBuilderOptions = { mainModulePath: this.resolveFilePath(params.mainModulePath), dumpErrorState: true, logLevel: params.loggingLevel as LogLevel, }; const configFile = this.resolveFilePath(params.config, 'config/default.json'); - const variables = this.createVariables(params); // Create and execute the server initializer - this.createInitializer(loaderProperties, configFile, variables) + this.getApp(loaderProperties, configFile, params) .then( - async(initializer): Promise => initializer.handleSafe(), + async(app): Promise => app.start(), (error: Error): void => { // Instantiation of components has failed, so there is no logger to use stderr.write(`Error: could not instantiate server from ${configFile}\n`); @@ -93,7 +105,7 @@ export class AppRunner { process.exit(1); }, ).catch((error): void => { - this.logger.error(`Could not initialize server: ${error}`, { error }); + this.logger.error(`Could not start server: ${error}`, { error }); process.exit(1); }); } @@ -131,14 +143,14 @@ export class AppRunner { /** * Creates the server initializer */ - protected async createInitializer( - componentsProperties: IComponentsManagerBuilderOptions, + protected async createApp( + componentsProperties: IComponentsManagerBuilderOptions, configFile: string, variables: Record, - ): Promise { + ): Promise { const componentsManager = await ComponentsManager.build(componentsProperties); - const initializer = 'urn:solid-server:default:Initializer'; + const initializer = 'urn:solid-server:default:App'; await componentsManager.configRegistry.register(configFile); return await componentsManager.instantiate(initializer, { variables }); } diff --git a/src/init/final/ParallelFinalizer.ts b/src/init/final/ParallelFinalizer.ts new file mode 100644 index 000000000..ed97d933e --- /dev/null +++ b/src/init/final/ParallelFinalizer.ts @@ -0,0 +1,16 @@ +import type { Finalizable } from './Finalizable'; + +/** + * Finalizes all the injected Finalizable classes in parallel. + */ +export class ParallelFinalizer implements Finalizable { + private readonly finalizers: Finalizable[]; + + public constructor(finalizers: Finalizable[] = []) { + this.finalizers = finalizers; + } + + public async finalize(): Promise { + await Promise.all(this.finalizers.map(async(finalizer): Promise => finalizer.finalize())); + } +} diff --git a/test/integration/Config.ts b/test/integration/Config.ts index 3ac037a0b..4ca9efffc 100644 --- a/test/integration/Config.ts +++ b/test/integration/Config.ts @@ -3,7 +3,6 @@ import { ComponentsManager } from 'componentsjs'; import * as rimraf from 'rimraf'; import { joinFilePath } from '../../src/util/PathUtil'; -export const BASE = 'http://test.com'; let cachedModuleState: IModuleState; /** @@ -42,3 +41,13 @@ export function getTestFolder(name: string): string { export function removeFolder(folder: string): void { rimraf.sync(folder, { glob: false }); } + +export function getDefaultVariables(port: number, baseUrl?: string): Record { + return { + 'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${port}/`, + 'urn:solid-server:default:variable:port': port, + 'urn:solid-server:default:variable:loggingLevel': 'off', + 'urn:solid-server:default:variable:showStackTrace': true, + 'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'), + }; +} diff --git a/test/integration/DynamicPods.test.ts b/test/integration/DynamicPods.test.ts index 7ed55ed8f..13b4fa7ed 100644 --- a/test/integration/DynamicPods.test.ts +++ b/test/integration/DynamicPods.test.ts @@ -1,13 +1,10 @@ import { mkdirSync } from 'fs'; -import type { Server } from 'http'; import { stringify } from 'querystring'; import fetch from 'cross-fetch'; -import type { Initializer } from '../../src/init/Initializer'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; -import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage'; +import type { App } from '../../src/init/App'; import { joinFilePath } from '../../src/util/PathUtil'; import { getPort } from '../util/Util'; -import { getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config'; +import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config'; const port = getPort('DynamicPods'); const baseUrl = `http://localhost:${port}/`; @@ -26,45 +23,34 @@ const configs: [string, any][] = [ // Using the actual templates instead of specific test ones to prevent a lot of duplication // Tests are very similar to subdomain/pod tests. Would be nice if they can be combined describe.each(configs)('A dynamic pod server with template config %s', (template, { teardown }): void => { - let server: Server; - let initializer: Initializer; - let factory: HttpServerFactory; - let expiringStorage: WrappedExpiringStorage; + let app: App; const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', template, createPod: true }; const podUrl = `${baseUrl}${settings.podName}/`; beforeAll(async(): Promise => { const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': baseUrl, + ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:rootFilePath': rootFilePath, 'urn:solid-server:default:variable:podConfigJson': podConfigJson, - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'), }; // Need to make sure the temp folder exists so the podConfigJson can be written to it mkdirSync(rootFilePath, { recursive: true }); - // Create and initialize the HTTP handler and related components + // Create and start the HTTP handler and related components const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', getTestConfigPath('server-dynamic-unsafe.json'), variables, ) as Record; - ({ factory, initializer, expiringStorage } = instances); + ({ app } = instances); - // Set up the internal store - await initializer.handleSafe(); - - server = factory.startServer(port); + await app.start(); }); afterAll(async(): Promise => { - expiringStorage.finalize(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); await teardown(); + await app.stop(); }); it('creates a pod with the given config.', async(): Promise => { diff --git a/test/integration/Identity.test.ts b/test/integration/Identity.test.ts index 49d48ca44..6628978b2 100644 --- a/test/integration/Identity.test.ts +++ b/test/integration/Identity.test.ts @@ -1,17 +1,13 @@ -import type { Server } from 'http'; import { stringify } from 'querystring'; import { URL } from 'url'; import { load } from 'cheerio'; import type { Response } from 'cross-fetch'; import { fetch } from 'cross-fetch'; import urljoin from 'url-join'; -import type { Initializer } from '../../src/init/Initializer'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; -import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage'; +import type { App } from '../../src/init/App'; import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes'; -import { joinFilePath } from '../../src/util/PathUtil'; import { getPort } from '../util/Util'; -import { getTestConfigPath, instantiateFromConfig } from './Config'; +import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; import { IdentityTestState } from './IdentityTestState'; const port = getPort('Identity'); @@ -39,10 +35,7 @@ async function postForm(url: string, formBody: string): Promise { // This is why the redirects are handled manually. // We also need to parse the HTML in several steps since there is no API. describe('A Solid server with IDP', (): void => { - let server: Server; - let initializer: Initializer; - let expiringStorage: WrappedExpiringStorage; - let factory: HttpServerFactory; + let app: App; const redirectUrl = 'http://mockedredirect/'; const oidcIssuer = baseUrl; const card = urljoin(baseUrl, 'profile/card'); @@ -61,15 +54,10 @@ describe('A Solid server with IDP', (): void => { const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', getTestConfigPath('server-memory.json'), - { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'), - }, + getDefaultVariables(port, baseUrl), ) as Record; - ({ factory, initializer, expiringStorage } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + ({ app } = instances); + await app.start(); // Create a simple webId const turtle = `<${webId}> <${baseUrl}> .`; @@ -81,10 +69,7 @@ describe('A Solid server with IDP', (): void => { }); afterAll(async(): Promise => { - expiringStorage.finalize(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); describe('doing registration', (): void => { diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index b8570981c..6f9bdfa8f 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -1,12 +1,11 @@ -import type { Server } from 'http'; import fetch from 'cross-fetch'; -import type { Initializer, ResourceStore } from '../../src/'; +import type { ResourceStore, App } from '../../src/'; import { BasicRepresentation } from '../../src/'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; import { AclHelper } from '../util/AclHelper'; import { deleteResource, getResource, postResource, putResource } from '../util/FetchUtil'; import { getPort } from '../util/Util'; import { + getDefaultVariables, getPresetConfigPath, getTestConfigPath, getTestFolder, @@ -30,21 +29,18 @@ const stores: [string, any][] = [ ]; describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig, teardown }): void => { - let server: Server; - let initializer: Initializer; - let factory: HttpServerFactory; + let app: App; let store: ResourceStore; let aclHelper: AclHelper; const permanent = `${baseUrl}document.txt`; beforeAll(async(): Promise => { - const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, + const variables = { + ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:rootFilePath': rootFilePath, }; - // Create and initialize the server + // Create and start the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ @@ -53,10 +49,9 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig, ], variables, ) as Record; - ({ factory, initializer, store } = instances); + ({ app, store } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + await app.start(); // Create test helper for manipulating acl aclHelper = new AclHelper(store); @@ -75,9 +70,7 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeConfig, afterAll(async(): Promise => { await teardown(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); it('can add a document, read it and delete it if allowed.', async(): Promise => { diff --git a/test/integration/LdpHandlerWithoutAuth.test.ts b/test/integration/LdpHandlerWithoutAuth.test.ts index e2249434f..932fd36be 100644 --- a/test/integration/LdpHandlerWithoutAuth.test.ts +++ b/test/integration/LdpHandlerWithoutAuth.test.ts @@ -1,14 +1,18 @@ import { createReadStream } from 'fs'; -import type { Server } from 'http'; import fetch from 'cross-fetch'; import { DataFactory, Parser } from 'n3'; import { joinFilePath, PIM, RDF } from '../../src/'; -import type { Initializer } from '../../src/'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import type { App } from '../../src/'; import { LDP } from '../../src/util/Vocabularies'; import { deleteResource, expectQuads, getResource, patchResource, postResource, putResource } from '../util/FetchUtil'; import { getPort } from '../util/Util'; -import { getPresetConfigPath, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config'; +import { + getDefaultVariables, + getPresetConfigPath, + getTestConfigPath, + getTestFolder, + instantiateFromConfig, removeFolder, +} from './Config'; const { literal, namedNode, quad } = DataFactory; const port = getPort('LpdHandlerWithoutAuth'); @@ -27,18 +31,15 @@ const stores: [string, any][] = [ ]; describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeConfig, teardown }): void => { - let server: Server; - let initializer: Initializer; - let factory: HttpServerFactory; + let app: App; beforeAll(async(): Promise => { - const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, + const variables = { + ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:rootFilePath': rootFilePath, }; - // Create and initialize the server + // Create and start the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ @@ -47,17 +48,14 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC ], variables, ) as Record; - ({ factory, initializer } = instances); + ({ app } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + await app.start(); }); afterAll(async(): Promise => { await teardown(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); it('can read a container listing.', async(): Promise => { diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts index 0681d0167..c511f6112 100644 --- a/test/integration/LockingResourceStore.test.ts +++ b/test/integration/LockingResourceStore.test.ts @@ -15,9 +15,6 @@ import type { ReadWriteLocker } from '../../src/util/locking/ReadWriteLocker'; import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker'; import { WrappedExpiringReadWriteLocker } from '../../src/util/locking/WrappedExpiringReadWriteLocker'; import { guardedStreamFrom } from '../../src/util/StreamUtil'; -import { BASE } from './Config'; - -// The modern fake timers implementation blocks certain parts with the current setup jest.useFakeTimers('legacy'); describe('A LockingResourceStore', (): void => { @@ -44,7 +41,7 @@ describe('A LockingResourceStore', (): void => { ); // Initialize store - const initializer = new RootContainerInitializer({ store: source, baseUrl: BASE }); + const initializer = new RootContainerInitializer({ store: source, baseUrl: base }); await initializer.handleSafe(); locker = new EqualReadWriteLocker(new SingleThreadedResourceLocker()); diff --git a/test/integration/RedisResourceLockerIntegration.test.ts b/test/integration/RedisResourceLockerIntegration.test.ts index d31a714b7..ebd2719dd 100644 --- a/test/integration/RedisResourceLockerIntegration.test.ts +++ b/test/integration/RedisResourceLockerIntegration.test.ts @@ -1,9 +1,8 @@ -import type { Server } from 'http'; import fetch from 'cross-fetch'; -import type { RedisResourceLocker } from '../../src'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import type { App, RedisResourceLocker } from '../../src'; + import { describeIf, getPort } from '../util/Util'; -import { getTestConfigPath, instantiateFromConfig } from './Config'; +import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; /** * Test the general functionality of the server using a RedisResourceLocker @@ -11,28 +10,21 @@ import { getTestConfigPath, instantiateFromConfig } from './Config'; describeIf('docker', 'A server with a RedisResourceLocker as ResourceLocker', (): void => { const port = getPort('RedisResourceLocker'); const baseUrl = `http://localhost:${port}/`; - let server: Server; + let app: App; let locker: RedisResourceLocker; - let factory: HttpServerFactory; beforeAll(async(): Promise => { const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', getTestConfigPath('run-with-redlock.json'), - { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, - }, + getDefaultVariables(port, baseUrl), ) as Record; - ({ factory, locker } = instances); - server = factory.startServer(port); + ({ app, locker } = instances); + await app.start(); }); afterAll(async(): Promise => { - await locker.finalize(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); it('can add a file to the store, read it and delete it.', async(): Promise => { diff --git a/test/integration/ServerFetch.test.ts b/test/integration/ServerFetch.test.ts index 6c6df074b..f68832fc3 100644 --- a/test/integration/ServerFetch.test.ts +++ b/test/integration/ServerFetch.test.ts @@ -1,41 +1,27 @@ -import type { Server } from 'http'; import fetch from 'cross-fetch'; -import type { Initializer } from '../../src/init/Initializer'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; -import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage'; +import type { App } from '../../src/init/App'; import { getPort } from '../util/Util'; -import { getTestConfigPath, instantiateFromConfig } from './Config'; +import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; const port = getPort('ServerFetch'); const baseUrl = `http://localhost:${port}/`; // Some tests with real Requests/Responses until the mocking library has been removed from the tests describe('A Solid server', (): void => { - let server: Server; - let initializer: Initializer; - let expiringStorage: WrappedExpiringStorage; - let factory: HttpServerFactory; + let app: App; beforeAll(async(): Promise => { const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', getTestConfigPath('server-memory.json'), - { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:idpTemplateFolder': '', - }, + getDefaultVariables(port, baseUrl), ) as Record; - ({ factory, initializer, expiringStorage } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + ({ app } = instances); + await app.start(); }); afterAll(async(): Promise => { - expiringStorage.finalize(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); it('can do a successful HEAD request to a container.', async(): Promise => { diff --git a/test/integration/SparqlStorage.test.ts b/test/integration/SparqlStorage.test.ts index fdc3047a6..f38ca662c 100644 --- a/test/integration/SparqlStorage.test.ts +++ b/test/integration/SparqlStorage.test.ts @@ -1,28 +1,23 @@ import { promises as fs } from 'fs'; -import type { Server } from 'http'; import { joinFilePath } from '../../src/'; -import type { Initializer } from '../../src/'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import type { App } from '../../src/'; import { putResource } from '../util/FetchUtil'; import { describeIf, getPort } from '../util/Util'; -import { getPresetConfigPath, getTestConfigPath, instantiateFromConfig } from './Config'; +import { getDefaultVariables, getPresetConfigPath, getTestConfigPath, instantiateFromConfig } from './Config'; const port = getPort('SparqlStorage'); const baseUrl = `http://localhost:${port}/`; describeIf('docker', 'A server with a SPARQL endpoint as storage', (): void => { - let server: Server; - let initializer: Initializer; - let factory: HttpServerFactory; + let app: App; beforeAll(async(): Promise => { - const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': baseUrl, - 'urn:solid-server:default:variable:showStackTrace': true, + const variables = { + ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:4000/sparql', }; - // Create and initialize the server + // Create and start the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ @@ -31,16 +26,13 @@ describeIf('docker', 'A server with a SPARQL endpoint as storage', (): void => { ], variables, ) as Record; - ({ factory, initializer } = instances); + ({ app } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + await app.start(); }); afterAll(async(): Promise => { - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); it('can add a Turtle file to the store.', async(): Promise => { diff --git a/test/integration/Subdomains.test.ts b/test/integration/Subdomains.test.ts index 4283a9a57..bae9d866e 100644 --- a/test/integration/Subdomains.test.ts +++ b/test/integration/Subdomains.test.ts @@ -1,12 +1,15 @@ -import type { Server } from 'http'; import { stringify } from 'querystring'; import fetch from 'cross-fetch'; -import type { Initializer } from '../../src/init/Initializer'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; -import type { WrappedExpiringStorage } from '../../src/storage/keyvalue/WrappedExpiringStorage'; -import { joinFilePath } from '../../src/util/PathUtil'; +import type { App } from '../../src/init/App'; import { getPort } from '../util/Util'; -import { getPresetConfigPath, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config'; +import { + getDefaultVariables, + getPresetConfigPath, + getTestConfigPath, + getTestFolder, + instantiateFromConfig, + removeFolder, +} from './Config'; const port = getPort('Subdomains'); const baseUrl = `http://localhost:${port}/`; @@ -25,23 +28,18 @@ const stores: [string, any][] = [ // Simulating subdomains using the forwarded header so no DNS changes are required describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardown }): void => { - let server: Server; - let initializer: Initializer; - let factory: HttpServerFactory; - let expiringStorage: WrappedExpiringStorage; + let app: App; const settings = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', createPod: true }; const podHost = `alice.localhost:${port}`; const podUrl = `http://${podHost}/`; beforeAll(async(): Promise => { const variables: Record = { - 'urn:solid-server:default:variable:baseUrl': baseUrl, + ...getDefaultVariables(port, baseUrl), 'urn:solid-server:default:variable:rootFilePath': rootFilePath, - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'), }; - // Create and initialize the server + // Create and start the server const instances = await instantiateFromConfig( 'urn:solid-server:test:Instances', [ @@ -50,18 +48,14 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo ], variables, ) as Record; - ({ factory, initializer, expiringStorage } = instances); + ({ app } = instances); - await initializer.handleSafe(); - server = factory.startServer(port); + await app.start(); }); afterAll(async(): Promise => { - expiringStorage.finalize(); - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); await teardown(); + await app.stop(); }); describe('handling resources', (): void => { diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts index 343bf23ea..5ad84c9ff 100644 --- a/test/integration/WebSocketsProtocol.test.ts +++ b/test/integration/WebSocketsProtocol.test.ts @@ -1,37 +1,31 @@ -import type { Server } from 'http'; import fetch from 'cross-fetch'; import WebSocket from 'ws'; -import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import type { App } from '../../src/init/App'; import { getPort } from '../util/Util'; -import { getTestConfigPath, instantiateFromConfig } from './Config'; +import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config'; const port = getPort('WebSocketsProtocol'); const serverUrl = `http://localhost:${port}/`; const headers = { forwarded: 'host=example.pod;proto=https' }; describe('A server with the Solid WebSockets API behind a proxy', (): void => { - let server: Server; + let app: App; beforeAll(async(): Promise => { - const factory = await instantiateFromConfig( - 'urn:solid-server:default:ServerFactory', + app = await instantiateFromConfig( + 'urn:solid-server:default:App', getTestConfigPath('server-without-auth.json'), - { - 'urn:solid-server:default:variable:baseUrl': 'https://example.pod/', - 'urn:solid-server:default:variable:showStackTrace': true, - }, - ) as HttpServerFactory; - server = factory.startServer(port); + getDefaultVariables(port, 'https://example.pod/'), + ) as App; + await app.start(); }); afterAll(async(): Promise => { - await new Promise((resolve, reject): void => { - server.close((error): void => error ? reject(error) : resolve()); - }); + await app.stop(); }); - it('returns a 404 if no data was initialized.', async(): Promise => { - const response = await fetch(serverUrl, { headers }); + it('returns a 404 if there is no data.', async(): Promise => { + const response = await fetch(`${serverUrl}foo`, { headers }); expect(response.status).toBe(404); }); diff --git a/test/integration/config/ldp-with-auth.json b/test/integration/config/ldp-with-auth.json index 2d8eb88ba..40260eed4 100644 --- a/test/integration/config/ldp-with-auth.json +++ b/test/integration/config/ldp-with-auth.json @@ -1,11 +1,12 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/simple.json", "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", @@ -28,17 +29,12 @@ "@type": "RecordObject", "record": [ { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } }, { "RecordObject:_record_key": "store", "RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } } ] } diff --git a/test/integration/config/run-with-redlock.json b/test/integration/config/run-with-redlock.json index e95aa0326..6e22689a2 100644 --- a/test/integration/config/run-with-redlock.json +++ b/test/integration/config/run-with-redlock.json @@ -1,11 +1,12 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/simple.json", "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", "files-scs:config/http/static/default.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/allow-everything.json", "files-scs:config/ldp/handler/default.json", @@ -29,17 +30,12 @@ "@type": "RecordObject", "record": [ { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } }, { "RecordObject:_record_key": "locker", "RecordObject:_record_value": { "@id": "urn:solid-server:default:RedisResourceLocker" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } } ] } diff --git a/test/integration/config/server-dynamic-unsafe.json b/test/integration/config/server-dynamic-unsafe.json index f7bf4acd8..476949939 100644 --- a/test/integration/config/server-dynamic-unsafe.json +++ b/test/integration/config/server-dynamic-unsafe.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/unsafe-no-check.json", "files-scs:config/identity/pod/dynamic.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", @@ -33,24 +34,10 @@ "@type": "RecordObject", "record": [ { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } - }, - { - "comment": "Timer needs to be stopped when tests are finished.", - "RecordObject:_record_key": "expiringStorage", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ExpiringIdpStorage" } + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } } ] - }, - { - "@id": "urn:solid-server:default:ResourcesGenerator", - "TemplatedResourcesGenerator:_templateFolder": "$PACKAGE_ROOT/test/assets/templates" } ] } diff --git a/test/integration/config/server-memory.json b/test/integration/config/server-memory.json index 1c4adb500..250c65ad5 100644 --- a/test/integration/config/server-memory.json +++ b/test/integration/config/server-memory.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", @@ -8,7 +10,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/token.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", @@ -31,18 +32,8 @@ "@type": "RecordObject", "RecordObject:_record": [ { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } - }, - { - "comment": "Timer needs to be stopped when tests are finished.", - "RecordObject:_record_key": "expiringStorage", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ExpiringIdpStorage" } + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } } ] }, diff --git a/test/integration/config/server-subdomains-unsafe.json b/test/integration/config/server-subdomains-unsafe.json index 24ef6e904..c971044d2 100644 --- a/test/integration/config/server-subdomains-unsafe.json +++ b/test/integration/config/server-subdomains-unsafe.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/no-websockets.json", "files-scs:config/http/server-factory/no-websockets.json", @@ -9,7 +11,6 @@ "files-scs:config/identity/handler/default.json", "files-scs:config/identity/ownership/unsafe-no-check.json", "files-scs:config/identity/pod/static.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/debug-auth-header.json", "files-scs:config/ldp/authorization/webacl.json", "files-scs:config/ldp/handler/default.json", @@ -32,18 +33,8 @@ "@type": "RecordObject", "record": [ { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } - }, - { - "comment": "Timer needs to be stopped when tests are finished.", - "RecordObject:_record_key": "expiringStorage", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ExpiringIdpStorage" } + "RecordObject:_record_key": "app", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:App" } } ] } diff --git a/test/integration/config/server-without-auth.json b/test/integration/config/server-without-auth.json index 959c0c4ab..f97ef2358 100644 --- a/test/integration/config/server-without-auth.json +++ b/test/integration/config/server-without-auth.json @@ -1,11 +1,12 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", "import": [ + "files-scs:config/app/app/default.json", + "files-scs:config/app/init/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", "files-scs:config/http/static/default.json", - "files-scs:config/init/handler/default.json", "files-scs:config/ldp/authentication/dpop-bearer.json", "files-scs:config/ldp/authorization/allow-everything.json", "files-scs:config/ldp/handler/default.json", @@ -23,22 +24,6 @@ "files-scs:config/util/variables/default.json" ], "@graph": [ - { - "@id": "urn:solid-server:test:Instances", - "@type": "RecordObject", - "RecordObject:_record": [ - { - "comment": "Only use the parallel initializer. Starting the server is done in the test code..", - "RecordObject:_record_key": "initializer", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ParallelInitializer" } - }, - { - "RecordObject:_record_key": "factory", - "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } - } - ] - }, - { "@id": "urn:solid-server:default:IdentityProviderHandler", "@type": "UnsupportedAsyncHandler" diff --git a/test/unit/init/App.test.ts b/test/unit/init/App.test.ts new file mode 100644 index 000000000..95cd81885 --- /dev/null +++ b/test/unit/init/App.test.ts @@ -0,0 +1,25 @@ +import { App } from '../../../src/init/App'; +import type { Finalizable } from '../../../src/init/final/Finalizable'; +import type { Initializer } from '../../../src/init/Initializer'; + +describe('An App', (): void => { + let initializer: Initializer; + let finalizer: Finalizable; + let app: App; + + beforeEach(async(): Promise => { + initializer = { handleSafe: jest.fn() } as any; + finalizer = { finalize: jest.fn() }; + app = new App(initializer, finalizer); + }); + + it('can start with the initializer.', async(): Promise => { + await expect(app.start()).resolves.toBeUndefined(); + expect(initializer.handleSafe).toHaveBeenCalledTimes(1); + }); + + it('can stop with the finalizer.', async(): Promise => { + await expect(app.stop()).resolves.toBeUndefined(); + expect(finalizer.finalize).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index af8288dc2..40a80c30b 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -1,14 +1,14 @@ import { ComponentsManager } from 'componentsjs'; +import type { App } from '../../../src/init/App'; import { AppRunner } from '../../../src/init/AppRunner'; -import type { Initializer } from '../../../src/init/Initializer'; import { joinFilePath } from '../../../src/util/PathUtil'; -const initializer: jest.Mocked = { - handleSafe: jest.fn(), +const app: jest.Mocked = { + start: jest.fn(), } as any; -const manager: jest.Mocked> = { - instantiate: jest.fn(async(): Promise => initializer), +const manager: jest.Mocked> = { + instantiate: jest.fn(async(): Promise => app), configRegistry: { register: jest.fn(), }, @@ -17,7 +17,7 @@ const manager: jest.Mocked> = { jest.mock('componentsjs', (): any => ({ // eslint-disable-next-line @typescript-eslint/naming-convention ComponentsManager: { - build: jest.fn(async(): Promise> => manager), + build: jest.fn(async(): Promise> => manager), }, })); @@ -60,7 +60,7 @@ describe('AppRunner', (): void => { .toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json')); expect(manager.instantiate).toHaveBeenCalledTimes(1); expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:Initializer', + 'urn:solid-server:default:App', { variables: { 'urn:solid-server:default:variable:port': 3000, @@ -74,8 +74,8 @@ describe('AppRunner', (): void => { }, }, ); - expect(initializer.handleSafe).toHaveBeenCalledTimes(1); - expect(initializer.handleSafe).toHaveBeenCalledWith(); + expect(app.start).toHaveBeenCalledTimes(1); + expect(app.start).toHaveBeenCalledWith(); }); }); @@ -85,7 +85,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -101,7 +101,7 @@ describe('AppRunner', (): void => { .toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json')); expect(manager.instantiate).toHaveBeenCalledTimes(1); expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:Initializer', + 'urn:solid-server:default:App', { variables: { 'urn:solid-server:default:variable:port': 3000, @@ -115,8 +115,8 @@ describe('AppRunner', (): void => { }, }, ); - expect(initializer.handleSafe).toHaveBeenCalledTimes(1); - expect(initializer.handleSafe).toHaveBeenCalledWith(); + expect(app.start).toHaveBeenCalledTimes(1); + expect(app.start).toHaveBeenCalledWith(); }); it('accepts abbreviated flags.', async(): Promise => { @@ -136,7 +136,7 @@ describe('AppRunner', (): void => { ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -150,7 +150,7 @@ describe('AppRunner', (): void => { expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:Initializer', + 'urn:solid-server:default:App', { variables: { 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', @@ -183,7 +183,7 @@ describe('AppRunner', (): void => { ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -197,7 +197,7 @@ describe('AppRunner', (): void => { expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:Initializer', + 'urn:solid-server:default:App', { variables: { 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', @@ -231,7 +231,7 @@ describe('AppRunner', (): void => { new AppRunner().runCli(); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -245,7 +245,7 @@ describe('AppRunner', (): void => { expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:Initializer', + 'urn:solid-server:default:App', { variables: { 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', @@ -269,7 +269,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -285,12 +285,12 @@ describe('AppRunner', (): void => { }); it('exits without output to stderr when initialization fails.', async(): Promise => { - initializer.handleSafe.mockRejectedValueOnce(new Error('Fatal')); + app.start.mockRejectedValueOnce(new Error('Fatal')); new AppRunner().runCli({ argv: [ 'node', 'script' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -305,7 +305,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script', '--foo' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -320,7 +320,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script', '-s' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -335,7 +335,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); @@ -350,7 +350,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script', '-l', 'info', '-l', 'debug' ], }); - // Wait until initializer has been called, because we can't await AppRunner.run. + // Wait until app.start has been called, because we can't await AppRunner.run. await new Promise((resolve): void => { setImmediate(resolve); }); diff --git a/test/unit/init/final/ParallelFinalizer.test.ts b/test/unit/init/final/ParallelFinalizer.test.ts new file mode 100644 index 000000000..71dc8201f --- /dev/null +++ b/test/unit/init/final/ParallelFinalizer.test.ts @@ -0,0 +1,30 @@ +import type { Finalizable } from '../../../../src/init/final/Finalizable'; +import { ParallelFinalizer } from '../../../../src/init/final/ParallelFinalizer'; + +describe('A ParallelFinalizer', (): void => { + let finalizers: Finalizable[]; + let finalizer: ParallelFinalizer; + let results: number[]; + + beforeEach(async(): Promise => { + results = []; + finalizers = [ + { finalize: jest.fn((): any => results.push(0)) }, + { finalize: jest.fn((): any => results.push(1)) }, + ]; + + finalizer = new ParallelFinalizer(finalizers); + }); + + it('is finished when all finalizers are finished.', async(): Promise => { + await expect(finalizer.finalize()).resolves.toBeUndefined(); + expect(finalizers[0].finalize).toHaveBeenCalledTimes(1); + expect(finalizers[1].finalize).toHaveBeenCalledTimes(1); + expect(results).toEqual([ 0, 1 ]); + }); + + it('works if there are no input finalizers.', async(): Promise => { + finalizer = new ParallelFinalizer(); + await expect(finalizer.finalize()).resolves.toBeUndefined(); + }); +});