/** * @jest-environment jsdom * */ import { UpdateManager, Store, Fetcher } from "rdflib"; import { createAclLogic } from "../src/acl/aclLogic"; import { createChatLogic } from '../src/chat/chatLogic'; import { createProfileLogic } from "../src/profile/profileLogic"; import { createContainerLogic } from "../src/util/containerLogic"; import { createUtilityLogic } from "../src/util/utilityLogic"; import { alice, bob } from "./helpers/dataSetup"; window.$SolidTestEnvironment = { username: alice.uri } describe("Chat logic", () => { let chatLogic; let store; beforeEach(() => { fetchMock.resetMocks(); fetchMock.mockResponse("Not Found", { status: 404, }); store = new Store() store.fetcher = new Fetcher(store, { fetch: fetch }); store.updater = new UpdateManager(store); const authn = { currentUser: () => { return alice; }, }; const util = createUtilityLogic(store, createAclLogic(store), createContainerLogic(store)) chatLogic = createChatLogic(store, createProfileLogic(store, authn, util)) }); describe("get chat, without creating", () => { describe("when no chat exists yet", () => { let result; beforeEach(async () => { aliceHasValidProfile(); noChatWithBobExists(); result = await chatLogic.getChat(bob, false); }); it("does not return a chat", async () => { expect(result).toBeNull(); }); it("loaded the current user profile", () => { expect(fetchMock.mock.calls[0][0]).toBe( "https://alice.example.com/profile/card.ttl" ); }); it("tried to load the chat document", () => { expect(fetchMock.mock.calls[1][0]).toBe( "https://alice.example.com/IndividualChats/bob.example.com/index.ttl" ); }); it("has no additional fetch requests", () => { expect(fetchMock.mock.calls.length).toBe(2); }); }); }); describe("get chat, create if missing", () => { describe("when no chat exists yet", () => { let result; beforeEach(async () => { Date.now = jest.fn(() => new Date(Date.UTC(2021, 1, 6, 10, 11, 12)).valueOf() ); aliceHasValidProfile(); noChatWithBobExists(); chatWithBobCanBeCreated(); bobHasAnInbox(); invitationCanBeSent(); chatContainerIsFound(); chatContainerAclCanBeSet(); editablePrivateTypeIndexIsFound(); privateTypeIndexIsUpdated(); result = await chatLogic.getChat(bob, true); }); it("returns the chat URI based on the invitee's WebID", () => { expect(result.uri).toBe( "https://alice.example.com/IndividualChats/bob.example.com/index.ttl#this" ); }); it("created a chat document", () => { const request = getRequestTo( "PUT", "https://alice.example.com/IndividualChats/bob.example.com/index.ttl" ); expect(request.body).toBe(`@prefix : <#>. @prefix dc: . @prefix meeting: . @prefix xsd: . @prefix c: . :this a meeting:LongChat; dc:author c:me; dc:created "2021-02-06T10:11:12Z"^^xsd:dateTime; dc:title "Chat channel". `); }); it("allowed Bob to participate in the chat by adding an ACL", () => { const request = getRequestTo( "PUT", "https://alice.example.com/IndividualChats/bob.example.com/.acl" ); expect(request.body).toBe(` @prefix acl: . <#owner> a acl:Authorization; acl:agent ; acl:accessTo <.>; acl:default <.>; acl:mode acl:Read, acl:Write, acl:Control. <#invitee> a acl:Authorization; acl:agent ; acl:accessTo <.>; acl:default <.>; acl:mode acl:Read, acl:Append. `); }); it("sent an invitation to invitee inbox", () => { const request = getRequestTo("POST", "https://bob.example.com/inbox"); expect(request.body).toContain(` <> a ; . `); }); it("added the new chat to private type index", () => { const request = getRequestTo( "PATCH", "https://alice.example.com/settings/privateTypeIndex.ttl" ); expect(request.body) .toBe(`INSERT DATA { . . . } `); }); it("has no additional fetch requests", () => { expect(fetchMock.mock.calls.length).toBe(9); }); }); }); describe("possible errors", () => { it("profile does not link to storage", async () => { fetchMock.mockOnceIf("https://alice.example.com/profile/card.ttl", "<><><>.", { headers: { "Content-Type": "text/turtle", }, }); const expectedError = new Error("User pod root not found!"); await expect(chatLogic.getChat(bob, false)).rejects.toEqual(expectedError); }); it("invitee inbox not found", async () => { aliceHasValidProfile(); noChatWithBobExists(); chatWithBobCanBeCreated(); bobDoesNotHaveAnInbox(); const expectedError = new Error( "Invitee inbox not found! https://bob.example.com/profile/card.ttl#me" ); await expect(chatLogic.getChat(bob, true)).rejects.toEqual(expectedError); }); }); function aliceHasValidProfile() { fetchMock.mockOnceIf( "https://alice.example.com/profile/card.ttl", ` ; ; .`, { headers: { "Content-Type": "text/turtle", }, } ); } function noChatWithBobExists() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/IndividualChats/bob.example.com/index.ttl" && method === "GET", "Not found", { status: 404, } ); } function chatWithBobCanBeCreated() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/IndividualChats/bob.example.com/index.ttl" && method === "PUT", "Created", { status: 201, } ); } function bobHasAnInbox() { fetchMock.mockOnceIf( "https://bob.example.com/profile/card.ttl", ".", { headers: { "Content-Type": "text/turtle" }, } ); } function bobDoesNotHaveAnInbox() { fetchMock.mockOnceIf("https://bob.example.com/profile/card.ttl", "<><><>.", { headers: { "Content-Type": "text/turtle", }, }); } function invitationCanBeSent() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://bob.example.com/inbox" && method === "POST", "Created", { status: 201, headers: { location: "https://bob.example.com/inbox/22373339-6cc0-49fc-b69e-0402edda6e4e.ttl", }, } ); } function chatContainerIsFound() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/IndividualChats/bob.example.com/" && method === "GET", "<><><>.", { status: 200, headers: { "Content-Type": "text/turtle", Link: '<.acl>; rel="acl"', }, } ); } function chatContainerAclCanBeSet() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/IndividualChats/bob.example.com/.acl" && method === "PUT", "Created", { status: 201, } ); } function editablePrivateTypeIndexIsFound() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/settings/privateTypeIndex.ttl" && method === "GET", "<><><>.", { status: 200, headers: { "Content-Type": "text/turtle", "wac-allow": 'user="read write append control",public=""', "ms-author-via": "SPARQL", }, } ); } function privateTypeIndexIsUpdated() { return fetchMock.mockOnceIf( ({ url, method }) => url === "https://alice.example.com/settings/privateTypeIndex.ttl" && method === "PATCH", "OK", { status: 200, } ); } function getRequestTo( method: "GET" | "PUT" | "POST" | "DELETE" | "PATCH", url: string ): RequestInit { const call = fetchMock.mock.calls.find( (it) => it[0] === url && method === it[1]?.method ); expect(call).not.toBeNull(); const request = call?.[1]; expect(request).not.toBeNull(); return request!; } });