rxrest
Version:
RxRest a reactive REST utility
766 lines (632 loc) • 20.1 kB
JavaScript
/* global describe it before beforeEach after */
const chai = require('chai')
const spies = require('chai-spies')
chai.use(spies)
const expect = chai.expect
const express = require('express')
const {from, of, merge} = require('rxjs')
const {mergeMap, switchMap, delay} = require('rxjs/operators')
const {Headers, Response, Request} = require('node-fetch')
require('./urlsearchparamspolyfill.js')
global.Headers = Headers
global.Response = Response
global.Request = Request
global.FormData = require('form-data')
const {RxRestConfiguration, RxRestItem, RxRestCollection, RxRest} = require('../lib/index.js')
let temp = new RxRestConfiguration()
const fetch = temp.fetch
let rxrest
let config
let server
describe('RxRest', function () {
before(function (cb) {
const app = express()
const bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}))
app.get('/test', function (req, res) {
res.json([{foo: req.query.foo, id: 3}])
})
app.get('/test/:id', function (req, res) {
res.json({foo: req.query.foo, id: parseInt(req.params.id)})
})
app.patch('/test/3', function (req, res) {
res.end()
})
app.trace('/test/3', function (req, res) {
res.end()
})
app.post('/test', function (req, res) {
req.body.method = 'post'
req.body.id = 4
return res.status(201).json(req.body)
})
app.put('/test/:id', function (req, res) {
req.body.method = 'put'
return res.status(200).json(req.body)
})
app.head('/404', function (req, res) {
res.status(404).send('fail')
})
app.delete('/test/:id', function (req, res) {
res.json({'method': 'delete'})
})
app.delete('/foobar', function (req, res) {
res.set('Content-Length', 0).status(204).end()
})
app.get('/error', function (req, res) {
res.status(500).send('fail')
})
app.get('/empty', function (req, res) {
res.status(200).json([])
})
app.get('/timeout', function (req, res) {
setTimeout(() => {
res.status(504).end()
}, 100)
})
app.post('/resource', function (req, res) {
res.status(201).json({id: 1})
})
// force id from above
app.put('/resource/1', function (req, res) {
res.status(200).json({id: 1})
})
server = app.listen(3333, cb)
})
beforeEach(function () {
config = new RxRestConfiguration()
config.baseURL = 'http://localhost:3333'
expect(config.baseURL).to.equal('http://localhost:3333/')
config.identifier = 'id'
expect(config.identifier).to.equal('id')
rxrest = new RxRest(config)
})
it.skip('should use a new instance', function () {
let i = 0
return rxrest.all('test')
.get()
.toPromise()
.then((data) => {
data.push(new RxRestItem('test', {id: 5}))
return from(data.map(item => rxrest.one('test', item.id)))
.pipe(
mergeMap(item => item.get({foo: 'bar'}))
)
.subscribe(e => {
if (i === 0) {
expect(e.id).to.equal(3)
expect(e.foo).to.equal('bar')
i++
return
}
expect(e.foo).to.equal('bar')
expect(e.id).to.equal(5)
})
})
})
it('should get one', function () {
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.one('test', 3)
.get({foo: 'foo'}, {'Accept': 'application/json'})
.subscribe(item => {
expect(item.$fromServer).to.be.true
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.plain()).to.deep.equal({foo: 'foo', id: 3})
expect(item).to.have.ownProperty('foo', 'foo')
item.bar = 'bar'
delete item.foo
Object.defineProperty(item, 'foobar', {
value: 'foobar',
enumerable: true
})
//can't override internal property
Object.defineProperty(item, '$element', {
value: 'foobar',
enumerable: true
})
expect(item.plain()).to.deep.equal({bar: 'bar', id: 3, foobar: 'foobar'})
let clone = item.clone()
expect(clone.plain()).to.deep.equal({bar: 'bar', id: 3, foobar: 'foobar'})
expect(clone.$fromServer).to.equal(true)
expect(clone.URL).to.equal('http://localhost:3333/test/3')
})
})
it('should get one with global parameters', function () {
config.queryParams.set('foo', 'bar')
config.headers.set('Accept', 'application/json')
return rxrest.one('test', 3)
.get()
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.plain()).to.deep.equal({foo: 'bar', id: 3})
expect(item).to.have.ownProperty('foo', 'bar')
})
})
it('should get one with global parameters (from object)', function () {
config.queryParams = {foo: 'bar'}
config.headers = {'Accept': 'application/json'}
return rxrest.one('test', 3)
.get()
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.plain()).to.deep.equal({foo: 'bar', id: 3})
expect(item).to.have.ownProperty('foo', 'bar')
})
})
it('should get all', function () {
let params = new URLSearchParams()
params.set('foo', 'bar')
let headers = new Headers()
headers.set('Accept', 'application/json')
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.all('test')
.getList(params, headers)
.toPromise()
.then(function (values) {
expect(values).to.be.an.instanceof(RxRestCollection)
for (let item of values) {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.$fromServer).to.be.true
}
expect(values.URL).to.equal('http://localhost:3333/test')
expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}])
expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}]))
let clone = values.clone()
expect(clone[0].$fromServer).to.be.true
expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}])
})
})
it('should add request interceptor', function () {
let spy = chai.spy(function () {})
config.requestInterceptors = [
function (req) {
spy()
req.foo = 'FOO'
return of(req)
},
function (req) {
return new Promise((resolve, reject) => {
spy()
expect(req.foo).to.equal('FOO')
req.foo = 'GET'
resolve(req)
})
},
function (req) {
spy()
expect(req.method).to.equal('GET')
return new Request(req.url + '?foo=bar')
}
]
config.responseInterceptors.push(function (response) {
return response.text()
.then(e => {
let body = JSON.parse(e)
body.bar = 'foo'
spy()
return new Response(JSON.stringify(body), response)
})
})
return rxrest.one('test', 3)
.get()
.subscribe((value) => {
expect(spy).to.have.been.called.exactly(4)
expect(value.plain()).to.deep.equal({foo: 'bar', id: 3, bar: 'foo'})
})
})
it('should save a resource', function () {
config.headers.set('Content-Type', 'application/json')
return rxrest.one('test', 3)
.get()
.pipe(
mergeMap(e => {
e.bar = 'foo'
return e.save()
})
)
.subscribe(e => {
expect(e).to.deep.equal({bar: 'foo', id: 3, method: 'put'})
})
})
it('should save a resource from object', function () {
config.headers.set('Content-Type', 'application/json')
return rxrest.fromObject('test', {foo: 'bar'})
.save()
.subscribe(e => {
expect(e).to.deep.equal({foo: 'bar', id: 4, method: 'post'})
})
})
it('should save a resource by using post', function () {
config.headers.set('Content-Type', 'application/json')
return rxrest.one('test')
.post({bar: 'foo'})
.subscribe(e => {
expect(e).to.deep.equal({bar: 'foo', id: 4, method: 'post'})
})
})
it('should handle error', function () {
let spy = chai.spy(function () {})
config.errorInterceptors.push(function (response) {
expect(response.status).to.equal(404)
spy()
})
return rxrest.one('404')
.head()
.toPromise()
.catch((e) => {
expect(spy).to.have.been.called
expect(e.status).to.equal(404)
})
})
it('should handle error with promise', function () {
return rxrest.one('404')
.head()
.toPromise()
.then(() => {})
.catch((e) => {
expect(e.status).to.equal(404)
})
})
it('should create a collection from an array', function () {
config.headers.set('Content-Type', 'application/json')
rxrest.fromObject('test', [{foo: 'bar', id: 3}, {foo: 'foo', id: 4}])
.map(e => {
expect(e).to.be.an.instanceof(RxRestItem)
})
})
it('should create a custom request', function () {
rxrest = rxrest.one('test')
rxrest.$route = ['test/3']
return rxrest.request('GET')
.subscribe(e => {
expect(e).to.be.an.instanceof(RxRestItem)
})
})
it('should get one and put', function () {
return rxrest.one('test', 3)
.setHeaders({'Content-Type': 'application/json'})
.get()
.toPromise()
.then(e => {
e.foo = 'bar'
return e.put()
.subscribe(function (e) {
expect(e).to.be.an.instanceof(RxRestItem)
expect(e.method).to.equal('put')
expect(e.foo).to.equal('bar')
})
})
})
it('should change request/response body handlers', function () {
let spy = chai.spy(function () {})
config.requestBodyHandler = function (body) {
spy()
return undefined
}
config.responseBodyHandler = function (body) {
spy()
return body.text()
.then((t) => {
return {body: JSON.parse(t), metadata: 'foo'}
})
}
return rxrest.one('test', 3)
.get()
.subscribe(e => {
expect(e).to.be.an.instanceof(RxRestItem)
expect(e.$metadata).to.equal('foo')
expect(spy).to.have.been.called.exactly(2)
})
})
it('should delete and patch/trace one', function (cb) {
return rxrest
.one('test', 3)
.remove()
.subscribe(function (e) {
expect(e).to.be.an.instanceof(RxRestItem)
expect(e.method).to.equal('delete')
merge(e.patch(), e.trace())
.subscribe(() => {}, () => {}, () => {
cb()
})
})
})
it('should throw non-request errors', function (cb) {
config.requestInterceptors.push(function (body) {
throw TypeError('fail')
})
rxrest
.one('test', 3)
.get()
.toPromise()
.catch(e => {
expect(e).to.be.an.instanceof(TypeError)
cb()
})
})
it('should abort a request', function (cb) {
config.abortCallback = chai.spy()
let t = rxrest.all('timeout')
from([0, 1])
.pipe(delay(10), mergeMap(() => t.get()))
.subscribe(() => cb(new Error('Next called')),
(err) => {
expect(err.status).to.equal(504)
expect(config.abortCallback).to.have.been.called
cb()
}
)
})
it('should chain query params', function (cb) {
let spy = chai.spy(function () {})
config.requestInterceptors = [
function (request) {
spy()
expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded')
expect(request.method).to.equal('GET')
}
]
rxrest.all('test')
.setQueryParams({foo: 'bar'})
.setHeaders({'Content-Type': 'application/x-www-form-urlencoded'})
.request('GET')
.subscribe(item => {
expect(item[0].foo).to.equal('bar')
expect(spy).to.have.been.called.exactly(1)
cb()
})
})
it('should use fetch with a string', function (cb) {
fetch('http://localhost:3333/test')
.subscribe(e => {
e.json()
.then(f => {
expect(f).to.deep.equal([{id: 3}])
cb()
})
})
})
it('should be promise compatible', function () {
return rxrest.one('test', 3).get()
.toPromise()
})
it('should error properly', function (cb) {
fetch('http://localhost:3333/error')
.toPromise()
.catch(e => {
cb()
})
})
it('should create a resource without id', function () {
let item = rxrest.one('test')
item.foo = 'bar'
return item.save()
.toPromise()
.then(e => {
expect(e).to.have.property('foo', 'bar')
expect(e.$route).to.deep.equal(['test', '4'])
return e
})
})
it('should work when no response (content-length = 0)', function () {
let item = rxrest.one('foobar')
return item.remove()
.toPromise()
.then((e) => {
return 'ok'
})
})
it('should handle array query parameters', function () {
let item = rxrest.one('foobar')
item.queryParams = {
'foo[]': [0, 1]
}
expect(item.queryParams.toString()).to.equal('foo%5B%5D=0&foo%5B%5D=1')
expect(item.requestQueryParams.toString()).to.equal('?foo%5B%5D=0&foo%5B%5D=1')
})
it('should get end when no results', function (cb) {
rxrest.all('empty')
.get()
.subscribe({
next: () => {},
error: () => {},
complete: () => {
cb()
}
})
})
it.skip('should work with rxjs switch map and get end event on empty', function (cb) {
var source = of('v')
.pipe(switchMap(function (x) {
return rxrest.all('empty').get()
}))
source.subscribe((v) => {}, () => {}, () => cb())
})
it('should get options', function () {
let spy = chai.spy(function () {})
config.requestBodyHandler = function (body) {
spy()
return undefined
}
config.responseBodyHandler = function (body) {
spy()
return body.text()
.then((t) => {
return {body: t}
})
}
return rxrest.one('test', 3)
.options()
.subscribe(e => {
expect(e).to.be.an.instanceof(RxRestItem)
expect(spy).to.have.been.called.exactly(2)
})
})
// since v5 it's the default behaviour, next test tests old behaviour
it.skip('should get all as iterable', function () {
let params = new URLSearchParams()
params.set('foo', 'bar')
let headers = new Headers()
headers.set('Accept', 'application/json')
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.all('test')
.asIterable()
.get(params, headers)
.subscribe((values) => {
expect(values).to.be.an.instanceof(RxRestCollection)
for (let item of values) {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.$fromServer).to.be.true
}
expect(values.URL).to.equal('http://localhost:3333/test')
expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}])
expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}]))
let clone = values.clone()
expect(clone[0].$fromServer).to.be.true
expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}])
})
})
it('should not get all as iterable', function () {
let params = new URLSearchParams()
params.set('foo', 'bar')
let headers = new Headers()
headers.set('Accept', 'application/json')
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.all('test')
.asIterable(false)
.get(params, headers)
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.$fromServer).to.be.true
})
})
// This is default behavior since v5, next test for opposite
it.skip('should get all as iterable (2nd way of doing it)', function () {
let params = new URLSearchParams()
params.set('foo', 'bar')
let headers = new Headers()
headers.set('Accept', 'application/json')
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.all('test', true)
.get(params, headers)
.subscribe((values) => {
expect(values).to.be.an.instanceof(RxRestCollection)
for (let item of values) {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.$fromServer).to.be.true
}
expect(values.URL).to.equal('http://localhost:3333/test')
expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}])
expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}]))
let clone = values.clone()
expect(clone[0].$fromServer).to.be.true
expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}])
})
})
it('should not get all as iterable (2nd way of doing it)', function () {
let params = new URLSearchParams()
params.set('foo', 'bar')
let headers = new Headers()
headers.set('Accept', 'application/json')
config.requestInterceptors.push(function (request) {
expect(request.headers.has('Accept')).to.be.true
})
return rxrest.all('test', false)
.get(params, headers)
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.URL).to.equal('http://localhost:3333/test/3')
expect(item.$fromServer).to.be.true
})
})
// replicates a bug where second put was /resource/id/id
it('should update', function (done) {
const resource = rxrest.one('resource')
// POST
resource.save()
.subscribe(() => {
expect(resource.$route).to.deep.equal(['resource', '1'])
// PUT
resource.save()
.subscribe(() => {
expect(resource.$route).to.deep.equal(['resource', '1'])
// PUT
resource.save()
.subscribe(() => {
expect(resource.$route).to.deep.equal(['resource', '1'])
done()
})
})
})
})
it('should check $pristine', function () {
return rxrest.one('test', 3)
.get()
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.$pristine).to.be.true
item.foo = 'bar'
expect(item.$pristine).to.be.false
expect(item.$fromServer).to.be.true
})
})
it('should not change $pristine if value is the same', function () {
return rxrest.one('test', 3)
.get({foo: 'bar'})
.subscribe((item) => {
expect(item).to.be.an.instanceof(RxRestItem)
expect(item.$pristine).to.be.true
item.foo = 'bar'
expect(item.$pristine).to.be.true
expect(item.$fromServer).to.be.true
})
})
it('should add uuids', function (done) {
const config = new RxRestConfiguration()
config.baseURL = 'http://localhost:3333'
config.identifier = 'id'
config.uuid = true
rxrest = new RxRest(config)
rxrest.all('test')
.get()
.toPromise()
.then((data) => {
expect(data[0].$uuid).to.be.a.string
expect(data[0].$uuid).not.to.be.undefined
data.unshift(rxrest.fromObject('test', {}))
expect(data[0].$uuid).to.be.a.string
expect(data[0].$uuid).not.to.be.undefined
done()
})
})
it('should have a suffix', function () {
expect(rxrest.one('test', 1, 'suffix').$route).to.deep.equal(['test', '1', 'suffix'])
expect(rxrest.fromObject('test', {id: '1'}, 'suffix', 'state').$route).to.deep.equal(['test', '1', 'suffix', 'state'])
expect(rxrest.one('test', 1, 'suffix', 'state').$route).to.deep.equal(['test', '1', 'suffix', 'state'])
})
after(() => {
server.close()
})
})
process.on('unhandledRejection', function(err) {
console.error(err.stack)
process.exit(1)
})