@opentermsarchive/engine 1.1.1 → 1.1.3
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/.eslintrc.yaml +2 -0
- package/package.json +2 -2
- package/scripts/declarations/lint/index.mocha.js +1 -1
- package/scripts/declarations/utils/fixtures/serviceA.json +13 -0
- package/scripts/declarations/utils/fixtures/serviceAMultipleTermsUpdated.json +20 -0
- package/scripts/declarations/utils/fixtures/serviceATermsAdded.json +17 -0
- package/scripts/declarations/utils/fixtures/serviceATermsRemoved.json +9 -0
- package/scripts/declarations/utils/fixtures/serviceATermsUpdated.history.json +9 -0
- package/scripts/declarations/utils/fixtures/serviceATermsUpdated.json +14 -0
- package/scripts/declarations/utils/fixtures/serviceB.json +9 -0
- package/scripts/declarations/utils/index.js +38 -32
- package/scripts/declarations/utils/index.test.js +162 -0
- package/scripts/declarations/validate/index.mocha.js +2 -2
- package/src/archivist/fetcher/index.test.js +1 -1
package/.eslintrc.yaml
CHANGED
|
@@ -10,6 +10,7 @@ plugins:
|
|
|
10
10
|
- chai-friendly
|
|
11
11
|
- import
|
|
12
12
|
- json-format
|
|
13
|
+
- no-only-tests
|
|
13
14
|
rules:
|
|
14
15
|
arrow-parens:
|
|
15
16
|
- error
|
|
@@ -101,6 +102,7 @@ rules:
|
|
|
101
102
|
- error
|
|
102
103
|
- properties: false
|
|
103
104
|
require-await: 1
|
|
105
|
+
no-only-tests/no-only-tests: error
|
|
104
106
|
|
|
105
107
|
overrides:
|
|
106
108
|
- files:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentermsarchive/engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Tracks and makes visible changes to the terms of online services",
|
|
5
5
|
"homepage": "https://opentermsarchive.org",
|
|
6
6
|
"bugs": {
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"croner": "^4.3.6",
|
|
66
66
|
"cross-env": "^7.0.3",
|
|
67
67
|
"datauri": "^4.1.0",
|
|
68
|
-
"deep-diff": "^1.0.2",
|
|
69
68
|
"dotenv": "^10.0.0",
|
|
70
69
|
"eslint": "^8.53.0",
|
|
71
70
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
@@ -104,6 +103,7 @@
|
|
|
104
103
|
"devDependencies": {
|
|
105
104
|
"@commitlint/cli": "^19.0.3",
|
|
106
105
|
"dir-compare": "^4.0.0",
|
|
106
|
+
"eslint-plugin-no-only-tests": "^3.1.0",
|
|
107
107
|
"keep-a-changelog": "^2.5.3",
|
|
108
108
|
"nock": "^13.2.1",
|
|
109
109
|
"node-stream-zip": "^1.15.0",
|
|
@@ -31,7 +31,7 @@ export default async options => {
|
|
|
31
31
|
if (options.modified) {
|
|
32
32
|
const declarationUtils = new DeclarationUtils(instancePath);
|
|
33
33
|
|
|
34
|
-
({ services: servicesToValidate } = await declarationUtils.
|
|
34
|
+
({ services: servicesToValidate } = await declarationUtils.getModifiedServicesAndTermsTypes());
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const lintFile = lintAndFixFile(options.fix);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Service",
|
|
3
|
+
"documents": {
|
|
4
|
+
"Terms of Service": {
|
|
5
|
+
"fetch": "https://domain.example/tos",
|
|
6
|
+
"select": "body",
|
|
7
|
+
"remove": "footer"
|
|
8
|
+
},
|
|
9
|
+
"Privacy Policy": {
|
|
10
|
+
"fetch": "https://domain.example/privacy",
|
|
11
|
+
"select": "body",
|
|
12
|
+
"remove": "footer"
|
|
13
|
+
},
|
|
14
|
+
"Imprint": {
|
|
15
|
+
"fetch": "https://domain.example/imprint",
|
|
16
|
+
"select": "body",
|
|
17
|
+
"remove": "footer"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Service",
|
|
3
|
+
"documents": {
|
|
4
|
+
"Terms of Service": {
|
|
5
|
+
"fetch": "https://domain.example/tos",
|
|
6
|
+
"select": "body"
|
|
7
|
+
},
|
|
8
|
+
"Privacy Policy": {
|
|
9
|
+
"fetch": "https://domain.example/privacy",
|
|
10
|
+
"select": "body"
|
|
11
|
+
},
|
|
12
|
+
"Imprint": {
|
|
13
|
+
"fetch": "https://domain.example/imprint",
|
|
14
|
+
"select": "body"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Service",
|
|
3
|
+
"documents": {
|
|
4
|
+
"Terms of Service": {
|
|
5
|
+
"fetch": "https://domain.example/tos",
|
|
6
|
+
"select": "body",
|
|
7
|
+
"remove": "footer"
|
|
8
|
+
},
|
|
9
|
+
"Privacy Policy": {
|
|
10
|
+
"fetch": "https://domain.example/privacy",
|
|
11
|
+
"select": "body"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
|
|
3
|
-
import DeepDiff from 'deep-diff';
|
|
4
3
|
import simpleGit from 'simple-git';
|
|
5
4
|
|
|
6
5
|
export default class DeclarationUtils {
|
|
@@ -9,13 +8,17 @@ export default class DeclarationUtils {
|
|
|
9
8
|
this.defaultBranch = defaultBranch;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
static
|
|
11
|
+
static getServiceIdFromFilePath(filePath) {
|
|
12
|
+
return path.parse(filePath.replace(/\.history|\.filters/, '')).name;
|
|
13
|
+
}
|
|
13
14
|
|
|
14
|
-
async
|
|
15
|
+
async getJSONFromFile(ref, filePath) {
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
17
|
+
const fileContent = await this.git.show([`${ref}:${filePath}`]);
|
|
18
|
+
|
|
19
|
+
return JSON.parse(fileContent);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// the file does not exist on the requested branch or it is not parsable
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -24,7 +27,7 @@ export default class DeclarationUtils {
|
|
|
24
27
|
|
|
25
28
|
const modifiedFilePaths = (modifiedFilePathsAsString ? modifiedFilePathsAsString.split('\0') : []).filter(str => str !== ''); // split on \0 rather than \n due to the -z option of git diff
|
|
26
29
|
|
|
27
|
-
return { modifiedFilePaths, modifiedServicesIds: Array.from(new Set(modifiedFilePaths.map(DeclarationUtils.
|
|
30
|
+
return { modifiedFilePaths, modifiedServicesIds: Array.from(new Set(modifiedFilePaths.map(DeclarationUtils.getServiceIdFromFilePath))) };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
async getModifiedServices() {
|
|
@@ -33,48 +36,51 @@ export default class DeclarationUtils {
|
|
|
33
36
|
return modifiedServicesIds;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
async
|
|
37
|
-
const { modifiedFilePaths
|
|
39
|
+
async getModifiedServicesAndTermsTypes() {
|
|
40
|
+
const { modifiedFilePaths } = await this.getModifiedData();
|
|
38
41
|
const servicesTermsTypes = {};
|
|
39
42
|
|
|
40
43
|
await Promise.all(modifiedFilePaths.map(async modifiedFilePath => {
|
|
41
|
-
const serviceId = DeclarationUtils.
|
|
44
|
+
const serviceId = DeclarationUtils.getServiceIdFromFilePath(modifiedFilePath);
|
|
45
|
+
|
|
46
|
+
if (modifiedFilePath.endsWith('.history.json')) {
|
|
47
|
+
return; // Assuming history modifications imply corresponding changes in the service declaration and that the analysis of which terms types of this service have changed will be done when analysing the related declaration, no further action is required here
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (modifiedFilePath.endsWith('.filters.js')) {
|
|
51
|
+
const declaration = await this.getJSONFromFile(this.defaultBranch, `declarations/${serviceId}.json`);
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
// Here we should compare AST of both files to detect on which function
|
|
45
|
-
// change has been made, and then find which terms type depends on this
|
|
46
|
-
// function.
|
|
47
|
-
// As this is a complicated process, we will just send back all terms types
|
|
48
|
-
const declaration = await this.getJSONFile(`declarations/${serviceId}.json`, this.defaultBranch);
|
|
53
|
+
servicesTermsTypes[serviceId] = Object.keys(declaration.documents); // Considering how rarely filters are used, simply return all term types that could potentially be impacted to spare implementing a function change check
|
|
49
54
|
|
|
50
|
-
return
|
|
55
|
+
return;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
const
|
|
54
|
-
const
|
|
58
|
+
const originalService = await this.getJSONFromFile(this.defaultBranch, modifiedFilePath);
|
|
59
|
+
const modifiedService = await this.getJSONFromFile('HEAD', modifiedFilePath);
|
|
60
|
+
const modifiedServiceTermsTypes = Object.keys(modifiedService.documents);
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
if (!originalService) {
|
|
63
|
+
servicesTermsTypes[serviceId] = modifiedServiceTermsTypes;
|
|
57
64
|
|
|
58
|
-
if (!diff) {
|
|
59
|
-
// This can happen if only a lint has been applied to a document
|
|
60
65
|
return;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
const
|
|
64
|
-
if (modifiedFilePath.includes('.history')) {
|
|
65
|
-
acc.add(path[0]);
|
|
66
|
-
} else if (path[0] == 'documents') {
|
|
67
|
-
acc.add(path[1]);
|
|
68
|
-
}
|
|
68
|
+
const originalServiceTermsTypes = Object.keys(originalService.documents);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
modifiedServiceTermsTypes.forEach(termsType => {
|
|
71
|
+
const areTermsAdded = !originalServiceTermsTypes.includes(termsType);
|
|
72
|
+
const areTermsModified = JSON.stringify(originalService.documents[termsType]) != JSON.stringify(modifiedService.documents[termsType]);
|
|
73
|
+
|
|
74
|
+
if (!areTermsAdded && !areTermsModified) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
servicesTermsTypes[serviceId] = [termsType].concat(servicesTermsTypes[serviceId] || []);
|
|
79
|
+
});
|
|
74
80
|
}));
|
|
75
81
|
|
|
76
82
|
return {
|
|
77
|
-
services:
|
|
83
|
+
services: Object.keys(servicesTermsTypes),
|
|
78
84
|
servicesTermsTypes,
|
|
79
85
|
};
|
|
80
86
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import DeclarationUtils from './index.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
let declarationUtils;
|
|
12
|
+
|
|
13
|
+
const SUBJECT_PATH = path.resolve(__dirname, './test');
|
|
14
|
+
|
|
15
|
+
const FIXTURES = {
|
|
16
|
+
serviceA: { path: './fixtures/serviceA.json' },
|
|
17
|
+
serviceATermsUpdated: { path: './fixtures/serviceATermsUpdated.json' },
|
|
18
|
+
serviceATermsUpdatedHistory: { path: './fixtures/serviceATermsUpdated.history.json' },
|
|
19
|
+
serviceAMultipleTermsUpdated: { path: './fixtures/serviceAMultipleTermsUpdated.json' },
|
|
20
|
+
serviceATermsAdded: { path: './fixtures/serviceATermsAdded.json' },
|
|
21
|
+
serviceATermsRemoved: { path: './fixtures/serviceATermsRemoved.json' },
|
|
22
|
+
serviceB: { path: './fixtures/serviceB.json' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const COMMIT_PATHS = {
|
|
26
|
+
serviceA: './declarations/ServiceA.json',
|
|
27
|
+
serviceAHistory: './declarations/ServiceA.history.json',
|
|
28
|
+
serviceB: './declarations/ServiceB.json',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const commitChanges = async (filePath, content) => {
|
|
32
|
+
await fs.writeFile(path.resolve(SUBJECT_PATH, filePath), JSON.stringify(content, null, 2));
|
|
33
|
+
await declarationUtils.git.add(filePath);
|
|
34
|
+
await declarationUtils.git.commit('Update declarations', filePath);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const removeLatestCommit = async () => {
|
|
38
|
+
await declarationUtils.git.reset('hard', ['HEAD~1']);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe('DeclarationUtils', () => {
|
|
42
|
+
describe('#getModifiedServicesAndTermsTypes', () => {
|
|
43
|
+
before(async () => {
|
|
44
|
+
await loadFixtures();
|
|
45
|
+
await setupRepository();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
after(() => fs.rm(SUBJECT_PATH, { recursive: true }));
|
|
49
|
+
|
|
50
|
+
context('when an existing declaration has been modified', () => {
|
|
51
|
+
before(async () => {
|
|
52
|
+
await commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceATermsUpdated.content);
|
|
53
|
+
await commitChanges(COMMIT_PATHS.serviceAHistory, FIXTURES.serviceATermsUpdatedHistory.content);
|
|
54
|
+
});
|
|
55
|
+
after(async () => {
|
|
56
|
+
await removeLatestCommit();
|
|
57
|
+
await removeLatestCommit();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns the service ID and the updated terms type', async () => {
|
|
61
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
62
|
+
services: ['ServiceA'],
|
|
63
|
+
servicesTermsTypes: { ServiceA: ['Terms of Service'] },
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
context('when new terms are declared in an existing declaration', () => {
|
|
69
|
+
before(() => commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceATermsAdded.content));
|
|
70
|
+
after(removeLatestCommit);
|
|
71
|
+
|
|
72
|
+
it('returns the service ID and the added terms type', async () => {
|
|
73
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
74
|
+
services: ['ServiceA'],
|
|
75
|
+
servicesTermsTypes: { ServiceA: ['Imprint'] },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
context('when a new declaration has been added', () => {
|
|
81
|
+
before(() => commitChanges(COMMIT_PATHS.serviceB, FIXTURES.serviceB.content));
|
|
82
|
+
after(removeLatestCommit);
|
|
83
|
+
|
|
84
|
+
it('returns the added service ID along with the associated terms type', async () => {
|
|
85
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
86
|
+
services: ['ServiceB'],
|
|
87
|
+
servicesTermsTypes: { ServiceB: ['Terms of Service'] },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
context('when a declaration has been removed', () => {
|
|
93
|
+
before(removeLatestCommit);
|
|
94
|
+
after(async () => {
|
|
95
|
+
await fs.mkdir(path.resolve(SUBJECT_PATH, './declarations'), { recursive: true });
|
|
96
|
+
await commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceA.content);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns no services and no terms types', async () => {
|
|
100
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
101
|
+
services: [],
|
|
102
|
+
servicesTermsTypes: {},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
context('when terms are removed from an existing declaration', () => {
|
|
108
|
+
before(() => commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceATermsRemoved.content));
|
|
109
|
+
after(removeLatestCommit);
|
|
110
|
+
|
|
111
|
+
it('returns no services and no terms types', async () => {
|
|
112
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
113
|
+
services: [],
|
|
114
|
+
servicesTermsTypes: {},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
context('when there is a combination of an updated declaration and an added declaration', () => {
|
|
120
|
+
before(async () => {
|
|
121
|
+
await commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceAMultipleTermsUpdated.content);
|
|
122
|
+
await commitChanges(COMMIT_PATHS.serviceB, FIXTURES.serviceB.content);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
after(async () => {
|
|
126
|
+
await removeLatestCommit();
|
|
127
|
+
await removeLatestCommit();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns the services IDs and the updated terms types', async () => {
|
|
131
|
+
expect(await declarationUtils.getModifiedServicesAndTermsTypes()).to.deep.equal({
|
|
132
|
+
services: [ 'ServiceA', 'ServiceB' ],
|
|
133
|
+
servicesTermsTypes: {
|
|
134
|
+
ServiceA: [ 'Imprint', 'Privacy Policy', 'Terms of Service' ],
|
|
135
|
+
ServiceB: ['Terms of Service'],
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
async function loadFixtures() {
|
|
144
|
+
await Promise.all(Object.values(FIXTURES).map(async fixture => {
|
|
145
|
+
const content = await fs.readFile(path.resolve(__dirname, fixture.path), 'utf-8');
|
|
146
|
+
|
|
147
|
+
fixture.content = JSON.parse(content);
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function setupRepository() {
|
|
152
|
+
await fs.mkdir(path.resolve(SUBJECT_PATH, './declarations'), { recursive: true });
|
|
153
|
+
|
|
154
|
+
declarationUtils = new DeclarationUtils(SUBJECT_PATH, 'main');
|
|
155
|
+
await declarationUtils.git.init();
|
|
156
|
+
await declarationUtils.git.addConfig('user.name', 'Open Terms Archive Testing Bot');
|
|
157
|
+
await declarationUtils.git.addConfig('user.email', 'testing-bot@opentermsarchive.org');
|
|
158
|
+
await declarationUtils.git.checkoutLocalBranch('main');
|
|
159
|
+
await commitChanges('./README.md', 'This directory is auto-generated by test executions and requires cleanup after each test run');
|
|
160
|
+
await commitChanges(COMMIT_PATHS.serviceA, FIXTURES.serviceA.content);
|
|
161
|
+
await declarationUtils.git.checkoutLocalBranch('updated');
|
|
162
|
+
}
|
|
@@ -37,11 +37,11 @@ export default async options => {
|
|
|
37
37
|
if (options.modified) {
|
|
38
38
|
const declarationUtils = new DeclarationUtils(instancePath);
|
|
39
39
|
|
|
40
|
-
({ services: servicesToValidate, servicesTermsTypes } = await declarationUtils.
|
|
40
|
+
({ services: servicesToValidate, servicesTermsTypes } = await declarationUtils.getModifiedServicesAndTermsTypes());
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
describe('Service declarations validation', async function () {
|
|
44
|
-
this.timeout(
|
|
44
|
+
this.timeout(60000);
|
|
45
45
|
this.slow(SLOW_DOCUMENT_THRESHOLD);
|
|
46
46
|
|
|
47
47
|
servicesToValidate.forEach(serviceId => {
|
|
@@ -20,7 +20,7 @@ const termsHTML = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>First
|
|
|
20
20
|
const termsWithOtherCharsetHTML = '<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1251"><title>TOS на първия доставчик</title></head><body><h1>Условия за ползване</h1><p>Dapibus quis diam sagittis</p></body></html>';
|
|
21
21
|
|
|
22
22
|
describe('Fetcher', function () {
|
|
23
|
-
this.timeout(
|
|
23
|
+
this.timeout(60000);
|
|
24
24
|
|
|
25
25
|
before(launchHeadlessBrowser);
|
|
26
26
|
|