@adobe/spacecat-shared-tokowaka-client 1.5.2 → 1.5.4
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 +14 -0
- package/package.json +1 -1
- package/src/index.js +9 -3
- package/src/utils/custom-html-utils.js +24 -8
- package/test/index.test.js +297 -8
- package/test/utils/html-utils.test.js +279 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.3...@adobe/spacecat-shared-tokowaka-client-v1.5.4) (2026-01-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* edge preview api headers handling ([#1276](https://github.com/adobe/spacecat-shared/issues/1276)) ([e2a7ba8](https://github.com/adobe/spacecat-shared/commit/e2a7ba88df77ee7e369d39e623966b5760ac9d12))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.3](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.2...@adobe/spacecat-shared-tokowaka-client-v1.5.3) (2026-01-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* update tokowaka config ([#1275](https://github.com/adobe/spacecat-shared/issues/1275)) ([06cedcd](https://github.com/adobe/spacecat-shared/commit/06cedcd3d6f5956d895f7dedb7579d2eefffe58e))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-tokowaka-client-v1.5.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.1...@adobe/spacecat-shared-tokowaka-client-v1.5.2) (2026-01-21)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -349,15 +349,21 @@ class TokowakaClient {
|
|
|
349
349
|
|
|
350
350
|
// dont override api keys
|
|
351
351
|
// if patches exist, they cannot reset to empty object
|
|
352
|
+
const hasForceFail = options.forceFail !== undefined
|
|
353
|
+
|| existingMetaconfig.forceFail !== undefined;
|
|
354
|
+
const forceFail = options.forceFail
|
|
355
|
+
?? existingMetaconfig.forceFail
|
|
356
|
+
?? false;
|
|
357
|
+
|
|
352
358
|
const metaconfig = {
|
|
353
359
|
siteId,
|
|
354
360
|
apiKeys: existingMetaconfig.apiKeys,
|
|
355
|
-
tokowakaEnabled: options.tokowakaEnabled ?? true,
|
|
356
|
-
enhancements: options.enhancements ?? true,
|
|
361
|
+
tokowakaEnabled: options.tokowakaEnabled ?? existingMetaconfig.tokowakaEnabled ?? true,
|
|
362
|
+
enhancements: options.enhancements ?? existingMetaconfig.enhancements ?? true,
|
|
357
363
|
patches: isNonEmptyObject(options.patches)
|
|
358
364
|
? options.patches
|
|
359
365
|
: (existingMetaconfig.patches ?? {}),
|
|
360
|
-
...(
|
|
366
|
+
...(hasForceFail && { forceFail }),
|
|
361
367
|
};
|
|
362
368
|
|
|
363
369
|
const s3Path = await this.uploadMetaconfig(url, metaconfig);
|
|
@@ -24,8 +24,11 @@ function sleep(ms) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Makes an HTTP request with retry logic
|
|
28
|
-
*
|
|
27
|
+
* Makes an HTTP request with retry logic for both original and optimized HTML.
|
|
28
|
+
* Header validation logic (same for both):
|
|
29
|
+
* - No proxy AND no cache header: Return response immediately (success)
|
|
30
|
+
* - Proxy header present BUT no cache header: Retry until cache header found
|
|
31
|
+
* - Cache header present (regardless of proxy): Return response (success)
|
|
29
32
|
* @param {string} url - URL to fetch
|
|
30
33
|
* @param {Object} options - Fetch options
|
|
31
34
|
* @param {number} maxRetries - Maximum number of retries
|
|
@@ -48,23 +51,35 @@ async function fetchWithRetry(url, options, maxRetries, retryDelayMs, log, fetch
|
|
|
48
51
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
// Check for
|
|
54
|
+
// Check for edge optimize headers
|
|
52
55
|
const cacheHeader = response.headers.get('x-edge-optimize-cache');
|
|
56
|
+
const proxyHeader = response.headers.get('x-edge-optimize-proxy');
|
|
57
|
+
|
|
58
|
+
log.debug(`Headers - cache: ${cacheHeader || 'none'}, proxy: ${proxyHeader || 'none'}`);
|
|
59
|
+
|
|
60
|
+
// Case 1: Cache header present (regardless of proxy) -> Success
|
|
53
61
|
if (cacheHeader) {
|
|
54
62
|
log.debug(`Cache header found (x-edge-optimize-cache: ${cacheHeader}), stopping retry logic`);
|
|
55
63
|
return response;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
//
|
|
66
|
+
// Case 2: No cache header AND no proxy header -> Success (return immediately)
|
|
67
|
+
if (!proxyHeader) {
|
|
68
|
+
log.debug('No edge optimize headers found, proceeding as successful flow');
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Case 3: Proxy header present BUT no cache header -> Retry until cache found
|
|
73
|
+
log.debug('Proxy header present without cache header, will retry...');
|
|
74
|
+
|
|
75
|
+
// If we haven't exhausted retries, continue
|
|
59
76
|
if (attempt < maxRetries + 1) {
|
|
60
|
-
log.debug(`No cache header found on attempt ${attempt}, will retry...`);
|
|
61
|
-
// Wait before retrying
|
|
62
77
|
log.debug(`Waiting ${retryDelayMs}ms before retry...`);
|
|
63
78
|
// eslint-disable-next-line no-await-in-loop
|
|
64
79
|
await sleep(retryDelayMs);
|
|
65
80
|
} else {
|
|
66
|
-
// Last attempt
|
|
67
|
-
log.error(`Max retries (${maxRetries}) exhausted
|
|
81
|
+
// Last attempt - throw error
|
|
82
|
+
log.error(`Max retries (${maxRetries}) exhausted. Proxy header present but cache header not found`);
|
|
68
83
|
throw new Error(`Cache header (x-edge-optimize-cache) not found after ${maxRetries} retries`);
|
|
69
84
|
}
|
|
70
85
|
} catch (error) {
|
|
@@ -145,6 +160,7 @@ export async function fetchHtmlWithWarmup(
|
|
|
145
160
|
'x-forwarded-host': forwardedHost,
|
|
146
161
|
'x-edge-optimize-api-key': apiKey,
|
|
147
162
|
'x-edge-optimize-url': urlPath,
|
|
163
|
+
'Accept-Encoding': 'identity', // Disable compression to avoid content-length: 0 issue
|
|
148
164
|
};
|
|
149
165
|
|
|
150
166
|
if (isOptimized) {
|
package/test/index.test.js
CHANGED
|
@@ -598,8 +598,9 @@ describe('TokowakaClient', () => {
|
|
|
598
598
|
expect(result).to.have.property('siteId', siteId);
|
|
599
599
|
expect(result).to.have.property('apiKeys');
|
|
600
600
|
expect(result.apiKeys).to.deep.equal(['existing-api-key-123']);
|
|
601
|
-
|
|
602
|
-
expect(result).to.have.property('
|
|
601
|
+
// Should preserve existing metaconfig values when options not provided
|
|
602
|
+
expect(result).to.have.property('tokowakaEnabled', false);
|
|
603
|
+
expect(result).to.have.property('enhancements', false);
|
|
603
604
|
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
604
605
|
expect(result).to.not.have.property('forceFail');
|
|
605
606
|
});
|
|
@@ -611,7 +612,7 @@ describe('TokowakaClient', () => {
|
|
|
611
612
|
const result = await client.updateMetaconfig(url, siteId, { tokowakaEnabled: false });
|
|
612
613
|
|
|
613
614
|
expect(result).to.have.property('tokowakaEnabled', false);
|
|
614
|
-
expect(result).to.have.property('enhancements',
|
|
615
|
+
expect(result).to.have.property('enhancements', false);
|
|
615
616
|
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
616
617
|
expect(result).to.not.have.property('forceFail');
|
|
617
618
|
});
|
|
@@ -623,8 +624,9 @@ describe('TokowakaClient', () => {
|
|
|
623
624
|
const result = await client.updateMetaconfig(url, siteId, { tokowakaEnabled: true });
|
|
624
625
|
|
|
625
626
|
expect(result).to.have.property('tokowakaEnabled', true);
|
|
626
|
-
expect(result).to.have.property('enhancements',
|
|
627
|
+
expect(result).to.have.property('enhancements', false);
|
|
627
628
|
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
629
|
+
expect(result).to.not.have.property('forceFail');
|
|
628
630
|
});
|
|
629
631
|
|
|
630
632
|
it('should update metaconfig with enhancements set to false', async () => {
|
|
@@ -633,9 +635,10 @@ describe('TokowakaClient', () => {
|
|
|
633
635
|
|
|
634
636
|
const result = await client.updateMetaconfig(url, siteId, { enhancements: false });
|
|
635
637
|
|
|
636
|
-
expect(result).to.have.property('tokowakaEnabled',
|
|
638
|
+
expect(result).to.have.property('tokowakaEnabled', false);
|
|
637
639
|
expect(result).to.have.property('enhancements', false);
|
|
638
640
|
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
641
|
+
expect(result).to.not.have.property('forceFail');
|
|
639
642
|
});
|
|
640
643
|
|
|
641
644
|
it('should update metaconfig with enhancements set to true explicitly', async () => {
|
|
@@ -644,9 +647,10 @@ describe('TokowakaClient', () => {
|
|
|
644
647
|
|
|
645
648
|
const result = await client.updateMetaconfig(url, siteId, { enhancements: true });
|
|
646
649
|
|
|
647
|
-
expect(result).to.have.property('tokowakaEnabled',
|
|
650
|
+
expect(result).to.have.property('tokowakaEnabled', false);
|
|
648
651
|
expect(result).to.have.property('enhancements', true);
|
|
649
652
|
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
653
|
+
expect(result).to.not.have.property('forceFail');
|
|
650
654
|
});
|
|
651
655
|
|
|
652
656
|
it('should override patches when non-empty patches object is provided', async () => {
|
|
@@ -708,13 +712,13 @@ describe('TokowakaClient', () => {
|
|
|
708
712
|
expect(result).to.have.property('forceFail', true);
|
|
709
713
|
});
|
|
710
714
|
|
|
711
|
-
it('should
|
|
715
|
+
it('should include forceFail when set to false', async () => {
|
|
712
716
|
const siteId = 'site-789';
|
|
713
717
|
const url = 'https://example.com';
|
|
714
718
|
|
|
715
719
|
const result = await client.updateMetaconfig(url, siteId, { forceFail: false });
|
|
716
720
|
|
|
717
|
-
expect(result).to.
|
|
721
|
+
expect(result).to.have.property('forceFail', false);
|
|
718
722
|
});
|
|
719
723
|
|
|
720
724
|
it('should not include forceFail when undefined', async () => {
|
|
@@ -726,6 +730,38 @@ describe('TokowakaClient', () => {
|
|
|
726
730
|
expect(result).to.not.have.property('forceFail');
|
|
727
731
|
});
|
|
728
732
|
|
|
733
|
+
it('should use forceFail as false when options.forceFail is null and existingMetaconfig has no forceFail', async () => {
|
|
734
|
+
const siteId = 'site-789';
|
|
735
|
+
const url = 'https://example.com';
|
|
736
|
+
|
|
737
|
+
const result = await client.updateMetaconfig(url, siteId, { forceFail: null });
|
|
738
|
+
|
|
739
|
+
expect(result).to.have.property('forceFail', false);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should preserve existingMetaconfig forceFail when options.forceFail is null', async () => {
|
|
743
|
+
const configWithForceFail = {
|
|
744
|
+
siteId: 'site-456',
|
|
745
|
+
apiKeys: ['existing-api-key-123'],
|
|
746
|
+
tokowakaEnabled: true,
|
|
747
|
+
enhancements: true,
|
|
748
|
+
patches: {},
|
|
749
|
+
forceFail: true,
|
|
750
|
+
};
|
|
751
|
+
s3Client.send.onFirstCall().resolves({
|
|
752
|
+
Body: {
|
|
753
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const siteId = 'site-789';
|
|
758
|
+
const url = 'https://example.com';
|
|
759
|
+
|
|
760
|
+
const result = await client.updateMetaconfig(url, siteId, { forceFail: null });
|
|
761
|
+
|
|
762
|
+
expect(result).to.have.property('forceFail', true);
|
|
763
|
+
});
|
|
764
|
+
|
|
729
765
|
it('should update metaconfig with multiple options', async () => {
|
|
730
766
|
const siteId = 'site-789';
|
|
731
767
|
const url = 'https://example.com';
|
|
@@ -855,6 +891,259 @@ describe('TokowakaClient', () => {
|
|
|
855
891
|
|
|
856
892
|
expect(result.patches).to.deep.equal(singlePatch);
|
|
857
893
|
});
|
|
894
|
+
|
|
895
|
+
it('should preserve existing patches when options.patches is null', async () => {
|
|
896
|
+
const siteId = 'site-789';
|
|
897
|
+
const url = 'https://example.com';
|
|
898
|
+
|
|
899
|
+
const result = await client.updateMetaconfig(url, siteId, { patches: null });
|
|
900
|
+
|
|
901
|
+
expect(result.patches).to.deep.equal({ 'existing-patch': 'value' });
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('should preserve tokowakaEnabled=true from existingMetaconfig when options not provided', async () => {
|
|
905
|
+
const configWithTokowakaEnabled = {
|
|
906
|
+
siteId: 'site-456',
|
|
907
|
+
apiKeys: ['existing-api-key-123'],
|
|
908
|
+
tokowakaEnabled: true,
|
|
909
|
+
enhancements: false,
|
|
910
|
+
patches: {},
|
|
911
|
+
};
|
|
912
|
+
s3Client.send.onFirstCall().resolves({
|
|
913
|
+
Body: {
|
|
914
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithTokowakaEnabled)),
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const siteId = 'site-789';
|
|
919
|
+
const url = 'https://example.com';
|
|
920
|
+
|
|
921
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
922
|
+
|
|
923
|
+
expect(result).to.have.property('tokowakaEnabled', true);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('should preserve enhancements=true from existingMetaconfig when options not provided', async () => {
|
|
927
|
+
const configWithEnhancements = {
|
|
928
|
+
siteId: 'site-456',
|
|
929
|
+
apiKeys: ['existing-api-key-123'],
|
|
930
|
+
tokowakaEnabled: false,
|
|
931
|
+
enhancements: true,
|
|
932
|
+
patches: {},
|
|
933
|
+
};
|
|
934
|
+
s3Client.send.onFirstCall().resolves({
|
|
935
|
+
Body: {
|
|
936
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithEnhancements)),
|
|
937
|
+
},
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
const siteId = 'site-789';
|
|
941
|
+
const url = 'https://example.com';
|
|
942
|
+
|
|
943
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
944
|
+
|
|
945
|
+
expect(result).to.have.property('enhancements', true);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should default tokowakaEnabled to true when not in existingMetaconfig or options', async () => {
|
|
949
|
+
const configWithoutTokowakaEnabled = {
|
|
950
|
+
siteId: 'site-456',
|
|
951
|
+
apiKeys: ['existing-api-key-123'],
|
|
952
|
+
enhancements: false,
|
|
953
|
+
patches: {},
|
|
954
|
+
};
|
|
955
|
+
s3Client.send.onFirstCall().resolves({
|
|
956
|
+
Body: {
|
|
957
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithoutTokowakaEnabled)),
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const siteId = 'site-789';
|
|
962
|
+
const url = 'https://example.com';
|
|
963
|
+
|
|
964
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
965
|
+
|
|
966
|
+
expect(result).to.have.property('tokowakaEnabled', true);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('should default enhancements to true when not in existingMetaconfig or options', async () => {
|
|
970
|
+
const configWithoutEnhancements = {
|
|
971
|
+
siteId: 'site-456',
|
|
972
|
+
apiKeys: ['existing-api-key-123'],
|
|
973
|
+
tokowakaEnabled: false,
|
|
974
|
+
patches: {},
|
|
975
|
+
};
|
|
976
|
+
s3Client.send.onFirstCall().resolves({
|
|
977
|
+
Body: {
|
|
978
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithoutEnhancements)),
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const siteId = 'site-789';
|
|
983
|
+
const url = 'https://example.com';
|
|
984
|
+
|
|
985
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
986
|
+
|
|
987
|
+
expect(result).to.have.property('enhancements', true);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should preserve forceFail=true from existingMetaconfig when options not provided', async () => {
|
|
991
|
+
const configWithForceFail = {
|
|
992
|
+
siteId: 'site-456',
|
|
993
|
+
apiKeys: ['existing-api-key-123'],
|
|
994
|
+
tokowakaEnabled: true,
|
|
995
|
+
enhancements: true,
|
|
996
|
+
patches: {},
|
|
997
|
+
forceFail: true,
|
|
998
|
+
};
|
|
999
|
+
s3Client.send.onFirstCall().resolves({
|
|
1000
|
+
Body: {
|
|
1001
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const siteId = 'site-789';
|
|
1006
|
+
const url = 'https://example.com';
|
|
1007
|
+
|
|
1008
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1009
|
+
|
|
1010
|
+
expect(result).to.have.property('forceFail', true);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('should override existingMetaconfig forceFail when explicitly set to false in options', async () => {
|
|
1014
|
+
const configWithForceFail = {
|
|
1015
|
+
siteId: 'site-456',
|
|
1016
|
+
apiKeys: ['existing-api-key-123'],
|
|
1017
|
+
tokowakaEnabled: true,
|
|
1018
|
+
enhancements: true,
|
|
1019
|
+
patches: {},
|
|
1020
|
+
forceFail: true,
|
|
1021
|
+
};
|
|
1022
|
+
s3Client.send.onFirstCall().resolves({
|
|
1023
|
+
Body: {
|
|
1024
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const siteId = 'site-789';
|
|
1029
|
+
const url = 'https://example.com';
|
|
1030
|
+
|
|
1031
|
+
const result = await client.updateMetaconfig(url, siteId, { forceFail: false });
|
|
1032
|
+
|
|
1033
|
+
expect(result).to.have.property('forceFail', false);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('should override existingMetaconfig forceFail when explicitly set to true in options', async () => {
|
|
1037
|
+
const configWithoutForceFail = {
|
|
1038
|
+
siteId: 'site-456',
|
|
1039
|
+
apiKeys: ['existing-api-key-123'],
|
|
1040
|
+
tokowakaEnabled: true,
|
|
1041
|
+
enhancements: true,
|
|
1042
|
+
patches: {},
|
|
1043
|
+
forceFail: false,
|
|
1044
|
+
};
|
|
1045
|
+
s3Client.send.onFirstCall().resolves({
|
|
1046
|
+
Body: {
|
|
1047
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithoutForceFail)),
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const siteId = 'site-789';
|
|
1052
|
+
const url = 'https://example.com';
|
|
1053
|
+
|
|
1054
|
+
const result = await client.updateMetaconfig(url, siteId, { forceFail: true });
|
|
1055
|
+
|
|
1056
|
+
expect(result).to.have.property('forceFail', true);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should preserve forceFail=false from existingMetaconfig when options not provided', async () => {
|
|
1060
|
+
const configWithForceFail = {
|
|
1061
|
+
siteId: 'site-456',
|
|
1062
|
+
apiKeys: ['existing-api-key-123'],
|
|
1063
|
+
tokowakaEnabled: true,
|
|
1064
|
+
enhancements: true,
|
|
1065
|
+
patches: {},
|
|
1066
|
+
forceFail: false,
|
|
1067
|
+
};
|
|
1068
|
+
s3Client.send.onFirstCall().resolves({
|
|
1069
|
+
Body: {
|
|
1070
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const siteId = 'site-789';
|
|
1075
|
+
const url = 'https://example.com';
|
|
1076
|
+
|
|
1077
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1078
|
+
|
|
1079
|
+
expect(result).to.have.property('forceFail', false);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it('should override existingMetaconfig tokowakaEnabled=false when explicitly set to true', async () => {
|
|
1083
|
+
const siteId = 'site-789';
|
|
1084
|
+
const url = 'https://example.com';
|
|
1085
|
+
// existingMetaconfig has tokowakaEnabled: false
|
|
1086
|
+
|
|
1087
|
+
const result = await client.updateMetaconfig(url, siteId, { tokowakaEnabled: true });
|
|
1088
|
+
|
|
1089
|
+
expect(result).to.have.property('tokowakaEnabled', true);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('should override existingMetaconfig enhancements=false when explicitly set to true', async () => {
|
|
1093
|
+
const siteId = 'site-789';
|
|
1094
|
+
const url = 'https://example.com';
|
|
1095
|
+
// existingMetaconfig has enhancements: false
|
|
1096
|
+
|
|
1097
|
+
const result = await client.updateMetaconfig(url, siteId, { enhancements: true });
|
|
1098
|
+
|
|
1099
|
+
expect(result).to.have.property('enhancements', true);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it('should handle case where options.forceFail and existingMetaconfig.forceFail are both true', async () => {
|
|
1103
|
+
const configWithForceFail = {
|
|
1104
|
+
siteId: 'site-456',
|
|
1105
|
+
apiKeys: ['existing-api-key-123'],
|
|
1106
|
+
tokowakaEnabled: true,
|
|
1107
|
+
enhancements: true,
|
|
1108
|
+
patches: {},
|
|
1109
|
+
forceFail: true,
|
|
1110
|
+
};
|
|
1111
|
+
s3Client.send.onFirstCall().resolves({
|
|
1112
|
+
Body: {
|
|
1113
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
const siteId = 'site-789';
|
|
1118
|
+
const url = 'https://example.com';
|
|
1119
|
+
|
|
1120
|
+
const result = await client.updateMetaconfig(url, siteId, { forceFail: true });
|
|
1121
|
+
|
|
1122
|
+
expect(result).to.have.property('forceFail', true);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('should handle case where options.forceFail and existingMetaconfig.forceFail are both false', async () => {
|
|
1126
|
+
const configWithForceFail = {
|
|
1127
|
+
siteId: 'site-456',
|
|
1128
|
+
apiKeys: ['existing-api-key-123'],
|
|
1129
|
+
tokowakaEnabled: true,
|
|
1130
|
+
enhancements: true,
|
|
1131
|
+
patches: {},
|
|
1132
|
+
forceFail: false,
|
|
1133
|
+
};
|
|
1134
|
+
s3Client.send.onFirstCall().resolves({
|
|
1135
|
+
Body: {
|
|
1136
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithForceFail)),
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
const siteId = 'site-789';
|
|
1141
|
+
const url = 'https://example.com';
|
|
1142
|
+
|
|
1143
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1144
|
+
|
|
1145
|
+
expect(result).to.have.property('forceFail', false);
|
|
1146
|
+
});
|
|
858
1147
|
});
|
|
859
1148
|
|
|
860
1149
|
describe('uploadConfig', () => {
|
|
@@ -154,6 +154,150 @@ describe('HTML Utils', () => {
|
|
|
154
154
|
expect(actualUrl).to.equal('https://edge.example.com/page?tokowakaPreview=true');
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
it('should return immediately for optimized HTML when no headers present', async () => {
|
|
158
|
+
// Warmup succeeds
|
|
159
|
+
fetchStub.onCall(0).resolves({
|
|
160
|
+
ok: true,
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: 'OK',
|
|
163
|
+
headers: {
|
|
164
|
+
get: () => null,
|
|
165
|
+
},
|
|
166
|
+
text: async () => 'warmup',
|
|
167
|
+
});
|
|
168
|
+
// First actual call - no headers, should succeed
|
|
169
|
+
fetchStub.onCall(1).resolves({
|
|
170
|
+
ok: true,
|
|
171
|
+
status: 200,
|
|
172
|
+
statusText: 'OK',
|
|
173
|
+
headers: {
|
|
174
|
+
get: () => null,
|
|
175
|
+
},
|
|
176
|
+
text: async () => '<html>No headers</html>',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const html = await fetchHtmlWithWarmup(
|
|
180
|
+
'https://example.com/page',
|
|
181
|
+
'api-key',
|
|
182
|
+
'host',
|
|
183
|
+
'https://edge.example.com',
|
|
184
|
+
log,
|
|
185
|
+
true, // isOptimized
|
|
186
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(html).to.equal('<html>No headers</html>');
|
|
190
|
+
// Should succeed immediately (warmup + 1 attempt)
|
|
191
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should throw error for optimized HTML when proxy present but cache not found after retries', async () => {
|
|
195
|
+
// Warmup succeeds
|
|
196
|
+
fetchStub.onCall(0).resolves({
|
|
197
|
+
ok: true,
|
|
198
|
+
status: 200,
|
|
199
|
+
statusText: 'OK',
|
|
200
|
+
headers: {
|
|
201
|
+
get: () => null,
|
|
202
|
+
},
|
|
203
|
+
text: async () => 'warmup',
|
|
204
|
+
});
|
|
205
|
+
// All actual calls have proxy but no cache header
|
|
206
|
+
fetchStub.onCall(1).resolves({
|
|
207
|
+
ok: true,
|
|
208
|
+
status: 200,
|
|
209
|
+
statusText: 'OK',
|
|
210
|
+
headers: {
|
|
211
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
212
|
+
},
|
|
213
|
+
text: async () => '<html>Proxy only 1</html>',
|
|
214
|
+
});
|
|
215
|
+
fetchStub.onCall(2).resolves({
|
|
216
|
+
ok: true,
|
|
217
|
+
status: 200,
|
|
218
|
+
statusText: 'OK',
|
|
219
|
+
headers: {
|
|
220
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
221
|
+
},
|
|
222
|
+
text: async () => '<html>Proxy only 2</html>',
|
|
223
|
+
});
|
|
224
|
+
fetchStub.onCall(3).resolves({
|
|
225
|
+
ok: true,
|
|
226
|
+
status: 200,
|
|
227
|
+
statusText: 'OK',
|
|
228
|
+
headers: {
|
|
229
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
230
|
+
},
|
|
231
|
+
text: async () => '<html>Proxy only 3</html>',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await fetchHtmlWithWarmup(
|
|
236
|
+
'https://example.com/page',
|
|
237
|
+
'api-key',
|
|
238
|
+
'host',
|
|
239
|
+
'https://edge.example.com',
|
|
240
|
+
log,
|
|
241
|
+
true, // isOptimized
|
|
242
|
+
{ warmupDelayMs: 0, maxRetries: 2, retryDelayMs: 0 },
|
|
243
|
+
);
|
|
244
|
+
expect.fail('Should have thrown error');
|
|
245
|
+
} catch (error) {
|
|
246
|
+
expect(error.message).to.include('Failed to fetch optimized HTML');
|
|
247
|
+
expect(error.message).to.include('Cache header (x-edge-optimize-cache) not found after 2 retries');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Should have tried 3 times (initial + 2 retries) plus warmup
|
|
251
|
+
expect(fetchStub.callCount).to.equal(4);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should retry for optimized HTML when proxy present until cache found', async () => {
|
|
255
|
+
// Warmup succeeds
|
|
256
|
+
fetchStub.onCall(0).resolves({
|
|
257
|
+
ok: true,
|
|
258
|
+
status: 200,
|
|
259
|
+
statusText: 'OK',
|
|
260
|
+
headers: {
|
|
261
|
+
get: () => null,
|
|
262
|
+
},
|
|
263
|
+
text: async () => 'warmup',
|
|
264
|
+
});
|
|
265
|
+
// First call has only proxy header - should retry
|
|
266
|
+
fetchStub.onCall(1).resolves({
|
|
267
|
+
ok: true,
|
|
268
|
+
status: 200,
|
|
269
|
+
statusText: 'OK',
|
|
270
|
+
headers: {
|
|
271
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
272
|
+
},
|
|
273
|
+
text: async () => '<html>Proxy only</html>',
|
|
274
|
+
});
|
|
275
|
+
// Second call has cache header (proxy might still be there) - should succeed
|
|
276
|
+
fetchStub.onCall(2).resolves({
|
|
277
|
+
ok: true,
|
|
278
|
+
status: 200,
|
|
279
|
+
statusText: 'OK',
|
|
280
|
+
headers: {
|
|
281
|
+
get: (name) => (name === 'x-edge-optimize-cache' ? 'HIT' : null),
|
|
282
|
+
},
|
|
283
|
+
text: async () => '<html>Cached HTML</html>',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const html = await fetchHtmlWithWarmup(
|
|
287
|
+
'https://example.com/page',
|
|
288
|
+
'api-key',
|
|
289
|
+
'host',
|
|
290
|
+
'https://edge.example.com',
|
|
291
|
+
log,
|
|
292
|
+
true, // isOptimized
|
|
293
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(html).to.equal('<html>Cached HTML</html>');
|
|
297
|
+
// Should retry when only proxy present (warmup + 2 attempts)
|
|
298
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
299
|
+
});
|
|
300
|
+
|
|
157
301
|
it('should throw error when HTTP response is not ok', async () => {
|
|
158
302
|
// Warmup succeeds
|
|
159
303
|
fetchStub.onCall(0).resolves({
|
|
@@ -285,7 +429,7 @@ describe('HTML Utils', () => {
|
|
|
285
429
|
}
|
|
286
430
|
});
|
|
287
431
|
|
|
288
|
-
it('should
|
|
432
|
+
it('should return immediately when no edge optimize headers are present', async () => {
|
|
289
433
|
// Warmup succeeds
|
|
290
434
|
fetchStub.onCall(0).resolves({
|
|
291
435
|
ok: true,
|
|
@@ -296,7 +440,7 @@ describe('HTML Utils', () => {
|
|
|
296
440
|
},
|
|
297
441
|
text: async () => 'warmup',
|
|
298
442
|
});
|
|
299
|
-
// First actual call - no
|
|
443
|
+
// First actual call - no headers, should succeed immediately
|
|
300
444
|
fetchStub.onCall(1).resolves({
|
|
301
445
|
ok: true,
|
|
302
446
|
status: 200,
|
|
@@ -304,17 +448,58 @@ describe('HTML Utils', () => {
|
|
|
304
448
|
headers: {
|
|
305
449
|
get: () => null,
|
|
306
450
|
},
|
|
307
|
-
text: async () => '<html>No
|
|
451
|
+
text: async () => '<html>No headers</html>',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const html = await fetchHtmlWithWarmup(
|
|
455
|
+
'https://example.com/page',
|
|
456
|
+
'api-key',
|
|
457
|
+
'host',
|
|
458
|
+
'https://edge.example.com',
|
|
459
|
+
log,
|
|
460
|
+
false,
|
|
461
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(html).to.equal('<html>No headers</html>');
|
|
465
|
+
// Should succeed immediately without retry (warmup + 1 attempt)
|
|
466
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should retry when proxy header present without cache until cache is found', async () => {
|
|
470
|
+
// Warmup succeeds
|
|
471
|
+
fetchStub.onCall(0).resolves({
|
|
472
|
+
ok: true,
|
|
473
|
+
status: 200,
|
|
474
|
+
statusText: 'OK',
|
|
475
|
+
headers: {
|
|
476
|
+
get: () => null,
|
|
477
|
+
},
|
|
478
|
+
text: async () => 'warmup',
|
|
479
|
+
});
|
|
480
|
+
// First call has proxy header but no cache - should retry
|
|
481
|
+
fetchStub.onCall(1).resolves({
|
|
482
|
+
ok: true,
|
|
483
|
+
status: 200,
|
|
484
|
+
statusText: 'OK',
|
|
485
|
+
headers: {
|
|
486
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
487
|
+
},
|
|
488
|
+
text: async () => '<html>Proxy only</html>',
|
|
308
489
|
});
|
|
309
|
-
// Second
|
|
490
|
+
// Second call has both headers - should succeed
|
|
310
491
|
fetchStub.onCall(2).resolves({
|
|
311
492
|
ok: true,
|
|
312
493
|
status: 200,
|
|
313
494
|
statusText: 'OK',
|
|
314
495
|
headers: {
|
|
315
|
-
get: (name) =>
|
|
496
|
+
get: (name) => {
|
|
497
|
+
if (name === 'x-edge-optimize-cache') return 'HIT';
|
|
498
|
+
if (name === 'x-edge-optimize-proxy') return 'true';
|
|
499
|
+
return null;
|
|
500
|
+
},
|
|
316
501
|
},
|
|
317
|
-
text: async () => '<html>
|
|
502
|
+
text: async () => '<html>Both headers</html>',
|
|
318
503
|
});
|
|
319
504
|
|
|
320
505
|
const html = await fetchHtmlWithWarmup(
|
|
@@ -327,12 +512,12 @@ describe('HTML Utils', () => {
|
|
|
327
512
|
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
328
513
|
);
|
|
329
514
|
|
|
330
|
-
expect(html).to.equal('<html>
|
|
331
|
-
// Should
|
|
515
|
+
expect(html).to.equal('<html>Both headers</html>');
|
|
516
|
+
// Should retry when only proxy present (warmup + 2 attempts)
|
|
332
517
|
expect(fetchStub.callCount).to.equal(3);
|
|
333
518
|
});
|
|
334
519
|
|
|
335
|
-
it('should throw error when
|
|
520
|
+
it('should throw error when proxy header present but cache not found after max retries', async () => {
|
|
336
521
|
// Warmup succeeds
|
|
337
522
|
fetchStub.onCall(0).resolves({
|
|
338
523
|
ok: true,
|
|
@@ -343,33 +528,33 @@ describe('HTML Utils', () => {
|
|
|
343
528
|
},
|
|
344
529
|
text: async () => 'warmup',
|
|
345
530
|
});
|
|
346
|
-
// All actual calls
|
|
531
|
+
// All actual calls have proxy but no cache header
|
|
347
532
|
fetchStub.onCall(1).resolves({
|
|
348
533
|
ok: true,
|
|
349
534
|
status: 200,
|
|
350
535
|
statusText: 'OK',
|
|
351
536
|
headers: {
|
|
352
|
-
get: () => null,
|
|
537
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
353
538
|
},
|
|
354
|
-
text: async () => '<html>
|
|
539
|
+
text: async () => '<html>Proxy only 1</html>',
|
|
355
540
|
});
|
|
356
541
|
fetchStub.onCall(2).resolves({
|
|
357
542
|
ok: true,
|
|
358
543
|
status: 200,
|
|
359
544
|
statusText: 'OK',
|
|
360
545
|
headers: {
|
|
361
|
-
get: () => null,
|
|
546
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
362
547
|
},
|
|
363
|
-
text: async () => '<html>
|
|
548
|
+
text: async () => '<html>Proxy only 2</html>',
|
|
364
549
|
});
|
|
365
550
|
fetchStub.onCall(3).resolves({
|
|
366
551
|
ok: true,
|
|
367
552
|
status: 200,
|
|
368
553
|
statusText: 'OK',
|
|
369
554
|
headers: {
|
|
370
|
-
get: () => null,
|
|
555
|
+
get: (name) => (name === 'x-edge-optimize-proxy' ? 'true' : null),
|
|
371
556
|
},
|
|
372
|
-
text: async () => '<html>
|
|
557
|
+
text: async () => '<html>Proxy only 3</html>',
|
|
373
558
|
});
|
|
374
559
|
|
|
375
560
|
try {
|
|
@@ -428,6 +613,84 @@ describe('HTML Utils', () => {
|
|
|
428
613
|
// Should not retry if cache header found on first attempt
|
|
429
614
|
expect(fetchStub.callCount).to.equal(2); // warmup + 1 actual
|
|
430
615
|
});
|
|
616
|
+
|
|
617
|
+
it('should return immediately when cache header is present (with or without proxy)', async () => {
|
|
618
|
+
// Warmup succeeds
|
|
619
|
+
fetchStub.onCall(0).resolves({
|
|
620
|
+
ok: true,
|
|
621
|
+
status: 200,
|
|
622
|
+
statusText: 'OK',
|
|
623
|
+
headers: {
|
|
624
|
+
get: () => null,
|
|
625
|
+
},
|
|
626
|
+
text: async () => 'warmup',
|
|
627
|
+
});
|
|
628
|
+
// First actual call has cache header (proxy may or may not be present)
|
|
629
|
+
fetchStub.onCall(1).resolves({
|
|
630
|
+
ok: true,
|
|
631
|
+
status: 200,
|
|
632
|
+
statusText: 'OK',
|
|
633
|
+
headers: {
|
|
634
|
+
get: (name) => {
|
|
635
|
+
if (name === 'x-edge-optimize-cache') return 'HIT';
|
|
636
|
+
if (name === 'x-edge-optimize-proxy') return 'true';
|
|
637
|
+
return null;
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
text: async () => '<html>Cache header present</html>',
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const html = await fetchHtmlWithWarmup(
|
|
644
|
+
'https://example.com/page',
|
|
645
|
+
'api-key',
|
|
646
|
+
'host',
|
|
647
|
+
'https://edge.example.com',
|
|
648
|
+
log,
|
|
649
|
+
false,
|
|
650
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
expect(html).to.equal('<html>Cache header present</html>');
|
|
654
|
+
// Should succeed immediately when cache header present (warmup + 1 attempt)
|
|
655
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should succeed when only cache header is present (no proxy header)', async () => {
|
|
659
|
+
// Warmup succeeds
|
|
660
|
+
fetchStub.onCall(0).resolves({
|
|
661
|
+
ok: true,
|
|
662
|
+
status: 200,
|
|
663
|
+
statusText: 'OK',
|
|
664
|
+
headers: {
|
|
665
|
+
get: () => null,
|
|
666
|
+
},
|
|
667
|
+
text: async () => 'warmup',
|
|
668
|
+
});
|
|
669
|
+
// First actual call has only cache header
|
|
670
|
+
fetchStub.onCall(1).resolves({
|
|
671
|
+
ok: true,
|
|
672
|
+
status: 200,
|
|
673
|
+
statusText: 'OK',
|
|
674
|
+
headers: {
|
|
675
|
+
get: (name) => (name === 'x-edge-optimize-cache' ? 'HIT' : null),
|
|
676
|
+
},
|
|
677
|
+
text: async () => '<html>Cache only HTML</html>',
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const html = await fetchHtmlWithWarmup(
|
|
681
|
+
'https://example.com/page',
|
|
682
|
+
'api-key',
|
|
683
|
+
'host',
|
|
684
|
+
'https://edge.example.com',
|
|
685
|
+
log,
|
|
686
|
+
false,
|
|
687
|
+
{ warmupDelayMs: 0, maxRetries: 3, retryDelayMs: 0 },
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
expect(html).to.equal('<html>Cache only HTML</html>');
|
|
691
|
+
// Should succeed immediately with cache header only (warmup + 1 attempt)
|
|
692
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
693
|
+
});
|
|
431
694
|
});
|
|
432
695
|
|
|
433
696
|
describe('calculateForwardedHost', () => {
|