|
1 | 1 | const resolve4 = jest.fn(); |
2 | 2 | const resolve6 = jest.fn(); |
3 | 3 | const getBoolean = jest.fn().mockReturnValue(true); |
| 4 | +const getPublicUrl = jest.fn(); |
4 | 5 |
|
5 | 6 | jest.mock('dns', () => ({ promises: { resolve4, resolve6 } })); |
6 | 7 | jest.mock('../../src/lib/system', () => ({ |
7 | | - system: { getBoolean }, |
8 | | - SharedSystemProp: { ENABLE_HOST_VALIDATION: 'ENABLE_HOST_VALIDATION' }, |
| 8 | + system: { |
| 9 | + getBoolean, |
| 10 | + getOrThrow: jest.fn().mockReturnValue('x-forwarded-for'), |
| 11 | + }, |
| 12 | + SharedSystemProp: { |
| 13 | + ENABLE_HOST_VALIDATION: 'ENABLE_HOST_VALIDATION', |
| 14 | + FRONTEND_URL: 'FRONTEND_URL', |
| 15 | + }, |
| 16 | + AppSystemProp: { CLIENT_REAL_IP_HEADER: 'CLIENT_REAL_IP_HEADER' }, |
| 17 | +})); |
| 18 | +jest.mock('../../src/lib/network-utils', () => ({ |
| 19 | + networkUtls: { getPublicUrl }, |
9 | 20 | })); |
10 | 21 |
|
11 | | -import { validateHost } from '../../src/lib/host-validation'; |
| 22 | +import { |
| 23 | + validateHost, |
| 24 | + validateHostAllowingPublicWebhookUrl, |
| 25 | +} from '../../src/lib/host-validation'; |
12 | 26 |
|
13 | 27 | describe('Host Validation', () => { |
14 | 28 | beforeEach(() => { |
@@ -114,4 +128,76 @@ describe('Host Validation', () => { |
114 | 128 | 'Host must not be an internal address', |
115 | 129 | ); |
116 | 130 | }); |
| 131 | + |
| 132 | + describe('validateHostAllowingPublicWebhookUrl', () => { |
| 133 | + test('should skip for empty url', async () => { |
| 134 | + const url = ''; |
| 135 | + await expect( |
| 136 | + validateHostAllowingPublicWebhookUrl(url), |
| 137 | + ).resolves.toBeUndefined(); |
| 138 | + }); |
| 139 | + |
| 140 | + test('should allow public webhook url even if it resolves to private ip', async () => { |
| 141 | + const publicUrl = 'https://openops.example.com/'; |
| 142 | + const webhookUrl = |
| 143 | + 'https://openops.example.com/v1/webhooks/123456789012345678901/sync'; |
| 144 | + getPublicUrl.mockResolvedValue(publicUrl); |
| 145 | + resolve4.mockResolvedValue(['127.0.0.1']); |
| 146 | + resolve6.mockResolvedValue([]); |
| 147 | + |
| 148 | + await expect( |
| 149 | + validateHostAllowingPublicWebhookUrl(webhookUrl), |
| 150 | + ).resolves.toBeUndefined(); |
| 151 | + }); |
| 152 | + |
| 153 | + test('should throw for internal host that is not the public webhook url', async () => { |
| 154 | + const publicUrl = 'https://openops.example.com/'; |
| 155 | + const internalUrl = |
| 156 | + 'https://10.0.0.1/v1/webhooks/123456789012345678901/sync'; |
| 157 | + getPublicUrl.mockResolvedValue(publicUrl); |
| 158 | + resolve4.mockResolvedValue(['10.0.0.1']); |
| 159 | + resolve6.mockResolvedValue([]); |
| 160 | + |
| 161 | + await expect( |
| 162 | + validateHostAllowingPublicWebhookUrl(internalUrl), |
| 163 | + ).rejects.toThrow('Host must not be an internal address'); |
| 164 | + }); |
| 165 | + |
| 166 | + test('should throw for public webhook url with invalid id length', async () => { |
| 167 | + const publicUrl = 'https://openops.example.com/'; |
| 168 | + const webhookUrl = |
| 169 | + 'https://openops.example.com/v1/webhooks/too-short/sync'; |
| 170 | + getPublicUrl.mockResolvedValue(publicUrl); |
| 171 | + resolve4.mockResolvedValue(['127.0.0.1']); |
| 172 | + resolve6.mockResolvedValue([]); |
| 173 | + |
| 174 | + await expect( |
| 175 | + validateHostAllowingPublicWebhookUrl(webhookUrl), |
| 176 | + ).rejects.toThrow('Host must not be an internal address'); |
| 177 | + }); |
| 178 | + |
| 179 | + test('should allow public host', async () => { |
| 180 | + const publicUrl = 'https://openops.example.com/'; |
| 181 | + const somePublicUrl = 'https://google.com'; |
| 182 | + getPublicUrl.mockResolvedValue(publicUrl); |
| 183 | + resolve4.mockResolvedValue(['8.8.8.8']); |
| 184 | + resolve6.mockResolvedValue([]); |
| 185 | + |
| 186 | + await expect( |
| 187 | + validateHostAllowingPublicWebhookUrl(somePublicUrl), |
| 188 | + ).resolves.toBeUndefined(); |
| 189 | + }); |
| 190 | + |
| 191 | + test('should throw error for DNS resolution failure on non-webhook URL', async () => { |
| 192 | + const publicUrl = 'https://openops.example.com/'; |
| 193 | + const unknownUrl = 'https://unknown.example.com'; |
| 194 | + getPublicUrl.mockResolvedValue(publicUrl); |
| 195 | + resolve4.mockRejectedValue(new Error('DNS resolution failed')); |
| 196 | + resolve6.mockRejectedValue(new Error('DNS resolution failed')); |
| 197 | + |
| 198 | + await expect( |
| 199 | + validateHostAllowingPublicWebhookUrl(unknownUrl), |
| 200 | + ).rejects.toThrow('Failed to resolve host'); |
| 201 | + }); |
| 202 | + }); |
117 | 203 | }); |
0 commit comments