From 3a06c277c44b5825fcf185a11861c18cd7ca6db9 Mon Sep 17 00:00:00 2001 From: Henning Heitkoetter Date: Tue, 22 May 2018 21:13:33 +0200 Subject: [PATCH] Initial commit of mock server --- .cfignore | 3 + .gitignore | 13 + LICENSE | 209 +++++++ NOTICE | 1 + README.md | 103 ++++ app.js | 29 + business-partner/business-partner-api.js | 119 ++++ business-partner/business-partner-data.js | 515 ++++++++++++++++++ business-partner/business-partner-model.js | 244 +++++++++ integration-tests/pom.xml | 123 +++++ .../ModifyBusinessPartnerAddressTest.java | 155 ++++++ .../ModifyBusinessPartnerTest.java | 105 ++++ .../ReadBusinessPartnerAddressTest.java | 140 +++++ .../ReadBusinessPartnerTest.java | 273 ++++++++++ .../src/test/resources/credentials.yml | 5 + .../src/test/resources/systems.yml | 7 + manifest.yml | 6 + odata-helpers.js | 220 ++++++++ package-lock.json | 362 ++++++++++++ package.json | 20 + 20 files changed, 2652 insertions(+) create mode 100644 .cfignore create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 app.js create mode 100644 business-partner/business-partner-api.js create mode 100644 business-partner/business-partner-data.js create mode 100644 business-partner/business-partner-model.js create mode 100644 integration-tests/pom.xml create mode 100644 integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerAddressTest.java create mode 100644 integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerTest.java create mode 100644 integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerAddressTest.java create mode 100644 integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerTest.java create mode 100644 integration-tests/src/test/resources/credentials.yml create mode 100644 integration-tests/src/test/resources/systems.yml create mode 100644 manifest.yml create mode 100644 odata-helpers.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.cfignore b/.cfignore new file mode 100644 index 00000000..f2500954 --- /dev/null +++ b/.cfignore @@ -0,0 +1,3 @@ +integration-tests/ +s4hana_pipeline/ +.vscode/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ab01a148 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +target/ + +.vscode/ +.idea/ +*.iml +.settings/ +.classpath +.project + +s4hana_pipeline/ + +business-partner/API_BUSINESS_PARTNER.edmx \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..47a8f613 --- /dev/null +++ b/LICENSE @@ -0,0 +1,209 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ +APIs + +This project may include APIs to SAP or third party products or services. The use of these APIs, products and services may be subject to additional agreements. In no event shall the application of the Apache Software License, v.2 to this project grant any rights in or to these APIs, products or services that would alter, expand, be inconsistent with, or supersede any terms of these additional agreements. “API” means application programming interfaces, as well as their respective specifications and implementing code that allows other software products to communicate with or call on SAP or third party products or services (for example, SAP Enterprise Services, BAPIs, Idocs, RFCs and ABAP calls or other user exits) and may be made available through SAP or third party products, SDKs, documentation or other media. + +------------------------------------------------------------------------------ diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..8adf0514 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. diff --git a/README.md b/README.md new file mode 100644 index 00000000..f0f748de --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# OData Mock Service for Business Partner API of SAP S/4HANA Cloud +The simple Node.js-based server contained in this repository represents a mock server for the purposes of testing the SAP S/4HANA integration capabilities of the SAP S/4HANA Cloud SDK. +The server makes it possible to test the SAP S/4HANA integration capabilities of the SAP S/4HANA Cloud SDK without access to an SAP S/4HANA system. +The server hosts an OData v2 mock service that mimicks the business partner API of SAP S/4HANA Cloud to a limited extent. + +The mock server can be used as a stand-in for simple tests and experiments with the SAP S/4HANA Cloud SDK if no SAP S/4HANA system is available. +It is especially tailored towards the examples found in the book [_Extending SAP S/4HANA Cloud_](https://www.sap-press.com/extending-sap-s4hana_4655/) or in the [tutorials of the SAP S/4HANA Cloud SDK](https://blogs.sap.com/2017/05/10/first-steps-with-sap-s4hana-cloud-sdk/). +This page explains how to run the mock server and how to integrate it into the tests of the sample application. + +> **Note**: the server is not secured in any way. Run the server on your own risk and only for experiments. Do not use the server to store any personal data - only use fake data. + +## How to run the server +When you have cloned this repository, checkout the branch `mock-server`. +Alternatively, download [this archive](https://github.com/SAP/cloud-s4-sdk-book/archive/mock-server.zip) and unzip it to your local machine. +All of the following steps shall happen in this folder where you checked out or extraced the code of the mock server. + +Before you can launch the mock server, you need to manually put the metadata EDMX document of the business partner OData service into the folder `business-partner` and prepare the document: +* Go to the description of the [Business Partner API in the SAP API Business Hub](https://api.sap.com/shell/discover/contentpackage/SAPS4HANACloud/api/API_BUSINESS_PARTNER). +* Click on *Login* and login with your credentials (you may need to register beforehand). +* Click on *Download API* and choose *EDMX*. +* Store the downloaded file with the name `API_BUSINESS_PARTNER.edmx` in the subfolder `business-partner` of the mock server folder. +* Open the metadata document and locate the entity type `A_BusinessPartnerType`. Within the `EntityType` item with that name, add the following two lines behind the line that contains ``: +``` + + +``` + +After you have thus prepared the mock server, you can run the mock server on your local machine (at http://localhost:3000) or on SAP Cloud Platform, Cloud Foundry, as described in either of the following two sections. + +### Locally +#### Prerequisites +The following tools need to be installed on your local machine. +* [node.js](http://npmjs.com) 8.x or higher +* [npm](http://npmjs.com) + +#### Launch the mock server +Execute the following commands in a command line shell of your choice within the folder where you stored the artifacts: +``` +npm install +npm start +``` + +Wait until you see the output `Mock server started`. Access the mock OData service at http://localhost:3000/sap/opu/odata/sap/API_BUSINESS_PARTNER (no credentials required). + +Use `http://localhost:3000` as the URL for your destination `ErpQueryEndpoint` with any dummy user and password, for example: +``` +destinations=[{name: 'ErpQueryEndpoint', url: 'http://localhost:3000', username: 'DUMMY', password: 'dummy'}] +``` + +### On SAP Cloud Platform, Cloud Foundry +#### Prerequisites +The following tools need to be installed on your local machine. +* [Cloud Foundry command line interface (CLI)](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) + +#### Launch the mock server +Execute the following commands (this assumes that you have set up your Cloud Foundry account at region EU10): +``` +cf api https://api.cf.eu10.hana.ondemand.com +cf login +cf push +``` + +Watch the output of the command for the URL of the Cloud Foundry app. Look for a line towards the end similar to the following: +``` +urls: bupa-mock-odata-.cfapps.eu10.hana.ondemand.com +``` +Access the mock OData service at that URL, by appending the path `/sap/opu/odata/sap/API_BUSINESS_PARTNER`. + +Use your specific URL like `https://bupa-mock-odata-.cfapps.eu10.hana.ondemand.com` as the URL for your destination `ErpQueryEndpoint` with any dummy user and password (or choose _No Authentication_ when defining the destination with the destination service on SAP Cloud Platform). + +## Limitations +As a mock server, the functionality of the mock OData service is limited to the most essential features. It is by no means a complete OData service compliant with the OData v2 specification. Also, it is beyond the plain API not comparable with the business partner API of SAP S/4HANA Cloud. + +Consider the following detailed limitations: +* The service performs almost no business validations, most fields can be set to any value independent of business semantics. +* The behavior is not comparable at all to SAP S/4HANA Cloud. +* Error handling and error messages are entirely different. +* No security: no authentication, no CSRF protection. +* The mock service supports deleting business partners, in contrast to the API of SAP S/4HANA. +* Only simple filters of the kind `PropertyName eq 'value'` are supported. +* Sorting via `orderby` is only supported for single simple String properties of the returned entities, not complex-typed or navigation properties. +* `PUT` behaves like `PATCH` and merges the supplied entity's properties into the existing entity. +* Only multi-valued navigation properties are properly handled in expand and select query options. + +## Supported OData operations and capabilities +The OData mock service supports the following features centered around the `API_BUSINESS_PARTNER`, and thus allows to test certain scenarios using this mock server instead of an SAP S/4HANA system: +* Entity types _Business Partner_ and _Business Partner Address_ with all properties (including two custom fields on business partners). +* OData queries (`GET`) on entity sets and by key with query options `expand`, `select`, `filter`, and `orderby`. For filtering, only string equality comparisons are supported, no complex filter expressions nor other operators than `eq`. +* Creating (`DELETE`), updating (`PATCH`, `PUT`), and deleting (`DELETE`) entities. Modifications are only stored in-memory. +* Retrieving metadata document (`/$metadata`). The full metadata of the API is returned, not only the supported part. + +Also see the accompanying test suite in folder `integration-tests`, implemented as Java JUnit tests using the SAP S/4HANA Cloud SDK, specifically its Virtual Data Model (VDM). + +## Background +The mock server is implemented in JavaScript / Node.js using [Express](http://expressjs.com). + +Entry point of the server is in `app.js`. +The implementation of the business partner and address api and model can be found in folder `business-partner`. The demo data is defined in the file `business-partner-data.js`. +OData features are implemented as generic Express middlewares in `odata-helper.js`. + +## License +Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. +This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the [LICENSE](LICENSE) file. diff --git a/app.js b/app.js new file mode 100644 index 00000000..8dd04903 --- /dev/null +++ b/app.js @@ -0,0 +1,29 @@ +const nodeAppStarted = Date.now(); +const express = require('express'); +const app = express(); + +const bupaApi = require('./business-partner/business-partner-api.js'); + +const logRequests = function(req, res, next) { + console.log(`Request: ${req.method} ${req.originalUrl}`) + next(); +}; + +app.use(logRequests); + +app.use('/sap/opu/odata/sap/API_BUSINESS_PARTNER', bupaApi); + +app.get('/', function(req, res) { + res.set('Content-Type', 'text/html'); + res.send(` + + OData Mock Service for Business Partner API of SAP S/4HANA Cloud + + +
OData mock service for Business Partner API of SAP S/4HANA Cloud is running at /sap/opu/odata/sap/API_BUSINESS_PARTNER
+ +`); +}); + +const port = process.env.PORT || 3000; +app.listen(port, () => console.log(`Mock server started on port ${port} after ${Date.now() - nodeAppStarted} ms, running - stop with CTRL+C (or CMD+C)...`)) diff --git a/business-partner/business-partner-api.js b/business-partner/business-partner-api.js new file mode 100644 index 00000000..462c66b8 --- /dev/null +++ b/business-partner/business-partner-api.js @@ -0,0 +1,119 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const router = express.Router(); + +const odata = require('../odata-helpers.js'); +const bupaModel = require('./business-partner-model.js'); + +const retrieveAllBusinessPartners = function(req, res, next) { + console.log('Reading business partner entity set'); + res.result = bupaModel.getBusinessPartners(); + next(); +}; + +const retrieveSingleBusinessPartner = function(req, res, next) { + console.log(`Reading business partner ${req.params.id}`); + res.result = bupaModel.findBusinessPartner(req.params.id); + next(); +}; + +const retrieveAllAddresses = function(req, res, next) { + console.log('Reading address entity set'); + res.result = bupaModel.getAddresses(); + next(); +}; + +const retrieveSingleAddress = function(req, res, next) { + console.log(`Reading address (${req.params.bupaId},${req.params.addressId})`); + res.result = bupaModel.findAddress(req.params.bupaId, req.params.addressId); + next(); +}; + +const createBusinessPartner = function(req, res, next) { + console.log('Creating business partner'); + res.result = bupaModel.createAndAddBusinessPartner(req.body); + console.log(`Created business partner ${res.result.BusinessPartner}`) + next(); +}; + +const createAddress = function(req, res, next) { + console.log('Creating address'); + res.result = bupaModel.createAndAddAddress(req.body); + console.log(`Created address (${res.result.BusinessPartner},${res.result.AddressID})`) + next(); +}; + +const deleteBusinessPartner = function(req, res, next) { + console.log(`Deleting business partner ${req.params.id}`); + bupaModel.deleteBusinessPartner(req.params.id); + next(); +}; + +const deleteAddress = function(req, res, next) { + console.log(`Deleting address (${req.params.bupaId},${req.params.addressId})`); + bupaModel.deleteAddress(req.params.bupaId, req.params.addressId); + next(); +}; + +const modifyBusinessPartner = function(req, res, next) { + console.log(`Modifying business partner ${req.params.id}`); + bupaModel.modifyBusinessPartner(req.params.id, req.body); + next(); +}; + +const modifyAddress = function(req, res, next) { + console.log(`Modifying address (${req.params.bupaId},${req.params.addressId})`); + bupaModel.modifyAddress(req.params.bupaId, req.params.addressId, req.body); + next(); +}; + +// Serve EDMX file for /$metadata +router.get('/([$])metadata', function(req, res) { + const options = { + root: __dirname + '/', + headers: { + 'Content-Type': 'application/xml' + } + }; + console.log('Serving metadata for Business Partner API'); + res.sendFile('API_BUSINESS_PARTNER.edmx', options, function(err) { + if(err) { + console.error('No metadata file found at business-partner/API_BUSINESS_PARTNER.edmx. Please check the documentation on how to retrieve and where to store this file.') + res.sendStatus(404); + } + }); +}); + +const handlersForBusinessPartnerUpdate = odata.middlewareForUpdate(retrieveSingleBusinessPartner, modifyBusinessPartner); +const handlersForAddressUpdate = odata.middlewareForUpdate(retrieveSingleAddress, modifyAddress); + +router.route('/A_BusinessPartner') +.get(retrieveAllBusinessPartners, odata.middlewareForSet()) +.post(bodyParser.json(), createBusinessPartner, odata.sendAsODataResult); + +router.route('/A_BusinessPartner\\((BusinessPartner=)?\':id\'\\)') +.get(retrieveSingleBusinessPartner, odata.middlewareForEntity()) +.delete(retrieveSingleBusinessPartner, odata.send404IfNotFound, deleteBusinessPartner, odata.send204NoContent) +.patch(handlersForBusinessPartnerUpdate).put(handlersForBusinessPartnerUpdate); + +router.route('/A_BusinessPartnerAddress') +.get(retrieveAllAddresses, odata.middlewareForSet()) +.post(bodyParser.json(), createAddress, odata.sendAsODataResult); + +router.route('/A_BusinessPartnerAddress\\((BusinessPartner=)?\':bupaId\',(AddressID=)?\':addressId\'\\)') +.get(retrieveSingleAddress, odata.middlewareForEntity()) +.delete(retrieveSingleAddress, odata.send404IfNotFound, deleteAddress, odata.send204NoContent) +.patch(handlersForAddressUpdate).put(handlersForAddressUpdate); + +router.get('/', function(req, res) { + res.json({ + "d": { + "EntitySets": [ + "A_BusinessPartner", + "A_BusinessPartnerAddress" + ] + } + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/business-partner/business-partner-data.js b/business-partner/business-partner-data.js new file mode 100644 index 00000000..4c017335 --- /dev/null +++ b/business-partner/business-partner-data.js @@ -0,0 +1,515 @@ +/* + * This module hosts the initial data of the mock server. + * Replace the content of the array "data" with the intended demo data. + * Replace the value of deferred (non-expanded) navigation properties with the appropriate empty value, also recursively: + * - for multi-valued propeties: { "results": [] } + * - for single-valued properties: null + */ +module.exports = { + data: [ + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003764')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003764')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" + }, + "BusinessPartner": "1003764", + "Customer": "", + "Supplier": "", + "AcademicTitle": "", + "AuthorizationGroup": "", + "BusinessPartnerCategory": "1", + "BusinessPartnerFullName": "John Doe", + "BusinessPartnerGrouping": "BP02", + "BusinessPartnerName": "John Doe", + "BusinessPartnerUUID": "00163e30-4e2a-1ed8-8483-a08c52249f04", + "CorrespondenceLanguage": "", + "CreatedByUser": "CC0000000002", + "CreationDate": "/Date(1518393600000)/", + "CreationTime": "PT17H49M05S", + "FirstName": "John", + "FormOfAddress": "", + "Industry": "", + "InternationalLocationNumber1": "0", + "InternationalLocationNumber2": "0", + "IsFemale": false, + "IsMale": true, + "IsNaturalPerson": "", + "IsSexUnknown": false, + "Language": "", + "LastChangeDate": null, + "LastChangeTime": "PT00H00M00S", + "LastChangedByUser": "", + "LastName": "Doe", + "LegalForm": "", + "OrganizationBPName1": "", + "OrganizationBPName2": "", + "OrganizationBPName3": "", + "OrganizationBPName4": "", + "OrganizationFoundationDate": null, + "OrganizationLiquidationDate": null, + "SearchTerm1": "", + "AdditionalLastName": "", + "BirthDate": null, + "BusinessPartnerIsBlocked": false, + "BusinessPartnerType": "", + "ETag": "CC000000000220180212174905", + "GroupBusinessPartnerName1": "", + "GroupBusinessPartnerName2": "", + "IndependentAddressID": "", + "InternationalLocationNumber3": "0", + "MiddleName": "", + "NameCountry": "", + "NameFormat": "", + "PersonFullName": "", + "PersonNumber": "28237", + "IsMarkedForArchiving": false, + "BusinessPartnerIDByExtSystem": "", + "YY1_AddrLastCheckedOn_bus": null, + "YY1_AddrLastCheckedBy_bus": "", + "to_BuPaIdentification": { "results": [] }, + "to_BusinessPartnerAddress": { + "results": [ + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003764',AddressID='28238')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003764',AddressID='28238')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerAddressType" + }, + "BusinessPartner": "1003764", + "AddressID": "28238", + "ValidityStartDate": "/Date(1518393600000+0000)/", + "ValidityEndDate": "/Date(253402300799000+0000)/", + "AuthorizationGroup": "", + "AddressUUID": "00163e30-4e2a-1ed8-8483-a08c5224bf04", + "AdditionalStreetPrefixName": "", + "AdditionalStreetSuffixName": "", + "AddressTimeZone": "CET", + "CareOfName": "", + "CityCode": "", + "CityName": "Walldorf", + "CompanyPostalCode": "", + "Country": "DE", + "County": "", + "DeliveryServiceNumber": "", + "DeliveryServiceTypeCode": "", + "District": "", + "FormOfAddress": "", + "FullName": "", + "HomeCityName": "", + "HouseNumber": "16", + "HouseNumberSupplementText": "", + "Language": "", + "POBox": "", + "POBoxDeviatingCityName": "", + "POBoxDeviatingCountry": "", + "POBoxDeviatingRegion": "", + "POBoxIsWithoutNumber": false, + "POBoxLobbyName": "", + "POBoxPostalCode": "", + "Person": "28237", + "PostalCode": "69190", + "PrfrdCommMediumType": "", + "Region": "", + "StreetName": "Dietmar-Hopp-Allee", + "StreetPrefixName": "", + "StreetSuffixName": "", + "TaxJurisdiction": "", + "TransportZone": "", + "AddressIDByExternalSystem": "", + "to_AddressUsage": { "results": [] }, + "to_EmailAddress": { "results": [] }, + "to_FaxNumber": { "results": [] }, + "to_MobilePhoneNumber": { "results": [] }, + "to_PhoneNumber": { "results": [] }, + "to_URLAddress": { "results": [] } + } + ] + }, + "to_BusinessPartnerBank": { "results": [] }, + "to_BusinessPartnerContact": { "results": [] }, + "to_BusinessPartnerRole": { "results": [] }, + "to_BusinessPartnerTax": { "results": [] }, + "to_Customer": null, + "to_Supplier": null + }, + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003765')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003765')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" + }, + "BusinessPartner": "1003765", + "Customer": "", + "Supplier": "", + "AcademicTitle": "", + "AuthorizationGroup": "", + "BusinessPartnerCategory": "1", + "BusinessPartnerFullName": "Jane Roe", + "BusinessPartnerGrouping": "BP02", + "BusinessPartnerName": "Jane Roe", + "BusinessPartnerUUID": "00163e30-4e2a-1ed8-8483-a0a5f4c2bf04", + "CorrespondenceLanguage": "", + "CreatedByUser": "CC0000000002", + "CreationDate": "/Date(1518393600000)/", + "CreationTime": "PT17H49M06S", + "FirstName": "Jane", + "FormOfAddress": "", + "Industry": "", + "InternationalLocationNumber1": "0", + "InternationalLocationNumber2": "0", + "IsFemale": true, + "IsMale": false, + "IsNaturalPerson": "", + "IsSexUnknown": false, + "Language": "", + "LastChangeDate": null, + "LastChangeTime": "PT00H00M00S", + "LastChangedByUser": "", + "LastName": "Roe", + "LegalForm": "", + "OrganizationBPName1": "", + "OrganizationBPName2": "", + "OrganizationBPName3": "", + "OrganizationBPName4": "", + "OrganizationFoundationDate": null, + "OrganizationLiquidationDate": null, + "SearchTerm1": "", + "AdditionalLastName": "", + "BirthDate": null, + "BusinessPartnerIsBlocked": false, + "BusinessPartnerType": "", + "ETag": "CC000000000220180212174906", + "GroupBusinessPartnerName1": "", + "GroupBusinessPartnerName2": "", + "IndependentAddressID": "", + "InternationalLocationNumber3": "0", + "MiddleName": "", + "NameCountry": "", + "NameFormat": "", + "PersonFullName": "", + "PersonNumber": "28240", + "IsMarkedForArchiving": false, + "BusinessPartnerIDByExtSystem": "", + "YY1_AddrLastCheckedOn_bus": null, + "YY1_AddrLastCheckedBy_bus": "", + "to_BuPaIdentification": { "results": [] }, + "to_BusinessPartnerAddress": { + "results": [ + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003765',AddressID='28241')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003765',AddressID='28241')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerAddressType" + }, + "BusinessPartner": "1003765", + "AddressID": "28241", + "ValidityStartDate": "/Date(1518393600000+0000)/", + "ValidityEndDate": "/Date(253402300799000+0000)/", + "AuthorizationGroup": "", + "AddressUUID": "00163e30-4e2a-1ed8-8483-a0a5f4c2df04", + "AdditionalStreetPrefixName": "", + "AdditionalStreetSuffixName": "", + "AddressTimeZone": "PST", + "CareOfName": "", + "CityCode": "", + "CityName": "Palo Alto", + "CompanyPostalCode": "", + "Country": "US", + "County": "", + "DeliveryServiceNumber": "", + "DeliveryServiceTypeCode": "", + "District": "", + "FormOfAddress": "", + "FullName": "", + "HomeCityName": "", + "HouseNumber": "3410", + "HouseNumberSupplementText": "", + "Language": "", + "POBox": "", + "POBoxDeviatingCityName": "", + "POBoxDeviatingCountry": "", + "POBoxDeviatingRegion": "", + "POBoxIsWithoutNumber": false, + "POBoxLobbyName": "", + "POBoxPostalCode": "", + "Person": "28240", + "PostalCode": "CA 94304", + "PrfrdCommMediumType": "", + "Region": "", + "StreetName": "Hillview Avenue", + "StreetPrefixName": "", + "StreetSuffixName": "", + "TaxJurisdiction": "", + "TransportZone": "", + "AddressIDByExternalSystem": "", + "to_AddressUsage": { "results": [] }, + "to_EmailAddress": { "results": [] }, + "to_FaxNumber": { "results": [] }, + "to_MobilePhoneNumber": { "results": [] }, + "to_PhoneNumber": { "results": [] }, + "to_URLAddress": { "results": [] } + } + ] + }, + "to_BusinessPartnerBank": { "results": [] }, + "to_BusinessPartnerContact": { "results": [] }, + "to_BusinessPartnerRole": { "results": [] }, + "to_BusinessPartnerTax": { "results": [] }, + "to_Customer": null, + "to_Supplier": null + }, + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003766')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003766')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" + }, + "BusinessPartner": "1003766", + "Customer": "", + "Supplier": "", + "AcademicTitle": "", + "AuthorizationGroup": "", + "BusinessPartnerCategory": "1", + "BusinessPartnerFullName": "John Smith", + "BusinessPartnerGrouping": "BP02", + "BusinessPartnerName": "John Smith", + "BusinessPartnerUUID": "00163e30-4e2a-1ed8-8483-a0b2387e1f04", + "CorrespondenceLanguage": "", + "CreatedByUser": "CC0000000002", + "CreationDate": "/Date(1518393600000)/", + "CreationTime": "PT17H49M07S", + "FirstName": "John", + "FormOfAddress": "", + "Industry": "", + "InternationalLocationNumber1": "0", + "InternationalLocationNumber2": "0", + "IsFemale": false, + "IsMale": true, + "IsNaturalPerson": "", + "IsSexUnknown": false, + "Language": "", + "LastChangeDate": null, + "LastChangeTime": "PT00H00M00S", + "LastChangedByUser": "", + "LastName": "Smith", + "LegalForm": "", + "OrganizationBPName1": "", + "OrganizationBPName2": "", + "OrganizationBPName3": "", + "OrganizationBPName4": "", + "OrganizationFoundationDate": null, + "OrganizationLiquidationDate": null, + "SearchTerm1": "", + "AdditionalLastName": "", + "BirthDate": null, + "BusinessPartnerIsBlocked": false, + "BusinessPartnerType": "", + "ETag": "CC000000000220180212174907", + "GroupBusinessPartnerName1": "", + "GroupBusinessPartnerName2": "", + "IndependentAddressID": "", + "InternationalLocationNumber3": "0", + "MiddleName": "", + "NameCountry": "", + "NameFormat": "", + "PersonFullName": "", + "PersonNumber": "28243", + "IsMarkedForArchiving": false, + "BusinessPartnerIDByExtSystem": "", + "YY1_AddrLastCheckedOn_bus": null, + "YY1_AddrLastCheckedBy_bus": "", + "to_BuPaIdentification": { "results": [] }, + "to_BusinessPartnerAddress": { + "results": [ + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003766',AddressID='28244')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003766',AddressID='28244')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerAddressType" + }, + "BusinessPartner": "1003766", + "AddressID": "28244", + "ValidityStartDate": "/Date(1518393600000+0000)/", + "ValidityEndDate": "/Date(253402300799000+0000)/", + "AuthorizationGroup": "", + "AddressUUID": "00163e30-4e2a-1ed8-8483-a0b2387e3f04", + "AdditionalStreetPrefixName": "", + "AdditionalStreetSuffixName": "", + "AddressTimeZone": "CET", + "CareOfName": "", + "CityCode": "", + "CityName": "Hallbergmoos", + "CompanyPostalCode": "", + "Country": "DE", + "County": "", + "DeliveryServiceNumber": "", + "DeliveryServiceTypeCode": "", + "District": "", + "FormOfAddress": "", + "FullName": "", + "HomeCityName": "", + "HouseNumber": "2", + "HouseNumberSupplementText": "", + "Language": "", + "POBox": "", + "POBoxDeviatingCityName": "", + "POBoxDeviatingCountry": "", + "POBoxDeviatingRegion": "", + "POBoxIsWithoutNumber": false, + "POBoxLobbyName": "", + "POBoxPostalCode": "", + "Person": "28243", + "PostalCode": "85399", + "PrfrdCommMediumType": "", + "Region": "", + "StreetName": "Zeppelinstraße", + "StreetPrefixName": "", + "StreetSuffixName": "", + "TaxJurisdiction": "", + "TransportZone": "", + "AddressIDByExternalSystem": "", + "to_AddressUsage": { "results": [] }, + "to_EmailAddress": { "results": [] }, + "to_FaxNumber": { "results": [] }, + "to_MobilePhoneNumber": { "results": [] }, + "to_PhoneNumber": { "results": [] }, + "to_URLAddress": { "results": [] } + } + ] + }, + "to_BusinessPartnerBank": { "results": [] }, + "to_BusinessPartnerContact": { "results": [] }, + "to_BusinessPartnerRole": { "results": [] }, + "to_BusinessPartnerTax": { "results": [] }, + "to_Customer": null, + "to_Supplier": null + }, + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003767')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('1003767')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" + }, + "BusinessPartner": "1003767", + "Customer": "", + "Supplier": "", + "AcademicTitle": "", + "AuthorizationGroup": "", + "BusinessPartnerCategory": "1", + "BusinessPartnerFullName": "Carla Coe", + "BusinessPartnerGrouping": "BP02", + "BusinessPartnerName": "Carla Coe", + "BusinessPartnerUUID": "00163e30-4e2a-1ed8-8483-a0c9ef089f04", + "CorrespondenceLanguage": "", + "CreatedByUser": "CC0000000002", + "CreationDate": "/Date(1518393600000)/", + "CreationTime": "PT17H49M08S", + "FirstName": "Carla", + "FormOfAddress": "", + "Industry": "", + "InternationalLocationNumber1": "0", + "InternationalLocationNumber2": "0", + "IsFemale": true, + "IsMale": false, + "IsNaturalPerson": "", + "IsSexUnknown": false, + "Language": "", + "LastChangeDate": "/Date(1519084800000)/", + "LastChangeTime": "PT12H30M13S", + "LastChangedByUser": "CC0000000002", + "LastName": "Coe", + "LegalForm": "", + "OrganizationBPName1": "", + "OrganizationBPName2": "", + "OrganizationBPName3": "", + "OrganizationBPName4": "", + "OrganizationFoundationDate": null, + "OrganizationLiquidationDate": null, + "SearchTerm1": "", + "AdditionalLastName": "", + "BirthDate": null, + "BusinessPartnerIsBlocked": false, + "BusinessPartnerType": "", + "ETag": "CC000000000220180220123013", + "GroupBusinessPartnerName1": "", + "GroupBusinessPartnerName2": "", + "IndependentAddressID": "", + "InternationalLocationNumber3": "0", + "MiddleName": "", + "NameCountry": "", + "NameFormat": "", + "PersonFullName": "", + "PersonNumber": "28246", + "IsMarkedForArchiving": false, + "BusinessPartnerIDByExtSystem": "", + "YY1_AddrLastCheckedOn_bus": null, + "YY1_AddrLastCheckedBy_bus": "", + "to_BuPaIdentification": { "results": [] }, + "to_BusinessPartnerAddress": { + "results": [ + { + "__metadata": { + "id": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003767',AddressID='28247')", + "uri": "https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='1003767',AddressID='28247')", + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerAddressType" + }, + "BusinessPartner": "1003767", + "AddressID": "28247", + "ValidityStartDate": "/Date(1518393600000+0000)/", + "ValidityEndDate": "/Date(253402300799000+0000)/", + "AuthorizationGroup": "", + "AddressUUID": "00163e30-4e2a-1ed8-8483-a0c9ef08bf04", + "AdditionalStreetPrefixName": "", + "AdditionalStreetSuffixName": "", + "AddressTimeZone": "CET", + "CareOfName": "", + "CityCode": "", + "CityName": "Potsdam", + "CompanyPostalCode": "", + "Country": "DE", + "County": "", + "DeliveryServiceNumber": "", + "DeliveryServiceTypeCode": "", + "District": "", + "FormOfAddress": "", + "FullName": "", + "HomeCityName": "", + "HouseNumber": "10", + "HouseNumberSupplementText": "", + "Language": "", + "POBox": "", + "POBoxDeviatingCityName": "", + "POBoxDeviatingCountry": "", + "POBoxDeviatingRegion": "", + "POBoxIsWithoutNumber": false, + "POBoxLobbyName": "", + "POBoxPostalCode": "", + "Person": "28246", + "PostalCode": "14469", + "PrfrdCommMediumType": "", + "Region": "", + "StreetName": "Konrad-Zuse-Ring", + "StreetPrefixName": "", + "StreetSuffixName": "", + "TaxJurisdiction": "", + "TransportZone": "", + "AddressIDByExternalSystem": "", + "to_AddressUsage": { "results": [] }, + "to_EmailAddress": { "results": [] }, + "to_FaxNumber": { "results": [] }, + "to_MobilePhoneNumber": { "results": [] }, + "to_PhoneNumber": { "results": [] }, + "to_URLAddress": { "results": [] } + } + ] + }, + "to_BusinessPartnerBank": { "results": [] }, + "to_BusinessPartnerContact": { "results": [] }, + "to_BusinessPartnerRole": { "results": [] }, + "to_BusinessPartnerTax": { "results": [] }, + "to_Customer": null, + "to_Supplier": null + } + ] +}; \ No newline at end of file diff --git a/business-partner/business-partner-model.js b/business-partner/business-partner-model.js new file mode 100644 index 00000000..10805218 --- /dev/null +++ b/business-partner/business-partner-model.js @@ -0,0 +1,244 @@ +const uuid = require('uuid/v1'); +const data = require('./business-partner-data.js').data; + +const today = function () { + const time = Date.now(); + return time - (time % 24 * 60 * 60 * 1000); +} + +const nextBusinessPartnerId = function(existingBusinessPartners) { + return String(Math.max(...existingBusinessPartners.map( (item) => Number(item.BusinessPartner) )) + 1); +}; + +const nextAddressId = function(existingAddresses) { + return String(Math.max(...existingAddresses.map( (item) => Number(item.AddressID) )) + 1); +}; + +module.exports = { + data: data, + newBusinessPartner: function (id) { + return Object.seal({ + "__metadata": { + "id": `https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner(${id})`, + "uri": `https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('${id}')`, + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" + }, + "BusinessPartner": id, + "Customer": "", + "Supplier": "", + "AcademicTitle": "", + "AuthorizationGroup": "", + "BusinessPartnerCategory": "", + "BusinessPartnerFullName": "", + "BusinessPartnerGrouping": "BP02", + "BusinessPartnerName": "", + "BusinessPartnerUUID": uuid(), + "CorrespondenceLanguage": "", + "CreatedByUser": "ANONYMOUS001", + "CreationDate": `/Date(${today()})/`, + "CreationTime": "PT17H49M05S", + "FirstName": "", + "FormOfAddress": "", + "Industry": "", + "InternationalLocationNumber1": "0", + "InternationalLocationNumber2": "0", + "IsFemale": false, + "IsMale": false, + "IsNaturalPerson": "", + "IsSexUnknown": true, + "Language": "", + "LastChangeDate": null, + "LastChangeTime": "PT00H00M00S", + "LastChangedByUser": "", + "LastName": "Doe", + "LegalForm": "", + "OrganizationBPName1": "", + "OrganizationBPName2": "", + "OrganizationBPName3": "", + "OrganizationBPName4": "", + "OrganizationFoundationDate": null, + "OrganizationLiquidationDate": null, + "SearchTerm1": "", + "AdditionalLastName": "", + "BirthDate": null, + "BusinessPartnerIsBlocked": false, + "BusinessPartnerType": "", + "ETag": "", + "GroupBusinessPartnerName1": "", + "GroupBusinessPartnerName2": "", + "IndependentAddressID": "", + "InternationalLocationNumber3": "0", + "MiddleName": "", + "NameCountry": "", + "NameFormat": "", + "PersonFullName": "", + "PersonNumber": "", + "IsMarkedForArchiving": false, + "BusinessPartnerIDByExtSystem": "", + "YY1_AddrLastCheckedOn_bus": null, + "YY1_AddrLastCheckedBy_bus": "", + "to_BuPaIdentification": { + "results": [] + }, + "to_BusinessPartnerAddress": { + "results": [] + }, + "to_BusinessPartnerBank": { + "results": [] + }, + "to_BusinessPartnerContact": { + "results": [] + }, + "to_BusinessPartnerRole": { + "results": [] + }, + "to_BusinessPartnerTax": { + "results": [] + }, + "to_Customer": null, + "to_Supplier": null + }); + }, + getBusinessPartners: function () { + return this.data; + }, + findBusinessPartner: function (id) { + return this.getBusinessPartners().find(function (element) { + return element.BusinessPartner == id; + }); + }, + createAndAddBusinessPartner: function(businessPartnerInput) { + const newId = nextBusinessPartnerId(this.getBusinessPartners()); + const newBusinessPartner = this.newBusinessPartner(newId); + Object.assign(newBusinessPartner, businessPartnerInput); + this.getBusinessPartners().push(newBusinessPartner); + return newBusinessPartner; + }, + deleteBusinessPartner: function(id) { + if(!this.findBusinessPartner(id)) { + throw new Error(`Cannot delete business partner: business partner with ID ${id} does not exist.`); + } + this.data = this.getBusinessPartners() + .filter( (bupa) => bupa.BusinessPartner != id ); + }, + modifyBusinessPartner: function(id, businessPartnerInput) { + const businessPartnerToUpdate = this.findBusinessPartner(id); + if(!businessPartnerToUpdate) { + throw new Error(`Cannot modify business partner: business partner with ID ${id} does not exist.`); + } + if(businessPartnerInput.BusinessPartner && businessPartnerInput.BusinessPartner != id) { + throw new Error(`Cannot modify business partner: identifier must not be changed`); + } + Object.assign(businessPartnerToUpdate, businessPartnerInput); + }, + + getAddresses: function() { + return this.getBusinessPartners() + .map( (bupa) => bupa.to_BusinessPartnerAddress.results) + .reduce( (acc,val) => acc.concat(val) , []); + }, + findAddress: function(businessPartnerId, addressId) { + return this.getAddresses().find(function(element) { + return element.BusinessPartner == businessPartnerId && + element.AddressID == addressId; + }); + }, + newAddress: function(businessPartnerId, addressId) { + const businessPartner = this.findBusinessPartner(businessPartnerId); + if(!businessPartner) { + throw new Error(`Cannot create address: associated business partner with ID ${businessPartnerId} does not exist.`); + } + return Object.seal({ + "__metadata": { + "id": `https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='${businessPartnerId}',AddressID='${addressId}')`, + "uri": `https://{host}:{port}/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress(BusinessPartner='${businessPartnerId}',AddressID='${addressId}')`, + "type": "API_BUSINESS_PARTNER.A_BusinessPartnerAddressType" + }, + "BusinessPartner": businessPartnerId, + "AddressID": addressId, + "ValidityStartDate": "/Date(1518393600000+0000)/", + "ValidityEndDate": "/Date(253402300799000+0000)/", + "AuthorizationGroup": "", + "AddressUUID": uuid(), + "AdditionalStreetPrefixName": "", + "AdditionalStreetSuffixName": "", + "AddressTimeZone": "CET", + "CareOfName": "", + "CityCode": "", + "CityName": "", + "CompanyPostalCode": "", + "Country": "", + "County": "", + "DeliveryServiceNumber": "", + "DeliveryServiceTypeCode": "", + "District": "", + "FormOfAddress": "", + "FullName": "", + "HomeCityName": "", + "HouseNumber": "", + "HouseNumberSupplementText": "", + "Language": "", + "POBox": "", + "POBoxDeviatingCityName": "", + "POBoxDeviatingCountry": "", + "POBoxDeviatingRegion": "", + "POBoxIsWithoutNumber": false, + "POBoxLobbyName": "", + "POBoxPostalCode": "", + "Person": businessPartner.PersonNumber, + "PostalCode": "", + "PrfrdCommMediumType": "", + "Region": "", + "StreetName": "", + "StreetPrefixName": "", + "StreetSuffixName": "", + "TaxJurisdiction": "", + "TransportZone": "", + "AddressIDByExternalSystem": "", + "to_AddressUsage": { + "results": [] + }, + "to_EmailAddress": { + "results": [] + }, + "to_FaxNumber": { + "results": [] + }, + "to_MobilePhoneNumber": { + "results": [] + }, + "to_PhoneNumber": { + "results": [] + }, + "to_URLAddress": { + "results": [] + } + }); + }, + createAndAddAddress: function(addressInput) { + const newAddressId = nextAddressId(this.getAddresses()); + const newAddress = this.newAddress(addressInput.BusinessPartner, newAddressId); + Object.assign(newAddress, addressInput); + this.findBusinessPartner(newAddress.BusinessPartner).to_BusinessPartnerAddress.results.push(newAddress); + return newAddress; + }, + deleteAddress: function(businessPartnerId, addressId) { + const businessPartner = this.findBusinessPartner(businessPartnerId); + if(!businessPartner || !this.findAddress(businessPartnerId, addressId)) { + throw new Error(`Cannot delete address: address with key (${businessPartnerId},${addressId}) does not exist.`); + } + businessPartner.to_BusinessPartnerAddress.results = businessPartner.to_BusinessPartnerAddress.results + .filter( (address) => address.AddressID != addressId); + }, + modifyAddress: function(businessPartnerId, addressId, addressInput) { + const addressToUpdate = this.findAddress(businessPartnerId, addressId); + if(!addressToUpdate) { + throw new Error(`Cannot modify address: address with key (${businessPartnerId},${addressId}) does not exist.`); + } + if((addressInput.BusinessPartner && addressInput.BusinessPartner != businessPartnerId) || + (addressInput.AddressID && addressInput.AddressID != addressId)) { + throw new Error(`Cannot modify address: key fields must not be changed`); + } + Object.assign(addressToUpdate, addressInput); + } +}; \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml new file mode 100644 index 00000000..10409e3f --- /dev/null +++ b/integration-tests/pom.xml @@ -0,0 +1,123 @@ + + + + 4.0.0 + + odata-mock-service - Integration Tests + odata-mock-service - Integration Tests + + com.sap.cloud.s4hana.book + odata-mock-service-integration-tests + 1.0-SNAPSHOT + + + 1.8 + + ${java.version} + ${java.version} + ${java.version} + ${java.version} + + UTF-8 + UTF-8 + UTF-8 + + false + 1 + * + + + + 1024m + 512m + warn + + ${project.build.directory}/coverage-reports/jacoco.exec + ${project.reporting.outputDirectory}/jacoco + + + + + + com.sap.cloud.s4hana + sdk-bom + 2.0.0 + pom + import + + + + + + + com.sap.cloud.s4hana.cloudplatform + scp-cf + test + + + com.sap.cloud.s4hana + s4hana-all + test + + + com.sap.cloud.s4hana + testutil + test + + + com.sap.cloud.s4hana.quality + listeners-all + test + + + + junit + junit + test + + + + io.rest-assured + rest-assured + test + + + + ch.qos.logback + logback-classic + 1.2.3 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.20 + + @{argLine} + -Xmx${surefire.maxMemorySize} + -XX:MaxPermSize=${surefire.maxPermSize} + -Dorg.slf4j.simpleLogger.defaultLogLevel=${surefire.defaultLogLevel} + -Djava.io.tmpdir=target/arquillian-working-dir/${surefire.forkNumber}/ + + ${surefire.skipTests} + + ${surefire.include} + + + ${surefire.exclude} + + ${surefire.forkCount} + false + ${surefire.groups} + ${surefire.excludedGroups} + + + + + diff --git a/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerAddressTest.java b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerAddressTest.java new file mode 100644 index 00000000..ec3e89d7 --- /dev/null +++ b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerAddressTest.java @@ -0,0 +1,155 @@ +package com.sap.cloud.s4hana.book.odatatestservice; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import com.sap.cloud.sdk.odatav2.connectivity.ODataDeleteResult; +import com.sap.cloud.sdk.odatav2.connectivity.ODataException; +import com.sap.cloud.sdk.odatav2.connectivity.ODataUpdateResult; +import com.sap.cloud.sdk.testutil.MockUtil; + +import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartnerAddress; +import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.*; + +public class ModifyBusinessPartnerAddressTest { + private static final String BUPA_ID = "1003764"; + + private MockUtil mockUtil; + + @Before + public void setup() { + mockUtil = new MockUtil(); + mockUtil.mockDefaults(); + mockUtil.mockErpDestination(); + } + + @Test + public void testCreate() throws ODataException { + final String streetName = RandomStringUtils.randomAlphabetic(15); + final BusinessPartnerAddress addressCreated = createBusinessPartnerAddress(streetName); + + final BusinessPartnerAddress addressRetrieved = new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(addressCreated.getBusinessPartner(), addressCreated.getAddressID()) + .execute(); + + assertThat(addressRetrieved, notNullValue()); + assertThat(addressRetrieved.getStreetName(), allOf(not(isEmptyOrNullString()), equalTo(streetName))); + } + + private BusinessPartnerAddress createBusinessPartnerAddress(final String streetName) throws ODataException { + final BusinessPartnerAddress addressToCreate = BusinessPartnerAddress.builder() + .businessPartner(BUPA_ID) + .streetName(streetName) + .cityName("Potsdam") + .build(); + return new DefaultBusinessPartnerService() + .createBusinessPartnerAddress(addressToCreate) + .execute(); + } + + @Test(expected = ODataException.class) + public void testCreateForInvalidBusinessPartner() throws ODataException { + final BusinessPartnerAddress addressToCreate = BusinessPartnerAddress.builder() + .businessPartner("1") + .streetName("Konrad-Zuse-Ring") + .cityName("Potsdam") + .build(); + new DefaultBusinessPartnerService().createBusinessPartnerAddress(addressToCreate).execute(); + } + + @Test + public void testDelete() throws ODataException { + final BusinessPartnerAddress freshAddress = createBusinessPartnerAddress("Delete Me"); + final BusinessPartnerAddress addressToDelete = BusinessPartnerAddress.builder() + .businessPartner(freshAddress.getBusinessPartner()) + .addressID(freshAddress.getAddressID()) + .build(); + + final ODataDeleteResult deleteResult = new DefaultBusinessPartnerService() + .deleteBusinessPartnerAddress(addressToDelete) + .execute(); + + assertThat(deleteResult.getHttpStatusCode(), is(204)); + + try { + new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(freshAddress.getBusinessPartner(), freshAddress.getAddressID()) + .execute(); + fail("Deleted address can still be retrieved."); + } catch (final ODataException e) { + assertThat(e.getCode(), is("404")); + } + } + + @Test(expected = ODataException.class) + public void testDeleteNonExisting() throws ODataException { + new DefaultBusinessPartnerService() + .deleteBusinessPartnerAddress(BusinessPartnerAddress.builder() + .businessPartner(BUPA_ID) + .addressID("1") + .build()) + .execute(); + } + + @Test + public void testUpdate() throws ODataException { + final BusinessPartnerAddress freshAddress = createBusinessPartnerAddress("Update Me"); + final String updatedStreetName = "Updated Street Name"; + final BusinessPartnerAddress addressToUpdate = BusinessPartnerAddress.builder() + .businessPartner(freshAddress.getBusinessPartner()) + .addressID(freshAddress.getAddressID()) + .build(); + addressToUpdate.setStreetName(updatedStreetName); + final ODataUpdateResult updateResult = new DefaultBusinessPartnerService() + .updateBusinessPartnerAddress(addressToUpdate) + .execute(); + assertThat(updateResult.getHttpStatusCode(), is(204)); + + final BusinessPartnerAddress addressUpdated = new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(freshAddress.getBusinessPartner(), freshAddress.getAddressID()) + .execute(); + + final String addressUpdatedStreetName = addressUpdated.getStreetName(); + assertThat(addressUpdatedStreetName, not(isEmptyOrNullString())); + assertThat(addressUpdatedStreetName, equalTo(updatedStreetName)); + } + + @Test + public void testUpdateWithWorkaround() throws ODataException { + final BusinessPartnerAddress freshAddress = createBusinessPartnerAddress("Update Me"); + final String updatedStreetName = "Updated Street Name"; + freshAddress.setStreetName(updatedStreetName); + final ODataUpdateResult updateResult = new DefaultBusinessPartnerService() + .updateBusinessPartnerAddress(freshAddress) + .execute(); + assertThat(updateResult.getHttpStatusCode(), is(204)); + + final BusinessPartnerAddress addressUpdated = new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(freshAddress.getBusinessPartner(), freshAddress.getAddressID()) + .execute(); + + final String addressUpdatedStreetName = addressUpdated.getStreetName(); + assertThat(addressUpdatedStreetName, not(isEmptyOrNullString())); + assertThat(addressUpdatedStreetName, equalTo(updatedStreetName)); + } + + @Test(expected = ODataException.class) + public void testUpdateNonExisting() throws ODataException { + new DefaultBusinessPartnerService() + .updateBusinessPartnerAddress(BusinessPartnerAddress.builder() + .businessPartner(BUPA_ID) + .addressID("1") + .build()) + .execute(); + } +} diff --git a/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerTest.java b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerTest.java new file mode 100644 index 00000000..da32b9da --- /dev/null +++ b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ModifyBusinessPartnerTest.java @@ -0,0 +1,105 @@ +package com.sap.cloud.s4hana.book.odatatestservice; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import com.sap.cloud.sdk.odatav2.connectivity.ODataException; +import com.sap.cloud.sdk.odatav2.connectivity.ODataUpdateResult; +import com.sap.cloud.sdk.testutil.MockUtil; + +import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartner; +import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class ModifyBusinessPartnerTest { + private MockUtil mockUtil; + + @Before + public void setup() { + mockUtil = new MockUtil(); + mockUtil.mockDefaults(); + mockUtil.mockErpDestination(); + } + + @Test + public void testCreate() throws ODataException { + final String firstName = RandomStringUtils.randomAlphabetic(10); + final BusinessPartner businessPartnerCreated = createBusinessPartner(firstName); + + final BusinessPartner businessPartnerRetrieved = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(businessPartnerCreated.getBusinessPartner()) + .execute(); + + assertThat(businessPartnerRetrieved, notNullValue()); + assertThat(businessPartnerRetrieved.getFirstName(), allOf(not(isEmptyOrNullString()), equalTo(firstName))); + } + + private BusinessPartner createBusinessPartner(final String firstName) throws ODataException { + final BusinessPartner businessPartnerToCreate = BusinessPartner.builder() + .firstName(firstName) + .lastName("Doe") + .businessPartnerCategory("1") + .isFemale(true) + .build(); + return new DefaultBusinessPartnerService() + .createBusinessPartner(businessPartnerToCreate) + .execute(); + } + + @Test + public void testUpdate() throws ODataException { + final BusinessPartner freshBusinessPartner = createBusinessPartner("Update Me"); + final String updatedFirstName = "Updated First Name"; + final BusinessPartner businessPartnerToUpdate = BusinessPartner.builder() + .businessPartner(freshBusinessPartner.getBusinessPartner()) + .build(); + businessPartnerToUpdate.setFirstName(updatedFirstName); + final ODataUpdateResult updateResult = new DefaultBusinessPartnerService() + .updateBusinessPartner(businessPartnerToUpdate) + .execute(); + assertThat(updateResult.getHttpStatusCode(), is(204)); + + final BusinessPartner businessPartnerUpdated = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(freshBusinessPartner.getBusinessPartner()) + .execute(); + + final String businessPartnerUpdatedFirstName = businessPartnerUpdated.getFirstName(); + assertThat(businessPartnerUpdatedFirstName, not(isEmptyOrNullString())); + assertThat(businessPartnerUpdatedFirstName, equalTo(updatedFirstName)); + } + + @Test + public void testUpdateWithWorkaround() throws ODataException { + final BusinessPartner freshBusinessPartner = createBusinessPartner("Update Me"); + final String updatedFirstName = "Updated First Name"; + + // Reuse complete entity to work around bug in SAP S/4HANA Cloud SDK + freshBusinessPartner.setFirstName(updatedFirstName); + final ODataUpdateResult updateResult = new DefaultBusinessPartnerService() + .updateBusinessPartner(freshBusinessPartner) + .execute(); + assertThat(updateResult.getHttpStatusCode(), is(204)); + + final BusinessPartner businessPartnerUpdated = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(freshBusinessPartner.getBusinessPartner()) + .execute(); + + final String businessPartnerUpdatedFirstName = businessPartnerUpdated.getFirstName(); + assertThat(businessPartnerUpdatedFirstName, not(isEmptyOrNullString())); + assertThat(businessPartnerUpdatedFirstName, equalTo(updatedFirstName)); + } + + @Test(expected = ODataException.class) + public void testUpdateNonExisting() throws ODataException { + new DefaultBusinessPartnerService() + .updateBusinessPartner(BusinessPartner.builder() + .businessPartner("1") + .firstName("Test") + .build()) + .execute(); + } +} diff --git a/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerAddressTest.java b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerAddressTest.java new file mode 100644 index 00000000..ee88a1c6 --- /dev/null +++ b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerAddressTest.java @@ -0,0 +1,140 @@ +package com.sap.cloud.s4hana.book.odatatestservice; + +import org.hamcrest.CustomTypeSafeMatcher; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import com.sap.cloud.sdk.odatav2.connectivity.ODataException; +import com.sap.cloud.sdk.testutil.MockUtil; + +import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartnerAddress; +import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.*; + +public class ReadBusinessPartnerAddressTest { + private static final String BUPA_ID = "1003764"; + private static final String ADDRESS_ID = "28238"; + + private MockUtil mockUtil; + + @Before + public void setup() { + mockUtil = new MockUtil(); + mockUtil.mockDefaults(); + mockUtil.mockErpDestination(); + } + + @Test + public void testGetAllPlain() throws ODataException { + final List addresses = new DefaultBusinessPartnerService() + .getAllBusinessPartnerAddress() + .execute(); + + verifyListMatchesSize(addresses, greaterThan(1)); + + assertForAll_BupaNonNull_IdNonNull(addresses); + } + + private void assertForAll_BupaNonNull_IdNonNull(final List addresses) { + for(final BusinessPartnerAddress address : addresses) { + assertThat(address.getBusinessPartner(), not(isEmptyOrNullString())); + assertThat(address.getAddressID(), not(isEmptyOrNullString())); + } + } + + private void verifyListMatchesSize(List businessPartners, Matcher sizeMatcher) { + assertThat(businessPartners, allOf(notNullValue(), not(empty()))); + assertThat(businessPartners, hasSize(sizeMatcher)); + } + + private int sizeOfUnfilteredResult() throws ODataException { + return new DefaultBusinessPartnerService() + .getAllBusinessPartnerAddress() + .execute().size(); + } + + @Test + public void testGetAllWithSelect() throws ODataException { + final List addresses = new DefaultBusinessPartnerService() + .getAllBusinessPartnerAddress() + .select(BusinessPartnerAddress.STREET_NAME, BusinessPartnerAddress.CITY_NAME) + .execute(); + + verifyListMatchesSize(addresses, greaterThan(1)); + + assertForAll_IdNull_StreetNonNull_CityNonNull(addresses); + } + + private void assertForAll_IdNull_StreetNonNull_CityNonNull(List addresses) { + for(final BusinessPartnerAddress address : addresses) { + assert_IdNull_StreetNonNull_CityNonNull(address); + } + } + + private void assert_IdNull_StreetNonNull_CityNonNull(BusinessPartnerAddress address) { + assertThat(address.getBusinessPartner(), nullValue()); + assertThat(address.getAddressID(), nullValue()); + assertThat(address.getStreetName(), not(isEmptyOrNullString())); + assertThat(address.getCityName(), not(isEmptyOrNullString())); + } + + @Test + public void testGetAllWithFilter() throws ODataException { + final List addresses = new DefaultBusinessPartnerService() + .getAllBusinessPartnerAddress() + .filter(BusinessPartnerAddress.BUSINESS_PARTNER.eq(BUPA_ID)) + .execute(); + + verifyListMatchesSize(addresses, lessThan(sizeOfUnfilteredResult())); + + assertForAll_BupaIdAsExpected(addresses, BUPA_ID); + } + + private void assertForAll_BupaIdAsExpected(List addresses, final String expectedBupaId) { + assertThat(addresses, everyItem(new CustomTypeSafeMatcher("an address for business partner "+ expectedBupaId) { + @Override + protected boolean matchesSafely(BusinessPartnerAddress item) { + final String firstName = item.getBusinessPartner(); + return firstName != null && firstName.equals(expectedBupaId); + } + })); + } + + @Test + public void testGetByKeyPlain() throws ODataException { + final BusinessPartnerAddress address = new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(BUPA_ID, ADDRESS_ID) + .execute(); + + assertThat(address, notNullValue()); + assertThat(address.getBusinessPartner(), equalTo(BUPA_ID)); + assertThat(address.getAddressID(), equalTo(ADDRESS_ID)); + assertThat(address.getCityName(), not(isEmptyOrNullString())); + } + + @Test + public void testGetByKeyWithSelect() throws ODataException { + final BusinessPartnerAddress address = new DefaultBusinessPartnerService() + .getBusinessPartnerAddressByKey(BUPA_ID, ADDRESS_ID) + .select(BusinessPartnerAddress.CITY_NAME, BusinessPartnerAddress.STREET_NAME) + .execute(); + + assertThat(address, notNullValue()); + assert_IdNull_StreetNonNull_CityNonNull(address); + } +} diff --git a/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerTest.java b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerTest.java new file mode 100644 index 00000000..ea23ff31 --- /dev/null +++ b/integration-tests/src/test/java/com/sap/cloud/s4hana/book/odatatestservice/ReadBusinessPartnerTest.java @@ -0,0 +1,273 @@ +package com.sap.cloud.s4hana.book.odatatestservice; + +import com.sap.cloud.sdk.odatav2.connectivity.ODataCreateRequestBuilder; +import com.sap.cloud.sdk.odatav2.connectivity.ODataException; + +import com.sap.cloud.sdk.s4hana.datamodel.odata.helper.Order; +import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartner; +import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartnerAddress; +import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService; + +import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder; +import com.sap.cloud.sdk.testutil.MockUtil; + +import org.hamcrest.CustomTypeSafeMatcher; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +public class ReadBusinessPartnerTest { + private static final String BUPA_ID = "1003764"; + private static final String EXPECTED_FIRST_NAME = "John"; + + private MockUtil mockUtil; + + @Before + public void setup() { + mockUtil = new MockUtil(); + mockUtil.mockDefaults(); + mockUtil.mockErpDestination(); + } + + @Test + public void testGetAllPlain() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + assertForAll_IdNonNull_CategoryNonNull(businessPartners); + } + + private void assertForAll_IdNonNull_CategoryNonNull(final List businessPartners) { + for (final BusinessPartner businessPartner : businessPartners) { + assertThat(businessPartner.getBusinessPartner(), not(isEmptyOrNullString())); + assertThat(businessPartner.getBusinessPartnerCategory(), not(isEmptyOrNullString())); + } + } + + private void verifyListMatchesSize(List businessPartners, Matcher sizeMatcher) { + assertThat(businessPartners, allOf(notNullValue(), not(empty()))); + assertThat(businessPartners, hasSize(sizeMatcher)); + } + + private int sizeOfUnfilteredResult() throws ODataException { + return new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .execute().size(); + } + + @Test + public void testGetAllWithSelect() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.FIRST_NAME, BusinessPartner.CREATED_BY_USER) + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + assertForAll_IdNull_FirstNameNonNull_CreatedByNonNull(businessPartners); + } + + @Test + public void testGetAllWithSelectAllFields() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.ALL_FIELDS) + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + assertForAll_IdNonNull_CategoryNonNull(businessPartners); + } + + private void assertForAll_IdNull_FirstNameNonNull_CreatedByNonNull(List businessPartners) { + for (final BusinessPartner businessPartner : businessPartners) { + assert_IdNull_FirstNameNonNull_CreatedByNonNull(businessPartner); + } + } + + private void assert_IdNull_FirstNameNonNull_CreatedByNonNull(BusinessPartner businessPartner) { + assertThat(businessPartner.getBusinessPartner(), nullValue()); + assertThat(businessPartner.getFirstName(), not(isEmptyOrNullString())); + assertThat(businessPartner.getCreatedByUser(), not(isEmptyOrNullString())); + } + + @Test + public void testGetAllWithFilter() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .filter(BusinessPartner.FIRST_NAME.eq(EXPECTED_FIRST_NAME)) + .execute(); + + verifyListMatchesSize(businessPartners, lessThan(sizeOfUnfilteredResult())); + + assertForAll_FirstNameAsExpected(businessPartners, EXPECTED_FIRST_NAME); + } + + private void assertForAll_FirstNameAsExpected(List businessPartners, final String expectedFirstName) { + assertThat(businessPartners, everyItem(new CustomTypeSafeMatcher("a business partner with first name " + expectedFirstName) { + @Override + protected boolean matchesSafely(BusinessPartner item) { + final String firstName = item.getFirstName(); + return firstName != null && firstName.equals(expectedFirstName); + } + })); + } + + @Test + public void testGetAllWithSelectAndFilter() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.FIRST_NAME, BusinessPartner.CREATED_BY_USER) + .filter(BusinessPartner.FIRST_NAME.eq(EXPECTED_FIRST_NAME)) + .execute(); + + verifyListMatchesSize(businessPartners, lessThan(sizeOfUnfilteredResult())); + + assertForAll_IdNull_FirstNameNonNull_CreatedByNonNull(businessPartners); + assertForAll_FirstNameAsExpected(businessPartners, EXPECTED_FIRST_NAME); + } + + @Test + public void testGetAllWithSelectAndFilterNotPartOfSelect() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.BUSINESS_PARTNER_CATEGORY, BusinessPartner.CREATED_BY_USER) + .filter(BusinessPartner.FIRST_NAME.eq(EXPECTED_FIRST_NAME)) + .execute(); + + verifyListMatchesSize(businessPartners, lessThan(sizeOfUnfilteredResult())); + } + + @Test + public void testGetAllWithExpand() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.BUSINESS_PARTNER, BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS) + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + for (final BusinessPartner businessPartner : businessPartners) { + assert_IdNonNull_ToAddressNonNull(businessPartner); + } + } + + @Test + public void testGetAllWithExpandMultiple() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.BUSINESS_PARTNER, + BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS.select(BusinessPartnerAddress.ADDRESS_ID, + BusinessPartnerAddress.TO_EMAIL_ADDRESS), + BusinessPartner.TO_BUSINESS_PARTNER_ROLE) + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + for (final BusinessPartner businessPartner : businessPartners) { + assert_IdNonNull_ToAddressNonNull(businessPartner); + assertThat(businessPartner.getBusinessPartnerRoleIfPresent().orElse(null), notNullValue()); + assertThat(businessPartner.getBusinessPartnerContactIfPresent().orElse(null), nullValue()); + for (final BusinessPartnerAddress address : businessPartner.getBusinessPartnerAddressIfPresent().orElse(Collections.emptyList())) { + assertThat(address.getAddressID(), not(isEmptyOrNullString())); + assertThat(address.getEmailAddressIfPresent().orElse(null), notNullValue()); + } + } + } + + @Test + public void testGetAllWithExpandSpecificFields() throws ODataException { + final List businessPartners = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.BUSINESS_PARTNER, BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS + .select(BusinessPartnerAddress.ADDRESS_ID, BusinessPartnerAddress.STREET_NAME)) + .execute(); + + verifyListMatchesSize(businessPartners, greaterThan(1)); + + for (final BusinessPartner businessPartner : businessPartners) { + assert_IdNonNull_ToAddressNonNull(businessPartner); + for (final BusinessPartnerAddress address : businessPartner.getBusinessPartnerAddressIfPresent().orElse(Collections.emptyList())) { + assertThat(address.getAddressID(), not(isEmptyOrNullString())); + assertThat(address.getStreetName(), not(isEmptyOrNullString())); + assertThat(address.getHouseNumber(), isEmptyOrNullString()); + } + } + } + + private void assert_IdNonNull_ToAddressNonNull(final BusinessPartner businessPartner) { + assertThat(businessPartner.getBusinessPartner(), not(isEmptyOrNullString())); + assertThat(businessPartner.getBusinessPartnerAddressIfPresent().orElse(null), notNullValue()); + } + + @Test + public void testGetAllWithOrderByAsc() throws ODataException { + List listAsc = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.FIRST_NAME) + .orderBy(BusinessPartner.FIRST_NAME, Order.ASC) + .execute(); + + for(int i = 1; i < listAsc.size(); i++) { + assertTrue("List is not sorted: " + listAsc.toString(), + listAsc.get(i-1).getFirstName().compareTo(listAsc.get(i).getFirstName()) <= 0); + } + } + + @Test + public void testGetAllWithOrderByDesc() throws ODataException { + List listAsc = new DefaultBusinessPartnerService() + .getAllBusinessPartner() + .select(BusinessPartner.FIRST_NAME) + .orderBy(BusinessPartner.FIRST_NAME, Order.DESC) + .execute(); + + for(int i = 1; i < listAsc.size(); i++) { + assertTrue("List is not sorted: " + listAsc.toString(), + listAsc.get(i-1).getFirstName().compareTo(listAsc.get(i).getFirstName()) >= 0); + } + } + + @Test + public void testGetByKeyPlain() throws ODataException { + final BusinessPartner businessPartner = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(BUPA_ID) + .execute(); + + assertThat(businessPartner, notNullValue()); + assertThat(businessPartner.getBusinessPartner(), equalTo(BUPA_ID)); + assertThat(businessPartner.getBusinessPartnerCategory(), not(isEmptyOrNullString())); + } + + @Test + public void testGetByKeyWithSelect() throws ODataException { + final BusinessPartner businessPartner = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(BUPA_ID) + .select(BusinessPartner.FIRST_NAME, BusinessPartner.CREATED_BY_USER) + .execute(); + + assertThat(businessPartner, notNullValue()); + assert_IdNull_FirstNameNonNull_CreatedByNonNull(businessPartner); + } + + @Test + public void testGetByKeyWithExpand() throws ODataException { + final BusinessPartner businessPartner = new DefaultBusinessPartnerService() + .getBusinessPartnerByKey(BUPA_ID) + .select(BusinessPartner.BUSINESS_PARTNER, BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS) + .execute(); + + assertThat(businessPartner, notNullValue()); + assert_IdNonNull_ToAddressNonNull(businessPartner); + } +} diff --git a/integration-tests/src/test/resources/credentials.yml b/integration-tests/src/test/resources/credentials.yml new file mode 100644 index 00000000..f4df4607 --- /dev/null +++ b/integration-tests/src/test/resources/credentials.yml @@ -0,0 +1,5 @@ +--- +credentials: +- alias: "MOCKED_ERP_SYSTEM" + username: "DUMMY" + password: "DUMMY" \ No newline at end of file diff --git a/integration-tests/src/test/resources/systems.yml b/integration-tests/src/test/resources/systems.yml new file mode 100644 index 00000000..c81037da --- /dev/null +++ b/integration-tests/src/test/resources/systems.yml @@ -0,0 +1,7 @@ +--- +erp: + default: "MOCKED_ERP_SYSTEM" + systems: + - alias: "MOCKED_ERP_SYSTEM" + uri: "http://localhost:3000" + diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 00000000..a1de8bec --- /dev/null +++ b/manifest.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: bupa-mock-odata + memory: 64M + buildpack: nodejs_buildpack + random-route: true diff --git a/odata-helpers.js b/odata-helpers.js new file mode 100644 index 00000000..5264698f --- /dev/null +++ b/odata-helpers.js @@ -0,0 +1,220 @@ +const bodyParser = require('body-parser'); + +/** Returns true if the property name refers to a navigation property (i.e., begins with "to_") */ +const isNavigationProperty = function(propertyName) { + return propertyName.startsWith('to_'); +}; + +/** + * Removes the first segment of the navigation property path + * @param {string} propertyName Navigation property path + */ +const removeFirstNavigationPath = function(propertyName) { + return propertyName.substr(propertyName.split('/', 1)[0].length + 1); +}; + +/** + * Deals with navigation properties of the supplied entity per the expand specification. + * Navigation properties that are part of the expanded properties are included in the result object. + * The value of navigation properties that shall not be expanded is set to an object { "__deferred": ... }. + * Non-navigation properties are left untouched. + * + * The transformation is applied recursively. + * @param {*} entity Entity to process + * @param {string[]} expandedProperties Navigation properties to include as expanded properties. + */ +const handleEntityNavPropertiesForExpand = function(entity, expandedProperties = []) { + return Object.entries(entity).reduce(function(result, [key, value]) { + if(isNavigationProperty(key)) { + if(expandedProperties.includes(key) || + expandedProperties.find( (property) => property.startsWith(key))) { + // TODO: handle single-valued nav properties + if(value && value.results && value.results.length > 0) { + const associatedEntities = value.results; + // Recursively handle expansion of navigation properties + const expandedPropertiesForNavProperty = expandedProperties + .filter( (property) => property.startsWith(key + '/') ) + .map(removeFirstNavigationPath); + // Construct a new object, do not modify value + result[key] = { + results: handleEntitySetNavPropertiesForExpand(value.results, expandedPropertiesForNavProperty) + }; + } else { + result[key] = value; + } + } else { + result[key] = { + "__deferred": { + "uri": `${entity.__metadata.uri}/${key}` + } + }; + } + } else { + result[key] = value; + } + + return result; + }, {}); +}; + +/** + * Deals with navigation properties of the supplied entity set per the expand specification. + * @see handleEntityNavPropertiesForExpand + */ +const handleEntitySetNavPropertiesForExpand = function(entityArray, expandedProperties) { + return entityArray.map( (item) => handleEntityNavPropertiesForExpand(item, expandedProperties) ); +} + +/** + * Reduces the entity to only contain the selected properties (and meta properties). + * @param {Object} entity Entity to reduce + * @param {string[]} selectedProperties Properties to keep, or all, if empty + */ +const reduceEntityToSelect = function(entity, selectedProperties = []) { + if(0 === selectedProperties.length) { + return entity; + } + return Object.entries(entity).reduce(function(result, [key, value]) { + const isNavProperty = isNavigationProperty(key); + if('__metadata' === key || selectedProperties.includes(key) || + (selectedProperties.includes('*')) && !isNavProperty) { + result[key] = value; + } + else if(isNavProperty) { + const selectedPropertiesForNavProperty = selectedProperties + .filter( (item) => item.startsWith(key + '/') ) + .map( removeFirstNavigationPath ); + if(selectedPropertiesForNavProperty.length > 0 && value) { + // TODO: handle single-valued nav properties + const associatedEntities = value.results; + // Construct a new object, do not modify value + result[key] = { results: reduceEntitySetToSelect(associatedEntities, selectedPropertiesForNavProperty)}; + } + } + return result; + }, {}); +}; + +/** + * Reduces all entities of the given array to only contain the selected properties (and meta properties) + * @param {Object[]} entityArray Array of entities to reduce + * @param {string[]} selectedProperties Properties to keep, or all, if empty + */ +const reduceEntitySetToSelect = function(entityArray, selectedProperties) { + return entityArray.map( (item) => reduceEntityToSelect(item, selectedProperties) ); +}; + +const insertHostIntoBody = function(body, req) { + const urlPrefix = `${req.protocol}://${req.get('host')}`; + return body.replace(/https:\/\/{host}:{port}/g, urlPrefix); +}; + +module.exports = { + /** Send the result as an OData response */ + sendAsODataResult: function(req, res, next) { + const result = res.result; + const arrayWrapped = Array.isArray(result) ? { results: result } : result; + const bodyAsString = JSON.stringify({ d: arrayWrapped }); + const bodyWithHost = insertHostIntoBody(bodyAsString, req); + + res.set('Content-Type', 'application/json'); + res.send(bodyWithHost); + }, + + /** Send 404 response if result is undefined */ + send404IfNotFound: function (req, res, next) { + if(res.result) { + next(); + } else { + console.log("No result, responding with 404") + res.sendStatus(404); + } + }, + + /** Send 204 response for no content */ + send204NoContent: function(req, res, next) { + res.sendStatus(204); + }, + + /** Expand each result entity (and replace not queried content with __deferred) */ + expand: function(req, res, next) { + const expandQuery = req.query.$expand; + const propertiesToExpand = expandQuery? expandQuery.split(',') : []; + + res.result = Array.isArray(res.result)? + handleEntitySetNavPropertiesForExpand(res.result, propertiesToExpand) : + handleEntityNavPropertiesForExpand(res.result, propertiesToExpand); + + next(); + }, + + /** Filter the result set per the $filter query option */ + filter: function(req, res, next) { + const filterQuery = req.query.$filter; + + if(filterQuery) { + // RegExp that matches filters such as "FirstName eq 'John'" and groups property and value + const filterRegex = /^(\w+) eq '(.*)'$/; + const [, filterProperty, filterValue] = filterRegex.exec(filterQuery); + + res.result = res.result.filter((item) => { + return item[filterProperty] == filterValue; + }); + } + + next(); + }, + + /** Sort the result set per the $orderby query option (not yet implemented) */ + sort: function(req, res, next) { + const orderbyQuery = req.query.$orderby; + + if(orderbyQuery) { + const [sortByProperty,sortOrder] = orderbyQuery.split(' '); + res.result.sort(function(a, b) { + const valueA = a[sortByProperty]; + const valueB = b[sortByProperty]; + if(undefined === valueA || undefined === valueB) { + console.warn('Invalid property for sorting: ', sortByProperty); + return 0; + } + const compareResult = valueA < valueB ? -1 : (valueA == valueB ? 0: 1); + // if no sort order has been specified, sort ascending + return (sortOrder == 'desc' ? -1 : 1) * compareResult; + }); + } + next(); + }, + + /** Select only the properties of each result entity specified by the $select query option */ + select: function(req, res, next) { + const selectQuery = req.query.$select; + + if(selectQuery) { + const selectedProperties = selectQuery.split(','); + res.result = Array.isArray(res.result) ? + reduceEntitySetToSelect(res.result, selectedProperties) : + reduceEntityToSelect(res.result, selectedProperties); + } + + next(); + }, + + /** Limits the result set per the $top and $skip query options (not yet implemented) */ + limit: function(req, res, next) { + next(); + }, + + /** All generic middlewares for entity sets */ + middlewareForSet: function() { + return [this.expand, this.filter, this.sort, this.select, this.limit, this.sendAsODataResult]; + }, + /** All generic middlewares for single entities */ + middlewareForEntity: function() { + return [this.send404IfNotFound, this.expand, this.select, this.sendAsODataResult]; + }, + /** Create middleware chain for update based on retrieve and modify function */ + middlewareForUpdate: function(retrieveFunction, modifyFunction) { + return [retrieveFunction, this.send404IfNotFound, bodyParser.json(), modifyFunction, this.send204NoContent]; + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..6c18c2c3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,362 @@ +{ + "name": "bupa-mock-odata", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", + "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.15" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + } + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.5.tgz", + "integrity": "sha1-ZwI1ypWYiQpa6BcLg9tyK4Qu2Sc=", + "requires": { + "accepts": "1.3.4", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.0.6", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "1.1.5", + "qs": "6.5.0", + "range-parser": "1.2.0", + "send": "0.15.6", + "serve-static": "1.12.6", + "setprototypeof": "1.0.3", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.0", + "vary": "1.1.2" + } + }, + "finalhandler": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.6.tgz", + "integrity": "sha1-AHrqM9Gk0+QgF/YkhIrVjSEvgU8=", + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz", + "integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz", + "integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.4.0" + } + }, + "qs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz", + "integrity": "sha1-jQSVTTZN7z78VbWgeT4eLIsebkk=" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "send": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.6.tgz", + "integrity": "sha1-IPI6nJJbdiq4JwX+L52yUqzkfjQ=", + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + } + }, + "serve-static": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.6.tgz", + "integrity": "sha1-uXN3P2NEmTTaVOW+ul4x2fQhFXc=", + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.15.6" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..324a7270 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "bupa-mock-odata", + "version": "1.0.0", + "description": "OData mock service for Business Partner API of SAP S/4HANA Cloud", + "main": "app.js", + "scripts": { + "start": "node app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "SAP", + "license": "Apache-2.0", + "dependencies": { + "body-parser": "^1.18.2", + "express": "^4.15.5", + "uuid": "^3.2.1" + }, + "engines": { + "node": "^8.9.4" + } +}