Skip to content

Commit 0cb4e11

Browse files
committed
Configure pm2, port 80, Render deployment, and blue/white UI theme
1 parent ed444fe commit 0cb4e11

13 files changed

Lines changed: 732 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI
1+
name: CI/CD
22

33
on:
44
push:
@@ -11,8 +11,9 @@ on:
1111
- master
1212

1313
jobs:
14-
backend:
15-
name: Backend CI
14+
# ─────────────────────────── Backend ───────────────────────────
15+
backend-lint:
16+
name: Backend — Lint & Format
1617
runs-on: ubuntu-latest
1718
defaults:
1819
run:
@@ -29,22 +30,43 @@ jobs:
2930
cache-dependency-path: backend/package-lock.json
3031

3132
- name: Install dependencies
32-
# Uses npm install instead of npm ci in case package-lock.json is missing or out of sync
33-
# Though npm ci is preferred in CI if package-lock is present
34-
run: npm install
33+
run: npm ci
3534

3635
- name: Check code formatting
3736
run: npx prettier --check .
38-
continue-on-error: true
3937

4038
- name: Run linter
4139
run: npm run lint
4240

43-
- name: Run tests
44-
run: npm test
41+
backend-test:
42+
name: Backend — Tests
43+
runs-on: ubuntu-latest
44+
needs: backend-lint
45+
defaults:
46+
run:
47+
working-directory: ./backend
48+
steps:
49+
- name: Checkout repository
50+
uses: actions/checkout@v4
51+
52+
- name: Set up Node.js
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: 20
56+
cache: 'npm'
57+
cache-dependency-path: backend/package-lock.json
58+
59+
- name: Install dependencies
60+
run: npm ci
4561

46-
frontend:
47-
name: Frontend CI
62+
- name: Run tests with coverage
63+
run: npx jest --coverage --forceExit
64+
env:
65+
NODE_ENV: test
66+
67+
# ─────────────────────────── Frontend ──────────────────────────
68+
frontend-lint:
69+
name: Frontend — Lint & Format
4870
runs-on: ubuntu-latest
4971
defaults:
5072
run:
@@ -61,17 +83,97 @@ jobs:
6183
cache-dependency-path: frontend/package-lock.json
6284

6385
- name: Install dependencies
64-
run: npm install
86+
run: npm ci
6587

6688
- name: Check code formatting
6789
run: npx prettier --check .
68-
continue-on-error: true
6990

7091
- name: Run linter
7192
run: npm run lint
7293

73-
- name: Build project
94+
frontend-test:
95+
name: Frontend — Tests
96+
runs-on: ubuntu-latest
97+
needs: frontend-lint
98+
defaults:
99+
run:
100+
working-directory: ./frontend
101+
steps:
102+
- name: Checkout repository
103+
uses: actions/checkout@v4
104+
105+
- name: Set up Node.js
106+
uses: actions/setup-node@v4
107+
with:
108+
node-version: 20
109+
cache: 'npm'
110+
cache-dependency-path: frontend/package-lock.json
111+
112+
- name: Install dependencies
113+
run: npm ci
114+
115+
- name: Run tests with coverage
116+
run: npx jest --coverage --forceExit
117+
env:
118+
CI: true
119+
120+
frontend-build:
121+
name: Frontend — Build
122+
runs-on: ubuntu-latest
123+
needs: frontend-test
124+
defaults:
125+
run:
126+
working-directory: ./frontend
127+
steps:
128+
- name: Checkout repository
129+
uses: actions/checkout@v4
130+
131+
- name: Set up Node.js
132+
uses: actions/setup-node@v4
133+
with:
134+
node-version: 20
135+
cache: 'npm'
136+
cache-dependency-path: frontend/package-lock.json
137+
138+
- name: Install dependencies
139+
run: npm ci
140+
141+
- name: Build for production
74142
run: npm run build
75143

