feat: init files

This commit is contained in:
2025-06-17 22:58:08 +08:00
parent 3c77faf93e
commit 6b090a93c9
10 changed files with 385 additions and 0 deletions

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# Use Node.js Alpine for smaller image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Copy your project-specific .npmrc
#COPY .npmrc ./
# Install dependencies
RUN npm install
# Copy the rest of the app
COPY . .
# Expose port
EXPOSE 3000
# Run the app
CMD ["node", "src/server.mjs"]

View File

@ -0,0 +1,3 @@
1. Clone
2. Build: docker build --no-cache -t crud-demo-app .
3. Run: docker run -d -p 3000:3000 --name crud-demo-app-running crud-demo-app

7
cucumber.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
default: {
require: ["step-definitions/**/*.js"], // make sure this path matches your actual step definitions folder
format: ["json:reports/cucumber-report.json"], // this will output a JSON report to the reports folder
publishQuiet: true, // suppress publish messages, good to keep clean
},
};

10
docker-compose.yaml Normal file
View File

@ -0,0 +1,10 @@
version: "3.8"
services:
crud-demo-app:
image: crud-demo-app
container_name: crud-demo-app-running
ports:
- "3000:3000"
# volumes:
# - ./data:/app/data # Persist SQLite DB here

45
features/crud.feature Normal file
View File

@ -0,0 +1,45 @@
Feature: CRUD Operations on Records
Scenario Outline: Create a record with name "<name>", age <age>, and email "<email>"
Given the API is runing
When I create a record with name "<name>", age <age>, email "<email>"
Then the response status should be 201
And the response should contain an id
Examples:
| name | age | email |
| John Smith | 30 | john@example.com |
| Jane Doe | 25 | jane@example.com |
Scenario Outline: Get the record by ID after creation with name "<name>", age <age>, email "<email>"
Given the API is running
And I have a created record with name "<name>", age <age>, email "<email>"
When I get the record by ID
Then the response status should be 200
And the response should contain the correct data for name "<name>", age <age>, email "<email>"
Examples:
| name | age | email |
| Temp_Name | 25 | temp@example.com |
Scenario Outline: Update the record with name "<oldName>", age <oldAge>, email "<oldEmail>" to name "<newName>", age <newAge>, email "<newEmail>"
Given the API is running
And I have a created record with name "<oldName>", age <oldAge>, email "<oldEmail>"
When I update the record with name "<newName>", age <newAge>, email "<newEmail>"
Then the response status should be 200
And the response should contain updated true
Examples:
| oldName | oldAge | oldEmail | newName | newAge | newEmail |
| Temp_Name | 25 | temp@example.com | Jane Smith | 28 | jane@example.com |
Scenario Outline: Delete the record with name "<name>", age <age>, email "<email>"
Given the API is running
And I have a created record with name "<name>", age <age>, email "<email>"
When I delete the record by ID
Then the response status should be 200
And the response should contain deleted true
Examples:
| name | age | email |
| Temp_Name | 25 | temp@example.com |

View File

@ -0,0 +1,93 @@
import { Given, When, Then } from '@cucumber/cucumber';
import assert from 'assert';
import axios from 'axios';
const BASE_URL = 'http://localhost:3000';
let response;
let createdRecord;
// Utility to log requests
function logRequest(method, url, data) {
console.log(`\n>>> REQUEST: ${method.toUpperCase()} ${url}`);
if (data) {
console.log('Payload:', JSON.stringify(data, null, 2));
}
}
// Utility to log responses
function logResponse(res) {
console.log(`<<< RESPONSE: Status ${res.status}`);
console.log('Response data:', JSON.stringify(res.data, null, 2));
}
Given('the API is running', async function () {
// Optionally ping /health or just assume running
// Example:
// const res = await axios.get(`${BASE_URL}/health`);
// assert.strictEqual(res.status, 200);
});
When('I create a record with name {string}, age {int}, email {string}', async function (name, age, email) {
const url = `${BASE_URL}/records`;
const payload = { name, age, email };
logRequest('post', url, payload);
response = await axios.post(url, payload);
logResponse(response);
createdRecord = response.data;
});
Then('the response status should be {int}', function (status) {
assert.strictEqual(response.status, status);
});
Then('the response should contain an id', function () {
assert.ok(createdRecord.id, 'Response does not contain an id');
});
Given('I have a created record with name {string}, age {int}, email {string}', async function (name, age, email) {
const url = `${BASE_URL}/records`;
const payload = { name, age, email };
logRequest('post', url, payload);
const res = await axios.post(url, payload);
logResponse(res);
createdRecord = res.data;
});
When('I get the record by ID', async function () {
const url = `${BASE_URL}/records/${createdRecord.id}`;
logRequest('get', url);
response = await axios.get(url);
logResponse(response);
});
Then(
'the response should contain the correct data for name {string}, age {int}, email {string}',
function (name, age, email) {
assert.strictEqual(response.data.name, name);
assert.strictEqual(response.data.age, age);
assert.strictEqual(response.data.email, email);
}
);
When('I update the record with name {string}, age {int}, email {string}', async function (name, age, email) {
const url = `${BASE_URL}/records/${createdRecord.id}`;
const payload = { name, age, email };
logRequest('put', url, payload);
response = await axios.put(url, payload);
logResponse(response);
});
Then('the response should contain updated true', function () {
assert.strictEqual(response.data.updated, true);
});
When('I delete the record by ID', async function () {
const url = `${BASE_URL}/records/${createdRecord.id}`;
logRequest('delete', url);
response = await axios.delete(url);
logResponse(response);
});
Then('the response should contain deleted true', function () {
assert.strictEqual(response.data.deleted, true);
});

