@@ -27,95 +27,141 @@ 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 already contains the internal base path after public host matching' ,
89+ 'https://public.openops.com' ,
90+ 'http://internal-api:3000/api' ,
91+ 'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync' ,
92+ 'http://internal-api:3000/api/v1/webhooks/123456789012345678901/sync' ,
93+ ] ,
94+ ] ) (
95+ '%s' ,
96+ async (
97+ _caseName : string ,
98+ publicUrl : string ,
99+ internalApiUrl : string ,
100+ userUrl : string ,
101+ expectedUrl : string ,
102+ ) => {
103+ const originalError = new Error ( 'Host must not be an internal address' ) ;
104+
105+ ( validateHost as jest . Mock ) . mockRejectedValue ( originalError ) ;
106+ ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue ( publicUrl ) ;
107+ ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
108+ internalApiUrl ,
109+ ) ;
110+
111+ await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . resolves . toBe (
112+ expectedUrl ,
113+ ) ;
114+
115+ expect ( validateHost ) . toHaveBeenCalledWith ( userUrl ) ;
116+ expect ( networkUtls . getPublicUrl ) . toHaveBeenCalledTimes ( 1 ) ;
117+ expect ( networkUtls . getInternalApiUrl ) . toHaveBeenCalledTimes ( 1 ) ;
118+ } ,
119+ ) ;
120+
121+ test . each ( [
122+ [
123+ 'throws when user URL host does not match public URL host' ,
72124 'https://public.openops.com' ,
73- ) ;
74- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
75125 '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 (
126+ 'https://other.openops.com/v1/webhooks/123456789012345678901/sync' ,
127+ ] ,
128+ [
129+ 'throws when path is not a valid webhook sync path' ,
90130 'https://public.openops.com' ,
91- ) ;
92- ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
93131 '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- } ) ;
132+ 'https://public.openops.com/v1/webhooks/invalid/sync' ,
133+ ] ,
134+ [
135+ 'throws when path does not match webhook route' ,
136+ 'https://public.openops.com/api' ,
137+ 'http://internal-api:3000' ,
138+ 'https://public.openops.com/api/v1/other/123456789012345678901/sync' ,
139+ ] ,
140+ [
141+ 'throws when webhook path has extra suffix' ,
142+ 'https://public.openops.com' ,
143+ 'http://internal-api:3000' ,
144+ 'https://public.openops.com/v1/webhooks/123456789012345678901/sync/extra' ,
145+ ] ,
146+ ] ) (
147+ '%s' ,
148+ async (
149+ _caseName : string ,
150+ publicUrl : string ,
151+ internalApiUrl : string ,
152+ userUrl : string ,
153+ ) => {
154+ const originalError = new Error ( 'Host must not be an internal address' ) ;
155+
156+ ( validateHost as jest . Mock ) . mockRejectedValue ( originalError ) ;
157+ ( networkUtls . getPublicUrl as jest . Mock ) . mockResolvedValue ( publicUrl ) ;
158+ ( networkUtls . getInternalApiUrl as jest . Mock ) . mockReturnValue (
159+ internalApiUrl ,
160+ ) ;
161+
162+ await expect ( validateAndRewritePublicWebhookUrl ( userUrl ) ) . rejects . toBe (
163+ originalError ,
164+ ) ;
165+ } ,
166+ ) ;
121167} ) ;
0 commit comments