@tuturuuu/utils 0.0.3 → 0.6.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 +313 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
getRedis: vi.fn(),
|
|
5
|
+
redis: {
|
|
6
|
+
expire: vi.fn(),
|
|
7
|
+
get: vi.fn(),
|
|
8
|
+
incr: vi.fn(),
|
|
9
|
+
set: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('../../upstash-rest', () => ({
|
|
14
|
+
getUpstashRestRedisClient: () => mocks.getRedis(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
extractIPFromRequest,
|
|
19
|
+
recordMalformedAuthCookieEdge,
|
|
20
|
+
recordSuspiciousApiRequestEdge,
|
|
21
|
+
} from '../edge';
|
|
22
|
+
|
|
23
|
+
describe('abuse-protection edge', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
vi.setSystemTime(new Date());
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
mocks.getRedis.mockResolvedValue(mocks.redis);
|
|
29
|
+
mocks.redis.expire.mockResolvedValue(1);
|
|
30
|
+
mocks.redis.get.mockResolvedValue(0);
|
|
31
|
+
mocks.redis.incr.mockResolvedValue(1);
|
|
32
|
+
mocks.redis.set.mockResolvedValue('OK');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('extractIPFromRequest', () => {
|
|
40
|
+
it('prefers cf-connecting-ip over x-forwarded-for', () => {
|
|
41
|
+
const headers = new Headers();
|
|
42
|
+
headers.set('cf-connecting-ip', '203.0.113.10');
|
|
43
|
+
headers.set('x-forwarded-for', '198.51.100.10, 10.0.0.1');
|
|
44
|
+
|
|
45
|
+
expect(extractIPFromRequest(headers)).toBe('203.0.113.10');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to true-client-ip before generic proxy headers', () => {
|
|
49
|
+
const headers = new Headers();
|
|
50
|
+
headers.set('true-client-ip', '203.0.113.20');
|
|
51
|
+
headers.set('x-forwarded-for', '198.51.100.20, 10.0.0.1');
|
|
52
|
+
|
|
53
|
+
expect(extractIPFromRequest(headers)).toBe('203.0.113.20');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('falls back to x-forwarded-for when cloud proxy headers are absent', () => {
|
|
57
|
+
const headers = new Headers();
|
|
58
|
+
headers.set('x-forwarded-for', '198.51.100.30, 10.0.0.1');
|
|
59
|
+
|
|
60
|
+
expect(extractIPFromRequest(headers)).toBe('198.51.100.30');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('recordMalformedAuthCookieEdge', () => {
|
|
65
|
+
it('tracks malformed-cookie hits without blocking on the first offense', async () => {
|
|
66
|
+
mocks.redis.incr.mockResolvedValueOnce(1);
|
|
67
|
+
|
|
68
|
+
const blockInfo = await recordMalformedAuthCookieEdge('203.0.113.10');
|
|
69
|
+
|
|
70
|
+
expect(blockInfo).toBeNull();
|
|
71
|
+
expect(mocks.redis.expire).toHaveBeenCalledWith(
|
|
72
|
+
'api:auth:malformed-cookie:203.0.113.10',
|
|
73
|
+
60
|
|
74
|
+
);
|
|
75
|
+
expect(mocks.redis.set).not.toHaveBeenCalledWith(
|
|
76
|
+
'ip:blocked:203.0.113.10',
|
|
77
|
+
expect.anything(),
|
|
78
|
+
expect.anything()
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('escalates repeated malformed-cookie hits into an IP block', async () => {
|
|
83
|
+
mocks.redis.incr.mockResolvedValueOnce(3);
|
|
84
|
+
mocks.redis.get.mockResolvedValueOnce(0);
|
|
85
|
+
|
|
86
|
+
const blockInfo = await recordMalformedAuthCookieEdge('203.0.113.10');
|
|
87
|
+
|
|
88
|
+
expect(blockInfo?.blockLevel).toBe(1);
|
|
89
|
+
expect(blockInfo?.reason).toBe('api_abuse');
|
|
90
|
+
expect(mocks.redis.set).toHaveBeenCalledWith(
|
|
91
|
+
'ip:blocked:203.0.113.10',
|
|
92
|
+
expect.any(String),
|
|
93
|
+
expect.objectContaining({ ex: 300 })
|
|
94
|
+
);
|
|
95
|
+
expect(mocks.redis.set).toHaveBeenCalledWith(
|
|
96
|
+
'ip:block:level:203.0.113.10',
|
|
97
|
+
1,
|
|
98
|
+
expect.objectContaining({ ex: 86400 })
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('recordSuspiciousApiRequestEdge', () => {
|
|
104
|
+
it('tracks suspicious anonymous hits without blocking on the first offense', async () => {
|
|
105
|
+
mocks.redis.incr.mockResolvedValueOnce(1);
|
|
106
|
+
|
|
107
|
+
const blockInfo = await recordSuspiciousApiRequestEdge('203.0.113.20');
|
|
108
|
+
|
|
109
|
+
expect(blockInfo).toBeNull();
|
|
110
|
+
expect(mocks.redis.expire).toHaveBeenCalledWith(
|
|
111
|
+
'api:suspicious:203.0.113.20',
|
|
112
|
+
60
|
|
113
|
+
);
|
|
114
|
+
expect(mocks.redis.set).not.toHaveBeenCalledWith(
|
|
115
|
+
'ip:blocked:203.0.113.20',
|
|
116
|
+
expect.anything(),
|
|
117
|
+
expect.anything()
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('escalates repeated suspicious anonymous hits into an IP block', async () => {
|
|
122
|
+
mocks.redis.incr.mockResolvedValueOnce(3);
|
|
123
|
+
mocks.redis.get.mockResolvedValueOnce(0);
|
|
124
|
+
|
|
125
|
+
const blockInfo = await recordSuspiciousApiRequestEdge('203.0.113.20');
|
|
126
|
+
|
|
127
|
+
expect(blockInfo?.blockLevel).toBe(1);
|
|
128
|
+
expect(blockInfo?.reason).toBe('api_abuse');
|
|
129
|
+
expect(mocks.redis.set).toHaveBeenCalledWith(
|
|
130
|
+
'ip:blocked:203.0.113.20',
|
|
131
|
+
expect.any(String),
|
|
132
|
+
expect.objectContaining({ ex: 300 })
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for OTP Abuse Protection System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
ABUSE_THRESHOLDS,
|
|
8
|
+
BLOCK_DURATIONS,
|
|
9
|
+
checkOTPSendAllowed,
|
|
10
|
+
classifyPotentialSpamUserAgent,
|
|
11
|
+
extractIPFromHeaders,
|
|
12
|
+
hashEmail,
|
|
13
|
+
MAX_BLOCK_LEVEL,
|
|
14
|
+
REDIS_KEYS,
|
|
15
|
+
recordOTPSendSuccess,
|
|
16
|
+
resetOtpLimitsForEmail,
|
|
17
|
+
WINDOW_MS,
|
|
18
|
+
} from '../index';
|
|
19
|
+
|
|
20
|
+
describe('abuse-protection', () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
vi.unstubAllEnvs();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('extractIPFromHeaders', () => {
|
|
27
|
+
it('should extract IP from x-forwarded-for header', () => {
|
|
28
|
+
const headers = new Headers();
|
|
29
|
+
headers.set('x-forwarded-for', '192.168.1.1, 10.0.0.1');
|
|
30
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should extract single IP from x-forwarded-for', () => {
|
|
34
|
+
const headers = new Headers();
|
|
35
|
+
headers.set('x-forwarded-for', '203.0.113.50');
|
|
36
|
+
expect(extractIPFromHeaders(headers)).toBe('203.0.113.50');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should extract IP from x-real-ip header', () => {
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
headers.set('x-real-ip', '192.168.1.100');
|
|
42
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.100');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should extract IP from cf-connecting-ip header (Cloudflare)', () => {
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
headers.set('cf-connecting-ip', '172.16.0.1');
|
|
48
|
+
expect(extractIPFromHeaders(headers)).toBe('172.16.0.1');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should prefer cf-connecting-ip over forwarded proxy headers', () => {
|
|
52
|
+
const headers = new Headers();
|
|
53
|
+
headers.set('x-forwarded-for', '192.168.1.1');
|
|
54
|
+
headers.set('x-real-ip', '192.168.1.2');
|
|
55
|
+
headers.set('cf-connecting-ip', '192.168.1.3');
|
|
56
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.3');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should prefer true-client-ip when cf-connecting-ip is absent', () => {
|
|
60
|
+
const headers = new Headers();
|
|
61
|
+
headers.set('x-forwarded-for', '192.168.1.1');
|
|
62
|
+
headers.set('x-real-ip', '192.168.1.2');
|
|
63
|
+
headers.set('true-client-ip', '192.168.1.4');
|
|
64
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.4');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fall back to x-forwarded-for if explicit client IP headers are invalid', () => {
|
|
68
|
+
const headers = new Headers();
|
|
69
|
+
headers.set('cf-connecting-ip', 'invalid-ip');
|
|
70
|
+
headers.set('true-client-ip', 'also-invalid');
|
|
71
|
+
headers.set('x-forwarded-for', '192.168.1.100, 10.0.0.1');
|
|
72
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.100');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return "unknown" when no valid IP is found', () => {
|
|
76
|
+
const headers = new Headers();
|
|
77
|
+
expect(extractIPFromHeaders(headers)).toBe('unknown');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return "unknown" for invalid IP formats', () => {
|
|
81
|
+
const headers = new Headers();
|
|
82
|
+
headers.set('x-forwarded-for', 'not-an-ip');
|
|
83
|
+
headers.set('x-real-ip', 'also-invalid');
|
|
84
|
+
headers.set('cf-connecting-ip', 'still.not.valid');
|
|
85
|
+
expect(extractIPFromHeaders(headers)).toBe('unknown');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should accept IPs with values matching format regex', () => {
|
|
89
|
+
// Note: The regex validates format, not IPv4 value ranges
|
|
90
|
+
// 999.999.999.999 matches the pattern \d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}
|
|
91
|
+
const headers = new Headers();
|
|
92
|
+
headers.set('x-forwarded-for', '999.999.999.999');
|
|
93
|
+
expect(extractIPFromHeaders(headers)).toBe('999.999.999.999');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should work with Map-based headers', () => {
|
|
97
|
+
const headers = new Map<string, string>();
|
|
98
|
+
headers.set('x-forwarded-for', '10.0.0.5');
|
|
99
|
+
expect(extractIPFromHeaders(headers)).toBe('10.0.0.5');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should work with plain object headers', () => {
|
|
103
|
+
const headers: Record<string, string | null> = {
|
|
104
|
+
'x-forwarded-for': '10.0.0.10',
|
|
105
|
+
};
|
|
106
|
+
expect(extractIPFromHeaders(headers)).toBe('10.0.0.10');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle IPv6 addresses', () => {
|
|
110
|
+
const headers = new Headers();
|
|
111
|
+
headers.set('x-forwarded-for', '2001:db8::1');
|
|
112
|
+
expect(extractIPFromHeaders(headers)).toBe('2001:db8::1');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle IPv6 in x-real-ip', () => {
|
|
116
|
+
const headers = new Headers();
|
|
117
|
+
headers.set('x-real-ip', '::1');
|
|
118
|
+
expect(extractIPFromHeaders(headers)).toBe('::1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle whitespace in IP list', () => {
|
|
122
|
+
const headers = new Headers();
|
|
123
|
+
headers.set('x-forwarded-for', ' 192.168.1.1 , 10.0.0.1');
|
|
124
|
+
expect(extractIPFromHeaders(headers)).toBe('192.168.1.1');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('hashEmail', () => {
|
|
129
|
+
it('should return a 16-character hex string', () => {
|
|
130
|
+
const hash = hashEmail('test@example.com');
|
|
131
|
+
expect(hash).toMatch(/^[a-f0-9]{16}$/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should be case-insensitive', () => {
|
|
135
|
+
const hash1 = hashEmail('Test@Example.COM');
|
|
136
|
+
const hash2 = hashEmail('test@example.com');
|
|
137
|
+
expect(hash1).toBe(hash2);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should produce different hashes for different emails', () => {
|
|
141
|
+
const hash1 = hashEmail('user1@example.com');
|
|
142
|
+
const hash2 = hashEmail('user2@example.com');
|
|
143
|
+
expect(hash1).not.toBe(hash2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should be deterministic', () => {
|
|
147
|
+
const email = 'consistent@test.com';
|
|
148
|
+
const hash1 = hashEmail(email);
|
|
149
|
+
const hash2 = hashEmail(email);
|
|
150
|
+
expect(hash1).toBe(hash2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle special characters in email', () => {
|
|
154
|
+
const hash = hashEmail('user+tag@example.com');
|
|
155
|
+
expect(hash).toMatch(/^[a-f0-9]{16}$/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle unicode in email', () => {
|
|
159
|
+
const hash = hashEmail('用户@example.com');
|
|
160
|
+
expect(hash).toMatch(/^[a-f0-9]{16}$/);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('classifyPotentialSpamUserAgent', () => {
|
|
165
|
+
it('blocks known automation frameworks', () => {
|
|
166
|
+
expect(
|
|
167
|
+
classifyPotentialSpamUserAgent('Mozilla/5.0 HeadlessChrome Playwright')
|
|
168
|
+
).toMatchObject({
|
|
169
|
+
reason: 'known_automation_framework',
|
|
170
|
+
riskLevel: 'block',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('blocks scripted HTTP clients', () => {
|
|
175
|
+
expect(classifyPotentialSpamUserAgent('curl/8.7.1')).toMatchObject({
|
|
176
|
+
reason: 'scripted_http_client',
|
|
177
|
+
riskLevel: 'block',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('allows common browser user agents', () => {
|
|
182
|
+
expect(
|
|
183
|
+
classifyPotentialSpamUserAgent(
|
|
184
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
|
|
185
|
+
)
|
|
186
|
+
).toMatchObject({
|
|
187
|
+
reason: null,
|
|
188
|
+
riskLevel: 'allow',
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('allows native mobile app user agents when requested', () => {
|
|
193
|
+
expect(
|
|
194
|
+
classifyPotentialSpamUserAgent('Dart/3.9 (dart:io)', {
|
|
195
|
+
allowNativeAppUserAgents: true,
|
|
196
|
+
})
|
|
197
|
+
).toMatchObject({
|
|
198
|
+
reason: null,
|
|
199
|
+
riskLevel: 'allow',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('ABUSE_THRESHOLDS constants', () => {
|
|
205
|
+
it('should have valid OTP send limits', () => {
|
|
206
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_PER_MINUTE).toBe(30);
|
|
207
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_PER_HOUR).toBe(180);
|
|
208
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_PER_DAY).toBe(300);
|
|
209
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_EMAIL_COOLDOWN_WINDOW_MS).toBe(
|
|
210
|
+
15 * 60 * 1000
|
|
211
|
+
);
|
|
212
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_EMAIL_PER_HOUR).toBe(2);
|
|
213
|
+
expect(ABUSE_THRESHOLDS.OTP_SEND_EMAIL_PER_DAY).toBe(4);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should have valid OTP verify limits', () => {
|
|
217
|
+
expect(ABUSE_THRESHOLDS.OTP_VERIFY_FAILED_WINDOW_MS).toBe(5 * 60 * 1000);
|
|
218
|
+
expect(ABUSE_THRESHOLDS.OTP_VERIFY_FAILED_MAX).toBe(5);
|
|
219
|
+
expect(ABUSE_THRESHOLDS.OTP_VERIFY_FAILED_EMAIL_WINDOW_MS).toBe(
|
|
220
|
+
15 * 60 * 1000
|
|
221
|
+
);
|
|
222
|
+
expect(ABUSE_THRESHOLDS.OTP_VERIFY_FAILED_EMAIL_MAX).toBe(10);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should have valid MFA limits', () => {
|
|
226
|
+
expect(ABUSE_THRESHOLDS.MFA_CHALLENGE_PER_MINUTE).toBe(5);
|
|
227
|
+
expect(ABUSE_THRESHOLDS.MFA_VERIFY_FAILED_WINDOW_MS).toBe(5 * 60 * 1000);
|
|
228
|
+
expect(ABUSE_THRESHOLDS.MFA_VERIFY_FAILED_MAX).toBe(5);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should have valid reauth limits', () => {
|
|
232
|
+
expect(ABUSE_THRESHOLDS.REAUTH_SEND_PER_MINUTE).toBe(3);
|
|
233
|
+
expect(ABUSE_THRESHOLDS.REAUTH_VERIFY_FAILED_WINDOW_MS).toBe(
|
|
234
|
+
5 * 60 * 1000
|
|
235
|
+
);
|
|
236
|
+
expect(ABUSE_THRESHOLDS.REAUTH_VERIFY_FAILED_MAX).toBe(5);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should have valid password login limits', () => {
|
|
240
|
+
expect(ABUSE_THRESHOLDS.PASSWORD_LOGIN_FAILED_WINDOW_MS).toBe(
|
|
241
|
+
5 * 60 * 1000
|
|
242
|
+
);
|
|
243
|
+
expect(ABUSE_THRESHOLDS.PASSWORD_LOGIN_FAILED_MAX).toBe(10);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('BLOCK_DURATIONS constants', () => {
|
|
248
|
+
it('should have level 1 at 5 minutes', () => {
|
|
249
|
+
expect(BLOCK_DURATIONS[1]).toBe(5 * 60);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should have level 2 at 15 minutes', () => {
|
|
253
|
+
expect(BLOCK_DURATIONS[2]).toBe(15 * 60);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should have level 3 at 1 hour', () => {
|
|
257
|
+
expect(BLOCK_DURATIONS[3]).toBe(60 * 60);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should have level 4 at 24 hours', () => {
|
|
261
|
+
expect(BLOCK_DURATIONS[4]).toBe(24 * 60 * 60);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should have progressively increasing durations', () => {
|
|
265
|
+
expect(BLOCK_DURATIONS[1]).toBeLessThan(BLOCK_DURATIONS[2]);
|
|
266
|
+
expect(BLOCK_DURATIONS[2]).toBeLessThan(BLOCK_DURATIONS[3]);
|
|
267
|
+
expect(BLOCK_DURATIONS[3]).toBeLessThan(BLOCK_DURATIONS[4]);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('MAX_BLOCK_LEVEL constant', () => {
|
|
272
|
+
it('should be 4', () => {
|
|
273
|
+
expect(MAX_BLOCK_LEVEL).toBe(4);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('REDIS_KEYS', () => {
|
|
278
|
+
const testIP = '192.168.1.1';
|
|
279
|
+
const testEmailHash = 'abc123';
|
|
280
|
+
|
|
281
|
+
it('should generate correct OTP send keys', () => {
|
|
282
|
+
expect(REDIS_KEYS.OTP_SEND(testIP)).toBe(`otp:send:${testIP}`);
|
|
283
|
+
expect(REDIS_KEYS.OTP_SEND_HOURLY(testIP)).toBe(
|
|
284
|
+
`otp:send:hourly:${testIP}`
|
|
285
|
+
);
|
|
286
|
+
expect(REDIS_KEYS.OTP_SEND_DAILY(testIP)).toBe(
|
|
287
|
+
`otp:send:daily:${testIP}`
|
|
288
|
+
);
|
|
289
|
+
expect(REDIS_KEYS.OTP_SEND_EMAIL_COOLDOWN(testEmailHash)).toBe(
|
|
290
|
+
`otp:send:email:cooldown:${testEmailHash}`
|
|
291
|
+
);
|
|
292
|
+
expect(REDIS_KEYS.OTP_SEND_EMAIL_HOURLY(testEmailHash)).toBe(
|
|
293
|
+
`otp:send:email:hourly:${testEmailHash}`
|
|
294
|
+
);
|
|
295
|
+
expect(REDIS_KEYS.OTP_SEND_EMAIL_DAILY(testEmailHash)).toBe(
|
|
296
|
+
`otp:send:email:daily:${testEmailHash}`
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should generate correct OTP verify keys', () => {
|
|
301
|
+
expect(REDIS_KEYS.OTP_VERIFY_FAILED(testIP)).toBe(
|
|
302
|
+
`otp:verify:failed:${testIP}`
|
|
303
|
+
);
|
|
304
|
+
expect(REDIS_KEYS.OTP_VERIFY_FAILED_EMAIL(testEmailHash)).toBe(
|
|
305
|
+
`otp:verify:failed:email:${testEmailHash}`
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should generate correct MFA keys', () => {
|
|
310
|
+
expect(REDIS_KEYS.MFA_CHALLENGE(testIP)).toBe(`mfa:challenge:${testIP}`);
|
|
311
|
+
expect(REDIS_KEYS.MFA_VERIFY_FAILED(testIP)).toBe(
|
|
312
|
+
`mfa:verify:failed:${testIP}`
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should generate correct reauth keys', () => {
|
|
317
|
+
expect(REDIS_KEYS.REAUTH_SEND(testIP)).toBe(`reauth:send:${testIP}`);
|
|
318
|
+
expect(REDIS_KEYS.REAUTH_VERIFY_FAILED(testIP)).toBe(
|
|
319
|
+
`reauth:verify:failed:${testIP}`
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should generate correct password login keys', () => {
|
|
324
|
+
expect(REDIS_KEYS.PASSWORD_LOGIN_FAILED(testIP)).toBe(
|
|
325
|
+
`password:login:failed:${testIP}`
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should generate correct IP block keys', () => {
|
|
330
|
+
expect(REDIS_KEYS.IP_BLOCKED(testIP)).toBe(`ip:blocked:${testIP}`);
|
|
331
|
+
expect(REDIS_KEYS.IP_BLOCK_LEVEL(testIP)).toBe(
|
|
332
|
+
`ip:block:level:${testIP}`
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('WINDOW_MS constants', () => {
|
|
338
|
+
it('should have correct time windows', () => {
|
|
339
|
+
expect(WINDOW_MS.ONE_MINUTE).toBe(60 * 1000);
|
|
340
|
+
expect(WINDOW_MS.TEN_MINUTES).toBe(10 * 60 * 1000);
|
|
341
|
+
expect(WINDOW_MS.ONE_HOUR).toBe(60 * 60 * 1000);
|
|
342
|
+
expect(WINDOW_MS.TWENTY_FOUR_HOURS).toBe(24 * 60 * 60 * 1000);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should have windows in proper order', () => {
|
|
346
|
+
expect(WINDOW_MS.ONE_MINUTE).toBeLessThan(WINDOW_MS.ONE_HOUR);
|
|
347
|
+
expect(WINDOW_MS.TEN_MINUTES).toBeLessThan(WINDOW_MS.ONE_HOUR);
|
|
348
|
+
expect(WINDOW_MS.ONE_HOUR).toBeLessThan(WINDOW_MS.TWENTY_FOUR_HOURS);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('checkOTPSendAllowed', () => {
|
|
353
|
+
it('does not consume quota during preflight checks', async () => {
|
|
354
|
+
const email = `preflight-${Date.now()}@example.com`;
|
|
355
|
+
|
|
356
|
+
const firstAttempt = await checkOTPSendAllowed('198.51.100.1', email);
|
|
357
|
+
const secondAttempt = await checkOTPSendAllowed('198.51.100.2', email);
|
|
358
|
+
|
|
359
|
+
expect(firstAttempt.allowed).toBe(true);
|
|
360
|
+
expect(secondAttempt.allowed).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('blocks repeated sends to the same email across different IPs during cooldown', async () => {
|
|
364
|
+
const email = `cooldown-${Date.now()}@example.com`;
|
|
365
|
+
|
|
366
|
+
const firstAttempt = await checkOTPSendAllowed('198.51.100.1', email);
|
|
367
|
+
await recordOTPSendSuccess('198.51.100.1', email);
|
|
368
|
+
const secondAttempt = await checkOTPSendAllowed('198.51.100.2', email);
|
|
369
|
+
|
|
370
|
+
expect(firstAttempt.allowed).toBe(true);
|
|
371
|
+
expect(secondAttempt.allowed).toBe(false);
|
|
372
|
+
expect(secondAttempt.retryAfter).toBeGreaterThan(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('allows multiple unique teachers behind one shared IP', async () => {
|
|
376
|
+
const emailBase = `shared-ip-${Date.now()}`;
|
|
377
|
+
|
|
378
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
379
|
+
const allowedAttempt = await checkOTPSendAllowed(
|
|
380
|
+
'198.51.100.10',
|
|
381
|
+
`${emailBase}-${attempt}@example.com`
|
|
382
|
+
);
|
|
383
|
+
expect(allowedAttempt.allowed).toBe(true);
|
|
384
|
+
await recordOTPSendSuccess(
|
|
385
|
+
'198.51.100.10',
|
|
386
|
+
`${emailBase}-${attempt}@example.com`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('honors configured per-minute shared-IP OTP send limits', async () => {
|
|
392
|
+
vi.stubEnv('ABUSE_OTP_SEND_IP_LIMIT_MINUTE', '2');
|
|
393
|
+
|
|
394
|
+
const emailBase = `configured-minute-${Date.now()}`;
|
|
395
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
396
|
+
const allowedAttempt = await checkOTPSendAllowed(
|
|
397
|
+
'198.51.100.11',
|
|
398
|
+
`${emailBase}-${attempt}@example.com`
|
|
399
|
+
);
|
|
400
|
+
expect(allowedAttempt.allowed).toBe(true);
|
|
401
|
+
await recordOTPSendSuccess(
|
|
402
|
+
'198.51.100.11',
|
|
403
|
+
`${emailBase}-${attempt}@example.com`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const blockedAttempt = await checkOTPSendAllowed(
|
|
408
|
+
'198.51.100.11',
|
|
409
|
+
`${emailBase}-2@example.com`
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(blockedAttempt.allowed).toBe(false);
|
|
413
|
+
expect(blockedAttempt.retryAfter).toBeGreaterThan(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('caps successful sends for the same email across distributed IPs within an hour', async () => {
|
|
417
|
+
vi.useFakeTimers();
|
|
418
|
+
vi.setSystemTime(new Date('2026-03-12T00:00:00.000Z'));
|
|
419
|
+
|
|
420
|
+
const email = `hourly-${Date.now()}@example.com`;
|
|
421
|
+
|
|
422
|
+
const first = await checkOTPSendAllowed('203.0.113.1', email);
|
|
423
|
+
await recordOTPSendSuccess('203.0.113.1', email);
|
|
424
|
+
vi.advanceTimersByTime(
|
|
425
|
+
ABUSE_THRESHOLDS.OTP_SEND_EMAIL_COOLDOWN_WINDOW_MS + 1
|
|
426
|
+
);
|
|
427
|
+
const second = await checkOTPSendAllowed('203.0.113.2', email);
|
|
428
|
+
await recordOTPSendSuccess('203.0.113.2', email);
|
|
429
|
+
vi.advanceTimersByTime(
|
|
430
|
+
ABUSE_THRESHOLDS.OTP_SEND_EMAIL_COOLDOWN_WINDOW_MS + 1
|
|
431
|
+
);
|
|
432
|
+
const third = await checkOTPSendAllowed('203.0.113.3', email);
|
|
433
|
+
|
|
434
|
+
expect(first.allowed).toBe(true);
|
|
435
|
+
expect(second.allowed).toBe(true);
|
|
436
|
+
expect(third.allowed).toBe(false);
|
|
437
|
+
expect(third.retryAfter).toBeGreaterThan(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('caps slow OTP send abuse from a single IP over a day', async () => {
|
|
441
|
+
vi.useFakeTimers();
|
|
442
|
+
vi.setSystemTime(new Date('2026-03-12T00:00:00.000Z'));
|
|
443
|
+
const configuredDayLimit = 12;
|
|
444
|
+
vi.stubEnv('ABUSE_OTP_SEND_IP_LIMIT_DAY', String(configuredDayLimit));
|
|
445
|
+
|
|
446
|
+
const emailBase = `slow-ip-${Date.now()}`;
|
|
447
|
+
let lastAttempt = await checkOTPSendAllowed(
|
|
448
|
+
'198.51.100.20',
|
|
449
|
+
`${emailBase}-0@example.com`
|
|
450
|
+
);
|
|
451
|
+
await recordOTPSendSuccess('198.51.100.20', `${emailBase}-0@example.com`);
|
|
452
|
+
|
|
453
|
+
for (let attempt = 1; attempt <= configuredDayLimit; attempt++) {
|
|
454
|
+
vi.advanceTimersByTime(90 * 60 * 1000);
|
|
455
|
+
lastAttempt = await checkOTPSendAllowed(
|
|
456
|
+
'198.51.100.20',
|
|
457
|
+
`${emailBase}-${attempt}@example.com`
|
|
458
|
+
);
|
|
459
|
+
if (lastAttempt.allowed) {
|
|
460
|
+
await recordOTPSendSuccess(
|
|
461
|
+
'198.51.100.20',
|
|
462
|
+
`${emailBase}-${attempt}@example.com`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
expect(lastAttempt.allowed).toBe(false);
|
|
468
|
+
expect(lastAttempt.retryAfter).toBeGreaterThan(0);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('reports remaining attempts based on the next accepted send', async () => {
|
|
472
|
+
const email = `remaining-${Date.now()}@example.com`;
|
|
473
|
+
|
|
474
|
+
const attempt = await checkOTPSendAllowed('198.51.100.50', email);
|
|
475
|
+
|
|
476
|
+
expect(attempt.allowed).toBe(true);
|
|
477
|
+
expect(attempt.remainingAttempts).toBe(
|
|
478
|
+
ABUSE_THRESHOLDS.OTP_SEND_PER_MINUTE - 1
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('resetOtpLimitsForEmail', () => {
|
|
484
|
+
it.each([
|
|
485
|
+
'user@@example.com',
|
|
486
|
+
'user@example',
|
|
487
|
+
'user..name@example.com',
|
|
488
|
+
'user@-example.com',
|
|
489
|
+
'user@example..com',
|
|
490
|
+
])('rejects malformed email input: %s', async (email) => {
|
|
491
|
+
await expect(
|
|
492
|
+
resetOtpLimitsForEmail({
|
|
493
|
+
email,
|
|
494
|
+
clearEmailScoped: true,
|
|
495
|
+
clearRelatedIpCounters: false,
|
|
496
|
+
clearRelatedIpBlocks: false,
|
|
497
|
+
adminUserId: 'admin-user-id',
|
|
498
|
+
})
|
|
499
|
+
).rejects.toThrow('Email is invalid');
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('abuse-protection types', () => {
|
|
505
|
+
it('should have all expected abuse event types as valid strings', () => {
|
|
506
|
+
// These are the valid AbuseEventType values as defined in types.ts
|
|
507
|
+
const validTypes = [
|
|
508
|
+
'otp_send',
|
|
509
|
+
'otp_verify_failed',
|
|
510
|
+
'mfa_challenge',
|
|
511
|
+
'mfa_verify_failed',
|
|
512
|
+
'reauth_send',
|
|
513
|
+
'reauth_verify_failed',
|
|
514
|
+
'password_login_failed',
|
|
515
|
+
'manual',
|
|
516
|
+
];
|
|
517
|
+
// Just verify these are valid string values
|
|
518
|
+
validTypes.forEach((type) => {
|
|
519
|
+
expect(typeof type).toBe('string');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('IP address validation', () => {
|
|
525
|
+
// Test IPv4 validation through extractIPFromHeaders
|
|
526
|
+
describe('IPv4 addresses', () => {
|
|
527
|
+
it('should accept valid IPv4 addresses', () => {
|
|
528
|
+
const validIPs = [
|
|
529
|
+
'0.0.0.0',
|
|
530
|
+
'192.168.1.1',
|
|
531
|
+
'255.255.255.255',
|
|
532
|
+
'10.0.0.1',
|
|
533
|
+
'172.16.0.1',
|
|
534
|
+
];
|
|
535
|
+
validIPs.forEach((ip) => {
|
|
536
|
+
const headers = new Headers();
|
|
537
|
+
headers.set('x-forwarded-for', ip);
|
|
538
|
+
expect(extractIPFromHeaders(headers)).toBe(ip);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe('IPv6 addresses', () => {
|
|
544
|
+
it('should accept common IPv6 addresses', () => {
|
|
545
|
+
// Test the IPv6 addresses that match the current regex pattern
|
|
546
|
+
const supportedIPs = ['::1', '2001:db8::1', 'fe80::1'];
|
|
547
|
+
supportedIPs.forEach((ip) => {
|
|
548
|
+
const headers = new Headers();
|
|
549
|
+
headers.set('x-forwarded-for', ip);
|
|
550
|
+
expect(extractIPFromHeaders(headers)).toBe(ip);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should handle IPv6 formats matching the regex pattern', () => {
|
|
555
|
+
// The regex pattern is basic and may not match all valid IPv6 formats
|
|
556
|
+
// like ::ffff:192.168.1.1 (IPv4-mapped IPv6)
|
|
557
|
+
const headers = new Headers();
|
|
558
|
+
headers.set('x-forwarded-for', '::1');
|
|
559
|
+
expect(extractIPFromHeaders(headers)).toBe('::1');
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|