mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Support seeding pods and accounts
* feat(seeding): seed accounts and pods with seeded-pod-config.json * feat(seeding): dry up SeededPodInitializer by using RegistrationManager directly and make compatible with version/3.0.0 * feat(seeding): update seed config files to version 3.0.0 context * feat(seeding): simplify seeded-root config by importing pre-existing prefilled-root config * feat(seeding): Add seeding as a default initializer, update seeded pod copy and guide, change seeded pod config to array * feat(seeding): remove template info from seeded pod guide, use mockFs, code style nit, fix redlock test * feat(seeding): remove old config file
This commit is contained in:
parent
44dd56d4f6
commit
c8d4bfec39
@ -69,7 +69,7 @@ npm start -- # add parameters if needed
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 📦 Running via Docker
|
### 📦 Running via Docker
|
||||||
Docker allows you to run the server without having Node.js installed. Images are built on each tagged version and hosted on [Docker Hub](https://hub.docker.com/r/solidproject/community-server).
|
Docker allows you to run the server without having Node.js installed. Images are built on each tagged version and hosted on [Docker Hub](https://hub.docker.com/r/solidproject/community-server).
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Clone the repo to get access to the configs
|
# Clone the repo to get access to the configs
|
||||||
@ -110,6 +110,7 @@ to some commonly used settings:
|
|||||||
| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. |
|
| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. |
|
||||||
| `--showStackTrace, -t` | false | Enables detailed logging on error pages. |
|
| `--showStackTrace, -t` | false | Enables detailed logging on error pages. |
|
||||||
| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. |
|
| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. |
|
||||||
|
| `--seededPodConfigJson` | | Path to the file that keeps track of seeded Pod configurations. |
|
||||||
| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations.
|
| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations.
|
||||||
|
|
||||||
### 🧶 Custom configurations
|
### 🧶 Custom configurations
|
||||||
|
@ -10,7 +10,7 @@ it is always possible to not choose any of them and create your own custom versi
|
|||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
The easiest way to create a new config is by creating a JSON-LD file
|
The easiest way to create a new config is by creating a JSON-LD file
|
||||||
that imports one option from every component subfolder
|
that imports one option from every component subfolder
|
||||||
(such as either `allow-all.json` or `webacl.json` from `ldp/authorization`).
|
(such as either `allow-all.json` or `webacl.json` from `ldp/authorization`).
|
||||||
In case none of the available options suffice, there are 2 other ways to handle this:
|
In case none of the available options suffice, there are 2 other ways to handle this:
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"files-scs:config/app/init/initializers/base-url.json",
|
"files-scs:config/app/init/initializers/base-url.json",
|
||||||
"files-scs:config/app/init/initializers/logger.json",
|
"files-scs:config/app/init/initializers/logger.json",
|
||||||
"files-scs:config/app/init/initializers/server.json",
|
"files-scs:config/app/init/initializers/server.json",
|
||||||
|
"files-scs:config/app/init/initializers/seeded-pod.json",
|
||||||
"files-scs:config/app/init/initializers/version.json"
|
"files-scs:config/app/init/initializers/version.json"
|
||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
@ -15,6 +16,7 @@
|
|||||||
{ "@id": "urn:solid-server:default:LoggerInitializer" },
|
{ "@id": "urn:solid-server:default:LoggerInitializer" },
|
||||||
{ "@id": "urn:solid-server:default:BaseUrlVerifier" },
|
{ "@id": "urn:solid-server:default:BaseUrlVerifier" },
|
||||||
{ "@id": "urn:solid-server:default:ParallelInitializer" },
|
{ "@id": "urn:solid-server:default:ParallelInitializer" },
|
||||||
|
{ "@id": "urn:solid-server:default:SeededPodInitializer" },
|
||||||
{ "@id": "urn:solid-server:default:ServerInitializer" },
|
{ "@id": "urn:solid-server:default:ServerInitializer" },
|
||||||
{ "@id": "urn:solid-server:default:ModuleVersionVerifier" }
|
{ "@id": "urn:solid-server:default:ModuleVersionVerifier" }
|
||||||
]
|
]
|
||||||
|
23
config/app/init/initializers/seeded-pod.json
Normal file
23
config/app/init/initializers/seeded-pod.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"comment": "Separate manager from the RegistrationHandler in case registration is disabled.",
|
||||||
|
"@id": "urn:solid-server:default:SeededPodRegistrationManager",
|
||||||
|
"@type": "RegistrationManager",
|
||||||
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
|
"args_webIdSuffix": "/profile/card#me",
|
||||||
|
"args_identifierGenerator": { "@id": "urn:solid-server:default:IdentifierGenerator" },
|
||||||
|
"args_ownershipValidator": { "@id": "urn:solid-server:auth:password:OwnershipValidator" },
|
||||||
|
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||||
|
"args_podManager": { "@id": "urn:solid-server:default:PodManager" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Initializer that instantiates all the seeded accounts and pods.",
|
||||||
|
"@id": "urn:solid-server:default:SeededPodInitializer",
|
||||||
|
"@type": "SeededPodInitializer",
|
||||||
|
"registrationManager": { "@id": "urn:solid-server:default:SeededPodRegistrationManager" },
|
||||||
|
"configFilePath": { "@id": "urn:solid-server:default:variable:seededPodConfigJson" },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -57,6 +57,11 @@
|
|||||||
"requiresArg": true,
|
"requiresArg": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"describe": "Path to the file that keeps track of dynamic Pod configurations."
|
"describe": "Path to the file that keeps track of dynamic Pod configurations."
|
||||||
|
},
|
||||||
|
"seededPodConfigJson": {
|
||||||
|
"requiresArg": true,
|
||||||
|
"type": "string",
|
||||||
|
"describe": "Path to the file that will be used to seed pods."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -52,12 +52,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver",
|
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:podConfigJson",
|
||||||
"CombinedSettingsResolver:_computers_value": {
|
"CombinedSettingsResolver:_computers_value": {
|
||||||
"@type": "AssetPathExtractor",
|
"@type": "AssetPathExtractor",
|
||||||
"key": "podConfigJson",
|
"key": "podConfigJson",
|
||||||
"defaultPath": "./pod-config.json"
|
"defaultPath": "./pod-config.json"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CombinedSettingsResolver:_computers_key": "urn:solid-server:default:variable:seededPodConfigJson",
|
||||||
|
"CombinedSettingsResolver:_computers_value": {
|
||||||
|
"@type": "AssetPathExtractor",
|
||||||
|
"key": "seededPodConfigJson"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,11 @@
|
|||||||
"comment": "Path to the JSON file used to store configuration for dynamic pods.",
|
"comment": "Path to the JSON file used to store configuration for dynamic pods.",
|
||||||
"@id": "urn:solid-server:default:variable:podConfigJson",
|
"@id": "urn:solid-server:default:variable:podConfigJson",
|
||||||
"@type": "Variable"
|
"@type": "Variable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Path to the JSON file used to seed pods.",
|
||||||
|
"@id": "urn:solid-server:default:variable:seededPodConfigJson",
|
||||||
|
"@type": "Variable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
20
guides/seeding-pods.md
Normal file
20
guides/seeding-pods.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# How to seed Accounts and Pods
|
||||||
|
If you need to seed accounts and pods, set the `--seededPodConfigJson` option to a file such as `./seeded-pod-config.json` to set your desired accounts and pods. The contents of `./seeded-pod-config.json` (or whatever file name you choose) should be a JSON array whose entries are objects which include
|
||||||
|
`podName`, `email`, and `password`. For example:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"podName": "example",
|
||||||
|
"email": "hello@example.com",
|
||||||
|
"password": "abc123"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You may optionally specify other parameters accepted by the `register` method of [RegistrationManager](https://github.com/solid/community-server/blob/3b353affb1f0919fdcb66172364234eb59c2e3f6/src/identity/interaction/email-password/util/RegistrationManager.ts#L173). For example:
|
||||||
|
|
||||||
|
To use a pre-existing wedId:
|
||||||
|
```json
|
||||||
|
createWebId: false,
|
||||||
|
webId: "https://pod.inrupt.com/example/profile/card#me"
|
||||||
|
```
|
@ -207,6 +207,7 @@ export * from './init/ConfigPodInitializer';
|
|||||||
export * from './init/ContainerInitializer';
|
export * from './init/ContainerInitializer';
|
||||||
export * from './init/Initializer';
|
export * from './init/Initializer';
|
||||||
export * from './init/LoggerInitializer';
|
export * from './init/LoggerInitializer';
|
||||||
|
export * from './init/SeededPodInitializer';
|
||||||
export * from './init/ServerInitializer';
|
export * from './init/ServerInitializer';
|
||||||
export * from './init/ModuleVersionVerifier';
|
export * from './init/ModuleVersionVerifier';
|
||||||
|
|
||||||
|
52
src/init/SeededPodInitializer.ts
Normal file
52
src/init/SeededPodInitializer.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { promises as fsPromises } from 'fs';
|
||||||
|
import type { RegistrationManager } from '../identity/interaction/email-password/util/RegistrationManager';
|
||||||
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import { Initializer } from './Initializer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses a {@link RegistrationManager} to initialize accounts and pods
|
||||||
|
* for all seeded pods. Reads the pod settings from seededPodConfigJson.
|
||||||
|
*/
|
||||||
|
export class SeededPodInitializer extends Initializer {
|
||||||
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
private readonly registrationManager: RegistrationManager;
|
||||||
|
private readonly configFilePath: string | null;
|
||||||
|
|
||||||
|
public constructor(registrationManager: RegistrationManager, configFilePath: string | null) {
|
||||||
|
super();
|
||||||
|
this.registrationManager = registrationManager;
|
||||||
|
this.configFilePath = configFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(): Promise<void> {
|
||||||
|
if (!this.configFilePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const configText = await fsPromises.readFile(this.configFilePath, 'utf8');
|
||||||
|
const configuration: NodeJS.Dict<unknown>[] = JSON.parse(configText);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for await (const input of configuration) {
|
||||||
|
const config = {
|
||||||
|
confirmPassword: input.password,
|
||||||
|
createPod: true,
|
||||||
|
createWebId: true,
|
||||||
|
register: true,
|
||||||
|
...input,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.info(`Initializing pod ${input.podName}`);
|
||||||
|
|
||||||
|
// Validate the input JSON
|
||||||
|
const validated = this.registrationManager.validateInput(config, true);
|
||||||
|
this.logger.debug(`Validated input: ${JSON.stringify(validated)}`);
|
||||||
|
|
||||||
|
// Register and/or create a pod as requested. Potentially does nothing if all booleans are false.
|
||||||
|
await this.registrationManager.register(validated, true);
|
||||||
|
this.logger.info(`Initialized seeded pod and account for "${input.podName}".`);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
this.logger.info(`Initialized ${count} seeded pods.`);
|
||||||
|
}
|
||||||
|
}
|
@ -18,9 +18,14 @@ export class AssetPathExtractor extends SettingsExtractor {
|
|||||||
|
|
||||||
public async handle(args: Settings): Promise<unknown> {
|
public async handle(args: Settings): Promise<unknown> {
|
||||||
const path = args[this.key] ?? this.defaultPath;
|
const path = args[this.key] ?? this.defaultPath;
|
||||||
if (typeof path !== 'string') {
|
if (path) {
|
||||||
throw new Error(`Invalid ${this.key} argument`);
|
if (typeof path !== 'string') {
|
||||||
|
throw new Error(`Invalid ${this.key} argument`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveAssetPath(path);
|
||||||
}
|
}
|
||||||
return resolveAssetPath(path);
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,5 +48,6 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
|||||||
'urn:solid-server:default:variable:port': port,
|
'urn:solid-server:default:variable:port': port,
|
||||||
'urn:solid-server:default:variable:loggingLevel': 'off',
|
'urn:solid-server:default:variable:loggingLevel': 'off',
|
||||||
'urn:solid-server:default:variable:showStackTrace': true,
|
'urn:solid-server:default:variable:showStackTrace': true,
|
||||||
|
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
|
|||||||
getTestConfigPath('server-without-auth.json'),
|
getTestConfigPath('server-without-auth.json'),
|
||||||
getDefaultVariables(port, 'https://example.pod/'),
|
getDefaultVariables(port, 'https://example.pod/'),
|
||||||
) as App;
|
) as App;
|
||||||
|
|
||||||
await app.start();
|
await app.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
"files-scs:config/http/static/default.json",
|
"files-scs:config/http/static/default.json",
|
||||||
"files-scs:config/identity/access/public.json",
|
"files-scs:config/identity/access/public.json",
|
||||||
"files-scs:config/identity/handler/default.json",
|
"files-scs:config/identity/handler/default.json",
|
||||||
|
"files-scs:config/identity/ownership/token.json",
|
||||||
|
"files-scs:config/identity/pod/static.json",
|
||||||
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
||||||
"files-scs:config/ldp/authorization/webacl.json",
|
"files-scs:config/ldp/authorization/webacl.json",
|
||||||
"files-scs:config/ldp/handler/default.json",
|
"files-scs:config/ldp/handler/default.json",
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
"files-scs:config/http/middleware/no-websockets.json",
|
"files-scs:config/http/middleware/no-websockets.json",
|
||||||
"files-scs:config/http/server-factory/no-websockets.json",
|
"files-scs:config/http/server-factory/no-websockets.json",
|
||||||
"files-scs:config/http/static/default.json",
|
"files-scs:config/http/static/default.json",
|
||||||
|
"files-scs:config/identity/handler/account-store/default.json",
|
||||||
|
"files-scs:config/identity/ownership/unsafe-no-check.json",
|
||||||
|
"files-scs:config/identity/pod/static.json",
|
||||||
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
"files-scs:config/ldp/authentication/debug-auth-header.json",
|
||||||
"files-scs:config/ldp/authorization/allow-all.json",
|
"files-scs:config/ldp/authorization/allow-all.json",
|
||||||
"files-scs:config/ldp/handler/default.json",
|
"files-scs:config/ldp/handler/default.json",
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
"files-scs:config/http/middleware/websockets.json",
|
"files-scs:config/http/middleware/websockets.json",
|
||||||
"files-scs:config/http/server-factory/websockets.json",
|
"files-scs:config/http/server-factory/websockets.json",
|
||||||
"files-scs:config/http/static/default.json",
|
"files-scs:config/http/static/default.json",
|
||||||
|
"files-scs:config/identity/handler/account-store/default.json",
|
||||||
|
"files-scs:config/identity/ownership/unsafe-no-check.json",
|
||||||
|
"files-scs:config/identity/pod/static.json",
|
||||||
"files-scs:config/ldp/authentication/dpop-bearer.json",
|
"files-scs:config/ldp/authentication/dpop-bearer.json",
|
||||||
"files-scs:config/ldp/authorization/allow-all.json",
|
"files-scs:config/ldp/authorization/allow-all.json",
|
||||||
"files-scs:config/ldp/handler/default.json",
|
"files-scs:config/ldp/handler/default.json",
|
||||||
|
@ -62,6 +62,7 @@ describe('AppRunner', (): void => {
|
|||||||
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
||||||
'urn:solid-server:default:variable:showStackTrace': false,
|
'urn:solid-server:default:variable:showStackTrace': false,
|
||||||
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
|
'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(
|
const createdApp = await new AppRunner().create(
|
||||||
{
|
{
|
||||||
@ -99,6 +100,7 @@ describe('AppRunner', (): void => {
|
|||||||
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
'urn:solid-server:default:variable:rootFilePath': '/var/cwd/',
|
||||||
'urn:solid-server:default:variable:showStackTrace': false,
|
'urn:solid-server:default:variable:showStackTrace': false,
|
||||||
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
|
'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(
|
await new AppRunner().run(
|
||||||
{
|
{
|
||||||
@ -166,6 +168,7 @@ describe('AppRunner', (): void => {
|
|||||||
'-s', 'http://localhost:5000/sparql',
|
'-s', 'http://localhost:5000/sparql',
|
||||||
'-t',
|
'-t',
|
||||||
'--podConfigJson', '/different-path.json',
|
'--podConfigJson', '/different-path.json',
|
||||||
|
'--seededPodConfigJson', '/different-path.json',
|
||||||
];
|
];
|
||||||
process.argv = argvParameters;
|
process.argv = argvParameters;
|
||||||
|
|
||||||
|
47
test/unit/init/SeededPodInitializer.test.ts
Normal file
47
test/unit/init/SeededPodInitializer.test.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { promises as fsPromises } from 'fs';
|
||||||
|
import type { RegistrationManager } from '../../../src/identity/interaction/email-password/util/RegistrationManager';
|
||||||
|
import { SeededPodInitializer } from '../../../src/init/SeededPodInitializer';
|
||||||
|
import { mockFs } from '../../util/Util';
|
||||||
|
|
||||||
|
jest.mock('fs');
|
||||||
|
|
||||||
|
describe('A SeededPodInitializer', (): void => {
|
||||||
|
const dummyConfig = JSON.stringify([
|
||||||
|
{
|
||||||
|
podName: 'example',
|
||||||
|
email: 'hello@example.com',
|
||||||
|
password: 'abc123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
podName: 'example2',
|
||||||
|
email: 'hello2@example.com',
|
||||||
|
password: '123abc',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let registrationManager: RegistrationManager;
|
||||||
|
let configFilePath: string | null;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
configFilePath = './seeded-pod-config.json';
|
||||||
|
registrationManager = {
|
||||||
|
validateInput: jest.fn((input): any => input),
|
||||||
|
register: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockFs('/');
|
||||||
|
await fsPromises.writeFile(configFilePath, dummyConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate any accounts or pods if no config file is specified.', async(): Promise<void> => {
|
||||||
|
configFilePath = null;
|
||||||
|
await new SeededPodInitializer(registrationManager, configFilePath).handle();
|
||||||
|
expect(registrationManager.validateInput).not.toHaveBeenCalled();
|
||||||
|
expect(registrationManager.register).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates an account and a pod for every entry in the seeded pod configuration.', async(): Promise<void> => {
|
||||||
|
await new SeededPodInitializer(registrationManager, configFilePath).handle();
|
||||||
|
expect(registrationManager.validateInput).toHaveBeenCalledTimes(2);
|
||||||
|
expect(registrationManager.register).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
@ -25,4 +25,9 @@ describe('An AssetPathExtractor', (): void => {
|
|||||||
resolver = new AssetPathExtractor('path', '/root');
|
resolver = new AssetPathExtractor('path', '/root');
|
||||||
await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toBe('/root');
|
await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toBe('/root');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns null if not default value or default is provided.', async(): Promise<void> => {
|
||||||
|
resolver = new AssetPathExtractor('path');
|
||||||
|
await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user