Skip to content

Commit c771b55

Browse files
authored
Merge pull request #6 from hoangsonww/feat/add-ci-cd-tests
feat(ci/cd): enhance CI/CD testing and deployment pipelines
2 parents c0fd89f + 99b8b06 commit c771b55

File tree

16 files changed

+3196
-1417
lines changed

16 files changed

+3196
-1417
lines changed

.github/workflows/ci.yml

Lines changed: 126 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,181 @@
11
name: CI/CD Pipeline for Budget Management API
2+
23
on:
34
push:
4-
branches: [master]
5+
branches:
6+
- master
57
pull_request:
6-
branches: [master]
8+
branches:
9+
- master
710

811
jobs:
912
formatting:
10-
name: "🔧 Formatting & Lint"
13+
name: 🔧 Install, Lint & Format
1114
runs-on: ubuntu-latest
1215
steps:
1316
- name: Checkout code
1417
uses: actions/checkout@v3
15-
continue-on-error: true
1618

17-
- name: Setup Node.js
19+
- name: Use Node.js 18
1820
uses: actions/setup-node@v3
1921
with:
2022
node-version: 18
21-
continue-on-error: true
2223

2324
- name: Install dependencies
2425
run: npm ci
25-
continue-on-error: true
2626

27-
- name: Prettier format
27+
- name: Run Prettier
2828
run: npm run format
29-
continue-on-error: true
30-
31-
- name: ESLint lint
32-
run: npm run lint
33-
continue-on-error: true
3429

35-
backend:
36-
name: "✅ Backend Build & Test"
30+
test:
31+
name: ✅ Run Jest Tests
3732
runs-on: ubuntu-latest
38-
needs: [formatting]
39-
if: ${{ always() }}
33+
needs: formatting
4034
steps:
4135
- name: Checkout code
4236
uses: actions/checkout@v3
43-
continue-on-error: true
4437

45-
- name: Setup Node.js
38+
- name: Use Node.js 18
4639
uses: actions/setup-node@v3
4740
with:
4841
node-version: 18
49-
continue-on-error: true
5042

51-
- name: Install backend deps
52-
run: npm --workspace backend ci
53-
continue-on-error: true
43+
- name: Install dependencies
44+
run: npm ci
5445

55-
- name: Backend format
56-
run: npm --workspace backend run format
57-
continue-on-error: true
46+
- name: Run Jest tests
47+
run: npm test
5848

59-
- name: Backend lint
60-
run: npm --workspace backend run lint
61-
continue-on-error: true
49+
mocha:
50+
name: 🧪 Run Mocha & Chai Tests
51+
runs-on: ubuntu-latest
52+
needs: test
53+
steps:
54+
- name: Checkout code
55+
uses: actions/checkout@v3
6256

63-
- name: Build backend
64-
run: npm --workspace backend run build
65-
continue-on-error: true
57+
- name: Use Node.js 18
58+
uses: actions/setup-node@v3
59+
with:
60+
node-version: 18
6661

67-
- name: Backend tests
68-
run: npm --workspace backend run test
69-
continue-on-error: true
62+
- name: Install dependencies
63+
run: npm ci
64+
65+
- name: Run Mocha & Chai tests
66+
run: npm run test:mocha
7067

7168
frontend:
72-
name: "🌐 Frontend Build & Test"
69+
name: 🌐 Frontend Test, Build & Artifact
7370
runs-on: ubuntu-latest
74-
needs: [backend]
75-
if: ${{ always() }}
71+
needs: mocha
7672
steps:
7773
- name: Checkout code
7874
uses: actions/checkout@v3
79-
continue-on-error: true
8075

81-
- name: Setup Node.js
76+
- name: Use Node.js 18
8277
uses: actions/setup-node@v3
8378
with:
8479
node-version: 18
85-
continue-on-error: true
8680

87-
- name: Install frontend deps
88-
run: npm --workspace frontend ci
89-
continue-on-error: true
81+
- name: Install frontend dependencies
82+
working-directory: frontend
83+
run: npm ci
9084

91-
- name: Frontend format
92-
run: npm --workspace frontend run format
93-
continue-on-error: true
85+
- name: Build Frontend
86+
working-directory: frontend
87+
run: npm run build
9488

