Repository: parse-community/Parse-SDK-JS
Pull request #2441 (2025-02-11 23:17:52)

fix: `Parse.Hooks` requests have double forward slash in URL
## Pull Request

- Report security issues [confidentially](https://github.com/parse-community/Parse-SDK-JS/security/policy).
- Any contribution is under this [license](https://github.com/parse-community/Parse-SDK-JS/blob/alpha/LICENSE).
- Link this pull request to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

## Issue
<!-- Add the link to the issue that this PR closes. -->

Closes: https://github.com/parse-community/Parse-SDK-JS/issues/2406

## Approach
<!-- Describe the changes in this PR. -->
Remove extra slash

## Tasks
<!-- Delete tasks that don't apply. -->

- [x] Add tests
- [x] Add changes to documentation (guides, repository pages, code comments)


parse-github-assistant[bot]: 
### Thanks for opening this pull request!

<!-- parse-issue-bot-meta-tag-id {"suggestPr":false,"thanksFeature":false,"thanksPr":false} -->


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/pull/2441?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`abadac9`)](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/commit/abadac947d8453afdf86f4a008aee189b4a6bfd2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) to head [(`c36b681`)](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/commit/c36b681b63e33a089e37adcb241e3704308e1f04?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).
> Report is 1 commits behind head on alpha.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             alpha     #2441   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         6392      6392           
  Branches      1549      1549           
=========================================
  Hits          6392      6392           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/parse-community/Parse-SDK-JS/pull/2441?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).

mtrezza: Could this possibly be a breaking change, thinking about custom parsing of the URI on the server or firewall?
dplewis: @pocketcolin 
mtrezza: We might want to merge this as a breaking change, make a stable release and then add that to the Parse Sever 8 release as a breaking change. Even though these are internal requests and therefore may be less likely to be restricted by a firewall, it's still possible.
mtrezza: BREAKING CHANGE: Internal REST requests for Cloud Code Hooks use a URL that contains a double forward slash, for example `http://localhost/parse//hooks/functions`. This release changes the double forward slash to a single forward slash.

@dplewis Is this correct?
dplewis: LGTM!

