@opentermsarchive/engine 0.28.0 → 0.29.1

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.
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env node
2
+ import './env.js';
3
+
4
+ import path from 'path';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ await import(pathToFileURL(path.resolve(__dirname, '../src/api/server.js'))); // load asynchronously to ensure env.js is loaded before
package/bin/ota.js CHANGED
@@ -14,4 +14,5 @@ program
14
14
  .command('validate', 'Run a series of tests to check the validity of terms declarations')
15
15
  .command('lint', 'Check format and stylistic errors in declarations and auto fix them')
16
16
  .command('dataset', 'Export the versions dataset into a ZIP file and optionally publish it to GitHub releases')
17
+ .command('serve', 'Start the collection metadata API server')
17
18
  .parse(process.argv);
@@ -67,5 +67,9 @@
67
67
  "dataset": {
68
68
  "title": "sandbox",
69
69
  "versionsRepositoryURL": "https://github.com/OpenTermsArchive/sandbox"
70
+ },
71
+ "api": {
72
+ "port": 3000,
73
+ "basePath": "/api"
70
74
  }
71
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentermsarchive/engine",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "Tracks and makes visible changes to the terms of online services",
5
5
  "homepage": "https://github.com/OpenTermsArchive/engine#readme",
6
6
  "bugs": {
@@ -39,6 +39,7 @@
39
39
  "lint": "eslint src test scripts bin",
40
40
  "lint:fix": "npm run lint -- --fix",
41
41
  "start": "node --max-http-header-size=32768 bin/ota.js track",
42
+ "start:api": "node bin/ota.js serve",
42
43
  "start:scheduler": "npm start -- --schedule",
43
44
  "test": "cross-env NODE_ENV=test mocha --recursive \"./src/**/*.test.js\" \"./scripts/**/*.test.js\" --exit",
44
45
  "posttest": "npm run lint",
@@ -68,7 +69,9 @@
68
69
  "eslint-plugin-chai-friendly": "^0.7.2",
69
70
  "eslint-plugin-import": "^2.25.3",
70
71
  "eslint-plugin-json-format": "^2.0.1",
72
+ "express": "^4.18.2",
71
73
  "fs-extra": "^10.0.0",
74
+ "helmet": "^6.0.1",
72
75
  "http-proxy-agent": "^5.0.0",
73
76
  "https": "^1.0.0",
74
77
  "https-proxy-agent": "^5.0.0",
@@ -80,6 +83,7 @@
80
83
  "mime": "^2.5.2",
81
84
  "mocha": "^9.1.3",
82
85
  "mongodb": "^4.9.0",
86
+ "morgan": "^1.10.0",
83
87
  "node-fetch": "^3.1.0",
84
88
  "octokit": "^1.7.0",
85
89
  "pdfjs-dist": "^2.9.359",
@@ -88,6 +92,7 @@
88
92
  "puppeteer-extra-plugin-stealth": "^2.9.0",
89
93
  "sib-api-v3-sdk": "^8.2.1",
90
94
  "simple-git": "^3.8.0",
95
+ "swagger-jsdoc": "^6.2.8",
91
96
  "winston": "^3.3.3",
92
97
  "winston-mail": "^2.0.0"
93
98
  },
@@ -97,7 +102,8 @@
97
102
  "node-stream-zip": "^1.15.0",
98
103
  "prettier": "^2.2.1",
99
104
  "sinon": "^12.0.1",
100
- "sinon-chai": "^3.7.0"
105
+ "sinon-chai": "^3.7.0",
106
+ "supertest": "^6.3.3"
101
107
  },
102
108
  "peerDependencies": {
103
109
  "@opentermsarchive/terms-types": "~0.1.1"
@@ -90,7 +90,7 @@ export default async options => {
90
90
  })
