diff --git a/test/bookshop.test.js b/test/bookshop.test.js deleted file mode 100644 index ca5b71e1..00000000 --- a/test/bookshop.test.js +++ /dev/null @@ -1,167 +0,0 @@ -describe('@capire/bookshop', () => { - const { GET, POST, expect } = require('@capire/tests').launch('@capire/bookshop') - - it('should serve $metadata documents in v4', async () => { - const { headers, status, data } = await GET`/browse/$metadata` - expect(headers).to.contain({ - 'content-type': 'application/xml', - 'odata-version': '4.0', - }) - expect(data).to.contain( - '' - ) - expect(data).to.contain( - '' - ) - expect(status).to.equal(200) - }) - - it('should serve localized $metadata documents', async () => { - const { data } = await GET`/browse/$metadata?sap-language=de` - expect(data).to.contain( - '' - ) - }) - - it('should serve localized Books with $expanded currency', async () => { - const { - data, - } = await GET`/browse/Books?&$select=title,author&$expand=currency&sap-language=de` - expect(data.value).to.containSubset([ - { - ID: 201, - title: 'Sturmhöhe', - author: 'Emily Brontë', - currency: { - code: 'GBP', - descr: 'Britische Pfund', - name: 'Pfund', - symbol: '£', - }, - }, - { - ID: 207, - title: 'Jane Eyre', - author: 'Charlotte Brontë', - currency: { - descr: 'Britische Pfund', - }, - }, - { - ID: 251, - title: 'The Raven', - author: 'Edgar Allen Poe', - currency: { - code: 'USD', - name: 'US-Dollar', - symbol: '$', - }, - }, - { - ID: 252, - title: 'Eleonora', - author: 'Edgar Allen Poe', - }, - { - ID: 271, - title: 'Catweazle', - author: 'Richard Carpenter', - currency: { - code: 'EUR', - name: 'Euro', - symbol: '€', - }, - }, - ]) - }) - - it('should serve localized Authors w/ $expanded book and currency', async () => { - const { data } = await GET( - `/admin/Authors/101?sap-language=de` + - `&$expand=books($select=title;$expand=currency($select=code,name,symbol))` + - `&$select=name` - ) - expect(data).to.eql({ - '@odata.context': - '$metadata#Authors(name,ID,books(title,ID,currency(code,name,symbol)))/$entity', - ID: 101, - name: 'Emily Brontë', - books: [ - { - ID: 201, - title: 'Sturmhöhe', - currency: { - code: 'GBP', - name: 'Pfund', - symbol: '£', - }, - }, - ], - }) - }) - - it('should check on current stocks with $value', async () => { - const { data } = await GET`/admin/Books/201/stock/$value` - expect(data).to.equal(12) - }) - - it('should reject out-of-stock orders', () => { - return expect( - Promise.all([ - POST('/browse/submitOrder', { book: 201, amount: 5 }), - POST('/browse/submitOrder', { book: 201, amount: 5 }), - POST('/browse/submitOrder', { book: 201, amount: 5 }), - ]) - ).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/) - }) - - it('should serve $select requests', async () => { - const { data } = await GET`/browse/Books?$select=ID,title` - expect(data.value).to.eql([ - { ID: 201, title: 'Wuthering Heights' }, - { ID: 207, title: 'Jane Eyre' }, - { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' }, - { ID: 271, title: 'Catweazle' }, - ]) - }) - - it('should serve $search requests', async () => { - const { data } = await GET`/admin/Authors?$search=Bro&$select=name` - expect(data.value).to.eql([ - { ID: 101, name: 'Emily Brontë' }, - { ID: 107, name: 'Charlotte Brontë' }, - ]) - }) - - it('should serve $expand requests', async () => { - const { - data, - } = await GET`/admin/Authors?$select=name&$expand=books($select=ID,title)` - expect(data.value).to.eql([ - { - ID: 101, - name: 'Emily Brontë', - books: [{ ID: 201, title: 'Wuthering Heights' }], - }, - { - ID: 107, - name: 'Charlotte Brontë', - books: [{ ID: 207, title: 'Jane Eyre' }], - }, - { - ID: 150, - name: 'Edgar Allen Poe', - books: [ - { ID: 251, title: 'The Raven' }, - { ID: 252, title: 'Eleonora' }, - ], - }, - { - ID: 170, - name: 'Richard Carpenter', - books: [{ ID: 271, title: 'Catweazle' }], - }, - ]) - }) -}) diff --git a/test/lib/helpers.js b/test/capire.js similarity index 51% rename from test/lib/helpers.js rename to test/capire.js index 820fa64d..6d1d1548 100644 --- a/test/lib/helpers.js +++ b/test/capire.js @@ -1,31 +1,39 @@ /** For static usage w/o launching a server */ -module.exports = exports = { - get chai() { return _chai() }, - get expect(){ return this.chai.expect }, - get assert(){ return this.chai.assert }, +const { resolve, dirname } = require('path') + +class CDSTestKit { + for (...paths) { + const tk = new CDSTestKit + tk.root = resolve (...paths) + return tk + } + get chai() { + const chai = require('chai') + chai.use (require('chai-as-promised')) + chai.use (require('chai-subset')) + chai.should() + Object.defineProperty (this, 'chai', {value:chai}) + return chai + } + get expect(){ return this.chai.expect } + get assert(){ return this.chai.assert } } +const lazy = (new CDSTestKit) .for (__dirname,'../..') +module.exports = exports = lazy + // harmonizing jest and mocha const is_mocha = !global.test const is_jest = !!global.test if (is_jest) { // it's jest - global.before = global.beforeAll - global.after = global.afterAll + global.before = (msg,fn) => global.beforeAll(fn||msg) + global.after = (msg,fn) => global.afterAll(fn||msg) } else { // it's mocha global.beforeAll = global.before global.afterAll = global.after global.test = global.it } -// lazy-loading chai -function _chai(){ - const chai = exports.chai = require('chai') - .use (require('chai-as-promised')) - .use (require('chai-subset')) - chai.should() - return chai -} - /** Launching and testing a cds server */ exports.launch = (project, ...args) => { @@ -35,37 +43,38 @@ exports.launch = (project, ...args) => { const axios = require('axios').default const test = { - GET: (path) => axios.get (test.url+path) .catch(_error), - PUT: (path,data) => axios.put (test.url+path,data) .catch(_error), - POST: (path,data) => axios.post (test.url+path,data) .catch(_error), - PATCH: (path,data) => axios.patch (test.url+path,data) .catch(_error), - DELETE: (path) => axios.delete (test.url+path) .catch(_error), + GET: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error), + PUT: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error), + POST: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error), + PATCH: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error), + DELETE: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error), - get: (path) => axios.get (test.url+path) .catch(_error), - put: (path,data) => axios.put (test.url+path,data) .catch(_error), - post: (path,data) => axios.post (test.url+path,data) .catch(_error), - patch: (path,data) => axios.patch (test.url+path,data) .catch(_error), - delete: (path) => axios.delete (test.url+path) .catch(_error), + get: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error), + put: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error), + post: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error), + patch: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error), + delete: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error), - get chai(){ return _chai() }, - get expect(){ return this.chai.expect }, - get assert(){ return this.chai.assert }, + get chai(){ return lazy.chai }, + get expect(){ return lazy.expect }, + get assert(){ return lazy.assert }, } // launch cds server... before (done => { - const cds = require('@sap/cds') + const cds = require('@sap/cds'), { isdir } = cds.utils let cmd = 'run' if (project.startsWith('cds ')) [ cmd, project ] = [ project.slice(4), args.shift() ] if (!args.length) args = ['--in-memory?'] // Supporting .launch () - if (cmd === 'run' && !cds.utils.existsSync(project)) try { - project = require('path').dirname (require.resolve(project+'/package.json')) - } catch(e) { - throw cds.error (`Cannot resolve project folder for '${project}'`) + if (cmd === 'run') { + if (isdir(project)) ; //> all fine + else if (isdir(resolve(this.root,project))) project = resolve(this.root,project) + else try { project = dirname (require.resolve(project+'/package.json')) } + catch(e) { throw cds.error (`Cannot resolve project folder for '${project}'`) } } if (!process.env.CDS_TEST_VERBOSE) global.console = { __proto__: global.console, logs, diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js index d9ed5417..ed2d362d 100644 --- a/test/cds.ql.test.js +++ b/test/cds.ql.test.js @@ -1,4 +1,4 @@ -const { expect } = require('@capire/tests') +const { expect } = require('./capire') const cds = require('@sap/cds') const CQL = ([cql]) => cds.parse.cql(cql) const Foo = { name: 'Foo' } diff --git a/test/custom-handlers.test.js b/test/custom-handlers.test.js new file mode 100644 index 00000000..5efec938 --- /dev/null +++ b/test/custom-handlers.test.js @@ -0,0 +1,15 @@ +describe('Custom Handlers', () => { + const { GET, POST, expect } = require('./capire').launch('bookshop') + + it('should reject out-of-stock orders', async () => { + await expect( + Promise.all([ + POST('/browse/submitOrder', { book: 201, amount: 5 }), + POST('/browse/submitOrder', { book: 201, amount: 5 }), + POST('/browse/submitOrder', { book: 201, amount: 5 }), + ]) + ).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/) + const { data } = await GET`/admin/Books/201/stock/$value` + expect(data).to.equal(2) + }) +}) diff --git a/test/genres.test.js b/test/genres.test.js deleted file mode 100644 index e46b6a84..00000000 --- a/test/genres.test.js +++ /dev/null @@ -1,70 +0,0 @@ -const {expect} = require('@capire/tests') -const cds = require ('@sap/cds') - -describe('Hierarchical CodeLists', ()=>{ - - it ('should bootstrap sqlite in-memory db...', async()=>{ - await cds.deploy ('@capire/bookshop') .to ('sqlite::memory:') - expect (cds.db) .to.exist - expect (cds.db.model) .to.exist - }) - - it ('should insert hierarchy of genres', ()=>{ - const { Genres } = cds.entities - return INSERT.into (Genres) .entries ( - { ID:100, name:'Some Sample Genres...', children:[ - { ID:101, name:'Cat', children:[ - { ID:102, name:'Kitty', children:[ - { ID:103, name:'Kitty Cat', children:[ - { ID:104, name:'Aristocat' } ]}, - { ID:105, name:'Kitty Bat' } ]}, - { ID:106, name:'Catwoman', children:[ - { ID:107, name:'Catalina' } ]} ]}, - { ID:108, name:'Ca{ - const { Genres } = cds.entities - expect (await - - SELECT.one.from (Genres, c=>{ - c.ID, c.name.as('parent'), c.children (c=>{ - c.name.as('child') - }) - }) .where ({name:'Cat'}) - - ) .to.containSubset ( - - { ID:101, parent:'Cat', children:[ - { child:'Kitty' }, - { child:'Catwoman' }, - ]} - - ) - }) - - it ('should read deep hierarchy of genres', async()=>{ - const { Genres } = cds.entities - expect (await - - SELECT.one.from (Genres, c=>{ - c.ID, c.name, c.children (c=>{ c.name },{levels:3}) - }) .where ({name:'Cat'}) - - ) .to.containSubset ( - - { ID:101, name:'Cat', children:[ - { name:'Kitty', children:[ - { name:'Kitty Cat', children:[ - { name:'Aristocat' }, ]}, - { name:'Kitty Bat' }, ]}, - { name:'Catwoman', children:[ - { name:'Catalina' } ]}, - ]} - - ) - }) - -}) diff --git a/test/hello-world.test.js b/test/hello-world.test.js index 790c03bf..34ea78bb 100644 --- a/test/hello-world.test.js +++ b/test/hello-world.test.js @@ -1,6 +1,6 @@ describe('Hello world!', () => { - const { GET, expect } = require('@capire/tests').launch('cds serve', __dirname+'/../hello/world.cds', '') + const { GET, expect } = require('./capire').launch('cds serve', __dirname+'/../hello/world.cds', '') it('should say hello with class impl', async () => { const {data} = await GET `/say/hello(to='world')` diff --git a/test/hierarchical-data.test.js b/test/hierarchical-data.test.js new file mode 100644 index 00000000..7e0068dd --- /dev/null +++ b/test/hierarchical-data.test.js @@ -0,0 +1,81 @@ +const {expect} = require('./capire') +const cds = require ('@sap/cds') + +const model = cds.parse(` + entity Categories { + key ID : Integer; + name : String; + children : Composition of many Categories on children.parent = $self; + parent : Association to Categories; + } +`) +const {Categories:Cats} = model.definitions + + +describe('Hierarchical Data', ()=>{ + + before ('bootstrap sqlite in-memory db...', async()=>{ + await cds.deploy (model) .to ('sqlite::memory:') + expect (cds.db) .to.exist + expect (cds.db.model) .to.exist + }) + + it ('supports deeply nested inserts', ()=> INSERT.into (Cats, + { ID:100, name:'Some Cats...', children:[ + { ID:101, name:'Cat', children:[ + { ID:102, name:'Kitty', children:[ + { ID:103, name:'Kitty Cat', children:[ + { ID:104, name:'Aristocat' } ]}, + { ID:105, name:'Kitty Bat' } ]}, + { ID:106, name:'Catwoman', children:[ + { ID:107, name:'Catalina' } ]} ]}, + { ID:108, name:'Catweazle' } + ]} + )) + + it ('supports nested reads', async()=>{ + expect (await + SELECT.one.from (Cats, c=>{ + c.ID, c.name.as('parent'), c.children (c=>{ + c.name.as('child') + }) + }) .where ({name:'Cat'}) + ) .to.eql ( + { ID:101, parent:'Cat', children:[ + { ID:102, child:'Kitty' }, + { ID:106, child:'Catwoman' }, + ]} + ) + }) + + it ('supports deeply nested reads', async()=>{ + expect (await SELECT.one.from (Cats, c=>{ + c.ID, c.name, c.children ( + c => { c.name }, + {levels:3} + ) + }) .where ({name:'Cat'}) + ) .to.eql ( + { ID:101, name:'Cat', children:[ + { ID:102, name:'Kitty', children:[ + { ID:103, name:'Kitty Cat', children:[ + { ID:104, name:'Aristocat' }, ]}, // level 3 + { ID:105, name:'Kitty Bat', children:[] }, ]}, + { ID:106, name:'Catwoman', children:[ + { ID:107, name:'Catalina', children:[] } ]}, + ]} + ) + }) + + it ('supports cascaded deletes', async()=>{ + const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]}) + expect (affectedRows) .to.equal (5) + expect ( await SELECT.from(Cats) ).to.eql ([ + { ID:100, name:'Some Cats...' }, + { ID:101, name:'Cat' }, + { ID:104, name:'Aristocat' }, // REVISIT: Should be deleted as well? + { ID:108, name:'Catweazle' } + ]) + }) + +}) diff --git a/test/localized-data.test.js b/test/localized-data.test.js new file mode 100644 index 00000000..882ea4e0 --- /dev/null +++ b/test/localized-data.test.js @@ -0,0 +1,74 @@ +describe('Localized Data', () => { + const { GET, expect } = require('./capire').launch('bookshop') + + it('serves localized $metadata documents', async () => { + const { data } = await GET`/browse/$metadata?sap-language=de` + expect(data).to.contain('') + }) + + it('supports sap-language param', async () => { + const { data } = await GET(`/browse/Books?$select=title,author` + '&sap-language=de') + expect(data.value).to.containSubset([ + { title: 'Sturmhöhe', author: 'Emily Brontë' }, + { title: 'Jane Eyre', author: 'Charlotte Brontë' }, + { title: 'The Raven', author: 'Edgar Allen Poe' }, + { title: 'Eleonora', author: 'Edgar Allen Poe' }, + { title: 'Catweazle', author: 'Richard Carpenter' }, + ]) + }) + + it('supports accept-language header', async () => { + const { data } = await GET(`/browse/Books?$select=title,author`, { + headers: { 'Accept-Language': 'de' }, + }) + expect(data.value).to.containSubset([ + { title: 'Sturmhöhe', author: 'Emily Brontë' }, + { title: 'Jane Eyre', author: 'Charlotte Brontë' }, + { title: 'The Raven', author: 'Edgar Allen Poe' }, + { title: 'Eleonora', author: 'Edgar Allen Poe' }, + { title: 'Catweazle', author: 'Richard Carpenter' }, + ]) + }) + + it('supports queries with $expand', async () => { + const { data } = await GET(`/browse/Books?&$select=title,author&$expand=currency`, { + headers: { 'Accept-Language': 'de' }, + }) + expect(data.value).to.containSubset([ + { title: 'Sturmhöhe', author: 'Emily Brontë', currency: { name: 'Pfund' } }, + { title: 'Jane Eyre', author: 'Charlotte Brontë', currency: { name: 'Pfund' } }, + { title: 'The Raven', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } }, + { title: 'Eleonora', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } }, + { title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Euro' } }, + ]) + }) + + it('supports queries with nested $expand', async () => { + const { data } = await GET(`/admin/Authors`, { + params: { + $filter: `startswith(name,'E')`, + $expand: `books( + $select=title; + $expand=currency( + $select=name,symbol + ) + )`.replace(/\s/g, ''), + $select: `name`, + }, + headers: { 'Accept-Language': 'de' }, + }) + expect(data.value).to.containSubset([ + { + name: 'Emily Brontë', + books: [{ title: 'Sturmhöhe', currency: { name: 'Pfund', symbol: '£' } }], + }, + { + name: 'Edgar Allen Poe', + books: [ + { title: 'The Raven', currency: { name: 'US-Dollar', symbol: '$' } }, + { title: 'Eleonora', currency: { name: 'US-Dollar', symbol: '$' } }, + ], + }, + ]) + }) +}) diff --git a/test/messaging.test.js b/test/messaging.test.js index bd6545e6..d2b939fe 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -1,4 +1,4 @@ -const {expect} = require('@capire/tests') +const {expect} = require('./capire') const cds = require ('@sap/cds') const _model = '@capire/reviews' diff --git a/test/odata.test.js b/test/odata.test.js new file mode 100644 index 00000000..4fb532a0 --- /dev/null +++ b/test/odata.test.js @@ -0,0 +1,73 @@ +describe('OData Protocol', () => { + const { GET, expect } = require('./capire').launch('bookshop') + + it('serves $metadata documents in v4', async () => { + const { headers, status, data } = await GET`/browse/$metadata` + expect(status).to.equal(200) + expect(headers).to.contain({ + 'content-type': 'application/xml', + 'odata-version': '4.0', + }) + expect(data).to.contain('') + expect(data).to.contain('') + }) + + it('supports $search in multiple fields', async () => { + const { data } = await GET(`/browse/Books`, { + params: { $search: 'Po', $select: `title,author` }, + }) + expect(data.value).to.eql([ + { ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' }, + { ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' }, + { ID: 251, title: 'The Raven', author: 'Edgar Allen Poe' }, + { ID: 252, title: 'Eleonora', author: 'Edgar Allen Poe' }, + ]) + }) + + it('supports $select', async () => { + const { data } = await GET(`/browse/Books`, { + params: { $select: `ID,title` }, + }) + expect(data.value).to.eql([ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' }, + ]) + }) + + it('supports $expand', async () => { + const { data } = await GET(`/admin/Authors`, { + params: { + $select: `name`, + $expand: `books($select=title)`, + }, + }) + expect(data.value).to.containSubset([ + { name: 'Emily Brontë', books: [{ title: 'Wuthering Heights' }] }, + { name: 'Charlotte Brontë', books: [{ title: 'Jane Eyre' }] }, + { name: 'Edgar Allen Poe', books: [{ title: 'The Raven' }, { title: 'Eleonora' }] }, + { name: 'Richard Carpenter', books: [{ title: 'Catweazle' }] }, + ]) + }) + + it('supports $value requests', async () => { + const { data } = await GET`/admin/Books/201/stock/$value` + expect(data).to.equal(12) + }) + + it('supports $top/$skip paging', async () => { + const { data: p1 } = await GET`/browse/Books?$select=title&$top=3` + expect(p1.value).to.eql([ + { ID: 201, title: 'Wuthering Heights' }, + { ID: 207, title: 'Jane Eyre' }, + { ID: 251, title: 'The Raven' }, + ]) + const { data: p2 } = await GET`/browse/Books?$select=title&$skip=3` + expect(p2.value).to.eql([ + { ID: 252, title: 'Eleonora' }, + { ID: 271, title: 'Catweazle' }, + ]) + }) +}) diff --git a/test/package.json b/test/package.json index 0e31900d..b6fe7d28 100644 --- a/test/package.json +++ b/test/package.json @@ -1,5 +1,5 @@ { "name": "@capire/tests", "version": "1.0.0", - "main": "lib/helpers.js" + "main": "capire.js" } \ No newline at end of file