@adobe/spacecat-shared-tokowaka-client 1.1.0 → 1.2.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.
@@ -0,0 +1,140 @@
1
+ /*
2
+ * Copyright 2025 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
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import {
17
+ normalizePath,
18
+ getHostName,
19
+ base64UrlEncode,
20
+ getTokowakaConfigS3Path,
21
+ getTokowakaMetaconfigS3Path,
22
+ } from '../../src/utils/s3-utils.js';
23
+
24
+ describe('S3 Utils', () => {
25
+ describe('normalizePath', () => {
26
+ it('should add leading slash if missing', () => {
27
+ const result = normalizePath('page1');
28
+ expect(result).to.equal('/page1');
29
+ });
30
+
31
+ it('should keep single slash', () => {
32
+ const result = normalizePath('/');
33
+ expect(result).to.equal('/');
34
+ });
35
+
36
+ it('should remove trailing slash except for root', () => {
37
+ const result = normalizePath('/page1/');
38
+ expect(result).to.equal('/page1');
39
+ });
40
+
41
+ it('should handle path with leading slash', () => {
42
+ const result = normalizePath('/page1');
43
+ expect(result).to.equal('/page1');
44
+ });
45
+ });
46
+
47
+ describe('getHostName', () => {
48
+ it('should extract hostname and remove www', () => {
49
+ const url = new URL('https://www.example.com/page');
50
+ const logger = { error: () => {} };
51
+ const result = getHostName(url, logger);
52
+ expect(result).to.equal('example.com');
53
+ });
54
+
55
+ it('should handle hostname without www', () => {
56
+ const url = new URL('https://example.com/page');
57
+ const logger = { error: () => {} };
58
+ const result = getHostName(url, logger);
59
+ expect(result).to.equal('example.com');
60
+ });
61
+
62
+ it('should throw error on invalid URL', () => {
63
+ const logger = { error: () => {} };
64
+ const invalidUrl = { hostname: null };
65
+
66
+ try {
67
+ getHostName(invalidUrl, logger);
68
+ expect.fail('Should have thrown error');
69
+ } catch (error) {
70
+ expect(error.message).to.include('Error extracting host name');
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('getTokowakaConfigS3Path', () => {
76
+ const logger = { error: () => {} };
77
+
78
+ it('should generate correct S3 path for deploy', () => {
79
+ const url = 'https://example.com/page1';
80
+ const result = getTokowakaConfigS3Path(url, logger, false);
81
+ expect(result).to.equal('opportunities/example.com/L3BhZ2Ux');
82
+ });
83
+
84
+ it('should generate correct S3 path for preview', () => {
85
+ const url = 'https://example.com/page1';
86
+ const result = getTokowakaConfigS3Path(url, logger, true);
87
+ expect(result).to.equal('preview/opportunities/example.com/L3BhZ2Ux');
88
+ });
89
+
90
+ it('should throw error on invalid URL', () => {
91
+ try {
92
+ getTokowakaConfigS3Path('not-a-valid-url', logger, false);
93
+ expect.fail('Should have thrown error');
94
+ } catch (error) {
95
+ expect(error.message).to.include('Failed to generate S3 path');
96
+ }
97
+ });
98
+ });
99
+
100
+ describe('getTokowakaMetaconfigS3Path', () => {
101
+ it('should generate correct metaconfig S3 path for deploy', () => {
102
+ const url = 'https://example.com/page1';
103
+ const logger = { error: () => {} };
104
+ const result = getTokowakaMetaconfigS3Path(url, logger, false);
105
+ expect(result).to.equal('opportunities/example.com/config');
106
+ });
107
+
108
+ it('should generate correct metaconfig S3 path for preview', () => {
109
+ const url = 'https://example.com/page1';
110
+ const logger = { error: () => {} };
111
+ const result = getTokowakaMetaconfigS3Path(url, logger, true);
112
+ expect(result).to.equal('preview/opportunities/example.com/config');
113
+ });
114
+
115
+ it('should throw error on invalid URL', () => {
116
+ const logger = { error: () => {} };
117
+
118
+ try {
119
+ getTokowakaMetaconfigS3Path('not-a-valid-url', logger, false);
120
+ expect.fail('Should have thrown error');
121
+ } catch (error) {
122
+ expect(error.message).to.include('Failed to generate metaconfig S3 path');
123
+ }
124
+ });
125
+ });
126
+
127
+ describe('base64UrlEncode', () => {
128
+ it('should encode string to base64url', () => {
129
+ const result = base64UrlEncode('/page1');
130
+ expect(result).to.equal('L3BhZ2Ux');
131
+ });
132
+
133
+ it('should handle special characters', () => {
134
+ const result = base64UrlEncode('/page?query=1');
135
+ // Should replace + with - and / with _
136
+ expect(result).to.not.include('+');
137
+ expect(result).to.not.include('=');
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright 2025 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
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import { getEffectiveBaseURL } from '../../src/utils/site-utils.js';
17
+
18
+ describe('Site Utils', () => {
19
+ describe('getEffectiveBaseURL', () => {
20
+ it('should return site baseURL when no override', () => {
21
+ const site = {
22
+ getBaseURL: () => 'https://example.com',
23
+ getConfig: () => ({
24
+ getFetchConfig: () => ({}),
25
+ }),
26
+ };
27
+
28
+ const result = getEffectiveBaseURL(site);
29
+ expect(result).to.equal('https://example.com');
30
+ });
31
+
32
+ it('should return override baseURL when valid', () => {
33
+ const site = {
34
+ getBaseURL: () => 'https://example.com',
35
+ getConfig: () => ({
36
+ getFetchConfig: () => ({
37
+ overrideBaseURL: 'https://override.com',
38
+ }),
39
+ }),
40
+ };
41
+
42
+ const result = getEffectiveBaseURL(site);
43
+ expect(result).to.equal('https://override.com');
44
+ });
45
+
46
+ it('should return site baseURL when override is invalid', () => {
47
+ const site = {
48
+ getBaseURL: () => 'https://example.com',
49
+ getConfig: () => ({
50
+ getFetchConfig: () => ({
51
+ overrideBaseURL: 'not-a-valid-url',
52
+ }),
53
+ }),
54
+ };
55
+
56
+ const result = getEffectiveBaseURL(site);
57
+ expect(result).to.equal('https://example.com');
58
+ });
59
+
60
+ it('should handle missing getFetchConfig', () => {
61
+ const site = {
62
+ getBaseURL: () => 'https://example.com',
63
+ getConfig: () => ({}),
64
+ };
65
+
66
+ const result = getEffectiveBaseURL(site);
67
+ expect(result).to.equal('https://example.com');
68
+ });
69
+
70
+ it('should handle missing config', () => {
71
+ const site = {
72
+ getBaseURL: () => 'https://example.com',
73
+ getConfig: () => null,
74
+ };
75
+
76
+ const result = getEffectiveBaseURL(site);
77
+ expect(result).to.equal('https://example.com');
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,187 @@
1
+ /*
2
+ * Copyright 2025 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
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from '../../src/utils/suggestion-utils.js';
17
+
18
+ describe('Suggestion Utils', () => {
19
+ describe('groupSuggestionsByUrlPath', () => {
20
+ const log = {
21
+ warn: () => {},
22
+ debug: () => {},
23
+ };
24
+
25
+ it('should group suggestions by URL path', () => {
26
+ const suggestions = [
27
+ {
28
+ getId: () => 'sugg-1',
29
+ getData: () => ({ url: 'https://example.com/page1' }),
30
+ },
31
+ {
32
+ getId: () => 'sugg-2',
33
+ getData: () => ({ url: 'https://example.com/page1' }),
34
+ },
35
+ {
36
+ getId: () => 'sugg-3',
37
+ getData: () => ({ url: 'https://example.com/page2' }),
38
+ },
39
+ ];
40
+
41
+ const result = groupSuggestionsByUrlPath(suggestions, 'https://example.com', log);
42
+
43
+ expect(result).to.have.property('/page1');
44
+ expect(result).to.have.property('/page2');
45
+ expect(result['/page1']).to.have.lengthOf(2);
46
+ expect(result['/page2']).to.have.lengthOf(1);
47
+ });
48
+
49
+ it('should skip suggestions without URL', () => {
50
+ const suggestions = [
51
+ {
52
+ getId: () => 'sugg-1',
53
+ getData: () => ({}), // No URL
54
+ },
55
+ {
56
+ getId: () => 'sugg-2',
57
+ getData: () => ({ url: 'https://example.com/page1' }),
58
+ },
59
+ ];
60
+
61
+ const result = groupSuggestionsByUrlPath(suggestions, 'https://example.com', log);
62
+
63
+ expect(result).to.not.have.property('undefined');
64
+ expect(result).to.have.property('/page1');
65
+ expect(result['/page1']).to.have.lengthOf(1);
66
+ });
67
+
68
+ it('should skip suggestions with invalid URL', () => {
69
+ const suggestions = [
70
+ {
71
+ getId: () => 'sugg-1',
72
+ getData: () => ({ url: 'not-a-valid-url' }),
73
+ },
74
+ {
75
+ getId: () => 'sugg-2',
76
+ getData: () => ({ url: 'https://example.com/page1' }),
77
+ },
78
+ ];
79
+
80
+ const result = groupSuggestionsByUrlPath(suggestions, 'https://example.com', log);
81
+
82
+ expect(result).to.have.property('/page1');
83
+ expect(result['/page1']).to.have.lengthOf(1);
84
+ });
85
+
86
+ it('should skip suggestions with malformed URL that throws', () => {
87
+ const suggestions = [
88
+ {
89
+ getId: () => 'sugg-1',
90
+ getData: () => ({ url: 'http://[invalid' }), // Malformed URL that will throw
91
+ },
92
+ {
93
+ getId: () => 'sugg-2',
94
+ getData: () => ({ url: 'https://example.com/page1' }),
95
+ },
96
+ ];
97
+
98
+ const result = groupSuggestionsByUrlPath(suggestions, 'invalid-base', log);
99
+
100
+ // Should skip sugg-1 due to URL parsing error
101
+ expect(Object.keys(result)).to.have.lengthOf(0); // Both fail due to invalid base
102
+ });
103
+ });
104
+
105
+ describe('filterEligibleSuggestions', () => {
106
+ const mapper = {
107
+ canDeploy: (suggestion) => {
108
+ const data = suggestion.getData();
109
+ if (data.shouldDeploy) {
110
+ return { eligible: true };
111
+ }
112
+ return { eligible: false, reason: 'Not eligible' };
113
+ },
114
+ };
115
+
116
+ it('should filter eligible and ineligible suggestions', () => {
117
+ const suggestions = [
118
+ {
119
+ getId: () => 'sugg-1',
120
+ getData: () => ({ shouldDeploy: true }),
121
+ },
122
+ {
123
+ getId: () => 'sugg-2',
124
+ getData: () => ({ shouldDeploy: false }),
125
+ },
126
+ ];
127
+
128
+ const result = filterEligibleSuggestions(suggestions, mapper);
129
+
130
+ expect(result.eligible).to.have.lengthOf(1);
131
+ expect(result.ineligible).to.have.lengthOf(1);
132
+ expect(result.eligible[0].getId()).to.equal('sugg-1');
133
+ expect(result.ineligible[0].suggestion.getId()).to.equal('sugg-2');
134
+ expect(result.ineligible[0].reason).to.equal('Not eligible');
135
+ });
136
+
137
+ it('should handle all eligible suggestions', () => {
138
+ const suggestions = [
139
+ {
140
+ getId: () => 'sugg-1',
141
+ getData: () => ({ shouldDeploy: true }),
142
+ },
143
+ {
144
+ getId: () => 'sugg-2',
145
+ getData: () => ({ shouldDeploy: true }),
146
+ },
147
+ ];
148
+
149
+ const result = filterEligibleSuggestions(suggestions, mapper);
150
+
151
+ expect(result.eligible).to.have.lengthOf(2);
152
+ expect(result.ineligible).to.have.lengthOf(0);
153
+ });
154
+
155
+ it('should handle all ineligible suggestions', () => {
156
+ const suggestions = [
157
+ {
158
+ getId: () => 'sugg-1',
159
+ getData: () => ({ shouldDeploy: false }),
160
+ },
161
+ ];
162
+
163
+ const result = filterEligibleSuggestions(suggestions, mapper);
164
+
165
+ expect(result.eligible).to.have.lengthOf(0);
166
+ expect(result.ineligible).to.have.lengthOf(1);
167
+ });
168
+
169
+ it('should use default reason when eligibility.reason is empty', () => {
170
+ const mapperWithoutReason = {
171
+ canDeploy: () => ({ eligible: false }), // No reason provided
172
+ };
173
+
174
+ const suggestions = [
175
+ {
176
+ getId: () => 'sugg-1',
177
+ getData: () => ({}),
178
+ },
179
+ ];
180
+
181
+ const result = filterEligibleSuggestions(suggestions, mapperWithoutReason);
182
+
183
+ expect(result.ineligible).to.have.lengthOf(1);
184
+ expect(result.ineligible[0].reason).to.equal('Suggestion cannot be deployed');
185
+ });
186
+ });
187
+ });