@linkforty/core 1.44.1 → 1.44.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/README.md +20 -588
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/dist/index.config.test.d.ts +0 -2
- package/dist/index.config.test.d.ts.map +0 -1
- package/dist/index.config.test.js +0 -87
- package/dist/index.config.test.js.map +0 -1
- package/dist/lib/bot-detection.test.d.ts +0 -2
- package/dist/lib/bot-detection.test.d.ts.map +0 -1
- package/dist/lib/bot-detection.test.js +0 -65
- package/dist/lib/bot-detection.test.js.map +0 -1
- package/dist/lib/client-ip.test.d.ts +0 -7
- package/dist/lib/client-ip.test.d.ts.map +0 -1
- package/dist/lib/client-ip.test.js +0 -114
- package/dist/lib/client-ip.test.js.map +0 -1
- package/dist/lib/config.test.d.ts +0 -2
- package/dist/lib/config.test.d.ts.map +0 -1
- package/dist/lib/config.test.js +0 -126
- package/dist/lib/config.test.js.map +0 -1
- package/dist/lib/database.test.d.ts +0 -2
- package/dist/lib/database.test.d.ts.map +0 -1
- package/dist/lib/database.test.js +0 -180
- package/dist/lib/database.test.js.map +0 -1
- package/dist/lib/deep-link-payload.test.d.ts +0 -2
- package/dist/lib/deep-link-payload.test.d.ts.map +0 -1
- package/dist/lib/deep-link-payload.test.js +0 -202
- package/dist/lib/deep-link-payload.test.js.map +0 -1
- package/dist/lib/event-emitter.test.d.ts +0 -2
- package/dist/lib/event-emitter.test.d.ts.map +0 -1
- package/dist/lib/event-emitter.test.js +0 -162
- package/dist/lib/event-emitter.test.js.map +0 -1
- package/dist/lib/fingerprint.test.d.ts +0 -2
- package/dist/lib/fingerprint.test.d.ts.map +0 -1
- package/dist/lib/fingerprint.test.js +0 -750
- package/dist/lib/fingerprint.test.js.map +0 -1
- package/dist/lib/launchpad.test.d.ts +0 -2
- package/dist/lib/launchpad.test.d.ts.map +0 -1
- package/dist/lib/launchpad.test.js +0 -368
- package/dist/lib/launchpad.test.js.map +0 -1
- package/dist/lib/link-lookup.test.d.ts +0 -2
- package/dist/lib/link-lookup.test.d.ts.map +0 -1
- package/dist/lib/link-lookup.test.js +0 -61
- package/dist/lib/link-lookup.test.js.map +0 -1
- package/dist/lib/link-resolution-cache.test.d.ts +0 -2
- package/dist/lib/link-resolution-cache.test.d.ts.map +0 -1
- package/dist/lib/link-resolution-cache.test.js +0 -45
- package/dist/lib/link-resolution-cache.test.js.map +0 -1
- package/dist/lib/link-safety.test.d.ts +0 -2
- package/dist/lib/link-safety.test.d.ts.map +0 -1
- package/dist/lib/link-safety.test.js +0 -211
- package/dist/lib/link-safety.test.js.map +0 -1
- package/dist/lib/not-found-page.test.d.ts +0 -2
- package/dist/lib/not-found-page.test.d.ts.map +0 -1
- package/dist/lib/not-found-page.test.js +0 -151
- package/dist/lib/not-found-page.test.js.map +0 -1
- package/dist/lib/portable/cli.test.d.ts +0 -2
- package/dist/lib/portable/cli.test.d.ts.map +0 -1
- package/dist/lib/portable/cli.test.js +0 -98
- package/dist/lib/portable/cli.test.js.map +0 -1
- package/dist/lib/portable/portable.integration.test.d.ts +0 -2
- package/dist/lib/portable/portable.integration.test.d.ts.map +0 -1
- package/dist/lib/portable/portable.integration.test.js +0 -139
- package/dist/lib/portable/portable.integration.test.js.map +0 -1
- package/dist/lib/portable/registry.schema.test.d.ts +0 -2
- package/dist/lib/portable/registry.schema.test.d.ts.map +0 -1
- package/dist/lib/portable/registry.schema.test.js +0 -59
- package/dist/lib/portable/registry.schema.test.js.map +0 -1
- package/dist/lib/scanner-detection.test.d.ts +0 -2
- package/dist/lib/scanner-detection.test.d.ts.map +0 -1
- package/dist/lib/scanner-detection.test.js +0 -124
- package/dist/lib/scanner-detection.test.js.map +0 -1
- package/dist/lib/schema-guard.test.d.ts +0 -2
- package/dist/lib/schema-guard.test.d.ts.map +0 -1
- package/dist/lib/schema-guard.test.js +0 -83
- package/dist/lib/schema-guard.test.js.map +0 -1
- package/dist/lib/utils.test.d.ts +0 -2
- package/dist/lib/utils.test.d.ts.map +0 -1
- package/dist/lib/utils.test.js +0 -261
- package/dist/lib/utils.test.js.map +0 -1
- package/dist/lib/webhook.test.d.ts +0 -2
- package/dist/lib/webhook.test.d.ts.map +0 -1
- package/dist/lib/webhook.test.js +0 -340
- package/dist/lib/webhook.test.js.map +0 -1
- package/dist/routes/health.test.d.ts +0 -2
- package/dist/routes/health.test.d.ts.map +0 -1
- package/dist/routes/health.test.js +0 -82
- package/dist/routes/health.test.js.map +0 -1
- package/dist/routes/preview.test.d.ts +0 -2
- package/dist/routes/preview.test.d.ts.map +0 -1
- package/dist/routes/preview.test.js +0 -111
- package/dist/routes/preview.test.js.map +0 -1
- package/dist/routes/qr.test.d.ts +0 -2
- package/dist/routes/qr.test.d.ts.map +0 -1
- package/dist/routes/qr.test.js +0 -67
- package/dist/routes/qr.test.js.map +0 -1
- package/dist/routes/redirect.click-event.test.d.ts +0 -2
- package/dist/routes/redirect.click-event.test.d.ts.map +0 -1
- package/dist/routes/redirect.click-event.test.js +0 -85
- package/dist/routes/redirect.click-event.test.js.map +0 -1
- package/dist/routes/redirect.host-scope.integration.test.d.ts +0 -2
- package/dist/routes/redirect.host-scope.integration.test.d.ts.map +0 -1
- package/dist/routes/redirect.host-scope.integration.test.js +0 -105
- package/dist/routes/redirect.host-scope.integration.test.js.map +0 -1
- package/dist/routes/redirect.interstitial.test.d.ts +0 -2
- package/dist/routes/redirect.interstitial.test.d.ts.map +0 -1
- package/dist/routes/redirect.interstitial.test.js +0 -128
- package/dist/routes/redirect.interstitial.test.js.map +0 -1
- package/dist/routes/redirect.launchpad.test.d.ts +0 -2
- package/dist/routes/redirect.launchpad.test.d.ts.map +0 -1
- package/dist/routes/redirect.launchpad.test.js +0 -515
- package/dist/routes/redirect.launchpad.test.js.map +0 -1
- package/dist/routes/redirect.no-web-destination.test.d.ts +0 -2
- package/dist/routes/redirect.no-web-destination.test.d.ts.map +0 -1
- package/dist/routes/redirect.no-web-destination.test.js +0 -176
- package/dist/routes/redirect.no-web-destination.test.js.map +0 -1
- package/dist/routes/redirect.not-found.test.d.ts +0 -2
- package/dist/routes/redirect.not-found.test.d.ts.map +0 -1
- package/dist/routes/redirect.not-found.test.js +0 -280
- package/dist/routes/redirect.not-found.test.js.map +0 -1
- package/dist/routes/redirect.safety.test.d.ts +0 -2
- package/dist/routes/redirect.safety.test.d.ts.map +0 -1
- package/dist/routes/redirect.safety.test.js +0 -284
- package/dist/routes/redirect.safety.test.js.map +0 -1
- package/dist/routes/redirect.store-url.test.d.ts +0 -2
- package/dist/routes/redirect.store-url.test.d.ts.map +0 -1
- package/dist/routes/redirect.store-url.test.js +0 -239
- package/dist/routes/redirect.store-url.test.js.map +0 -1
- package/dist/routes/redirect.test.d.ts +0 -2
- package/dist/routes/redirect.test.d.ts.map +0 -1
- package/dist/routes/redirect.test.js +0 -158
- package/dist/routes/redirect.test.js.map +0 -1
- package/dist/routes/redirect.web-link.test.d.ts +0 -2
- package/dist/routes/redirect.web-link.test.d.ts.map +0 -1
- package/dist/routes/redirect.web-link.test.js +0 -138
- package/dist/routes/redirect.web-link.test.js.map +0 -1
- package/dist/routes/sdk-cache-bypass.test.d.ts +0 -2
- package/dist/routes/sdk-cache-bypass.test.d.ts.map +0 -1
- package/dist/routes/sdk-cache-bypass.test.js +0 -175
- package/dist/routes/sdk-cache-bypass.test.js.map +0 -1
- package/dist/routes/sdk.device-values.integration.test.d.ts +0 -2
- package/dist/routes/sdk.device-values.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.device-values.integration.test.js +0 -132
- package/dist/routes/sdk.device-values.integration.test.js.map +0 -1
- package/dist/routes/sdk.event.test.d.ts +0 -2
- package/dist/routes/sdk.event.test.d.ts.map +0 -1
- package/dist/routes/sdk.event.test.js +0 -284
- package/dist/routes/sdk.event.test.js.map +0 -1
- package/dist/routes/sdk.install-certainty.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-certainty.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-certainty.integration.test.js +0 -104
- package/dist/routes/sdk.install-certainty.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-live-links.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-live-links.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-live-links.integration.test.js +0 -92
- package/dist/routes/sdk.install-live-links.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-referrer.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-referrer.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-referrer.integration.test.js +0 -187
- package/dist/routes/sdk.install-referrer.integration.test.js.map +0 -1
- package/dist/routes/sdk.install-window-once.integration.test.d.ts +0 -2
- package/dist/routes/sdk.install-window-once.integration.test.d.ts.map +0 -1
- package/dist/routes/sdk.install-window-once.integration.test.js +0 -147
- package/dist/routes/sdk.install-window-once.integration.test.js.map +0 -1
- package/dist/routes/sdk.install.test.d.ts +0 -2
- package/dist/routes/sdk.install.test.d.ts.map +0 -1
- package/dist/routes/sdk.install.test.js +0 -99
- package/dist/routes/sdk.install.test.js.map +0 -1
- package/dist/routes/sdk.resolve-click.test.d.ts +0 -2
- package/dist/routes/sdk.resolve-click.test.d.ts.map +0 -1
- package/dist/routes/sdk.resolve-click.test.js +0 -79
- package/dist/routes/sdk.resolve-click.test.js.map +0 -1
- package/dist/routes/sdk.resolve-scope.test.d.ts +0 -2
- package/dist/routes/sdk.resolve-scope.test.d.ts.map +0 -1
- package/dist/routes/sdk.resolve-scope.test.js +0 -105
- package/dist/routes/sdk.resolve-scope.test.js.map +0 -1
package/dist/lib/utils.test.js
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateShortCode, parseUserAgent, buildRedirectUrl, detectDevice, getLocationFromIP, resolveClickUtms, extractLinkParams, } from './utils';
|
|
3
|
-
describe('generateShortCode', () => {
|
|
4
|
-
it('should generate a short code of default length 8', () => {
|
|
5
|
-
const code = generateShortCode();
|
|
6
|
-
expect(code).toHaveLength(8);
|
|
7
|
-
expect(typeof code).toBe('string');
|
|
8
|
-
});
|
|
9
|
-
it('should generate a short code of custom length', () => {
|
|
10
|
-
const code = generateShortCode(12);
|
|
11
|
-
expect(code).toHaveLength(12);
|
|
12
|
-
});
|
|
13
|
-
it('should generate unique codes', () => {
|
|
14
|
-
const code1 = generateShortCode();
|
|
15
|
-
const code2 = generateShortCode();
|
|
16
|
-
expect(code1).not.toBe(code2);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
describe('parseUserAgent', () => {
|
|
20
|
-
it('should parse Chrome on Windows user agent', () => {
|
|
21
|
-
const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
|
|
22
|
-
const result = parseUserAgent(ua);
|
|
23
|
-
expect(result.deviceType).toBe('desktop');
|
|
24
|
-
expect(result.platform).toBe('Windows');
|
|
25
|
-
expect(result.browser).toBe('Chrome');
|
|
26
|
-
});
|
|
27
|
-
it('should parse iPhone user agent', () => {
|
|
28
|
-
const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1';
|
|
29
|
-
const result = parseUserAgent(ua);
|
|
30
|
-
expect(result.deviceType).toBe('mobile');
|
|
31
|
-
expect(result.platform).toBe('iOS');
|
|
32
|
-
expect(result.browser).toBe('Mobile Safari');
|
|
33
|
-
});
|
|
34
|
-
it('should parse Android user agent', () => {
|
|
35
|
-
const ua = 'Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36';
|
|
36
|
-
const result = parseUserAgent(ua);
|
|
37
|
-
expect(result.deviceType).toBe('mobile');
|
|
38
|
-
expect(result.platform).toBe('Android');
|
|
39
|
-
expect(result.browser).toBe('Chrome');
|
|
40
|
-
});
|
|
41
|
-
it('should handle empty user agent', () => {
|
|
42
|
-
const result = parseUserAgent('');
|
|
43
|
-
expect(result.deviceType).toBe('desktop');
|
|
44
|
-
expect(result.platform).toBe('unknown');
|
|
45
|
-
expect(result.browser).toBe('unknown');
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('detectDevice', () => {
|
|
49
|
-
it('should detect iOS devices', () => {
|
|
50
|
-
expect(detectDevice('Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)')).toBe('ios');
|
|
51
|
-
expect(detectDevice('Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X)')).toBe('ios');
|
|
52
|
-
expect(detectDevice('Mozilla/5.0 (iPod touch; CPU iPhone OS 14_6 like Mac OS X)')).toBe('ios');
|
|
53
|
-
});
|
|
54
|
-
it('should detect Android devices', () => {
|
|
55
|
-
expect(detectDevice('Mozilla/5.0 (Linux; Android 11; Pixel 5)')).toBe('android');
|
|
56
|
-
expect(detectDevice('Mozilla/5.0 (Linux; Android 10; SM-G973F)')).toBe('android');
|
|
57
|
-
});
|
|
58
|
-
it('should default to web for desktop browsers', () => {
|
|
59
|
-
expect(detectDevice('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0')).toBe('web');
|
|
60
|
-
expect(detectDevice('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')).toBe('web');
|
|
61
|
-
});
|
|
62
|
-
it('should be case insensitive', () => {
|
|
63
|
-
expect(detectDevice('MOZILLA/5.0 (IPHONE; CPU IPHONE OS 14_6)')).toBe('ios');
|
|
64
|
-
expect(detectDevice('MOZILLA/5.0 (LINUX; ANDROID 11)')).toBe('android');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('buildRedirectUrl', () => {
|
|
68
|
-
it('should return original URL when no UTM parameters provided', () => {
|
|
69
|
-
const url = 'https://example.com/page';
|
|
70
|
-
const result = buildRedirectUrl(url);
|
|
71
|
-
expect(result).toBe(url);
|
|
72
|
-
});
|
|
73
|
-
it('should append UTM parameters to URL', () => {
|
|
74
|
-
const url = 'https://example.com/page';
|
|
75
|
-
const utmParams = {
|
|
76
|
-
source: 'newsletter',
|
|
77
|
-
medium: 'email',
|
|
78
|
-
campaign: 'summer-sale',
|
|
79
|
-
};
|
|
80
|
-
const result = buildRedirectUrl(url, utmParams);
|
|
81
|
-
expect(result).toContain('utm_source=newsletter');
|
|
82
|
-
expect(result).toContain('utm_medium=email');
|
|
83
|
-
expect(result).toContain('utm_campaign=summer-sale');
|
|
84
|
-
});
|
|
85
|
-
it('should preserve existing query parameters', () => {
|
|
86
|
-
const url = 'https://example.com/page?foo=bar';
|
|
87
|
-
const utmParams = { source: 'google' };
|
|
88
|
-
const result = buildRedirectUrl(url, utmParams);
|
|
89
|
-
expect(result).toContain('foo=bar');
|
|
90
|
-
expect(result).toContain('utm_source=google');
|
|
91
|
-
});
|
|
92
|
-
it('should skip UTM parameters with empty values', () => {
|
|
93
|
-
const url = 'https://example.com/page';
|
|
94
|
-
const utmParams = {
|
|
95
|
-
source: 'twitter',
|
|
96
|
-
medium: '',
|
|
97
|
-
campaign: 'promo',
|
|
98
|
-
};
|
|
99
|
-
const result = buildRedirectUrl(url, utmParams);
|
|
100
|
-
expect(result).toContain('utm_source=twitter');
|
|
101
|
-
expect(result).not.toContain('utm_medium');
|
|
102
|
-
expect(result).toContain('utm_campaign=promo');
|
|
103
|
-
});
|
|
104
|
-
it('should URL encode UTM parameter values', () => {
|
|
105
|
-
const url = 'https://example.com/page';
|
|
106
|
-
const utmParams = {
|
|
107
|
-
source: 'email newsletter',
|
|
108
|
-
campaign: 'summer sale 2024',
|
|
109
|
-
};
|
|
110
|
-
const result = buildRedirectUrl(url, utmParams);
|
|
111
|
-
expect(result).toContain('utm_source=email+newsletter');
|
|
112
|
-
expect(result).toContain('utm_campaign=summer+sale+2024');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
describe('getLocationFromIP', () => {
|
|
116
|
-
it('should return null values for invalid IP', () => {
|
|
117
|
-
const result = getLocationFromIP('invalid-ip');
|
|
118
|
-
expect(result.countryCode).toBeNull();
|
|
119
|
-
expect(result.countryName).toBeNull();
|
|
120
|
-
expect(result.city).toBeNull();
|
|
121
|
-
expect(result.latitude).toBeNull();
|
|
122
|
-
expect(result.longitude).toBeNull();
|
|
123
|
-
});
|
|
124
|
-
it('should return null values for private IP addresses', () => {
|
|
125
|
-
const result = getLocationFromIP('192.168.1.1');
|
|
126
|
-
expect(result.countryCode).toBeNull();
|
|
127
|
-
expect(result.countryName).toBeNull();
|
|
128
|
-
});
|
|
129
|
-
it('should parse public IP addresses', () => {
|
|
130
|
-
// Google DNS IP (known to be US-based)
|
|
131
|
-
const result = getLocationFromIP('8.8.8.8');
|
|
132
|
-
expect(result.countryCode).toBe('US');
|
|
133
|
-
expect(result.countryName).toBe('United States');
|
|
134
|
-
expect(result.latitude).toBeDefined();
|
|
135
|
-
expect(result.longitude).toBeDefined();
|
|
136
|
-
});
|
|
137
|
-
it('should handle countries not in the mapping', () => {
|
|
138
|
-
// This test might need adjustment based on actual geoip-lite behavior
|
|
139
|
-
// but demonstrates the fallback logic
|
|
140
|
-
const result = getLocationFromIP('8.8.8.8');
|
|
141
|
-
if (result.countryCode && !result.countryName) {
|
|
142
|
-
// Fallback to country code
|
|
143
|
-
expect(result.countryName).toBe(result.countryCode);
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
// Has a mapped name
|
|
147
|
-
expect(typeof result.countryName).toBe('string');
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
describe('resolveClickUtms', () => {
|
|
152
|
-
const CONFIG = { source: 'youtube', medium: 'social', campaign: 'time_video' };
|
|
153
|
-
it('uses inbound query-string values when the link has no configured UTMs', () => {
|
|
154
|
-
expect(resolveClickUtms({ utm_source: 'fb', utm_medium: 'cpc', utm_campaign: 'launch' }, undefined))
|
|
155
|
-
.toEqual({ utmSource: 'fb', utmMedium: 'cpc', utmCampaign: 'launch' });
|
|
156
|
-
});
|
|
157
|
-
it('falls back to the link configuration when nothing is inbound', () => {
|
|
158
|
-
expect(resolveClickUtms({}, CONFIG))
|
|
159
|
-
.toEqual({ utmSource: 'youtube', utmMedium: 'social', utmCampaign: 'time_video' });
|
|
160
|
-
});
|
|
161
|
-
it('lets inbound win over configuration on conflict', () => {
|
|
162
|
-
// An ad platform appending a per-ad campaign id must not be overwritten by
|
|
163
|
-
// the link's static label.
|
|
164
|
-
expect(resolveClickUtms({ utm_source: 'an', utm_campaign: '120252316702960562' }, { source: 'test', campaign: 'test' }))
|
|
165
|
-
.toMatchObject({ utmSource: 'an', utmCampaign: '120252316702960562' });
|
|
166
|
-
});
|
|
167
|
-
it('mixes the two sources per key rather than all-or-nothing', () => {
|
|
168
|
-
expect(resolveClickUtms({ utm_source: 'fb' }, CONFIG))
|
|
169
|
-
.toEqual({ utmSource: 'fb', utmMedium: 'social', utmCampaign: 'time_video' });
|
|
170
|
-
});
|
|
171
|
-
it('treats an empty inbound value as absent and falls through to configuration', () => {
|
|
172
|
-
expect(resolveClickUtms({ utm_source: '' }, CONFIG).utmSource).toBe('youtube');
|
|
173
|
-
});
|
|
174
|
-
it('treats a whitespace-only inbound value as absent', () => {
|
|
175
|
-
expect(resolveClickUtms({ utm_source: ' ' }, CONFIG).utmSource).toBe('youtube');
|
|
176
|
-
});
|
|
177
|
-
it('never returns an empty string when configuration holds a blank', () => {
|
|
178
|
-
// Real production shape: a link saved with campaign set to ''.
|
|
179
|
-
const result = resolveClickUtms({}, { source: 'clevertap', medium: 'email', campaign: '' });
|
|
180
|
-
expect(result.utmCampaign).toBeUndefined();
|
|
181
|
-
expect(result.utmSource).toBe('clevertap');
|
|
182
|
-
});
|
|
183
|
-
it('trims values so " ig" and "ig" do not split into two library entries', () => {
|
|
184
|
-
expect(resolveClickUtms({ utm_source: ' ig ' }, undefined).utmSource).toBe('ig');
|
|
185
|
-
expect(resolveClickUtms({}, { source: ' youtube ' }).utmSource).toBe('youtube');
|
|
186
|
-
});
|
|
187
|
-
it.each([
|
|
188
|
-
['null', null],
|
|
189
|
-
['an array', []],
|
|
190
|
-
['a JSON string', '{"source":"x"}'],
|
|
191
|
-
['undefined', undefined],
|
|
192
|
-
])('treats %s utm_parameters as absent', (_label, configured) => {
|
|
193
|
-
expect(resolveClickUtms({ utm_source: 'fb' }, configured))
|
|
194
|
-
.toEqual({ utmSource: 'fb', utmMedium: undefined, utmCampaign: undefined });
|
|
195
|
-
});
|
|
196
|
-
it('ignores a repeated query parameter arriving as an array', () => {
|
|
197
|
-
const query = { utm_source: ['a', 'b'] };
|
|
198
|
-
expect(resolveClickUtms(query, CONFIG).utmSource).toBe('youtube');
|
|
199
|
-
});
|
|
200
|
-
it('returns all-undefined when neither source supplies anything', () => {
|
|
201
|
-
expect(resolveClickUtms({}, {}))
|
|
202
|
-
.toEqual({ utmSource: undefined, utmMedium: undefined, utmCampaign: undefined });
|
|
203
|
-
expect(resolveClickUtms(undefined, undefined))
|
|
204
|
-
.toEqual({ utmSource: undefined, utmMedium: undefined, utmCampaign: undefined });
|
|
205
|
-
});
|
|
206
|
-
it('ignores content and term, which have no click_events columns', () => {
|
|
207
|
-
const result = resolveClickUtms({ utm_content: 'cta', utm_term: 'kw' }, {
|
|
208
|
-
source: 'frontend', content: 'aasa_footer_cta', term: 'kw',
|
|
209
|
-
});
|
|
210
|
-
expect(Object.keys(result).sort()).toEqual(['utmCampaign', 'utmMedium', 'utmSource']);
|
|
211
|
-
expect(result.utmSource).toBe('frontend');
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
describe('extractLinkParams', () => {
|
|
215
|
-
it('keeps ordinary parameters', () => {
|
|
216
|
-
expect(extractLinkParams({ slug: 'titanic', id: '42' })).toEqual({
|
|
217
|
-
slug: 'titanic',
|
|
218
|
-
id: '42',
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
it('drops utm_ parameters, which have their own columns', () => {
|
|
222
|
-
expect(extractLinkParams({ utm_source: 'ig', utm_campaign: 'spring', slug: 'titanic' })).toEqual({ slug: 'titanic' });
|
|
223
|
-
});
|
|
224
|
-
it('drops fp_ parameters, which feed the fingerprint', () => {
|
|
225
|
-
expect(extractLinkParams({ fp_tz: 'UTC', fp_sw: '390', slug: 'titanic' })).toEqual({
|
|
226
|
-
slug: 'titanic',
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
it('drops lf_click, the correlation id we set ourselves', () => {
|
|
230
|
-
expect(extractLinkParams({ lf_click: 'abc-123', slug: 'titanic' })).toEqual({
|
|
231
|
-
slug: 'titanic',
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
it('matches reserved prefixes case-insensitively', () => {
|
|
235
|
-
expect(extractLinkParams({ UTM_Source: 'ig', FP_TZ: 'UTC', LF_Click: 'x' })).toEqual({});
|
|
236
|
-
});
|
|
237
|
-
it('truncates a value longer than 256 characters', () => {
|
|
238
|
-
const result = extractLinkParams({ slug: 'a'.repeat(300) });
|
|
239
|
-
expect(result.slug).toHaveLength(256);
|
|
240
|
-
});
|
|
241
|
-
it('keeps a value of exactly 256 characters intact', () => {
|
|
242
|
-
const result = extractLinkParams({ slug: 'a'.repeat(256) });
|
|
243
|
-
expect(result.slug).toHaveLength(256);
|
|
244
|
-
});
|
|
245
|
-
it('stops after 16 parameters', () => {
|
|
246
|
-
const query = {};
|
|
247
|
-
for (let i = 0; i < 30; i++)
|
|
248
|
-
query[`k${i}`] = String(i);
|
|
249
|
-
expect(Object.keys(extractLinkParams(query))).toHaveLength(16);
|
|
250
|
-
});
|
|
251
|
-
it('skips empty and non-string values', () => {
|
|
252
|
-
expect(extractLinkParams({ empty: '', missing: undefined, repeated: ['a', 'b'], slug: 'titanic' })).toEqual({ slug: 'titanic' });
|
|
253
|
-
});
|
|
254
|
-
it('returns {} for an empty query, which is the ordinary case', () => {
|
|
255
|
-
expect(extractLinkParams({})).toEqual({});
|
|
256
|
-
});
|
|
257
|
-
it('returns {} rather than throwing when there is no query at all', () => {
|
|
258
|
-
expect(extractLinkParams(undefined)).toEqual({});
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
//# sourceMappingURL=utils.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../../src/lib/utils.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.test.d.ts","sourceRoot":"","sources":["../../src/lib/webhook.test.ts"],"names":[],"mappings":""}
|
package/dist/lib/webhook.test.js
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
vi.mock('./database.js', () => ({
|
|
3
|
-
db: {
|
|
4
|
-
query: vi.fn(),
|
|
5
|
-
},
|
|
6
|
-
}));
|
|
7
|
-
import { generateWebhookSignature, generateWebhookSecret, deliverWebhook, triggerWebhooks, findWebhooksForLink, setWebhookDeliveryLogger, } from './webhook.js';
|
|
8
|
-
import { db } from './database.js';
|
|
9
|
-
// Helper to build a Webhook fixture
|
|
10
|
-
function makeWebhook(overrides = {}) {
|
|
11
|
-
return {
|
|
12
|
-
id: 'wh-1',
|
|
13
|
-
user_id: 'user-1',
|
|
14
|
-
name: 'Test Webhook',
|
|
15
|
-
url: 'https://example.com/hook',
|
|
16
|
-
secret: 'test-secret',
|
|
17
|
-
events: ['click_event'],
|
|
18
|
-
is_active: true,
|
|
19
|
-
retry_count: 1,
|
|
20
|
-
timeout_ms: 5000,
|
|
21
|
-
headers: {},
|
|
22
|
-
created_at: '2024-01-01T00:00:00.000Z',
|
|
23
|
-
updated_at: '2024-01-01T00:00:00.000Z',
|
|
24
|
-
...overrides,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Helper to build a WebhookPayload fixture
|
|
28
|
-
function makePayload(overrides = {}) {
|
|
29
|
-
return {
|
|
30
|
-
event: 'click_event',
|
|
31
|
-
event_id: 'evt-1',
|
|
32
|
-
timestamp: '2024-01-01T00:00:00.000Z',
|
|
33
|
-
data: { id: 'data-1' },
|
|
34
|
-
...overrides,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
// Helper to make a minimal Response-like mock
|
|
38
|
-
function mockResponse(ok, status, statusText, body = '') {
|
|
39
|
-
return {
|
|
40
|
-
ok,
|
|
41
|
-
status,
|
|
42
|
-
statusText,
|
|
43
|
-
text: () => Promise.resolve(body),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
describe('generateWebhookSignature', () => {
|
|
47
|
-
it('returns a hex string', () => {
|
|
48
|
-
const sig = generateWebhookSignature('payload', 'secret');
|
|
49
|
-
expect(typeof sig).toBe('string');
|
|
50
|
-
expect(sig).toMatch(/^[0-9a-f]{64}$/);
|
|
51
|
-
});
|
|
52
|
-
it('is deterministic for the same inputs', () => {
|
|
53
|
-
const sig1 = generateWebhookSignature('hello', 'key');
|
|
54
|
-
const sig2 = generateWebhookSignature('hello', 'key');
|
|
55
|
-
expect(sig1).toBe(sig2);
|
|
56
|
-
});
|
|
57
|
-
it('produces different signatures for different payloads', () => {
|
|
58
|
-
const sig1 = generateWebhookSignature('payload-a', 'secret');
|
|
59
|
-
const sig2 = generateWebhookSignature('payload-b', 'secret');
|
|
60
|
-
expect(sig1).not.toBe(sig2);
|
|
61
|
-
});
|
|
62
|
-
it('produces different signatures for different secrets', () => {
|
|
63
|
-
const sig1 = generateWebhookSignature('payload', 'secret-a');
|
|
64
|
-
const sig2 = generateWebhookSignature('payload', 'secret-b');
|
|
65
|
-
expect(sig1).not.toBe(sig2);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
describe('generateWebhookSecret', () => {
|
|
69
|
-
it('returns a 64-character hex string (32 random bytes)', () => {
|
|
70
|
-
const secret = generateWebhookSecret();
|
|
71
|
-
expect(typeof secret).toBe('string');
|
|
72
|
-
expect(secret).toMatch(/^[0-9a-f]{64}$/);
|
|
73
|
-
});
|
|
74
|
-
it('returns unique values on each call', () => {
|
|
75
|
-
const s1 = generateWebhookSecret();
|
|
76
|
-
const s2 = generateWebhookSecret();
|
|
77
|
-
expect(s1).not.toBe(s2);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('deliverWebhook', () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.useFakeTimers();
|
|
83
|
-
});
|
|
84
|
-
afterEach(() => {
|
|
85
|
-
vi.restoreAllMocks();
|
|
86
|
-
vi.unstubAllGlobals();
|
|
87
|
-
vi.useRealTimers();
|
|
88
|
-
});
|
|
89
|
-
it('returns a success result on HTTP 200', async () => {
|
|
90
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK', 'received')));
|
|
91
|
-
const webhook = makeWebhook();
|
|
92
|
-
const payload = makePayload();
|
|
93
|
-
const result = await deliverWebhook(webhook, payload);
|
|
94
|
-
expect(result.success).toBe(true);
|
|
95
|
-
expect(result.webhookId).toBe(webhook.id);
|
|
96
|
-
expect(result.eventType).toBe(payload.event);
|
|
97
|
-
expect(result.eventId).toBe(payload.event_id);
|
|
98
|
-
expect(result.responseStatus).toBe(200);
|
|
99
|
-
expect(result.responseBody).toBe('received');
|
|
100
|
-
expect(result.attemptNumber).toBe(1);
|
|
101
|
-
expect(result.deliveredAt).toBeDefined();
|
|
102
|
-
expect(result.errorMessage).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
it('returns a failure result on HTTP 500', async () => {
|
|
105
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(false, 500, 'Internal Server Error', 'error body')));
|
|
106
|
-
// Use logDelivery to capture per-attempt details (the final return is a
|
|
107
|
-
// generic "Failed after N attempts" object once retries are exhausted).
|
|
108
|
-
const logDelivery = vi.fn().mockResolvedValue(undefined);
|
|
109
|
-
const webhook = makeWebhook({ retry_count: 1 });
|
|
110
|
-
const payload = makePayload();
|
|
111
|
-
const result = await deliverWebhook(webhook, payload, logDelivery);
|
|
112
|
-
// Per-attempt result captured via logDelivery
|
|
113
|
-
const attemptResult = logDelivery.mock.calls[0][0];
|
|
114
|
-
expect(attemptResult.responseStatus).toBe(500);
|
|
115
|
-
expect(attemptResult.errorMessage).toBe('HTTP 500: Internal Server Error');
|
|
116
|
-
expect(attemptResult.responseBody).toBe('error body');
|
|
117
|
-
// Final returned result after retries exhausted
|
|
118
|
-
expect(result.success).toBe(false);
|
|
119
|
-
expect(result.deliveredAt).toBeUndefined();
|
|
120
|
-
});
|
|
121
|
-
it('sets correct request headers', async () => {
|
|
122
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
123
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
124
|
-
const webhook = makeWebhook({ headers: { 'X-Custom': 'value' } });
|
|
125
|
-
const payload = makePayload();
|
|
126
|
-
await deliverWebhook(webhook, payload);
|
|
127
|
-
const [url, init] = mockFetch.mock.calls[0];
|
|
128
|
-
expect(url).toBe(webhook.url);
|
|
129
|
-
expect(init.method).toBe('POST');
|
|
130
|
-
const headers = init.headers;
|
|
131
|
-
expect(headers['Content-Type']).toBe('application/json');
|
|
132
|
-
expect(headers['X-LinkForty-Event']).toBe(payload.event);
|
|
133
|
-
expect(headers['X-LinkForty-Event-ID']).toBe(payload.event_id);
|
|
134
|
-
expect(headers['X-LinkForty-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/);
|
|
135
|
-
expect(headers['User-Agent']).toBe('LinkForty-Webhook/1.0');
|
|
136
|
-
expect(headers['X-Custom']).toBe('value');
|
|
137
|
-
});
|
|
138
|
-
it('sends the correct JSON body', async () => {
|
|
139
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
140
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
141
|
-
const webhook = makeWebhook();
|
|
142
|
-
const payload = makePayload();
|
|
143
|
-
await deliverWebhook(webhook, payload);
|
|
144
|
-
const [, init] = mockFetch.mock.calls[0];
|
|
145
|
-
expect(JSON.parse(init.body)).toEqual(payload);
|
|
146
|
-
});
|
|
147
|
-
it('truncates response body to 1000 characters', async () => {
|
|
148
|
-
const longBody = 'x'.repeat(2000);
|
|
149
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK', longBody)));
|
|
150
|
-
const result = await deliverWebhook(makeWebhook(), makePayload());
|
|
151
|
-
expect(result.responseBody).toHaveLength(1000);
|
|
152
|
-
});
|
|
153
|
-
it('retries on failure and succeeds on second attempt', async () => {
|
|
154
|
-
const mockFetch = vi.fn()
|
|
155
|
-
.mockResolvedValueOnce(mockResponse(false, 503, 'Service Unavailable'))
|
|
156
|
-
.mockResolvedValueOnce(mockResponse(true, 200, 'OK'));
|
|
157
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
158
|
-
const webhook = makeWebhook({ retry_count: 2 });
|
|
159
|
-
const payload = makePayload();
|
|
160
|
-
const promise = deliverWebhook(webhook, payload);
|
|
161
|
-
await vi.runAllTimersAsync();
|
|
162
|
-
const result = await promise;
|
|
163
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
expect(result.attemptNumber).toBe(2);
|
|
166
|
-
});
|
|
167
|
-
it('exhausts all retries and returns final failure', async () => {
|
|
168
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(false, 500, 'Error')));
|
|
169
|
-
const webhook = makeWebhook({ retry_count: 3 });
|
|
170
|
-
const payload = makePayload();
|
|
171
|
-
const promise = deliverWebhook(webhook, payload);
|
|
172
|
-
await vi.runAllTimersAsync();
|
|
173
|
-
const result = await promise;
|
|
174
|
-
expect(result.success).toBe(false);
|
|
175
|
-
expect(result.errorMessage).toBe('Failed after 3 attempts');
|
|
176
|
-
expect(result.attemptNumber).toBe(3);
|
|
177
|
-
});
|
|
178
|
-
it('calls logDelivery for each attempt', async () => {
|
|
179
|
-
const mockFetch = vi.fn()
|
|
180
|
-
.mockResolvedValueOnce(mockResponse(false, 500, 'Error'))
|
|
181
|
-
.mockResolvedValueOnce(mockResponse(true, 200, 'OK'));
|
|
182
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
183
|
-
const logDelivery = vi.fn().mockResolvedValue(undefined);
|
|
184
|
-
const webhook = makeWebhook({ retry_count: 2 });
|
|
185
|
-
const payload = makePayload();
|
|
186
|
-
const promise = deliverWebhook(webhook, payload, logDelivery);
|
|
187
|
-
await vi.runAllTimersAsync();
|
|
188
|
-
await promise;
|
|
189
|
-
expect(logDelivery).toHaveBeenCalledTimes(2);
|
|
190
|
-
expect(logDelivery.mock.calls[0][0].success).toBe(false);
|
|
191
|
-
expect(logDelivery.mock.calls[1][0].success).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
it('continues even if logDelivery throws', async () => {
|
|
194
|
-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK')));
|
|
195
|
-
const logDelivery = vi.fn().mockRejectedValue(new Error('log failed'));
|
|
196
|
-
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
197
|
-
const result = await deliverWebhook(makeWebhook(), makePayload(), logDelivery);
|
|
198
|
-
expect(result.success).toBe(true);
|
|
199
|
-
consoleSpy.mockRestore();
|
|
200
|
-
});
|
|
201
|
-
it('captures timeout error in logDelivery when fetch exceeds timeout_ms', async () => {
|
|
202
|
-
const abortError = new Error('The operation was aborted');
|
|
203
|
-
abortError.name = 'AbortError';
|
|
204
|
-
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(abortError));
|
|
205
|
-
const logDelivery = vi.fn().mockResolvedValue(undefined);
|
|
206
|
-
const webhook = makeWebhook({ timeout_ms: 1000, retry_count: 1 });
|
|
207
|
-
const result = await deliverWebhook(webhook, makePayload(), logDelivery);
|
|
208
|
-
// Per-attempt error is exposed via logDelivery
|
|
209
|
-
const attemptResult = logDelivery.mock.calls[0][0];
|
|
210
|
-
expect(attemptResult.success).toBe(false);
|
|
211
|
-
expect(attemptResult.errorMessage).toBe('Timeout after 1000ms');
|
|
212
|
-
// Final result reflects exhausted retries
|
|
213
|
-
expect(result.success).toBe(false);
|
|
214
|
-
});
|
|
215
|
-
it('captures network error message in logDelivery', async () => {
|
|
216
|
-
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED')));
|
|
217
|
-
const logDelivery = vi.fn().mockResolvedValue(undefined);
|
|
218
|
-
const webhook = makeWebhook({ retry_count: 1 });
|
|
219
|
-
const result = await deliverWebhook(webhook, makePayload(), logDelivery);
|
|
220
|
-
const attemptResult = logDelivery.mock.calls[0][0];
|
|
221
|
-
expect(attemptResult.success).toBe(false);
|
|
222
|
-
expect(attemptResult.errorMessage).toBe('ECONNREFUSED');
|
|
223
|
-
expect(result.success).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
describe('triggerWebhooks', () => {
|
|
227
|
-
beforeEach(() => {
|
|
228
|
-
vi.useFakeTimers();
|
|
229
|
-
});
|
|
230
|
-
afterEach(() => {
|
|
231
|
-
vi.restoreAllMocks();
|
|
232
|
-
vi.unstubAllGlobals();
|
|
233
|
-
vi.useRealTimers();
|
|
234
|
-
});
|
|
235
|
-
it('does nothing when no webhooks provided', async () => {
|
|
236
|
-
const mockFetch = vi.fn();
|
|
237
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
238
|
-
await triggerWebhooks([], 'click_event', 'evt-1', {});
|
|
239
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
240
|
-
});
|
|
241
|
-
it('skips inactive webhooks', async () => {
|
|
242
|
-
const mockFetch = vi.fn();
|
|
243
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
244
|
-
const inactive = makeWebhook({ is_active: false });
|
|
245
|
-
await triggerWebhooks([inactive], 'click_event', 'evt-1', {});
|
|
246
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
247
|
-
});
|
|
248
|
-
it('skips webhooks not subscribed to the event', async () => {
|
|
249
|
-
const mockFetch = vi.fn();
|
|
250
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
251
|
-
const webhook = makeWebhook({ events: ['install_event'] });
|
|
252
|
-
await triggerWebhooks([webhook], 'click_event', 'evt-1', {});
|
|
253
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
254
|
-
});
|
|
255
|
-
it('delivers to matching active webhooks', async () => {
|
|
256
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
257
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
258
|
-
const webhook = makeWebhook({ events: ['click_event', 'install_event'] });
|
|
259
|
-
triggerWebhooks([webhook], 'click_event', 'evt-1', { foo: 'bar' });
|
|
260
|
-
await vi.runAllTimersAsync();
|
|
261
|
-
expect(mockFetch).toHaveBeenCalledOnce();
|
|
262
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
263
|
-
expect(body.event).toBe('click_event');
|
|
264
|
-
expect(body.event_id).toBe('evt-1');
|
|
265
|
-
expect(body.data).toEqual({ foo: 'bar' });
|
|
266
|
-
});
|
|
267
|
-
it('delivers to multiple matching webhooks', async () => {
|
|
268
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
269
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
270
|
-
const wh1 = makeWebhook({ id: 'wh-1', url: 'https://a.com/hook' });
|
|
271
|
-
const wh2 = makeWebhook({ id: 'wh-2', url: 'https://b.com/hook' });
|
|
272
|
-
triggerWebhooks([wh1, wh2], 'click_event', 'evt-2', {});
|
|
273
|
-
await vi.runAllTimersAsync();
|
|
274
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
275
|
-
});
|
|
276
|
-
it('calls logDelivery with webhookId, result and the payload sent', async () => {
|
|
277
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
278
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
279
|
-
const logDelivery = vi.fn().mockResolvedValue(undefined);
|
|
280
|
-
const webhook = makeWebhook({ id: 'wh-log' });
|
|
281
|
-
triggerWebhooks([webhook], 'click_event', 'evt-3', { foo: 'bar' }, logDelivery);
|
|
282
|
-
await vi.runAllTimersAsync();
|
|
283
|
-
expect(logDelivery).toHaveBeenCalledWith('wh-log', expect.objectContaining({ success: true }), expect.objectContaining({ event: 'click_event', event_id: 'evt-3', data: { foo: 'bar' } }));
|
|
284
|
-
});
|
|
285
|
-
it('falls back to the logger set with setWebhookDeliveryLogger', async () => {
|
|
286
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
287
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
288
|
-
const logger = vi.fn().mockResolvedValue(undefined);
|
|
289
|
-
setWebhookDeliveryLogger(logger);
|
|
290
|
-
try {
|
|
291
|
-
triggerWebhooks([makeWebhook({ id: 'wh-default' })], 'click_event', 'evt-4', {});
|
|
292
|
-
await vi.runAllTimersAsync();
|
|
293
|
-
}
|
|
294
|
-
finally {
|
|
295
|
-
setWebhookDeliveryLogger(undefined);
|
|
296
|
-
}
|
|
297
|
-
expect(logger).toHaveBeenCalledWith('wh-default', expect.objectContaining({ success: true }), expect.any(Object));
|
|
298
|
-
});
|
|
299
|
-
it('does not log when no logger is set', async () => {
|
|
300
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
301
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
302
|
-
const logger = vi.fn().mockResolvedValue(undefined);
|
|
303
|
-
setWebhookDeliveryLogger(logger);
|
|
304
|
-
setWebhookDeliveryLogger(undefined);
|
|
305
|
-
triggerWebhooks([makeWebhook()], 'click_event', 'evt-5', {});
|
|
306
|
-
await vi.runAllTimersAsync();
|
|
307
|
-
expect(logger).not.toHaveBeenCalled();
|
|
308
|
-
});
|
|
309
|
-
it('includes the timestamp in the payload', async () => {
|
|
310
|
-
const mockFetch = vi.fn().mockResolvedValue(mockResponse(true, 200, 'OK'));
|
|
311
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
312
|
-
triggerWebhooks([makeWebhook()], 'click_event', 'evt-4', {});
|
|
313
|
-
await vi.runAllTimersAsync();
|
|
314
|
-
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
315
|
-
expect(body.timestamp).toBeDefined();
|
|
316
|
-
expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp);
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
describe('findWebhooksForLink', () => {
|
|
320
|
-
const mockDbQuery = db.query;
|
|
321
|
-
beforeEach(() => {
|
|
322
|
-
mockDbQuery.mockReset();
|
|
323
|
-
});
|
|
324
|
-
it('looks webhooks up through the link, by organization or else by user', async () => {
|
|
325
|
-
const rows = [makeWebhook({ id: 'wh-org', organization_id: 'org-1' })];
|
|
326
|
-
mockDbQuery.mockResolvedValueOnce({ rows });
|
|
327
|
-
const result = await findWebhooksForLink('link-1');
|
|
328
|
-
expect(result).toEqual(rows);
|
|
329
|
-
const [sql, params] = mockDbQuery.mock.calls[0];
|
|
330
|
-
expect(params).toEqual(['link-1']);
|
|
331
|
-
// An organization's webhook hears every link in the organization...
|
|
332
|
-
expect(sql).toMatch(/w\.organization_id IS NOT NULL AND w\.organization_id = l\.organization_id/);
|
|
333
|
-
// ...one without an organization only its own user's links...
|
|
334
|
-
expect(sql).toMatch(/w\.organization_id IS NULL AND w\.user_id = l\.user_id/);
|
|
335
|
-
// ...and an unowned one (no accounts) only unowned links.
|
|
336
|
-
expect(sql).toMatch(/w\.user_id IS NULL\s+AND l\.user_id IS NULL AND l\.organization_id IS NULL/);
|
|
337
|
-
expect(sql).toMatch(/w\.is_active = true/);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
//# sourceMappingURL=webhook.test.js.map
|