95-
- name: Frontend lint
96-
run: npm --workspace frontend run lint
97-
continue-on-error: true
89+
- name: Upload Frontend Build Artifact
90+
uses: actions/upload-artifact@v4
91+
with:
92+
name: frontend-build
93+
path: frontend/build
9894

99-
- name: Build frontend
100-
run: npm --workspace frontend run build
101-
continue-on-error: true
95+
coverage:
96+
name: 📊 Generate Coverage
97+
runs-on: ubuntu-latest
98+
needs:
99+
- test
100+
- mocha
101+
- frontend
102+
steps:
103+
- name: Checkout code
104+
uses: actions/checkout@v3
102105

103-
- name: Frontend tests
104-
run: npm --workspace frontend run test:unit
105-
continue-on-error: true
106+
- name: Use Node.js 18
107+
uses: actions/setup-node@v3
108+
with:
109+
node-version: 18
110+
111+
- name: Install dependencies
112+
run: npm ci
113+
114+
- name: Run coverage
115+
run: npm run coverage
116+
117+
- name: Archive coverage report
118+
if: always()
119+
uses: actions/upload-artifact@v4
120+
with:
121+
name: coverage-report
122+
path: coverage
123+
124+
docker:
125+
name: 🐳 Build & Push Docker Images
126+
runs-on: ubuntu-latest
127+
needs:
128+
- coverage
129+
permissions:
130+
contents: read
131+
packages: write
132+
steps:
133+
- name: Checkout code
134+
uses: actions/checkout@v3
135+
136+
- name: Log in to GitHub Container Registry
137+
uses: docker/login-action@v2
138+
with:
139+
registry: ghcr.io
140+
username: ${{ github.actor }}
141+
password: ${{ secrets.GITHUB_TOKEN }}
142+
143+
- name: Build & push backend image
144+
uses: docker/build-push-action@v3
145+
with:
146+
context: .
147+
file: ./Dockerfile
148+
push: true
149+
tags: |
150+
ghcr.io/${{ github.repository_owner }}/budget-management-backend:${{ github.sha }}
151+
ghcr.io/${{ github.repository_owner }}/budget-management-backend:latest
152+
153+
- name: Build & push frontend image
154+
uses: docker/build-push-action@v3
155+
with:
156+
context: ./frontend
157+
file: ./frontend/Dockerfile
158+
push: true
159+
tags: |
160+
ghcr.io/${{ github.repository_owner }}/budget-management-frontend:${{ github.sha }}
161+
ghcr.io/${{ github.repository_owner }}/budget-management-frontend:latest
162+
163+
deploy:
164+
name: 🚀 Deploying
165+
runs-on: ubuntu-latest
166+
needs: docker
167+
steps:
168+
- name: Simulate deployment to Render
169+
run: |
170+
echo "✅ Backend deployed to Render"
171+
- name: Simulate deployment to Vercel
172+
run: |
173+
echo "✅ Frontend deployed to Vercel"
106174
107175
complete:
108-
name: "🎉 Pipeline Complete"
176+
name: 🎉 All Done
109177
runs-on: ubuntu-latest
110-
needs: [formatting, backend, frontend]
111-
if: ${{ always() }}
178+
needs: deploy
112179
steps:
113180
- name: Final status
114181
run: echo "✅ CI/CD pipeline finished successfully."

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ Budget-Management-Backend-API/
172172
├── .prettierrc # Prettier configuration for code formatting
173173
├── LICENSE # License information
174174
├── README.md # Documentation for the project
175-
├── app.test.js # Main test file for application
175+
├── __tests__/app.test.js # Main test file for application
176176
├── cli.js # CLI tool for interacting with the backend
177177
├── docker-compose.yml # Docker Compose configuration
178178
├── Dockerfile # Dockerfile for containerizing the application

