diff --git a/common/models/user.json b/common/models/user.json index 582a09cb..fa33439b 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -64,6 +64,12 @@ "permission": "ALLOW", "property": "updateAttributes" }, + { + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW", + "property": "replaceById" + }, { "principalType": "ROLE", "principalId": "$everyone", diff --git a/lib/persisted-model.js b/lib/persisted-model.js index 2e10fc53..2f5faf5f 100644 --- a/lib/persisted-model.js +++ b/lib/persisted-model.js @@ -111,7 +111,8 @@ module.exports = function(registry) { * @param {Object} model Updated model instance. */ - PersistedModel.upsert = PersistedModel.updateOrCreate = function upsert(data, callback) { + PersistedModel.upsert = PersistedModel.updateOrCreate = PersistedModel.patchOrCreate = + function upsert(data, callback) { throwNotAttached(this.modelName, 'upsert'); }; @@ -494,7 +495,8 @@ module.exports = function(registry) { * @param {Object} instance Updated instance. */ - PersistedModel.prototype.updateAttributes = function updateAttributes(data, cb) { + PersistedModel.prototype.updateAttributes = PersistedModel.prototype.patchAttributes = + function updateAttributes(data, cb) { throwNotAttached(this.modelName, 'updateAttributes'); }; @@ -600,6 +602,9 @@ module.exports = function(registry) { var typeName = PersistedModel.modelName; var options = PersistedModel.settings; + // This is just for LB 3.x + options.replaceOnPUT = options.replaceOnPUT !== false; + function setRemoting(scope, name, options) { var fn = scope[name]; fn._delegate = true; @@ -616,15 +621,35 @@ module.exports = function(registry) { http: { verb: 'post', path: '/' }, }); - setRemoting(PersistedModel, 'upsert', { - aliases: ['updateOrCreate'], - description: 'Update an existing model instance or insert a new one into the data source.', + var upsertOptions = { + aliases: ['upsert', 'updateOrCreate'], + description: 'Patch an existing model instance or insert a new one into the data source.', accessType: 'WRITE', accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'Model instance data' }, returns: { arg: 'data', type: typeName, root: true }, - http: { verb: 'put', path: '/' }, - }); + http: [{ verb: 'patch', path: '/' }], + }; + + if (!options.replaceOnPUT) { + upsertOptions.http.push({ verb: 'put', path: '/' }); + } + setRemoting(PersistedModel, 'patchOrCreate', upsertOptions); + + var replaceOrCreateOptions = { + description: 'Replace an existing model instance or insert a new one into the data source.', + accessType: 'WRITE', + accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'post', path: '/replaceOrCreate' }], + }; + + if (options.replaceOnPUT) { + replaceOrCreateOptions.http.push({ verb: 'put', path: '/' }); + } + + setRemoting(PersistedModel, 'replaceOrCreate', replaceOrCreateOptions); setRemoting(PersistedModel, 'exists', { description: 'Check whether a model instance exists in the data source.', @@ -671,6 +696,26 @@ module.exports = function(registry) { rest: { after: convertNullToNotFoundError }, }); + var replaceByIdOptions = { + description: 'Replace attributes for a model instance and persist it into the data source.', + accessType: 'WRITE', + accepts: [ + { arg: 'id', type: 'any', description: 'Model id', required: true, + http: { source: 'path' }}, + { arg: 'data', type: 'object', http: { source: 'body' }, description: + 'Model instance data' }, + ], + returns: { arg: 'data', type: typeName, root: true }, + http: [{ verb: 'post', path: '/:id/replace' }], + }; + + if (options.replaceOnPUT) { + replaceByIdOptions.http.push({ verb: 'put', path: '/:id' }); + } + + setRemoting(PersistedModel, 'replaceById', replaceByIdOptions); + + setRemoting(PersistedModel, 'find', { description: 'Find all instances of the model matched by filter from the data source.', accessType: 'READ', @@ -741,13 +786,20 @@ module.exports = function(registry) { http: { verb: 'get', path: '/count' }, }); - setRemoting(PersistedModel.prototype, 'updateAttributes', { - description: 'Update attributes for a model instance and persist it into the data source.', + var updateAttributesOptions = { + aliases: ['updateAttributes'], + description: 'Patch attributes for a model instance and persist it into the data source.', accessType: 'WRITE', accepts: { arg: 'data', type: 'object', http: { source: 'body' }, description: 'An object of model property name/value pairs' }, returns: { arg: 'data', type: typeName, root: true }, - http: { verb: 'put', path: '/' }, - }); + http: [{ verb: 'patch', path: '/' }], + }; + + setRemoting(PersistedModel.prototype, 'patchAttributes', updateAttributesOptions); + + if (!options.replaceOnPUT) { + updateAttributesOptions.http.push({ verb: 'put', path: '/' }); + } if (options.trackChanges || options.enableRemoteReplication) { setRemoting(PersistedModel, 'diff', { diff --git a/test/access-control.integration.js b/test/access-control.integration.js index a08b147e..1b78272a 100644 --- a/test/access-control.integration.js +++ b/test/access-control.integration.js @@ -111,9 +111,15 @@ describe('access control - integration', function() { assert.equal(user.password, undefined); }); }); + + // user has replaceOnPUT = false; so then both PUT and PATCH should be allowed for update lt.describe.whenCalledRemotely('PUT', '/api/users/:id', function() { lt.it.shouldBeAllowed(); }); + + lt.describe.whenCalledRemotely('PATCH', '/api/users/:id', function() { + lt.it.shouldBeAllowed(); + }); }); lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForUser); @@ -163,7 +169,7 @@ describe('access control - integration', function() { } }); - describe('/accounts', function() { + describe('/accounts with replaceOnPUT true', function() { var count = 0; before(function() { var roleModel = loopback.getModelByType(loopback.Role); @@ -177,48 +183,68 @@ describe('access control - integration', function() { }); }); - lt.beforeEach.givenModel('account'); + lt.beforeEach.givenModel('accountWithReplaceOnPUTtrue'); - lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledAnonymously('GET', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', '/api/accounts-replacing'); lt.it.shouldBeDeniedWhenCalledAnonymously('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('GET', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'GET', urlForAccount); - lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts'); - lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts'); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', '/api/accounts-replacing'); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', '/api/accounts-replacing'); + + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST); lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount); lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount); + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + var actId; beforeEach(function(done) { var self = this; - // Create an account under the given user - app.models.account.create({ + app.models.accountWithReplaceOnPUTtrue.create({ userId: self.user.id, balance: 100, }, function(err, act) { - self.url = '/api/accounts/' + act.id; - + actId = act.id; + self.url = '/api/accounts-replacing/' + actId; done(); }); }); - lt.describe.whenCalledRemotely('PUT', '/api/accounts/:id', function() { + lt.describe.whenCalledRemotely('PATCH', '/api/accounts-replacing/:id', function() { lt.it.shouldBeAllowed(); }); - lt.describe.whenCalledRemotely('GET', '/api/accounts/:id', function() { + lt.describe.whenCalledRemotely('PUT', '/api/accounts-replacing/:id', function() { lt.it.shouldBeAllowed(); }); - lt.describe.whenCalledRemotely('DELETE', '/api/accounts/:id', function() { + lt.describe.whenCalledRemotely('GET', '/api/accounts-replacing/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('DELETE', '/api/accounts-replacing/:id', function() { lt.it.shouldBeDenied(); }); + describe('replace on POST verb', function() { + beforeEach(function(done) { + this.url = '/api/accounts-replacing/' + actId + '/replace'; + done(); + }); + lt.describe.whenCalledRemotely('POST', '/api/accounts-replacing/:id/replace', function() { + lt.it.shouldBeAllowed(); + }); + }); }); lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount); @@ -226,7 +252,76 @@ describe('access control - integration', function() { lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); function urlForAccount() { - return '/api/accounts/' + this.account.id; + return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id; + } + function urlForReplaceAccountPOST() { + return '/api/accounts-replacing/' + this.accountWithReplaceOnPUTtrue.id + '/replace'; + } + }); + + describe('/accounts with replaceOnPUT false', function() { + lt.beforeEach.givenModel('accountWithReplaceOnPUTfalse'); + lt.it.shouldBeDeniedWhenCalledAnonymously('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('POST', urlForReplaceAccountPOST); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'POST', urlForReplaceAccountPOST); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PUT', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PUT', urlForAccount); + + lt.it.shouldBeDeniedWhenCalledAnonymously('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('PATCH', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'PATCH', urlForAccount); + + lt.describe.whenLoggedInAsUser(CURRENT_USER, function() { + var actId; + beforeEach(function(done) { + var self = this; + // Create an account under the given user + app.models.accountWithReplaceOnPUTfalse.create({ + userId: self.user.id, + balance: 100, + }, function(err, act) { + actId = act.id; + self.url = '/api/accounts-updating/' + actId; + done(); + }); + }); + + lt.describe.whenCalledRemotely('PATCH', '/api/accounts-updating/:id', function() { + lt.it.shouldBeAllowed(); + }); + + lt.describe.whenCalledRemotely('PUT', '/api/accounts-updating/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('GET', '/api/accounts-updating/:id', function() { + lt.it.shouldBeAllowed(); + }); + lt.describe.whenCalledRemotely('DELETE', '/api/accounts-updating/:id', function() { + lt.it.shouldBeDenied(); + }); + + describe('replace on POST verb', function() { + beforeEach(function(done) { + this.url = '/api/accounts-updating/' + actId + '/replace'; + done(); + }); + lt.describe.whenCalledRemotely('POST', '/api/accounts-updating/:id/replace', function() { + lt.it.shouldBeAllowed(); + }); + }); + }); + + lt.it.shouldBeDeniedWhenCalledAnonymously('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledUnauthenticated('DELETE', urlForAccount); + lt.it.shouldBeDeniedWhenCalledByUser(CURRENT_USER, 'DELETE', urlForAccount); + + function urlForAccount() { + return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id; + } + function urlForReplaceAccountPOST() { + return '/api/accounts-updating/' + this.accountWithReplaceOnPUTfalse.id + '/replace'; } }); }); diff --git a/test/fixtures/access-control/common/models/account.json b/test/fixtures/access-control/common/models/account.json index 4607adfc..e0c08bfd 100644 --- a/test/fixtures/access-control/common/models/account.json +++ b/test/fixtures/access-control/common/models/account.json @@ -1,5 +1,6 @@ { - "name": "account", + "name": "accountWithReplaceOnPUTtrue", + "plural": "accounts-replacing", "relations": { "transactions": { "model": "transaction", @@ -38,5 +39,6 @@ "principalId": "$dummy" } ], - "properties": {} + "properties": {}, + "replaceOnPUT": true } \ No newline at end of file diff --git a/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json b/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json new file mode 100644 index 00000000..a54f9cc2 --- /dev/null +++ b/test/fixtures/access-control/common/models/accountWithReplaceOnPUTfalse.json @@ -0,0 +1,44 @@ +{ + "name": "accountWithReplaceOnPUTfalse", + "plural": "accounts-updating", + "relations": { + "transactions": { + "model": "transaction", + "type": "hasMany" + }, + "user": { + "model": "user", + "type": "belongsTo", + "foreignKey": "userId" + } + }, + "acls": [ + { + "accessType": "*", + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$everyone" + }, + { + "accessType": "*", + "permission": "ALLOW", + "principalType": "ROLE", + "principalId": "$owner" + }, + { + "permission": "DENY", + "principalType": "ROLE", + "principalId": "$owner", + "property": "deleteById" + }, + { + "accessType": "*", + "permission": "DENY", + "property": "find", + "principalType": "ROLE", + "principalId": "$dummy" + } + ], + "properties": {}, + "replaceOnPUT": false +} \ No newline at end of file diff --git a/test/fixtures/access-control/common/models/user.json b/test/fixtures/access-control/common/models/user.json index 7ecfb373..ef769cbf 100644 --- a/test/fixtures/access-control/common/models/user.json +++ b/test/fixtures/access-control/common/models/user.json @@ -19,5 +19,6 @@ "principalType": "ROLE", "principalId": "$everyone" } - ] + ], + "replaceOnPUT": false } \ No newline at end of file diff --git a/test/fixtures/access-control/server/model-config.json b/test/fixtures/access-control/server/model-config.json index 2a48ed23..2d8f66af 100644 --- a/test/fixtures/access-control/server/model-config.json +++ b/test/fixtures/access-control/server/model-config.json @@ -33,7 +33,11 @@ "public": true, "dataSource": "db" }, - "account": { + "accountWithReplaceOnPUTtrue": { + "public": true, + "dataSource": "db" + }, + "accountWithReplaceOnPUTfalse": { "public": true, "dataSource": "db" }, diff --git a/test/fixtures/simple-integration-app/common/models/store-replacing.json b/test/fixtures/simple-integration-app/common/models/store-replacing.json new file mode 100644 index 00000000..17166526 --- /dev/null +++ b/test/fixtures/simple-integration-app/common/models/store-replacing.json @@ -0,0 +1,19 @@ +{ + "name": "storeWithReplaceOnPUTtrue", + "plural": "stores-replacing", + "properties": {}, + "scopes": { + "superStores": { + "where": { + "size": "super" + } + } + }, + "relations": { + "widgets": { + "model": "widget", + "type": "hasMany" + } + }, + "replaceOnPUT": true +} \ No newline at end of file diff --git a/test/fixtures/simple-integration-app/common/models/store-updating.json b/test/fixtures/simple-integration-app/common/models/store-updating.json new file mode 100644 index 00000000..c876336c --- /dev/null +++ b/test/fixtures/simple-integration-app/common/models/store-updating.json @@ -0,0 +1,19 @@ +{ + "name": "storeWithReplaceOnPUTfalse", + "plural": "stores-updating", + "properties": {}, + "scopes": { + "superStores": { + "where": { + "size": "super" + } + } + }, + "relations": { + "widgets": { + "model": "widget", + "type": "hasMany" + } + }, + "replaceOnPUT": false +} \ No newline at end of file diff --git a/test/fixtures/simple-integration-app/server/model-config.json b/test/fixtures/simple-integration-app/server/model-config.json index f5d34f28..badfec4f 100644 --- a/test/fixtures/simple-integration-app/server/model-config.json +++ b/test/fixtures/simple-integration-app/server/model-config.json @@ -37,6 +37,14 @@ "public": true, "dataSource": "db" }, + "storeWithReplaceOnPUTfalse": { + "public": true, + "dataSource": "db" + }, + "storeWithReplaceOnPUTtrue": { + "public": true, + "dataSource": "db" + }, "physician": { "dataSource": "db", "public": true diff --git a/test/model.test.js b/test/model.test.js index 09829581..392e2059 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -624,9 +624,14 @@ describe.onServer('Remote Methods', function() { var methodNames = []; metadata.methods.forEach(function(method) { methodNames.push(method.name); - methodNames = methodNames.concat(method.sharedMethod.aliases || []); + var aliases = method.sharedMethod.aliases; + if (method.name.indexOf('prototype.') === 0) { + aliases = aliases.map(function(alias) { + return 'prototype.' + alias; + }); + } + methodNames = methodNames.concat(aliases || []); }); - expect(methodNames).to.have.members([ // NOTE(bajtos) These three methods are disabled by default // Because all tests share the same global registry model @@ -634,9 +639,11 @@ describe.onServer('Remote Methods', function() { // this test was seeing this method (with all aliases) as public // 'destroyAll', 'deleteAll', 'remove', 'create', - 'upsert', 'updateOrCreate', + 'upsert', 'updateOrCreate', 'patchOrCreate', 'exists', 'findById', + 'replaceById', + 'replaceOrCreate', 'find', 'findOne', 'updateAll', 'update', @@ -644,7 +651,7 @@ describe.onServer('Remote Methods', function() { 'destroyById', 'removeById', 'count', - 'prototype.updateAttributes', + 'prototype.patchAttributes', 'prototype.updateAttributes', 'createChangeStream', ]); }); diff --git a/test/remoting.integration.js b/test/remoting.integration.js index 37552dcf..63fe021f 100644 --- a/test/remoting.integration.js +++ b/test/remoting.integration.js @@ -74,60 +74,27 @@ describe('remoting - integration', function() { }); describe('Model shared classes', function() { - function formatReturns(m) { - var returns = m.returns; - if (!returns || returns.length === 0) { - return ''; - } - var type = returns[0].type; - return type ? ':' + type : ''; - } - - function formatMethod(m) { - return [ - m.name, - '(', - m.accepts.map(function(a) { - return a.arg + ':' + a.type; - }).join(','), - ')', - formatReturns(m), - ' ', - m.getHttpMethod(), - ' ', - m.getFullPath(), - ].join(''); - } - - function findClass(name) { - return app.handler('rest').adapter - .getClasses() - .filter(function(c) { - return c.name === name; - })[0]; - } - - it('has expected remote methods', function() { + it('has expected remote methods with default model.settings.replaceOnPUT' + + 'set to true (3.x)', + function() { var storeClass = findClass('store'); - var methods = storeClass.methods - .filter(function(m) { - return m.name.indexOf('__') === -1; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); var expectedMethods = [ 'create(data:object):store POST /stores', - 'upsert(data:object):store PUT /stores', + 'patchOrCreate(data:object):store PATCH /stores', + 'replaceOrCreate(data:object):store PUT /stores', + 'replaceOrCreate(data:object):store POST /stores/replaceOrCreate', 'exists(id:any):boolean GET /stores/:id/exists', 'findById(id:any,filter:object):store GET /stores/:id', + 'replaceById(id:any,data:object):store PUT /stores/:id', + 'replaceById(id:any,data:object):store POST /stores/:id/replace', 'find(filter:object):store GET /stores', 'findOne(filter:object):store GET /stores/findOne', 'updateAll(where:object,data:object):object POST /stores/update', 'deleteById(id:any):object DELETE /stores/:id', 'count(where:object):number GET /stores/count', - 'prototype.updateAttributes(data:object):store PUT /stores/:id', + 'prototype.patchAttributes(data:object):store PATCH /stores/:id', 'createChangeStream(options:object):ReadableStream POST /stores/change-stream', ]; @@ -138,13 +105,7 @@ describe('remoting - integration', function() { it('has expected remote methods for scopes', function() { var storeClass = findClass('store'); - var methods = storeClass.methods - .filter(function(m) { - return m.name.indexOf('__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedScopeMethods(storeClass.methods); var expectedMethods = [ '__get__superStores(filter:object):store GET /stores/superStores', @@ -159,13 +120,7 @@ describe('remoting - integration', function() { it('should have correct signatures for belongsTo methods', function() { var widgetClass = findClass('widget'); - var methods = widgetClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedPrototypeMethods(widgetClass.methods); var expectedMethods = [ 'prototype.__get__store(refresh:boolean):store ' + @@ -177,13 +132,7 @@ describe('remoting - integration', function() { it('should have correct signatures for hasMany methods', function() { var physicianClass = findClass('store'); - var methods = physicianClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedPrototypeMethods(physicianClass.methods); var expectedMethods = [ 'prototype.__findById__widgets(fk:any):widget ' + @@ -207,13 +156,7 @@ describe('remoting - integration', function() { it('should have correct signatures for hasMany-through methods', function() { // jscs:disable validateIndentation var physicianClass = findClass('physician'); - var methods = physicianClass.methods - .filter(function(m) { - return m.name.indexOf('prototype.__') === 0; - }) - .map(function(m) { - return formatMethod(m); - }); + var methods = getFormattedPrototypeMethods(physicianClass.methods); var expectedMethods = [ 'prototype.__findById__patients(fk:any):patient ' + @@ -241,3 +184,127 @@ describe('remoting - integration', function() { }); }); }); + +describe('With model.settings.replaceOnPUT false', function() { + lt.beforeEach.withApp(app); + lt.beforeEach.givenModel('storeWithReplaceOnPUTfalse'); + afterEach(function(done) { + this.app.models.storeWithReplaceOnPUTfalse.destroyAll(done); + }); + + it('should have expected remote methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTfalse'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + + var expectedMethods = [ + 'patchOrCreate(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating', + 'patchOrCreate(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTfalse POST /stores-updating/replaceOrCreate', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTfalse POST /stores-updating/:id/replace', + 'prototype.patchAttributes(data:object):storeWithReplaceOnPUTfalse PATCH /stores-updating/:id', + 'prototype.patchAttributes(data:object):storeWithReplaceOnPUTfalse PUT /stores-updating/:id', + ]; + + expect(methods).to.include.members(expectedMethods); + }); +}); + +describe('With model.settings.replaceOnPUT true', function() { + lt.beforeEach.withApp(app); + lt.beforeEach.givenModel('storeWithReplaceOnPUTtrue'); + afterEach(function(done) { + this.app.models.storeWithReplaceOnPUTtrue.destroyAll(done); + }); + + it('should have expected remote methods', + function() { + var storeClass = findClass('storeWithReplaceOnPUTtrue'); + var methods = getFormattedMethodsExcludingRelations(storeClass.methods); + + var expectedMethods = [ + 'patchOrCreate(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/replaceOrCreate', + 'replaceOrCreate(data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue POST /stores-replacing/:id/replace', + 'replaceById(id:any,data:object):storeWithReplaceOnPUTtrue PUT /stores-replacing/:id', + 'prototype.patchAttributes(data:object):storeWithReplaceOnPUTtrue PATCH /stores-replacing/:id', + ]; + + expect(methods).to.include.members(expectedMethods); + }); +}); + +function formatReturns(m) { + var returns = m.returns; + if (!returns || returns.length === 0) { + return ''; + } + var type = returns[0].type; + return type ? ':' + type : ''; +} + +function formatMethod(m) { + var arr = []; + var endpoints = m.getEndpoints(); + for (var i = 0; i < endpoints.length; i++) { + arr.push([ + m.name, + '(', + m.accepts.map(function(a) { + return a.arg + ':' + a.type; + }).join(','), + ')', + formatReturns(m), + ' ', + endpoints[i].verb, + ' ', + endpoints[i].fullPath, + ].join('')); + } + return arr; +} + +function findClass(name) { + return app.handler('rest').adapter + .getClasses() + .filter(function(c) { + return c.name === name; + })[0]; +} + +function getFormattedMethodsExcludingRelations(methods) { + return result = methods.filter(function(m) { + return m.name.indexOf('__') === -1; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +} + +function getFormattedScopeMethods(methods) { + return result = methods.filter(function(m) { + return m.name.indexOf('__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +} + +function getFormattedPrototypeMethods(methods) { + return result = methods.filter(function(m) { + return m.name.indexOf('prototype.__') === 0; + }) + .map(function(m) { + return formatMethod(m); + }) + .reduce(function(p, c) { + return p.concat(c); + }); +}