@@ -27,95 +27,162 @@ describe('webhook-url-validator', () => {
2727 expect ( validateHost ) . toHaveBeenCalledWith ( userUrl ) ;
2828 } ) ;
2929
30- it ( 'should rewrite the URL if it matches the public URL origin and valid webhook path' , async ( ) => {
31- const error = new Error ( 'Host must not be an internal address' ) ;
32- ( validateHost as jest . Mock ) . mockRejectedValue ( error ) ;
33- ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue (
30+ test . each ( [
31+ [
32+ 'rewrites when public URL has no base path and internal URL has no base path' ,
3433 'https://public.openops.com' ,
35- ) ;
36- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
3734 'http://internal-api:3000' ,
38- ) ;
39-
40- const userUrl =
41- 'https://public.openops.com/v1/webhooks/123456789012345678901/sync' ;
42- const result = await validateAndRewritePublicWebhookUrl ( userUrl ) ;
43-
44- expect ( result ) . toBe (
35+ 'https://public.openops.com/v1/webhooks/123456789012345678901/sync' ,
4536 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync' ,
46- ) ;
47- } ) ;
48-
49- it ( 'should rewrite the URL when public URL has a base path' , async ( ) => {
50- const error = new Error ( 'Host must not be an internal address' ) ;
51- ( validateHost as jest . Mock ) . mockRejectedValue ( error ) ;
52- ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue (
53- 'https://openops.com/' ,
54- ) ;
55- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
37+ ] ,
38+ [
39+ 'rewrites when public URL has a base path and internal URL has no base path' ,
40+ 'http://localhost:4200/api' ,
41+ 'http://127.0.0.1:3000' ,
42+ 'http://localhost:4200/api/v1/webhooks/123456789012345678901/sync' ,
43+ 'http://127.0.0.1:3000/v1/webhooks/123456789012345678901/sync' ,
44+ ] ,
45+ [
46+ 'rewrites when public URL has a base path and internal URL has a different base path' ,
47+ 'https://public.openops.com/api' ,
48+ 'http://internal-api:3000/internal' ,
49+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
50+ 'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync' ,
51+ ] ,
52+ [
53+ 'rewrites when public URL and internal URL share the same base path' ,
54+ 'https://public.openops.com/api' ,
5655 'http://internal-api:3000/api' ,
57- ) ;
58-
59- const userUrl =
60- 'https://openops.com/api/v1/webhooks/123456789012345678901/sync' ;
61- const result = await validateAndRewritePublicWebhookUrl ( userUrl ) ;
62-
63- expect ( result ) . toBe (
56+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
6457 'http://internal-api:3000/api/v1/webhooks/123456789012345678901/sync' ,
65- ) ;
66- } ) ;
67-
68- it ( 'should throw the original error if origin does not match public URL origin' , async ( ) => {
69- const error = new Error ( 'Host must not be an internal address' ) ;
70- ( validateHost as jest . Mock ) . mockRejectedValue ( error ) ;
71- ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue (
58+ ] ,
59+ [
60+ 'rewrites when public URL ends with a trailing slash' ,
61+ 'https://public.openops.com/api/' ,
62+ 'http://internal-api:3000' ,
63+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
64+ 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync' ,
65+ ] ,
66+ [
67+ 'rewrites when internal URL ends with a trailing slash' ,
68+ 'https://public.openops.com/api' ,
69+ 'http://internal-api:3000/' ,
70+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
71+ 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync' ,
72+ ] ,
73+ [
74+ 'rewrites when both public and internal URLs end with trailing slashes' ,
75+ 'https://public.openops.com/api/' ,
76+ 'http://internal-api:3000/internal/' ,
77+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
78+ 'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync' ,
79+ ] ,
80+ [
81+ 'rewrites localhost public URL to internal host' ,
82+ 'http://localhost:4200' ,
83+ 'http://internal-api:3000' ,
84+ 'http://localhost:4200/v1/webhooks/123456789012345678901/sync' ,
85+ 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync' ,
86+ ] ,
87+ [
88+ 'rewrites when user URL includes query parameters' ,
7289 'https://public.openops.com' ,
73- ) ;
74- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
7590 'http://internal-api:3000' ,
76- ) ;
77-
78- const userUrl =
79- 'https://other-domain.com/v1/webhooks/123456789012345678901/sync' ;
80-
81- await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . rejects . toThrow (
82- error ,
83- ) ;
84- } ) ;
85-
86- it ( 'should throw the original error if the path does not match the webhook pattern' , async ( ) => {
87- const error = new Error ( 'Host must not be an internal address' ) ;
88- ( validateHost as jest . Mock ) . mockRejectedValue ( error ) ;
89- ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue (
91+ 'https://public.openops.com/v1/webhooks/123456789012345678901/sync?test=true' ,
92+ 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync?test=true' ,
93+ ] ,
94+ [
95+ 'rewrites when user URL includes a hash fragment' ,
9096 'https://public.openops.com' ,
91- ) ;
92- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
9397 'http://internal-api:3000' ,
94- ) ;
95-
96- const userUrl = 'https://public.openops.com/v1/webhooks/invalid-id/sync' ;
97-
98- await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . rejects . toThrow (
99- error ,
100- ) ;
101- } ) ;
102-
103- it ( 'should handle multiple slashes correctly during rewrite' , async ( ) => {
104- const error = new Error ( 'Host must not be an internal address' ) ;
105- ( validateHost as jest . Mock ) . mockRejectedValue ( error ) ;
106- ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue (
107- 'https://public.openops.com/' ,
108- ) ;
109- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
110- 'http://internal-api:3000/' ,
111- ) ;
112-
113- const userUrl =
114- 'https://public.openops.com/v1/webhooks/123456789012345678901/sync' ;
115- const result = await validateAndRewritePublicWebhookUrl ( userUrl ) ;
116-
117- expect ( result ) . toBe (
118- 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync' ,
119- ) ;
120- } ) ;
98+ 'https://public.openops.com/v1/webhooks/123456789012345678901/sync#section' ,
99+ 'http://internal-api:3000/v1/webhooks/123456789012345678901/sync#section' ,
100+ ] ,
101+ [
102+ 'rewrites when user URL includes both query parameters and a hash fragment' ,
103+ 'https://public.openops.com/api' ,
104+ 'http://internal-api:3000/internal' ,
105+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync?foo=bar#section' ,
106+ 'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync?foo=bar#section' ,
107+ ] ,
108+ [
109+ 'rewrites when user URL already contains the internal base path after public host matching' ,
110+ 'https://public.openops.com' ,
111+ 'http://internal-api:3000/api' ,
112+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
113+ 'http://internal-api:3000/api/v1/webhooks/123456789012345678901/sync' ,
114+ ] ,
115+ ] ) (
116+ '%s' ,
117+ async (
118+ _caseName : string ,
119+ publicUrl : string ,
120+ internalApiUrl : string ,
121+ userUrl : string ,
122+ expectedUrl : string ,
123+ ) => {
124+ const originalError = new Error ( 'Host must not be an internal address' ) ;
125+
126+ ( validateHost as jest . Mock ) . mockRejectedValue ( originalError ) ;
127+ ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue ( publicUrl ) ;
128+ ( networkUtls . getInternalApiUrl as jest . Mock ) . mockResolvedValue (
129+ internalApiUrl ,
130+ ) ;
131+
132+ await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . resolves . toBe (
133+ expectedUrl ,
134+ ) ;
135+
136+ expect ( validateHost ) . toHaveBeenCalledWith ( userUrl ) ;
137+ expect ( networkUtls . getPublicUrl ) . toHaveBeenCalledTimes ( 1 ) ;
138+ expect ( networkUtls . getInternalApiUrl ) . toHaveBeenCalledTimes ( 1 ) ;
139+ } ,
140+ ) ;
141+
142+ test . each ( [
143+ [
144+ 'throws when user URL host does not match public URL host' ,
145+ 'https://public.openops.com' ,
146+ 'http://internal-api:3000' ,
147+ 'https://other.openops.com/v1/webhooks/123456789012345678901/sync' ,
148+ ] ,
149+ [
150+ 'throws when path is not a valid webhook sync path' ,
151+ 'https://public.openops.com' ,
152+ 'http://internal-api:3000' ,
153+ 'https://public.openops.com/v1/webhooks/invalid/sync' ,
154+ ] ,
155+ [
156+ 'throws when path does not match webhook route' ,
157+ 'https://public.openops.com/api' ,
158+ 'http://internal-api:3000' ,
159+ 'https://public.openops.com/api/v1/other/123456789012345678901/sync' ,
160+ ] ,
161+ [
162+ 'throws when webhook path has extra suffix' ,
163+ 'https://public.openops.com' ,
164+ 'http://internal-api:3000' ,
165+ 'https://public.openops.com/v1/webhooks/123456789012345678901/sync/extra' ,
166+ ] ,
167+ ] ) (
168+ '%s' ,
169+ async (
170+ _caseName : string ,
171+ publicUrl : string ,
172+ internalApiUrl : string ,
173+ userUrl : string ,
174+ ) => {
175+ const originalError = new Error ( 'Host must not be an internal address' ) ;
176+
177+ ( validateHost as jest . Mock ) . mockRejectedValue ( originalError ) ;
178+ ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue ( publicUrl ) ;
179+ ( networkUtls . getInternalApiUrl as jest . Mock ) . mockResolvedValue (
180+ internalApiUrl ,
181+ ) ;
182+
183+ await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . rejects . toBe (
184+ originalError ,
185+ ) ;
186+ } ,
187+ ) ;
121188} ) ;
0 commit comments