@adobe/spacecat-shared-tokowaka-client 1.17.3 → 1.18.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/index.js +450 -236
- package/src/utils/metaconfig-utils.js +52 -0
- package/src/utils/pattern-utils.js +51 -0
- package/src/utils/suggestion-utils.js +174 -0
- package/test/index.test.js +1575 -229
- package/test/utils/metaconfig-utils.test.js +69 -0
- package/test/utils/pattern-utils.test.js +119 -0
- package/test/utils/suggestion-utils.test.js +43 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { expect } from 'chai';
|
|
14
|
+
import {
|
|
15
|
+
addPatternsToMetaconfig,
|
|
16
|
+
removePatternFromMetaconfig,
|
|
17
|
+
} from '../../src/utils/metaconfig-utils.js';
|
|
18
|
+
|
|
19
|
+
describe('metaconfig-utils', () => {
|
|
20
|
+
describe('addPatternsToMetaconfig', () => {
|
|
21
|
+
it('adds patterns to an empty allowList', () => {
|
|
22
|
+
const metaconfig = {};
|
|
23
|
+
const changed = addPatternsToMetaconfig(metaconfig, ['/products/*']);
|
|
24
|
+
expect(changed).to.be.true;
|
|
25
|
+
expect(metaconfig.prerender.allowList).to.deep.equal(['/products/*']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('appends patterns to an existing allowList', () => {
|
|
29
|
+
const metaconfig = { prerender: { allowList: ['/blog/*'] } };
|
|
30
|
+
addPatternsToMetaconfig(metaconfig, ['/products/*']);
|
|
31
|
+
expect(metaconfig.prerender.allowList).to.deep.equal(['/blog/*', '/products/*']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('deduplicates patterns already present', () => {
|
|
35
|
+
const metaconfig = { prerender: { allowList: ['/blog/*'] } };
|
|
36
|
+
const changed = addPatternsToMetaconfig(metaconfig, ['/blog/*']);
|
|
37
|
+
expect(changed).to.be.false;
|
|
38
|
+
expect(metaconfig.prerender.allowList).to.deep.equal(['/blog/*']);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('removePatternFromMetaconfig', () => {
|
|
43
|
+
it('removes a pattern from the allowList', () => {
|
|
44
|
+
const metaconfig = { prerender: { allowList: ['/blog/*', '/products/*'] } };
|
|
45
|
+
const changed = removePatternFromMetaconfig(metaconfig, '/blog/*');
|
|
46
|
+
expect(changed).to.be.true;
|
|
47
|
+
expect(metaconfig.prerender.allowList).to.deep.equal(['/products/*']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false when the pattern is not in the allowList', () => {
|
|
51
|
+
const metaconfig = { prerender: { allowList: ['/blog/*'] } };
|
|
52
|
+
const changed = removePatternFromMetaconfig(metaconfig, '/products/*');
|
|
53
|
+
expect(changed).to.be.false;
|
|
54
|
+
expect(metaconfig.prerender.allowList).to.deep.equal(['/blog/*']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('deletes metaconfig.prerender entirely when allowList becomes empty', () => {
|
|
58
|
+
const metaconfig = { prerender: { allowList: ['/blog/*'] } };
|
|
59
|
+
removePatternFromMetaconfig(metaconfig, '/blog/*');
|
|
60
|
+
expect(metaconfig.prerender).to.be.undefined;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns false when prerender is absent', () => {
|
|
64
|
+
const metaconfig = {};
|
|
65
|
+
const changed = removePatternFromMetaconfig(metaconfig, '/blog/*');
|
|
66
|
+
expect(changed).to.be.false;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { expect } from 'chai';
|
|
14
|
+
import { buildUrlMatcher } from '../../src/utils/pattern-utils.js';
|
|
15
|
+
|
|
16
|
+
describe('pattern-utils', () => {
|
|
17
|
+
describe('buildUrlMatcher', () => {
|
|
18
|
+
describe('invalid inputs', () => {
|
|
19
|
+
it('returns null for null', () => {
|
|
20
|
+
expect(buildUrlMatcher(null)).to.be.null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns null for undefined', () => {
|
|
24
|
+
expect(buildUrlMatcher(undefined)).to.be.null;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns null for a number', () => {
|
|
28
|
+
expect(buildUrlMatcher(42)).to.be.null;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns null for an empty string', () => {
|
|
32
|
+
expect(buildUrlMatcher('')).to.be.null;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('/* — domain-wide pattern', () => {
|
|
37
|
+
let match;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
match = buildUrlMatcher('/*');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('matches the root path', () => {
|
|
43
|
+
expect(match('https://example.com/')).to.be.true;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('matches any sub-path', () => {
|
|
47
|
+
expect(match('https://example.com/products/item')).to.be.true;
|
|
48
|
+
expect(match('https://example.com/a/b/c')).to.be.true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns false for an invalid URL', () => {
|
|
52
|
+
expect(match('not-a-url')).to.be.false;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('/path/* — prefix patterns', () => {
|
|
57
|
+
let match;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
match = buildUrlMatcher('/products/*');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('matches a direct child path', () => {
|
|
63
|
+
expect(match('https://example.com/products/item')).to.be.true;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('matches a nested child path', () => {
|
|
67
|
+
expect(match('https://example.com/products/sub/item')).to.be.true;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('matches the exact prefix path without a trailing slash', () => {
|
|
71
|
+
expect(match('https://example.com/products')).to.be.true;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('matches the exact prefix path with a trailing slash', () => {
|
|
75
|
+
expect(match('https://example.com/products/')).to.be.true;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not over-match adjacent path segments (boundary safety)', () => {
|
|
79
|
+
expect(match('https://example.com/productsabc')).to.be.false;
|
|
80
|
+
expect(match('https://example.com/products-new')).to.be.false;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not match an unrelated path', () => {
|
|
84
|
+
expect(match('https://example.com/blog/post')).to.be.false;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns false for an invalid URL', () => {
|
|
88
|
+
expect(match('not-a-url')).to.be.false;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('regex patterns', () => {
|
|
93
|
+
it('matches against the full URL string (not pathname only) for backward compatibility', () => {
|
|
94
|
+
// Regex patterns are tested against the full URL (e.g. 'https://example.com/page').
|
|
95
|
+
// A pathname-only regex like '^/blog' will never match because the URL string starts with
|
|
96
|
+
// a scheme. Use the '/*' suffix form for pathname-only prefix matching.
|
|
97
|
+
const fullUrlMatch = buildUrlMatcher('^https://example\\.com/blog');
|
|
98
|
+
expect(fullUrlMatch('https://example.com/blog/post')).to.be.true;
|
|
99
|
+
expect(fullUrlMatch('https://example.com/news/post')).to.be.false;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('matches a full-URL regex with wildcard', () => {
|
|
103
|
+
const match = buildUrlMatcher('^https://example\\.com/.*');
|
|
104
|
+
expect(match('https://example.com/products/item')).to.be.true;
|
|
105
|
+
expect(match('https://other.com/products/item')).to.be.false;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('matches a simple substring against the full URL', () => {
|
|
109
|
+
const match = buildUrlMatcher('/products');
|
|
110
|
+
expect(match('https://example.com/products/item')).to.be.true;
|
|
111
|
+
expect(match('https://example.com/blog')).to.be.false;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns null for an invalid regex pattern', () => {
|
|
115
|
+
expect(buildUrlMatcher('[invalid')).to.be.null;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { expect } from 'chai';
|
|
14
|
-
import
|
|
14
|
+
import sinon from 'sinon';
|
|
15
|
+
import { groupSuggestionsByUrlPath, filterEligibleSuggestions, saveSuggestions } from '../../src/utils/suggestion-utils.js';
|
|
15
16
|
|
|
16
17
|
describe('Suggestion Utils', () => {
|
|
17
18
|
describe('groupSuggestionsByUrlPath', () => {
|
|
@@ -182,4 +183,45 @@ describe('Suggestion Utils', () => {
|
|
|
182
183
|
expect(result.ineligible[0].reason).to.equal('Suggestion cannot be deployed');
|
|
183
184
|
});
|
|
184
185
|
});
|
|
186
|
+
|
|
187
|
+
describe('saveSuggestions', () => {
|
|
188
|
+
it('should no-op for empty array', async () => {
|
|
189
|
+
const dataAccess = { Suggestion: { saveMany: sinon.stub() } };
|
|
190
|
+
await saveSuggestions(dataAccess, []);
|
|
191
|
+
expect(dataAccess.Suggestion.saveMany.called).to.be.false;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should use saveMany for <= 1700 suggestions', async () => {
|
|
195
|
+
const dataAccess = { Suggestion: { saveMany: sinon.stub().resolves() } };
|
|
196
|
+
const items = [{ save: sinon.stub().resolves() }];
|
|
197
|
+
await saveSuggestions(dataAccess, items);
|
|
198
|
+
expect(dataAccess.Suggestion.saveMany.calledOnce).to.be.true;
|
|
199
|
+
expect(items[0].save.called).to.be.false;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should use Promise.allSettled for > 1700 suggestions', async () => {
|
|
203
|
+
const dataAccess = { Suggestion: { saveMany: sinon.stub() } };
|
|
204
|
+
const items = Array.from({ length: 1701 }, () => ({ save: sinon.stub().resolves() }));
|
|
205
|
+
await saveSuggestions(dataAccess, items);
|
|
206
|
+
expect(dataAccess.Suggestion.saveMany.called).to.be.false;
|
|
207
|
+
expect(items[0].save.calledOnce).to.be.true;
|
|
208
|
+
expect(items[1700].save.calledOnce).to.be.true;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should throw when Promise.allSettled has failures', async () => {
|
|
212
|
+
const dataAccess = { Suggestion: { saveMany: sinon.stub() } };
|
|
213
|
+
const items = Array.from({ length: 1701 }, (_, i) => ({
|
|
214
|
+
save: i === 0
|
|
215
|
+
? sinon.stub().rejects(new Error('DB error'))
|
|
216
|
+
: sinon.stub().resolves(),
|
|
217
|
+
}));
|
|
218
|
+
try {
|
|
219
|
+
await saveSuggestions(dataAccess, items);
|
|
220
|
+
expect.fail('should have thrown');
|
|
221
|
+
} catch (error) {
|
|
222
|
+
expect(error.message).to.include('1 of 1701 suggestions failed');
|
|
223
|
+
expect(error.message).to.include('DB error');
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
185
227
|
});
|