Skip to content

Commit

Permalink
Merge from feat-map
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanBirtch-aot committed Jul 8, 2024
2 parents 8001826 + c260fe2 commit dc9f60a
Show file tree
Hide file tree
Showing 19 changed files with 935 additions and 409 deletions.
8 changes: 1 addition & 7 deletions .github/actions/build-push-container/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,11 @@ inputs:
app_chefs_geo_address_apiurl:
description: Proxy URL to BC Geo Address API URL
required: true
default: "../api/v1/bcgeoaddress/address"
app_chefs_advance_geo_address_apiurl:
description: Proxy URL to BC Geo Address API URL for advance search
required: true
default: "../api/v1/bcgeoaddress/advance/address"
ref:
description: The checkout ref id
required: false
default: ''
default: ""
pr_number:
description: Pull request number
required: false
Expand Down Expand Up @@ -98,7 +94,6 @@ runs:
VITE_CHEFSTOURURL: ${{ inputs.app_chefstoururl }}
VITE_FRONTEND_BASEPATH: ${{ inputs.route_path }}
VITE_CHEFS_GEO_ADDRESS_APIURL: ${{ inputs.app_chefs_geo_address_apiurl }}
VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL: ${{ inputs.app_chefs_advance_geo_address_apiurl }}
VITE_BC_GEO_ADDRESS_APIURL: ${{ inputs.app_bc_geo_address_apiurl }}
ENV_PATH: ./app/frontend/.env
shell: bash
Expand All @@ -109,7 +104,6 @@ runs:
echo VITE_HOWTOURL=$VITE_HOWTOURL >> $ENV_PATH
echo VITE_CHEFSTOURURL=$VITE_CHEFSTOURURL >> $ENV_PATH
echo VITE_CHEFS_GEO_ADDRESS_APIURL=$VITE_CHEFS_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL=$VITE_CHEFS_ADVANCE_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_BC_GEO_ADDRESS_APIURL=$VITE_BC_GEO_ADDRESS_APIURL >> $ENV_PATH
echo VITE_FRONTEND_BASEPATH=$VITE_FRONTEND_BASEPATH >> $ENV_PATH
Expand Down
7 changes: 4 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
{
"cwd": "${workspaceFolder}/app/frontend",
"env": {
"VITE_TITLE": "Common Hosted Forms - Local",
"VITE_CHEFS_GEO_ADDRESS_APIURL": "http://localhost:8080/app/api/v1/bcgeoaddress/advance/address",
"VITE_CHEFSTOURURL": "https://www.youtube.com/embed/obOhyYusMjM",
"VITE_CONTACT": "[email protected]",
"VITE_FRONTEND_BASEPATH": "/app",
"VITE_CHEFSTOURURL": "https://www.youtube.com/embed/obOhyYusMjM",
"VITE_HOWTOURL": "https://www.youtube.com/playlist?list=PL9CV_8JBQHirsQAShw45PZeU1CkU88Q53"
"VITE_HOWTOURL": "https://www.youtube.com/playlist?list=PL9CV_8JBQHirsQAShw45PZeU1CkU88Q53",
"VITE_TITLE": "Common Hosted Forms - Local"
},
"name": "CHEFS Frontend",
"outputCapture": "std",
Expand Down
20 changes: 10 additions & 10 deletions app/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 33 additions & 15 deletions app/src/components/errorToProblem.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@ const Problem = require('api-problem');

const log = require('./log')(module.filename);

module.exports = function (service, e) {
if (e.response) {
// Handle raw data
let data;
if (typeof e.response.data === 'string' || e.response.data instanceof String) {
data = JSON.parse(e.response.data);
} else {
data = e.response.data;
}
/**
* Try to convert response data to JSON, but failing that just return it as-is.
*
* @param {*} data the data to attempt to parse into JSON.
* @returns an object if data is JSON, otherwise data itself
*/
const _parseResponseData = (data) => {
let parsedData;

try {
parsedData = JSON.parse(data);
} catch (error) {
// Syntax Error: It's not valid JSON.
parsedData = data;
}

return parsedData;
};

module.exports = function (service, error) {
if (error.response) {
const data = _parseResponseData(error.response.data);

log.error(`Error from ${service}: status = ${error.response.status}, data: ${JSON.stringify(data)}`, error);

log.error(`Error from ${service}: status = ${e.response.status}, data : ${JSON.stringify(data)}`, e);
// Validation Error
if (e.response.status === 422) {
throw new Problem(e.response.status, {
if (error.response.status === 422) {
throw new Problem(error.response.status, {
detail: data.detail,
errors: data.errors,
});
}

// Something else happened but there's a response
throw new Problem(e.response.status, { detail: e.response.data.toString() });
throw new Problem(error.response.status, { detail: error.response.data.toString() });
} else {
log.error(`Unknown error calling ${service}: ${e.message}`, e);
throw new Problem(502, `Unknown ${service} Error`, { detail: e.message });
log.error(`Unknown error calling ${service}: ${error.message}`, error);

throw new Problem(502, `Unknown ${service} Error`, {
detail: error.message,
});
}
};
46 changes: 40 additions & 6 deletions app/src/forms/proxy/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const jwtService = require('../../components/jwtService');
const axios = require('axios');
const { ExternalAPIStatuses } = require('../common/constants');
const Problem = require('api-problem');
const ProxyServiceError = require('./error');
const { NotFoundError } = require('objection');
const log = require('../../components/log')(module.filename);

module.exports = {
generateProxyHeaders: async (req, res, next) => {
Expand All @@ -19,22 +22,53 @@ module.exports = {
const proxyHeaderInfo = await service.readProxyHeaders(req.headers);
// find the specified external api configuration...
const extAPI = await service.getExternalAPI(req.headers, proxyHeaderInfo);
if (extAPI.code != ExternalAPIStatuses.APPROVED) {
throw new Problem(407, 'External API has not been approved by CHEFS.');
}
// add path to endpoint url if included in headers...
const extUrl = service.createExternalAPIUrl(req.headers, extAPI.endpointUrl);
// build list of request headers based on configuration...
const extHeaders = await service.createExternalAPIHeaders(extAPI, proxyHeaderInfo);
// check for approval before we call it..
if (extAPI.code != ExternalAPIStatuses.APPROVED) {
throw new Problem(407, 'External API has not been approved by CHEFS.');
}
let axiosInstance = axios.create({
headers: extHeaders,
});
// call the external api
const { data } = await axiosInstance.get(extUrl);
const { data, status } = await axiosInstance.get(extUrl).catch(function (err) {
let message = err.message;
if (err.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
log.warn(`Error returned from the external API': ${message}`);
} else if (err.request) {
message = 'External API call made, no response received.';
log.warn(message);
} else {
// Something happened in setting up the request that triggered an Error
log.warn(`Error setting up the external API request: ${message}`);
}
// send a bad gateway, the message should contain the real status
throw new Problem(502, message);
});
// if all good return data
res.status(200).json(data);
res.status(status).json(data);
} catch (error) {
next(error);
if (error instanceof ProxyServiceError) {
// making an assumption that the form component making this call
// has not been setup correctly yet.
// formio components will call as soon as the URL is entered while designing.
// calls will fire before the designer has added the headers.
log.warn(error.message);
// send back status 400 Bad Request
res.sendStatus(400);
} else if (error instanceof NotFoundError) {
// may have created formio component before adding the External API config.
log.warn('External API configuration does not exist.');
// send back status 400 Bad Request
res.sendStatus(400);
} else {
next(error);
}
}
},
};
8 changes: 8 additions & 0 deletions app/src/forms/proxy/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ProxyServiceError extends Error {
constructor(message) {
super(message);
this.name = 'ProxyServiceError';
}
}

module.exports = ProxyServiceError;
20 changes: 12 additions & 8 deletions app/src/forms/proxy/service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { encryptionService } = require('../../components/encryptionService');
const jwtService = require('../../components/jwtService');

const ProxyServiceError = require('./error');
const { ExternalAPI } = require('../../forms/common/models');

const headerValue = (headers, key) => {
Expand All @@ -21,7 +21,7 @@ const trimTrailingSlashes = (str) => str.replace(/\/+$/g, '');
const service = {
generateProxyHeaders: async (payload, currentUser, token) => {
if (!payload || !currentUser || !currentUser.idp) {
throw new Error('Cannot generate proxy headers with missing or incomplete parameters');
throw new ProxyServiceError('Cannot generate proxy headers with missing or incomplete parameters');
}

const headerData = {
Expand Down Expand Up @@ -51,16 +51,20 @@ const service = {
const data = JSON.parse(decryptedHeaderData);
return data;
} catch (error) {
throw new Error(`Could not decrypt proxy headers: ${error.message}`);
throw new ProxyServiceError(`Could not decrypt proxy headers: ${error.message}`);
}
} else {
throw new Error('Proxy headers not found');
throw new ProxyServiceError('X-CHEFS-PROXY-DATA headers not found or empty.');
}
},
getExternalAPI: async (headers, proxyHeaderInfo) => {
const externalApiName = headerValue(headers, 'X-CHEFS-EXTERNAL-API-NAME');
const externalAPI = await ExternalAPI.query().modify('findByFormIdAndName', proxyHeaderInfo['formId'], externalApiName).first().throwIfNotFound();
return externalAPI;
if (externalApiName) {
const externalAPI = await ExternalAPI.query().modify('findByFormIdAndName', proxyHeaderInfo['formId'], externalApiName).first().throwIfNotFound();
return externalAPI;
} else {
throw new ProxyServiceError('X-CHEFS-EXTERNAL-API-NAME header not found or empty.');
}
},
createExternalAPIUrl: (headers, endpointUrl) => {
//check incoming request headers for path to add to the endpoint url
Expand All @@ -78,15 +82,15 @@ const service = {
if (externalAPI.sendUserToken) {
if (!proxyHeaderInfo || !proxyHeaderInfo.token) {
// just assume that if there is no idpUserId than it isn't a userInfo object
throw new Error('Cannot create user token headers for External API without populated proxy header info token.');
throw new ProxyServiceError('Cannot create user token headers for External API without populated proxy header info token.');
}
const val = externalAPI.userTokenBearer ? `Bearer ${proxyHeaderInfo['token']}` : proxyHeaderInfo['token'];
result[externalAPI.userTokenHeader] = val;
}
if (externalAPI.sendUserInfo) {
if (!proxyHeaderInfo || !proxyHeaderInfo.idp) {
// just assume that if there is no idp than it isn't a userInfo object
throw new Error('Cannot create user headers for External API without populated proxy header info object.');
throw new ProxyServiceError('Cannot create user headers for External API without populated proxy header info object.');
}

// user information (no token)
Expand Down
21 changes: 17 additions & 4 deletions app/tests/unit/components/errorToProblem.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ const errorToProblem = require('../../../src/components/errorToProblem');
const SERVICE = 'TESTSERVICE';

describe('errorToProblem', () => {
it('should throw a 404', () => {
const error = {
response: {
data: { detail: 'detail' },
status: 404,
},
};

expect(() => errorToProblem(SERVICE, error)).toThrow('404');
});

it('should throw a 422', () => {
const e = {
const error = {
response: {
data: { detail: 'detail' },
status: 422,
},
};
expect(() => errorToProblem(SERVICE, e)).toThrow('422');

expect(() => errorToProblem(SERVICE, error)).toThrow('422');
});

it('should throw a 502', () => {
const e = {
const error = {
message: 'msg',
};
expect(() => errorToProblem(SERVICE, e)).toThrow('502');

expect(() => errorToProblem(SERVICE, error)).toThrow('502');
});
});
26 changes: 24 additions & 2 deletions app/tests/unit/forms/proxy/controller.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { getMockReq, getMockRes } = require('@jest-mock/express');
const controller = require('../../../../src/forms/proxy/controller');
const service = require('../../../../src/forms/proxy/service');
const jwtService = require('../../../../src/components/jwtService');
const { NotFoundError } = require('objection');

const bearerToken = Math.random().toString(36).substring(2);

Expand Down Expand Up @@ -138,9 +139,30 @@ describe('callExternalApi', () => {

expect(service.readProxyHeaders).toBeCalledTimes(1);
expect(service.getExternalAPI).toBeCalledTimes(1);
expect(service.createExternalAPIUrl).not.toHaveBeenCalled();
expect(service.createExternalAPIHeaders).not.toHaveBeenCalled();
expect(service.createExternalAPIUrl).toBeCalledTimes(1);
expect(service.createExternalAPIHeaders).toBeCalledTimes(1);
// this is the point where we check the status code for external api
expect(axios.get).not.toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
expect(next).toBeCalledTimes(1);
});

it('should return 400 when headers missing', async () => {
const req = getMockReq({ headers: { 'X-CHEFS-PROXY-DATA': 'encrypted blob of proxy data' } });
const { res, next } = getMockRes();

service.readProxyHeaders = jest.fn().mockReturnValue({});
service.getExternalAPI = jest.fn().mockRejectedValueOnce(new NotFoundError());
service.createExternalAPIUrl = jest.fn().mockReturnValue('http://external.api/private');
service.createExternalAPIHeaders = jest.fn().mockReturnValue({ 'X-TEST-HEADERS': 'test-headers-err' });

await controller.callExternalApi(req, res, next);

expect(service.readProxyHeaders).toBeCalledTimes(1);
expect(service.getExternalAPI).toBeCalledTimes(1);
expect(service.createExternalAPIUrl).not.toHaveBeenCalled();
expect(service.createExternalAPIHeaders).not.toHaveBeenCalled();
expect(res.sendStatus).toBeCalledWith(400);
expect(next).not.toHaveBeenCalled();
});
});
Loading

0 comments on commit dc9f60a

Please sign in to comment.