Skip to content

Commit bda034a

Browse files
committed
feat: Add Tests for PREP
1 parent beaaa03 commit bda034a

File tree

3 files changed

+302
-1
lines changed

3 files changed

+302
-1
lines changed

package-lock.json

+69
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"vhost": "^3.0.2"
118118
},
119119
"devDependencies": {
120+
"@cxres/structured-headers": "^2.0.0-alpha.1-nesting.0",
120121
"@solid/solid-auth-oidc": "0.3.0",
121122
"c8": "^10.1.2",
122123
"chai": "^4.4.1",
@@ -129,6 +130,7 @@
129130
"nock": "^13.5.4",
130131
"node-mocks-http": "^1.14.1",
131132
"pre-commit": "1.2.2",
133+
"prep-fetch": "^0.1.0",
132134
"randombytes": "2.1.0",
133135
"sinon": "12.0.1",
134136
"sinon-chai": "3.7.0",
@@ -177,7 +179,9 @@
177179
"before",
178180
"beforeEach",
179181
"describe",
180-
"it"
182+
"it",
183+
"fetch",
184+
"AbortController"
181185
]
182186
},
183187
"bin": {

test/integration/prep-test.js

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const { expect } = require('chai')
4+
const { parseDictionary } = require('structured-headers')
5+
const prepFetch = require('prep-fetch').default
6+
const { createServer } = require('../utils')
7+
8+
const samplePath = path.join(__dirname, '../resources', 'sampleContainer')
9+
const sampleFile = fs.readFileSync(path.join(samplePath, 'example1.ttl'))
10+
11+
describe('Per Resource Events Protocol', function () {
12+
let server
13+
14+
before((done) => {
15+
server = createServer({
16+
live: true,
17+
dataBrowserPath: 'default',
18+
root: path.join(__dirname, '../resources'),
19+
auth: 'oidc',
20+
webid: false
21+
})
22+
server.listen(8443, done)
23+
})
24+
25+
after(() => {
26+
server.close()
27+
})
28+
29+
it('should set `Accept-Events` header on a GET response with "prep"',
30+
async function () {
31+
const response = await fetch('http://localhost:8443/sampleContainer/example1.ttl')
32+
expect(response.headers.get('Accept-Events')).to.match(/^"prep"/)
33+
expect(response.status).to.equal(200)
34+
}
35+
)
36+
37+
it('should send an ordinary response, if `Accept-Events` header is not specified',
38+
async function () {
39+
const response = await fetch('http://localhost:8443/sampleContainer/example1.ttl')
40+
expect(response.headers.get('Content-Type')).to.match(/text\/turtle/)
41+
expect(response.headers.has('Events')).to.equal(false)
42+
expect(response.status).to.equal(200)
43+
})
44+
45+
describe('with prep response on container', async function () {
46+
let response
47+
let prepResponse
48+
const controller = new AbortController()
49+
const { signal } = controller
50+
51+
it('should set headers correctly', async function () {
52+
response = await fetch('http://localhost:8443/sampleContainer/', {
53+
headers: {
54+
'Accept-Events': '"prep";accept=application/ld+json',
55+
Accept: 'text/turtle'
56+
},
57+
signal
58+
})
59+
expect(response.status).to.equal(200)
60+
expect(response.headers.get('Vary')).to.match(/Accept-Events/)
61+
const eventsHeader = parseDictionary(response.headers.get('Events'))
62+
expect(eventsHeader.get('protocol')?.[0]).to.equal('prep')
63+
expect(eventsHeader.get('status')?.[0]).to.equal(200)
64+
expect(eventsHeader.get('expires')?.[0]).to.be.a('string')
65+
expect(response.headers.get('Content-Type')).to.match(/^multipart\/mixed/)
66+
})
67+
68+
it('should send a representation as the first part, matching the content size on disk',
69+
async function () {
70+
prepResponse = prepFetch(response)
71+
const representation = await prepResponse.getRepresentation()
72+
expect(representation.headers.get('Content-Type')).to.match(/text\/turtle/)
73+
await representation.text()
74+
})
75+
76+
describe('should send notifications in the second part', async function () {
77+
let notifications
78+
let notificationsIterator
79+
80+
it('when a contained resource is created', async function () {
81+
notifications = await prepResponse.getNotifications()
82+
notificationsIterator = notifications.notifications()
83+
await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
84+
method: 'PUT',
85+
headers: {
86+
'Content-Type': 'text/turtle'
87+
},
88+
body: sampleFile
89+
})
90+
const { value } = await notificationsIterator.next()
91+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
92+
const notification = await value.json()
93+
expect(notification).to.haveOwnProperty('published')
94+
expect(notification.type).to.equal('Add')
95+
expect(notification.target).to.match(/sampleContainer\/example-prep\.ttl$/)
96+
expect(notification.object).to.match(/sampleContainer\/$/)
97+
})
98+
99+
it('when contained resource is modified', async function () {
100+
await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
101+
method: 'PATCH',
102+
headers: {
103+
'Content-Type': 'text/n3'
104+
},
105+
body: `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
106+
<> a solid:InsertDeletePatch;
107+
solid:inserts { <u> <v> <z>. }.`
108+
})
109+
const { value } = await notificationsIterator.next()
110+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
111+
const notification = await value.json()
112+
expect(notification).to.haveOwnProperty('published')
113+
expect(notification.type).to.equal('Update')
114+
expect(notification.object).to.match(/sampleContainer\/$/)
115+
})
116+
117+
it('when contained resource is deleted',
118+
async function () {
119+
await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
120+
method: 'DELETE'
121+
})
122+
const { value } = await notificationsIterator.next()
123+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
124+
const notification = await value.json()
125+
expect(notification).to.haveOwnProperty('published')
126+
expect(notification.type).to.equal('Remove')
127+
expect(notification.object).to.match(/sampleContainer\/$/)
128+
})
129+
130+
it('when resource is created by POST',
131+
async function () {
132+
await fetch('http://localhost:8443/sampleContainer/', {
133+
method: 'POST',
134+
headers: {
135+
slug: 'example-prep.ttl',
136+
'content-type': 'text/turtle'
137+
},
138+
body: sampleFile
139+
})
140+
const { value } = await notificationsIterator.next()
141+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
142+
const notification = await value.json()
143+
expect(notification).to.haveOwnProperty('published')
144+
expect(notification.type).to.equal('Update')
145+
expect(notification.object).to.match(/sampleContainer\/$/)
146+
controller.abort()
147+
})
148+
})
149+
})
150+
151+
describe('with prep response on RDF resource', async function () {
152+
let response
153+
let prepResponse
154+
155+
it('should set headers correctly', async function () {
156+
response = await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
157+
headers: {
158+
'Accept-Events': '"prep";accept=application/ld+json',
159+
Accept: 'text/n3'
160+
}
161+
})
162+
expect(response.status).to.equal(200)
163+
expect(response.headers.get('Vary')).to.match(/Accept-Events/)
164+
const eventsHeader = parseDictionary(response.headers.get('Events'))
165+
expect(eventsHeader.get('protocol')?.[0]).to.equal('prep')
166+
expect(eventsHeader.get('status')?.[0]).to.equal(200)
167+
expect(eventsHeader.get('expires')?.[0]).to.be.a('string')
168+
expect(response.headers.get('Content-Type')).to.match(/^multipart\/mixed/)
169+
})
170+
171+
it('should send a representation as the first part, matching the content size on disk',
172+
async function () {
173+
prepResponse = prepFetch(response)
174+
const representation = await prepResponse.getRepresentation()
175+
expect(representation.headers.get('Content-Type')).to.match(/text\/n3/)
176+
const blob = await representation.blob()
177+
expect(function (done) {
178+
const size = fs.statSync(path.join(__dirname,
179+
'../resources/sampleContainer/example-prep.ttl')).size
180+
if (blob.size !== size) {
181+
return done(new Error('files are not of the same size'))
182+
}
183+
})
184+
})
185+
186+
describe('should send notifications in the second part', async function () {
187+
let notifications
188+
let notificationsIterator
189+
190+
it('when modified with PATCH', async function () {
191+
notifications = await prepResponse.getNotifications()
192+
notificationsIterator = notifications.notifications()
193+
await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
194+
method: 'PATCH',
195+
headers: {
196+
'content-type': 'text/n3'
197+
},
198+
body: `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
199+
<> a solid:InsertDeletePatch;
200+
solid:inserts { <u> <v> <z>. }.`
201+
})
202+
const { value } = await notificationsIterator.next()
203+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
204+
const notification = await value.json()
205+
expect(notification).to.haveOwnProperty('published')
206+
expect(notification).to.haveOwnProperty('state')
207+
expect(notification.type).to.equal('Update')
208+
expect(notification.object).to.match(/sampleContainer\/example-prep\.ttl$/)
209+
})
210+
211+
it('when removed with DELETE, it should also close the connection',
212+
async function () {
213+
await fetch('http://localhost:8443/sampleContainer/example-prep.ttl', {
214+
method: 'DELETE'
215+
})
216+
const { value } = await notificationsIterator.next()
217+
expect(value.headers.get('content-type')).to.match(/application\/ld\+json/)
218+
const notification = await value.json()
219+
expect(notification).to.haveOwnProperty('published')
220+
expect(notification).to.haveOwnProperty('state')
221+
expect(notification.type).to.equal('Delete')
222+
expect(notification.object).to.match(/sampleContainer\/example-prep\.ttl$/)
223+
const { done } = await notificationsIterator.next()
224+
expect(done).to.equal(true)
225+
})
226+
})
227+
})
228+
})

0 commit comments

Comments
 (0)