Skip to content

Commit

Permalink
Topics/k1ch/Admin APIs - POST:/personas (#79)
Browse files Browse the repository at this point in the history
* feat: topics/k1ch/ introduce POST/personas

* feat: topics/k1ch/POST-personas/error handling

* chore: topics/k1ch/POST-personas/db-layer and API tests

* chore: topics/k1ch/POST-personas/code style clean

* chore: topics/k1ch/POST-personas/minor updates
  • Loading branch information
k1ch authored Dec 27, 2023
1 parent 8045065 commit 668da0c
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 22 deletions.
55 changes: 55 additions & 0 deletions database/constant/PgErrorCodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const PgErrorCodes = {
// Class 08 - Connection Exception
ConnectionException: '08000',
ConnectionDoesNotExist: '08003',
ConnectionFailure: '08006',

// Class 22 - Data Exception
DataException: '22000',
NullValueNotAllowed: '22004',
NumericValueOutOfRange: '22003',
InvalidTextRepresentation: '22P02',

// Class 23 - Integrity Constraint Violation
IntegrityConstraintViolation: '23000',
NotNullViolation: '23502',
ForeignKeyViolation: '23503',
UniqueViolation: '23505',
CheckViolation: '23514',

// Class 25 - Invalid Transaction State
InvalidTransactionState: '25000',
ActiveSQLTransaction: '25001',
InFailedSQLTransaction: '25P02',

// Class 28 - Invalid Authorization Specification
InvalidAuthorizationSpecification: '28000',
InvalidPassword: '28P01',

// Class 40 - Transaction Rollback
TransactionRollback: '40000',
SerializationFailure: '40001',
DeadlockDetected: '40P01',

// Class 42 - Syntax Error or Access Rule Violation
SyntaxErrorOrAccessRuleViolation: '42000',
SyntaxError: '42601',
UndefinedColumn: '42703',
UndefinedTable: '42P01',
DuplicateColumn: '42701',
DuplicateTable: '42P07',

// Class 53 - Insufficient Resources
InsufficientResources: '53000',
DiskFull: '53100',
OutOfMemory: '53200',
TooManyConnections: '53300',

// Class 54 - Program Limit Exceeded
ProgramLimitExceeded: '54000',
StatementTooComplex: '54001',
}

module.exports = {
PgErrorCodes,
}
1 change: 1 addition & 0 deletions database/knexfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const env = require('./database-env')
module.exports = {
client: 'pg',
connection: env.PGURI,
searchPath: [env.PGSCHEMA, 'public'],
migrations: {
tableName: 'knex_migrations',
schemaName: env.PGSCHEMA
Expand Down
32 changes: 23 additions & 9 deletions database/layer/admin-persona.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
const { PGPool } = require('./pg_pool')
const pool = new PGPool()
const { usherDb } = require('./knex')
const { pgErrorHandler } = require('../utils/pgErrorHandler')

module.exports = {
insertPersona,
deletePersona,
updatePersona
}

async function insertPersona (tenantName, issClaim, subClaim, userContext) {
const insertPersona = async (tenantName, issClaim, subClaim, userContext) => {
const sql = `INSERT INTO usher.personas (tenantkey, sub_claim, user_context)
SELECT key, $3, $4
FROM usher.tenants
Expand All @@ -30,7 +26,7 @@ async function insertPersona (tenantName, issClaim, subClaim, userContext) {
}
}

async function deletePersona (tenantName, issClaim, subClaim, userContext) {
const deletePersona = async (tenantName, issClaim, subClaim, userContext) => {
const sql = `DELETE FROM usher.personas p
WHERE EXISTS (SELECT 1 FROM usher.tenants t WHERE t.KEY = p.tenantkey AND t.name = $1 and t.iss_claim = $2)
AND p.sub_claim = $3 AND p.user_context = $4`
Expand All @@ -48,7 +44,7 @@ async function deletePersona (tenantName, issClaim, subClaim, userContext) {
}
}

async function updatePersona (tenantName, issClaim, oldSubClaim, newSubClaim, oldUserContext, newUserContext) {
const updatePersona = async (tenantName, issClaim, oldSubClaim, newSubClaim, oldUserContext, newUserContext) => {
const sql = `UPDATE usher.personas p SET sub_claim = $4, user_context = $6
WHERE EXISTS (SELECT 1 FROM usher.tenants t WHERE t.KEY = p.tenantkey AND t.name = $1 and t.iss_claim = $2)
AND p.sub_claim = $3 AND p.user_context = $5`
Expand All @@ -65,3 +61,21 @@ async function updatePersona (tenantName, issClaim, oldSubClaim, newSubClaim, ol
return `Update failed: ${error.message}`
}
}

const insertPersonaByTenantKey = async (tenantKey, subClaim, userContext = '') => {
try {
const [persona] = await usherDb('personas')
.insert({ tenantkey: tenantKey, sub_claim: subClaim, user_context: userContext })
.returning(['key', 'sub_claim', 'tenantkey', 'user_context', 'created_at'])
return persona
} catch (err) {
throw pgErrorHandler(err)
}
}

module.exports = {
insertPersona,
deletePersona,
updatePersona,
insertPersonaByTenantKey,
}
5 changes: 5 additions & 0 deletions database/layer/knex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const knex = require('knex');
const knexDbConfig = require('../knexfile');
const usherDb = knex(knexDbConfig);

module.exports = { usherDb }
2 changes: 1 addition & 1 deletion database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.6.1",
"description": "Database layer for TheUsher",
"scripts": {
"test": "mocha",
"test": "mocha --exit",
"db-reset-test-data": "node ./init/reset_testdata.js",
"db-drop-create-schema": "node ./init/drop-create-schema.js",
"db-security-test-data": "node ./init/load_security_test_data.js",
Expand Down
30 changes: 19 additions & 11 deletions database/test/db-admin-persona.test.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,53 @@
const { describe, it } = require('mocha')
const assert = require('assert')
const postPersonas = require('../layer/admin-persona.js')
const { usherDb } = require('../layer/knex')

describe('Admin persona view', function () {
describe('Test INSERT personas', function () {
it('Should insert persona without an exception', async function () {
describe('Admin persona view', () => {
describe('Test INSERT personas', () => {
it('Should insert persona without an exception', async () => {
const insertResult = await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
assert.strictEqual(insertResult, 'Insert successful')
await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
})
it('Should fail to insert for a nonexistent tenant', async function () {
it('Should fail to insert for a nonexistent tenant', async () => {
const insertResult = await postPersonas.insertPersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
assert.strictEqual(insertResult, 'Insert failed: Tenant does not exist matching tenantname test-tenant1 Non-existent iss_claim http://idp.dmgt.com.mock.localhost:3002/')
})
it('Should fail to insert duplicate tenant/persona combination - check tenantname', async function () {
it('Should fail to insert duplicate tenant/persona combination - check tenantname', async () => {
await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
const result = await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
assert.strictEqual(result, 'Insert failed: A persona (sub_claim = [email protected]; user_context = ) already exists on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/')
await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
})
it('Should insert persona by tenant key without an exception', async () => {
const subClaim = '[email protected]'
const [tenant] = await usherDb('tenants').select('*').limit(1)
const persona = await postPersonas.insertPersonaByTenantKey(tenant.key, subClaim)
assert.strictEqual(persona.sub_claim, subClaim)
await usherDb('personas').where({ key: persona.key }).del()
})
})

describe('Test UPDATE personas', function () {
it('Should update persona without an exception by tenantname', async function () {
describe('Test UPDATE personas', () => {
it('Should update persona without an exception by tenantname', async () => {
await postPersonas.insertPersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
const resultTenantname = await postPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '[email protected]', '', '')
assert.strictEqual(resultTenantname, 'Update successful')
await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
})
it('Should fail to update for a nonexistent tenant', async function () {
it('Should fail to update for a nonexistent tenant', async () => {
const resultTenantname = await postPersonas.updatePersona('test-tenant1 Non-existent', 'http://idp.dmgt.com.mock.localhost:3002/', 'auth0|test-persona2-REPLACE', 'should_not_replace_sub_claim', '', '')
assert.strictEqual(resultTenantname, 'Update failed: A persona (sub_claim = auth0|test-persona2-REPLACE; user_context = ) does not exist on tenantname test-tenant1 Non-existent iss_claim http://idp.dmgt.com.mock.localhost:3002/')
})
it('Should fail to update for a nonexistent persona', async function () {
it('Should fail to update for a nonexistent persona', async () => {
const resultTenantname = await postPersonas.updatePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', 'should_not_replace_sub_claim', '', '')
assert.strictEqual(resultTenantname, 'Update failed: A persona (sub_claim = [email protected]; user_context = ) does not exist on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/')
})
})

describe('Test DELETE personas', function () {
it('Should fail to delete a persona not linked to a tenant', async function () {
describe('Test DELETE personas', () => {
it('Should fail to delete a persona not linked to a tenant', async () => {
const resultDelete = await postPersonas.deletePersona('test-tenant1', 'http://idp.dmgt.com.mock.localhost:3002/', '[email protected]', '')
assert.strictEqual(resultDelete, 'Delete failed: A persona (sub_claim = [email protected]; user_context = ) does not exist on tenantname test-tenant1 iss_claim http://idp.dmgt.com.mock.localhost:3002/')
})
Expand Down
82 changes: 82 additions & 0 deletions database/utils/pgErrorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { PgErrorCodes } = require('../constant/PgErrorCodes')

/**
* Handles database errors into a generic message and appropriate http status code
* @param pgDbError The error thrown by the database.
* @returns {message: text, httpStatusCode: number}
*/
const pgErrorHandler = (pgDbError) => {
const error = {}
switch (pgDbError.code) {
case PgErrorCodes.UniqueViolation:
error.message = 'The operation would result in duplicate resources!'
error.httpStatusCode = 409
break

case PgErrorCodes.CheckViolation:
error.message = 'The operation would violate a check constraint!'
error.httpStatusCode = 400
break

case PgErrorCodes.NotNullViolation:
error.message = 'A required value is missing!'
error.httpStatusCode = 400
break

case PgErrorCodes.ForeignKeyViolation:
error.message = 'Referenced resource is invalid!'
error.httpStatusCode = 400
break

case PgErrorCodes.InvalidTextRepresentation:
error.message = 'The provided data format is invalid!'
error.httpStatusCode = 400
break

case PgErrorCodes.SerializationFailure:
error.message = 'Internal DB Error: A transaction serialization error occurred!'
error.httpStatusCode = 500
break

case PgErrorCodes.DeadlockDetected:
error.message = 'Internal DB Error: The operation was halted due to a potential deadlock!'
error.httpStatusCode = 500
break

case PgErrorCodes.SyntaxError:
error.message = 'Internal DB Error: There is a syntax error in the provided SQL or data!'
error.httpStatusCode = 500
break

case PgErrorCodes.UndefinedTable:
error.message = 'Internal DB Error: The table or view you are trying to access does not exist!'
error.httpStatusCode = 500
break

case PgErrorCodes.DiskFull:
error.message = 'Internal DB Error: The operation failed due to insufficient disk space!'
error.httpStatusCode = 500
break

case PgErrorCodes.OutOfMemory:
error.message = 'Internal DB Error: The system ran out of memory!'
error.httpStatusCode = 500
break

case PgErrorCodes.TooManyConnections:
error.message = 'Internal DB Error: There are too many connections to the database!'
error.httpStatusCode = 500
break

default:
error.message = `Unexpected Error: ${error?.message}!`
error.httpStatusCode = 500
break
}

return error
}

module.exports = {
pgErrorHandler,
}
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"main": "the-usher.js",
"scripts": {
"test": "mocha --file test/* --timeout 10000",
"test": "mocha --exit --file test/*",
"start": "nodemon -L --watch the-usher-openapi-spec.yaml --watch ./ --watch ../database/layer the-usher.js",
"debug": "nodemon -L --watch the-usher-openapi-spec.yaml --watch ./src --watch ./the-usher.js --watch ../database/layer --inspect=0.0.0.0:9229 the-usher.js"
},
Expand Down
16 changes: 16 additions & 0 deletions server/src/api_endpoints/personas/persona.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const createError = require('http-errors')
const dbAdminPersona = require('database/layer/admin-persona')

const createPersona = async (req, res, next) => {
try {
const { tenant_key, sub_claim, user_context } = req.body
const persona = await dbAdminPersona.insertPersonaByTenantKey(tenant_key, sub_claim, user_context)
res.status(201).send(persona)
} catch ({ httpStatusCode = 500, message }) {
return next(createError(httpStatusCode, { message }))
}
}

module.exports = {
createPersona,
}
67 changes: 67 additions & 0 deletions server/test/endpoint_admin_personas.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { describe, it, before, afterEach } = require('mocha')
const fetch = require('node-fetch')
const assert = require('assert')

const { getAdmin1IdPToken } = require('./lib/tokens')
const { getServerUrl } = require('./lib/urls')
const { usherDb } = require('../../database/layer/knex')

describe('Admin Personas', () => {
let requestHeaders
const url = `${getServerUrl()}`

before(async () => {
const userAccessToken = await getAdmin1IdPToken()
requestHeaders = {
'Content-Type': 'application/json',
Authorization: `Bearer ${userAccessToken}`,
}
})

describe('POST:/personas', () => {
const validPersonaPayload = {
'sub_claim': '[email protected]',
'tenant_key': 1,
}

it('should return 201 - create a persona', async () => {
const response = await fetch(`${url}/personas`, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(validPersonaPayload)
})
assert.strictEqual(response.status, 201)
const persona = await response.json()
assert.strictEqual(persona.sub_claim, validPersonaPayload.sub_claim)
})

it('should return 400 - fail to create a persona due to invalid tenant', async () => {
const response = await fetch(`${url}/personas`, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify({
...validPersonaPayload,
'tenant_key': 1000,
})
})
assert.strictEqual(response.status, 400)
})

it('should return 409 - fail to create persona due to conflict', async () => {
const { sub_claim, tenant_key: tenantkey } = validPersonaPayload
await usherDb('personas').insert({ sub_claim, tenantkey })
const response = await fetch(`${url}/personas`, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(validPersonaPayload)
})
assert.strictEqual(response.status, 409)
})

afterEach(async () => {
try {
await usherDb('personas').where({ sub_claim: validPersonaPayload.sub_claim }).del()
} catch { }
})
})
})
Loading

0 comments on commit 668da0c

Please sign in to comment.