Filled out the test-solid-server library

main
Jackson Morgan 4 months ago
parent f4e861de02
commit 5b7982cbce
  1. 534
      package-lock.json
  2. 1
      packages/test-solid-server/.gitignore
  3. 21
      packages/test-solid-server/LICENSE.txt
  4. 33
      packages/test-solid-server/package.json
  5. 114
      packages/test-solid-server/src/authFetch.ts
  6. 44
      packages/test-solid-server/src/configs/server-config-without-websocket.json
  7. 43
      packages/test-solid-server/src/configs/server-config.json
  8. 9
      packages/test-solid-server/src/configs/solid-css-seed.json
  9. 37
      packages/test-solid-server/src/createServer.ts
  10. 2
      packages/test-solid-server/src/index.ts
  11. 76
      packages/test-solid-server/src/resourceUtils.ts
  12. 62
      packages/test-solid-server/src/setupTestServer.ts
  13. 0
      packages/test-solid-server/tsconfig.build.json

534
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1 +1,2 @@
node_modules node_modules
*/data

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jackson Morgan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,33 @@
{
"name": "@ldo/test-solid-server",
"version": "1.0.0-alpha.3",
"description": "A solid server to be used in jest tests",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"prepublishOnly": "npm run build",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern"
},
"repository": {
"type": "git",
"url": "git+https://github.com/o-development/ldo.git"
},
"author": "Jackson Morgan",
"license": "MIT",
"bugs": {
"url": "https://github.com/o-development/ldo/issues"
},
"homepage": "https://github.com/o-development/ldo/tree/main/packages/solid#readme",
"dependencies": {
"@inrupt/solid-client-authn-core": "^2.2.6",
"@solid/community-server": "^7.1.3",
"cross-env": "^7.0.3"
},
"files": [
"dist",
"src"
],
"publishConfig": {
"access": "public"
}
}

@ -0,0 +1,114 @@
import type { KeyPair } from "@inrupt/solid-client-authn-core";
import {
buildAuthenticatedFetch,
createDpopHeader,
generateDpopKeyPair,
} from "@inrupt/solid-client-authn-core";
import fetch from "cross-fetch";
const config = {
podName: "example",
email: "hello@example.com",
password: "abc123",
};
async function getAuthorization(port: number): Promise<string> {
// First we request the account API controls to find out where we can log in
const indexResponse = await fetch(`http://localhost:${port}/.account/`);
const { controls } = await indexResponse.json();
// And then we log in to the account API
const response = await fetch(controls.password.login, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
email: config.email,
password: config.password,
}),
});
// This authorization value will be used to authenticate in the next step
const result = await response.json();
return result.authorization;
}
async function getSecret(
port: number,
authorization: string,
): Promise<{ id: string; secret: string; resource: string }> {
// Now that we are logged in, we need to request the updated controls from the server.
// These will now have more values than in the previous example.
const indexResponse = await fetch(`http://localhost:${port}/.account/`, {
headers: { authorization: `CSS-Account-Token ${authorization}` },
});
const { controls } = await indexResponse.json();
// Here we request the server to generate a token on our account
const response = await fetch(controls.account.clientCredentials, {
method: "POST",
headers: {
authorization: `CSS-Account-Token ${authorization}`,
"content-type": "application/json",
},
// The name field will be used when generating the ID of your token.
// The WebID field determines which WebID you will identify as when using the token.
// Only WebIDs linked to your account can be used.
body: JSON.stringify({
name: "my-token",
webId: `http://localhost:${port}/${config.podName}/profile/card#me`,
}),
});
// These are the identifier and secret of your token.
// Store the secret somewhere safe as there is no way to request it again from the server!
// The `resource` value can be used to delete the token at a later point in time.
const response2 = await response.json();
return response2;
}
async function getAccessToken(
port: number,
id: string,
secret: string,
): Promise<{ accessToken: string; dpopKey: KeyPair }> {
try {
// A key pair is needed for encryption.
// This function from `solid-client-authn` generates such a pair for you.
const dpopKey = await generateDpopKeyPair();
// These are the ID and secret generated in the previous step.
// Both the ID and the secret need to be form-encoded.
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(
secret,
)}`;
// This URL can be found by looking at the "token_endpoint" field at
// http://localhost:3001/.well-known/openid-configuration
// if your server is hosted at http://localhost:3000/.
const tokenUrl = `http://localhost:${port}/.oidc/token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
// The header needs to be in base64 encoding.
authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
"content-type": "application/x-www-form-urlencoded",
dpop: await createDpopHeader(tokenUrl, "POST", dpopKey),
},
body: "grant_type=client_credentials&scope=webid",
});
// This is the Access token that will be used to do an authenticated request to the server.
// The JSON also contains an "expires_in" field in seconds,
// which you can use to know when you need request a new Access token.
const response2 = await response.json();
return { accessToken: response2.access_token, dpopKey };
} catch (err) {
console.error(err);
throw err;
}
}
export async function generateAuthFetch(port: number) {
const authorization = await getAuthorization(port);
const { id, secret } = await getSecret(port, authorization);
const { accessToken, dpopKey } = await getAccessToken(port, id, secret);
return await buildAuthenticatedFetch(accessToken, { dpopKey });
}

