Skip to content
This repository has been archived by the owner on Jul 14, 2023. It is now read-only.

Commit

Permalink
feat: fill managed fields with username (#154)
Browse files Browse the repository at this point in the history
* test: add authenticated endpoint for managed

* test: add skip

* feat: fill managed fields with username
fix #153

* chore: remove unused @sap/cds

* test: improve authorization, check created entity

* chore: remove @sap/cds / add missing @sap/cds

* chore: adjust descriptions
  • Loading branch information
gregorwolf committed Jun 9, 2021
1 parent 5f972aa commit 0640e13
Show file tree
Hide file tree
Showing 12 changed files with 435 additions and 124 deletions.
67 changes: 67 additions & 0 deletions __tests__/__assets__/beershop-admin-service.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
CREATE TABLE BeershopAdminService_UserScopes (
username VARCHAR(5000) NOT NULL,
is_admin BOOLEAN,
PRIMARY KEY(username)
);

CREATE TABLE csw_Beers (
ID VARCHAR(36) NOT NULL,
createdAt TIMESTAMP,
createdBy VARCHAR(255),
modifiedAt TIMESTAMP,
modifiedBy VARCHAR(255),
name VARCHAR(100),
abv DECIMAL(3, 1),
ibu INTEGER,
brewery_ID VARCHAR(36),
PRIMARY KEY(ID)
);

CREATE TABLE csw_Brewery (
ID VARCHAR(36) NOT NULL,
createdAt TIMESTAMP,
createdBy VARCHAR(255),
modifiedAt TIMESTAMP,
modifiedBy VARCHAR(255),
name VARCHAR(150),
PRIMARY KEY(ID)
);

CREATE TABLE csw_TypeChecks (
ID VARCHAR(36) NOT NULL,
type_Boolean BOOLEAN,
type_Int32 INTEGER,
type_Int64 BIGINT,
type_Decimal DECIMAL(2, 1),
type_Double NUMERIC(30, 15),
type_Date DATE,
type_Time TIME,
type_DateTime TIMESTAMP,
type_Timestamp TIMESTAMP,
type_String VARCHAR(5000),
type_Binary CHAR(100),
type_LargeBinary BYTEA,
type_LargeString TEXT,
PRIMARY KEY(ID)
);

CREATE VIEW BeershopAdminService_Beers AS SELECT
Beers_0.ID,
Beers_0.createdAt,
Beers_0.createdBy,
Beers_0.modifiedAt,
Beers_0.modifiedBy,
Beers_0.name,
Beers_0.abv,
Beers_0.ibu,
Beers_0.brewery_ID
FROM csw_Beers AS Beers_0;

CREATE VIEW BeershopAdminService_Breweries AS SELECT
Brewery_0.ID,
Brewery_0.createdAt,
Brewery_0.createdBy,
Brewery_0.modifiedAt,
Brewery_0.modifiedBy,
Brewery_0.name
FROM csw_Brewery AS Brewery_0
27 changes: 27 additions & 0 deletions __tests__/__assets__/cap-proj/db/init/beershop.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ postgres;

\c beershop

CREATE TABLE BeershopAdminService_UserScopes (
username VARCHAR(5000) NOT NULL,
is_admin BOOLEAN,
PRIMARY KEY(username)
);

CREATE TABLE csw_Beers
(
ID VARCHAR(36) NOT NULL,
Expand Down Expand Up @@ -188,6 +194,27 @@ AS
TypeChecks_0.type_LargeString
FROM superbeer.csw_TypeChecks AS TypeChecks_0;

CREATE VIEW BeershopAdminService_Beers AS SELECT
Beers_0.ID,
Beers_0.createdAt,
Beers_0.createdBy,
Beers_0.modifiedAt,
Beers_0.modifiedBy,
Beers_0.name,
Beers_0.abv,
Beers_0.ibu,
Beers_0.brewery_ID
FROM csw_Beers AS Beers_0;

CREATE VIEW BeershopAdminService_Breweries AS SELECT
Brewery_0.ID,
Brewery_0.createdAt,
Brewery_0.createdBy,
Brewery_0.modifiedAt,
Brewery_0.modifiedBy,
Brewery_0.name
FROM csw_Brewery AS Brewery_0

COPY superbeer.csw_Beers
(ID, name, abv, ibu, brewery_ID) FROM '/tmp/data/csw-Beers.csv' DELIMITER ',' CSV HEADER;
COPY superbeer.csw_Brewery
Expand Down
33 changes: 33 additions & 0 deletions __tests__/__assets__/cap-proj/rest-client-test/beers-admin.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### Read Entities
GET https://localhost:4004/beershop-admin/
Authorization: Basic bob:
### Read UserScopes
GET https://localhost:4004/beershop-admin/UserScopes
Authorization: Basic bob:
### Read Beers
GET https://localhost:4004/beershop-admin/Beers
Authorization: Basic bob:
### Create Beer with POST
# @name create
POST https://localhost:4004/beershop-admin/Beers
Content-Type: application/json
Authorization: Basic bob:

{
"ID": "{{$guid}}",
"name": "Testbier with POST"
}
###
@id = {{create.response.body.ID}}
### Read Beer
GET https://localhost:4004/beershop/Beers({{id}})
### Delete Beer
DELETE https://localhost:4004/beershop/Beers({{id}})

### Create Beer with PUT
PUT https://localhost:4004/beershop/Beers({{id}})
Content-Type: application/json

{
"name": "Testbier with PUT"
}
14 changes: 14 additions & 0 deletions __tests__/__assets__/cap-proj/srv/beershop-admin-service.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using {csw} from '../db/schema';

@(requires : 'authenticated-user')
service BeershopAdminService {

entity Beers as projection on csw.Beers;
entity Breweries as projection on csw.Brewery;

@readonly
entity UserScopes {
key username : String;
is_admin : Boolean;
};
}
11 changes: 11 additions & 0 deletions __tests__/__assets__/cap-proj/srv/beershop-admin-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = async function (srv) {
srv.on('READ', 'UserScopes', async (req) => {
const users = [
{
username: req.user.id,
is_admin: req.user.is('admin'),
},
]
return users
})
}
3 changes: 2 additions & 1 deletion __tests__/__assets__/cap-proj/srv/beershop-service.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const cds = require('@sap/cds')

module.exports = (srv) => {
srv.on('reset', async () => {
let db
Expand All @@ -11,7 +12,7 @@ module.exports = (srv) => {
})
srv.before('READ', '*', async (req) => {
if (req.headers.schema) {
req.user.schema = req.headers.schema;
req.user.schema = req.headers.schema
}
})
}
12 changes: 11 additions & 1 deletion __tests__/cdssql2pgsql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('fs')

describe('CDS SQL to PostgreSQL', () => {
describe('Create Statements', () => {
test('+ should return PostgreSQL compatible statment', async () => {
test('+ should return PostgreSQL compatible statment for beershop-service', async () => {
const servicePath = `${__dirname}/__assets__/cap-proj/srv/beershop-service`
const csn = await cds.load(`${servicePath}`)
const cdssql = cds.compile.to.sql(csn, { as: 'str' })
Expand All @@ -14,5 +14,15 @@ describe('CDS SQL to PostgreSQL', () => {
const pgsqlMatch = fs.readFileSync(`${__dirname}/__assets__/test.sql`, 'utf-8')
expect(pgsql).toMatch(pgsqlMatch)
})
test('+ should return PostgreSQL compatible statment for beershop-admin-service', async () => {
const servicePath = `${__dirname}/__assets__/cap-proj/srv/beershop-admin-service`
const csn = await cds.load(`${servicePath}`)
const cdssql = cds.compile.to.sql(csn, { as: 'str' })
const cdspg = new postgresDatabase()
let pgsql = cdspg.cdssql2pgsql(cdssql).trim()
// fs.writeFileSync(`${__dirname}/__assets__/beershop-admin-service.sql`, pgsql)
const pgsqlMatch = fs.readFileSync(`${__dirname}/__assets__/beershop-admin-service.sql`, 'utf-8')
expect(pgsql).toMatch(pgsqlMatch)
})
})
})
2 changes: 2 additions & 0 deletions __tests__/lib/pg/_runLocal.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ module.exports = async (model, credentials, app, deployDB = false) => {
// that matches the db content/setup in dockered pg
const servicePath = path.resolve(model, 'beershop-service')
await cds.serve('BeershopService').from(servicePath).in(app)
const adminServicePath = path.resolve(model, 'beershop-admin-service')
await cds.serve('BeershopAdminService').from(adminServicePath).in(app)
}
95 changes: 95 additions & 0 deletions __tests__/lib/pg/service-admin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const cds = require('@sap/cds')
const deploy = require('@sap/cds/lib/deploy')

cds.env.requires.db = { kind: 'postgres' }
cds.env.requires.postgres = {
impl: './cds-pg', // hint: not really sure as to why this is, but...
}

// default (single) test environment is local,
// so running against a dockerized postgres with a local cap bootstrap service.
// when there's a .env in /__tests__/__assets__/cap-proj/
// with a scpServiceURL (see .env.example in that dir)
// tests are also run against a deployed service url (cf hyperscaler postgres)
const { suiteEnvironments, app } = require('./_buildSuiteEnvironments')

describe.each(suiteEnvironments)(
'[%s] OData to Postgres dialect',
(_suitename /* translates to %s via printf */, credentials, model, request) => {
beforeAll(async () => {
// mock console.*
// in order not to pollute test logs
global.console = {
log: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}
this._model = model
this._dbProperties = {
kind: 'postgres',
model: this._model,
credentials: credentials,
}

// only bootstrap in local mode as scp app is deployed and running
if (_suitename.startsWith('local')) {
await require('./_runLocal')(model, credentials, app, false) // don't deploy content initially
}
})

beforeEach(async () => {
// "reset" aka re-deploy static content
if (_suitename.startsWith('local')) {
await deploy(this._model, {}).to(this._dbProperties)
} else if (_suitename === 'scp') {
await request.post(`/beershop/reset`).send({}).set('content-type', 'application/json')
}
})

afterAll(() => {
delete global.console // avoid side effect
})

test('OData: List of entities exposed by the admin service', async () => {
const response = await request.get('/beershop-admin/').auth('bob', '')

expect(response.status).toStrictEqual(200)
expect(response.body.value.length).toStrictEqual(3)
})

test('OData: List of entities exposed by the service', async () => {
const response = await request.get('/beershop/')

expect(response.status).toStrictEqual(200)
expect(response.body.value.length).toStrictEqual(4)
})

describe('OData admin: CREATE', () => {
test('odata: entityset Beers -> sql: insert into beers', async () => {
const response = await request
.post('/beershop-admin/Beers')
.send({
name: 'Schlappe Seppel',
ibu: 10,
abv: '16.2',
})
.set('content-type', 'application/json;charset=UTF-8;IEEE754Compatible=true')
.auth('bob', '')

expect(response.body.createdAt).toBeTruthy()
expect(response.body.modifiedAt).toBeTruthy()
expect(response.body.createdBy).toStrictEqual('bob')
expect(response.body.modifiedBy).toStrictEqual('bob')
expect(response.status).toStrictEqual(201)

const responseGet = await request.get(`/beershop-admin/Beers(${response.body.ID})`).auth('bob', '')

expect(responseGet.status).toStrictEqual(200)
expect(responseGet.body.createdBy).toStrictEqual('bob')
expect(responseGet.body.modifiedBy).toStrictEqual('bob')
})
})
}
)
23 changes: 13 additions & 10 deletions lib/pg/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const DEBUG = cds.debug('cds-pg')
* @param {*} txTimestamp
* @return {import('pg').QueryArrayResult}
*/
const executeGenericCQN = (model, dbc, query /*, user, locale, txTimestamp */) => {
const { sql, values = [] } = _cqnToSQL(model, query)
const executeGenericCQN = (model, dbc, query, user /*, locale, txTimestamp */) => {
const { sql, values = [] } = _cqnToSQL(model, query, user)
const isOne = query.SELECT && query.SELECT.one
const postPropertyMapper = getPostProcessMapper(PG_TYPE_CONVERSION_MAP, model, query)
return _executeSQLReturningRows(dbc, sql, values, isOne, postPropertyMapper)
Expand All @@ -41,11 +41,11 @@ const executeGenericCQN = (model, dbc, query /*, user, locale, txTimestamp */) =
* @param {*} txTimestamp
* @return {import('pg').QueryArrayResult}
*/
const executeSelectCQN = (model, dbc, query /*, user, locale, txTimestamp*/) => {
const executeSelectCQN = (model, dbc, query, user /*, locale, txTimestamp*/) => {
if (hasExpand(query)) {
return processExpand(dbc, query, model)
return processExpand(dbc, query, model, user)
} else {
const { sql, values = [] } = _cqnToSQL(model, query)
const { sql, values = [] } = _cqnToSQL(model, query, user)
const isOne = query.SELECT && query.SELECT.one
const postPropertyMapper = getPostProcessMapper(PG_TYPE_CONVERSION_MAP, model, query)
return _executeSQLReturningRows(dbc, sql, values, isOne, postPropertyMapper)
Expand All @@ -65,8 +65,8 @@ const executeSelectCQN = (model, dbc, query /*, user, locale, txTimestamp*/) =>
* @param {*} txTimestamp
* @return {Array}
*/
const executeInsertCQN = async (model, dbc, cqn) => {
const { sql, values = [] } = _cqnToSQL(model, cqn)
const executeInsertCQN = async (model, dbc, cqn, user) => {
const { sql, values = [] } = _cqnToSQL(model, cqn, user)
const postPropertyMapper = getPostProcessMapper(PG_TYPE_CONVERSION_MAP, model, cqn)
const resultPromises = []

Expand Down Expand Up @@ -113,15 +113,16 @@ async function executePlainSQL(dbc, rawSql, rawValues) {
* @param {import('pg').PoolClient} dbc
* @param {Object} cqn
* @param {Object} model
* @param {*} user
* @return {import('pg').QueryArrayResult} the
*/
const processExpand = (dbc, cqn, model) => {
const processExpand = (dbc, cqn, model, user) => {
let queries = []
const expandQueries = createJoinCQNFromExpanded(cqn, model, true)
for (const cqn of expandQueries.queries) {
// REVISIT
// Why is the post processing in expand different?
const { sql, values } = _cqnToSQL(model, cqn, true)
const { sql, values } = _cqnToSQL(model, cqn, user, true)
const postPropertyMapper = getPostProcessMapper(PG_TYPE_CONVERSION_MAP, model, cqn)

queries.push(_executeSQLReturningRows(dbc, sql, values, false, postPropertyMapper))
Expand All @@ -135,10 +136,11 @@ const processExpand = (dbc, cqn, model) => {
*
* @param {Object} model
* @param {Object} cqn
* @param {*} user
* @param {Boolean} isExpand
* @return {Object} the query object containing sql and values
*/
function _cqnToSQL(model, cqn, isExpand = false) {
function _cqnToSQL(model, cqn, user, isExpand = false) {
return _replacePlaceholders(
sqlFactory(
cqn,
Expand All @@ -151,6 +153,7 @@ function _cqnToSQL(model, cqn, isExpand = false) {
},
isExpand, // Passed to inform the select builder that we are dealing with an expand call
now: 'NOW ()',
user,
},
model,
isExpand
Expand Down
Loading

0 comments on commit 0640e13

Please sign in to comment.