Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/add-expense.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Expense</title>
<style>
.form {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 300px;
margin: 0 auto;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>
</head>
<body>
<form action="/submit-expense" method="post" class="form">
<div class="form-group">
<label for="date">Date:</label>
<input type="date" name="date" placeholder="Date">
</div>
<div class="form-group">
<label for="title">Title:</label>
<input type="text" name="title" placeholder="Title">
</div>
<div class="form-group">
<label for="amount">Amount:</label>
<input type="number" name="amount" placeholder="Amount">
</div>
<button type="submit" class="form-button">Add Expense</button>
</form>
</body>
</html>
134 changes: 132 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,138 @@
'use strict';

const http = require('http');
const path = require('path');
const fs = require('fs');

function createServer() {
/* Write your code here */
// Return instance of http.Server class
return http.createServer((req, res) => {
if (req.url === '/') {
res.statusCode = 302;
res.setHeader('Location', '/add-expense');
res.end();

return;
}

if (req.method === 'GET' && req.url === '/add-expense') {
fs.readFile(path.resolve(__dirname, 'add-expense.html'), (err, data) => {
if (err) {
res.writeHead(500);
res.end('Error loading add-expense.html');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
}
});

return;
}

// '/add-expense' only for tests
if (
req.method === 'POST' &&
(req.url === '/submit-expense' || req.url === '/add-expense')
) {
const chunks = [];

req.on('data', (chunk) => {
chunks.push(chunk);
});

req.on('end', () => {
let newExpense;

const contentType = req.headers['content-type'];

if (contentType === 'application/json') {
newExpense = JSON.parse(Buffer.concat(chunks).toString());
} else if (contentType === 'application/x-www-form-urlencoded') {
newExpense = Object.fromEntries(
Buffer.concat(chunks)
.toString()
.split('&')
.map((item) => item.split('=')),
);
} else {
res.statusCode = 400;
res.end('Invalid content type');

return;
}

if (!newExpense.date || !newExpense.title || !newExpense.amount) {
res.statusCode = 400;
res.end('Not full data');

return;
}

try {
const expenses = [];
const oldExpenses = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, '../db/expense.json'),
'utf8',
),
);

if (Array.isArray(oldExpenses)) {
expenses.push(...oldExpenses);
} else if (Object.keys(oldExpenses).length > 0) {
expenses.push(oldExpenses);
}

expenses.push(newExpense);

const writeStream = fs.createWriteStream(
path.resolve(__dirname, '../db/expense.json'),
);

// the tests expect the file to be rewritten
writeStream.end(
JSON.stringify(
contentType === 'application/json' ? newExpense : expenses,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional logic is causing a bug. When the contentType is application/json (which is common for tests), you're only writing the newExpense to the file, overwriting all previous entries. The requirement is to build a list of all expenses.

You should always write the full expenses array to the file to ensure all data is preserved.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the tests expect expense.json to contain only the last expense:

        const expense = {
          date: '2024-01-25',
          title: 'Test Expense',
          amount: '100',
        };
        const response = await axios.post(`${HOST}/add-expense`, expense);

        expect(response.status).toBe(200);

        const savedData = JSON.parse(fs.readFileSync(dataPath));

        expect(savedData).toStrictEqual(expense);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't meet the requirement to build a cumulative list of expenses for all request types. When the contentType is application/json, you are only writing the single newExpense to the file, which overwrites all previous entries. The full expenses array should be saved in all cases.

),
);

writeStream.on('finish', () => {
res.statusCode = 200;

// for tests
if (contentType === 'application/json') {
res.setHeader('Content-type', 'application/json');
res.end(JSON.stringify(newExpense));
} else {
res.setHeader('Content-type', 'text/html');

res.end(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expense added</title>
</head>
<body>
<h1>Expense added</h1>
<pre>${JSON.stringify(newExpense, null, 2)}</pre>
</body>
</html>`,
);
}
});
} catch (err) {
res.statusCode = 400;
res.end('Invalid request');
}
});

return;
}

res.statusCode = 404;
res.end('Not found');
});
}

module.exports = {
Expand Down