91
91
  .forEach(type => {
92
92
  describe(type, () => {
93
- const terms = service.getTerms(type);
93
+ const terms = service.getTerms({ type });
94
94
 
95
95
  terms.sourceDocuments.forEach(sourceDocument => {
96
96
  let filteredContent;
@@ -95,10 +95,10 @@ let recorder;
95
95
  continue;
96
96
  }
97
97
 
98
- const terms = servicesDeclarations[serviceId].getTerms(
99
- termsType,
100
- commit.date,
101
- );
98
+ const terms = servicesDeclarations[serviceId].getTerms({
99
+ type: termsType,
100
+ date: commit.date,
101
+ });
102
102
 
103
103
  if (!terms) {
104
104
  console.log(`⌙ Skip unknown terms type "${termsType}" for service "${serviceId}"`);
@@ -0,0 +1,40 @@
1
+ import os from 'os';
2
+
3
+ import config from 'config';
4
+ import dotenv from 'dotenv';
5
+ import winston from 'winston';
6
+ import 'winston-mail';
7
+
8
+ dotenv.config();
9
+
10
+ const { combine, timestamp, printf, colorize } = winston.format;
11
+
12
+ const transports = [new winston.transports.Console()];
13
+
14
+ if (config.get('logger.sendMailOnError')) {
15
+ transports.push(new winston.transports.Mail({
16
+ to: config.get('logger.sendMailOnError.to'),
17
+ from: config.get('logger.sendMailOnError.from'),
18
+ host: config.get('logger.smtp.host'),
19
+ username: config.get('logger.smtp.username'),
20
+ password: process.env.SMTP_PASSWORD,
21
+ ssl: true,
22
+ timeout: 30 * 1000,
23
+ formatter: args => args[Object.getOwnPropertySymbols(args)[1]], // Returns the full error message, the same visible in the console. It is referenced in the argument object with a Symbol of which we do not have the reference but we know it is the second one.
24
+ exitOnError: true,
25
+ level: 'error',
26
+ subject: `[OTA API] Error Report — ${os.hostname()}`,
27
+ }));
28
+ }
29
+
30
+ const logger = winston.createLogger({
31
+ format: combine(
32
+ colorize(),
33
+ timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
34
+ printf(({ level, message, timestamp }) => `${timestamp} ${level.padEnd(15)} ${message}`),
35
+ ),
36
+ transports,
37
+ rejectionHandlers: transports,
38
+ });
39
+
40
+ export default logger;
@@ -0,0 +1,6 @@
1
+ import logger from '../logger.js';
2
+
3
+ export default function errorsMiddleware(err, req, res, next) {
4
+ logger.error(err.stack);
5
+ res.status(500).send('Something went wrong!');
6
+ }
@@ -0,0 +1,7 @@
1
+ import morgan from 'morgan';
2
+
3
+ import logger from '../logger.js';
4
+
5
+ const middleware = morgan('tiny', { stream: { write: message => logger.info(message.trim()) } });
6
+
7
+ export default middleware;
@@ -0,0 +1,11 @@
1
+ import express from 'express';
2
+
3
+ import servicesRouter from './services.js';
4
+ import specsRouter from './specs.js';
5
+
6
+ const apiRouter = express.Router();
7
+
8
+ apiRouter.use('/specs', specsRouter);
9
+ apiRouter.use('/services', servicesRouter);
10
+
11
+ export default apiRouter;
@@ -0,0 +1,142 @@
1
+ import express from 'express';
2
+
3
+ import * as Services from '../../archivist/services/index.js';
4
+
5
+ const services = await Services.load();
6
+
7
+ /**
8
+ * @swagger
9
+ * tags:
10
+ * name: Services
11
+ * description: Services API
12
+ * components:
13
+ * schemas:
14
+ * Service:
15
+ * type: object
16
+ * description: Definition of a service and the agreements its provider sets forth. While the information is the same, the format differs from the JSON declaration files that are designed for readability by contributors.
17
+ * properties:
18
+ * id:
19
+ * type: string
20
+ * description: The ID of the service.
21
+ * name:
22
+ * type: string
23
+ * description: The name of the service.
24
+ * terms:
25
+ * type: array
26
+ * description: Information that enables tracking the content of agreements defined by the service provider.
27
+ * items:
28
+ * type: object
29
+ * properties:
30
+ * type:
31
+ * type: string
32
+ * description: The type of terms.
33
+ * sourceDocuments:
34
+ * type: array
35
+ * items:
36
+ * type: object
37
+ * properties:
38
+ * location:
39
+ * type: string
40
+ * format: uri
41
+ * description: The URL of the source document.
42
+ * executeClientScripts:
43
+ * type: boolean
44
+ * description: Whether client-side scripts should be executed.
45
+ * contentSelectors:
46
+ * type: string
47
+ * description: The CSS selectors for selecting significant content.
48
+ * insignificantContentSelectors:
49
+ * type: string
50
+ * description: The CSS selectors for selecting insignificant content.
51
+ * filters:
52
+ * type: array
53
+ * description: The names of filters to apply to the content.
54
+ * items:
55
+ * type: string
56
+ */
57
+ const router = express.Router();
58
+
59
+ /**
60
+ * @swagger
61
+ * /services:
62
+ * get:
63
+ * summary: Enumerate all services.
64
+ * tags: [Services]
65
+ * produces:
66
+ * - application/json
67
+ * responses:
68
+ * 200:
69
+ * description: A JSON array of all services.
70
+ * content:
71
+ * application/json:
72
+ * schema:
73
+ * type: array
74
+ * items:
75
+ * $ref: '#/components/schemas/Service'
76
+ */
77
+ router.get('/', (req, res) => {
78
+ res.status(200).json(Object.values(services).map(service => ({
79
+ id: service.id,
80
+ name: service.name,
81
+ })));
82
+ });
83
+
84
+ /**
85
+ * @swagger
86
+ * /service/{serviceId}:
87
+ * get:
88
+ * summary: Retrieve the declaration of a specific service through its ID.
89
+ * tags: [Services]
90
+ * produces:
91
+ * - application/json
92
+ * parameters:
93
+ * - in: path
94
+ * name: serviceId
95
+ * description: The ID of the service.
96
+ * schema:
97
+ * type: string
98
+ * required: true
99
+ * examples:
100
+ * service-1:
101
+ * value: service-1
102
+ * summary: Simple service ID
103
+ * service-2:
104
+ * value: Service 2!
105
+ * summary: Service ID with spaces and special characters
106
+ * responses:
107
+ * 200:
108
+ * description: The full JSON declaration of the service with the given ID.
109
+ * content:
110
+ * application/json:
111
+ * schema:
112
+ * $ref: '#/components/schemas/Service'
113
+ * 404:
114
+ * description: No service matching the provided ID is found.
115
+ */
116
+ router.get('/:serviceId', (req, res) => {
117
+ const matchedServiceID = Object.keys(services).find(key => key.toLowerCase() === req.params.serviceId?.toLowerCase());
118
+ const service = services[matchedServiceID];
119
+
120
+ if (!service) {
121
+ res.status(404).send('Service not found');
122
+
123
+ return;
124
+ }
125
+
126
+ res.status(200).json({
127
+ id: service.id,
128
+ name: service.name,
129
+ terms: service.getTerms().map(terms => ({
130
+ type: terms.type,
131
+ sourceDocuments: terms.sourceDocuments.map(({ location, contentSelectors, insignificantContentSelectors, filters, executeClientScripts }) => ({
132
+ location,
133
+ contentSelectors,
134
+ insignificantContentSelectors,
135
+ executeClientScripts,
136
+ filters: filters?.map(filter => filter.name),
137
+ })),
138
+ })),
139
+ });
140
+ });
141
+
142
+ export default router;
@@ -0,0 +1,151 @@
1
+ import { expect } from 'chai';
2
+ import config from 'config';
3
+ import request from 'supertest';
4
+
5
+ import app from '../server.js';
6
+
7
+ const basePath = config.get('api.basePath');
8
+
9
+ describe('Services API', () => {
10
+ describe('GET /services', () => {
11
+ let response;
12
+
13
+ before(async () => {
14
+ response = await request(app).get(`${basePath}/v1/services`);
15
+ });
16
+
17
+ it('responds with 200 status code', () => {
18
+ expect(response.status).to.equal(200);
19
+ });
20
+
21
+ it('responds with Content-Type application/json', () => {
22
+ expect(response.type).to.equal('application/json');
23
+ });
24
+
25
+ it('returns an array of services', () => {
26
+ expect(response.body).to.be.an('array');
27
+ });
28
+
29
+ it('each service should have an id', () => {
30
+ response.body.forEach(service => {
31
+ expect(service).to.have.property('id');
32
+ });
33
+ });
34
+
35
+ it('each service should have a name', () => {
36
+ response.body.forEach(service => {
37
+ expect(service).to.have.property('name');
38
+ });
39
+ });
40
+ });
41
+
42
+ describe('GET /services/:serviceId', () => {
43
+ let response;
44
+ const SERVICE_ID = 'Service B!';
45
+ const CASE_INSENSITIVE_SERVICE_ID = 'service b!';
46
+
47
+ before(async () => {
48
+ response = await request(app).get(`${basePath}/v1/services/${encodeURI(SERVICE_ID)}`);
49
+ });
50
+
51
+ it('responds with 200 status code', () => {
52
+ expect(response.status).to.equal(200);
53
+ });
54
+
55
+ it('responds with Content-Type application/json', () => {
56
+ expect(response.type).to.equal('application/json');
57
+ });
58
+
59
+ it('returns a service object with id', () => {
60
+ expect(response.body).to.have.property('id');
61
+ });
62
+
63
+ it('returns the proper service object', () => {
64
+ expect(response.body.id).to.equal(SERVICE_ID);
65
+ });
66
+
67
+ it('returns a service object with name', () => {
68
+ expect(response.body).to.have.property('name');
69
+ });
70
+
71
+ it('returns a service object with an array of terms', () => {
72
+ expect(response.body).to.have.property('terms').that.is.an('array');
73
+ });
74
+
75
+ it('each terms should have a type property', () => {
76
+ response.body.terms.forEach(terms => {
77
+ expect(terms).to.have.property('type');
78
+ });
79
+ });
80
+
81
+ it('each terms should have an array of source documents', () => {
82
+ response.body.terms.forEach(terms => {
83
+ expect(terms).to.have.property('sourceDocuments').that.is.an('array');
84
+ });
85
+ });
86
+
87
+ it('each source document should have a location', () => {
88
+ response.body.terms.forEach(terms => {
89
+ terms.sourceDocuments.forEach(sourceDocument => {
90
+ expect(sourceDocument).to.have.property('location');
91
+ });
92
+ });
93
+ });
94
+
95
+ context('With a case-insensitive service ID parameter', () => {
96
+ before(async () => {
97
+ response = await request(app).get(`${basePath}/v1/services/${encodeURI(CASE_INSENSITIVE_SERVICE_ID)}`);
98
+ });
99
+
100
+ it('responds with 200 status code', () => {
101
+ expect(response.status).to.equal(200);
102
+ });
103
+
104
+ it('returns a service object with id', () => {
105
+ expect(response.body).to.have.property('id');
106
+ });
107
+
108
+ it('returns the proper service object', () => {
109
+ expect(response.body.id).to.equal(SERVICE_ID);
110
+ });
111
+
112
+ it('returns a service object with name', () => {
113
+ expect(response.body).to.have.property('name');
114
+ });
115
+
116
+ it('returns a service object with an array of terms', () => {
117
+ expect(response.body).to.have.property('terms').that.is.an('array');
118
+ });
119
+
120
+ it('each terms should have a type property', () => {
121
+ response.body.terms.forEach(terms => {
122
+ expect(terms).to.have.property('type');
123
+ });
124
+ });
125
+
126
+ it('each terms should have an array of source documents', () => {
127
+ response.body.terms.forEach(terms => {
128
+ expect(terms).to.have.property('sourceDocuments').that.is.an('array');
129
+ });
130
+ });
131
+
132
+ it('each source document should have a location', () => {
133
+ response.body.terms.forEach(terms => {
134
+ terms.sourceDocuments.forEach(sourceDocument => {
135
+ expect(sourceDocument).to.have.property('location');
136
+ });
137
+ });
138
+ });
139
+ });
140
+
141
+ context('When no matching service is found', () => {
142
+ before(async () => {
143
+ response = await request(app).get(`${basePath}/v1/nonExistentService`);
144
+ });
145
+
146
+ it('responds with 404 status code', () => {
147
+ expect(response.status).to.equal(404);
148
+ });
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,28 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ import express from 'express';
5
+ import swaggerJsdoc from 'swagger-jsdoc';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const router = express.Router();
9
+
10
+ router.get('/', (req, res) => {
11
+ res.json(swaggerJsdoc({
12
+ definition: {
13
+ swagger: '2.0',
14
+ openapi: '3.1.0',
15
+ info: {
16
+ title: 'Open Terms Archive API',
17
+ version: '1.0.0',
18
+ license: {
19
+ name: 'EUPL-1.2',
20
+ url: 'https://eupl.eu/1.2/',
21
+ },
22
+ },
23
+ },
24
+ apis: [`${__dirname}/*.js`],
25
+ }));
26
+ });
27
+
28
+ export default router;
@@ -0,0 +1,59 @@
1
+ import { expect } from 'chai';
2
+ import config from 'config';
3
+ import request from 'supertest';
4
+
5
+ import app from '../server.js';
6
+
7
+ const basePath = config.get('api.basePath');
8
+
9
+ describe('Specs API', () => {
10
+ describe('GET /specs', () => {
11
+ let response;
12
+
13
+ before(async () => {
14
+ response = await request(app).get(`${basePath}/v1/specs`);
15
+ });
16
+
17
+ it('responds with 200 status code', () => {
18
+ expect(response.status).to.equal(200);
19
+ });
20
+
21
+ it('responds with Content-Type application/json', () => {
22
+ expect(response.type).to.equal('application/json');
23
+ });
24
+
25
+ describe('body response defines', () => {
26
+ let subject;
27
+
28
+ before(async () => {
29
+ subject = response.body;
30
+ });
31
+
32
+ it('openapi version', () => {
33
+ expect(subject).to.have.property('openapi');
34
+ });
35
+
36
+ it('swagger version', () => {
37
+ expect(subject).to.have.property('swagger');
38
+ });
39
+
40
+ it('paths', () => {
41
+ expect(subject).to.have.property('paths');
42
+ });
43
+
44
+ describe('with endpoints', () => {
45
+ before(async () => {
46
+ subject = response.body.paths;
47
+ });
48
+
49
+ it('/services', () => {
50
+ expect(subject).to.have.property('/services');
51
+ });
52
+
53
+ it('/service/{serviceId}', () => {
54
+ expect(subject).to.have.property('/service/{serviceId}');
55
+ });
56
+ });
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,24 @@
1
+ import config from 'config';
2
+ import express from 'express';
3
+ import helmet from 'helmet';
4
+
5
+ import logger from './logger.js';
6
+ import errorsMiddleware from './middlewares/errors.js';
7
+ import loggerMiddleware from './middlewares/logger.js';
8
+ import apiRouter from './routes/index.js';
9
+
10
+ const app = express();
11
+
12
+ app.use(helmet());
13
+
14
+ if (process.env.NODE_ENV !== 'test') {
15
+ app.use(loggerMiddleware);
16
+ }
17
+
18
+ app.use(`${config.get('api.basePath')}/v1`, apiRouter);
19
+ app.use(errorsMiddleware);
20
+
21
+ app.listen(config.get('api.port'));
22
+ logger.info('Start Open Terms Archive API\n');
23
+
24
+ export default app;
@@ -65,6 +65,8 @@ export default class Archivist extends events.EventEmitter {
65
65
  await this.recorder.finalize().then(() => console.log('Recorder finalized'));
66
66
  process.exit(1);
67
67
  });
68
+
69
+ return this;
68
70
  }
69
71
 
70
72
  initQueue() {
@@ -106,7 +108,7 @@ export default class Archivist extends events.EventEmitter {
106
108
  return;
107
109
  }
108
110
 
109
- this.trackingQueue.push({ terms: this.services[serviceId].getTerms(termsType), extractOnly });
111
+ this.trackingQueue.push({ terms: this.services[serviceId].getTerms({ type: termsType }), extractOnly });
110
112
  });
111
113
  });
112
114
 
@@ -42,14 +42,14 @@ describe('Archivist', function () {
42
42
  let serviceASnapshotExpectedContent;
43
43
  let serviceAVersionExpectedContent;
44
44
 
45
- const SERVICE_B_ID = 'service_B';
45
+ const SERVICE_B_ID = 'Service B!';
46
46
  const SERVICE_B_TYPE = 'Privacy Policy';
47
47
  const SERVICE_B_EXPECTED_SNAPSHOT_FILE_PATH = `${SNAPSHOTS_PATH}/${SERVICE_B_ID}/${SERVICE_B_TYPE}.pdf`;
48
48
  const SERVICE_B_EXPECTED_VERSION_FILE_PATH = `${VERSIONS_PATH}/${SERVICE_B_ID}/${SERVICE_B_TYPE}.md`;
49
49
  let serviceBSnapshotExpectedContent;
50
50
  let serviceBVersionExpectedContent;
51
51
 
52
- const services = [ 'service_A', 'service_B' ];
52
+ const services = [ 'service_A', 'Service B!' ];
53
53
 
54
54
  before(async () => {
55
55
  gitVersion = new Git({
@@ -164,9 +164,9 @@ describe('Archivist', function () {
164
164
 
165
165
  serviceBCommits = await gitVersion.log({ file: SERVICE_B_EXPECTED_VERSION_FILE_PATH });
166
166
 
167
- app.services[SERVICE_A_ID].getTerms(SERVICE_A_TYPE).sourceDocuments[0].contentSelectors = 'h1';
167
+ app.services[SERVICE_A_ID].getTerms({ type: SERVICE_A_TYPE }).sourceDocuments[0].contentSelectors = 'h1';
168
168
 
169
- await app.track({ services: [ 'service_A', 'service_B' ], extractOnly: true });
169
+ await app.track({ services: [ 'service_A', 'Service B!' ], extractOnly: true });
170
170
 
171
171
  const [reExtractedVersionCommit] = await gitVersion.log({ file: SERVICE_A_EXPECTED_VERSION_FILE_PATH });
172
172
 
@@ -218,12 +218,12 @@ describe('Archivist', function () {
218
218
 
219
219
  await app.initialize();
220
220
  await app.track({ services });
221
- app.services[SERVICE_A_ID].getTerms(SERVICE_A_TYPE).sourceDocuments[0].contentSelectors = 'inexistant-selector';
221
+ app.services[SERVICE_A_ID].getTerms({ type: SERVICE_A_TYPE }).sourceDocuments[0].contentSelectors = 'inexistant-selector';
222
222
  inaccessibleContentSpy = sinon.spy();
223
223
  versionNotChangedSpy = sinon.spy();
224
224
  app.on('inaccessibleContent', inaccessibleContentSpy);
225
225
  app.on('versionNotChanged', record => {
226
- if (record.serviceId == 'service_B') {
226
+ if (record.serviceId == 'Service B!') {
227
227
  versionB = record;
228
228
  }
229
229
  versionNotChangedSpy(record);
@@ -282,7 +282,7 @@ describe('Archivist', function () {
282
282
  let snapshot;
283
283
 
284
284
  before(async () => {
285
- terms = app.services.service_A.getTerms(SERVICE_A_TYPE);
285
+ terms = app.services.service_A.getTerms({ type: SERVICE_A_TYPE });
286
286
  terms.fetchDate = FETCH_DATE;
287
287
  terms.sourceDocuments.forEach(async sourceDocument => {
288
288
  sourceDocument.content = serviceASnapshotExpectedContent;
@@ -364,7 +364,7 @@ describe('Archivist', function () {
364
364
  let version;
365
365
 
366
366
  before(async () => {
367
- terms = app.services.service_A.getTerms(SERVICE_A_TYPE);
367
+ terms = app.services.service_A.getTerms({ type: SERVICE_A_TYPE });
368
368
  terms.fetchDate = FETCH_DATE;
369
369
  terms.sourceDocuments.forEach(async sourceDocument => {
370
370
  sourceDocument.content = serviceASnapshotExpectedContent;
@@ -22,7 +22,7 @@ describe('Services', () => {
22
22
  let actualInsignificantContentSelectors;
23
23
  let actualExecuteClientScripts;
24
24
 
25
- const expectedTerms = expected.getTerms(termsType);
25
+ const expectedTerms = expected.getTerms({ type: termsType });
26
26
 
27
27
  const { sourceDocuments } = expectedTerms;
28
28
 
@@ -36,7 +36,7 @@ describe('Services', () => {
36
36
 
37
37
  context(`source document: ${sourceDocument.id}`, () => {
38
38
  before(() => {
39
- actualTerms = result[serviceId].getTerms(termsType);
39
+ actualTerms = result[serviceId].getTerms({ type: termsType });
40
40
  const { sourceDocuments: actualDocuments } = actualTerms;
41
41
 
42
42
  ({
@@ -102,7 +102,7 @@ describe('Services', () => {
102
102
  });
103
103
 
104
104
  describe('Service B', async () => {
105
- await validateServiceWithoutHistory('service_B', expectedServices.service_B);
105
+ await validateServiceWithoutHistory('Service B!', expectedServices['Service B!']);
106
106
  });
107
107
 
108
108
  describe('Service without history', async () => {
@@ -127,11 +127,11 @@ describe('Services', () => {
127
127
 
128
128
  context('when specifying services to load', async () => {
129
129
  before(async () => {
130
- result = await services.load([ 'service_A', 'service_B' ]);
130
+ result = await services.load([ 'service_A', 'Service B!' ]);
131
131
  });
132
132
 
133
133
  it('loads only the given services', async () => {
134
- expect(result).to.have.all.keys('service_A', 'service_B');
134
+ expect(result).to.have.all.keys('service_A', 'Service B!');
135
135
  });
136
136
  });
137
137
  });
@@ -148,12 +148,12 @@ describe('Services', () => {
148
148
 
149
149
  let actualTerms;
150
150
  let actualFilters;
151
- const expectedTerms = expected.getTerms(termsType);
151
+ const expectedTerms = expected.getTerms({ type: termsType });
152
152
 
153
153
  const { sourceDocuments } = expectedTerms;
154
154
 
155
155
  before(() => {
156
- actualTerms = result[serviceId].getTerms(termsType);
156
+ actualTerms = result[serviceId].getTerms({ type: termsType });
157
157
  });
158
158
 
159
159
  it('has the proper service name', () => {
@@ -182,7 +182,7 @@ describe('Services', () => {
182
182
  let insignificantContentSelectorsForThisDate;
183
183
  let actualExecuteClientScriptsForThisDate;
184
184
 
185
- const { sourceDocuments: documentsForThisDate } = expected.getTerms(termsType, date);
185
+ const { sourceDocuments: documentsForThisDate } = expected.getTerms({ type: termsType, date });
186
186
  const {
187
187
  filters: expectedFiltersForThisDate,
188
188
  contentSelectors: expectedContentSelectors,
@@ -191,7 +191,7 @@ describe('Services', () => {
191
191
  } = documentsForThisDate[index];
192
192
 
193
193
  before(() => {
194
- const { sourceDocuments: actualDocumentsForThisDate } = result[serviceId].getTerms(termsType, date);
194
+ const { sourceDocuments: actualDocumentsForThisDate } = result[serviceId].getTerms({ type: termsType, date });
195
195
 
196
196
  ({
197
197
  filters: actualFiltersForThisDate,
@@ -271,7 +271,7 @@ describe('Services', () => {
271
271
  });
272
272
 
273
273
  describe('Service B', async () => {
274
- await validateServiceWithHistory('service_B', expectedServices.service_B);
274
+ await validateServiceWithHistory('Service B!', expectedServices['Service B!']);
275
275
  });
276
276
 
277
277
  describe('Service without history', async () => {
@@ -296,11 +296,11 @@ describe('Services', () => {
296
296
 
297
297
  context('when specifying services to load', async () => {
298
298
  before(async () => {
299
- result = await services.loadWithHistory([ 'service_A', 'service_B' ]);
299
+ result = await services.loadWithHistory([ 'service_A', 'Service B!' ]);
300
300
  });
301
301
 
302
302
  it('loads only the given services', async () => {
303
- expect(result).to.have.all.keys('service_A', 'service_B');
303
+ expect(result).to.have.all.keys('service_A', 'Service B!');
304
304
  });
305
305
  });
306
306
  });
@@ -6,21 +6,26 @@ export default class Service {
6
6
  this.name = name;
7
7
  }
8
8
 
9
- getTerms(termsType, date) {
10
- if (!this.terms[termsType]) {
11
- return null;
12
- }
9
+ getTerms({ type, date } = {}) {
10
+ if (type) {
11
+ if (date) {
12
+ return this.#getTermsAtDate(type, date);
13
+ }
13
14
 
14
- const { latest: currentlyValidTerms, history } = this.terms[termsType];
15
+ return this.#getTermsAtDate(type, new Date());
16
+ }
15
17
 
16
- if (!date) {
17
- return currentlyValidTerms;
18
+ if (date) {
19
+ return this.getTermsTypes().map(termsType => this.#getTermsAtDate(termsType, date));
18
20
  }
19
21
 
20
- return (
21
- history?.find(entry => new Date(date) <= new Date(entry.validUntil))
22
- || currentlyValidTerms
23
- );
22
+ return this.getTermsTypes().map(termsType => this.#getTermsAtDate(termsType, new Date()));
23
+ }
24
+
25
+ #getTermsAtDate(termsType, date) {
26
+ const { latest: currentlyValidTerms, history } = this.terms[termsType];
27
+
28
+ return history?.find(entry => new Date(date) <= new Date(entry.validUntil)) || currentlyValidTerms;
24
29
  }
25
30
 
26
31
  getTermsTypes() {
@@ -19,18 +19,18 @@ describe('Service', () => {
19
19
  });
20
20
  });
21
21
 
22
- context('when terms declaration has no validity date', () => {
22
+ context('when terms have no validity date', () => {
23
23
  before(async () => {
24
24
  subject = new Service({ id: 'serviceID', name: 'serviceName' });
25
25
  subject.addTerms(terms);
26
26
  });
27
27
 
28
- it('adds the terms as the last valid terms declaration', async () => {
29
- expect(subject.getTerms(TERMS_TYPE)).to.deep.eql(terms);
28
+ it('adds the terms as the last valid terms', async () => {
29
+ expect(subject.getTerms({ type: TERMS_TYPE })).to.deep.eql(terms);
30
30
  });
31
31
  });
32
32
 
33
- context('when terms declaration has a validity date', () => {
33
+ context('when terms have a validity date', () => {
34
34
  let expiredTerms;
35
35
  const VALIDITY_DATE = new Date('2020-07-22T11:30:21.000Z');
36
36
 
@@ -46,7 +46,7 @@ describe('Service', () => {
46
46
  });
47
47
 
48
48
  it('adds the terms with the proper validity date', async () => {
49
- expect(subject.getTerms(TERMS_TYPE, VALIDITY_DATE)).to.deep.eql(expiredTerms);
49
+ expect(subject.getTerms({ type: TERMS_TYPE, date: VALIDITY_DATE })).to.deep.eql(expiredTerms);
50
50
  });
51
51
  });
52
52
  });
@@ -54,59 +54,76 @@ describe('Service', () => {
54
54
  describe('#getTerms', () => {
55
55
  let subject;
56
56
 
57
- const lastDeclaration = new Terms({ type: TERMS_TYPE });
57
+ const EARLIEST_DATE = '2020-06-21T11:30:21.000Z';
58
+ const DATE = '2020-07-22T11:30:21.000Z';
59
+ const LATEST_DATE = '2020-08-21T11:30:21.000Z';
58
60
 
59
- context('when there is no history', () => {
60
- before(async () => {
61
- subject = new Service({ id: 'serviceID', name: 'serviceName' });
62
- subject.addTerms(lastDeclaration);
63
- });
61
+ const firstTermsOfService = new Terms({ type: TERMS_TYPE, validUntil: DATE });
62
+ const firstPrivacyPolicy = new Terms({ type: 'Privacy Policy', validUntil: DATE });
64
63
 
65
- context('without given date', () => {
66
- it('returns the last terms declaration', async () => {
67
- expect(subject.getTerms(TERMS_TYPE)).to.eql(lastDeclaration);
68
- });
69
- });
64
+ const latestTermsOfService = new Terms({ type: TERMS_TYPE });
65
+ const latestPrivacyPolicy = new Terms({ type: 'Privacy Policy' });
70
66
 
71
- context('with a date', () => {
72
- it('returns the last terms declaration', async () => {
73
- expect(subject.getTerms(TERMS_TYPE, '2020-08-21T11:30:21.000Z')).to.eql(lastDeclaration);
74
- });
75
- });
67
+ const latestDeveloperTerms = new Terms({ type: 'Developer Terms' });
68
+
69
+ before(async () => {
70
+ subject = new Service({ id: 'serviceID', name: 'serviceName' });
71
+ subject.addTerms(firstTermsOfService);
72
+ subject.addTerms(firstPrivacyPolicy);
73
+ subject.addTerms(latestTermsOfService);
74
+ subject.addTerms(latestPrivacyPolicy);
75
+ subject.addTerms(latestDeveloperTerms);
76
76
  });
77
77
 
78
- context('when the terms have a history', () => {
79
- const firstDeclaration = new Terms({
80
- type: TERMS_TYPE,
81
- validUntil: '2020-07-22T11:30:21.000Z',
78
+ context('when no params are given', () => {
79
+ it('returns all latest terms', async () => {
80
+ expect(subject.getTerms()).to.deep.eql([ latestTermsOfService, latestPrivacyPolicy, latestDeveloperTerms ]);
82
81
  });
82
+ });
83
83
 
84
- const secondDeclaration = new Terms({
85
- type: TERMS_TYPE,
86
- validUntil: '2020-08-22T11:30:21.000Z',
84
+ context('when a terms type is given', () => {
85
+ context('when a date is given', () => {
86
+ context('when the terms has no history', () => {
87
+ it('returns the latest terms according to the given type', async () => {
88
+ expect(subject.getTerms({ type: 'Developer Terms', date: LATEST_DATE })).to.eql(latestDeveloperTerms);
89
+ });
90
+ });
91
+
92
+ context('when the terms have a history', () => {
93
+ it('returns the terms according to the given type and date', async () => {
94
+ expect(subject.getTerms({ type: TERMS_TYPE, date: EARLIEST_DATE })).to.eql(firstTermsOfService);
95
+ });
96
+
97
+ context('when the given date is strictly equal to a terms validity date', () => {
98
+ it('returns the terms according to the given type with the validity date equal to the given date', async () => {
99
+ expect(subject.getTerms({ type: TERMS_TYPE, date: DATE })).to.eql(firstTermsOfService);
100
+ });
101
+ });
102
+ });
87
103
  });
88
104
 
89
- before(async () => {
90
- subject = new Service({ id: 'serviceID', name: 'serviceName' });
91
- subject.addTerms(lastDeclaration);
92
- subject.addTerms(firstDeclaration);
93
- subject.addTerms(secondDeclaration);
105
+ context('without a given date', () => {
106
+ it('returns the latest terms of given type', async () => {
107
+ expect(subject.getTerms({ type: TERMS_TYPE })).to.eql(latestTermsOfService);
108
+ });
94
109
  });
110
+ });
95
111
 
96
- context('without given date', () => {
97
- it('returns the last terms declaration', async () => {
98
- expect(subject.getTerms(TERMS_TYPE)).to.eql(lastDeclaration);
112
+ context('when only a date is given', () => {
113
+ context('when there is no history', () => {
114
+ it('returns all latest terms', async () => {
115
+ expect(subject.getTerms({ date: LATEST_DATE })).to.deep.eql([ latestTermsOfService, latestPrivacyPolicy, latestDeveloperTerms ]);
99
116
  });
100
117
  });
101
118
 
102
- context('with a date', () => {
103
- it('returns the terms declaration according to the given date', async () => {
104
- expect(subject.getTerms(TERMS_TYPE, '2020-08-21T11:30:21.000Z')).to.eql(secondDeclaration);
119
+ context('when the terms have a history', () => {
120
+ it('returns all the terms according to the given date', async () => {
121
+ expect(subject.getTerms({ date: EARLIEST_DATE })).to.deep.eql([ firstTermsOfService, firstPrivacyPolicy, latestDeveloperTerms ]);
105
122
  });
106
123
 
107
- context('strictly equal to a terms declaration validity date', () => {
108
- it('returns the terms declaration with the validity date equal to the given date', async () => {
109
- expect(subject.getTerms(TERMS_TYPE, secondDeclaration.validUntil)).to.eql(secondDeclaration);
124
+ context('when the given date is strictly equal to a terms validity date', () => {
125
+ it('returns all the terms with the validity date equal to the given date', async () => {
126
+ expect(subject.getTerms({ date: DATE })).to.deep.eql([ firstTermsOfService, firstPrivacyPolicy, latestDeveloperTerms ]);
110
127
  });
111
128
  });
112
129
  });
@@ -115,27 +132,27 @@ describe('Service', () => {
115
132
 
116
133
  describe('#getTermsTypes', () => {
117
134
  let subject;
118
- let termsOfServiceDeclaration;
119
- let privacyPolicyDeclaration;
135
+ let termsOfService;
136
+ let privacyPolicy;
120
137
 
121
138
  before(async () => {
122
139
  subject = new Service({ id: 'serviceID', name: 'serviceName' });
123
140
 
124
- termsOfServiceDeclaration = new Terms({ type: TERMS_TYPE });
141
+ termsOfService = new Terms({ type: TERMS_TYPE });
125
142
 
126
- privacyPolicyDeclaration = new Terms({
143
+ privacyPolicy = new Terms({
127
144
  type: 'Privacy Policy',
128
145
  validUntil: '2020-07-22T11:30:21.000Z',
129
146
  });
130
147
 
131
- subject.addTerms(termsOfServiceDeclaration);
132
- subject.addTerms(privacyPolicyDeclaration);
148
+ subject.addTerms(termsOfService);
149
+ subject.addTerms(privacyPolicy);
133
150
  });
134
151
 
135
152
  it('returns the service terms types', async () => {
136
153
  expect(subject.getTermsTypes()).to.have.members([
137
- termsOfServiceDeclaration.type,
138
- privacyPolicyDeclaration.type,
154
+ termsOfService.type,
155
+ privacyPolicy.type,
139
156
  ]);
140
157
  });
141
158
  });
@@ -69,7 +69,7 @@ export default class Tracker {
69
69
  return this.onVersionRecorded(serviceId, type);
70
70
  }
71
71
 
72
- async onInaccessibleContent(error, serviceId, type, terms) {
72
+ async onInaccessibleContent(error, terms) {
73
73
  const { title, body } = Tracker.formatIssueTitleAndBody({ message: error.toString(), repository: this.repository, terms });
74
74
 
75
75
  await this.createIssueIfNotExists({