/** * @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' declare global { interface Window { $SolidTestEnvironment?: { username: string } } } 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! } })