Patch:
diff --git a/src/ParseHooks.ts b/src/ParseHooks.ts
index 9ec21de..1e6a9ef 100644
--- a/src/ParseHooks.ts
+++ b/src/ParseHooks.ts
@@ -61,7 +61,7 @@ export function remove(hook: HookDeleteArg) {
 
 const DefaultController = {
   get(type: string, functionName?: string, triggerName?: string) {
-    let url = '/hooks/' + type;
+    let url = 'hooks/' + type;
     if (functionName) {
       url += '/' + functionName;
       if (triggerName) {
@@ -74,9 +74,9 @@ const DefaultController = {
   create(hook) {
     let url;
     if (hook.functionName && hook.url) {
-      url = '/hooks/functions';
+      url = 'hooks/functions';
     } else if (hook.className && hook.triggerName && hook.url) {
-      url = '/hooks/triggers';
+      url = 'hooks/triggers';
     } else {
       return Promise.reject({ error: 'invalid hook declaration', code: 143 });
     }
@@ -86,10 +86,10 @@ const DefaultController = {
   remove(hook) {
     let url;
     if (hook.functionName) {
-      url = '/hooks/functions/' + hook.functionName;
+      url = 'hooks/functions/' + hook.functionName;
       delete hook.functionName;
     } else if (hook.className && hook.triggerName) {
-      url = '/hooks/triggers/' + hook.className + '/' + hook.triggerName;
+      url = 'hooks/triggers/' + hook.className + '/' + hook.triggerName;
       delete hook.className;
       delete hook.triggerName;
     } else {
@@ -101,10 +101,10 @@ const DefaultController = {
   update(hook) {
     let url;
     if (hook.functionName && hook.url) {
-      url = '/hooks/functions/' + hook.functionName;
+      url = 'hooks/functions/' + hook.functionName;
       delete hook.functionName;
     } else if (hook.className && hook.triggerName && hook.url) {
-      url = '/hooks/triggers/' + hook.className + '/' + hook.triggerName;
+      url = 'hooks/triggers/' + hook.className + '/' + hook.triggerName;
       delete hook.className;
       delete hook.triggerName;
     } else {


Test patch:
diff --git a/src/__tests__/Hooks-test.js b/src/__tests__/Hooks-test.js
index 03d9b24..2bac07f 100644
--- a/src/__tests__/Hooks-test.js
+++ b/src/__tests__/Hooks-test.js
@@ -4,13 +4,27 @@ jest.dontMock('../decode');
 jest.dontMock('../encode');
 jest.dontMock('../ParseError');
 jest.dontMock('../ParseObject');
+jest.dontMock('../RESTController');
 
 const Hooks = require('../ParseHooks');
 const CoreManager = require('../CoreManager');
+const RESTController = require('../RESTController');
 
 const defaultController = CoreManager.getHooksController();
 const { sendRequest } = defaultController;
 
+CoreManager.setInstallationController({
+  currentInstallationId() {
+    return Promise.resolve('iid');
+  },
+  currentInstallation() {},
+  updateInstallationOnDisk() {},
+});
+CoreManager.set('APPLICATION_ID', 'A');
+CoreManager.set('JAVASCRIPT_KEY', 'B');
+CoreManager.set('MASTER_KEY', 'C');
+CoreManager.set('VERSION', 'V');
+
 describe('Hooks', () => {
   beforeEach(() => {
     const run = jest.fn();
@@ -19,125 +33,176 @@ describe('Hooks', () => {
         result: {},
       })
     );
+    const ajax = jest.fn();
+    ajax.mockReturnValue(
+      Promise.resolve({
+        response: {},
+      })
+    );
+    RESTController.ajax = ajax;
     defaultController.sendRequest = run;
     CoreManager.setHooksController(defaultController);
+    CoreManager.setRESTController(RESTController);
   });
 
-  it('shoud properly build GET functions', () => {
+  it('should properly build GET functions', async () => {
     Hooks.getFunctions();
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'GET',
-      '/hooks/functions',
+      'hooks/functions',
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.getFunctions();
+    expect(RESTController.ajax.mock.calls[0][1]).toBe('https://api.parse.com/1/hooks/functions');
   });
 
-  it('shoud properly build GET triggers', () => {
+  it('should properly build GET triggers', async () => {
     Hooks.getTriggers();
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'GET',
-      '/hooks/triggers',
+      'hooks/triggers',
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.getTriggers();
+    expect(RESTController.ajax.mock.calls[0][1]).toBe('https://api.parse.com/1/hooks/triggers');
   });
 
-  it('shoud properly build GET function', () => {
+  it('should properly build GET function', async () => {
     Hooks.getFunction('functionName');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'GET',
-      '/hooks/functions/functionName',
+      'hooks/functions/functionName',
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.getFunction('functionName');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/functions/functionName'
+    );
   });
 
-  it('shoud properly build GET trigger', () => {
+  it('should properly build GET trigger', async () => {
     Hooks.getTrigger('MyClass', 'beforeSave');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'GET',
-      '/hooks/triggers/MyClass/beforeSave',
+      'hooks/triggers/MyClass/beforeSave',
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.getTrigger('MyClass', 'beforeSave');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/triggers/MyClass/beforeSave'
+    );
   });
 
-  it('shoud properly build POST function', () => {
-    Hooks.createFunction('myFunction', 'https://dummy.com');
+  it('should properly build POST function', async () => {
+    Hooks.createFunction('myFunction', 'https://example.com');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'POST',
-      '/hooks/functions',
+      'hooks/functions',
       {
         functionName: 'myFunction',
-        url: 'https://dummy.com',
+        url: 'https://example.com',
       },
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.createFunction('myFunction', 'https://example.com');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe('https://api.parse.com/1/hooks/functions');
   });
 
-  it('shoud properly build POST trigger', () => {
-    Hooks.createTrigger('MyClass', 'beforeSave', 'https://dummy.com');
+  it('should properly build POST trigger', async () => {
+    Hooks.createTrigger('MyClass', 'beforeSave', 'https://example.com');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'POST',
-      '/hooks/triggers',
+      'hooks/triggers',
       {
         className: 'MyClass',
         triggerName: 'beforeSave',
-        url: 'https://dummy.com',
+        url: 'https://example.com',
       },
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.createTrigger('MyClass', 'beforeSave', 'https://example.com');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe('https://api.parse.com/1/hooks/triggers');
   });
 
-  it('shoud properly build PUT function', () => {
-    Hooks.updateFunction('myFunction', 'https://dummy.com');
+  it('should properly build PUT function', async () => {
+    Hooks.updateFunction('myFunction', 'https://example.com');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'PUT',
-      '/hooks/functions/myFunction',
+      'hooks/functions/myFunction',
       {
-        url: 'https://dummy.com',
+        url: 'https://example.com',
       },
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.updateFunction('myFunction', 'https://example.com');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/functions/myFunction'
+    );
   });
 
-  it('shoud properly build PUT trigger', () => {
-    Hooks.updateTrigger('MyClass', 'beforeSave', 'https://dummy.com');
+  it('should properly build PUT trigger', async () => {
+    Hooks.updateTrigger('MyClass', 'beforeSave', 'https://example.com');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'PUT',
-      '/hooks/triggers/MyClass/beforeSave',
+      'hooks/triggers/MyClass/beforeSave',
       {
-        url: 'https://dummy.com',
+        url: 'https://example.com',
       },
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.updateTrigger('MyClass', 'beforeSave', 'https://example.com');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/triggers/MyClass/beforeSave'
+    );
   });
 
-  it('shoud properly build removeFunction', () => {
+  it('should properly build removeFunction', async () => {
     Hooks.removeFunction('myFunction');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'PUT',
-      '/hooks/functions/myFunction',
+      'hooks/functions/myFunction',
       { __op: 'Delete' },
     ]);
+
+    defaultController.sendRequest = sendRequest;
+    await Hooks.removeFunction('myFunction');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/functions/myFunction'
+    );
   });
 
-  it('shoud properly build removeTrigger', () => {
+  it('should properly build removeTrigger', async () => {
     Hooks.removeTrigger('MyClass', 'beforeSave');
 
     expect(CoreManager.getHooksController().sendRequest.mock.calls[0]).toEqual([
       'PUT',
-      '/hooks/triggers/MyClass/beforeSave',
+      'hooks/triggers/MyClass/beforeSave',
       { __op: 'Delete' },
     ]);
+    defaultController.sendRequest = sendRequest;
+    await Hooks.removeTrigger('MyClass', 'beforeSave');
+    expect(RESTController.ajax.mock.calls[0][1]).toBe(
+      'https://api.parse.com/1/hooks/triggers/MyClass/beforeSave'
+    );
   });
 
-  it('shoud throw invalid create', async () => {
+  it('should throw invalid create', async () => {
     expect.assertions(10);
     const p1 = Hooks.create({ functionName: 'myFunction' }).catch(err => {
       expect(err.code).toBe(143);
       expect(err.error).toBe('invalid hook declaration');
     });
 
-    const p2 = Hooks.create({ url: 'http://dummy.com' }).catch(err => {
+    const p2 = Hooks.create({ url: 'http://example.com' }).catch(err => {
       expect(err.code).toBe(143);
       expect(err.error).toBe('invalid hook declaration');
     });
@@ -147,7 +212,7 @@ describe('Hooks', () => {
       expect(err.error).toBe('invalid hook declaration');
     });
 
-    const p4 = Hooks.create({ className: 'MyClass', url: 'http://dummy.com' }).catch(err => {
+    const p4 = Hooks.create({ className: 'MyClass', url: 'http://example.com' }).catch(err => {
       expect(err.code).toBe(143);
       expect(err.error).toBe('invalid hook declaration');
     });
@@ -160,7 +225,7 @@ describe('Hooks', () => {
     await Promise.all([p1, p2, p3, p4, p5]);
   });
 
-  it('shoud throw invalid update', async () => {
+  it('should throw invalid update', async () => {
     expect.assertions(6);
     const p1 = Hooks.update({ functionssName: 'myFunction' }).catch(err => {
       expect(err.code).toBe(143);
@@ -172,14 +237,14 @@ describe('Hooks', () => {
       expect(err.error).toBe('invalid hook declaration');
     });
 
-    const p3 = Hooks.update({ className: 'MyClass', url: 'http://dummy.com' }).catch(err => {
+    const p3 = Hooks.update({ className: 'MyClass', url: 'http://example.com' }).catch(err => {
       expect(err.code).toBe(143);
       expect(err.error).toBe('invalid hook declaration');
     });
     await Promise.all([p1, p2, p3]);
   });
 
-  it('shoud throw invalid remove', async () => {
+  it('should throw invalid remove', async () => {
     expect.assertions(6);
     const p1 = Hooks.remove({ functionssName: 'myFunction' }).catch(err => {
       expect(err.code).toBe(143);
@@ -191,7 +256,7 @@ describe('Hooks', () => {
       expect(err.error).toBe('invalid hook declaration');
     });
 
-    const p3 = Hooks.remove({ className: 'MyClass', url: 'http://dummy.com' }).catch(err => {
+    const p3 = Hooks.remove({ className: 'MyClass', url: 'http://example.com' }).catch(err => {
       expect(err.code).toBe(143);
       expect(err.error).toBe('invalid hook declaration');
     });