Skip to content

Commit

Permalink
Merge pull request #862 from US-Trustee-Program/CAMS-356-functions-v4…
Browse files Browse the repository at this point in the history
…-upgrade

CAMS-356 functions v4 upgrade
  • Loading branch information
btposey authored Sep 4, 2024
2 parents 453ab47 + c19eecf commit ea87980
Show file tree
Hide file tree
Showing 188 changed files with 3,489 additions and 4,093 deletions.
9 changes: 8 additions & 1 deletion architecture/cams.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ workspace {
ordersSync = component "Sync" "Creates events in CAMS based on orders to transfer transactions in DXTR"
consolidations = component "Consolidations" "Consolidation Orders API"
associatedCases = component "Associated Cases" "Associated Cases API"

me = component "Me" "User Info API"
contextCreator = component "Context Creator" "API Application Context Manager"
}
dxtrsql = container "DXTR DB" "DXTR SQL Database"
cosmos = container "Cosmos DB" "NoSQL Database" {
Expand All @@ -40,6 +41,7 @@ workspace {
ordersCosmosContainer = component "Orders Container" "Stores case events"
consolidationsCosmosContainer = component "Consolidations Container" "Stores consolidation orders"
runtimeStateCosmosContainer = component "Runtime State Container" "Stores tracking information for automation"
sessionCacheCosmosContainer = component "Session Cache" "Stores active sessions"
}
}

