@opentermsarchive/engine 14.1.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/archivist/recorder/repositories/git/dataMapper.js +4 -0
- package/src/archivist/recorder/repositories/git/git.js +90 -8
- package/src/archivist/recorder/repositories/git/index.js +91 -4
- package/src/archivist/recorder/repositories/git/index.test.js +288 -0
- package/src/archivist/recorder/repositories/interface.js +34 -0
- package/src/archivist/recorder/repositories/mongo/index.js +53 -0
- package/src/archivist/recorder/repositories/mongo/index.test.js +171 -0
- package/src/collection-api/middlewares/errors.js +1 -1
- package/src/collection-api/routes/index.js +2 -1
- package/src/collection-api/routes/services.js +34 -21
- package/src/collection-api/routes/versions.js +521 -27
- package/src/collection-api/routes/versions.test.js +655 -25
|
@@ -12,57 +12,668 @@ const basePath = config.get('@opentermsarchive/engine.collection-api.basePath');
|
|
|
12
12
|
const request = supertest(app);
|
|
13
13
|
|
|
14
14
|
describe('Versions API', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
snapshotId: ['snapshot_id'],
|
|
23
|
-
};
|
|
15
|
+
let versionsRepository;
|
|
16
|
+
const FETCH_DATE = new Date('2023-01-01T12:00:00Z');
|
|
17
|
+
const VERSION_COMMON_ATTRIBUTES = {
|
|
18
|
+
serviceId: 'service-1',
|
|
19
|
+
termsType: 'Terms of Service',
|
|
20
|
+
snapshotId: ['snapshot_id'],
|
|
21
|
+
};
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
before(async () => {
|
|
24
|
+
versionsRepository = RepositoryFactory.create(config.get('@opentermsarchive/engine.recorder.versions.storage'));
|
|
25
|
+
await versionsRepository.initialize();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
after(() => versionsRepository.removeAll());
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
describe('GET /versions/:serviceId/:termsType', () => {
|
|
31
|
+
let version1;
|
|
32
|
+
let version2;
|
|
33
|
+
let version3;
|
|
29
34
|
|
|
35
|
+
before(async () => {
|
|
30
36
|
const ONE_HOUR = 60 * 60 * 1000;
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
version1 = new Version({
|
|
33
39
|
...VERSION_COMMON_ATTRIBUTES,
|
|
34
40
|
content: 'initial content',
|
|
35
41
|
fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR),
|
|
36
|
-
})
|
|
42
|
+
});
|
|
43
|
+
await versionsRepository.save(version1);
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
version2 = new Version({
|
|
39
46
|
...VERSION_COMMON_ATTRIBUTES,
|
|
40
47
|
content: 'updated content',
|
|
41
48
|
fetchDate: FETCH_DATE,
|
|
42
49
|
});
|
|
50
|
+
await versionsRepository.save(version2);
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
version3 = new Version({
|
|
53
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
54
|
+
content: 'latest content',
|
|
55
|
+
fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR),
|
|
56
|
+
});
|
|
57
|
+
await versionsRepository.save(version3);
|
|
45
58
|
|
|
46
59
|
await versionsRepository.save(new Version({
|
|
60
|
+
serviceId: 'service-2',
|
|
61
|
+
termsType: 'Privacy Policy',
|
|
62
|
+
snapshotId: ['snapshot_id'],
|
|
63
|
+
content: 'other service content',
|
|
64
|
+
fetchDate: FETCH_DATE,
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let response;
|
|
69
|
+
|
|
70
|
+
context('when versions are found', () => {
|
|
71
|
+
before(async () => {
|
|
72
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('responds with 200 status code', () => {
|
|
76
|
+
expect(response.status).to.equal(200);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('responds with Content-Type application/json', () => {
|
|
80
|
+
expect(response.type).to.equal('application/json');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns response with metadata structure', () => {
|
|
84
|
+
expect(response.body).to.have.all.keys('data', 'count', 'limit', 'offset');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns all versions for the service and terms type', () => {
|
|
88
|
+
expect(response.body.data).to.be.an('array').with.lengthOf(3);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns correct count', () => {
|
|
92
|
+
expect(response.body.count).to.equal(3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns versions with id, serviceId, termsType, fetchDate, isFirstRecord, isTechnicalUpgrade, additions and deletions', () => {
|
|
96
|
+
response.body.data.forEach(version => {
|
|
97
|
+
expect(version).to.have.all.keys('id', 'serviceId', 'termsType', 'fetchDate', 'isFirstRecord', 'isTechnicalUpgrade', 'additions', 'deletions');
|
|
98
|
+
expect(version).to.not.have.property('content');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns versions with correct serviceId and termsType', () => {
|
|
103
|
+
response.body.data.forEach(version => {
|
|
104
|
+
expect(version.serviceId).to.equal('service-1');
|
|
105
|
+
expect(version.termsType).to.equal('Terms of Service');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns versions in reverse chronological order', () => {
|
|
110
|
+
expect(response.body.data[0].id).to.equal(version3.id);
|
|
111
|
+
expect(response.body.data[1].id).to.equal(version2.id);
|
|
112
|
+
expect(response.body.data[2].id).to.equal(version1.id);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns versions with correct fetchDates', () => {
|
|
116
|
+
expect(response.body.data[0].fetchDate).to.equal(toISODateWithoutMilliseconds(version3.fetchDate));
|
|
117
|
+
expect(response.body.data[1].fetchDate).to.equal(toISODateWithoutMilliseconds(version2.fetchDate));
|
|
118
|
+
expect(response.body.data[2].fetchDate).to.equal(toISODateWithoutMilliseconds(version1.fetchDate));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
context('with pagination', () => {
|
|
123
|
+
context('with default limit (no query parameters)', () => {
|
|
124
|
+
before(async () => {
|
|
125
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('responds with 200 status code', () => {
|
|
129
|
+
expect(response.status).to.equal(200);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns all versions when total is less than default limit', () => {
|
|
133
|
+
expect(response.body.data).to.be.an('array').with.lengthOf(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('includes default pagination metadata', () => {
|
|
137
|
+
expect(response.body).to.have.property('limit', 100);
|
|
138
|
+
expect(response.body).to.have.property('offset', 0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
context('with limit parameter', () => {
|
|
143
|
+
before(async () => {
|
|
144
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?limit=2`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('responds with 200 status code', () => {
|
|
148
|
+
expect(response.status).to.equal(200);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns limited number of versions', () => {
|
|
152
|
+
expect(response.body.data).to.be.an('array').with.lengthOf(2);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns correct total count', () => {
|
|
156
|
+
expect(response.body.count).to.equal(3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('includes pagination metadata', () => {
|
|
160
|
+
expect(response.body).to.have.property('limit', 2);
|
|
161
|
+
expect(response.body).to.have.property('offset', 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns first two versions in reverse chronological order', () => {
|
|
165
|
+
expect(response.body.data[0].id).to.equal(version3.id);
|
|
166
|
+
expect(response.body.data[1].id).to.equal(version2.id);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
context('with limit and offset parameters', () => {
|
|
171
|
+
before(async () => {
|
|
172
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?limit=1&offset=1`);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('responds with 200 status code', () => {
|
|
176
|
+
expect(response.status).to.equal(200);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns limited number of versions starting from offset', () => {
|
|
180
|
+
expect(response.body.data).to.be.an('array').with.lengthOf(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns correct total count', () => {
|
|
184
|
+
expect(response.body.count).to.equal(3);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('includes pagination metadata', () => {
|
|
188
|
+
expect(response.body).to.have.property('limit', 1);
|
|
189
|
+
expect(response.body).to.have.property('offset', 1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('returns second version', () => {
|
|
193
|
+
expect(response.body.data[0].id).to.equal(version2.id);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
context('with only offset parameter', () => {
|
|
198
|
+
before(async () => {
|
|
199
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?offset=1`);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('responds with 200 status code', () => {
|
|
203
|
+
expect(response.status).to.equal(200);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('returns all versions starting from offset', () => {
|
|
207
|
+
expect(response.body.data).to.be.an('array').with.lengthOf(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns correct total count', () => {
|
|
211
|
+
expect(response.body.count).to.equal(3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('includes offset in metadata', () => {
|
|
215
|
+
expect(response.body).to.have.property('offset', 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns last two versions', () => {
|
|
219
|
+
expect(response.body.data[0].id).to.equal(version2.id);
|
|
220
|
+
expect(response.body.data[1].id).to.equal(version1.id);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
context('with invalid limit parameter (too small)', () => {
|
|
225
|
+
before(async () => {
|
|
226
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?limit=0`);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('responds with 400 status code', () => {
|
|
230
|
+
expect(response.status).to.equal(400);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns error message', () => {
|
|
234
|
+
expect(response.body).to.have.property('error');
|
|
235
|
+
expect(response.body.error).to.include('Invalid limit parameter');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
context('with invalid limit parameter (exceeds maximum)', () => {
|
|
240
|
+
before(async () => {
|
|
241
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?limit=501`);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('responds with 400 status code', () => {
|
|
245
|
+
expect(response.status).to.equal(400);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns error message', () => {
|
|
249
|
+
expect(response.body).to.have.property('error');
|
|
250
|
+
expect(response.body.error).to.include('Invalid limit parameter');
|
|
251
|
+
expect(response.body.error).to.include('500');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
context('with invalid offset parameter', () => {
|
|
256
|
+
before(async () => {
|
|
257
|
+
response = await request.get(`${basePath}/v1/versions/service-1/Terms%20of%20Service?offset=-1`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('responds with 400 status code', () => {
|
|
261
|
+
expect(response.status).to.equal(400);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns error message', () => {
|
|
265
|
+
expect(response.body).to.have.property('error');
|
|
266
|
+
expect(response.body.error).to.include('Invalid offset parameter');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
context('when no versions are found', () => {
|
|
272
|
+
before(async () => {
|
|
273
|
+
response = await request.get(`${basePath}/v1/versions/non-existent-service/Terms%20of%20Service`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('responds with 404 status code', () => {
|
|
277
|
+
expect(response.status).to.equal(404);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('responds with Content-Type application/json', () => {
|
|
281
|
+
expect(response.type).to.equal('application/json');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('returns an error message', () => {
|
|
285
|
+
expect(response.body.error).to.contain('No versions found').and.to.contain('non-existent-service').and.to.contain('Terms of Service');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
context('when the service ID is a path traversal attempt', () => {
|
|
290
|
+
before(async () => {
|
|
291
|
+
response = await request.get(`${basePath}/v1/versions/..%2F..%2Foutside/Terms%20of%20Service`);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('responds with 404 status code', () => {
|
|
295
|
+
expect(response.status).to.equal(404);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('does not expose the repository location', () => {
|
|
299
|
+
expect(response.body.error).to.contain('No versions found').and.to.not.contain('outside repository');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('GET /versions/:serviceId', () => {
|
|
305
|
+
let response;
|
|
306
|
+
|
|
307
|
+
context('when the service ID is a path traversal attempt', () => {
|
|
308
|
+
before(async () => {
|
|
309
|
+
response = await request.get(`${basePath}/v1/versions/..%2F..%2Foutside`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('responds with 404 status code', () => {
|
|
313
|
+
expect(response.status).to.equal(404);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('does not expose the repository location', () => {
|
|
317
|
+
expect(response.body.error).to.contain('No versions found').and.to.not.contain('outside repository');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('GET /version/:versionId', () => {
|
|
323
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
324
|
+
let firstVersion;
|
|
325
|
+
let middleVersion;
|
|
326
|
+
let lastVersion;
|
|
327
|
+
|
|
328
|
+
before(async () => {
|
|
329
|
+
await versionsRepository.removeAll();
|
|
330
|
+
|
|
331
|
+
firstVersion = new Version({
|
|
332
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
333
|
+
content: 'first content',
|
|
334
|
+
fetchDate: new Date(FETCH_DATE.getTime() - ONE_HOUR),
|
|
335
|
+
});
|
|
336
|
+
await versionsRepository.save(firstVersion);
|
|
337
|
+
|
|
338
|
+
middleVersion = new Version({
|
|
339
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
340
|
+
content: 'middle content',
|
|
341
|
+
fetchDate: FETCH_DATE,
|
|
342
|
+
});
|
|
343
|
+
await versionsRepository.save(middleVersion);
|
|
344
|
+
|
|
345
|
+
lastVersion = new Version({
|
|
346
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
347
|
+
content: 'last content',
|
|
348
|
+
fetchDate: new Date(FETCH_DATE.getTime() + ONE_HOUR),
|
|
349
|
+
});
|
|
350
|
+
await versionsRepository.save(lastVersion);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
let response;
|
|
354
|
+
|
|
355
|
+
context('when requesting the first version', () => {
|
|
356
|
+
before(async () => {
|
|
357
|
+
response = await request.get(`${basePath}/v1/version/${firstVersion.id}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('responds with 200 status code', () => {
|
|
361
|
+
expect(response.status).to.equal(200);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('responds with Content-Type application/json', () => {
|
|
365
|
+
expect(response.type).to.equal('application/json');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('returns the version content and metadata', () => {
|
|
369
|
+
expect(response.body.id).to.equal(firstVersion.id);
|
|
370
|
+
expect(response.body.serviceId).to.equal(firstVersion.serviceId);
|
|
371
|
+
expect(response.body.termsType).to.equal(firstVersion.termsType);
|
|
372
|
+
expect(response.body.fetchDate).to.equal(toISODateWithoutMilliseconds(firstVersion.fetchDate));
|
|
373
|
+
expect(response.body.content).to.equal(firstVersion.content);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('returns fetchUrls as an empty array when no matching snapshots exist', () => {
|
|
377
|
+
expect(response.body.fetchUrls).to.deep.equal([]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('returns links object', () => {
|
|
381
|
+
expect(response.body.links).to.be.an('object');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('returns first link pointing to itself', () => {
|
|
385
|
+
expect(response.body.links.first).to.equal(firstVersion.id);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('returns null for prev', () => {
|
|
389
|
+
expect(response.body.links.prev).to.be.null;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('returns next link', () => {
|
|
393
|
+
expect(response.body.links.next).to.equal(middleVersion.id);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('returns last link', () => {
|
|
397
|
+
expect(response.body.links.last).to.equal(lastVersion.id);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
context('when requesting a middle version', () => {
|
|
402
|
+
before(async () => {
|
|
403
|
+
response = await request.get(`${basePath}/v1/version/${middleVersion.id}`);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('responds with 200 status code', () => {
|
|
407
|
+
expect(response.status).to.equal(200);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('returns the version content and metadata', () => {
|
|
411
|
+
expect(response.body.id).to.equal(middleVersion.id);
|
|
412
|
+
expect(response.body.serviceId).to.equal(middleVersion.serviceId);
|
|
413
|
+
expect(response.body.termsType).to.equal(middleVersion.termsType);
|
|
414
|
+
expect(response.body.content).to.equal(middleVersion.content);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('returns first link', () => {
|
|
418
|
+
expect(response.body.links.first).to.equal(firstVersion.id);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('returns prev link', () => {
|
|
422
|
+
expect(response.body.links.prev).to.equal(firstVersion.id);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('returns next link', () => {
|
|
426
|
+
expect(response.body.links.next).to.equal(lastVersion.id);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('returns last link', () => {
|
|
430
|
+
expect(response.body.links.last).to.equal(lastVersion.id);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
context('when requesting the last version', () => {
|
|
435
|
+
before(async () => {
|
|
436
|
+
response = await request.get(`${basePath}/v1/version/${lastVersion.id}`);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('responds with 200 status code', () => {
|
|
440
|
+
expect(response.status).to.equal(200);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('returns the version content and metadata', () => {
|
|
444
|
+
expect(response.body.id).to.equal(lastVersion.id);
|
|
445
|
+
expect(response.body.serviceId).to.equal(lastVersion.serviceId);
|
|
446
|
+
expect(response.body.termsType).to.equal(lastVersion.termsType);
|
|
447
|
+
expect(response.body.content).to.equal(lastVersion.content);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('returns first link', () => {
|
|
451
|
+
expect(response.body.links.first).to.equal(firstVersion.id);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('returns prev link', () => {
|
|
455
|
+
expect(response.body.links.prev).to.equal(middleVersion.id);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns null for next', () => {
|
|
459
|
+
expect(response.body.links.next).to.be.null;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('returns last link pointing to itself', () => {
|
|
463
|
+
expect(response.body.links.last).to.equal(lastVersion.id);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
context('when the version does not exist', () => {
|
|
468
|
+
before(async () => {
|
|
469
|
+
response = await request.get(`${basePath}/v1/version/non-existent-id`);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('responds with 404 status code', () => {
|
|
473
|
+
expect(response.status).to.equal(404);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('responds with Content-Type application/json', () => {
|
|
477
|
+
expect(response.type).to.equal('application/json');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('returns an error message', () => {
|
|
481
|
+
expect(response.body.error).to.contain('No version found').and.to.contain('non-existent-id');
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
context('when a referenced snapshot is absent from the repository', () => {
|
|
486
|
+
let versionWithMissingSnapshot;
|
|
487
|
+
|
|
488
|
+
before(async () => {
|
|
489
|
+
versionWithMissingSnapshot = new Version({
|
|
490
|
+
serviceId: 'service-missing-snapshot',
|
|
491
|
+
termsType: 'Terms of Service',
|
|
492
|
+
content: 'content referencing a snapshot that is missing from the repository',
|
|
493
|
+
fetchDate: FETCH_DATE,
|
|
494
|
+
snapshotIds: ['ecd9407eb26b1bf0613186175ee80edbdeedd47f'], // well-formed ID that is absent from the snapshots repository
|
|
495
|
+
});
|
|
496
|
+
await versionsRepository.save(versionWithMissingSnapshot);
|
|
497
|
+
response = await request.get(`${basePath}/v1/version/${versionWithMissingSnapshot.id}`);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('responds with 200 rather than failing on the unresolved snapshot', () => {
|
|
501
|
+
expect(response.status).to.equal(200);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('degrades fetchUrls to an empty array', () => {
|
|
505
|
+
expect(response.body.fetchUrls).to.deep.equal([]);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('GET /version/:serviceId/:termsType/latest', () => {
|
|
511
|
+
let firstVersion;
|
|
512
|
+
let middleVersion;
|
|
513
|
+
let lastVersion;
|
|
514
|
+
|
|
515
|
+
before(async () => {
|
|
516
|
+
await versionsRepository.removeAll();
|
|
517
|
+
|
|
518
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
519
|
+
|
|
520
|
+
firstVersion = await versionsRepository.save(new Version({
|
|
521
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
522
|
+
content: 'first content',
|
|
523
|
+
fetchDate: new Date(FETCH_DATE.getTime() - ONE_HOUR),
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
middleVersion = await versionsRepository.save(new Version({
|
|
527
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
528
|
+
content: 'middle content',
|
|
529
|
+
fetchDate: FETCH_DATE,
|
|
530
|
+
}));
|
|
531
|
+
|
|
532
|
+
lastVersion = await versionsRepository.save(new Version({
|
|
533
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
534
|
+
content: 'last content',
|
|
535
|
+
fetchDate: new Date(FETCH_DATE.getTime() + ONE_HOUR),
|
|
536
|
+
}));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
let response;
|
|
540
|
+
|
|
541
|
+
context('when versions exist', () => {
|
|
542
|
+
before(async () => {
|
|
543
|
+
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/latest`);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('responds with 200 status code', () => {
|
|
547
|
+
expect(response.status).to.equal(200);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('responds with Content-Type application/json', () => {
|
|
551
|
+
expect(response.type).to.equal('application/json');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('returns the latest version', () => {
|
|
555
|
+
expect(response.body.id).to.equal(lastVersion.id);
|
|
556
|
+
expect(response.body.serviceId).to.equal(lastVersion.serviceId);
|
|
557
|
+
expect(response.body.termsType).to.equal(lastVersion.termsType);
|
|
558
|
+
expect(response.body.content).to.equal(lastVersion.content);
|
|
559
|
+
expect(response.body.fetchDate).to.equal(toISODateWithoutMilliseconds(lastVersion.fetchDate));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('returns fetchUrls as an empty array when no matching snapshots exist', () => {
|
|
563
|
+
expect(response.body.fetchUrls).to.deep.equal([]);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('returns diff stats (additions and deletions)', () => {
|
|
567
|
+
expect(response.body).to.have.property('additions');
|
|
568
|
+
expect(response.body).to.have.property('deletions');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('returns links object', () => {
|
|
572
|
+
expect(response.body.links).to.be.an('object');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('returns first link', () => {
|
|
576
|
+
expect(response.body.links.first).to.equal(firstVersion.id);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('returns prev link', () => {
|
|
580
|
+
expect(response.body.links.prev).to.equal(middleVersion.id);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('returns null for next', () => {
|
|
584
|
+
expect(response.body.links.next).to.be.null;
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('returns last link pointing to itself', () => {
|
|
588
|
+
expect(response.body.links.last).to.equal(lastVersion.id);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
context('when no versions exist', () => {
|
|
593
|
+
before(async () => {
|
|
594
|
+
response = await request.get(`${basePath}/v1/version/non-existent-service/Non%20Existent%20Terms/latest`);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('responds with 404 status code', () => {
|
|
598
|
+
expect(response.status).to.equal(404);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('responds with Content-Type application/json', () => {
|
|
602
|
+
expect(response.type).to.equal('application/json');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('returns an error message', () => {
|
|
606
|
+
expect(response.body.error).to.contain('No version found').and.to.contain('non-existent-service');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
context('when the service ID is a path traversal attempt', () => {
|
|
611
|
+
before(async () => {
|
|
612
|
+
response = await request.get(`${basePath}/v1/version/..%2F..%2Foutside/Terms%20of%20Service/latest`);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('responds with 404 status code', () => {
|
|
616
|
+
expect(response.status).to.equal(404);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('does not expose the repository location', () => {
|
|
620
|
+
expect(response.body.error).to.contain('No version found').and.to.not.contain('outside repository');
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe('GET /version/:serviceId/:termsType/:date', () => {
|
|
626
|
+
let expectedResult;
|
|
627
|
+
let firstVersion;
|
|
628
|
+
let middleVersion;
|
|
629
|
+
let lastVersion;
|
|
630
|
+
|
|
631
|
+
before(async () => {
|
|
632
|
+
await versionsRepository.removeAll();
|
|
633
|
+
|
|
634
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
635
|
+
|
|
636
|
+
firstVersion = await versionsRepository.save(new Version({
|
|
637
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
638
|
+
content: 'initial content',
|
|
639
|
+
fetchDate: new Date(new Date(FETCH_DATE).getTime() - ONE_HOUR),
|
|
640
|
+
}));
|
|
641
|
+
|
|
642
|
+
middleVersion = await versionsRepository.save(new Version({
|
|
643
|
+
...VERSION_COMMON_ATTRIBUTES,
|
|
644
|
+
content: 'updated content',
|
|
645
|
+
fetchDate: FETCH_DATE,
|
|
646
|
+
}));
|
|
647
|
+
|
|
648
|
+
lastVersion = await versionsRepository.save(new Version({
|
|
47
649
|
...VERSION_COMMON_ATTRIBUTES,
|
|
48
650
|
content: 'latest content',
|
|
49
651
|
fetchDate: new Date(new Date(FETCH_DATE).getTime() + ONE_HOUR),
|
|
50
652
|
}));
|
|
51
653
|
|
|
52
654
|
expectedResult = {
|
|
53
|
-
id:
|
|
54
|
-
|
|
55
|
-
|
|
655
|
+
id: middleVersion.id,
|
|
656
|
+
serviceId: middleVersion.serviceId,
|
|
657
|
+
termsType: middleVersion.termsType,
|
|
658
|
+
fetchDate: toISODateWithoutMilliseconds(middleVersion.fetchDate),
|
|
659
|
+
content: middleVersion.content,
|
|
660
|
+
isFirstRecord: false,
|
|
661
|
+
isTechnicalUpgrade: false,
|
|
662
|
+
fetchUrls: [],
|
|
663
|
+
links: {
|
|
664
|
+
first: firstVersion.id,
|
|
665
|
+
prev: firstVersion.id,
|
|
666
|
+
next: lastVersion.id,
|
|
667
|
+
last: lastVersion.id,
|
|
668
|
+
},
|
|
56
669
|
};
|
|
57
670
|
});
|
|
58
671
|
|
|
59
|
-
after(() => versionsRepository.removeAll());
|
|
60
|
-
|
|
61
672
|
let response;
|
|
62
673
|
|
|
63
674
|
context('when a version is found', () => {
|
|
64
675
|
before(async () => {
|
|
65
|
-
response = await request.get(`${basePath}/v1/version/service
|
|
676
|
+
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(FETCH_DATE))}`);
|
|
66
677
|
});
|
|
67
678
|
|
|
68
679
|
it('responds with 200 status code', () => {
|
|
@@ -74,13 +685,18 @@ describe('Versions API', () => {
|
|
|
74
685
|
});
|
|
75
686
|
|
|
76
687
|
it('returns the expected version', () => {
|
|
77
|
-
expect(response.body).to.deep.
|
|
688
|
+
expect(response.body).to.deep.include(expectedResult);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('returns diff stats (additions and deletions)', () => {
|
|
692
|
+
expect(response.body).to.have.property('additions');
|
|
693
|
+
expect(response.body).to.have.property('deletions');
|
|
78
694
|
});
|
|
79
695
|
});
|
|
80
696
|
|
|
81
697
|
context('when the requested date is anterior to the first available version', () => {
|
|
82
698
|
before(async () => {
|
|
83
|
-
response = await request.get(`${basePath}/v1/version/service
|
|
699
|
+
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/2000-01-01T12:00:00Z`);
|
|
84
700
|
});
|
|
85
701
|
|
|
86
702
|
it('responds with 404 status code', () => {
|
|
@@ -100,7 +716,7 @@ describe('Versions API', () => {
|
|
|
100
716
|
before(async () => {
|
|
101
717
|
const dateInTheFuture = new Date(Date.now() + 60000); // 1 minute in the future
|
|
102
718
|
|
|
103
|
-
response = await request.get(`${basePath}/v1/version/service
|
|
719
|
+
response = await request.get(`${basePath}/v1/version/service-1/Terms%20of%20Service/${encodeURIComponent(toISODateWithoutMilliseconds(dateInTheFuture))}`);
|
|
104
720
|
});
|
|
105
721
|
|
|
106
722
|
it('responds with 416 status code', () => {
|
|
@@ -115,5 +731,19 @@ describe('Versions API', () => {
|
|
|
115
731
|
expect(response.body.error).to.equal('Requested version is in the future');
|
|
116
732
|
});
|
|
117
733
|
});
|
|
734
|
+
|
|
735
|
+
context('when the service ID is a path traversal attempt', () => {
|
|
736
|
+
before(async () => {
|
|
737
|
+
response = await request.get(`${basePath}/v1/version/..%2F..%2Foutside/Terms%20of%20Service/2023-01-01T12:00:00Z`);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('responds with 404 status code', () => {
|
|
741
|
+
expect(response.status).to.equal(404);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('does not expose the repository location', () => {
|
|
745
|
+
expect(response.body.error).to.contain('No version found').and.to.not.contain('outside repository');
|
|
746
|
+
});
|
|
747
|
+
});
|
|
118
748
|
});
|
|
119
749
|
});
|