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