Skip to content

Commit b012a30

Browse files
authored
tests: increases testability and coverage for api server (#647)
* fix: actually adds valid responses for the api Signed-off-by: Anthony D. Mays <[email protected]> * fix: corrects file path to json file. * tests: adds basic e2e tests for server Signed-off-by: Anthony D. Mays <[email protected]> * tests: adds jest config Signed-off-by: Anthony D. Mays <[email protected]> * tests: refactored data access and completed tests Signed-off-by: Anthony D. Mays <[email protected]> * chore: update jest config * chore: properly configure types library * chore: add types library Signed-off-by: Anthony D. Mays <[email protected]> * chore: add prettier fix --------- Signed-off-by: Anthony D. Mays <[email protected]>
1 parent fc9313d commit b012a30

11 files changed

+5593
-1071
lines changed

lesson_27/api/jest.config.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} **/
2+
export default {
3+
testEnvironment: 'node',
4+
transform: {
5+
'^.+.tsx?$': ['ts-jest', {useESM: true}],
6+
},
7+
moduleNameMapper: {
8+
'^(\\.\\.?\\/.+)\\.js$': '$1',
9+
},
10+
extensionsToTreatAsEsm: ['.ts'],
11+
};

lesson_27/api/package-lock.json

+5,357-1,000
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lesson_27/api/package.json

+20-8
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,39 @@
22
"name": "codedifferently-web",
33
"version": "1.0.0",
44
"description": "",
5-
"main": "server.js",
5+
"main": "index.js",
6+
"type": "module",
67
"scripts": {
78
"build:deps": "cd ../types && npm install && npm run build",
89
"build": "npm run build:deps && npx tsc",
9-
"start": "npm run build:deps && node dist/server.js",
10-
"dev": "npm run build:deps && nodemon src/server.ts --quiet"
10+
"start": "npm run build:deps && tsx src/index.ts",
11+
"dev": "npm run build:deps && tsx src/index.ts",
12+
"test": "jest",
13+
"test:watch": "jest --watch",
14+
"fix": "prettier --write ."
1115
},
1216
"author": "",
1317
"license": "ISC",
1418
"devDependencies": {
1519
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
16-
"nodemon": "^3.1.0",
17-
"prettier": "3.2.5",
20+
"@types/jest": "^29.5.14",
21+
"@types/supertest": "^6.0.2",
22+
"jest": "^29.7.0",
23+
"nodemon": "^3.1.7",
24+
"prettier": "^3.4.1",
25+
"supertest": "^7.0.0",
26+
"ts-jest": "^29.2.5",
1827
"ts-node-dev": "^2.0.0",
19-
"typescript": "^5.4.4"
28+
"tsx": "^4.19.2",
29+
"typescript": "^5.7.2"
2030
},
2131
"dependencies": {
32+
"@code-differently/types": "file:../types",
2233
"@types/cors": "^2.8.17",
2334
"@types/express": "^4.17.21",
2435
"@types/node": "^20.12.5",
2536
"cors": "^2.8.5",
26-
"express": "^4.19.2"
37+
"express": "^4.21.1",
38+
"lowdb": "^7.0.1"
2739
}
28-
}
40+
}

lesson_27/api/src/data/programs.json

+16-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
[
1+
{
2+
"programs": [
23
{
3-
"id": "28348204-a665-48ea-a436-d962bca07e2d",
4-
"title": "1000 Kids Coding",
5-
"description": "The Code Differently 1000 Kids Coding program was created to expose New Castle County students to computing and programming. The 1000 Kids Coding courses are designed for all experience levels, no experience required."
4+
"id": "28348204-a665-48ea-a436-d962bca07e2d",
5+
"title": "1000 Kids Coding",
6+
"description": "The Code Differently 1000 Kids Coding program was created to expose New Castle County students to computing and programming. The 1000 Kids Coding courses are designed for all experience levels, no experience required."
67
},
78
{
8-
"id": "66521c31-f37d-47b2-9f5e-6ddeb8bc218a",
9-
"title": "Return Ready",
10-
"description": "The Code Differently Workforce Training Initiatives were created to help individuals underrepresented in tech reinvent their skills to align with the changing workforce market. If you are ready to start your tech journey, join our talent community today."
9+
"id": "66521c31-f37d-47b2-9f5e-6ddeb8bc218a",
10+
"title": "Return Ready",
11+
"description": "The Code Differently Workforce Training Initiatives were created to help individuals underrepresented in tech reinvent their skills to align with the changing workforce market. If you are ready to start your tech journey, join our talent community today."
1112
},
1213
{
13-
"id": "516190b1-89cf-4e75-858a-11e728034022",
14-
"title": "Pipeline DevShops",
15-
"description": "Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills."
14+
"id": "516190b1-89cf-4e75-858a-11e728034022",
15+
"title": "Pipeline DevShops",
16+
"description": "Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills."
1617
},
1718
{
18-
"id": "a06f970a-03b7-4cbb-9efd-f4e99029a456",
19-
"title": "Platform Programs",
20-
"description": "Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce."
19+
"id": "a06f970a-03b7-4cbb-9efd-f4e99029a456",
20+
"title": "Platform Programs",
21+
"description": "Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce."
2122
}
22-
]
23+
]
24+
}