76-
- name: Run tests
77-
run: npm test
144+
- name: Upload build artifact
145+
uses: actions/upload-artifact@v4
146+
with:
147+
name: frontend-dist
148+
path: frontend/dist/
149+
retention-days: 7
150+
151+
# ─────────────────────────── Docker ────────────────────────────
152+
docker-build:
153+
name: Docker — Build Images
154+
runs-on: ubuntu-latest
155+
needs: [backend-test, frontend-build]
156+
steps:
157+
- name: Checkout repository
158+
uses: actions/checkout@v4
159+
160+
- name: Set up Docker Buildx
161+
uses: docker/setup-buildx-action@v3
162+
163+
- name: Build backend image
164+
uses: docker/build-push-action@v5
165+
with:
166+
context: ./backend
167+
push: false
168+
tags: nexustech-backend:${{ github.sha }}
169+
cache-from: type=gha
170+
cache-to: type=gha,mode=max
171+
172+
- name: Build frontend image
173+
uses: docker/build-push-action@v5
174+
with:
175+
context: ./frontend
176+
push: false
177+
tags: nexustech-frontend:${{ github.sha }}
178+
cache-from: type=gha
179+
cache-to: type=gha,mode=max

backend/.dockerignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
npm-debug.log*
3+
tests
4+
coverage
5+
.env
6+
.DS_Store
7+
*.md
8+
eslint.config.mjs
9+
.prettierrc
10+
.prettierignore

backend/Dockerfile

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ---------- Build stage ----------
2+
FROM node:20-alpine AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy package files first for better layer caching
7+
COPY package.json package-lock.json ./
8+
RUN npm ci --omit=dev
9+
10+
# Copy application source
11+
COPY src/ ./src/
12+
13+
# ---------- Production stage ----------
14+
FROM node:20-alpine
15+
16+
WORKDIR /app
17+
18+
# Create a non-root user for security
19+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
20+
21+
# Copy built artifacts from builder
22+
COPY --from=builder /app/node_modules ./node_modules
23+
COPY --from=builder /app/src ./src
24+
COPY package.json ./
25+
26+
# Install PM2 globally
27+
RUN npm install -g pm2
28+
29+
# Create data directory for SQLite and set permissions
30+
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
31+
32+
ENV NODE_ENV=production
33+
ENV PORT=80
34+
35+
USER appuser
36+
37+
EXPOSE 80
38+
39+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
40+
CMD wget -qO- http://localhost:80/api/products || exit 1
41+
42+
CMD ["pm2-runtime", "src/index.js"]

backend/tests/api.test.js

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,139 @@ describe('API Tests', () => {
77
db.close();
88
});
99

