diff --git a/package.json b/package.json index 95149da..06ac00f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorm", - "version": "0.4.1", + "version": "0.5.8", "description": "Javascript ORM", "main": "_bundles/jsorm.js", "module": "lib-esm/index.js", diff --git a/src/configuration.ts b/src/configuration.ts index 7a9a0f3..ae2db9d 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -17,6 +17,8 @@ export default class Config { static logger: Logger = new Logger(); static jwtLocalStorage: string | false = 'jwt'; static localStorage; + static beforeFetch: Array = [] + static afterFetch: Array = [] static setup(options? : Object) : void { if (!options) options = {}; diff --git a/src/custom-extend.ts b/src/custom-extend.ts index ae51d0f..1b5caa9 100644 --- a/src/custom-extend.ts +++ b/src/custom-extend.ts @@ -3,7 +3,7 @@ // Allows 'inherited' hook let globalObj; -if (typeof window === 'undefined') { +if (typeof window === 'undefined' || window != (global as any)) { globalObj = global; } else { globalObj = window; @@ -15,7 +15,7 @@ const patchExtends = function() { Object['setPrototypeOf'] = function(subClass, superClass) { originalSetPrototypeOf(subClass, superClass); - if(superClass['inherited']) { + if(superClass['inherited'] && superClass['jsormVersion'] == 0) { superClass['inherited'](subClass); } } diff --git a/src/model.ts b/src/model.ts index a420b18..d16144f 100644 --- a/src/model.ts +++ b/src/model.ts @@ -44,6 +44,7 @@ export default class Model { klass: typeof Model; static attributeList = {}; + static jsormVersion = 0; private static _scope: Scope; static extend(obj : any) : any { @@ -63,6 +64,10 @@ export default class Model { static setJWT(token: string) : void { this.getJWTOwner().jwt = token; + + if (Config.jwtLocalStorage) { + Config.localStorage.setItem(Config.jwtLocalStorage, token) + } } static getJWT() : string { @@ -82,12 +87,24 @@ export default class Model { } if (this.getJWT()) { - options.headers.Authorization = `Token token="${this.getJWT()}"`; + options.headers.Authorization = this.generateAuthHeader(this.getJWT()); } return options } + static beforeFetch(url: RequestInfo, options: RequestInit) : void { + Config.beforeFetch.forEach((fn) => { + fn(url, options) + }) + } + + static afterFetch(response: Response, json: JSON) : void { + Config.afterFetch.forEach((fn) => { + fn(response, json) + }) + } + static getJWTOwner() : typeof Model { if (this.isJWTOwner) { return this; @@ -167,6 +184,10 @@ export default class Model { return deserialize(resource, payload); } + static generateAuthHeader(jwt: string) : string { + return `Token token="${jwt}"`; + } + constructor(attributes?: Object) { this._initializeAttributes(); this.attributes = attributes; @@ -215,7 +236,7 @@ export default class Model { if (this.klass.camelizeKeys) { attributeName = camelize(key); } - + if (key == 'id' || this.klass.attributeList[attributeName]) { this[attributeName] = attrs[key]; } @@ -281,7 +302,7 @@ export default class Model { destroy() : Promise { let url = this.klass.url(this.id); let verb = 'delete'; - let request = new Request(); + let request = new Request(this.klass); let requestPromise = request.delete(url, this._fetchOptions()); return this._writeRequest(requestPromise, () => { @@ -292,19 +313,18 @@ export default class Model { save(options: Object = {}) : Promise { let url = this.klass.url(); let verb = 'post'; - let request = new Request(); + let request = new Request(this.klass); let payload = new WritePayload(this, options['with']); if (this.isPersisted()) { url = this.klass.url(this.id); - verb = 'put'; + verb = 'patch'; } let json = payload.asJSON(); let requestPromise = request[verb](url, json, this._fetchOptions()); return this._writeRequest(requestPromise, (response) => { this.fromJsonapi(response['jsonPayload'].data, response['jsonPayload'], payload.includeDirective); - //this.isPersisted(true); payload.postProcess(); }); } @@ -320,10 +340,9 @@ export default class Model { private _writeRequest(requestPromise : Promise, callback: Function) : Promise { return new Promise((resolve, reject) => { - requestPromise.catch((e) => { throw(e) }); return requestPromise.then((response) => { this._handleResponse(response, resolve, reject, callback); - }); + }).catch(reject) }); } @@ -333,8 +352,6 @@ export default class Model { if (response.status == 422) { ValidationErrors.apply(this, response['jsonPayload']); resolve(false); - } else if (response.status >= 500) { - reject('Server Error'); } else { callback(response); resolve(true); diff --git a/src/request.ts b/src/request.ts index 39c26d5..2e560a8 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,10 +1,47 @@ import Config from './configuration'; +import Model from './model'; import colorize from './util/colorize'; +import patchExtends from './custom-extend'; +patchExtends() + +class RequestError extends Error { + url: string + options: RequestInit + originalError: Error + + constructor(message: string, url: string, options: RequestInit, originalError: Error) { + super(message) + this.url = url + this.options = options + this.originalError = originalError + } +} + +class ResponseError extends Error { + response: Response + originalError: Error + + constructor(response: Response | null, message?: string, originalError?: Error) { + super(message || 'Invalid Response') + this.response = response + this.originalError = originalError + } +} export default class Request { + modelClass: typeof Model + + constructor(modelClass: typeof Model) { + this.modelClass = modelClass + } + get(url : string, options: RequestInit) : Promise { - options.method = 'GET'; - return this._fetchWithLogging(url, options); + options.method = 'GET' + // Temp fix just in this version of jsorm + // Certain versions of ie11 will cache the response + options.headers['Pragma'] = 'no-cache' + options.headers['Cache-Control'] = 'no-cache, no-store' + return this._fetchWithLogging(url, options) } post(url: string, payload: Object, options: RequestInit) : Promise { @@ -14,8 +51,8 @@ export default class Request { return this._fetchWithLogging(url, options); } - put(url: string, payload: Object, options: RequestInit) : Promise { - options.method = 'PUT'; + patch(url: string, payload: Object, options: RequestInit) : Promise { + options.method = 'PATCH'; options.body = JSON.stringify(payload); return this._fetchWithLogging(url, options); @@ -39,23 +76,54 @@ export default class Request { private _fetchWithLogging(url: string, options: RequestInit) : Promise { this._logRequest(options.method, url); let promise = this._fetch(url, options); - promise.then((response : any) => { + return promise.then((response : any) => { this._logResponse(response['jsonPayload']); + return response }); - return promise; } private _fetch(url: string, options: RequestInit) : Promise { return new Promise((resolve, reject) => { + try { + this.modelClass.beforeFetch(url, options) + } catch(e) { + reject(new RequestError('beforeFetch failed; review Config.beforeFetch', url, options, e)) + } + let fetchPromise = fetch(url, options); fetchPromise.then((response) => { - response.json().then((json) => { - response['jsonPayload'] = json; - resolve(response); - }).catch((e) => { throw(e); }); + this._handleResponse(response, resolve, reject) }); - fetchPromise.catch(reject); + fetchPromise.catch((e) => { + // Fetch itself failed (usually network error) + reject(new ResponseError(null, e.message, e)) + }) + }); + } + + private _handleResponse(response: Response, resolve: Function, reject: Function) : void { + response.json().then((json) => { + try { + this.modelClass.afterFetch(response, json) + } catch(e) { + // afterFetch middleware failed + reject(new ResponseError(response, 'afterFetch failed; review Config.afterFetch', e)) + } + + if (response.status >= 500) { + reject(new ResponseError(response, 'Server Error')) + } else if (response.status !== 422 && json['data'] === undefined) { + // Bad JSON, for instance an errors payload + // Allow 422 since we specially handle validation errors + reject(new ResponseError(response, 'invalid json')) + } + + response['jsonPayload'] = json; + resolve(response); + }).catch((e) => { + // The response was probably not in JSON format + reject(new ResponseError(response, 'invalid json', e)) }); } } diff --git a/src/scope.ts b/src/scope.ts index 94f69dd..d8e92fd 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -29,7 +29,7 @@ export default class Scope { return this._fetch(this.model.url()).then((json : japiDoc) => { let collection = new CollectionProxy(json); return collection; - }); + }) } find(id : string | number) : Promise> { @@ -215,16 +215,16 @@ export default class Scope { } private _fetch(url : string) : Promise { - let qp = this.toQueryParams(); + let qp = this.toQueryParams() if (qp) { - url = `${url}?${qp}`; + url = `${url}?${qp}` } - let request = new Request(); + let request = new Request(this.model) let fetchOpts = this.model.fetchOptions() - - return request.get(url, fetchOpts).then((response) => { - refreshJWT(this.model, response); - return response['jsonPayload']; - }); + let promise = request.get(url, fetchOpts) + return promise.then((response) => { + refreshJWT(this.model, response) + return response['jsonPayload'] + }) } } diff --git a/src/util/extend.ts b/src/util/extend.ts index 936950f..6de6734 100644 --- a/src/util/extend.ts +++ b/src/util/extend.ts @@ -2,7 +2,7 @@ // use for non-typescript extends let globalObj; -if (typeof window === 'undefined') { +if (typeof window === 'undefined' || window !== (global as any)) { globalObj = global; } else { globalObj = window; diff --git a/src/util/refresh-jwt.ts b/src/util/refresh-jwt.ts index 2eb74c4..20d453c 100644 --- a/src/util/refresh-jwt.ts +++ b/src/util/refresh-jwt.ts @@ -3,8 +3,9 @@ import Config from '../configuration'; export default function refreshJWT(klass: typeof Model, serverResponse: Response) : void { let jwt = serverResponse.headers.get('X-JWT'); - let localStorage = Config.localStorage; + if (!jwt) return + let localStorage = Config.localStorage; if (localStorage) { let localStorageKey = Config.jwtLocalStorage; if (localStorageKey) { diff --git a/src/util/string.ts b/src/util/string.ts index 8b9f2df..e165879 100644 --- a/src/util/string.ts +++ b/src/util/string.ts @@ -6,4 +6,8 @@ const camelize = function(str) { return str.replace(/(\_[a-z])/g, function($1){return $1.toUpperCase().replace('_','');}); } -export { underscore, camelize }; +const decamelize = function(str) { + return str.replace(/([A-Z])/g, function($1){return $1.replace($1,'_' + $1).toLowerCase();}); +} + +export { underscore, camelize, decamelize }; diff --git a/src/util/write-payload.ts b/src/util/write-payload.ts index 61a1fb9..8daadb3 100644 --- a/src/util/write-payload.ts +++ b/src/util/write-payload.ts @@ -2,6 +2,7 @@ import Model from '../model'; import IncludeDirective from './include-directive'; import * as _snakeCase from './snakecase'; import tempId from './temp-id'; +import { decamelize } from './string'; let snakeCase: any = (_snakeCase).default || _snakeCase; snakeCase = snakeCase['default'] || snakeCase; @@ -87,7 +88,7 @@ export default class WritePayload { } if (data) { - _relationships[key] = { data } + _relationships[decamelize(key)] = { data } } } }); diff --git a/test/fixtures.ts b/test/fixtures.ts index 7c929ad..1268575 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -106,6 +106,7 @@ const configSetup = function(opts = {}) { configSetup(); export { + Config, configSetup, ApplicationRecord, TestJWTSubclass, diff --git a/test/integration/authorization-test.ts b/test/integration/authorization-test.ts index 0d9e373..f3e8a51 100644 --- a/test/integration/authorization-test.ts +++ b/test/integration/authorization-test.ts @@ -1,6 +1,6 @@ import { sinon, expect, fetchMock } from '../test-helper'; import { Config } from '../../src/index'; -import { configSetup, ApplicationRecord, Author } from '../fixtures'; +import { configSetup, ApplicationRecord, Author, Person } from '../fixtures'; after(function () { fetchMock.restore(); @@ -27,6 +27,30 @@ describe('authorization headers', function() { }); }); + describe('when header is set in a custom generateAuthHeader', function() { + let originalHeaderFn = Person.generateAuthHeader; + beforeEach(function() { + ApplicationRecord.jwt = 'cu570m70k3n'; + Person.generateAuthHeader = function(token) { + return `Bearer ${token}`; + }; + }); + + afterEach(function() { + fetchMock.restore(); + Person.generateAuthHeader = originalHeaderFn; + }); + + it("sends the custom Authorization token in the request's headers", function(done) { + fetchMock.mock((url, opts) => { + expect(opts.headers.Authorization).to.eq('Bearer cu570m70k3n'); + done(); + return true; + }, 200); + Person.find(1); + }); + }); + describe('when header is NOT returned in response', function() { beforeEach(function() { fetchMock.get('http://example.com/api/v1/authors', { diff --git a/test/integration/fetch-middleware-test.ts b/test/integration/fetch-middleware-test.ts new file mode 100644 index 0000000..bcc28e0 --- /dev/null +++ b/test/integration/fetch-middleware-test.ts @@ -0,0 +1,366 @@ +import { expect, sinon, fetchMock } from '../test-helper'; +import { Author, Config } from '../fixtures'; + +const mock401 = function() { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '*', + response: { + status: 401, + body: { + errors: [ + { + code: 'unauthenticated', + status: '401', + title: 'Authentication Error', + detail: 'You must sign in to access this resource', + meta: { } + } + ] + } + } + }) +} + +const mockBadJSON = function() { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '*', + response: { + status: 500, + body: undefined + } + }) +} + +const mock500 = function() { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '*', + response: { + status: 500, + body: { + errors: [] + } + } + }) +} + +const mockSuccess = function() { + fetchMock.restore(); + + fetchMock.mock({ + matcher: '*', + response: { + status: 200, + body: { + data: [] + } + } + }) +} + + +let before = {} as any +let after = {} as any +describe('fetch middleware', function() { + beforeEach(function () { + mockSuccess(); + + Config.beforeFetch.push(function(url, options) { + before = { url, options } + + // Author.first, or saving author with name 'abortme' + // should abort + let shouldAbort = false + if (url.indexOf('page') > -1) shouldAbort = true + if (options.body && options.body.indexOf('abortme') > -1) { + shouldAbort = true + } + + if (shouldAbort) { + throw('abort') + } + }) + + Config.afterFetch.push(function(response, json) { + after = { response, json } + + if (response.status == 401) { + throw('abort') + } + }) + }); + + afterEach(function() { + fetchMock.restore() + Config.beforeFetch = [] + Config.afterFetch = [] + before = {} + after = {} + }) + + describe('reads', function() { + describe('on successful response', function() { + it('correctly resolves the promise', function() { + return Author.all().then(({data}) => { + expect(data).to.deep.eq([]) + }) + }) + + it('runs beforeEach hooks', function() { + return Author.all().then(({data}) => { + expect(before.url).to.eq('http://example.com/api/v1/authors') + expect(before.options).to.deep.eq({ + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "Cache-Control": "no-cache, no-store", + "Pragma": "no-cache" + }, + method: 'GET' + }) + }) + }) + + it('runs afterEach hooks', function() { + return Author.all().then(({data}) => { + expect(after.response.status).to.eq(200) + }) + }) + }) + + describe('when beforeFetch middleware aborts', function() { + beforeEach(function() { + mockSuccess() + }) + + it('rejects the promise w/correct RequestError class', function() { + return Author.first().then(({data}) => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.message).to + .eq('beforeFetch failed; review Config.beforeFetch') + expect(e.originalError).to.eq('abort') + expect(e.url).to.eq('http://example.com/api/v1/authors?page[size]=1') + }) + }) + }) + + describe('when afterFetch middleware aborts', function() { + beforeEach(function() { + mock401() + }) + + it('rejects the promise w/correct ResponseError class', function() { + return Author.all().then(({data}) => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.message).to + .eq('afterFetch failed; review Config.afterFetch') + expect(e.response.status).to.eq(401) + expect(e.originalError).to.eq('abort') + }) + }) + }) + + describe('on 500 response', function() { + beforeEach(function() { + mock500() + }) + + it('rejects the promise with the response', function() { + return Author.all().then(({data}) => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.response.statusText).to.eq('Internal Server Error') + }) + }) + }) + + describe('on bad json response', function() { + beforeEach(function() { + mockBadJSON() + }) + + it('rejects the promise with original error', function() { + return Author.all().then(({data}) => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.response.statusText).to.eq('Internal Server Error') + expect(e.originalError.message) + .to.contain('Unexpected end of JSON input') + }) + }) + }) + + describe('when the model overrides the hooks', function() { + let originalBeforeFetch; + let originalAfterFetch; + + beforeEach(function() { + originalBeforeFetch = Author.beforeFetch + originalAfterFetch = Author.afterFetch + + Author.beforeFetch = function(url, options) { + before.overridden = true + } + + Author.afterFetch = function(url, options) { + after.overridden = true + } + }) + + afterEach(function() { + Author.beforeFetch = originalBeforeFetch + Author.afterFetch = originalAfterFetch + }) + + it('uses the override', function() { + Author.all().then(() => { + expect(before).to.deep.eq({ overridden: true }) + expect(after).to.deep.eq({ overridden: true }) + }) + }) + }) + }) + + describe('writes', function() { + describe('on successful response', function() { + it('correctly resolves the promise', function() { + let author = new Author() + return author.save().then((success) => { + expect(success).to.eq(true) + }) + }) + + it('runs beforeEach hooks', function() { + let author = new Author() + return author.save().then(() => { + expect(before.url).to.eq('http://example.com/api/v1/authors') + expect(before.options).to.deep.eq({ + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ data: { type: 'authors' } }), + method: 'POST' + }) + }) + }) + + it('runs afterEach hooks', function() { + let author = new Author() + return author.save().then(() => { + expect(after.response.status).to.eq(200) + }) + }) + }) + + describe('when beforeFetch middleware aborts', function() { + it('rejects the promise w/correct RequestError class', function() { + let author = new Author({ firstName: 'abortme' }) + return author.save().then(() => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.message).to + .eq('beforeFetch failed; review Config.beforeFetch') + expect(e.originalError).to.eq('abort') + expect(e.url).to.eq('http://example.com/api/v1/authors') + }) + }) + }) + + describe('when afterFetch middleware aborts', function() { + beforeEach(function() { + mock401() + }) + + it('rejects the promise w/correct ResponseError class', function() { + let author = new Author() + return author.save().then(() => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.message).to + .eq('afterFetch failed; review Config.afterFetch') + expect(e.response.status).to.eq(401) + expect(e.originalError).to.eq('abort') + }) + }) + }) + + describe('on 500 response', function() { + beforeEach(function() { + mock500() + }) + + it('rejects the promise with the response', function() { + let author = new Author() + return author.save().then(() => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.response.statusText).to.eq('Internal Server Error') + }) + }) + }) + + describe('on bad json response', function() { + beforeEach(function() { + mockBadJSON() + }) + + it('rejects the promise with original error', function() { + let author = new Author() + return author.save().then(() => { + expect('dont get here!').to.eq(true) + }) + .catch((e) => { + expect(e.response.statusText).to.eq('Internal Server Error') + expect(e.originalError.message) + .to.contain('Unexpected end of JSON input') + }) + }) + }) + + describe('when the model overrides the hooks', function() { + let originalBeforeFetch; + let originalAfterFetch; + + beforeEach(function() { + originalBeforeFetch = Author.beforeFetch + originalAfterFetch = Author.afterFetch + + Author.beforeFetch = function(url, options) { + before.overridden = true + } + + Author.afterFetch = function(url, options) { + after.overridden = true + } + }) + + afterEach(function() { + Author.beforeFetch = originalBeforeFetch + Author.afterFetch = originalAfterFetch + }) + + it('uses the override', function() { + let author = new Author() + author.save().then(() => { + expect(before).to.deep.eq({ overridden: true }) + expect(after).to.deep.eq({ overridden: true }) + }) + }) + }) + }) +}) diff --git a/test/integration/nested-persistence-test.ts b/test/integration/nested-persistence-test.ts index 6b481e4..13d8bb3 100644 --- a/test/integration/nested-persistence-test.ts +++ b/test/integration/nested-persistence-test.ts @@ -6,7 +6,7 @@ let fetchMock = require('fetch-mock'); let instance; let payloads; -let putPayloads; +let patchPayloads; let deletePayloads; let serverResponse; @@ -18,8 +18,8 @@ const resetMocks = function() { return serverResponse; }); - fetchMock.put('http://example.com/api/v1/authors/1', function(url, payload) { - putPayloads.push(JSON.parse(payload.body)); + fetchMock.patch('http://example.com/api/v1/authors/1', function(url, payload) { + patchPayloads.push(JSON.parse(payload.body)); return serverResponse; }); @@ -42,6 +42,15 @@ let expectedCreatePayload = { method: 'create' } ] + }, + special_books: { + data: [ + { + ['temp-id']: 'abc3', + type: 'books', + method: 'create' + } + ] } } }, @@ -68,6 +77,13 @@ let expectedCreatePayload = { attributes: { name: 'Horror' } + }, + { + ['temp-id']: 'abc3', + type: 'books', + attributes: { + title: 'The Stand' + } } ] }; @@ -122,8 +138,11 @@ const seedPersistedData = function() { genre.isPersisted(true); let book = new Book({ id: '10', title: 'The Shining', genre: genre }); book.isPersisted(true); + let specialBook = new Book({ id: '30', title: 'The Stand' }); + specialBook.isPersisted(true); instance.id = '1'; instance.books = [book]; + instance.specialBooks = [specialBook]; instance.isPersisted(true); genre.name = 'Updated Genre Name'; book.title = 'Updated Book Title'; @@ -132,7 +151,7 @@ const seedPersistedData = function() { describe('nested persistence', function() { beforeEach(function() { payloads = []; - putPayloads = []; + patchPayloads = []; deletePayloads = []; instance = new Author({ firstName: 'Stephen' }); serverResponse = { @@ -169,6 +188,12 @@ describe('nested persistence', function() { id: '20', type: 'genres', attributes: { name: 'name from server' } + }, + { + ['temp-id']: 'abc3', + id: '30', + type: 'books', + attributes: { title: 'another title from server' } } ] } @@ -199,14 +224,16 @@ describe('nested persistence', function() { beforeEach(function() { let genre = new Genre({ name: 'Horror' }); let book = new Book({ title: 'The Shining', genre: genre }); + let specialBook = new Book({ title: 'The Stand' }); instance.books = [book]; + instance.specialBooks = [specialBook]; }); // todo test on the way back - id set, attrs updated, isPersisted // todo remove #destroy? and just save when markwithpersisted? combo? for ombined payload // todo test unique includes/circular relationshio it('sends the correct payload', function(done) { - instance.save({ with: { books: 'genre' } }).then((response) => { + instance.save({ with: { books: 'genre', specialBooks: {} } }).then((response) => { expect(payloads[0]).to.deep.equal(expectedCreatePayload); done(); }); @@ -262,7 +289,7 @@ describe('nested persistence', function() { it('sends the correct payload', function(done) { instance.save({ with: { books: 'genre' } }).then((response) => { - expect(putPayloads[0]).to.deep.equal(expectedUpdatePayload('update')); + expect(patchPayloads[0]).to.deep.equal(expectedUpdatePayload('update')); done(); }); }); @@ -277,7 +304,7 @@ describe('nested persistence', function() { instance.books[0].isMarkedForDestruction(true); instance.books[0].genre.isMarkedForDestruction(true); instance.save({ with: { books: 'genre' } }).then((response) => { - expect(putPayloads[0]).to.deep.equal(expectedUpdatePayload('destroy')); + expect(patchPayloads[0]).to.deep.equal(expectedUpdatePayload('destroy')); done(); }); }); @@ -308,7 +335,7 @@ describe('nested persistence', function() { instance.books[0].isMarkedForDisassociation(true); instance.books[0].genre.isMarkedForDisassociation(true); instance.save({ with: { books: 'genre' } }).then((response) => { - expect(putPayloads[0]).to.deep.equal(expectedUpdatePayload('disassociate')); + expect(patchPayloads[0]).to.deep.equal(expectedUpdatePayload('disassociate')); done(); }); }); diff --git a/test/integration/persistence-test.ts b/test/integration/persistence-test.ts index 7caf2b0..4225cea 100644 --- a/test/integration/persistence-test.ts +++ b/test/integration/persistence-test.ts @@ -9,12 +9,12 @@ after(function () { let instance; let payloads; -let putPayloads; +let patchPayloads; let deletePayloads; let serverResponse; beforeEach(function() { payloads = []; - putPayloads = []; + patchPayloads = []; deletePayloads = []; instance = new Person(); serverResponse = { @@ -34,8 +34,8 @@ const resetMocks = function() { return serverResponse; }); - fetchMock.put('http://example.com/api/v1/people/1', function(url, payload) { - putPayloads.push(JSON.parse(payload.body)); + fetchMock.patch('http://example.com/api/v1/people/1', function(url, payload) { + patchPayloads.push(JSON.parse(payload.body)); return serverResponse; }); @@ -71,7 +71,7 @@ describe('Model persistence', function() { it('updates instead of creates', function(done) { instance.firstName = 'Joe'; instance.save().then(() => { - expect(putPayloads[0]).to.deep.equal({ + expect(patchPayloads[0]).to.deep.equal({ data: { id: '1', type: 'people', @@ -101,8 +101,8 @@ describe('Model persistence', function() { it('does not send attributes to the server', function(done) { instance.save().then(() => { - console.log(putPayloads[0]) - expect(putPayloads[0]).to.deep.equal({ + console.log(patchPayloads[0]) + expect(patchPayloads[0]).to.deep.equal({ data: { id: '1', type: 'people' @@ -188,7 +188,7 @@ describe('Model persistence', function() { it('rejects the promise', function(done) { instance.save().catch((err) => { - expect(err).to.eq('Server Error'); + expect(err.message).to.eq('Server Error'); done(); }); }); @@ -304,7 +304,7 @@ describe('Model persistence', function() { it('rejects the promise', function() { instance.destroy().catch((err) => { - expect(err).to.eq('Server Error'); + expect(err.message).to.eq('Server Error'); }); }); }); diff --git a/test/unit/model-test.ts b/test/unit/model-test.ts index 7230d91..b1840ac 100644 --- a/test/unit/model-test.ts +++ b/test/unit/model-test.ts @@ -132,6 +132,25 @@ describe('Model', function() { Author.setJWT('n3wt0k3n'); expect(ApplicationRecord.jwt).to.eq('n3wt0k3n'); }); + + describe('when localStorage is configured', function() { + beforeEach(function() { + Config.jwtLocalStorage = 'jwt' + Config.localStorage = { setItem: sinon.spy() } + }) + + afterEach(function() { + Config.jwtLocalStorage = undefined + Config.localStorage = undefined + }) + + it('adds to localStorage', function() { + Author.setJWT('n3wt0k3n'); + let called = Config.localStorage.setItem + .calledWith('jwt', 'n3wt0k3n'); + expect(called).to.eq(true); + }) + }) }); describe('#fetchOptions', function() {