From 6e055117b71717ed93c16b9eb244fbadc0c32ba6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:38:22 +0000 Subject: [PATCH 1/3] Initial plan From 021eeb9cc58c2edb374849ecf8093d4528fdad0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:40:57 +0000 Subject: [PATCH 2/3] Initial commit: mocha test dependency added Co-authored-by: robpitcher <13648061+robpitcher@users.noreply.github.com> --- exercisefiles/node/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/exercisefiles/node/package.json b/exercisefiles/node/package.json index 0a8b20f..6c6b401 100644 --- a/exercisefiles/node/package.json +++ b/exercisefiles/node/package.json @@ -9,5 +9,8 @@ "keywords": [], "author": "", "license": "ISC", - "description": "" + "description": "", + "devDependencies": { + "mocha": "^11.7.5" + } } From dcd7a9382ce52bac65e54219107f0d7df2e9b0a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:45:58 +0000 Subject: [PATCH 3/3] Refactor nodeserver.js into small modules and add missing tests Co-authored-by: robpitcher <13648061+robpitcher@users.noreply.github.com> --- exercisefiles/node/colorUtils.js | 20 ++ exercisefiles/node/countryData.js | 29 +++ exercisefiles/node/dateUtils.js | 18 ++ exercisefiles/node/fileUtils.js | 38 ++++ exercisefiles/node/nodeserver.js | 212 +++------------------- exercisefiles/node/routes/countryRoute.js | 22 +++ exercisefiles/node/routes/daysRoute.js | 28 +++ exercisefiles/node/routes/fileRoute.js | 22 +++ exercisefiles/node/routes/getRoute.js | 21 +++ exercisefiles/node/routes/healthRoute.js | 14 ++ exercisefiles/node/test.js | 77 ++++++++ exercisefiles/node/validators.js | 35 ++++ 12 files changed, 350 insertions(+), 186 deletions(-) create mode 100644 exercisefiles/node/colorUtils.js create mode 100644 exercisefiles/node/countryData.js create mode 100644 exercisefiles/node/dateUtils.js create mode 100644 exercisefiles/node/fileUtils.js create mode 100644 exercisefiles/node/routes/countryRoute.js create mode 100644 exercisefiles/node/routes/daysRoute.js create mode 100644 exercisefiles/node/routes/fileRoute.js create mode 100644 exercisefiles/node/routes/getRoute.js create mode 100644 exercisefiles/node/routes/healthRoute.js create mode 100644 exercisefiles/node/validators.js diff --git a/exercisefiles/node/colorUtils.js b/exercisefiles/node/colorUtils.js new file mode 100644 index 0000000..95fe146 --- /dev/null +++ b/exercisefiles/node/colorUtils.js @@ -0,0 +1,20 @@ +// Exercise 6: Node path rules + +const fs = require('fs'); + +// Load colors from JSON file +const colors = JSON.parse(fs.readFileSync('./colors.json', 'utf8')); + +/** + * Returns the hex color code for a given color name + * @param {string} colorName - The name of the color + * @returns {string|null} - The hex code or null if not found + */ +function returnColorCode(colorName) { + const color = colors.find(c => c.color.toLowerCase() === colorName.toLowerCase()); + return color ? color.code.hex : null; +} + +module.exports = { + returnColorCode +}; diff --git a/exercisefiles/node/countryData.js b/exercisefiles/node/countryData.js new file mode 100644 index 0000000..935cf01 --- /dev/null +++ b/exercisefiles/node/countryData.js @@ -0,0 +1,29 @@ +// Exercise 6: Node path rules + +// Array of European countries with their ISO codes +const europeanCountries = [ + { country: 'Germany', isoCode: 'DE' }, + { country: 'France', isoCode: 'FR' }, + { country: 'Italy', isoCode: 'IT' }, + { country: 'Spain', isoCode: 'ES' }, + { country: 'Poland', isoCode: 'PL' }, + { country: 'Romania', isoCode: 'RO' }, + { country: 'Netherlands', isoCode: 'NL' }, + { country: 'Belgium', isoCode: 'BE' }, + { country: 'Greece', isoCode: 'GR' }, + { country: 'Portugal', isoCode: 'PT' }, + { country: 'Sweden', isoCode: 'SE' }, + { country: 'Austria', isoCode: 'AT' }, + { country: 'Hungary', isoCode: 'HU' }, + { country: 'Switzerland', isoCode: 'CH' }, + { country: 'Denmark', isoCode: 'DK' }, + { country: 'Finland', isoCode: 'FI' }, + { country: 'Norway', isoCode: 'NO' }, + { country: 'Ireland', isoCode: 'IE' }, + { country: 'Croatia', isoCode: 'HR' }, + { country: 'Bulgaria', isoCode: 'BG' } +]; + +module.exports = { + europeanCountries +}; diff --git a/exercisefiles/node/dateUtils.js b/exercisefiles/node/dateUtils.js new file mode 100644 index 0000000..69dd514 --- /dev/null +++ b/exercisefiles/node/dateUtils.js @@ -0,0 +1,18 @@ +// Exercise 6: Node path rules + +/** + * Calculates the number of days between two dates + * @param {string} date1 - First date in YYYY-MM-DD format + * @param {string} date2 - Second date in YYYY-MM-DD format + * @returns {number} - Number of days between the dates + */ +function daysBetweenDates(date1, date2) { + const d1 = new Date(date1); + const d2 = new Date(date2); + const diffTime = Math.abs(d2 - d1); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); +} + +module.exports = { + daysBetweenDates +}; diff --git a/exercisefiles/node/fileUtils.js b/exercisefiles/node/fileUtils.js new file mode 100644 index 0000000..d06e286 --- /dev/null +++ b/exercisefiles/node/fileUtils.js @@ -0,0 +1,38 @@ +// Exercise 6: Node path rules + +const fs = require('fs'); +const readline = require('readline'); + +/** + * Reads a file line by line and returns lines containing "Fusce" + * @returns {Promise>} - Promise that resolves with an array of matching lines + */ +async function getLineByLineFromTextFile() { + return new Promise((resolve, reject) => { + const lines = []; + const fileStream = fs.createReadStream('./sample.txt'); + + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + + rl.on('line', (line) => { + if (line.includes('Fusce')) { + lines.push(line); + } + }); + + rl.on('close', () => { + resolve(lines); + }); + + rl.on('error', (error) => { + reject(error); + }); + }); +} + +module.exports = { + getLineByLineFromTextFile +}; diff --git a/exercisefiles/node/nodeserver.js b/exercisefiles/node/nodeserver.js index e9fc05f..598e253 100644 --- a/exercisefiles/node/nodeserver.js +++ b/exercisefiles/node/nodeserver.js @@ -1,3 +1,4 @@ +// Exercise 6: nodeserver file rules // write a nodejs server that will expose a method call "get" that will return the value of the key passed in the query string // example: http://localhost:3000/get?key=hello // if the key is not passed, return "key not passed" @@ -6,201 +7,40 @@ // when server is listening, log "server is listening on port 3000" // Import required Node.js modules -const http = require('http'); // For creating the HTTP server -const url = require('url'); // For parsing URL query strings -const fs = require('fs'); // For file system operations -const readline = require('readline'); // For reading files line by line +const http = require('http'); +const url = require('url'); -// Load colors from JSON file -const colors = JSON.parse(fs.readFileSync('./colors.json', 'utf8')); +// Import utility modules +const { validatePhoneNumber, validateSpanishDNI } = require('./validators'); +const { returnColorCode } = require('./colorUtils'); +const { daysBetweenDates } = require('./dateUtils'); -/** - * Validates a US phone number in the format XXX-XXX-XXXX - * @param {string} phoneNumber - The phone number to validate - * @returns {boolean} - True if valid, false otherwise - */ -function validatePhoneNumber(phoneNumber) { - const regex = /^\d{3}-\d{3}-\d{4}$/; - return regex.test(phoneNumber); -} - -/** - * Validates a Spanish DNI (8 digits followed by a letter) - * The letter is calculated based on the number - * @param {string} dni - The DNI to validate - * @returns {boolean} - True if valid, false otherwise - */ -function validateSpanishDNI(dni) { - const dniRegex = /^(\d{8})([A-Z])$/; - const match = dni.match(dniRegex); - if (!match) return false; - - const number = parseInt(match[1], 10); - const letter = match[2]; - const letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; - const expectedLetter = letters[number % 23]; - - return letter === expectedLetter; -} - -/** - * Returns the hex color code for a given color name - * @param {string} colorName - The name of the color - * @returns {string|null} - The hex code or null if not found - */ -function returnColorCode(colorName) { - const color = colors.find(c => c.color.toLowerCase() === colorName.toLowerCase()); - return color ? color.code.hex : null; -} - -/** - * Calculates the number of days between two dates - * @param {string} date1 - First date in YYYY-MM-DD format - * @param {string} date2 - Second date in YYYY-MM-DD format - * @returns {number} - Number of days between the dates - */ -function daysBetweenDates(date1, date2) { - const d1 = new Date(date1); - const d2 = new Date(date2); - const diffTime = Math.abs(d2 - d1); - return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); -} - -// Array of European countries with their ISO codes -// Used by the /RandomEuropeanCountry endpoint -const europeanCountries = [ - { country: 'Germany', isoCode: 'DE' }, - { country: 'France', isoCode: 'FR' }, - { country: 'Italy', isoCode: 'IT' }, - { country: 'Spain', isoCode: 'ES' }, - { country: 'Poland', isoCode: 'PL' }, - { country: 'Romania', isoCode: 'RO' }, - { country: 'Netherlands', isoCode: 'NL' }, - { country: 'Belgium', isoCode: 'BE' }, - { country: 'Greece', isoCode: 'GR' }, - { country: 'Portugal', isoCode: 'PT' }, - { country: 'Sweden', isoCode: 'SE' }, - { country: 'Austria', isoCode: 'AT' }, - { country: 'Hungary', isoCode: 'HU' }, - { country: 'Switzerland', isoCode: 'CH' }, - { country: 'Denmark', isoCode: 'DK' }, - { country: 'Finland', isoCode: 'FI' }, - { country: 'Norway', isoCode: 'NO' }, - { country: 'Ireland', isoCode: 'IE' }, - { country: 'Croatia', isoCode: 'HR' }, - { country: 'Bulgaria', isoCode: 'BG' } -]; - -// Function to read file line by line and return lines containing "Fusce" -// Returns a Promise that resolves with an array of matching lines -function getLineByLineFromTextFile() { - return new Promise((resolve, reject) => { - const lines = []; // Array to store matching lines - const fileStream = fs.createReadStream('./sample.txt'); // Create read stream for the file // Create read stream for the file - - // Create readline interface to process file line by line - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity // Recognize all instances of CR LF as a single line break - }); - - // Event handler for each line read from the file - rl.on('line', (line) => { - if (line.includes('Fusce')) { // Check if line contains "Fusce" - lines.push(line); // Add matching line to results array - } - }); - - // Event handler when file reading is complete - rl.on('close', () => { - resolve(lines); // Resolve promise with all matching lines - }); - - // Event handler for any errors during file reading - rl.on('error', (error) => { - reject(error); // Reject promise with error - }); - }); -} +// Import route handlers +const { handleGetRoute } = require('./routes/getRoute'); +const { handleDaysRoute } = require('./routes/daysRoute'); +const { handleFileRoute } = require('./routes/fileRoute'); +const { handleCountryRoute } = require('./routes/countryRoute'); +const { handleHealthRoute } = require('./routes/healthRoute'); // Create HTTP server with request handler const server = http.createServer((req, res) => { - // Parse the incoming request URL and extract components const parsedUrl = url.parse(req.url, true); - const pathname = parsedUrl.pathname; // Get the path (e.g., '/get') - const query = parsedUrl.query; // Get query parameters as an object + const pathname = parsedUrl.pathname; + const query = parsedUrl.query; - // Route: /get?key=value - // Returns a greeting with the provided key parameter if (pathname === '/get') { - const key = query.key; // Extract 'key' from query parameters - if (key) { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello ' + key); // Return greeting with the key - } else { - res.writeHead(400, { 'Content-Type': 'text/plain' }); - res.end('key not passed'); // Return error if key is missing - } - } - // Route: /DaysBetweenDates?date1=YYYY-MM-DD&date2=YYYY-MM-DD - // Calculates the number of days between two dates - else if (pathname === '/DaysBetweenDates') { - const date1 = query.date1; // First date parameter - const date2 = query.date2; // Second date parameter - - if (date1 && date2) { - const d1 = new Date(date1); // Parse first date - const d2 = new Date(date2); // Parse second date - const diffTime = Math.abs(d2 - d1); // Calculate time difference in milliseconds - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); // Convert to days - - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(`Days between ${date1} and ${date2}: ${diffDays}`); - } else { - res.writeHead(400, { 'Content-Type': 'text/plain' }); - res.end('date1 and date2 parameters are required'); // Return error if dates are missing - } - } - // Route: /GetLineByLineFromTextFile - // Reads sample.txt and returns lines containing "Fusce" - else if (pathname === '/GetLineByLineFromTextFile') { - getLineByLineFromTextFile() - .then(lines => { - // Success: return matching lines as JSON with count - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ lines: lines, count: lines.length })); - }) - .catch(error => { - // Error: return error message - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('Error reading file: ' + error.message); - }); - } - // Route: /RandomEuropeanCountry - // Returns a randomly selected European country with its ISO code - else if (pathname === '/RandomEuropeanCountry') { - const randomIndex = Math.floor(Math.random() * europeanCountries.length); // Generate random index - const randomCountry = europeanCountries[randomIndex]; // Get country at random index - - // Return the country and ISO code as JSON - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - country: randomCountry.country, - isoCode: randomCountry.isoCode - })); - } - /** - * Route: /health - * Simple health-check endpoint for monitoring - */ - else if (pathname === '/health') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok' })); - } - // Default route: handle all unrecognized paths - else { + handleGetRoute(query, res); + } else if (pathname === '/DaysBetweenDates') { + handleDaysRoute(query, res); + } else if (pathname === '/GetLineByLineFromTextFile') { + handleFileRoute(res); + } else if (pathname === '/RandomEuropeanCountry') { + handleCountryRoute(res); + } else if (pathname === '/health') { + handleHealthRoute(res); + } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('method not supported'); // Return 404 for unsupported routes + res.end('method not supported'); } }); diff --git a/exercisefiles/node/routes/countryRoute.js b/exercisefiles/node/routes/countryRoute.js new file mode 100644 index 0000000..425c215 --- /dev/null +++ b/exercisefiles/node/routes/countryRoute.js @@ -0,0 +1,22 @@ +// Exercise 6: Node path rules + +const { europeanCountries } = require('../countryData'); + +/** + * Handles the /RandomEuropeanCountry endpoint + * @param {object} res - HTTP response object + */ +function handleCountryRoute(res) { + const randomIndex = Math.floor(Math.random() * europeanCountries.length); + const randomCountry = europeanCountries[randomIndex]; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + country: randomCountry.country, + isoCode: randomCountry.isoCode + })); +} + +module.exports = { + handleCountryRoute +}; diff --git a/exercisefiles/node/routes/daysRoute.js b/exercisefiles/node/routes/daysRoute.js new file mode 100644 index 0000000..bc825ff --- /dev/null +++ b/exercisefiles/node/routes/daysRoute.js @@ -0,0 +1,28 @@ +// Exercise 6: Node path rules + +/** + * Handles the /DaysBetweenDates endpoint + * @param {object} query - Query parameters from the request + * @param {object} res - HTTP response object + */ +function handleDaysRoute(query, res) { + const date1 = query.date1; + const date2 = query.date2; + + if (date1 && date2) { + const d1 = new Date(date1); + const d2 = new Date(date2); + const diffTime = Math.abs(d2 - d1); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`Days between ${date1} and ${date2}: ${diffDays}`); + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('date1 and date2 parameters are required'); + } +} + +module.exports = { + handleDaysRoute +}; diff --git a/exercisefiles/node/routes/fileRoute.js b/exercisefiles/node/routes/fileRoute.js new file mode 100644 index 0000000..f19aa98 --- /dev/null +++ b/exercisefiles/node/routes/fileRoute.js @@ -0,0 +1,22 @@ +// Exercise 6: Node path rules + +const { getLineByLineFromTextFile } = require('../fileUtils'); + +/** + * Handles the /GetLineByLineFromTextFile endpoint + * @param {object} res - HTTP response object + */ +async function handleFileRoute(res) { + try { + const lines = await getLineByLineFromTextFile(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ lines: lines, count: lines.length })); + } catch (error) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error reading file: ' + error.message); + } +} + +module.exports = { + handleFileRoute +}; diff --git a/exercisefiles/node/routes/getRoute.js b/exercisefiles/node/routes/getRoute.js new file mode 100644 index 0000000..92d49b4 --- /dev/null +++ b/exercisefiles/node/routes/getRoute.js @@ -0,0 +1,21 @@ +// Exercise 6: Node path rules + +/** + * Handles the /get endpoint + * @param {object} query - Query parameters from the request + * @param {object} res - HTTP response object + */ +function handleGetRoute(query, res) { + const key = query.key; + if (key) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello ' + key); + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('key not passed'); + } +} + +module.exports = { + handleGetRoute +}; diff --git a/exercisefiles/node/routes/healthRoute.js b/exercisefiles/node/routes/healthRoute.js new file mode 100644 index 0000000..dcb1569 --- /dev/null +++ b/exercisefiles/node/routes/healthRoute.js @@ -0,0 +1,14 @@ +// Exercise 6: Node path rules + +/** + * Handles the /health endpoint + * @param {object} res - HTTP response object + */ +function handleHealthRoute(res) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); +} + +module.exports = { + handleHealthRoute +}; diff --git a/exercisefiles/node/test.js b/exercisefiles/node/test.js index ce58473..57328a4 100644 --- a/exercisefiles/node/test.js +++ b/exercisefiles/node/test.js @@ -95,4 +95,81 @@ describe('Node Server', () => { }); }); }); + + it('should calculate days between dates via HTTP endpoint', (done) => { + http.get('http://localhost:3000/DaysBetweenDates?date1=2024-01-01&date2=2024-01-11', (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + assert.equal(res.statusCode, 200); + assert.equal(data, 'Days between 2024-01-01 and 2024-01-11: 10'); + done(); + }); + }); + }); + + it('should return error when dates are missing', (done) => { + http.get('http://localhost:3000/DaysBetweenDates?date1=2024-01-01', (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + assert.equal(res.statusCode, 400); + assert.equal(data, 'date1 and date2 parameters are required'); + done(); + }); + }); + }); + + it('should return lines containing Fusce from text file', (done) => { + http.get('http://localhost:3000/GetLineByLineFromTextFile', (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + const json = JSON.parse(data); + assert.equal(res.statusCode, 200); + assert(Array.isArray(json.lines)); + assert.equal(typeof json.count, 'number'); + assert(json.lines.every(line => line.includes('Fusce'))); + done(); + }); + }); + }); + + it('should return a random European country with ISO code', (done) => { + http.get('http://localhost:3000/RandomEuropeanCountry', (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + const json = JSON.parse(data); + assert.equal(res.statusCode, 200); + assert(json.country); + assert(json.isoCode); + assert.equal(typeof json.country, 'string'); + assert.equal(typeof json.isoCode, 'string'); + done(); + }); + }); + }); + + it('should return 404 for unsupported routes', (done) => { + http.get('http://localhost:3000/unsupported', (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + assert.equal(res.statusCode, 404); + assert.equal(data, 'method not supported'); + done(); + }); + }); + }); }); diff --git a/exercisefiles/node/validators.js b/exercisefiles/node/validators.js new file mode 100644 index 0000000..ab92b79 --- /dev/null +++ b/exercisefiles/node/validators.js @@ -0,0 +1,35 @@ +// Exercise 6: Node path rules + +/** + * Validates a US phone number in the format XXX-XXX-XXXX + * @param {string} phoneNumber - The phone number to validate + * @returns {boolean} - True if valid, false otherwise + */ +function validatePhoneNumber(phoneNumber) { + const regex = /^\d{3}-\d{3}-\d{4}$/; + return regex.test(phoneNumber); +} + +/** + * Validates a Spanish DNI (8 digits followed by a letter) + * The letter is calculated based on the number + * @param {string} dni - The DNI to validate + * @returns {boolean} - True if valid, false otherwise + */ +function validateSpanishDNI(dni) { + const dniRegex = /^(\d{8})([A-Z])$/; + const match = dni.match(dniRegex); + if (!match) return false; + + const number = parseInt(match[1], 10); + const letter = match[2]; + const letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; + const expectedLetter = letters[number % 23]; + + return letter === expectedLetter; +} + +module.exports = { + validatePhoneNumber, + validateSpanishDNI +};