parent
f4e861de02
commit
5b7982cbce
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…
Reference in new issue