@ -0,0 +1,44 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"import": [
"css:config/app/init/initialize-root.json",
"css:config/app/main/default.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/webhooks.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/no-accounts.json",
"css:config/identity/oidc/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/file.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/location/root.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/file.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"comment": [
"A Solid server that stores its resources on disk and uses WAC for authorization.",
"No registration and the root container is initialized to allow full access for everyone so make sure to change this."
]
}
]
}

@ -0,0 +1,43 @@
{
"@context": [
"https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld"
],
"import": [
"css:config/app/init/static-root.json",
"css:config/app/main/default.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/all.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/oidc/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/memory.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/location/pod.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/memory.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"comment": "A Solid server that stores its resources on disk and uses WAC for authorization."
}
]
}

@ -0,0 +1,9 @@
[
{
"email": "hello@example.com",
"password": "abc123",
"pods": [
{ "name": "example" }
]
}
]

@ -0,0 +1,37 @@
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
import * as path from "path";
import type { App } from "@solid/community-server";
import { AppRunner, resolveModulePath } from "@solid/community-server";
import "jest-rdf";
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
export async function createApp(
port: number,
customConfigPath?: string,
): Promise<App> {
if (process.env.SERVER) {
return {
start: () => {},
stop: () => {},
} as App;
}
const appRunner = new AppRunner();
return appRunner.create({
loaderProperties: {
mainModulePath: resolveModulePath(""),
typeChecking: false,
},
config: customConfigPath ?? resolveModulePath("config/file-root.json"),
variableBindings: {},
shorthand: {
port: port,
loggingLevel: "off",
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"),
rootFilePath: path.join(__dirname, "./data"),
},
});
}

@ -0,0 +1,2 @@
export * from "./createServer";
export * from "./setupTestServer";

@ -0,0 +1,76 @@
export type ResourceInfo = ContainerInfo | LeafInfo;
interface ContainerInfo {
slug: string;
isContainer: true;
shouldNotInit: boolean;
contains: (ContainerInfo | LeafInfo)[];
}
interface LeafInfo {
slug: string;
isContainer: false;
data: string;
shouldNotInit?: boolean;
mimeType: string;
}
export async function initResources(
rootUri: string,
resourceInfo: ResourceInfo,
authFetch: typeof fetch,
): Promise<void> {
if (resourceInfo?.shouldNotInit) return;
if (resourceInfo.isContainer) {
await authFetch(rootUri, {
method: "POST",
headers: {
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"',
slug: resourceInfo.slug,
},
});
await Promise.all(
resourceInfo.contains.map((subResourceInfo) =>
initResources(
`${rootUri}${resourceInfo.slug}`,
subResourceInfo,
authFetch,
),
),
);
} else {
authFetch(rootUri, {
method: "POST",
headers: {
"content-type": resourceInfo.mimeType,
slug: resourceInfo.slug,
},
body: resourceInfo.data,
});
}
}
export async function cleanResources(
rootUri: string,
resourceInfo: ResourceInfo,
authFetch: typeof fetch,
): Promise<void> {
if (resourceInfo.isContainer) {
await Promise.all(
resourceInfo.contains.map((subResourceInfo) =>
cleanResources(
`${rootUri}${resourceInfo.slug}`,
subResourceInfo,
authFetch,
),
),
);
await authFetch(`${rootUri}${resourceInfo.slug}`, {
method: "DELETE",
});
} else {
await authFetch(`${rootUri}${resourceInfo.slug}`, {
method: "DELETE",
});
}
}

@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { App } from "@solid/community-server";
import { createApp } from "./createServer";
import path from "path";
import type { ResourceInfo } from "./resourceUtils";
import { cleanResources, initResources } from "./resourceUtils";
import { generateAuthFetch } from "./authFetch";
import fs from "fs/promises";
export function setupServer(
port: number,
resourceInfo: ResourceInfo,
customConfigPath?: string,
) {
const data: {
app: App;
fetchMock: jest.Mock<
Promise<Response>,
[input: RequestInfo | URL, init?: RequestInit | undefined]
>;
rootUri: string;
rootContainer: string;
} = {
rootUri: `https://localhost:${port}`,
rootContainer: `https://localhost:${port}/example/`,
} as any;
let authFetch: typeof fetch;
let previousJestId: string | undefined;
let previousNodeEnv: string | undefined;
beforeAll(async () => {
// Remove Jest ID so that community solid server doesn't use the Jest Import
previousJestId = process.env.JEST_WORKER_ID;
previousNodeEnv = process.env.NODE_ENV;
delete process.env.JEST_WORKER_ID;
process.env.NODE_ENV = "other_test";
// Start up the server
data.app = await createApp(port, customConfigPath);
await data.app.start();
authFetch = await generateAuthFetch(port);
});
afterAll(async () => {
data.app.stop();
process.env.JEST_WORKER_ID = previousJestId;
process.env.NODE_ENV = previousNodeEnv;
const testDataPath = path.join(__dirname, "./data");
await fs.rm(testDataPath, { recursive: true, force: true });
});
beforeEach(async () => {
data.fetchMock = jest.fn(authFetch);
// Create a new document called sample.ttl
await initResources(data.rootUri, resourceInfo, authFetch);
});
afterEach(async () => {
await cleanResources(data.rootUri, resourceInfo, authFetch);
});
return data;
}
Loading…
Cancel
Save