Skip to content

Commit

Permalink
[Closes #9] Setup Github Actions for CI (#35)
Browse files Browse the repository at this point in the history
* testing github actions yml

* adding comments for actions yml file

* renamed yml for test

* testing format actions

* testing prettier push

* on push test

* another test on push

* another test on push

* testing check format

* testing check format again

* added lint

* correcting lint run command

* Fix lint errors, allow unused args prefixed with _

* Run tests

* Tweak naming, remove unnecessary error check

* Switch to containerized job, launch a postgres container, generate Prisma client

* Configure and set up database

* Fix db url

* Tweak naming

* Restore setup-node action for dependency caching, add --force to prisma command

* Add name to step

* Temporarily disable test run while license server is down

---------

Co-authored-by: Francis Li <[email protected]>
  • Loading branch information
rooiss and francisli authored Feb 24, 2024
1 parent 9cf8f45 commit bf8ed83
Show file tree
Hide file tree
Showing 19 changed files with 125 additions and 55 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI
on: [push]
jobs:
ci:
name: CI
runs-on: ubuntu-latest
container: node:20.11.0-bookworm
services:
db:
image: postgres:15.5
env:
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Set up npm cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Check Formatting
run: npm run format:check
- name: Run ESLint
run: npm run lint --workspaces
- name: Initialize database, generate Prisma client
run: cd server; npx prisma migrate reset --force
env:
DATABASE_URL: postgresql://postgres@db/app
- name: Run Tests
if: ${{ false }}
run: npm test
env:
DATABASE_URL: postgresql://postgres@db/app
1 change: 1 addition & 0 deletions client/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
Expand Down
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"test": ""
},
"dependencies": {
"@mantine/core": "^7.4.2",
Expand Down
2 changes: 1 addition & 1 deletion client/src/stories/Page/Page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Page = () => {
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose
such data from the "args" of child component stories
such data from the &quot;args&quot; of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"scripts": {
"start": "nf start -j Procfile.dev",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "npm run test --workspaces",
"format": "prettier --write --ignore-unknown .",
"format:check": "prettier --check --ignore-unknown .",
"lint": "npm run lint --workspaces"
Expand Down
4 changes: 3 additions & 1 deletion server/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};
12 changes: 6 additions & 6 deletions server/errors/LicenseErrors.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
export class LicenseMatchError extends Error {
constructor(statusCode, message) {
super(message)
this.name = 'LicenseMatchError'
this.statusCode = statusCode
super(message);
this.name = 'LicenseMatchError';
this.statusCode = statusCode;
}
}

export class LicenseWebsiteError extends Error {
constructor(statusCode, message) {
super(message)
this.name = 'LicenseWebsiteError'
this.statusCode = statusCode
super(message);
this.name = 'LicenseWebsiteError';
this.statusCode = statusCode;
}
}
12 changes: 7 additions & 5 deletions server/helpers/fetchLicenseVerificationForm.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* Generates the necessary sessionCookie and formData object required to make a
* POST request to California's EMS personnel registry website
*
*
* @param {string} website California's EMS personnel registry website
* @param {string} license an EMS personnel's license
* @returns a Promise, once resolved, containing a sessionCookie string and formData object
* @returns a Promise, once resolved, containing a sessionCookie string and formData object
* necessary to make a POST request on the registry website
*/

Expand All @@ -13,14 +13,16 @@ export default async function fetchLicenseVerificationForm(website, license) {
formData.append('t_web_lookup__license_no', license);

const response = await fetch(website, {
method: 'GET'
method: 'GET',
});
const sessionCookie = response.headers.get('set-cookie').split(';')[0];

const html = await response.text(); // Need to await converting the fetch response into HTML

const viewstateRegex = /<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="([^"]+)" \/>/;
const eventValidationRegex = /<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="([^"]+)" \/>/;
const viewstateRegex =
/<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="([^"]+)" \/>/;
const eventValidationRegex =
/<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="([^"]+)" \/>/;

const viewstateMatch = html.match(viewstateRegex);
const eventValidationMatch = html.match(eventValidationRegex);
Expand Down
24 changes: 15 additions & 9 deletions server/helpers/fetchLicenseVerificationResults.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,34 @@ import { LicenseMatchError } from '../errors/LicenseErrors.js';

