Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4102,3 +4102,213 @@ describe('sendEmail', () => {
);
});
});

describe('custom HTTP codes', () => {
it('should set custom statusCode in save hook', async () => {
Parse.Cloud.beforeSave('TestObject', (req, res) => {
res.status(201);
});

const request = await fetch('http://localhost:8378/1/classes/TestObject', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.status).toBe(201);
});

it('should set custom headers in save hook', async () => {
Parse.Cloud.beforeSave('TestObject', (req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
});

const request = await fetch('http://localhost:8378/1/classes/TestObject', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.headers.get('X-Custom-Header')).toBe('custom-value');
});

it('should set custom statusCode in delete hook', async () => {
Parse.Cloud.beforeDelete('TestObject', (req, res) => {
res.status(201);
return true
});

const obj = new Parse.Object('TestObject');
await obj.save();

const request = await fetch(`http://localhost:8378/1/classes/TestObject/${obj.id}`, {
method: 'DELETE',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.status).toBe(201);
});

it('should set custom headers in delete hook', async () => {
Parse.Cloud.beforeDelete('TestObject', (req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
});

const obj = new TestObject();
await obj.save();
const request = await fetch(`http://localhost:8378/1/classes/TestObject/${obj.id}`, {
method: 'DELETE',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.headers.get('X-Custom-Header')).toBe('custom-value');
});

it('should set custom statusCode in find hook', async () => {
Parse.Cloud.beforeFind('TestObject', (req, res) => {
res.status(201);
});

const request = await fetch('http://localhost:8378/1/classes/TestObject', {
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.status).toBe(201);
});

it('should set custom headers in find hook', async () => {
Parse.Cloud.beforeFind('TestObject', (req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
});

const request = await fetch('http://localhost:8378/1/classes/TestObject', {
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(request.headers.get('X-Custom-Header')).toBe('custom-value');
});

it('should set custom statusCode in cloud function', async () => {
Parse.Cloud.define('customStatusCode', (req, res) => {
res.status(201);
return true;
});

const response = await fetch('http://localhost:8378/1/functions/customStatusCode', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(response.status).toBe(201);
});

it('should set custom headers in cloud function', async () => {
Parse.Cloud.define('customHeaders', (req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
return true;
});

const response = await fetch('http://localhost:8378/1/functions/customHeaders', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
}
});

expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
});

it('should set custom statusCode in beforeLogin hook', async () => {
Parse.Cloud.beforeLogin((req, res) => {
res.status(201);
});

await Parse.User.signUp('[email protected]', 'password');
const response = await fetch('http://localhost:8378/1/login', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: JSON.stringify({ username: '[email protected]', password: 'password' })
});

expect(response.status).toBe(201);
});

it('should set custom headers in beforeLogin hook', async () => {
Parse.Cloud.beforeLogin((req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
});

await Parse.User.signUp('[email protected]', 'password');
const response = await fetch('http://localhost:8378/1/login', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: JSON.stringify({ username: '[email protected]', password: 'password' })
});

expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
});

it('should set custom statusCode in file trigger', async () => {
Parse.Cloud.beforeSave(Parse.File, (req, res) => {
res.status(201);
});

const file = new Parse.File('test.txt', [1, 2, 3]);
const response = await fetch('http://localhost:8378/1/files/test.txt', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'text/plain',
},
body: file.getData()
});

expect(response.status).toBe(201);
});
Comment on lines +4277 to +4294
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: file.getData() returns a Promise and must be awaited.

Parse.File.getData() returns a Promise, but it's passed directly to the fetch body without awaiting. This will cause the request body to be [object Promise] instead of the actual file data.

   it('should set custom statusCode in file trigger', async () => {
     Parse.Cloud.beforeSave(Parse.File, (req, res) => {
       res.status(201);
     });

     const file = new Parse.File('test.txt', [1, 2, 3]);
     const response = await fetch('http://localhost:8378/1/files/test.txt', {
       method: 'POST',
       headers: {
         'X-Parse-Application-Id': 'test',
         'X-Parse-REST-API-Key': 'rest',
         'Content-Type': 'text/plain',
       },
-      body: file.getData()
+      body: new Uint8Array([1, 2, 3])
     });

     expect(response.status).toBe(201);
   });

Alternatively, since the file hasn't been saved yet, you can use the raw data directly or create a proper buffer/blob for the request body.

🤖 Prompt for AI Agents
In spec/CloudCode.spec.js around lines 4277 to 4294, the test passes
file.getData() (which returns a Promise) directly as the fetch body, causing the
request body to be a Promise instead of the actual bytes; fix by awaiting the
file data before calling fetch (e.g., const data = await file.getData()) and
then pass that resolved data (or convert it to a Buffer/Blob if needed) as the
fetch body so the request sends the actual file bytes.


it('should set custom headers in file trigger', async () => {
Parse.Cloud.beforeSave(Parse.File, (req, res) => {
res.setHeader('X-Custom-Header', 'custom-value');
});

const file = new Parse.File('test.txt', [1, 2, 3]);
const response = await fetch('http://localhost:8378/1/files/test.txt', {
method: 'POST',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'text/plain',
},
body: file.getData()
});

expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
});
Comment on lines +4296 to +4313
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same issue: file.getData() returns a Promise.

This test has the same problem as the previous one.

   it('should set custom headers in file trigger', async () => {
     Parse.Cloud.beforeSave(Parse.File, (req, res) => {
       res.setHeader('X-Custom-Header', 'custom-value');
     });

     const file = new Parse.File('test.txt', [1, 2, 3]);
     const response = await fetch('http://localhost:8378/1/files/test.txt', {
       method: 'POST',
       headers: {
         'X-Parse-Application-Id': 'test',
         'X-Parse-REST-API-Key': 'rest',
         'Content-Type': 'text/plain',
       },
-      body: file.getData()
+      body: new Uint8Array([1, 2, 3])
     });

     expect(response.headers.get('X-Custom-Header')).toBe('custom-value');
   });
🤖 Prompt for AI Agents
In spec/CloudCode.spec.js around lines 4296 to 4313, the test passes
file.getData() directly to fetch even though file.getData() returns a Promise;
await the promise before using it as the request body (e.g., const data = await
file.getData(); use data in fetch) so the fetch receives the actual file data
rather than a Promise.

})
2 changes: 1 addition & 1 deletion spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const defaultConfiguration = {
readOnlyMasterKey: 'read-only-test',
fileKey: 'test',
directAccess: true,
silent,
silent: false,
verbose: !silent,
logLevel,
liveQuery: {
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/AdaptableController.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class AdaptableController {
}, {});

if (Object.keys(mismatches).length > 0) {
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
// throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/LiveQueryController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
import { LiveQueryOptions } from '../Options';
import { getClassName } from './../triggers';
import { getClassName } from '../triggers';
export class LiveQueryController {
classNames: any;
liveQueryPublisher: any;
Expand Down
87 changes: 38 additions & 49 deletions src/PromiseRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import Parse from 'parse/node';
import express from 'express';
import log from './logger';
import { inspect } from 'util';
const Layer = require('express/lib/router/layer');

function validateParameter(key, value) {
Expand Down Expand Up @@ -135,68 +134,58 @@ export default class PromiseRouter {
// Express handlers should never throw; if a promise handler throws we
// just treat it like it resolved to an error.
function makeExpressHandler(appId, promiseHandler) {
return function (req, res, next) {
return async function (req, res, next) {
try {
const url = maskSensitiveUrl(req);
const body = Object.assign({}, req.body);
const body = { ...req.body };
const method = req.method;
const headers = req.headers;

log.logRequest({
method,
url,
headers,
body,
});
promiseHandler(req)
.then(
result => {
if (!result.response && !result.location && !result.text) {
log.error('the handler did not include a "response" or a "location" field');
throw 'control should not get here';
}

log.logResponse({ method, url, result });

var status = result.status || 200;
res.status(status);

if (result.headers) {
Object.keys(result.headers).forEach(header => {
res.set(header, result.headers[header]);
});
}

if (result.text) {
res.send(result.text);
return;
}

if (result.location) {
res.set('Location', result.location);
// Override the default expressjs response
// as it double encodes %encoded chars in URL
if (!result.response) {
res.send('Found. Redirecting to ' + result.location);
return;
}
}
res.json(result.response);
},
error => {
next(error);
}
)
.catch(e => {
log.error(`Error generating response. ${inspect(e)}`, { error: e });
next(e);
});
} catch (e) {
log.error(`Error handling request: ${inspect(e)}`, { error: e });
next(e);

const result = await promiseHandler(req);
if (!result.response && !result.location && !result.text) {
log.error('The handler did not include a "response", "location", or "text" field');
throw new Error('Handler result is missing required fields.');
}

log.logResponse({ method, url, result });

const status = result.status || 200;
res.status(status);

if (result.headers) {
for (const [header, value] of Object.entries(result.headers)) {
res.set(header, value);
}
}

if (result.text) {
res.send(result.text);
return;
}

if (result.location) {
res.set('Location', result.location);
if (!result.response) {
res.send(`Found. Redirecting to ${result.location}`);
return;
}
}

res.json(result.response);
} catch (error) {
next(error);
}
};
}


function maskSensitiveUrl(req) {
let maskUrl = req.originalUrl.toString();
const shouldMaskUrl =
Expand Down
4 changes: 3 additions & 1 deletion src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ async function RestQuery({
runAfterFind = true,
runBeforeFind = true,
context,
response
}) {
if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
Expand All @@ -60,7 +61,8 @@ async function RestQuery({
config,
auth,
context,
method === RestQuery.Method.get
method === RestQuery.Method.get,
response
)
: Promise.resolve({ restWhere, restOptions });

Expand Down
Loading
Loading