Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
},
collectCoverageFrom: [
"<rootDir>/src/**/*.ts",
"!<rootDir>/src/mocks/**",
"!<rootDir>/src/stacks/*.ts",
],
testEnvironment: 'node',
Expand Down
89 changes: 74 additions & 15 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,101 @@
/* istanbul ignore file */
import 'source-map-support/register'
import { App } from 'aws-cdk-lib'
import { AuthStack, HalfsiesStack } from './stacks'
import {
APIDomainStack,
AuthStack,
FoodHowStack,
HalfsiesStack,
} from './stacks'

const app = new App()
const ENV_REGION = 'us-east-2'
const { blankFamilyUserPool } = new AuthStack(app, 'Auth', {
stackName: 'Auth',
env: {
region: 'us-east-2',
region: ENV_REGION,
},
})

// PROD
const PROD_API_DOMAIN_CERTIFICATE_ARN = 'arn:aws:acm:us-east-1:382713793519:certificate/700ed0de-e320-4c84-b377-9984263f610d'
const PROD_API_DOMAIN_NAME = 'api.troyblank.com'
const PROD_RESOURCE_POSTFIX = 'Prod'

const { domainName: prodDomainName } = new APIDomainStack(
app,
'ApiDomain',
{
stackName: 'ApiDomain',
domainName: PROD_API_DOMAIN_NAME,
domainCertificateARN: PROD_API_DOMAIN_CERTIFICATE_ARN,
env: { region: ENV_REGION },
},
)

new FoodHowStack(
app,
'FoodHow',
{
stackName: 'FoodHow',
apiDomainName: prodDomainName,
resourcePostFix: PROD_RESOURCE_POSTFIX,
accessControlAllowOrigin: 'https://foodhow.troyblank.com',
userPool: blankFamilyUserPool,
env: { region: ENV_REGION },
},
)
new HalfsiesStack(
app,
'Halfsies',
{
stackName: 'Halfsies',
customDomainCertificateARN: 'arn:aws:acm:us-east-1:382713793519:certificate/700ed0de-e320-4c84-b377-9984263f610d',
customDomainName: 'api.troyblank.com',
resourcePostFix: 'Prod',
apiDomainName: prodDomainName,
resourcePostFix: PROD_RESOURCE_POSTFIX,
accessControlAllowOrigin: 'https://halfsies.troyblank.com',
userPool: blankFamilyUserPool,
env: {
region: 'us-east-2',
},
env: { region: ENV_REGION },
},
)

// STAGE
const STAGE_API_DOMAIN_CERTIFICATE_ARN = 'arn:aws:acm:us-east-1:382713793519:certificate/74c434a7-2ad0-4477-8a50-e3b41218f85a'
const STAGE_API_DOMAIN_NAME = 'stage-api.troyblank.com'
const STAGE_RESOURCE_POSTFIX = 'Stage'
const STAGE_ACCESS_CONTROL_ALLOW_ORIGIN = '*'

const { domainName: stageDomainName } = new APIDomainStack(
app,
'ApiDomain-Stage',
{
stackName: 'ApiDomain-Stage',
domainName: STAGE_API_DOMAIN_NAME,
domainCertificateARN: STAGE_API_DOMAIN_CERTIFICATE_ARN,
env: { region: ENV_REGION },
},
)

new FoodHowStack(
app,
'FoodHow-Stage',
{
stackName: 'FoodHow-Stage',
apiDomainName: stageDomainName,
resourcePostFix: STAGE_RESOURCE_POSTFIX,
accessControlAllowOrigin: STAGE_ACCESS_CONTROL_ALLOW_ORIGIN,
userPool: blankFamilyUserPool,
env: { region: ENV_REGION },
},
)
new HalfsiesStack(
app,
'Halfsies-Stage',
{
stackName: 'Halfsies-Stage',
customDomainCertificateARN: 'arn:aws:acm:us-east-1:382713793519:certificate/74c434a7-2ad0-4477-8a50-e3b41218f85a',
customDomainName: 'stage-api.troyblank.com',
resourcePostFix: 'Stage',
accessControlAllowOrigin: '*',
apiDomainName: stageDomainName,
resourcePostFix: STAGE_RESOURCE_POSTFIX,
accessControlAllowOrigin: STAGE_ACCESS_CONTROL_ALLOW_ORIGIN,
userPool: blankFamilyUserPool,
env: {
region: 'us-east-2',
},
env: { region: ENV_REGION },
},
)
16 changes: 16 additions & 0 deletions src/constants/foodHow/shoppingListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const SHOPPING_ITEM_TYPE = [
'meat',
'perishable',
'produce',
'frozen',
'imperishable',
'spice',
'uncommon',
] as const

export const SHOPPING_ITEM_STORE = [
'Jewel-Osco',
'Pete\'s',
'Wild Fork',
'Unspecified',
] as const
1 change: 0 additions & 1 deletion src/constants/index.ts

This file was deleted.

137 changes: 137 additions & 0 deletions src/lambdas/foodHow/createShoppingListItem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Chance } from 'chance'
import { RESPONSE_CODE_OK, RESPONSE_CODE_SERVER_ERROR } from '../../constants/responseCodes'
import { mockApiGatewayProxyEvent } from '../../mocks/apiGatewayProxyEvent'
import { mockShoppingListItem } from '../../mocks/shoppingListItem'
// import { sortLogs } from '../../utils/halfsies/log'
import { saveShoppingListItem } from './utils/shoppingList'
import { handler } from './createShoppingListItem'

jest.mock('./utils/shoppingList')

