parent
2fa3e52246
commit
9ae7dea357
@ -0,0 +1,31 @@ |
||||
import { ContextDefinition } from "jsonld"; |
||||
|
||||
/** |
||||
* ============================================================================= |
||||
* postContext: JSONLD Context for post |
||||
* ============================================================================= |
||||
*/ |
||||
export const postContext: ContextDefinition = { |
||||
type: { |
||||
"@id": "@type", |
||||
}, |
||||
SocialMediaPosting: "http://schema.org/SocialMediaPosting", |
||||
CreativeWork: "http://schema.org/CreativeWork", |
||||
Thing: "http://schema.org/Thing", |
||||
articleBody: { |
||||
"@id": "http://schema.org/articleBody", |
||||
"@type": "http://www.w3.org/2001/XMLSchema#string", |
||||
}, |
||||
uploadDate: { |
||||
"@id": "http://schema.org/uploadDate", |
||||
"@type": "http://www.w3.org/2001/XMLSchema#date", |
||||
}, |
||||
image: { |
||||
"@id": "http://schema.org/image", |
||||
"@type": "@id", |
||||
}, |
||||
publisher: { |
||||
"@id": "http://schema.org/publisher", |
||||
"@type": "@id", |
||||
}, |
||||
}; |
@ -0,0 +1,155 @@ |
||||
import { Schema } from "shexj"; |
||||
|
||||
/** |
||||
* ============================================================================= |
||||
* postSchema: ShexJ Schema for post |
||||
* ============================================================================= |
||||
*/ |
||||
export const postSchema: Schema = { |
||||
type: "Schema", |
||||
shapes: [ |
||||
{ |
||||
id: "https://example.com/PostSh", |
||||
type: "ShapeDecl", |
||||
shapeExpr: { |
||||
type: "Shape", |
||||
expression: { |
||||
type: "EachOf", |
||||
expressions: [ |
||||
{ |
||||
type: "TripleConstraint", |
||||
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||
valueExpr: { |
||||
type: "NodeConstraint", |
||||
values: [ |
||||
"http://schema.org/SocialMediaPosting", |
||||
"http://schema.org/CreativeWork", |
||||
"http://schema.org/Thing", |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
type: "TripleConstraint", |
||||
predicate: "http://schema.org/articleBody", |
||||
valueExpr: { |
||||
type: "NodeConstraint", |
||||
datatype: "http://www.w3.org/2001/XMLSchema#string", |
||||
}, |
||||
min: 0, |
||||
max: 1, |
||||
annotations: [ |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#label", |
||||
object: { |
||||
value: "articleBody", |
||||
}, |
||||
}, |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||
object: { |
||||
value: "The actual body of the article. ", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
type: "TripleConstraint", |
||||
predicate: "http://schema.org/uploadDate", |
||||
valueExpr: { |
||||
type: "NodeConstraint", |
||||
datatype: "http://www.w3.org/2001/XMLSchema#date", |
||||
}, |
||||
annotations: [ |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#label", |
||||
object: { |
||||
value: "uploadDate", |
||||
}, |
||||
}, |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||
object: { |
||||
value: |
||||
"Date when this media object was uploaded to this site.", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
type: "TripleConstraint", |
||||
predicate: "http://schema.org/image", |
||||
valueExpr: { |
||||
type: "NodeConstraint", |
||||
nodeKind: "iri", |
||||
}, |
||||
min: 0, |
||||
max: 1, |
||||
annotations: [ |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#label", |
||||
object: { |
||||
value: "image", |
||||
}, |
||||
}, |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||
object: { |
||||
value: |
||||
"A media object that encodes this CreativeWork. This property is a synonym for encoding.", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
type: "TripleConstraint", |
||||
predicate: "http://schema.org/publisher", |
||||
valueExpr: { |
||||
type: "NodeConstraint", |
||||
nodeKind: "iri", |
||||
}, |
||||
annotations: [ |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#label", |
||||
object: { |
||||
value: "publisher", |
||||
}, |
||||
}, |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||
object: { |
||||
value: "The publisher of the creative work.", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
annotations: [ |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#label", |
||||
object: { |
||||
value: "SocialMediaPost", |
||||
}, |
||||
}, |
||||
{ |
||||
type: "Annotation", |
||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
||||
object: { |
||||
value: |
||||
"A post to a social media platform, including blog posts, tweets, Facebook posts, etc.", |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
], |
||||
}; |
@ -0,0 +1,19 @@ |
||||
import { ShapeType } from "@ldo/ldo"; |
||||
import { postSchema } from "./post.schema"; |
||||
import { postContext } from "./post.context"; |
||||
import { PostSh } from "./post.typings"; |
||||
|
||||
/** |
||||
* ============================================================================= |
||||
* LDO ShapeTypes post |
||||
* ============================================================================= |
||||
*/ |
||||
|
||||
/** |
||||
* PostSh ShapeType |
||||
*/ |
||||
export const PostShShapeType: ShapeType<PostSh> = { |
||||
schema: postSchema, |
||||
shape: "https://example.com/PostSh", |
||||
context: postContext, |
||||
}; |
@ -0,0 +1,45 @@ |
||||
import { ContextDefinition } from "jsonld"; |
||||
|
||||
/** |
||||
* ============================================================================= |
||||
* Typescript Typings for post |
||||
* ============================================================================= |
||||
*/ |
||||
|
||||
/** |
||||
* PostSh Type |
||||
*/ |
||||
export interface PostSh { |
||||
"@id"?: string; |
||||
"@context"?: ContextDefinition; |
||||
type: |
||||
| { |
||||
"@id": "SocialMediaPosting"; |
||||
} |
||||
| { |
||||
"@id": "CreativeWork"; |
||||
} |
||||
| { |
||||
"@id": "Thing"; |
||||
}; |
||||
/** |
||||
* The actual body of the article. |
||||
*/ |
||||
articleBody?: string; |
||||
/** |
||||
* Date when this media object was uploaded to this site. |
||||
*/ |
||||
uploadDate: string; |
||||
/** |
||||
* A media object that encodes this CreativeWork. This property is a synonym for encoding. |
||||
*/ |
||||
image?: { |
||||
"@id": string; |
||||
}; |
||||
/** |
||||
* The publisher of the creative work. |
||||
*/ |
||||
publisher: { |
||||
"@id": string; |
||||
}; |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,256 @@ |
||||
// import type { App } from "@solid/community-server";
|
||||
// import { getAuthenticatedFetch, ROOT_COONTAINER } from "./solidServer.helper";
|
||||
// import type { SolidLdoDataset } from "../src/SolidLdoDataset";
|
||||
// import { createSolidLdoDataset } from "../src/createSolidLdoDataset";
|
||||
// import { LeafRequester } from "../src/requester/LeafRequester";
|
||||
// import { namedNode, quad as createQuad } from "@rdfjs/data-model";
|
||||
|
||||
describe("Leaf Requester", () => { |
||||
it("trivial", () => { |
||||
expect(true).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
// describe.skip("Leaf Requester", () => {
|
||||
// let _app: App;
|
||||
// let authFetch: typeof fetch;
|
||||
// let fetchMock: typeof fetch;
|
||||
// let solidLdoDataset: SolidLdoDataset;
|
||||
|
||||
// beforeAll(async () => {
|
||||
// // Start up the server
|
||||
// // app = await createApp();
|
||||
// // await app.start();
|
||||
|
||||
// authFetch = await getAuthenticatedFetch();
|
||||
// });
|
||||
|
||||
// beforeEach(async () => {
|
||||
// fetchMock = jest.fn(authFetch);
|
||||
// solidLdoDataset = createSolidLdoDataset({ fetch: fetchMock });
|
||||
// // Create a new document called sample.ttl
|
||||
// await Promise.all([
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/`, {
|
||||
// method: "POST",
|
||||
// headers: { "content-type": "text/turtle", slug: "sample.ttl" },
|
||||
// body: `@base <http://example.org/> .
|
||||
// @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
// @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
// @prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
||||
// @prefix rel: <http://www.perceive.net/schemas/relationship/> .
|
||||
|
||||
// <#green-goblin>
|
||||
// rel:enemyOf <#spiderman> ;
|
||||
// a foaf:Person ; # in the context of the Marvel universe
|
||||
// foaf:name "Green Goblin" .
|
||||
|
||||
// <#spiderman>
|
||||
// rel:enemyOf <#green-goblin> ;
|
||||
// a foaf:Person ;
|
||||
// foaf:name "Spiderman", "Человек-паук"@ru .`,
|
||||
// }),
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/`, {
|
||||
// method: "PUT",
|
||||
// headers: { "content-type": "text/plain", slug: "sample.txt" },
|
||||
// body: `some text.`,
|
||||
// }),
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// afterEach(async () => {
|
||||
// await Promise.all([
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/sample.ttl`, {
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/sample2.ttl`, {
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/sample.txt`, {
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// authFetch(`${ROOT_COONTAINER}test_leaf/sample2.txt`, {
|
||||
// method: "DELETE",
|
||||
// }),
|
||||
// ]);
|
||||
// });
|
||||
|
||||
// /**
|
||||
// * ===========================================================================
|
||||
// * Read
|
||||
// * ===========================================================================
|
||||
// */
|
||||
// it("reads data", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/sample.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.read();
|
||||
// expect(result.type).toBe("data");
|
||||
// expect(
|
||||
// solidLdoDataset.match(
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`),
|
||||
// ).size,
|
||||
// ).toBe(7);
|
||||
// });
|
||||
|
||||
// it("reads data that doesn't exist", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/doesnotexist.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.read();
|
||||
// expect(result.type).toBe("absent");
|
||||
// });
|
||||
|
||||
// /**
|
||||
// * ===========================================================================
|
||||
// * Create
|
||||
// * ===========================================================================
|
||||
// */
|
||||
// it("creates a data resource that doesn't exist while not overwriting", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/sample2.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.createDataResource();
|
||||
// expect(result.type).toBe("data");
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample2.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(true);
|
||||
// });
|
||||
|
||||
// it("creates a data resource that doesn't exist while overwriting", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/sample2.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.createDataResource(true);
|
||||
// expect(result.type).toBe("data");
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample2.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(true);
|
||||
// });
|
||||
|
||||
// it("creates a data resource that does exist while not overwriting", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/sample.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.createDataResource();
|
||||
// expect(result.type).toBe("data");
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode("http://example.org/#spiderman"),
|
||||
// namedNode("http://www.perceive.net/schemas/relationship/enemyOf"),
|
||||
// namedNode("http://example.org/#green-goblin"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(true);
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(true);
|
||||
// });
|
||||
|
||||
// it("creates a data resource that does exist while overwriting", async () => {
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}test_leaf/sample.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.createDataResource(true);
|
||||
// expect(result.type).toBe("data");
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode("http://example.org/#spiderman"),
|
||||
// namedNode("http://www.perceive.net/schemas/relationship/enemyOf"),
|
||||
// namedNode("http://example.org/#green-goblin"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(false);
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}test_leaf/`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(true);
|
||||
// });
|
||||
|
||||
// /**
|
||||
// * ===========================================================================
|
||||
// * Delete
|
||||
// * ===========================================================================
|
||||
// */
|
||||
// it("deletes data", async () => {
|
||||
// solidLdoDataset.add(
|
||||
// createQuad(
|
||||
// namedNode("a"),
|
||||
// namedNode("b"),
|
||||
// namedNode("c"),
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`),
|
||||
// ),
|
||||
// );
|
||||
// solidLdoDataset.add(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/`),
|
||||
// ),
|
||||
// );
|
||||
// const leafRequester = new LeafRequester(
|
||||
// `${ROOT_COONTAINER}/test_leaf/sample.ttl`,
|
||||
// solidLdoDataset.context,
|
||||
// );
|
||||
// const result = await leafRequester.delete();
|
||||
// expect(result.type).toBe("absent");
|
||||
// expect(
|
||||
// solidLdoDataset.match(
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`),
|
||||
// ).size,
|
||||
// ).toBe(0);
|
||||
// expect(
|
||||
// solidLdoDataset.has(
|
||||
// createQuad(
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/`),
|
||||
// namedNode("http://www.w3.org/ns/ldp#contains"),
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`),
|
||||
// namedNode(`${ROOT_COONTAINER}/test_leaf/`),
|
||||
// ),
|
||||
// ),
|
||||
// ).toBe(false);
|
||||
// });
|
||||
// });
|
@ -0,0 +1,112 @@ |
||||
import type { WaitingProcess } from "../src/util/RequestBatcher"; |
||||
import { RequestBatcher } from "../src/util/RequestBatcher"; |
||||
|
||||
describe("RequestBatcher", () => { |
||||
type ReadWaitingProcess = WaitingProcess<[string], string>; |
||||
|
||||
it("Batches a request", async () => { |
||||
const requestBatcher = new RequestBatcher({ batchMillis: 500 }); |
||||
const perform = async (input: string): Promise<string> => { |
||||
await wait(100); |
||||
return `Hello ${input}`; |
||||
}; |
||||
const perform1 = jest.fn(perform); |
||||
const perform2 = jest.fn(perform); |
||||
const perform3 = jest.fn((input: string): Promise<string> => { |
||||
expect(requestBatcher.isLoading("read")).toBe(true); |
||||
return perform(input); |
||||
}); |
||||
const perform4 = jest.fn(perform); |
||||
|
||||
const modifyQueue = (queue, currentlyProcessing, input: [string]) => { |
||||
const last = queue[queue.length - 1]; |
||||
if (last?.name === "read") { |
||||
(last as ReadWaitingProcess).args[0] += input; |
||||
return last; |
||||
} |
||||
return undefined; |
||||
}; |
||||
|
||||
let return1: string = ""; |
||||
let return2: string = ""; |
||||
let return3: string = ""; |
||||
let return4: string = ""; |
||||
|
||||
expect(requestBatcher.isLoading("read")).toBe(false); |
||||
|
||||
await Promise.all([ |
||||
requestBatcher |
||||
.queueProcess<[string], string>({ |
||||
name: "read", |
||||
args: ["a"], |
||||
perform: perform1, |
||||
modifyQueue, |
||||
}) |
||||
.then((val) => (return1 = val)), |
||||
requestBatcher |
||||
.queueProcess<[string], string>({ |
||||
name: "read", |
||||
args: ["b"], |
||||
perform: perform2, |
||||
modifyQueue, |
||||
}) |
||||
.then((val) => (return2 = val)), |
||||
, |
||||
requestBatcher |
||||
.queueProcess<[string], string>({ |
||||
name: "read", |
||||
args: ["c"], |
||||
perform: perform3, |
||||
modifyQueue, |
||||
}) |
||||
.then((val) => (return3 = val)), |
||||
, |
||||
requestBatcher |
||||
.queueProcess<[string], string>({ |
||||
name: "read", |
||||
args: ["d"], |
||||
perform: perform4, |
||||
modifyQueue, |
||||
}) |
||||
.then((val) => (return4 = val)), |
||||
, |
||||
]); |
||||
|
||||
expect(return1).toBe("Hello a"); |
||||
expect(return2).toBe("Hello bcd"); |
||||
expect(return3).toBe("Hello bcd"); |
||||
expect(return4).toBe("Hello bcd"); |
||||
|
||||
expect(perform1).toHaveBeenCalledTimes(1); |
||||
expect(perform1).toHaveBeenCalledWith("a"); |
||||
expect(perform2).toHaveBeenCalledTimes(1); |
||||
expect(perform2).toHaveBeenCalledWith("bcd"); |
||||
expect(perform3).toHaveBeenCalledTimes(0); |
||||
expect(perform4).toHaveBeenCalledTimes(0); |
||||
}); |
||||
|
||||
it("sets a default batch millis", () => { |
||||
const requestBatcher = new RequestBatcher(); |
||||
expect(requestBatcher.batchMillis).toBe(1000); |
||||
}); |
||||
|
||||
it("handles an error being thrown in the process", () => { |
||||
const requestBatcher = new RequestBatcher({ batchMillis: 500 }); |
||||
const perform = async (_input: string): Promise<string> => { |
||||
throw new Error("Test Error"); |
||||
}; |
||||
const perform1 = jest.fn(perform); |
||||
expect(() => |
||||
requestBatcher.queueProcess<[string], string>({ |
||||
name: "read", |
||||
args: ["a"], |
||||
perform: perform1, |
||||
modifyQueue: () => undefined, |
||||
}), |
||||
).rejects.toThrowError("Test Error"); |
||||
}); |
||||
}); |
||||
|
||||
function wait(millis: number): Promise<void> { |
||||
return new Promise((resolve) => setTimeout(resolve, millis)); |
||||
} |
@ -0,0 +1,48 @@ |
||||
import type { WebSocket, Event, ErrorEvent } from "ws"; |
||||
import { Websocket2023NotificationSubscription } from "../src/resource/notifications/Websocket2023NotificationSubscription"; |
||||
import type { SolidLdoDatasetContext } from "../src"; |
||||
import { Leaf } from "../src"; |
||||
import type { NotificationChannel } from "@solid-notifications/types"; |
||||
|
||||
describe("Websocket2023NotificationSubscription", () => { |
||||
it("returns an error when websockets have an error", async () => { |
||||
const WebSocketMock: WebSocket = {} as WebSocket; |
||||
|
||||
const subscription = new Websocket2023NotificationSubscription( |
||||
new Leaf("https://example.com", { |
||||
fetch, |
||||
} as unknown as SolidLdoDatasetContext), |
||||
() => {}, |
||||
{} as unknown as SolidLdoDatasetContext, |
||||
() => WebSocketMock, |
||||
); |
||||
|
||||
const subPromise = subscription.subscribeToWebsocket({ |
||||
receiveFrom: "http://example.com", |
||||
} as unknown as NotificationChannel); |
||||
WebSocketMock.onopen?.({} as Event); |
||||
|
||||
await subPromise; |
||||
|
||||
WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent); |
||||
}); |
||||
|
||||
it("returns an error when websockets have an error at the beginning", async () => { |
||||
const WebSocketMock: WebSocket = {} as WebSocket; |
||||
|
||||
const subscription = new Websocket2023NotificationSubscription( |
||||
new Leaf("https://example.com", { |
||||
fetch, |
||||
} as unknown as SolidLdoDatasetContext), |
||||
() => {}, |
||||
{} as unknown as SolidLdoDatasetContext, |
||||
() => WebSocketMock, |
||||
); |
||||
|
||||
const subPromise = subscription.subscribeToWebsocket({ |
||||
receiveFrom: "http://example.com", |
||||
} as unknown as NotificationChannel); |
||||
WebSocketMock.onerror?.({ error: new Error("Test Error") } as ErrorEvent); |
||||
await subPromise; |
||||
}); |
||||
}); |
@ -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,8 @@ |
||||
import { guaranteeFetch } from "../src/util/guaranteeFetch"; |
||||
import crossFetch from "cross-fetch"; |
||||
|
||||
describe("guaranteeFetch", () => { |
||||
it("returns crossfetch when no fetch is provided", () => { |
||||
expect(guaranteeFetch()).toBe(crossFetch); |
||||
}); |
||||
}); |
@ -0,0 +1,3 @@ |
||||
import { config } from "dotenv"; |
||||
|
||||
config(); |
@ -0,0 +1,40 @@ |
||||
// 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"; |
||||
|
||||
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}`; |
||||
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,7 @@ |
||||
import { isLeafUri } from "../src"; |
||||
|
||||
describe("isLeafUri", () => { |
||||
it("returns true if the given value is a leaf URI", () => { |
||||
expect(isLeafUri("https://example.com/index.ttl")).toBe(true); |
||||
}); |
||||
}); |
@ -0,0 +1,3 @@ |
||||
export async function wait(millis: number) { |
||||
return new Promise((resolve) => setTimeout(resolve, millis)); |
||||
} |
@ -0,0 +1,66 @@ |
||||
import { |
||||
AggregateError, |
||||
ErrorResult, |
||||
ResourceError, |
||||
UnexpectedResourceError, |
||||
} from "../src/results/error/ErrorResult"; |
||||
import { InvalidUriError } from "../src/results/error/InvalidUriError"; |
||||
import { MockResouce } from "./MockResource"; |
||||
|
||||
const mockResource = new MockResouce("https://example.com/"); |
||||
|
||||
describe("ErrorResult", () => { |
||||
describe("fromThrown", () => { |
||||
it("returns an UnexpecteResourceError if a string is provided", () => { |
||||
expect( |
||||
UnexpectedResourceError.fromThrown(mockResource, "hello").message, |
||||
).toBe("hello"); |
||||
}); |
||||
|
||||
it("returns an UnexpecteResourceError if an odd valud is provided", () => { |
||||
expect(UnexpectedResourceError.fromThrown(mockResource, 5).message).toBe( |
||||
"Error of type number thrown: 5", |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe("AggregateError", () => { |
||||
it("flattens aggregate errors provided to the constructor", () => { |
||||
const err1 = UnexpectedResourceError.fromThrown(mockResource, "1"); |
||||
const err2 = UnexpectedResourceError.fromThrown(mockResource, "2"); |
||||
const err3 = UnexpectedResourceError.fromThrown(mockResource, "3"); |
||||
const err4 = UnexpectedResourceError.fromThrown(mockResource, "4"); |
||||
const aggErr1 = new AggregateError([err1, err2]); |
||||
const aggErr2 = new AggregateError([err3, err4]); |
||||
const finalAgg = new AggregateError([aggErr1, aggErr2]); |
||||
expect(finalAgg.errors.length).toBe(4); |
||||
}); |
||||
}); |
||||
|
||||
describe("default messages", () => { |
||||
class ConcreteResourceError extends ResourceError<MockResouce> { |
||||
readonly type = "concreteResourceError" as const; |
||||
} |
||||
class ConcreteErrorResult extends ErrorResult { |
||||
readonly type = "concreteErrorResult" as const; |
||||
} |
||||
|
||||
it("ResourceError fallsback to a default message if none is provided", () => { |
||||
expect(new ConcreteResourceError(mockResource).message).toBe( |
||||
"An unkown error for https://example.com/", |
||||
); |
||||
}); |
||||
|
||||
it("ErrorResult fallsback to a default message if none is provided", () => { |
||||
expect(new ConcreteErrorResult().message).toBe( |
||||
"An unkown error was encountered.", |
||||
); |
||||
}); |
||||
|
||||
it("InvalidUriError fallsback to a default message if none is provided", () => { |
||||
expect(new InvalidUriError(mockResource).message).toBe( |
||||
"https://example.com/ is an invalid uri.", |
||||
); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,65 @@ |
||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||
import EventEmitter from "events"; |
||||
import { |
||||
Unfetched, |
||||
type ConnectedResult, |
||||
type Resource, |
||||
type ResourceEventEmitter, |
||||
type ResourceResult, |
||||
} from "../src"; |
||||
|
||||
export class MockResouce |
||||
extends (EventEmitter as new () => ResourceEventEmitter) |
||||
implements Resource |
||||
{ |
||||
isError = false as const; |
||||
uri: string; |
||||
type = "mock" as const; |
||||
status: ConnectedResult; |
||||
|
||||
constructor(uri: string) { |
||||
super(); |
||||
this.uri = uri; |
||||
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<ResourceResult<any>> { |
||||
throw new Error("Method not implemented."); |
||||
} |
||||
readIfAbsent(): Promise<ResourceResult<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."); |
||||
} |
||||
} |
Loading…
Reference in new issue