Expand Down Expand Up @@ -72,6 +74,7 @@ workspace {
webapp -> ordersSuggestions "Reads case summaries for data verification"
webapp -> consolidations "Reads and writes consolidation order data"
webapp -> associatedCases "Reads associated orders from consolidation"
webapp -> me "Reads the authenticated user's session"

nodeapi -> cosmos "Reads and writes case assignments, orders, cases, etc."

Expand Down Expand Up @@ -106,6 +109,10 @@ workspace {
ordersSync -> casesCosmosContainer "Writes case audit logs"
ordersSync -> ordersCosmosContainer "Writes case events"
ordersSync -> runtimeStateCosmosContainer "Reads and writes index for tracking last export"

me -> sessionCacheCosmosContainer "Reads authenticated user's session from the session cache"

contextCreator -> sessionCacheCosmosContainer "Reads and writes authenticated user's session from/to the session cache"
}

views {
Expand Down
59 changes: 35 additions & 24 deletions backend/functions/attorneys/attorneys.function.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { UnknownError } from '../lib/common-errors/unknown-error';
import httpTrigger from './attorneys.function';
import * as httpResponseModule from '../lib/adapters/utils/http-response';
import { AttorneysController } from '../lib/controllers/attorneys/attorneys.controller';
import { CamsError } from '../lib/common-errors/cams-error';
import ContextCreator from '../lib/adapters/utils/application-context-creator';
import { MockData } from '../../../common/src/cams/test-utilities/mock-data';
import { createMockAzureFunctionRequest } from '../azure/functions';
import {
buildTestResponseError,
buildTestResponseSuccess,
createMockAzureFunctionRequest,
} from '../azure/testing-helpers';
import AttorneyList from '../lib/use-cases/attorneys';
import handler from './attorneys.function';
import { InvocationContext } from '@azure/functions';
import { ResponseBody } from '../../../common/src/api/response';
import { AttorneyUser } from '../../../common/src/cams/users';
import ContextCreator from '../azure/application-context-creator';

describe('Attorneys Azure Function tests', () => {
const request = createMockAzureFunctionRequest();
/* eslint-disable-next-line @typescript-eslint/no-require-imports */
const context = require('azure-function-context-mock');
const context = new InvocationContext();

beforeEach(async () => {
jest
Expand All @@ -21,30 +25,37 @@ describe('Attorneys Azure Function tests', () => {
});

test('Should return an HTTP Error if getAttorneyList() throws an unexpected error', async () => {
const attorneysController = new AttorneysController(context);
jest
.spyOn(Object.getPrototypeOf(attorneysController), 'getAttorneyList')
.mockImplementation(() => {
throw new Error();
});

const httpErrorSpy = jest.spyOn(httpResponseModule, 'httpError');
const error = new Error();
const { azureHttpResponse } = buildTestResponseError(error);
jest.spyOn(AttorneysController, 'getAttorneyList').mockRejectedValue(error);

await httpTrigger(context, request);
const response = await handler(request, context);

expect(httpErrorSpy).toHaveBeenCalledWith(expect.any(UnknownError));
expect(response).toEqual(azureHttpResponse);
});

test('Should return an HTTP Error if getAttorneyList() throws a CamsError error', async () => {
jest
.spyOn(AttorneysController.prototype, 'getAttorneyList')
.mockRejectedValue(new CamsError('fake-module'));
const error = new CamsError('fake-module');
const { azureHttpResponse } = buildTestResponseError(error);
jest.spyOn(AttorneysController, 'getAttorneyList').mockRejectedValue(error);

const httpErrorSpy = jest.spyOn(httpResponseModule, 'httpError');
const response = await handler(request, context);

await httpTrigger(context, request);
expect(response).toEqual(azureHttpResponse);
});

expect(httpErrorSpy).toHaveBeenCalledWith(expect.any(CamsError));
expect(httpErrorSpy).not.toHaveBeenCalledWith(expect.any(UnknownError));
test('should return success with a list of attorneys', async () => {
const attorneys = MockData.buildArray(MockData.getAttorneyUser, 4);
const body: ResponseBody<AttorneyUser[]> = {
meta: {
self: 'self-url',
},
data: attorneys,
};
const { camsHttpResponse, azureHttpResponse } = buildTestResponseSuccess<AttorneyUser[]>(body);
jest.spyOn(AttorneysController, 'getAttorneyList').mockResolvedValue(camsHttpResponse);

const response = await handler(request, context);
expect(response).toEqual(azureHttpResponse);
});
});
54 changes: 24 additions & 30 deletions backend/functions/attorneys/attorneys.function.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { httpError, httpSuccess } from '../lib/adapters/utils/http-response';
import { AttorneysController } from '../lib/controllers/attorneys/attorneys.controller';
import ContextCreator from '../lib/adapters/utils/application-context-creator';
import * as dotenv from 'dotenv';
import { isCamsError } from '../lib/common-errors/cams-error';
import { UnknownError } from '../lib/common-errors/unknown-error';
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { AttorneysController } from '../lib/controllers/attorneys/attorneys.controller';
import { initializeApplicationInsights } from '../azure/app-insights';
import { httpRequestToCamsHttpRequest } from '../azure/functions';
import { toAzureError, toAzureSuccess } from '../azure/functions';
import ContextCreator from '../azure/application-context-creator';

dotenv.config();

initializeApplicationInsights();

const MODULE_NAME = 'ATTORNEYS-FUNCTION';

const httpTrigger: AzureFunction = async function (
functionContext: Context,
export default async function handler(
request: HttpRequest,
): Promise<void> {
const applicationContext = await ContextCreator.applicationContextCreator(
functionContext,
request,
);
const attorneysController = new AttorneysController(applicationContext);

invocationContext: InvocationContext,
): Promise<HttpResponseInit> {
const logger = ContextCreator.getLogger(invocationContext);
try {
applicationContext.session =
await ContextCreator.getApplicationContextSession(applicationContext);

const camsRequest = httpRequestToCamsHttpRequest(request);
const attorneysList = await attorneysController.getAttorneyList(camsRequest);
functionContext.res = httpSuccess(attorneysList);
} catch (originalError) {
const error = isCamsError(originalError)
? originalError
: new UnknownError(MODULE_NAME, { originalError });
applicationContext.logger.camsError(error);
functionContext.res = httpError(error);
const applicationContext = await ContextCreator.applicationContextCreator(
invocationContext,
logger,
request,
);
const attorneysList = await AttorneysController.getAttorneyList(applicationContext);
return toAzureSuccess(attorneysList);
} catch (error) {
return toAzureError(logger, MODULE_NAME, error);
}
};
}

export default httpTrigger;
app.http('attorneys', {
methods: ['GET'],
authLevel: 'anonymous',
handler,
route: 'attorneys/{id:int?}',
});
18 changes: 0 additions & 18 deletions backend/functions/attorneys/function.json

This file was deleted.

3 changes: 0 additions & 3 deletions backend/functions/attorneys/sample.dat

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { MockUserSessionGateway } from '../../testing/mock-gateways/mock-user-session-gateway';
import { createMockApplicationContext } from '../../testing/testing-utilities';
import { ApplicationContext } from '../types/basic';
import MockData from '../../../common/src/cams/test-utilities/mock-data';
import { ApplicationContext } from '../lib/adapters/types/basic';
import * as FeatureFlags from '../lib/adapters/utils/feature-flag';
import { ApplicationConfiguration } from '../lib/configs/application-configuration';
import { MockUserSessionGateway } from '../lib/testing/mock-gateways/mock-user-session-gateway';
import { createMockApplicationContext } from '../lib/testing/testing-utilities';
import ContextCreator from './application-context-creator';
import {
createMockAzureFunctionContext,
createMockAzureFunctionRequest,
} from '../../../azure/functions';
import { ApplicationConfiguration } from '../../configs/application-configuration';
import * as FeatureFlags from './feature-flag';
import { createMockAzureFunctionContext, createMockAzureFunctionRequest } from './testing-helpers';
import { azureToCamsHttpRequest } from './functions';
import { LoggerImpl } from '../lib/adapters/services/logger.service';

describe('Application Context Creator', () => {
describe('applicationContextCreator', () => {
test('should create an application context', async () => {
const functionContext = createMockAzureFunctionContext();
const featureFlagsSpy = jest.spyOn(FeatureFlags, 'getFeatureFlags');
const request = createMockAzureFunctionRequest();
const context = await ContextCreator.applicationContextCreator(functionContext, request);
const logger = new LoggerImpl('');
const context = await ContextCreator.applicationContextCreator(
functionContext,
logger,
request,
);
expect(context.logger instanceof Object && 'camsError' in context.logger).toBeTruthy();
expect(context.config instanceof ApplicationConfiguration).toBeTruthy();
expect(context.featureFlags instanceof Object).toBeTruthy();
expect(featureFlagsSpy).toHaveBeenCalled();
expect(context.req).toEqual(request);
expect(context.request).toEqual(await azureToCamsHttpRequest(request));
});
});

Expand All @@ -30,40 +35,52 @@ describe('Application Context Creator', () => {
context = await createMockApplicationContext();
});

test('should throw an UnauthorizedError if there is no request', async () => {
delete context.request;
await expect(ContextCreator.getApplicationContextSession(context)).rejects.toThrow(
'Authorization header missing.',
);
});

test('should throw an UnauthorizedError if authorization header is missing', async () => {
delete context.req.headers.authorization;
delete context.request.headers.authorization;
await expect(ContextCreator.getApplicationContextSession(context)).rejects.toThrow(
'Authorization header missing.',
);
});

test('should throw an UnauthorizedError if authorization header is not a bearer token', async () => {
context.req.headers.authorization = 'shouldthrowError';
context.request.headers.authorization = 'shouldthrowError';

await expect(ContextCreator.getApplicationContextSession(context)).rejects.toThrow(
'Bearer token not found in authorization header',
);
});

test('should throw an UnauthorizedError if authorization header contains Bearer but no token', async () => {
context.req.headers.authorization = 'Bearer ';
context.request.headers.authorization = 'Bearer ';

await expect(ContextCreator.getApplicationContextSession(context)).rejects.toThrow(
'Bearer token not found in authorization header',
);
});

test('should throw an UnauthorizedError if authorization header contains Bearer with malformed token', async () => {
context.req.headers.authorization = 'Bearer some-text-that-is-not-possibly-a-valid-jwt';
context.request.headers.authorization = 'Bearer some-text-that-is-not-possibly-a-valid-jwt';

await expect(ContextCreator.getApplicationContextSession(context)).rejects.toThrow(
'Malformed Bearer token in authorization header',
);
});

test('should call user session gateway lookup', async () => {
const lookupSpy = jest.spyOn(MockUserSessionGateway.prototype, 'lookup');
await ContextCreator.getApplicationContextSession(context);
const request = await azureToCamsHttpRequest(createMockAzureFunctionRequest());
const mockContext = await createMockApplicationContext();
mockContext.request = request;
const lookupSpy = jest
.spyOn(MockUserSessionGateway.prototype, 'lookup')
.mockResolvedValue(MockData.getCamsSession());
await ContextCreator.getApplicationContextSession(mockContext);
expect(lookupSpy).toHaveBeenCalled();
});
});
Expand Down
Loading

0 comments on commit ea87980

Please sign in to comment.