-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathfeatures.js
More file actions
161 lines (147 loc) · 7.36 KB
/
features.js
File metadata and controls
161 lines (147 loc) · 7.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { platformSupport } from '../src/features.js';
import path from 'path';
import { fileURLToPath } from 'url';
import { readFile } from 'fs/promises';
import * as glob from 'glob';
import { formatErrors } from '@duckduckgo/privacy-configuration/tests/schema-validation.js';
import ApiManipulation from '../src/features/api-manipulation.js';
// TODO: Ignore eslint redeclare as we're linting for esm and cjs
// eslint-disable-next-line no-redeclare
const __filename = fileURLToPath(import.meta.url);
// eslint-disable-next-line no-redeclare
const __dirname = path.dirname(__filename);
describe('Features definition', () => {
it('calls `webCompat` before `fingerPrintingScreenSize` https://app.asana.com/0/1177771139624306/1204944717262422/f', () => {
const arr = platformSupport.apple;
const webCompatIdx = arr.indexOf('webCompat');
const fpScreenSizeIdx = arr.indexOf('fingerprintingScreenSize');
expect(webCompatIdx).not.toBe(-1);
expect(fpScreenSizeIdx).not.toBe(-1);
expect(webCompatIdx).toBeLessThan(fpScreenSizeIdx);
});
});
describe('test-pages/*/config/*.json schema validation', () => {
let Ajv, schemaGenerator;
beforeAll(async () => {
Ajv = (await import('ajv')).default;
schemaGenerator = await import('ts-json-schema-generator');
});
// TODO make the config export all of this so it can be imported
function createGenerator() {
return schemaGenerator.createGenerator({
path: path.resolve(__dirname, '../../node_modules/@duckduckgo/privacy-configuration/schema/config.ts'),
});
}
function getSchema(schemaName) {
return createGenerator().createSchema(schemaName);
}
function createValidator(schemaName) {
const ajv = new Ajv({ allowUnionTypes: true });
return ajv.compile(getSchema(schemaName));
}
// Utility to ensure 'hash' exists on all features in the config
// Ideally we would not have these required in the config, it's pretty unnecessary for tests.
function ensureHashOnFeatures(config) {
if (config && typeof config === 'object' && config.features) {
for (const featureKey of Object.keys(config.features)) {
if (config.features[featureKey] && typeof config.features[featureKey] === 'object') {
if (!('hash' in config.features[featureKey])) {
config.features[featureKey].hash = '';
}
}
}
}
return config;
}
const configFiles = glob
.sync('../integration-test/test-pages/*/config/*.json', { cwd: __dirname })
.map((p) => path.resolve(__dirname, p));
// Legacy allowlist: skip schema validation for these known legacy files
// Some of these have expected invalid configs
const legacyAllowlist = [
// Favicon configs
path.resolve(__dirname, '../integration-test/test-pages/favicon/config/favicon-disabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/favicon/config/favicon-absent.json'),
path.resolve(__dirname, '../integration-test/test-pages/favicon/config/favicon-enabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/favicon/config/favicon-monitor-disabled.json'),
// Duckplayer configs
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/overlays-live.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/click-interceptions-disabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/disabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/overlays-drawer.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/overlays.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/thumbnail-overlays-disabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/video-alt-selectors.json'),
path.resolve(__dirname, '../integration-test/test-pages/duckplayer/config/video-overlays-disabled.json'),
// Message bridge configs
path.resolve(__dirname, '../integration-test/test-pages/message-bridge/config/message-bridge-enabled.json'),
path.resolve(__dirname, '../integration-test/test-pages/message-bridge/config/message-bridge-disabled.json'),
// Legacy conditionalChanges format (domain at root instead of condition.domain)
path.resolve(__dirname, '../integration-test/test-pages/ua-ch-brands/config/domain-brand-override-legacy.json'),
// Uses fireDetectionEvents which is not yet in the published schema
path.resolve(__dirname, '../integration-test/test-pages/web-interference-detection/config/youtube-detection-events.json'),
];
for (const configPath of configFiles) {
if (legacyAllowlist.includes(configPath)) {
xit(`LEGACY: skipped schema validation for ${path.relative(process.cwd(), configPath)}`, () => {});
continue;
}
it(`should match the CurrentGenericConfig schema: ${path.relative(process.cwd(), configPath)}`, async () => {
let config = JSON.parse(await readFile(configPath, 'utf-8'));
config = ensureHashOnFeatures(config);
const validate = createValidator('CurrentGenericConfig');
const valid = validate(config);
if (!valid) {
throw new Error(`Schema validation failed for ${configPath}: ` + formatErrors(validate.errors));
}
});
}
});
describe('ApiManipulation', () => {
let apiManipulation;
let dummyTarget;
beforeEach(() => {
apiManipulation = new ApiManipulation(
'apiManipulation',
{},
{},
{
bundledConfig: { features: { apiManipulation: { state: 'enabled', exceptions: [] } } },
site: { domain: 'test.com' },
platform: { version: '1.0.0' },
},
);
dummyTarget = {};
});
it('defines a new property if define: true is set and property does not exist', () => {
const change = {
type: 'descriptor',
getterValue: { type: 'string', value: 'defined!' },
define: true,
};
apiManipulation.wrapApiDescriptor(dummyTarget, 'definedByConfig', change);
expect(dummyTarget.definedByConfig).toBe('defined!');
});
it('does not define a property if define is not set and property does not exist', () => {
const change = {
type: 'descriptor',
getterValue: { type: 'string', value: 'should not exist' },
};
apiManipulation.wrapApiDescriptor(dummyTarget, 'notDefinedByConfig', change);
expect(dummyTarget.notDefinedByConfig).toBeUndefined();
});
it('wraps an existing property if present', () => {
Object.defineProperty(dummyTarget, 'hardwareConcurrency', {
get: () => 4,
configurable: true,
enumerable: true,
});
const change = {
type: 'descriptor',
getterValue: { type: 'number', value: 222 },
};
apiManipulation.wrapApiDescriptor(dummyTarget, 'hardwareConcurrency', change);
// The getter should now return 222
expect(dummyTarget.hardwareConcurrency).toBe(222);
});
});