diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 789ef326..5a923049 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,6 +16,7 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} GITHUB_SHA: ${{ github.sha }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + CLOUDFLARE_WORKER_HEALTH_URL: ${{ secrets.CLOUDFLARE_WORKER_HEALTH_URL }} steps: - uses: actions/checkout@v2 @@ -35,6 +36,9 @@ jobs: - name: Run unit tests run: npm run test-unit + + - name: Run integration tests + run: npm run test-integ - name: Run prompt tests run: npm run test -- --ci=github --model=gpt-3.5-turbo diff --git a/jest.config.js b/jest.config.js index b413e106..c7e09378 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,4 +2,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', + testRegex: '/src/.*.test.ts', }; \ No newline at end of file diff --git a/package.json b/package.json index 6c0552bb..b4209d60 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "ts-node ./src/index.ts review", "test": "ts-node ./src/index.ts test", "test-unit": "dotenv -e .env jest", + "test-integ": "jest -c services/tests/jest.config.js", "build": "node utils/build.js", "postbuild": "node utils/shebang.js && chmod +x ./dist/index.js" }, diff --git a/services/tests/emailWorker.integ.test.ts b/services/tests/emailWorker.integ.test.ts new file mode 100644 index 00000000..d9f4b9b1 --- /dev/null +++ b/services/tests/emailWorker.integ.test.ts @@ -0,0 +1,13 @@ +import * as dotenv from 'dotenv'; + +dotenv.config({ path: './services/tests/.env' }); + +describe("CloudFlare email worker health test", () => { + test("Should return 200 after successfully hitting the CloudFlare health endpoint", async() => { + const res = await fetch(process.env.CLOUDFLARE_WORKER_HEALTH_URL ?? "", { + method: "GET", + }); + + expect(res.status).toEqual(200); + }); +}); \ No newline at end of file diff --git a/services/tests/jest.config.js b/services/tests/jest.config.js new file mode 100644 index 00000000..cc95a72c --- /dev/null +++ b/services/tests/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testRegex: '/services/tests/.*.integ.test.ts', +}; \ No newline at end of file diff --git a/services/web-app/README.md b/services/web-app/README.md index 7d4a3865..486d900e 100644 --- a/services/web-app/README.md +++ b/services/web-app/README.md @@ -26,9 +26,11 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the Involves starting our SST dev environment. ```bash -npx sst dev +npx sst dev --stage ``` +Remember to use the same stage for both your SST and CDK stack. + Next in a separate terminal: ```bash diff --git a/services/web-app/functions/add-user/index.ts b/services/web-app/functions/add-user/index.ts new file mode 100644 index 00000000..0b7e7d13 --- /dev/null +++ b/services/web-app/functions/add-user/index.ts @@ -0,0 +1,89 @@ +import { DynamoDBStreamEvent } from "aws-lambda"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import fetch from "node-fetch"; +import { getVariableFromSSM } from "../../../core/functions/helpers/getVariable"; +import { Config } from "sst/node/config"; + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +const postEmail = async(email: string, name: string) => { + const url = await getVariableFromSSM(process.env.CLOUDFLARE_WORKER_URL_NAME) ?? ""; + return await fetch(url.concat("api/email"), { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": await getVariableFromSSM( + process.env.CLOUDFLARE_WORKER_TOKEN_NAME ?? "" + ), + }, + body: JSON.stringify({ + "to": {"email": email, "name": name }, + "from": { "email": "noreply@oriontools.ai", "name": "Matt from Orion Tools" }, + "subject": "Welcome to Code Review GPT", + "html": "

Thanks for signing up for Orion tools. We aim to make the best AI powered dev tools. We hope you enjoy using our code review product.
Here is a link to the repo, give it a star. Here is a link to our slack community.

" + }), + }); +}; + +export const main = async (event: DynamoDBStreamEvent) => { + if (event.Records == null) { + return Promise.resolve({ + statusCode: 400, + body: "The request does not contain a any dynamodb records as expected.", + }); + } + + try { + if (event.Records[0].dynamodb?.NewImage) { + const record = event.Records[0].dynamodb?.NewImage; + const userId = record.id['S']; + const name = record.name['S']; + const email = record.email['S']; + const pictureUrl = record.image['S']; + + if (userId === undefined || name === undefined || email === undefined || pictureUrl === undefined) { + return Promise.resolve({ + statusCode: 400, + body: "The request record does not contain the expected data.", + }); + } + + const res = await postEmail(email, name); + if (!res.ok) { + console.error("Failed to send welcome email due to this status code: ", res.status); + } + + const command = new PutCommand({ + TableName: `${process.env.SST_STAGE}-crgpt-data`, + Item: { + PK: `USERID#${userId}`, + SK: "ROOT", + userId: userId, + name: name, + email: email, + pictureUrl: pictureUrl, + }, + }); + await docClient.send(command); + + return Promise.resolve({ + statusCode: 200, + body: "Successfully added new user to the user db", + }); + } else { + return Promise.resolve({ + statusCode: 400, + body: "Dynamodb record did not contain any new users", + }); + } + } catch (err) { + console.error(err); + + return Promise.resolve({ + statusCode: 500, + body: "Error when updating user.", + }); + } +}; \ No newline at end of file diff --git a/services/web-app/package-lock.json b/services/web-app/package-lock.json index 6ca6c19e..41e9e34c 100644 --- a/services/web-app/package-lock.json +++ b/services/web-app/package-lock.json @@ -29,6 +29,8 @@ "typescript": "5.1.6" }, "devDependencies": { + "@types/aws-lambda": "^8.10.119", + "@types/node-fetch": "^2.6.4", "aws-cdk-lib": "2.84.0", "constructs": "10.1.156", "sst": "^2.22.10" @@ -4040,6 +4042,12 @@ "tslib": "^2.1.0" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.119", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.119.tgz", + "integrity": "sha512-Vqm22aZrCvCd6I5g1SvpW151jfqwTzEZ7XJ3yZ6xaZG31nUEOEyzzVImjRcsN8Wi/QyPxId/x8GTtgIbsy8kEw==", + "dev": true + }, "node_modules/@types/debug": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", @@ -4079,6 +4087,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz", "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", diff --git a/services/web-app/package.json b/services/web-app/package.json index 3d6a0e7e..5ef67287 100644 --- a/services/web-app/package.json +++ b/services/web-app/package.json @@ -31,6 +31,8 @@ "typescript": "5.1.6" }, "devDependencies": { + "@types/aws-lambda": "^8.10.119", + "@types/node-fetch": "^2.6.4", "aws-cdk-lib": "2.84.0", "constructs": "10.1.156", "sst": "^2.22.10" diff --git a/services/web-app/src/functions/add-user/index.ts b/services/web-app/src/functions/add-user/index.ts deleted file mode 100644 index 6bbd6f0f..00000000 --- a/services/web-app/src/functions/add-user/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { DynamoDBStreamEvent } from "aws-lambda"; -import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; - -const client = new DynamoDBClient({}); -const docClient = DynamoDBDocumentClient.from(client); - -export const main = async (event: DynamoDBStreamEvent) => { - if (event.Records == null) { - return Promise.resolve({ - statusCode: 400, - body: "The request does not contain a any dynamodb records as expected.", - }); - } - - try { - for (const record of event.Records) { - const userId = record.dynamodb?.NewImage.id['S']; - const name = record.dynamodb?.NewImage.name['S']; - const email = record.dynamodb?.NewImage.email['S']; - const pictureUrl = record.dynamodb?.NewImage.image['S']; - - if (userId === undefined || name === undefined || email === undefined || pictureUrl === undefined) { - return Promise.resolve({ - statusCode: 400, - body: "The request record does not contain the expected data.", - }); - } - - const command = new PutCommand({ - TableName: `${process.env.SST_STAGE}-crgpt-data`, - Item: { - PK: `EMAIL#${email}`, - SK: "ROOT", - userId: userId, - name: name, - email: email, - pictureUrl: pictureUrl, - }, - }); - await docClient.send(command); - - return Promise.resolve({ - statusCode: 200, - body: "User added successfully.", - }); - } - - return Promise.resolve({ - statusCode: 400, - body: "Dynamodb record did not contain any new users", - }); - } catch (err) { - console.error(err); - - return Promise.resolve({ - statusCode: 500, - body: "Error when updating user.", - }); - } -}; diff --git a/services/web-app/sst.config.ts b/services/web-app/sst.config.ts index f045d10f..97dbe6ba 100644 --- a/services/web-app/sst.config.ts +++ b/services/web-app/sst.config.ts @@ -1,7 +1,6 @@ import { Tags } from "aws-cdk-lib"; import { SSTConfig } from "sst"; import { Config, NextjsSite, Table } from "sst/constructs"; -import { getStage } from '../core/helpers'; export default { config(_input) { @@ -34,11 +33,12 @@ export default { consumers: { consumer1: { function: { - handler: "src/functions/add-user/index.main", - permissions: ["dynamodb"], + handler: "/functions/add-user/index.main", + permissions: ["dynamodb", "ssm"], environment: { - STAGE: getStage(), - } + CLOUDFLARE_WORKER_TOKEN_NAME: "CLOUDFLARE_WORKER_TOKEN", + CLOUDFLARE_WORKER_URL_NAME: "CLOUDFLARE_WORKER_URL", + }, }, filters: [ {