From 6b090a93c92c4df898e2355632b994822b9efe9a Mon Sep 17 00:00:00 2001 From: tech Date: Tue, 17 Jun 2025 22:58:08 +0800 Subject: [PATCH] feat: init files --- Dockerfile | 24 +++++ README.md | 3 + cucumber.js | 7 ++ docker-compose.yaml | 10 ++ features/crud.feature | 45 ++++++++ features/step_definitions/crud.steps.js | 93 +++++++++++++++++ generate-report.js | 18 ++++ inject-css.js | 29 ++++++ package.json | 26 +++++ src/server.mjs | 130 ++++++++++++++++++++++++ 10 files changed, 385 insertions(+) create mode 100644 Dockerfile create mode 100644 cucumber.js create mode 100644 docker-compose.yaml create mode 100644 features/crud.feature create mode 100644 features/step_definitions/crud.steps.js create mode 100644 generate-report.js create mode 100644 inject-css.js create mode 100644 package.json create mode 100644 src/server.mjs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..01bdf28 --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/README.md b/README.md index e69de29..f541e02 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cucumber.js b/cucumber.js new file mode 100644 index 0000000..5f888c4 --- /dev/null +++ b/cucumber.js @@ -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 + }, +}; diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f1c46ad --- /dev/null +++ b/docker-compose.yaml @@ -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 diff --git a/features/crud.feature b/features/crud.feature new file mode 100644 index 0000000..184acfb --- /dev/null +++ b/features/crud.feature @@ -0,0 +1,45 @@ +Feature: CRUD Operations on Records + + Scenario Outline: Create a record with name "", age , and email "" + Given the API is runing + When I create a record with name "", age , 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 "", age , email "" + Given the API is running + And I have a created record with name "", age , 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 "", age , email "" + + Examples: + | name | age | email | + | Temp_Name | 25 | temp@example.com | + + Scenario Outline: Update the record with name "", age , email "" to name "", age , email "" + Given the API is running + And I have a created record with name "", age , email "" + When I update the record with name "", age , email "" + 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 "", age , email "" + Given the API is running + And I have a created record with name "", age , 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 | diff --git a/features/step_definitions/crud.steps.js b/features/step_definitions/crud.steps.js new file mode 100644 index 0000000..3168d0a --- /dev/null +++ b/features/step_definitions/crud.steps.js @@ -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); +}); diff --git a/generate-report.js b/generate-report.js new file mode 100644 index 0000000..371c054 --- /dev/null +++ b/generate-report.js @@ -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); diff --git a/inject-css.js b/inject-css.js new file mode 100644 index 0000000..c132cbb --- /dev/null +++ b/inject-css.js @@ -0,0 +1,29 @@ +const fs = require("fs"); +const path = require("path"); + +const reportPath = path.join(__dirname, "reports", "cucumber-html-report.html"); + +const cssToInject = ` + +`; + +fs.readFile(reportPath, "utf8", (err, data) => { + if (err) throw err; + + const modified = data.replace("", `${cssToInject}`); + + fs.writeFile(reportPath, modified, "utf8", (err) => { + if (err) throw err; + console.log("Custom CSS injected successfully!"); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..720fcf6 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/server.mjs b/src/server.mjs new file mode 100644 index 0000000..db12ceb --- /dev/null +++ b/src/server.mjs @@ -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}`), + ); +});