__tests__/app.mocha.test.cjs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// __tests__/app.mocha.test.cjs
2+
const chai = require('chai');
3+
const chaiHttp = require('chai-http');
4+
const proxyquire = require('proxyquire').noCallThru();
5+
const express = require('express');
6+
const path = require('path');
7+
const fs = require('fs');
8+
9+
chai.use(chaiHttp);
10+
const { expect } = chai;
11+
12+
describe('Express app (Mocha & Chai)', function() {
13+
let app;
14+
15+
before(function() {
16+
// 1) Prepare static files
17+
fs.mkdirSync(path.join(__dirname, '../views'), { recursive: true });
18+
fs.writeFileSync(path.join(__dirname, '../views/home.html'), '<h1>Home Page</h1>');
19+
fs.mkdirSync(path.join(__dirname, '../public'), { recursive: true });
20+
fs.writeFileSync(path.join(__dirname, '../public/test.txt'), 'Hello, Static!');
21+
22+
// 2) Stub all external modules exactly as index.js requires them
23+
const stubs = {
24+
mongoose: { connect: () => Promise.resolve() },
25+
'./services/redisService': {},
26+
'./apache-kafka/kafkaService': { connectToKafka: () => Promise.resolve() },
27+
'./services/rabbitMQService': { connectToRabbitMQ: () => Promise.resolve() },
28+
'./services/dataSeeder': () => Promise.resolve(),
29+
'./grpcServer': () => {},
30+
morgan: () => (req, res, next) => next(),
31+
'serve-favicon': () => (req, res, next) => next(),
32+
'swagger-ui-express': {
33+
serve: (_req, _res, next) => next(),
34+
setup: () => (_req, res) => res.send('<html>SWAGGER UI</html>')
35+
},
36+
'./routes': (() => {
37+
const router = express.Router();
38+
router.get('/ping', (_req, res) => res.json({ pong: true }));
39+
return router;
40+
})(),
41+
};
42+
43+
// 3) Load index.js via proxyquire and grab the exported app
44+
app = proxyquire('../index', stubs);
45+
});
46+
47+
after(function() {
48+
// clean up test files
49+
const v = path.join(__dirname, '../views/home.html');
50+
if (fs.existsSync(v)) fs.unlinkSync(v);
51+
const p = path.join(__dirname, '../public/test.txt');
52+
if (fs.existsSync(p)) fs.unlinkSync(p);
53+
});
54+
55+
it('serves the homepage at GET /', (done) => {
56+
chai.request(app)
57+
.get('/')
58+
.end((err, res) => {
59+
expect(err).to.be.null;
60+
expect(res).to.have.status(200);
61+
expect(res.text).to.include('<h1>Home Page</h1>');
62+
done();
63+
});
64+
});
65+
66+
it('serves Swagger UI at GET /docs', (done) => {
67+
chai.request(app)
68+
.get('/docs')
69+
.end((err, res) => {
70+
expect(err).to.be.null;
71+
expect(res).to.have.status(200);
72+
expect(res.text).to.include('SWAGGER UI');
73+
done();
74+
});
75+
});
76+
77+
it('responds on ping route under /api', (done) => {
78+
chai.request(app)
79+
.get('/api/ping')
80+
.end((err, res) => {
81+
expect(err).to.be.null;
82+
expect(res).to.have.status(200);
83+
expect(res.body).to.deep.equal({ pong: true });
84+
done();
85+
});
86+
});
87+
88+
it('includes CORS headers on API responses', (done) => {
89+
chai.request(app)
90+
.get('/api/ping')
91+
.end((err, res) => {
92+
expect(err).to.be.null;
93+
expect(res).to.have.header('access-control-allow-origin', '*');
94+
done();
95+
});
96+
});
97+
98+
it('serves static files from public/', (done) => {
99+
chai.request(app)
100+
.get('/test.txt')
101+
.end((err, res) => {
102+
expect(err).to.be.null;
103+
expect(res).to.have.status(200);
104+
expect(res.text).to.equal('Hello, Static!');
105+
// allow for optional charset parameter
106+
expect(res).to.have.header('content-type');
107+
expect(res.header['content-type']).to.match(/^text\/plain(?:;.*)?$/);
108+
done();
109+
});
110+
});
111+
112+
it('404s on unknown non-API route', (done) => {
113+
chai.request(app)
114+
.get('/does-not-exist')
115+
.end((err, res) => {
116+
expect(res).to.have.status(404);
117+
done();
118+
});
119+
});
120+
121+
it('404s on nonexistent /api path', (done) => {
122+
chai.request(app)
123+
.get('/api/does-not-exist')
124+
.end((err, res) => {
125+
expect(res).to.have.status(404);
126+
done();
127+
});
128+
});
129+
});

0 commit comments

Comments
 (0)