parent
0c4b5e0770
commit
f4e861de02
@ -0,0 +1,101 @@ |
||||
import type { App } from "@solid/community-server"; |
||||
import type { ConnectedLdoDataset } from "../src"; |
||||
import { ROOT_CONTAINER, WEB_ID, createApp } from "./solidServer.helper"; |
||||
import { generateAuthFetch } from "./authFetch.helper"; |
||||
import type { SolidConnectedPlugin } from "@ldo/connected-solid"; |
||||
|
||||
describe("Link Traversal", () => { |
||||
let app: App; |
||||
let authFetch: typeof fetch; |
||||
let fetchMock: jest.Mock< |
||||
Promise<Response>, |
||||
[input: RequestInfo | URL, init?: RequestInit | undefined] |
||||
>; |
||||
let solidLdoDataset: ConnectedLdoDataset<SolidConnectedPlugin[]>; |
||||
|
||||
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
|
||||
app = await createApp(); |
||||
await app.start(); |
||||
authFetch = await generateAuthFetch(); |
||||
}); |
||||
|
||||
afterAll(async () => { |
||||
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 () => { |
||||
fetchMock = jest.fn(authFetch); |
||||
solidLdoDataset = createSolidLdoDataset(); |
||||
solidLdoDataset.setContext("solid", { fetch: fetchMock }); |
||||
// Create a new document called sample.ttl
|
||||
await authFetch(ROOT_CONTAINER, { |
||||
method: "POST", |
||||
headers: { |
||||
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"', |
||||
slug: TEST_CONTAINER_SLUG, |
||||
}, |
||||
}); |
||||
await authFetch(TEST_CONTAINER_ACL_URI, { |
||||
method: "PUT", |
||||
headers: { |
||||
"content-type": "text/turtle", |
||||
}, |
||||
body: TEST_CONTAINER_ACL, |
||||
}); |
||||
await Promise.all([ |
||||
authFetch(TEST_CONTAINER_URI, { |
||||
method: "POST", |
||||
headers: { "content-type": "text/turtle", slug: "sample.ttl" }, |
||||
body: SPIDER_MAN_TTL, |
||||
}), |
||||
authFetch(TEST_CONTAINER_URI, { |
||||
method: "POST", |
||||
headers: { "content-type": "text/plain", slug: "sample.txt" }, |
||||
body: "some text.", |
||||
}), |
||||
authFetch(TEST_CONTAINER_URI, { |
||||
method: "POST", |
||||
headers: { "content-type": "text/turtle", slug: "profile.ttl" }, |
||||
body: SAMPLE_PROFILE_TTL, |
||||
}), |
||||
]); |
||||
}); |
||||
|
||||
afterEach(async () => { |
||||
await Promise.all([ |
||||
authFetch(SAMPLE_DATA_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
authFetch(SAMPLE2_DATA_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
authFetch(SAMPLE_BINARY_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
authFetch(SAMPLE2_BINARY_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
authFetch(SAMPLE_PROFILE_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
authFetch(SAMPLE_CONTAINER_URI, { |
||||
method: "DELETE", |
||||
}), |
||||
]); |
||||
await authFetch(TEST_CONTAINER_URI, { |
||||
method: "DELETE", |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,112 @@ |
||||
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: process.env.USER_NAME || "example", |
||||
email: process.env.EMAIL || "hello@example.com", |
||||
password: process.env.PASSWORD || "abc123", |
||||
}; |
||||
|
||||
async function getAuthorization(): Promise<string> { |
||||
// First we request the account API controls to find out where we can log in
|
||||
const indexResponse = await fetch("http://localhost:3001/.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( |
||||
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:3001/.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:3001/${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( |
||||
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:3001/.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() { |
||||
const authorization = await getAuthorization(); |
||||
const { id, secret } = await getSecret(authorization); |
||||
const { accessToken, dpopKey } = await getAccessToken(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,3 @@ |
||||
import { config } from "dotenv"; |
||||
|
||||
config(); |
@ -0,0 +1,42 @@ |
||||
// 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"; |
||||
import type { SolidContainerUri } from "../src"; |
||||
|
||||
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; |
||||
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || ""; |
||||
export const ROOT_CONTAINER = |
||||
`${SERVER_DOMAIN}${ROOT_ROUTE}` as SolidContainerUri; |
||||
export const WEB_ID = |
||||
process.env.WEB_ID || `${SERVER_DOMAIN}example/profile/card#me`; |
||||
|
||||
// Use an increased timeout, since the CSS server takes too much setup time.
|
||||
jest.setTimeout(40_000); |
||||
|
||||
export async function createApp(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: 3_001, |
||||
loggingLevel: "off", |
||||
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"), |
||||
rootFilePath: path.join(__dirname, "./data"), |
||||
}, |
||||
}); |
||||
} |
@ -0,0 +1,5 @@ |
||||
{ |
||||
"extends": [ |
||||
"../../.eslintrc" |
||||
] |
||||
} |
@ -0,0 +1 @@ |
||||
node_modules |
@ -0,0 +1,3 @@ |
||||
# @ldo/test-solid-server |
||||
|
||||
This is a reusable Solid Server to be used in Jest integration tests. |
@ -0,0 +1,13 @@ |
||||
{ |
||||
"extends": "../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"outDir": "./dist" |
||||
}, |
||||
"include": [ |
||||
"./src" |
||||
], |
||||
"exclude": [ |
||||
"./dist", |
||||
"./coverage" |
||||
] |
||||
} |
Loading…
Reference in new issue