Skip to content

Adds receipt validation endpoint #515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 20, 2016
Merged
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
209 changes: 209 additions & 0 deletions spec/PurchaseValidation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
var request = require("request");



function createProduct() {
const file = new Parse.File("name", {
base64: new Buffer("download_file", "utf-8").toString("base64")
}, "text");
return file.save().then(function(){
var product = new Parse.Object("_Product");
product.set({
download: file,
icon: file,
title: "a product",
subtitle: "a product",
order: 1,
productIdentifier: "a-product"
})
return product.save();
})

}


describe("test validate_receipt endpoint", () => {

beforeEach( done => {
createProduct().then(done).fail(function(err){
console.error(err);
done();
})
})

it("should bypass appstore validation", (done) => {

request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.__type).toEqual("File");
const url = body.url;
request.get({
url: url
}, function(err, res, body) {
expect(body).toEqual("download_file");
done();
});
}
});
});

it("should fail for missing receipt", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});

it("should fail for missing product identifier", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.INVALID_JSON);
done();
}
});
});

it("should bypass appstore validation and not find product", (done) => {

request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "another-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
bypassAppStoreValidation: true
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
done();
} else {
expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
expect(body.error).toEqual('Object not found.');
done();
}
});
});

it("should fail at appstore validation", (done) => {

request.post({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'},
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
},
}
}, function(err, res, body){
if (typeof body != "object") {
fail("Body is not an object");
} else {
expect(body.status).toBe(21002);
expect(body.error).toBe('The data in the receipt-data property was malformed or missing.');
}
done();
});
});

it("should not create a _Product", (done) => {
var product = new Parse.Object("_Product");
product.save().then(function(){
fail("Should not be able to save");
done();
}, function(err){
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
done();
})
});

it("should be able to update a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.set("title", "a new title");
return product.save();
}).then(function(productAgain){
expect(productAgain.get('downloadName')).toEqual(productAgain.get('download').name());
expect(productAgain.get("title")).toEqual("a new title");
done();
}).fail(function(err){
fail(JSON.stringify(err));
done();
});
});

it("should not be able to remove a require key in a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.unset("title");
return product.save();
}).then(function(productAgain){
fail("Should not succeed");
done();
}).fail(function(err){
expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE);
expect(err.message).toEqual("title is required.");
done();
});
});

});
4 changes: 2 additions & 2 deletions src/ExportAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
// Returns a promise that resolves to the new schema.
// This does not update this.schema, because in a situation like a
// batch request, that could confuse other users of the schema.
ExportAdapter.prototype.validateObject = function(className, object) {
ExportAdapter.prototype.validateObject = function(className, object, query) {
return this.loadSchema().then((schema) => {
return schema.validateObject(className, object);
return schema.validateObject(className, object, query);
});
};

Expand Down
6 changes: 5 additions & 1 deletion src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ RestWrite.prototype.execute = function() {

// Validates this operation against the schema.
RestWrite.prototype.validateSchema = function() {
return this.config.database.validateObject(this.className, this.data);
return this.config.database.validateObject(this.className, this.data, this.query);
};

// Runs any beforeSave triggers against this operation.
Expand Down Expand Up @@ -683,6 +683,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
throw new Parse.Error(Parse.Error.SESSION_MISSING,
'cannot modify user ' + this.query.objectId);
}

if (this.className === '_Product' && this.data.download) {
this.data.downloadName = this.data.download.name;
}

// TODO: Add better detection for ACL, ensuring a user can't be locked from
// their own user record.
Expand Down
56 changes: 55 additions & 1 deletion src/Schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,23 @@ var defaultColumns = {
"sessionToken": {type:'String'},
"expiresAt": {type:'Date'},
"createdWith": {type:'Object'}
},
_Product: {
"productIdentifier": {type:'String'},
"download": {type:'File'},
"downloadName": {type:'String'},
"icon": {type:'File'},
"order": {type:'Number'},
"title": {type:'String'},
"subtitle": {type:'String'},
}
};


var requiredColumns = {
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"]
}

// Valid classes must:
// Be one of _User, _Installation, _Role, _Session OR
// Be a join table OR
Expand All @@ -75,6 +89,7 @@ function classNameIsValid(className) {
className === '_Session' ||
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
className === '_Role' ||
className === '_Product' ||
joinClassRegex.test(className) ||
//Class names have the same constraints as field names, but also allow the previous additional names.
fieldNameIsValid(className)
Expand Down Expand Up @@ -565,7 +580,7 @@ function thenValidateField(schemaPromise, className, key, type) {
// Validates an object provided in REST format.
// Returns a promise that resolves to the new schema if this object is
// valid.
Schema.prototype.validateObject = function(className, object) {
Schema.prototype.validateObject = function(className, object, query) {
var geocount = 0;
var promise = this.validateClassName(className);
for (var key in object) {
Expand All @@ -586,9 +601,48 @@ Schema.prototype.validateObject = function(className, object) {
}
promise = thenValidateField(promise, className, key, expected);
}
promise = thenValidateRequiredColumns(promise, className, object, query);
return promise;
};

// Given a schema promise, construct another schema promise that
// validates this field once the schema loads.
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
return schemaPromise.then((schema) => {
return schema.validateRequiredColumns(className, object, query);
});
}

// Validates that all the properties are set for the object
Schema.prototype.validateRequiredColumns = function(className, object, query) {

var columns = requiredColumns[className];
if (!columns || columns.length == 0) {
return Promise.resolve(this);
}

var missingColumns = columns.filter(function(column){
if (query && query.objectId) {
if (object[column] && typeof object[column] === "object") {
// Trying to delete a required column
return object[column].__op == 'Delete';
}
// Not trying to do anything there
return false;
}
return !object[column]
});

if (missingColumns.length > 0) {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
missingColumns[0]+' is required.');
}

return Promise.resolve(this);
}


// Validates an operation passes class-level-permissions set in the schema
Schema.prototype.validatePermission = function(className, aclGroup, operation) {
if (!this.perms[className] || !this.perms[className][operation]) {
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ function ParseServer(args) {
require('./functions'),
require('./schemas'),
new PushController(pushAdapter).getExpressRouter(),
new LoggerController(loggerAdapter).getExpressRouter()
new LoggerController(loggerAdapter).getExpressRouter(),
require('./validate_purchase')
];
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
routers.push(require('./global_config'));
Expand Down
Loading