18
generate-report.js Normal file
View File

@ -0,0 +1,18 @@
const reporter = require("cucumber-html-reporter");
const options = {
theme: "bootstrap",
jsonFile: "reports/cucumber-report.json",
output: "reports/cucumber-html-report.html",
reportSuiteAsScenarios: true,
launchReport: false,
metadata: {
"App Version": "1.0.0",
"Test Environment": "STAGING",
Browser: "N/A",
Platform: process.platform,
Executed: "Local",
},
};
reporter.generate(options);

29
inject-css.js Normal file
View File

@ -0,0 +1,29 @@
const fs = require("fs");
const path = require("path");
const reportPath = path.join(__dirname, "reports", "cucumber-html-report.html");
const cssToInject = `
<style>
.feature-passed > .col-lg-6 {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
.feature-passed > .col-lg-6 > .panel {
width: 100% !important;
}
</style>
`;
fs.readFile(reportPath, "utf8", (err, data) => {
if (err) throw err;
const modified = data.replace("</head>", `${cssToInject}</head>`);
fs.writeFile(reportPath, modified, "utf8", (err) => {
if (err) throw err;
console.log("Custom CSS injected successfully!");
});
});

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "crud-demo-app",
"version": "1.0.0",
"main": "index.js",
"description": "CRUD demo app with CucumberJS tests",
"author": "",
"license": "ISC",
"scripts": {
"start": "node src/server.js",
"test": "cucumber-js --format json:cucumber-report.json",
"report": "node generate-report.js && node inject-css.js"
},
"keywords": [],
"dependencies": {
"axios": "^1.9.0",
"express": "^5.1.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@cucumber/cucumber": "^11.3.0",
"cucumber-html-report": "^0.6.5",
"cucumber-html-reporter": "^7.2.0",
"multiple-cucumber-html-reporter": "^3.9.2"
}
}

130
src/server.mjs Normal file
View File

@ -0,0 +1,130 @@
import express from "express";
import sqlite3 from "sqlite3";
import { open } from "sqlite";
const app = express();
app.use(express.json());
// Logging middleware
app.use((req, res, next) => {
const start = Date.now();
console.log(`➡️ ${req.method} ${req.originalUrl} - Body:`, req.body);
const originalSend = res.send.bind(res);
res.send = (body) => {
const duration = Date.now() - start;
let responseBody = body;
if (typeof body === "string" && body.startsWith("{")) {
try {
responseBody = JSON.parse(body);
} catch {}
}
console.log(
`⬅️ ${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms - Response:`,
responseBody,
);
return originalSend(body);
};
next();
});
let db;
const initDb = async () => {
db = await open({
filename: "./data.db",
driver: sqlite3.Database,
});
await db.exec(`
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
age INTEGER,
email TEXT
)
`);
};
// Create
app.post("/records", async (req, res) => {
let { name, age, email } = req.body;
if (!name || typeof age !== "number") {
return res.status(400).json({ error: "Invalid input" });
}
name = name.replace(/\s+/g, "_"); // Replace spaces with underscores
const result = await db.run(
"INSERT INTO records (name, age, email) VALUES (?, ?, ?)",
[name, age, email],
);
res.status(201).json({ id: result.lastID });
});
// Read by ID
app.get("/records/:id", async (req, res) => {
const record = await db.get("SELECT * FROM records WHERE id = ?", [
req.params.id,
]);
if (!record) return res.status(404).json({ error: "Not found" });
res.json(record);
});
// Read by Name
app.get("/records/name/:name", async (req, res) => {
const records = await db.all("SELECT * FROM records WHERE name = ?", [
req.params.name,
]);
if (!records.length) return res.status(404).json({ error: "Not found" });
res.json(records);
});
// Update
app.put("/records/:id", async (req, res) => {
let { name, age, email } = req.body;
if (name) name = name.replace(/\s+/g, "_");
const result = await db.run(
"UPDATE records SET name = ?, age = ?, email = ? WHERE id = ?",
[name, age, email, req.params.id],
);
if (result.changes === 0) return res.status(404).json({ error: "Not found" });
res.json({ updated: true });
});
// Delete by ID
app.delete("/records/:id", async (req, res) => {
const result = await db.run("DELETE FROM records WHERE id = ?", [
req.params.id,
]);
if (result.changes === 0) return res.status(404).json({ error: "Not found" });
res.json({ deleted: true });
});
// Delete by Name
app.delete("/records/name/:name", async (req, res) => {
const result = await db.run("DELETE FROM records WHERE name = ?", [
req.params.name,
]);
res.json({ deletedCount: result.changes });
});
// Catch-all for unmatched routes
app.use((req, res) => {
console.log(`❌ No route for ${req.method} ${req.originalUrl}`);
res.status(404).json({ error: "Not found" });
});
// Start server
const PORT = 3000;
initDb().then(() => {
app.listen(PORT, () =>
console.log(`🚀 Server running at http://localhost:${PORT}`),
);
});