Began setting up tests for connected library

main
Jackson Morgan 4 months ago
parent 0c4b5e0770
commit f4e861de02
  1. 3
      packages/connected/package.json
  2. 6
      packages/connected/test/ErrorResult.test.ts
  3. 101
      packages/connected/test/LinkTraversalIntegration.test.ts
  4. 112
      packages/connected/test/authFetch.helper.ts
  5. 44
      packages/connected/test/configs/server-config-without-websocket.json
  6. 43
      packages/connected/test/configs/server-config.json
  7. 9
      packages/connected/test/configs/solid-css-seed.json
  8. 76
      packages/connected/test/mocks/MockResource.ts
  9. 3
      packages/connected/test/setup-tests.ts
  10. 42
      packages/connected/test/solidServer.helper.ts
  11. 5
      packages/test-solid-server/.eslintrc
  12. 1
      packages/test-solid-server/.gitignore
  13. 0
      packages/test-solid-server/LICENSE.txt
  14. 3
      packages/test-solid-server/Readme.md
  15. 13
      packages/test-solid-server/tsconfig.json

@ -23,6 +23,7 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme",
"devDependencies": {
"@ldo/connected-solid": "^1.0.0-alpha.3",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"cross-env": "^7.0.3",
@ -44,4 +45,4 @@
"publishConfig": {
"access": "public"
}
}
}

@ -5,9 +5,9 @@ import {
UnexpectedResourceError,
} from "../src/results/error/ErrorResult";
import { InvalidUriError } from "../src/results/error/InvalidUriError";
import { MockResouce } from "./mocks/MockResource";
import { MockResource } from "./mocks/MockResource";
const mockResource = new MockResouce("https://example.com/");
const mockResource = new MockResource("https://example.com/");
describe("ErrorResult", () => {
describe("fromThrown", () => {
@ -38,7 +38,7 @@ describe("ErrorResult", () => {
});
describe("default messages", () => {
class ConcreteResourceError extends ResourceError<MockResouce> {
class ConcreteResourceError extends ResourceError<MockResource> {
readonly type = "concreteResourceError" as const;
}
class ConcreteErrorResult extends ErrorResult {

@ -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" }
]
}
]

@ -11,7 +11,7 @@ import type { DatasetChanges } from "@ldo/rdf-utils";
import type { ReadSuccess } from "../../src/results/success/ReadSuccess";
import type { UpdateSuccess } from "../../src/results/success/UpdateSuccess";
export class MockResouce
export class MockResource
extends (EventEmitter as new () => ResourceEventEmitter)
implements Resource
{
@ -26,48 +26,34 @@ export class MockResouce
this.status = new Unfetched(this);
}
isLoading(): boolean {
throw new Error("Method not implemented.");
}
isFetched(): boolean {
throw new Error("Method not implemented.");
}
isUnfetched(): boolean {
throw new Error("Method not implemented.");
}
isDoingInitialFetch(): boolean {
throw new Error("Method not implemented.");
}
isPresent(): boolean | undefined {
throw new Error("Method not implemented.");
}
isAbsent(): boolean | undefined {
throw new Error("Method not implemented.");
}
isSubscribedToNotifications(): boolean {
throw new Error("Method not implemented.");
}
read(): Promise<ReadSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
readIfUnfetched(): Promise<ReadSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
update(
_datasetChanges: DatasetChanges,
): Promise<UpdateSuccess<any> | ResourceError<any>> {
throw new Error("Method not implemented.");
}
subscribeToNotifications(_callbacks?: {
onNotification: (message: any) => void;
onNotificationError: (err: Error) => void;
}): Promise<string> {
throw new Error("Method not implemented.");
}
unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {
throw new Error("Method not implemented.");
}
unsubscribeFromAllNotifications(): Promise<void> {
throw new Error("Method not implemented.");
}
isLoading = jest.fn<boolean, []>();
isFetched = jest.fn<boolean, []>();
isUnfetched = jest.fn<boolean, []>();
isDoingInitialFetch = jest.fn<boolean, []>();
isPresent = jest.fn<boolean | undefined, []>();
isAbsent = jest.fn<boolean | undefined, []>();
isSubscribedToNotifications = jest.fn<boolean, []>();
read = jest.fn<Promise<ReadSuccess<any> | ResourceError<any>>, []>();
readIfUnfetched = jest.fn<
Promise<ReadSuccess<any> | ResourceError<any>>,
[]
>();
update = jest.fn<
Promise<UpdateSuccess<any> | ResourceError<any>>,
[DatasetChanges]
>();
subscribeToNotifications = jest.fn<
Promise<string>,
[
{
onNotification: (message: any) => void;
onNotificationError: (err: Error) => void;
}?,
]
>();
unsubscribeFromNotifications = jest.fn<Promise<void>, [string]>();
unsubscribeFromAllNotifications = jest.fn<Promise<void>, []>();
}

@ -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…
Cancel
Save