lesson_27/api/src/db.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {Program} from '@code-differently/types';
2+
import {randomUUID} from 'crypto';
3+
import {Low} from 'lowdb';
4+
import {JSONFilePreset} from 'lowdb/node';
5+
6+
export interface Db {
7+
getPrograms: () => Promise<Program[]>;
8+
getProgram: (id: string) => Promise<Program | null>;
9+
addProgram: (program: Program) => Promise<void>;
10+
}
11+
12+
export interface DbData {
13+
programs: Program[];
14+
}
15+
16+
export class DbImpl implements Db {
17+
private readonly db: Promise<Low<DbData>>;
18+
19+
constructor(filePath: string) {
20+
this.db = this.loadDb(filePath);
21+
}
22+
23+
async loadDb(filePath: string): Promise<Low<DbData>> {
24+
const defaultData: DbData = {programs: []};
25+
return await JSONFilePreset(filePath, defaultData);
26+
}
27+
28+
async getPrograms(): Promise<Program[]> {
29+
const db = await this.db;
30+
return db.data.programs;
31+
}
32+
33+
async getProgram(id: string): Promise<Program | null> {
34+
const db = await this.db;
35+
return db.data.programs.find(p => p.id === id) || null;
36+
}
37+
38+
async addProgram(program: Program): Promise<void> {
39+
const db = await this.db;
40+
if (!program.id) {
41+
program.id = randomUUID();
42+
}
43+
db.data.programs.push(program);
44+
await db.write();
45+
}
46+
}

lesson_27/api/src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {DbImpl} from './db.js';
2+
import {createServer} from './server.js';
3+
import path from 'path';
4+
5+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
6+
const PROGRAMS_FILE = path.resolve(__dirname, './data/programs.json');
7+
8+
const db = new DbImpl(PROGRAMS_FILE);
9+
createServer(db);

lesson_27/api/src/server.e2e.spec.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Db} from './db.js';
2+
import {createServer} from './server.js';
3+
import {Express} from 'express';
4+
import request from 'supertest';
5+
6+
describe('Server (e2e)', () => {
7+
let mockDb: jest.MockedObjectDeep<Db> = jest.mocked({
8+
getPrograms: jest.fn(),
9+
getProgram: jest.fn(),
10+
addProgram: jest.fn(),
11+
});
12+
let app: Express = createServer(mockDb);
13+
14+
it('/programs (GET)', async () => {
15+
// Arrange
16+
const programs = [
17+
{
18+
id: '12345',
19+
title: 'Pipeline DevShops',
20+
description:
21+
'Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills.',
22+
},
23+
];
24+
mockDb.getPrograms.mockResolvedValue(programs);
25+
26+
// Act
27+
const result = request(app).get('/programs');
28+
29+
// Assert
30+
await result.expect(200).then(res => {
31+
expect(res.body).toEqual(programs);
32+
});
33+
});
34+
35+
it('/programs/:id (GET)', async () => {
36+
// Arrange
37+
const program = {
38+
id: 'a06f970a-03b7-4cbb-9efd-f4e99029a456',
39+
title: 'Platform Programs',
40+
description:
41+
'Platform programs are designed for high school graduates, college students, career changers, or professionals looking to develop the technology job readiness skills for today’s workforce.',
42+
};
43+
mockDb.getProgram.mockResolvedValue(program);
44+
45+
// Act
46+
const result = request(app).get(
47+
'/programs/516190b1-89cf-4e75-858a-11e728034022'
48+
);
49+
50+
// Assert
51+
await result.expect(200).then(res => {
52+
expect(res.body).toEqual(program);
53+
});
54+
});
55+
56+
it('/programs (POST)', async () => {
57+
// Arrange
58+
const program = {
59+
title: 'Pipeline DevShops',
60+
description:
61+
'Pipeline DevShop is a youth work-based learning program. Youth participants experience working in a real software development environment while sharpening their technology and soft skills.',
62+
};
63+
mockDb.addProgram.mockResolvedValue();
64+
65+
// Act
66+
const result = request(app).post('/programs').send(program);
67+
68+
// Assert
69+
await result.expect(201).then(() => {
70+
expect(mockDb.addProgram).toHaveBeenCalledWith(program);
71+
});
72+
});
73+
});