10-
test('GET /api/products returns products', async () => {
11-
const res = await request(app).get('/api/products');
12-
expect(res.statusCode).toBe(200);
13-
expect(Array.isArray(res.body)).toBe(true);
14-
expect(res.body.length).toBeGreaterThan(0);
15-
expect(res.body[0]).toHaveProperty('name');
10+
// ───────────────── GET /api/products ─────────────────
11+
describe('GET /api/products', () => {
12+
test('returns 200 status code', async () => {
13+
const res = await request(app).get('/api/products');
14+
expect(res.statusCode).toBe(200);
15+
});
16+
17+
test('returns an array of products', async () => {
18+
const res = await request(app).get('/api/products');
19+
expect(Array.isArray(res.body)).toBe(true);
20+
expect(res.body.length).toBeGreaterThan(0);
21+
});
22+
23+
test('each product has required fields', async () => {
24+
const res = await request(app).get('/api/products');
25+
for (const product of res.body) {
26+
expect(product).toHaveProperty('id');
27+
expect(product).toHaveProperty('name');
28+
expect(product).toHaveProperty('price');
29+
expect(product).toHaveProperty('image_url');
30+
}
31+
});
32+
33+
test('product price is a positive number', async () => {
34+
const res = await request(app).get('/api/products');
35+
for (const product of res.body) {
36+
expect(typeof product.price).toBe('number');
37+
expect(product.price).toBeGreaterThan(0);
38+
}
39+
});
40+
41+
test('returns JSON content type', async () => {
42+
const res = await request(app).get('/api/products');
43+
expect(res.headers['content-type']).toMatch(/json/);
44+
});
1645
});
1746

18-
test('POST /api/checkout works', async () => {
19-
const res = await request(app)
20-
.post('/api/checkout')
21-
.send({ items: [{ productId: 1, quantity: 2 }] });
22-
23-
expect(res.statusCode).toBe(200);
24-
expect(res.body).toHaveProperty('message', 'Checkout successful');
25-
expect(res.body).toHaveProperty('total');
47+
// ───────────────── POST /api/checkout ─────────────────
48+
describe('POST /api/checkout', () => {
49+
test('returns 200 with valid cart items', async () => {
50+
const res = await request(app)
51+
.post('/api/checkout')
52+
.send({ items: [{ productId: 1, quantity: 2 }] });
53+
54+
expect(res.statusCode).toBe(200);
55+
expect(res.body).toHaveProperty('message', 'Checkout successful');
56+
expect(res.body).toHaveProperty('total');
57+
expect(res.body).toHaveProperty('orderId');
58+
});
59+
60+
test('calculates total correctly for single item', async () => {
61+
const productsRes = await request(app).get('/api/products');
62+
const firstProduct = productsRes.body[0];
63+
const quantity = 3;
64+
65+
const res = await request(app)
66+
.post('/api/checkout')
67+
.send({ items: [{ productId: firstProduct.id, quantity }] });
68+
69+
expect(res.statusCode).toBe(200);
70+
const expectedTotal = firstProduct.price * quantity;
71+
expect(res.body.total).toBeCloseTo(expectedTotal, 2);
72+
});
73+
74+
test('calculates total correctly for multiple items', async () => {
75+
const productsRes = await request(app).get('/api/products');
76+
const products = productsRes.body;
77+
78+
const cartItems = [
79+
{ productId: products[0].id, quantity: 1 },
80+
{ productId: products[1].id, quantity: 2 },
81+
];
82+
83+
const res = await request(app)
84+
.post('/api/checkout')
85+
.send({ items: cartItems });
86+
87+
const expectedTotal =
88+
products[0].price * 1 + products[1].price * 2;
89+
expect(res.statusCode).toBe(200);
90+
expect(res.body.total).toBeCloseTo(expectedTotal, 2);
91+
});
92+
93+
test('returns 400 with empty items array', async () => {
94+
const res = await request(app)
95+
.post('/api/checkout')
96+
.send({ items: [] });
97+
98+
expect(res.statusCode).toBe(400);
99+
expect(res.body).toHaveProperty('error');
100+
});
101+
102+
test('returns 400 with missing items field', async () => {
103+
const res = await request(app)
104+
.post('/api/checkout')
105+
.send({});
106+
107+
expect(res.statusCode).toBe(400);
108+
expect(res.body).toHaveProperty('error', 'Invalid cart contents');
109+
});
110+
111+
test('returns 400 when items is not an array', async () => {
112+
const res = await request(app)
113+
.post('/api/checkout')
114+
.send({ items: 'not-an-array' });
115+
116+
expect(res.statusCode).toBe(400);
117+
});
118+
119+
test('returns orderId as a number', async () => {
120+
const res = await request(app)
121+
.post('/api/checkout')
122+
.send({ items: [{ productId: 1, quantity: 1 }] });
123+
124+
expect(typeof res.body.orderId).toBe('number');
125+
});
126+
127+
test('handles non-existent product id gracefully', async () => {
128+
const res = await request(app)
129+
.post('/api/checkout')
130+
.send({ items: [{ productId: 99999, quantity: 1 }] });
131+
132+
// Should succeed but total should be 0 (product not found)
133+
expect(res.statusCode).toBe(200);
134+
expect(res.body.total).toBe(0);
135+
});
26136
});
27137

28-
test('POST /api/checkout fails with empty items', async () => {
29-
const res = await request(app)
30-
.post('/api/checkout')
31-
.send({ items: [] });
32-
33-
expect(res.statusCode).toBe(400);
138+
// ───────────────── Unknown routes ─────────────────
139+
describe('Unknown routes', () => {
140+
test('GET /api/unknown returns 404', async () => {
141+
const res = await request(app).get('/api/unknown');
142+
expect(res.statusCode).toBe(404);
143+
});
34144
});
35145
});

0 commit comments

Comments
 (0)