feat: init files
This commit is contained in:
24
Dockerfile
Normal file
24
Dockerfile
Normal 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"]
|
||||||
|
|
||||||
@ -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
7
cucumber.js
Normal 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
10
docker-compose.yaml
Normal 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
45
features/crud.feature
Normal 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 |
|
||||||
93
features/step_definitions/crud.steps.js
Normal file
93
features/step_definitions/crud.steps.js
Normal 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
18
generate-report.js
Normal 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
29
inject-css.js
Normal 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
26
package.json
Normal 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
130
src/server.mjs
Normal 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}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user