lesson_27/api/src/server.ts

+45-47
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,63 @@
1-
import programs from './data/programs.json';
1+
import {Db} from './db.js';
2+
import {Program} from '@code-differently/types';
23
import cors from 'cors';
3-
import {randomUUID} from 'crypto';
44
import express, {Express, Request, Response} from 'express';
5-
import fs from 'fs';
6-
import path from 'path';
75

8-
import {Program} from '../../types';
9-
10-
const PROGRAMS_FILE = path.resolve(__dirname, './data/programs.json');
116
const UUID_PATTERN =
127
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
138

14-
const app: Express = express();
15-
16-
app.use(express.static('public'));
17-
app.use(express.json());
18-
app.use(express.urlencoded({extended: true}));
19-
app.use(cors());
9+
export const createServer = (db: Db): Express => {
10+
const app: Express = express();
2011

21-
app.get('/programs/:id', async (req: Request, res: Response<Program>) => {
22-
if (!isUuidValid(req.params.id)) {
23-
res.status(400).send();
24-
return;
25-
}
26-
const program = programs.find(p => p.id === req.params.id);
12+
app.use(express.static('public'));
13+
app.use(express.json());
14+
app.use(express.urlencoded({extended: true}));
15+
app.use(cors());
2716

28-
if (!program) {
29-
res.status(404).send();
30-
return;
31-
}
17+
app.get('/programs/:id', async (req: Request, res: Response<Program>) => {
18+
if (!isUuidValid(req.params.id)) {
19+
res.status(400).send();
20+
return;
21+
}
22+
const program = await db.getProgram(req.params.id);
3223

33-
res.status(200).send(program);
34-
});
24+
if (!program) {
25+
res.status(404).send();
26+
return;
27+
}
3528

36-
function isUuidValid(uuid: string): boolean {
37-
return !!uuid && !!uuid.match(UUID_PATTERN);
38-
}
29+
res.status(200).send(program);
30+
});
3931

40-
app.get('/programs', async (req: Request, res: Response<Program[]>) => {
41-
// Send the raw data back to the client as JSON.
42-
res.status(200).send(programs);
43-
});
32+
function isUuidValid(uuid: string): boolean {
33+
return !!uuid && !!uuid.match(UUID_PATTERN);
34+
}
4435

45-
app.post('/programs', async (req: Request<Partial<Program>>, res: Response) => {
46-
const newProgram = req.body;
47-
programs.push({id: randomUUID(), ...newProgram});
48-
fs.writeFile(
49-
PROGRAMS_FILE,
50-
JSON.stringify(programs, null, 2),
51-
(err: unknown) => {
52-
if (err) {
53-
res.status(500).send({error: 'Failed to write to file.'});
36+
app.get('/programs', async (req: Request, res: Response<Program[]>) => {
37+
// Send the raw data back to the client as JSON.
38+
const programs = await db.getPrograms();
39+
res.status(200).send(programs);
40+
});
41+
42+
app.post(
43+
'/programs',
44+
async (req: Request<Partial<Program>>, res: Response) => {
45+
const newProgram = req.body;
46+
try {
47+
db.addProgram(newProgram as Program);
48+
} catch (error: unknown) {
49+
res.status(500).send({error: 'Failed to add program.'});
5450
return;
5551
}
56-
console.log(`Updated ${PROGRAMS_FILE}`);
52+
console.log('Added new program');
5753
res.status(201).send();
5854
}
5955
);
60-
});
6156

62-
const port = 4000;
63-
app.listen(port, () => {
64-
console.log(`Server is running on http://localhost:${port}`);
65-
});
57+
const port = process.env.port || 4000;
58+
app.listen(port, () => {
59+
console.log(`Server is running on http://localhost:${port}`);
60+
});
61+
62+
return app;
63+
};

lesson_27/api/tsconfig.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"compilerOptions": {
33
"rootDir": "./src",
44
"outDir": "./dist",
5-
"target": "es2016",
6-
"module": "commonjs",
5+
"target": "ES2020",
6+
"module": "NodeNext",
77
"strict": true,
88
"esModuleInterop": true,
99
"skipLibCheck": true,

lesson_27/template/package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lesson_27/template/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"preview": "npm run build:deps && vite preview"
1313
},
1414
"dependencies": {
15+
"@code-differently/types": "file:../types",
1516
"@tanstack/react-query": "^5.29.2",
1617
"react": "^18.2.0",
1718
"react-dom": "^18.2.0",

0 commit comments

Comments
 (0)