/**
* Makes a POST request to California's EMS personnel registry website
*
*
* @param {string} website California's EMS personnel registry website
* @param {object} formData the necessary form data required to make a POST request for the registry website
* @param {string} sessionCookie a valid session cookie to authenticate the POST request
* @returns personnel info that matches the license number from the formData
* @throws a LicenseMatchError if there are not matching personnels or an Error if there were issues accessing the registry
*/
export default async function fetchLicenseVerificationResults(website, formData, sessionCookie) {
export default async function fetchLicenseVerificationResults(
website,
formData,
sessionCookie,
) {
const response = await fetch(website, {
method: 'POST',
body: formData,
headers: {
cookie: sessionCookie // Need a valid session cookie to search EMS website
}
cookie: sessionCookie, // Need a valid session cookie to search EMS website
},
});
const html = await response.text();

const personnelResultsRegex = /<a[^>]*?>(.*?)<\/a><\/td><td><span>(.*?)<\/span><\/td><td><span>(.*?)<\/span><\/td><td><span>(.*?)<\/span>/;
const noResultsRegex = /<table[^>]*id="datagrid_results"[^>]*>\s*(?!.*?datagrid_results__ctl3_result).*?<\/table>/s;
const personnelResultsRegex =
/<a[^>]*?>(.*?)<\/a><\/td><td><span>(.*?)<\/span><\/td><td><span>(.*?)<\/span><\/td><td><span>(.*?)<\/span>/;
const noResultsRegex =
/<table[^>]*id="datagrid_results"[^>]*>\s*(?!.*?datagrid_results__ctl3_result).*?<\/table>/s;

const matchingPersonnel = html.match(personnelResultsRegex); // Use the personnelResultsRegex pattern to extract the first table row from the HTML
const noResults = html.match(noResultsRegex) // Use the noResultsRegex to determine if there are no personnel in results table
const noResults = html.match(noResultsRegex); // Use the noResultsRegex to determine if there are no personnel in results table

if (matchingPersonnel) {
const [, name, licenseType, status, licenseNumber] = matchingPersonnel;
Expand All @@ -32,6 +38,6 @@ export default async function fetchLicenseVerificationResults(website, formData,
} else if (noResults && !matchingPersonnel) {
throw new LicenseMatchError(404, 'No match.');
} else {
throw new Error;
throw new Error();
}
}
}
31 changes: 24 additions & 7 deletions server/helpers/verifyLicense.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import fetchLicenseVerificationForm from './fetchLicenseVerificationForm.js';
import fetchLicenseVerificationResults from './fetchLicenseVerificationResults.js';
import { LicenseMatchError, LicenseWebsiteError } from '../errors/LicenseErrors.js';
import {
LicenseMatchError,
LicenseWebsiteError,
} from '../errors/LicenseErrors.js';

const EMS_VERIFICATION_WEBSITE = 'https://emsverification.emsa.ca.gov/Verification/Search.aspx';
const EMS_VERIFICATION_WEBSITE =
'https://emsverification.emsa.ca.gov/Verification/Search.aspx';

