/**
* @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!
}
})