change folders structure
This commit is contained in:
3
chinook/.gitignore
vendored
3
chinook/.gitignore
vendored
@@ -7,9 +7,10 @@ gen/
|
|||||||
node_modules/
|
node_modules/
|
||||||
target/
|
target/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
app/build
|
||||||
|
|
||||||
# html5Deployer
|
# html5Deployer
|
||||||
deployers/html5Deployer/resources/app/
|
app/deployers/html5Deployer/resources/
|
||||||
|
|
||||||
# Web IDE, App Studio
|
# Web IDE, App Studio
|
||||||
.che/
|
.che/
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ Welcome to your new project.
|
|||||||
It contains these folders and files, following our recommended project layout:
|
It contains these folders and files, following our recommended project layout:
|
||||||
|
|
||||||
| File or Folder | Purpose |
|
| File or Folder | Purpose |
|
||||||
|------------------|--------------------------------------|
|
| ---------------- | ------------------------------------ |
|
||||||
| `app/` | will contain compiled front bundles |
|
| `app/` | will contain compiled front bundles |
|
||||||
| `app/react/` | contains frontend app on react |
|
| `app/front/` | contains frontend app on react |
|
||||||
| `app/deployers/` | contains deployment staff |
|
| `app/deployers/` | contains deployment staff |
|
||||||
| `db/` | your domain models and data go here |
|
| `db/` | your domain models and data go here |
|
||||||
| `srv/` | your service models and code go here |
|
| `srv/` | your service models and code go here |
|
||||||
@@ -31,7 +31,7 @@ npm run deploy
|
|||||||
cds watch
|
cds watch
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open `app/react` folder and run next commands. This will install dependencies and run frontend src files watcher. When you will change src files your bundles in app directory will re-compiled. Now you can enjoy development:
|
- Open `app/front` folder and run next commands. This will install dependencies and run frontend src files watcher. When you will change src files your bundles in app directory will re-compiled. Now you can enjoy development:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
npm install
|
npm install
|
||||||
@@ -44,6 +44,22 @@ npm run watch
|
|||||||
> npm run start
|
> npm run start
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
- Change package.json db section
|
||||||
|
|
||||||
|
```json
|
||||||
|
"db": {
|
||||||
|
"kind": "sql"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run tests
|
||||||
|
|
||||||
|
```json
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
- Make sure you already have hanatrial instance in your cockpit dashboard (SAP Cloud Platform).
|
- Make sure you already have hanatrial instance in your cockpit dashboard (SAP Cloud Platform).
|
||||||
@@ -56,17 +72,17 @@ npm run watch
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Authenticate in the Cloud Foundry:
|
- Authenticate to the Cloud Foundry:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
cf login
|
cf login
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open `app/react` folder and run the following commands. This will create frontend production bundles in app subfolder:
|
- Open `app/front` folder and run the following commands. This will create frontend production bundles in app subfolder:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
- Clean up app/deployers/html5Deployer/resources folder from the previous frontend build
|
- Clean up app/deployers/html5Deployer/resources folder from the previous frontend build
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
"lint": "./node_modules/.bin/eslint"
|
"lint": "./node_modules/.bin/eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "4.3.0",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
"@umijs/hooks": "^1.9.3",
|
"@umijs/hooks": "^1.9.3",
|
||||||
"antd": "^4.8.2",
|
"antd": "^4.8.2",
|
||||||
"@ant-design/icons": "4.3.0",
|
|
||||||
"axios": "^0.20.0",
|
"axios": "^0.20.0",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^6.3.2",
|
"copy-webpack-plugin": "^6.3.2",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-dev-utils": "^11.0.1",
|
"react-dev-utils": "^11.0.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"terser-webpack-plugin": "^5.0.3",
|
"terser-webpack-plugin": "^5.0.3",
|
||||||
"webpack": "5.8.0",
|
"webpack": "5.8.0",
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -19,6 +19,10 @@ const DEBOUNCE_OPTIONS = {
|
|||||||
trailing: false,
|
trailing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isEven = (value) => {
|
||||||
|
return value % 2 === 0;
|
||||||
|
};
|
||||||
|
|
||||||
const renderGenres = (genres) =>
|
const renderGenres = (genres) =>
|
||||||
genres.map(({ ID, name }) => (
|
genres.map(({ ID, name }) => (
|
||||||
<Option key={ID} value={ID.toString()}>
|
<Option key={ID} value={ID.toString()}>
|
||||||
@@ -80,7 +84,9 @@ const TracksContainer = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const options = {
|
const options = {
|
||||||
$top: state.pagination.pageSize,
|
$top: state.pagination.pageSize,
|
||||||
substr: state.searchOptions.substr.replace(`'`, `''`),
|
substr: state.searchOptions.substr.replace(/'*/g, (value) =>
|
||||||
|
isEven(value.length) ? value : `${value}'`
|
||||||
|
),
|
||||||
genreIds: state.searchOptions.genreIds,
|
genreIds: state.searchOptions.genreIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,11 +103,15 @@ const EditAction = ({ ID, name, composer, genre, unitPrice, album, afterTrackUpd
|
|||||||
EditAction.propTypes = {
|
EditAction.propTypes = {
|
||||||
ID: PropTypes.number.isRequired,
|
ID: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
composer: PropTypes.string.isRequired,
|
composer: PropTypes.string,
|
||||||
genre: PropTypes.object.isRequired,
|
genre: PropTypes.object.isRequired,
|
||||||
unitPrice: PropTypes.number.isRequired,
|
unitPrice: PropTypes.number.isRequired,
|
||||||
album: PropTypes.object.isRequired,
|
album: PropTypes.object.isRequired,
|
||||||
afterTrackUpdate: PropTypes.func.isRequired,
|
afterTrackUpdate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EditAction.defaultProps = {
|
||||||
|
composer: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export { EditAction };
|
export { EditAction };
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -7,10 +7,10 @@ const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin({ dangerouslyAllowCleanPatternsOutsideProject: true }),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: path.join(__dirname, '../public/index.html'),
|
template: path.join(__dirname, '../public/index.html'),
|
||||||
filename: path.join(__dirname, '../../app/index.html'),
|
filename: path.join(__dirname, '../../build/index.html'),
|
||||||
publicPath: '/static/', // for js bundles path
|
publicPath: '/static/', // for js bundles path
|
||||||
}),
|
}),
|
||||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
||||||
@@ -20,7 +20,7 @@ module.exports = {
|
|||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
from: path.join(__dirname, '../public'),
|
from: path.join(__dirname, '../public'),
|
||||||
to: path.join(__dirname, '../../app'),
|
to: path.join(__dirname, '../../build'),
|
||||||
globOptions: {
|
globOptions: {
|
||||||
dot: true,
|
dot: true,
|
||||||
ignore: ['**/index.html'],
|
ignore: ['**/index.html'],
|
||||||
@@ -10,6 +10,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
test: /\.(png|jpg)$/,
|
test: /\.(png|jpg)$/,
|
||||||
use: [{ loader: 'url-loader' }],
|
use: [{ loader: 'url-loader' }],
|
||||||
@@ -3,28 +3,47 @@ const webpack = require('webpack');
|
|||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||||
const { rules } = require('./common-rules');
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'development',
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
entry: {
|
entry: {
|
||||||
index: './src/index.jsx',
|
app: './src/index.jsx',
|
||||||
},
|
},
|
||||||
|
devtool: 'inline-source-map',
|
||||||
devServer: {
|
devServer: {
|
||||||
contentBase: './dist',
|
contentBase: './dist',
|
||||||
compress: true, // compress files to gzip to increase download speed
|
hot: true,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
compress: true, // compress files to gzip to increase download speed
|
||||||
disableHostCheck: false, // by default true, it is not recomended,
|
disableHostCheck: false, // by default true, it is not recomended,
|
||||||
// because it makes app vulnerable to DNS rebinding attacks
|
// because it makes app vulnerable to DNS rebinding attacks
|
||||||
headers: {
|
|
||||||
'X-Custom-header': 'custom', // this requires apps with authentication
|
|
||||||
// useful config obj
|
|
||||||
},
|
|
||||||
open: true, // open the browser after server had been started
|
open: true, // open the browser after server had been started
|
||||||
hot: true, // hot module replacement
|
historyApiFallback: true,
|
||||||
historyApiFallback: true, // needs for react-router-dom
|
|
||||||
},
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /(node_modules)/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||||
|
plugins: ['react-refresh/babel'].filter(Boolean),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg)$/,
|
||||||
|
use: [{ loader: 'url-loader' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
|
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -33,24 +52,17 @@ module.exports = {
|
|||||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
||||||
PUBLIC_URL: '',
|
PUBLIC_URL: '',
|
||||||
}),
|
}),
|
||||||
new webpack.ProgressPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
|
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
|
||||||
}),
|
}),
|
||||||
// new webpack.HotModuleReplacementPlugin(), // for hot module replacement option of devServer
|
new webpack.ProgressPlugin(),
|
||||||
],
|
new webpack.HotModuleReplacementPlugin(), // for hot module replacement option of devServer
|
||||||
|
new ReactRefreshWebpackPlugin(),
|
||||||
|
].filter(Boolean),
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].[fullhash].js',
|
filename: '[name].bundle.js',
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
},
|
},
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: { extensions: ['*', '.js', '.jsx'] },
|
resolve: { extensions: ['*', '.js', '.jsx'] },
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,7 @@ module.exports = {
|
|||||||
// from update when developing (doing changes in the files)
|
// from update when developing (doing changes in the files)
|
||||||
filename: '[name].[fullhash].js',
|
filename: '[name].[fullhash].js',
|
||||||
// in this folder path bundles will be placed
|
// in this folder path bundles will be placed
|
||||||
path: path.resolve(__dirname, '../../app/static'),
|
path: path.resolve(__dirname, '../../build/static'),
|
||||||
// where you uploaded your bundled files. (Relative to server root)
|
// where you uploaded your bundled files. (Relative to server root)
|
||||||
// needs for react-router-dom
|
// needs for react-router-dom
|
||||||
publicPath: '/static/',
|
publicPath: '/static/',
|
||||||
@@ -25,6 +25,9 @@ modules:
|
|||||||
path: gen/srv
|
path: gen/srv
|
||||||
properties:
|
properties:
|
||||||
EXIT: 1 # required by deploy.js task to terminate
|
EXIT: 1 # required by deploy.js task to terminate
|
||||||
|
# REVISIT: This is not a good practice -> don't do it that way, we just did it to save some time :)
|
||||||
|
ACCESS_TOKEN_SECRET: secret
|
||||||
|
REFRESH_TOKEN_SECRET: refresh-secret
|
||||||
requires:
|
requires:
|
||||||
# Resources extracted from CAP configuration
|
# Resources extracted from CAP configuration
|
||||||
- name: media-store-hdi
|
- name: media-store-hdi
|
||||||
@@ -62,7 +65,7 @@ modules:
|
|||||||
- name: media-store-html5-app
|
- name: media-store-html5-app
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
type: html5
|
type: html5
|
||||||
path: app
|
path: app/build
|
||||||
build-parameters:
|
build-parameters:
|
||||||
supported-platforms: []
|
supported-platforms: []
|
||||||
build-result: /
|
build-result: /
|
||||||
|
|||||||
@@ -23,9 +23,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx cds run",
|
"start": "npx cds run",
|
||||||
"deploy": "cds deploy --to sqlite:mychinook.db",
|
"deploy": "cds deploy --to sqlite:mychinook.db",
|
||||||
"test": "mocha test/media-store.test.js --verbose --timeout 10000"
|
"test": "mocha ../test/chinook.test.js --verbose --timeout 10000",
|
||||||
|
"import": "cds deploy --to sqlite:sample.db && node ./util/importData.js"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"folders": {
|
||||||
|
"app": "app/build"
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sqlite",
|
"kind": "sqlite",
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
const FIRST_TRACK = {
|
|
||||||
"@odata.context": "$metadata#Tracks(genre(),album(artist()))/$entity",
|
|
||||||
ID: 1,
|
|
||||||
name: "For Those About To Rock (We Salute You)",
|
|
||||||
composer: "Angus Young, Malcolm Young, Brian Johnson",
|
|
||||||
unitPrice: 0.99,
|
|
||||||
album_ID: 1,
|
|
||||||
genre_ID: 1,
|
|
||||||
album: {
|
|
||||||
ID: 1,
|
|
||||||
title: "For Those About To Rock We Salute You",
|
|
||||||
artist_ID: 1,
|
|
||||||
artist: {
|
|
||||||
ID: 1,
|
|
||||||
name: "AC/DC",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
genre: {
|
|
||||||
ID: 1,
|
|
||||||
name: "Rock",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const SECOND_CUSTOMER = {
|
|
||||||
"@odata.context": "$metadata#Customers/$entity",
|
|
||||||
ID: 2,
|
|
||||||
lastName: "Köhler",
|
|
||||||
firstName: "Leonie",
|
|
||||||
city: "Stuttgart",
|
|
||||||
address: "Theodor-Heuss-Straße 34",
|
|
||||||
country: "Germany",
|
|
||||||
phone: "+49 0711 2842222",
|
|
||||||
email: "leonekohler@surfeu.de",
|
|
||||||
};
|
|
||||||
|
|
||||||
const FOURTH_MARKED_TRACK_FOR_SECOND_CUSTOMER = {
|
|
||||||
"@odata.context": "$metadata#MarkedTracks/$entity",
|
|
||||||
ID: 4,
|
|
||||||
name: "Restless and Wild",
|
|
||||||
composer:
|
|
||||||
"F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman",
|
|
||||||
unitPrice: 0.99,
|
|
||||||
alreadyOrdered: true,
|
|
||||||
album_ID: 3,
|
|
||||||
genre_ID: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SECOND_CUSTOMER_INVOICES = {
|
|
||||||
"@odata.context": "$metadata#Invoices",
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
invoiceDate: "2008-12-31T22:00:00Z",
|
|
||||||
total: 1.98,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 12,
|
|
||||||
invoiceDate: "2009-02-10T22:00:00Z",
|
|
||||||
total: 13.86,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 67,
|
|
||||||
invoiceDate: "2009-10-11T21:00:00Z",
|
|
||||||
total: 8.91,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 196,
|
|
||||||
invoiceDate: "2011-05-18T21:00:00Z",
|
|
||||||
total: 1.98,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 219,
|
|
||||||
invoiceDate: "2011-08-20T21:00:00Z",
|
|
||||||
total: 3.96,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 241,
|
|
||||||
invoiceDate: "2011-11-22T21:00:00Z",
|
|
||||||
total: 5.94,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 293,
|
|
||||||
invoiceDate: "2012-07-12T21:00:00Z",
|
|
||||||
total: 0.99,
|
|
||||||
status: 1,
|
|
||||||
customer_ID: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
FIRST_TRACK,
|
|
||||||
SECOND_CUSTOMER,
|
|
||||||
FOURTH_MARKED_TRACK_FOR_SECOND_CUSTOMER,
|
|
||||||
SECOND_CUSTOMER_INVOICES,
|
|
||||||
};
|
|
||||||
@@ -1,442 +0,0 @@
|
|||||||
const { GET, POST, expect } = require("../../test").run("media-store");
|
|
||||||
const cds = require("@sap/cds/lib");
|
|
||||||
const {
|
|
||||||
FIRST_TRACK,
|
|
||||||
SECOND_CUSTOMER,
|
|
||||||
FOURTH_MARKED_TRACK_FOR_SECOND_CUSTOMER,
|
|
||||||
SECOND_CUSTOMER_INVOICES,
|
|
||||||
} = require("./data/media-store.mock");
|
|
||||||
|
|
||||||
const DEFAULT_AXIOS_CONFIG = {
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
};
|
|
||||||
|
|
||||||
async function resetStore() {
|
|
||||||
const targetCSNModel = await cds.load(["db", "srv"]);
|
|
||||||
const model = cds.reflect(targetCSNModel);
|
|
||||||
await cds.deploy(model).to("sqlite:mychinook.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Media Store services", () => {
|
|
||||||
const CURRENT_CUSTOMER_DATA = {
|
|
||||||
ID: 2,
|
|
||||||
email: "leonekohler@surfeu.de",
|
|
||||||
password: "some",
|
|
||||||
roles: ["customer"],
|
|
||||||
};
|
|
||||||
const CURRENT_EMPLOYEE_DATA = {
|
|
||||||
ID: 4,
|
|
||||||
email: "margaret@chinookcorp.com",
|
|
||||||
password: "some",
|
|
||||||
roles: ["employee"],
|
|
||||||
};
|
|
||||||
let customerAccessToken;
|
|
||||||
let employeeAccessToken;
|
|
||||||
|
|
||||||
beforeEach("reset store per each test", async () => {
|
|
||||||
await resetStore();
|
|
||||||
});
|
|
||||||
|
|
||||||
before("login user", async () => {
|
|
||||||
customerLoginResponse = await POST(
|
|
||||||
"/users/login",
|
|
||||||
{
|
|
||||||
email: CURRENT_CUSTOMER_DATA.email,
|
|
||||||
password: CURRENT_CUSTOMER_DATA.password,
|
|
||||||
},
|
|
||||||
DEFAULT_AXIOS_CONFIG
|
|
||||||
);
|
|
||||||
customerAccessToken = customerLoginResponse.data.accessToken;
|
|
||||||
|
|
||||||
employeeLoginResponse = await POST(
|
|
||||||
"/users/login",
|
|
||||||
{
|
|
||||||
email: CURRENT_EMPLOYEE_DATA.email,
|
|
||||||
password: CURRENT_EMPLOYEE_DATA.password,
|
|
||||||
},
|
|
||||||
DEFAULT_AXIOS_CONFIG
|
|
||||||
);
|
|
||||||
employeeAccessToken = employeeLoginResponse.data.accessToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should bootstrap services successfully", () => {
|
|
||||||
const {
|
|
||||||
BrowseTracks,
|
|
||||||
BrowseInvoices,
|
|
||||||
ManageStore,
|
|
||||||
Users,
|
|
||||||
db,
|
|
||||||
} = cds.services;
|
|
||||||
const { Tracks } = BrowseTracks.entities;
|
|
||||||
|
|
||||||
expect(BrowseTracks).not.to.be.undefined;
|
|
||||||
expect(BrowseInvoices).not.to.be.undefined;
|
|
||||||
expect(ManageStore).not.to.be.undefined;
|
|
||||||
expect(Users).not.to.be.undefined;
|
|
||||||
expect(db).not.to.be.undefined;
|
|
||||||
expect(Tracks).not.to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Users", () => {
|
|
||||||
function compareAuthData(actualAuthData) {
|
|
||||||
expect(actualAuthData).not.to.be.undefined;
|
|
||||||
expect(actualAuthData).to.have.own.property("accessToken");
|
|
||||||
expect(actualAuthData).to.have.own.property("refreshToken");
|
|
||||||
expect(actualAuthData).to.have.own.property("ID");
|
|
||||||
expect(actualAuthData).to.have.own.property("email");
|
|
||||||
expect(actualAuthData).to.have.own.property("roles");
|
|
||||||
expect(actualAuthData.ID).to.equal(CURRENT_CUSTOMER_DATA.ID);
|
|
||||||
expect(actualAuthData.email).to.equal(CURRENT_CUSTOMER_DATA.email);
|
|
||||||
expect(actualAuthData.roles).to.deep.equal(CURRENT_CUSTOMER_DATA.roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should login user successfully", async () => {
|
|
||||||
const { data: actualData } = customerLoginResponse;
|
|
||||||
|
|
||||||
compareAuthData(actualData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shouldn't login customer with invalid credentials", async () => {
|
|
||||||
try {
|
|
||||||
await POST(
|
|
||||||
"/users/login",
|
|
||||||
{
|
|
||||||
email: CURRENT_CUSTOMER_DATA.email,
|
|
||||||
password: "some-invalid-password",
|
|
||||||
},
|
|
||||||
DEFAULT_AXIOS_CONFIG
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("401 - Unauthorized");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should refresh tokens successfully", async () => {
|
|
||||||
const {
|
|
||||||
data: { refreshToken },
|
|
||||||
} = customerLoginResponse;
|
|
||||||
const { data: actualRefreshTokensData } = await POST(
|
|
||||||
"/users/refreshTokens",
|
|
||||||
{
|
|
||||||
refreshToken,
|
|
||||||
},
|
|
||||||
DEFAULT_AXIOS_CONFIG
|
|
||||||
);
|
|
||||||
|
|
||||||
compareAuthData(actualRefreshTokensData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shouldn't refresh tokens due to invalid provided one", async () => {
|
|
||||||
try {
|
|
||||||
await POST(
|
|
||||||
"/users/refreshTokens",
|
|
||||||
{
|
|
||||||
refreshToken: "some-invalid-refresh-token",
|
|
||||||
},
|
|
||||||
DEFAULT_AXIOS_CONFIG
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("401 - Unauthorized");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("current customer should retrieve his data", async () => {
|
|
||||||
const { data: actualData } = await GET(
|
|
||||||
`/users/Customers(${CURRENT_CUSTOMER_DATA.ID})`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expect(actualData).not.to.be.undefined;
|
|
||||||
expect(actualData).to.deep.equal(SECOND_CUSTOMER);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("current employee shouldn't have access to customer data", async () => {
|
|
||||||
const someCustomerID = 15;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await GET(`/users/Customers(${someCustomerID})`, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("403 - Forbidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("current customer shouldn't retrieve his data without provided access token", async () => {
|
|
||||||
try {
|
|
||||||
await GET(`/users/Customers(11)`, DEFAULT_AXIOS_CONFIG);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("401 - Unauthorized");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("current customer shouldn't retrieve another customer data", async () => {
|
|
||||||
try {
|
|
||||||
await GET(`/users/Customers(11)`, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("404 - Not Found");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("BrowseTracks", () => {
|
|
||||||
it("should return track with ID eq 1", async () => {
|
|
||||||
const { data } = await GET(
|
|
||||||
"/browse-tracks/Tracks(1)?$expand=genre,album($expand=artist)"
|
|
||||||
);
|
|
||||||
expect(data).to.eql(FIRST_TRACK);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return track with ID eq 4 for second customer", async () => {
|
|
||||||
const { data } = await GET("/browse-tracks/MarkedTracks(4)", {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(data).to.eql(FOURTH_MARKED_TRACK_FOR_SECOND_CUSTOMER);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("BrowseInvoices", () => {
|
|
||||||
const NEW_INVOICE_ID = 413;
|
|
||||||
const CANCELLED_STATUS = -1;
|
|
||||||
|
|
||||||
async function getAllCustomerInvoices() {
|
|
||||||
const { data } = await GET("/browse-invoices/Invoices", {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createInvoice(tracks) {
|
|
||||||
await POST(
|
|
||||||
"/browse-invoices/invoice",
|
|
||||||
{ tracks },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should return all invoices only for current customer", async () => {
|
|
||||||
const data = await getAllCustomerInvoices();
|
|
||||||
|
|
||||||
expect(data).to.eql(SECOND_CUSTOMER_INVOICES);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create invoice for current customer", async () => {
|
|
||||||
const beforeData = await getAllCustomerInvoices();
|
|
||||||
expect(beforeData.value.length).to.equal(
|
|
||||||
SECOND_CUSTOMER_INVOICES.value.length
|
|
||||||
);
|
|
||||||
|
|
||||||
await createInvoice([{ ID: 3 }]);
|
|
||||||
|
|
||||||
const afterData = await getAllCustomerInvoices();
|
|
||||||
expect(afterData.value.length).to.equal(
|
|
||||||
SECOND_CUSTOMER_INVOICES.value.length + 1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not create invoice due to current customer already owns some of provided tracks", async () => {
|
|
||||||
const ALREADY_OWNED_TRACK_ID = 4;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createInvoice([{ ID: ALREADY_OWNED_TRACK_ID }]);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal(
|
|
||||||
"400 - Invoice contains already owned values"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should cancel invoice for current customer", async () => {
|
|
||||||
await createInvoice([{ ID: 3 }]);
|
|
||||||
|
|
||||||
const beforeData = await getAllCustomerInvoices();
|
|
||||||
expect(beforeData.value.length).to.equal(
|
|
||||||
SECOND_CUSTOMER_INVOICES.value.length + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
await POST(
|
|
||||||
"/browse-invoices/cancelInvoice",
|
|
||||||
{ ID: NEW_INVOICE_ID },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const afterData = await getAllCustomerInvoices();
|
|
||||||
expect(afterData.value[afterData.value.length - 1].status).to.equal(
|
|
||||||
CANCELLED_STATUS
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not cancel invoice due to leverage time has expired", async () => {
|
|
||||||
const beforeData = await getAllCustomerInvoices();
|
|
||||||
expect(beforeData.value.length).to.equal(
|
|
||||||
SECOND_CUSTOMER_INVOICES.value.length
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await POST(
|
|
||||||
"/browse-invoices/cancelInvoice",
|
|
||||||
{ ID: 12 },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("400 - Leverage time was expired");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not cancel invoice due to invoice with such ID si not belongs to current customer", async () => {
|
|
||||||
const NOT_OWNED_INVOICE_ID = 146;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await POST(
|
|
||||||
"/browse-invoices/cancelInvoice",
|
|
||||||
{ ID: NOT_OWNED_INVOICE_ID },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal(
|
|
||||||
"404 - Seems like you are not owning this invoice or it is not exists"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ManageStore", () => {
|
|
||||||
const NEW_TRACK_ID = 3504;
|
|
||||||
const newTrack = {
|
|
||||||
name: "Some track",
|
|
||||||
composer: "Some composer",
|
|
||||||
album: { ID: 14 },
|
|
||||||
genre: { ID: 15 },
|
|
||||||
unitPrice: "18.33",
|
|
||||||
};
|
|
||||||
|
|
||||||
async function createTrack(newTrack) {
|
|
||||||
await POST("/manage-store/Tracks", newTrack, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
"content-type": "application/json;IEEE754Compatible=true",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTrack(ID) {
|
|
||||||
return await GET(`/browse-tracks/Tracks(${ID})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should create new track", async () => {
|
|
||||||
await createTrack(newTrack);
|
|
||||||
const { data: createdTrack } = await getTrack(NEW_TRACK_ID);
|
|
||||||
|
|
||||||
expect(createdTrack).to.deep.equal({
|
|
||||||
"@odata.context": "$metadata#Tracks/$entity",
|
|
||||||
ID: NEW_TRACK_ID,
|
|
||||||
name: "Some track",
|
|
||||||
composer: "Some composer",
|
|
||||||
unitPrice: 18.33,
|
|
||||||
album_ID: 14,
|
|
||||||
genre_ID: 15,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("customer should can create track", async () => {
|
|
||||||
try {
|
|
||||||
await POST("/manage-store/Tracks", newTrack, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + customerAccessToken,
|
|
||||||
"content-type": "application/json;IEEE754Compatible=true",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.message).to.equal("403 - Forbidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create new artist", async () => {
|
|
||||||
const NEW_ARTIST_ID = 276;
|
|
||||||
|
|
||||||
await POST(
|
|
||||||
"/manage-store/Artists",
|
|
||||||
{ name: "some" },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await GET(`/manage-store/Artists(${NEW_ARTIST_ID})`, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect({
|
|
||||||
ID: NEW_ARTIST_ID,
|
|
||||||
name: "some",
|
|
||||||
"@odata.context": "$metadata#Artists/$entity",
|
|
||||||
}).to.deep.equal(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create new artist", async () => {
|
|
||||||
const NEW_ALBUM_ID = 349;
|
|
||||||
|
|
||||||
await POST(
|
|
||||||
"/manage-store/Albums",
|
|
||||||
{ title: "some", artist: { ID: 235 } },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
...DEFAULT_AXIOS_CONFIG.headers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await GET(`/manage-store/Albums(${NEW_ALBUM_ID})`, {
|
|
||||||
headers: {
|
|
||||||
authorization: "Basic " + employeeAccessToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect({
|
|
||||||
ID: NEW_ALBUM_ID,
|
|
||||||
title: "some",
|
|
||||||
artist_ID: 235,
|
|
||||||
"@odata.context": "$metadata#Albums/$entity",
|
|
||||||
}).to.deep.equal(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user