/**
* Search for an EMS personnel on California's EMS personnel registry website
Expand All @@ -18,25 +22,38 @@ export default async function verifyLicense(license) {
let sessionCookie;

try {
const res = await fetchLicenseVerificationForm(EMS_VERIFICATION_WEBSITE, license);
const res = await fetchLicenseVerificationForm(
EMS_VERIFICATION_WEBSITE,
license,
);

formData = res.formData;
sessionCookie = res.sessionCookie;
} catch (err) {
console.error(err);
throw new LicenseWebsiteError(503, 'Unable to access verification website, try again later.');
throw new LicenseWebsiteError(
503,
'Unable to access verification website, try again later.',
);
}

if (sessionCookie) {
try {
const emsPersonnelInfo = await fetchLicenseVerificationResults(EMS_VERIFICATION_WEBSITE, formData, sessionCookie);
const emsPersonnelInfo = await fetchLicenseVerificationResults(
EMS_VERIFICATION_WEBSITE,
formData,
sessionCookie,
);
return emsPersonnelInfo;
} catch (err) {
if (err instanceof LicenseMatchError) {
throw err;
}
console.error(err);
throw new LicenseWebsiteError(503, 'Unable to access verification results, try again later.');
throw new LicenseWebsiteError(
503,
'Unable to access verification results, try again later.',
);
}
}
};
}
1 change: 0 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"dev": "npx prisma migrate dev && fastify start -w -l info -P app.js",
"migrate": "docker-compose exec server sh -c 'cd server && npx prisma migrate $@' sh",
"db": "docker-compose exec server sh -c 'cd server && npx prisma db $@' sh",
"pretest": "standard",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
},
"keywords": [],
Expand Down
2 changes: 1 addition & 1 deletion server/plugins/prismaPlugin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fp from 'fastify-plugin';
import { PrismaClient } from '@prisma/client';

const prismaPlugin = fp(async (server, options) => {
const prismaPlugin = fp(async (server, _options) => {
const prisma = new PrismaClient();

await prisma.$connect();
Expand Down
2 changes: 1 addition & 1 deletion server/plugins/support.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fp from 'fastify-plugin';
// the use of fastify-plugin is required to be able
// to export the decorators to the outer scope

export default fp(async function (fastify, opts) {
export default fp(async function (fastify, _opts) {
fastify.decorate('someSupport', function () {
return 'hugs';
});
Expand Down
11 changes: 6 additions & 5 deletions server/routes/api/v1/licenses/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
'use strict';

import verifyLicense from "../../../../helpers/verifyLicense.js";
import verifyLicense from '../../../../helpers/verifyLicense.js';

export default async function (fastify) {
fastify.get(
'/',
{
schema: {
querystring: {
license: { type: 'string' }
}
}
license: { type: 'string' },
},
},
},
async function (request) {
const { license } = request.query;
const results = await verifyLicense(license);
return results;
});
},
);
}
2 changes: 1 addition & 1 deletion server/routes/api/v1/users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// }

// User template for the routes
export default async function (fastify, opts) {
export default async function (fastify, _opts) {
fastify.post(
'/',
{
Expand Down
4 changes: 2 additions & 2 deletions server/routes/example/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default async function (fastify, opts) {
fastify.get('/', async function (request, reply) {
export default async function (fastify, _opts) {
fastify.get('/', async function (_request, _reply) {
return 'this is an example';
});
}
4 changes: 2 additions & 2 deletions server/routes/root.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default async function (fastify, opts) {
fastify.get('/', async function (request, reply) {
export default async function (fastify, _opts) {
fastify.get('/', async function (_request, _reply) {
return { root: true };
});
}
2 changes: 1 addition & 1 deletion server/test/plugins/support.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as assert from 'node:assert';
import Fastify from 'fastify';
import Support from '../../plugins/support.js';

test('support works standalone', async (t) => {
test('support works standalone', async (_t) => {
const fastify = Fastify();
fastify.register(Support);

Expand Down
19 changes: 9 additions & 10 deletions server/test/routes/licenses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,26 @@ describe('Testing License API route', () => {
const app = await build(t);

const validLicense = {
name: "Koo, Chih Ren Nicholas",
licenseType: "Paramedic",
status: "Active",
licenseNumber: "P39332"
}
name: 'Koo, Chih Ren Nicholas',
licenseType: 'Paramedic',
status: 'Active',
licenseNumber: 'P39332',
};

const res = await app.inject({
url: '/api/v1/licenses?license=P39332'
url: '/api/v1/licenses?license=P39332',
});
assert.deepStrictEqual(JSON.parse(res.payload), validLicense)

assert.deepStrictEqual(JSON.parse(res.payload), validLicense);
});

it('should return a 404 error for no matching results', async (t) => {
const app = await build(t);

const res = await app.inject({
url: '/api/v1/licenses?license=1'
url: '/api/v1/licenses?license=1',
});
const { message } = JSON.parse(res.body);
assert.equal(res.statusCode, 404);
assert.equal(message, 'No match.');
});
});
});

0 comments on commit bf8ed83

Please sign in to comment.