diff --git a/.eslintrc b/.eslintrc index 343e8260..da867678 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,8 @@ "browser": true, "node": true, "es6": true, - "jest": true + "jest": true, + "mocha": true }, "parserOptions": { "ecmaVersion": 2018 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 438b613e..dcc16d69 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -9,7 +9,9 @@ "esbenp.prettier-vscode", "mechatroner.rainbow-csv", "humao.rest-client", - "alexcvzz.vscode-sqlite" + "alexcvzz.vscode-sqlite", + "hbenl.vscode-mocha-test-adapter", + "sdras.night-owl" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 72bc8cb5..f83fcc66 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "files.exclude": { "**/.gitignore": true, "**/.vscode": true - } + }, + "cds.checkDevKitInstalled": false } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..62a4304f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "jest", + "group": { + "kind": "test", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 5287921d..00df5953 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,14 @@ cds watch bookshop After that open this link in your browser: [http://localhost:4004](http://localhost:4004) +### Testing + +Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), e.g.: +```sh +npx jest +``` +> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation which allows to run all tests. + ## Get Support diff --git a/bookshop/index.cds b/bookshop/index.cds index 98605e0b..161677cd 100644 --- a/bookshop/index.cds +++ b/bookshop/index.cds @@ -1,3 +1,4 @@ +namespace sap.capire.bookshop; //> important for reflection using from './db/schema'; using from './srv/cat-service'; using from './srv/admin-service'; diff --git a/package.json b/package.json index 8afe517b..208ca3e7 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,20 @@ "@capire/bookshop": "file:bookshop", "@capire/common": "file:common", "@capire/fiori": "file:fiori", - "@capire/media": "file:media", "@capire/orders": "file:orders", - "@capire/reviews": "file:reviews" + "@capire/reviews": "file:reviews", + "@capire/tests": "file:test" }, "devDependencies": { "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", "chai-subset": "^1.6.0", "sqlite3": "^4" }, + "scripts": { + "mocha": "npx mocha || echo", + "jest": "npx jest" + }, "license": "SAP SAMPLE CODE LICENSE", "private": true } diff --git a/test/bookshop.test.js b/test/bookshop.test.js new file mode 100644 index 00000000..1075e47f --- /dev/null +++ b/test/bookshop.test.js @@ -0,0 +1,95 @@ +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/) + }) + +}) diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js new file mode 100644 index 00000000..d9ed5417 --- /dev/null +++ b/test/cds.ql.test.js @@ -0,0 +1,531 @@ +const { expect } = require('@capire/tests') +const cds = require('@sap/cds') +const CQL = ([cql]) => cds.parse.cql(cql) +const Foo = { name: 'Foo' } +const Books = { name: 'capire.bookshop.Books' } + +const is_cds_333 = cds.version >= '3.33.3' +if (!is_cds_333) { + // Monky-patching v3.33.3 features in older releases + const up = UPDATE('x').constructor.prototype + up.with = up.set +} + +// while jest has 'test' as alias to 'it', mocha doesn't +if (!global.test) global.test = it + +describe('cds.ql', () => { + // + + describe(`BUGS + GAPS...`, () => { + it.skip('should consistently handle *', () => { + expect({ + SELECT: { from: { ref: ['Foo'] }, columns: ['*'] }, + }) + .to.eql(CQL`SELECT * from Foo`) + .to.eql(CQL`SELECT from Foo{*}`) + .to.eql(SELECT('*').from(Foo)) + .to.eql(SELECT.from(Foo, ['*'])) + }) + + it.skip('should correctly handle { ... and:{...} }', () => { + expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 2 } } })).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [ + { ref: ['x'] }, + '=', + { val: 1 }, + 'and', + '(', + { ref: ['y'] }, + '=', + { val: 2 }, + 'or', + { ref: ['z'] }, + '=', + { val: 3 }, + ')', + ], + }, + }) + }) + }) + + describe(`SELECT...`, () => { + test('from ( Foo )', () => { + expect({ + SELECT: { from: { ref: ['Foo'] } }, + }) + .to.eql(CQL`SELECT from Foo`) + .to.eql(SELECT.from(Foo)) + }) + + test('from ( ..., )', () => { + // Compiler + expect(CQL`SELECT from Foo[11]`).to.eql({ + SELECT: { + // REVISIT: add one:true? + from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] }, + }, + }) + + expect(CQL`SELECT from Foo[ID=11]`).to.eql({ + SELECT: { + // REVISIT: add one:true + from: { + ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }], + }, + }, + }) + + // Runtime ds.ql + expect(SELECT.from(Foo, 11)) + .to.eql(SELECT.from(Foo, { ID: 11 })) + .to.eql(SELECT.from(Foo).byKey(11)) + .to.eql(SELECT.from(Foo).byKey({ ID: 11 })) + .to.eql(SELECT.one.from(Foo).where({ ID: 11 })) + .to.eql({ + // REVISIT: should produce CQN as the ones above? + SELECT: { + one: true, + from: { ref: ['Foo'] }, + where: [{ ref: ['ID'] }, '=', { val: 11 }], + }, + }) + + expect(CQL`SELECT from Foo[11]{a}`).to.eql({ + SELECT: { + // REVISIT: add one:true? + from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] }, + columns: [{ ref: ['a'] }], + }, + }) + + expect(SELECT.from(Foo, 11, ['a'])) + .to.eql(SELECT.from(Foo, 11, (foo) => foo.a)) + .to.eql({ + // REVISIT: should produce CQN as the ones above? + SELECT: { + one: true, + from: { ref: ['Foo'] }, + columns: [{ ref: ['a'] }], + where: [{ ref: ['ID'] }, '=', { val: 11 }], + }, + }) + }) + + test('from ( ..., => {...})', () => { + // single *, prefix and postfix, as array and function + expect(CQL`SELECT * from Foo`).to.eql(CQL`SELECT from Foo{*}`) + //> .to.eql... FIXME: see skipped 'should handle * correctly' below + expect(SELECT('*').from(Foo)) + .to.eql(SELECT.from(Foo, ['*'])) + .to.eql(SELECT.from(Foo, (foo) => foo('*'))) + .to.eql(SELECT.from(Foo).columns('*')) + .to.eql(SELECT.from(Foo).columns((foo) => foo('*'))) + .to.eql({ + SELECT: { from: { ref: ['Foo'] }, columns: [{ ref: ['*'] }] }, + }) + + // single column, prefix and postfix, as array and function + expect(CQL`SELECT a from Foo`) + expect(CQL`SELECT from Foo {a}`) + .to.eql(SELECT.from(Foo, ['a'])) + .to.eql(SELECT.from(Foo, (foo) => foo.a)) + .to.eql({ + SELECT: { from: { ref: ['Foo'] }, columns: [{ ref: ['a'] }] }, + }) + + // multiple columns, prefix and postfix, as array and function + expect(CQL`SELECT a,b as c from Foo`) + expect(CQL`SELECT from Foo {a,b as c}`) + .to.eql(SELECT.from(Foo, ['a', { b: 'c' }])) + .to.eql( + SELECT.from(Foo, (foo) => { + foo.a, foo.b.as('c') + }) + ) + .to.eql(SELECT.from(Foo).columns('a', { b: 'c' })) + .to.eql(SELECT.from(Foo).columns(['a', { b: 'c' }])) + .to.eql( + SELECT.from(Foo).columns((foo) => { + foo.a, foo.b.as('c') + }) + ) + .to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + columns: [{ ref: ['a'] }, { ref: ['b'], as: 'c' }], + }, + }) + + // multiple columns and *, prefix and postfix, as array and function + expect(CQL`SELECT *,a,b from Foo`).to.eql(CQL`SELECT from Foo{*,a,b}`) + //> .to.eql... FIXME: see skipped 'should handle * correctly' below + expect(SELECT.from(Foo, ['a', 'b', '*'])) + .to.eql(SELECT.from(Foo).columns('a', 'b', '*')) + .to.eql(SELECT.from(Foo).columns(['a', 'b', '*'])) + .to.eql( + SELECT.from(Foo, (foo) => { + foo.a, foo.b, foo('*') + }) + ) + .to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + columns: [{ ref: ['a'] }, { ref: ['b'] }, { ref: ['*'] }], + }, + }) + }) + + is_cds_333 && + test('from ( ..., => _.expand ( x=>{...}))', () => { + // SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } } + expect( + SELECT.from(Foo, (foo) => { + foo('*'), + foo.x, + foo.car('*'), + foo.boo((b) => { + b('*'), b.moo.zoo((x) => x.y.z) + }) + }) + ).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + columns: [ + { ref: ['*'] }, + { ref: ['x'] }, + { ref: ['car'], expand: ['*'] }, + { + ref: ['boo'], + expand: ['*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }], + }, + ], + }, + }) + }) + + is_cds_333 && + test('from ( ..., => _.inline ( _=>{...}))', () => { + // SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } } + expect( + SELECT.from(Foo, (foo) => { + foo.bar('*'), + foo.bar('.*'), //> leading dot indicates inline + foo.boo((x) => x.moo.zoo), + foo.boo((_) => _.moo.zoo) //> underscore arg name indicates inline + }) + ).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + columns: [ + { ref: ['bar'], expand: ['*'] }, + { ref: ['bar'], inline: ['*'] }, + { ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] }, + { ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] }, + ], + }, + }) + }) + + test('one / distinct ...', () => { + expect(SELECT.distinct.from(Foo).cqn) + // .to.eql(CQL(`SELECT distinct from Foo`).SELECT) + .to.eql(SELECT.distinct(Foo).cqn) + .to.eql({ distinct: true, from: { ref: ['Foo'] } }) + + expect(SELECT.one.from(Foo).cqn) + // .to.eql(CQL(`SELECT one from Foo`).SELECT) + .to.eql(SELECT.one(Foo).cqn) + .to.eql({ one: true, from: { ref: ['Foo'] } }) + + expect(SELECT.one('a').from(Foo).cqn) + // .to.eql(CQL(`SELECT distinct a from Foo`).SELECT) + .to.eql(SELECT.one(['a']).from(Foo).cqn) + .to.eql(SELECT.one(Foo, ['a']).cqn) + .to.eql(SELECT.one(Foo, (foo) => foo.a).cqn) + .to.eql(SELECT.one.from(Foo, (foo) => foo.a).cqn) + .to.eql(SELECT.one.from(Foo, ['a']).cqn) + .to.eql({ + one: true, + from: { ref: ['Foo'] }, + columns: [{ ref: ['a'] }], + }) + // same for works distinct + }) + + test('where ( ... cql | {x:y} )', () => { + const args = [`foo`, "'bar'", 3] + const ID = 11 + + // using simple predicate objects + // (Note: this doesn't support paths in left-hand-sides, nor references in arrays) + expect( + SELECT.from(Foo).where({ + ID, + args, + and: { x: { like: '%x%' }, or: { y: { '>=': 9 } } }, + }) + ).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [ + '(', //> this one is not required + { ref: ['ID'] }, + '=', + { val: ID }, + 'and', + { ref: ['args'] }, + 'in', + { val: args }, + 'and', + // '(', //> this one is missing, and that's changing the logic -> that's a BUG + { ref: ['x'] }, + 'like', + { val: '%x%' }, + 'or', + { ref: ['y'] }, + '>=', + { val: 9 }, + ')', + ], + }, + }) + + // using CQL fragments -> uses cds.parse.expr + expect(CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`) + .to.eql(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)) + .to.eql(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)) + .to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [ + { ref: ['ID'] }, + '=', + { val: ID }, + 'and', + { ref: ['x'] }, + 'in', + '(', + { ref: ['foo'] }, + ',', + { val: 'bar' }, + ',', + { val: 3 }, + ')', + ], + }, + }) + + expect(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`).to.eql( + SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`) + ) + + expect(CQL`SELECT from Foo where x between 1 and 9`).to.eql( + SELECT.from(Foo).where(`x between`, 1, `and`, 9) + ) + }) + + test('w/ sub selects', () => { + // in where causes + expect(CQL`SELECT from Foo where x in (SELECT y from Bar)`).to.eql( + SELECT.from(Foo).where({ x: SELECT('y').from('Bar') }) + ) + // in classical semi joins + expect(CQL`SELECT x from Foo where exists (SELECT 1 from Bar where y=x)`).to.eql( + SELECT('x').from(Foo) .where ( `exists`, + SELECT(1).from('Bar') .where ({ y: { ref: ['x'] } }) + ) // prettier-ignore + ) + // in select clauses + cds.version >= '3.33.3' && + expect(CQL`SELECT from Foo { x, (SELECT y from Bar) as y }`) + .to.eql( + SELECT.from(Foo, (foo) => { + foo.x, foo(SELECT.from('Bar', (b) => b.y)).as('y') + }) + ) + .to.eql( + SELECT.from(Foo, ['x', Object.assign(SELECT('y').from('Bar'), { as: 'y' })]) + ) + }) + + it('w/ plain SQL', () => { + expect(SELECT.from(Books) + 'WHERE ...').to.eql( + 'SELECT * FROM capire_bookshop_Books WHERE ...' + ) + }) + + // + }) + + describe(`INSERT...`, () => { + test('entries ({a,b}, ...)', () => { + const entries = [{ foo: 1 }, { boo: 2 }] + expect(INSERT(...entries).into(Foo)) + .to.eql(INSERT(entries).into(Foo)) + .to.eql(INSERT.into(Foo).entries(...entries)) + .to.eql(INSERT.into(Foo).entries(entries)) + .to.eql({ + INSERT: { into: 'Foo', entries }, + }) + }) + + test('rows ([1,2], ...)', () => { + expect( + INSERT.into(Foo) + .columns('a', 'b') + .rows([ + [1, 2], + [3, 4], + ]) + ) + .to.eql(INSERT.into(Foo).columns('a', 'b').rows([1, 2], [3, 4])) + .to.eql({ + INSERT: { + into: 'Foo', + columns: ['a', 'b'], + rows: [ + [1, 2], + [3, 4], + ], + }, + }) + }) + + test('values (1,2)', () => { + expect(INSERT.into(Foo).columns('a', 'b').values([1, 2])) + .to.eql(INSERT.into(Foo).columns('a', 'b').values(1, 2)) + .to.eql({ + INSERT: { into: 'Foo', columns: ['a', 'b'], values: [1, 2] }, + }) + }) + + test('w/ plain SQL', () => { + expect(INSERT.into(Books) + 'VALUES ...').to.eql( + 'INSERT INTO capire_bookshop_Books VALUES ...' + ) + }) + }) + + describe(`UPDATE...`, () => { + test('entity (..., )', () => { + expect(UPDATE(Books, 4711)) + .to.eql(UPDATE(Books, { ID: 4711 })) + .to.eql(UPDATE(Books).byKey(4711)) + .to.eql(UPDATE(Books).byKey({ ID: 4711 })) + .to.eql(UPDATE(Books).where({ ID: 4711 })) + .to.eql(UPDATE(Books).where(`ID=`, 4711)) + .to.eql(UPDATE.entity(Books, 4711)) + .to.eql(UPDATE.entity(Books, { ID: 4711 })) + // etc... + .to.eql({ + UPDATE: { + entity: 'capire.bookshop.Books', + where: [{ ref: ['ID'] }, '=', { val: 4711 }], + }, + }) + }) + + /* + UPDATE.with allows to pass in plain data payloads, e.g. as obtained from REST clients. + In addition, UPDATE.with supports specifying expressions, either in CQL fragements + notation or as simple expression objects. + */ + test('with', () => { + expect(UPDATE(Foo).with(`foo=11, bar = bar - 22`)) + .to.eql(UPDATE(Foo).with(`foo=`, 11, `bar-=`, 22)) + .to.eql(UPDATE(Foo).with({ foo: 11, bar: { '-=': 22 } })) + .to.eql({ + UPDATE: { + entity: 'Foo', + with: { + foo: { val: 11 }, + bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] }, + }, + }, + }) + + // some more + expect(UPDATE(Foo).with(`bar = coalesce(x,y), car = 'foo''s bar, car'`)).to.eql({ + UPDATE: { + entity: 'Foo', + with: { + bar: { func: 'coalesce', args: [{ ref: ['x'] }, { ref: ['y'] }] }, + car: { val: "foo's bar, car" }, + }, + }, + }) + }) + + /* + UPDATE.data allows to pass in plain data payloads, e.g. as obtained from REST clients. + The passed in object can be modified subsequently, e.g. by adding or modifying values + before the query is finally executed. + */ + test('data', () => { + const o = {} + const q = UPDATE(Foo).data(o).with(`bar-=`, 22) + o.foo = 11 + expect(q).to.eql({ + UPDATE: { + entity: 'Foo', + data: { foo: 11 }, + with: { + bar: { xpr: [{ ref: ['bar'] }, '-', { val: 22 }] }, + }, + }, + }) + }) + + test('w/ plain SQL', () => { + expect(UPDATE(Books) + 'SET ...').to.eql('UPDATE capire_bookshop_Books SET ...') + }) + }) + + describe(`DELETE...`, () => { + test('from (..., )', () => { + expect(DELETE(Books, 4711)) + .to.eql(DELETE(Books, { ID: 4711 })) + .to.eql(DELETE.from(Books, 4711)) + .to.eql(DELETE.from(Books, { ID: 4711 })) + .to.eql(DELETE.from(Books).byKey(4711)) + .to.eql(DELETE.from(Books).byKey({ ID: 4711 })) + .to.eql(DELETE.from(Books).where({ ID: 4711 })) + .to.eql(DELETE.from(Books).where(`ID=`, 4711)) + .to.eql({ + DELETE: { + from: 'capire.bookshop.Books', + where: [{ ref: ['ID'] }, '=', { val: 4711 }], + }, + }) + }) + + test('/w plain SQL', () => { + expect(DELETE.from(Books) + 'WHERE ...').to.eql( + 'DELETE FROM capire_bookshop_Books WHERE ...' + ) + }) + }) + + describe(`cds.ql etc...`, () => { + it('queries marked for cds repl', () => { + expect(UPDATE(Foo)._isQuery).to.be.true + }) + + it('should keep null and undefined', () => { + for (let each of [null, undefined]) { + expect(SELECT.from(Foo).where({ ID: each })).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [{ ref: ['ID'] }, '=', { val: each }], + }, + }) + } + }) + }) + + // +}) diff --git a/bookshop/test/genres.test.js b/test/genres.test.js similarity index 85% rename from bookshop/test/genres.test.js rename to test/genres.test.js index 42fe52af..e46b6a84 100644 --- a/bookshop/test/genres.test.js +++ b/test/genres.test.js @@ -1,10 +1,10 @@ +const {expect} = require('@capire/tests') const cds = require ('@sap/cds') -const {expect} = cds.require.chai -describe('reading/writing hierarchies', ()=>{ +describe('Hierarchical CodeLists', ()=>{ - it ('should bootstrap sqlite in-memory db', async()=>{ - await cds.deploy (__dirname+'/../db') .to ('sqlite::memory:') + 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 }) @@ -20,7 +20,7 @@ describe('reading/writing hierarchies', ()=>{ { ID:105, name:'Kitty Bat' } ]}, { ID:106, name:'Catwoman', children:[ { ID:107, name:'Catalina' } ]} ]}, - { ID:108, name:'Catweazle' } + { ID:108, name:'Ca{ ) }) - it ('should read hierarchy of genres', async()=>{ + it ('should read deep hierarchy of genres', async()=>{ const { Genres } = cds.entities expect (await diff --git a/test/lib/helpers.js b/test/lib/helpers.js new file mode 100644 index 00000000..04fbeaa2 --- /dev/null +++ b/test/lib/helpers.js @@ -0,0 +1,94 @@ +/** 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 }, +} + +// harmonizing jest and mocha +if (global.test) { // it's jest + global.before = global.beforeAll + global.after = global.afterAll +} else { // it's mocha + global.beforeAll = global.before + global.afterAll = global.after +} + +// 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=['--in-memory?']) => { + + const cds = require('@sap/cds') + + if (!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}'`) + } + + const axios = require('axios').default + let baseURL //... be filled in below + + // launch cds server... + before (done => { + + const console = global.console, logs=[] + global.console = { __proto__: global.console, logs, + time: ()=>{}, timeEnd: (...args)=> logs.push(args), + debug: (...args)=> logs.push(args), + log: (...args)=> logs.push(args), + warn: (...args)=> logs.push(args), + error: (...args)=> logs.push(args), + dump(){ for (let each of logs) console.log (...each) }, + } + + process.env.PORT = '0' + const p = cds.exec ('run', project, ...args) // TODO w/ @sap/cds@3.33.3: , '--port', '0') + if (p && 'catch' in p) p.catch (done) + + cds.once('listening', ({ server, url }) => { + after (done => { + if (global.console !== console) global.console = console + server.close (done) + }) + baseURL = url + done() + }) + }) + + return { + + GET: (path) => axios.get (baseURL+path) .catch(_error), + PUT: (path,data) => axios.put (baseURL+path,data) .catch(_error), + POST: (path,data) => axios.post (baseURL+path,data) .catch(_error), + PATCH: (path,data) => axios.patch (baseURL+path,data) .catch(_error), + DELETE: (path) => axios.delete (baseURL+path) .catch(_error), + + get: (path) => axios.get (baseURL+path) .catch(_error), + put: (path,data) => axios.put (baseURL+path,data) .catch(_error), + post: (path,data) => axios.post (baseURL+path,data) .catch(_error), + patch: (path,data) => axios.patch (baseURL+path,data) .catch(_error), + delete: (path) => axios.delete (baseURL+path) .catch(_error), + + get chai(){ return _chai() }, + get expect(){ return this.chai.expect }, + get assert(){ return this.chai.assert }, + } + + function _error (e) { + if (!e.response) throw e + if (!e.response.data) throw e + if (!e.response.data.error) throw e + const { code, message } = e.response.data.error + throw new Error (`${code} - ${message}`) + } +} diff --git a/reviews/test/messaging.test.js b/test/messaging.test.js similarity index 93% rename from reviews/test/messaging.test.js rename to test/messaging.test.js index d1ace70a..bd6545e6 100644 --- a/reviews/test/messaging.test.js +++ b/test/messaging.test.js @@ -1,8 +1,9 @@ -const _model = __dirname+'/..' +const {expect} = require('@capire/tests') const cds = require ('@sap/cds') -const {expect} = cds.require.chai +const _model = '@capire/reviews' -describe('messaging tests', ()=>{ + +describe('Messaging', ()=>{ it ('should bootstrap sqlite in-memory db', async()=>{ const db = await cds.deploy (_model) .to ('sqlite::memory:') @@ -10,7 +11,7 @@ describe('messaging tests', ()=>{ }) let srv - it ('should serve reviews services', async()=>{ + it ('should serve ReviewsService', async()=>{ srv = await cds.serve('ReviewsService') .from (_model) expect (srv.name) .to.match (/ReviewsService/) }) diff --git a/test/package.json b/test/package.json new file mode 100644 index 00000000..0e31900d --- /dev/null +++ b/test/package.json @@ -0,0 +1,5 @@ +{ + "name": "@capire/tests", + "version": "1.0.0", + "main": "lib/helpers.js" +} \ No newline at end of file