diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 949f3d8f..07592e01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ jobs: run: | npm install npm run lint + npm run test - name: get-version id: package-version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..6122157e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20.x, 18.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm i -g @sap/cds-dk + - run: npm i + - run: cd tests/incidents-app && npm i + - run: cds v + - run: npm run test \ No newline at end of file diff --git a/lib/basic.js b/lib/basic.js index 49619a19..8b42a8e2 100644 --- a/lib/basic.js +++ b/lib/basic.js @@ -82,7 +82,7 @@ module.exports = class AttachmentsService extends cds.Service { async getStatus(Attachments, key) { const result = await SELECT.from(Attachments, key).columns('status') - return result.status + return result?.status; } async deleteInfectedAttachment(Attachments, key) { diff --git a/package.json b/package.json index 87a7f709..b0aef765 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "srv" ], "scripts": { - "lint": "npx eslint ." + "lint": "npx eslint .", + "test": "npx jest attachments.test.js" }, "dependencies": { "@aws-sdk/client-s3": "^3.400.0", diff --git a/tests/incidents-app/db/attachments.cds b/tests/incidents-app/db/attachments.cds new file mode 100644 index 00000000..f58acd59 --- /dev/null +++ b/tests/incidents-app/db/attachments.cds @@ -0,0 +1,7 @@ + +using { sap.capire.incidents as my } from './schema'; +using { Attachments } from '@cap-js/attachments'; + +extend my.Incidents with { + attachments: Composition of many Attachments; +} \ No newline at end of file diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Addresses.csv b/tests/incidents-app/db/data/sap.capire.incidents-Addresses.csv new file mode 100644 index 00000000..e5ee93a6 --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Addresses.csv @@ -0,0 +1,4 @@ +ID,customer_ID,city,postCode,streetAddress +17e00347-dc7e-4ca9-9c5d-06ccef69f064,1004155,Rome,00164,Piazza Adriana +d8e797d9-6507-4aaa-b43f-5d2301df5135,1004161,Munich,80809,Olympia Park +ff13d2fa-e00f-4ee5-951c-3303f490777b,1004100,Walldorf,69190,Dietmar-Hopp-Allee diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Customers.csv b/tests/incidents-app/db/data/sap.capire.incidents-Customers.csv new file mode 100644 index 00000000..f69a5a8f --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Customers.csv @@ -0,0 +1,4 @@ +ID,firstName,lastName,email,phone +1004155,Daniel,Watts,daniel.watts@demo.com,+44-555-123 +1004161,Stormy,Weathers,stormy.weathers@demo.com, +1004100,Sunny,Sunshine,sunny.sunshine@demo.com,+01-555-789 diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Incidents.conversation.csv b/tests/incidents-app/db/data/sap.capire.incidents-Incidents.conversation.csv new file mode 100644 index 00000000..bc40667d --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Incidents.conversation.csv @@ -0,0 +1,5 @@ +ID,up__ID,timestamp,author,message +2b23bb4b-4ac7-4a24-ac02-aa10cabd842c,3b23bb4b-4ac7-4a24-ac02-aa10cabd842c,1995-12-17T03:24:00Z,Harry John,Can you please check if battery connections are fine? +2b23bb4b-4ac7-4a24-ac02-aa10cabd843c,3a4ede72-244a-4f5f-8efa-b17e032d01ee,1995-12-18T04:24:00Z,Emily Elizabeth,Can you please check if there are any loose connections? +9583f982-d7df-4aad-ab26-301d4a157cd7,3583f982-d7df-4aad-ab26-301d4a157cd7,2022-09-04T12:00:00Z,Sunny Sunshine,Please check why the solar panel is broken +9583f982-d7df-4aad-ab26-301d4a158cd7,3ccf474c-3881-44b7-99fb-59a2a4668418,2022-09-04T13:00:00Z,Bradley Flowers,What exactly is wrong? diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Incidents.csv b/tests/incidents-app/db/data/sap.capire.incidents-Incidents.csv new file mode 100644 index 00000000..6bf33b85 --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Incidents.csv @@ -0,0 +1,5 @@ +ID,customer_ID,title,urgency_code,status_code +3b23bb4b-4ac7-4a24-ac02-aa10cabd842c,1004155,Inverter not functional,H,C +3a4ede72-244a-4f5f-8efa-b17e032d01ee,1004161,No current on a sunny day,H,N +3ccf474c-3881-44b7-99fb-59a2a4668418,1004161,Strange noise when switching off Inverter,M,N +3583f982-d7df-4aad-ab26-301d4a157cd7,1004100,Solar panel broken,H,I \ No newline at end of file diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Status.csv b/tests/incidents-app/db/data/sap.capire.incidents-Status.csv new file mode 100644 index 00000000..c1170c22 --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Status.csv @@ -0,0 +1,7 @@ +code;descr;criticality +N;New;3 +A;Assigned;2 +I;In Process;2 +H;On Hold;3 +R;Resolved;2 +C;Closed;4 \ No newline at end of file diff --git a/tests/incidents-app/db/data/sap.capire.incidents-Urgency.csv b/tests/incidents-app/db/data/sap.capire.incidents-Urgency.csv new file mode 100644 index 00000000..7350a2ff --- /dev/null +++ b/tests/incidents-app/db/data/sap.capire.incidents-Urgency.csv @@ -0,0 +1,4 @@ +code;descr +H;High +M;Medium +L;Low \ No newline at end of file diff --git a/tests/incidents-app/db/schema.cds b/tests/incidents-app/db/schema.cds new file mode 100644 index 00000000..bd53af97 --- /dev/null +++ b/tests/incidents-app/db/schema.cds @@ -0,0 +1,66 @@ +using { cuid, managed, sap.common.CodeList } from '@sap/cds/common'; + +namespace sap.capire.incidents; + +/** + * Customers using products sold by our company. + * Customers can create support Incidents. + */ +entity Customers : managed { + key ID : String; + firstName : String; + lastName : String; + name : String = firstName ||' '|| lastName; + email : EMailAddress; + phone : PhoneNumber; + creditCardNo : String(16) @assert.format: '^[1-9]\d{15}$'; + addresses : Composition of many Addresses on addresses.customer = $self; + incidents : Association to many Incidents on incidents.customer = $self; +} + +entity Addresses : cuid, managed { + customer : Association to Customers; + city : String; + postCode : String; + streetAddress : String; +} + + +/** + * Incidents created by Customers. + */ +entity Incidents : cuid, managed { + customer : Association to Customers; + title : String @title: 'Title'; + urgency : Association to Urgency default 'M'; + status : Association to Status default 'N'; + conversation : Composition of many { + key ID : UUID; + timestamp : type of managed:createdAt; + author : type of managed:createdBy; + message : String; + }; +} + +entity Status : CodeList { + key code : String enum { + new = 'N'; + assigned = 'A'; + in_process = 'I'; + on_hold = 'H'; + resolved = 'R'; + closed = 'C'; + }; + criticality : Integer; +} + +entity Urgency : CodeList { + key code : String enum { + high = 'H'; + medium = 'M'; + low = 'L'; + }; +} + +type EMailAddress : String; +type PhoneNumber : String; diff --git a/tests/incidents-app/package.json b/tests/incidents-app/package.json new file mode 100644 index 00000000..dd1afd1c --- /dev/null +++ b/tests/incidents-app/package.json @@ -0,0 +1,33 @@ +{ + "name": "@capire/incidents", + "version": "1.0.0", + "dependencies": { + "@cap-js/attachments": "file:../../." + }, + "cds": { + "requires": { + "auth": { + "[development]": { + "users": { + "alice": { + "roles": [ + "support", + "admin" + ] + }, + "bob": { + "roles": [ + "support" + ] + } + } + } + }, + "attachments": { + "kind": "db", + "scan": false + } + } + }, + "private": true +} diff --git a/tests/incidents-app/srv/services.cds b/tests/incidents-app/srv/services.cds new file mode 100644 index 00000000..14db1dca --- /dev/null +++ b/tests/incidents-app/srv/services.cds @@ -0,0 +1,21 @@ +using { sap.capire.incidents as my } from '../db/schema'; + +/** + * Service used by support personell, i.e. the incidents' 'processors'. + */ +service ProcessorService { + entity Incidents as projection on my.Incidents; + entity Customers @readonly as projection on my.Customers; +} + +/** + * Service used by administrators to manage customers and incidents. + */ +service AdminService { + entity Customers as projection on my.Customers; + entity Incidents as projection on my.Incidents; +} + +annotate ProcessorService.Incidents with @odata.draft.enabled; +annotate ProcessorService with @(requires: 'support'); +annotate AdminService with @(requires: 'admin'); diff --git a/tests/incidents-app/srv/services.js b/tests/incidents-app/srv/services.js new file mode 100644 index 00000000..03130b87 --- /dev/null +++ b/tests/incidents-app/srv/services.js @@ -0,0 +1,32 @@ +const cds = require('@sap/cds') +const { SELECT } = cds.ql + +class ProcessorService extends cds.ApplicationService { + /** Registering custom event handlers */ + init() { + this.before('UPDATE', 'Incidents', req => this.onUpdate(req)) + this.before(['CREATE', 'UPDATE'], 'Incidents', req => this.changeUrgencyDueToSubject(req.data)) + return super.init() + } + + changeUrgencyDueToSubject(data) { + if (data) { + const incidents = Array.isArray(data) ? data : [data] + incidents.forEach(incident => { + if (incident.title?.toLowerCase().includes('urgent')) { + incident.urgency = { code: 'H', descr: 'High' } + } + }) + } + } + + /** Custom Validation */ + async onUpdate(req) { + const { status_code } = await SELECT.one(req.subject, i => i.status_code).where({ ID: req.data.ID }) + if (status_code === 'C') { + return req.reject(`Can't modify a closed incident`) + } + } +} + +module.exports = { ProcessorService } diff --git a/tests/integration/attachments.test.js b/tests/integration/attachments.test.js index a27f8ed1..92075a93 100644 --- a/tests/integration/attachments.test.js +++ b/tests/integration/attachments.test.js @@ -1,6 +1,7 @@ const cds = require("@sap/cds"); -const incidentsApp = require("path").resolve(__dirname, "./../../xmpl"); -const { expect, axios, GET, POST, DELETE } = cds.test(incidentsApp); +const path = require("path"); +const app = path.resolve(__dirname, "../incidents-app"); +const { expect, axios, GET, POST, DELETE } = cds.test(app); const { RequestSend } = require("../utils/api"); const { createReadStream } = cds.utils.fs; const { join } = cds.utils.path; @@ -8,54 +9,21 @@ const { join } = cds.utils.path; axios.defaults.auth = { username: "alice" }; jest.setTimeout(5 * 60 * 1000); -const utils = new RequestSend(POST); +let utils = null; let sampleDocID = null; let incidentID = null; - -describe("Tests for mock data in xmpl attachments - in-memory db", () => { - beforeAll(() => { - sampleDocID = null; - incidentID = "3b23bb4b-4ac7-4a24-ac02-aa10cabd842c"; - }); - - //Reading the attachment list and checking for content - it("Reading attachments list", async () => { - //read attachments list for Incident - Inverter not functional - try { - const response = await GET( - `odata/v4/processor/Incidents(ID=${incidentID},IsActiveEntity=true)/attachments` - ); - //the mock data has two attachments in this incident - expect(response.status).to.equal(200); - expect(response.data.value.length).to.equal(2); - sampleDocID = response.data.value[0].ID; - //to make sure content is not read - expect(response.data.value[0].content).to.be.undefined; - } catch (err) { - expect(err).to.be.undefined; - } - }); - - //Reading the uploaded attachment content and that it exists - it("Reading the uploaded attachment document", async () => { - //checking the uploaded attachment document - - try { - const response = await GET( - `odata/v4/processor/Incidents(ID=${incidentID},IsActiveEntity=true)/attachments(up__ID=${incidentID},ID=${sampleDocID},IsActiveEntity=true)/content` - ); - expect(response.status).to.equal(200); - expect(response.data).to.not.be.undefined; - } catch (err) { - expect(err).to.be.undefined; - } - }); -}); +let db = null; +let attachmentsService = null; describe("Tests for uploading/deleting attachments through API calls - in-memory db", () => { beforeAll(async () => { + cds.env.requires.db.kind = "sql"; + cds.env.requires.attachments.kind = "db"; + db = await cds.connect.to("sql:my.db"); + attachmentsService = await cds.connect.to("attachments"); sampleDocID = null; incidentID = "3ccf474c-3881-44b7-99fb-59a2a4668418"; + utils = new RequestSend(POST); }); //Draft mode uploading attachment @@ -94,15 +62,12 @@ describe("Tests for uploading/deleting attachments through API calls - in-memory const response = await GET( `odata/v4/processor/Incidents(ID=${incidentID},IsActiveEntity=true)/attachments` ); - //the data should have two attachments + //the data should have only one attachment expect(response.status).to.equal(200); - expect(response.data.value.length).to.equal(2); + expect(response.data.value.length).to.equal(1); //to make sure content is not read expect(response.data.value[0].content).to.be.undefined; - sampleDocID = - response.data.value[0].filename == "sample.pdf" - ? response.data.value[0].ID - : response.data.value[1].ID; + sampleDocID = response.data.value[0].ID; } catch (err) { expect(err).to.be.undefined; } @@ -149,6 +114,18 @@ describe("Tests for uploading/deleting attachments through API calls - in-memory expect(err).to.be.undefined; } + //read attachments list for Incident + try { + const response = await GET( + `odata/v4/processor/Incidents(ID=${incidentID},IsActiveEntity=true)/attachments` + ); + //the data should have no attachments + expect(response.status).to.equal(200); + expect(response.data.value.length).to.equal(0); + } catch (err) { + expect(err).to.be.undefined; + } + //content should not be there try { const response = await GET(