describe('Lambda - Create Shopping List Item', () => {
const chance = new Chance()

beforeEach(() => {
process.env.shoppingListTableName = chance.word({ syllables: 4 })
})

it('Should create a shopping list item.', async () => {
jest.mocked(saveShoppingListItem).mockResolvedValue({
isError: false,
})

const mockedShoppingListItem = mockShoppingListItem()
const expectedBody = { newShoppingListItem: mockedShoppingListItem }

const result = await handler(mockApiGatewayProxyEvent(
mockedShoppingListItem,
) as any)

expect(result).toStrictEqual({
body: JSON.stringify(expectedBody),
headers: {
'Access-Control-Allow-Origin': '',
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_OK,
})
})

it('Should return an error if there is critical problem when saving a shopping list item.', async () => {
const errorMessage = chance.sentence()
jest.mocked(saveShoppingListItem).mockRejectedValue(errorMessage)

const mockedShoppingListItem = mockShoppingListItem()
const expectedBody = {
errorMessage,
}

const result = await handler(mockApiGatewayProxyEvent(
mockedShoppingListItem,
) as any)

expect(result).toStrictEqual({
body: JSON.stringify(expectedBody),
headers: {
'Access-Control-Allow-Origin': '',
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_SERVER_ERROR,
})
})

it('Should return an error if there is problem when saving a shopping list item.', async () => {
const errorMessage = chance.sentence()
jest.mocked(saveShoppingListItem).mockResolvedValue({
isError: true,
errorMessage,
})

const mockedShoppingListItem = mockShoppingListItem()
const expectedBody = {
errorMessage,
}

const result = await handler(mockApiGatewayProxyEvent(
mockedShoppingListItem,
) as any)

expect(result).toStrictEqual({
body: JSON.stringify(expectedBody),
headers: {
'Access-Control-Allow-Origin': '',
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_SERVER_ERROR,
})
})

it('Should return an error when saving a shopping list item if the shopping list item is invalid.', async () => {
jest.mocked(saveShoppingListItem).mockResolvedValue({
isError: false,
})

const mockedShoppingListItem = mockShoppingListItem({
name: undefined,
})
const expectedBody = {
message: 'No valid shopping list item given to create.',
}

const result = await handler(mockApiGatewayProxyEvent(
mockedShoppingListItem,
) as any)

expect(result).toStrictEqual({
body: JSON.stringify(expectedBody),
headers: {
'Access-Control-Allow-Origin': '',
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_SERVER_ERROR,
})
})

it('Should return an error when saving without a body.', async () => {
jest.mocked(saveShoppingListItem).mockResolvedValue({
isError: false,
})

const expectedBody = {
message: 'No valid shopping list item given to create.',
}

const result = await handler(mockApiGatewayProxyEvent({}, {
body: undefined,
}) as any)

expect(result).toStrictEqual({
body: JSON.stringify(expectedBody),
headers: {
'Access-Control-Allow-Origin': '',
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_SERVER_ERROR,
})
})
})
45 changes: 45 additions & 0 deletions src/lambdas/foodHow/createShoppingListItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { type APIGatewayProxyEvent, type APIGatewayProxyResult } from 'aws-lambda'
import { type DatabaseResponse, type ShoppingListItem } from '../../types'
import { RESPONSE_CODE_OK, RESPONSE_CODE_SERVER_ERROR } from '../../constants/responseCodes'
import { isAShoppingListItem } from '../../utils/foodHow/shoppingListItem'
import { getUserName } from '../utils/user'
import { saveShoppingListItem } from './utils/shoppingList'

export const handler = ({ body, headers }: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => new Promise((resolve) => {
const { accessControlAllowOrigin = '' } = process.env
const newShoppingListItem: ShoppingListItem = JSON.parse(body || '{}')
const user: string = getUserName(headers)
let message: string | undefined = 'Successfully created a shopping list item.'

const result: APIGatewayProxyResult = {
body: JSON.stringify({ message }),
headers: {
'Access-Control-Allow-Origin': accessControlAllowOrigin,
'Content-Type': 'application/json',
},
statusCode: RESPONSE_CODE_OK,
}

if (!isAShoppingListItem(newShoppingListItem)) {
result.statusCode = RESPONSE_CODE_SERVER_ERROR
result.body = JSON.stringify({ message: 'No valid shopping list item given to create.' })

return resolve(result)
}

saveShoppingListItem(newShoppingListItem, user).then(({ isError, errorMessage }: DatabaseResponse) => {
if (isError) {
result.statusCode = RESPONSE_CODE_SERVER_ERROR
result.body = JSON.stringify({ errorMessage: errorMessage })
} else {
result.body = JSON.stringify({ newShoppingListItem })
}

resolve(result)
}).catch((error) => {
result.statusCode = RESPONSE_CODE_SERVER_ERROR
result.body = JSON.stringify({ errorMessage: error })

resolve(result)
})
})
32 changes: 32 additions & 0 deletions src/lambdas/foodHow/utils/shoppingList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'
import { getErrorMessage } from '../../../utils/error'
import { type DatabaseResponse } from '../../../types'
import { type ShoppingListItem } from '../../../types/lambdas/foodHow'

export const saveShoppingListItem = async (item: ShoppingListItem, userName: string): Promise<DatabaseResponse> => {
const dynamoDbClient = new DynamoDBClient()
const dynamoDocumentClient = DynamoDBDocumentClient.from(dynamoDbClient)
const { shoppingListTableName } = process.env
const now: Date = new Date()

try {
await dynamoDocumentClient.send(new PutCommand({
TableName: shoppingListTableName,
Item: {
...item,
id: now.getTime(),
user: userName,
},
}))

return {
isError: false,
}
} catch (error: unknown) {
return {
isError: true,
errorMessage: getErrorMessage(error),
}
}
}
Loading