diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4fec15ae8..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.tabSize": 2, - "javascript.validate.enable": false, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - } -} diff --git a/assignments/hackyourtemperature/.gitignore b/assignments/hackyourtemperature/.gitignore new file mode 100644 index 000000000..1dcef2d9f --- /dev/null +++ b/assignments/hackyourtemperature/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env \ No newline at end of file diff --git a/assignments/hackyourtemperature/__tests__/app.test.js b/assignments/hackyourtemperature/__tests__/app.test.js new file mode 100644 index 000000000..c5c8ff5b7 --- /dev/null +++ b/assignments/hackyourtemperature/__tests__/app.test.js @@ -0,0 +1,121 @@ +import app from "../app.js"; +import supertest from "supertest"; + +import * as helper from "../helper.js"; + +const request = supertest(app); + +describe("POST /weather", () => { + // Tests for empty city string + describe("Given an empty city string", () => { + it("should return 400 with message 'City name is required.'", async () => { + const cityName = ""; + await request + .post("/weather") + .send({ cityName }) + .expect(400) + .expect({ message: "City name is required." }); + }); + }); + + // Tests for non-existent city name + describe("Given a gibberish city name", () => { + it("should return 404", async () => { + const cityName = "ahfeiueriuter"; + await request.post("/weather").send({ cityName }).expect(404); + }); + + it("should return message 'City is not found!'", async () => { + const cityName = "ahfeiueriuter"; + await request + .post("/weather") + .send({ cityName }) + .expect((res) => { + expect(res.body).toHaveProperty("weatherText", "City is not found!"); + }); + }); + }); + + // Tests for valid city name + describe("Given a valid city name", () => { + it("should return 200", async () => { + const cityName = "Addis Ababa"; + await request.post("/weather").send({ cityName }).expect(200); + }); + + it("should return weather data containing city name", async () => { + const cityName = "Addis Ababa"; + const response = await request + .post("/weather") + .send({ cityName }) + .expect(200); + + expect(response.body).toHaveProperty( + "weatherText", + expect.stringContaining(cityName) + ); + }); + }); + + // Tests for wrong domain + describe("Given a wrong domain", () => { + it("should return 400", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongDomain(cityName); + + await request.post("/weather").send({ cityName, wrongUrl }).expect(400); + }); + + it("should return message 'Invalid domain name.'", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongDomain(cityName); + + await request + .post("/weather") + .send({ cityName, wrongUrl }) + .expect({ message: "Invalid domain name." }); + }); + }); + + // Tests for invalid API key + describe("Given a wrong API key", () => { + it("should return 401", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongApiKey(cityName); + await request.post("/weather").send({ cityName, wrongUrl }).expect(401); + }); + + it("should return message 'Unauthorized access.'", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongApiKey(cityName); + + await request + .post("/weather") + .send({ cityName, wrongUrl }) + .expect((res) => { + expect(res.body).toHaveProperty("message", "Unauthorized access."); + }); + }); + }); + + // Tests for wrong endpoint + describe("Given a wrong end point", () => { + it("should return 404", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongEndPoint(cityName); + await request.post("/weather").send({ cityName, wrongUrl }).expect(404); + }); + + it("should return message 'Resource not found.'", async () => { + const cityName = "Addis Ababa"; + const wrongUrl = helper.getWeatherUrlWithWrongEndPoint(cityName); + + await request + .post("/weather") + .send({ cityName, wrongUrl }) + .expect((res) => { + expect(res.body).toHaveProperty("message", "Resource not found."); + }); + }); + }); +}); diff --git a/assignments/hackyourtemperature/app.js b/assignments/hackyourtemperature/app.js new file mode 100644 index 000000000..57bc0a07c --- /dev/null +++ b/assignments/hackyourtemperature/app.js @@ -0,0 +1,81 @@ +import express from "express"; +import { API_KEY, WRONG_API_KEY } from "./sources/keys.js"; +import fetch from "node-fetch"; +import * as helper from "./helper.js"; + +const PORT = 3000; + +const app = express(); +app.use(express.json()); + +app.get("/", (req, res) => { + res.status(200).send("hello from backend to frontend!"); +}); + +app.post("/weather", async (req, res) => { + const cityName = req.body.cityName; + if (!cityName) { + return res.status(400).send({ message: "City name is required." }); + } + + let url = null; + if (req.body.wrongUrl) { + url = req.body.wrongUrl; + } else { + url = helper.getWeatherUrl(cityName); + } + + try { + const data = await fetchData(url); + + if (data.cod === 200) { + return res.status(200).send({ + weatherText: `The temperature in ${data.name} is ${data.main.temp}°`, + }); + + } + + if(data.cod === "404" && data.message === "city not found"){ + return res.status(404).send({weatherText: "City is not found!"}) + } + + if(data.cod === 401 && data.message.includes("API")){ + return res.status(401).send({message: "Unauthorized access."}) + } + + if(data.cod === "404" && data.message === "Internal error"){ + return res.status(404).send({message: "Resource not found."}) + } + + if(data.cod >= 400 && data.cod < 600){ + return res.status(data.cod).send({message: data.message}) + } + + return res.status(500).send({message: "Unexpected error occurred."}) + + } catch (error) { + if (error.message.includes("ENOTFOUND") || error.message.includes("ERR_NAME_NOT_RESOLVED")) { + return res.status(400).send({ message: "Invalid domain name." }); + } + return res.status(500).send({ message: "Unable to fetch data." }); + } +}); + +const fetchData = async (url) => { + try { + const response = await fetch(url); + + if (!response.ok) { + const errorData = await response.json(); + return errorData; + } + + const data = await response.json(); + return data; + + } catch (error) { + throw new Error(error); + } +}; + +export default app; diff --git a/assignments/hackyourtemperature/babel.config.cjs b/assignments/hackyourtemperature/babel.config.cjs new file mode 100644 index 000000000..fbb629af6 --- /dev/null +++ b/assignments/hackyourtemperature/babel.config.cjs @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + [ + // This is a configuration, here we are telling babel what configuration to use + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + ], +}; diff --git a/assignments/hackyourtemperature/helper.js b/assignments/hackyourtemperature/helper.js new file mode 100644 index 000000000..31110a1fd --- /dev/null +++ b/assignments/hackyourtemperature/helper.js @@ -0,0 +1,31 @@ +import { API_KEY, WRONG_API_KEY } from "./sources/keys.js"; + + +const units = "metric"; + +export const getWeatherUrl = (cityName) => { + const base = "https://api.openweathermap.org/data/2.5/weather"; + return `${base}?q=${cityName}&units=${units}&appid=${API_KEY}`; +}; + +export const getWeatherUrlWithWrongApiKey = (cityName) => { + const base = "https://api.openweathermap.org/data/2.5/weather"; + return `${base}?q=${cityName}&units=${units}&appid=${WRONG_API_KEY}`; +} + +export const getWeatherUrlWithEmptyApiKey = (cityName) => { + const base = "https://api.openweathermap.org/data/2.5/weather"; + return `${base}?q=${cityName}&units=${units}&appid=${EMPTY_API_KEY}`; +} + +export const getWeatherUrlWithWrongDomain = (cityName) => { + const base = "https://api.openweahermap.org/data/2.5/weather"; + return `${base}?q=${cityName}&units=${units}&appid=${API_KEY}`; +} + +export const getWeatherUrlWithWrongEndPoint = (cityName) => { + const base = "https://api.openweathermap.org/data/2.5/weat"; + return `${base}?q=${cityName}&units=${units}&appid=${API_KEY}`; +} + + diff --git a/assignments/hackyourtemperature/jest.config.js b/assignments/hackyourtemperature/jest.config.js new file mode 100644 index 000000000..19ba9649e --- /dev/null +++ b/assignments/hackyourtemperature/jest.config.js @@ -0,0 +1,8 @@ +export default { + // Tells jest that any file that has 2 .'s in it and ends with either js or jsx should be run through the babel-jest transformer + transform: { + "^.+\\.jsx?$": "babel-jest", + }, + // By default our `node_modules` folder is ignored by jest, this tells jest to transform those as well + transformIgnorePatterns: [], +}; diff --git a/assignments/hackyourtemperature/package.json b/assignments/hackyourtemperature/package.json new file mode 100644 index 000000000..9dd440235 --- /dev/null +++ b/assignments/hackyourtemperature/package.json @@ -0,0 +1,26 @@ +{ + "name": "hackyourtemperature", + "version": "1.0.0", + "type": "module", + "main": "server.js", + "scripts": { + "start": "nodemon server.js", + "dev": "node --watch server", + "test": "jest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.2", + "express-handlebars": "^8.0.1", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@babel/preset-env": "^7.26.9", + "babel-test": "^0.2.4", + "jest": "^29.7.0", + "supertest": "^7.0.0" + } +} diff --git a/assignments/hackyourtemperature/server.js b/assignments/hackyourtemperature/server.js new file mode 100644 index 000000000..4c62a76e8 --- /dev/null +++ b/assignments/hackyourtemperature/server.js @@ -0,0 +1,11 @@ +/** + * File: server.js + * Description: Sets up a server to listen on port 3000 for incoming requests. + */ + + +import app from "./app.js" + +const PORT = 3000; + +app.listen(PORT); diff --git a/assignments/hackyourtemperature/sources/keys.js b/assignments/hackyourtemperature/sources/keys.js new file mode 100644 index 000000000..95ab511ca --- /dev/null +++ b/assignments/hackyourtemperature/sources/keys.js @@ -0,0 +1,2 @@ +export const API_KEY = 'cb13636c87c0e5ab5d6f74d5dec66670'; +export const WRONG_API_KEY = 'cb13636c87c0e5ab5d6f74d5decf6d6c670'; diff --git a/week2/prep-exercises/1-blog-API/package.json b/week2/prep-exercises/1-blog-API/package.json index d89c4bd76..ba6b539d7 100644 --- a/week2/prep-exercises/1-blog-API/package.json +++ b/week2/prep-exercises/1-blog-API/package.json @@ -2,10 +2,11 @@ "name": "1-blog-api", "version": "1.0.0", "description": "", + "type": "module", "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "node server.js", + "dev": "node --watch server" }, "author": "", "license": "ISC", diff --git a/week2/prep-exercises/1-blog-API/server.js b/week2/prep-exercises/1-blog-API/server.js index 3f615e8f5..e67e42eaf 100644 --- a/week2/prep-exercises/1-blog-API/server.js +++ b/week2/prep-exercises/1-blog-API/server.js @@ -1,10 +1,114 @@ -const express = require('express') +import express from "express"; +import path from "path"; +import fs from "fs"; + const app = express(); - - -// YOUR CODE GOES IN HERE -app.get('/', function (req, res) { - res.send('Hello World') -}) - -app.listen(3000) \ No newline at end of file + +const blogsDir = path.resolve("blogs"); + +app.use(express.json()); + +app.get("/", function (req, res) { + res.send("Hello World"); +}); + +// Submit a blog +app.post("/blogs", (req, res) => { + const { title, content } = req.body; + + if (!title || !content) { + return res.status(400).send({ message: "Content is required." }); + } + const filePath = path.join(blogsDir, title); + const submitBlog = () => { + fs.writeFileSync(filePath, content); + res.status(200).send({ message: "Blog is created successfully." }); + }; + + handleFileOperation(submitBlog, res, filePath, content); +}); + +// Update a blog +app.put("/posts/:title", (req, res) => { + const { content } = req.body; + const title = req.params.title; + + if (!title || !content) { + return res.status(400).send({ message: "Content is required." }); + } + + const filePath = path.join(blogsDir, title); + checkFileExists(filePath, res); + + const updateBlog = () => { + fs.writeFileSync(filePath, content); + res.status(201).send({ message: "Blog is updated successfully" }); + }; + + handleFileOperation(updateBlog, res, filePath, content); +}); + +// Delete a blog +app.delete("/blogs/:title", (req, res) => { + const title = req.params.title; + + const filePath = path.join(blogsDir, title); + + checkFileExists(filePath, res); + + const deleteBlog = () => { + fs.unlinkSync(filePath); + res.status(200).send({ message: "Blog is deleted successfully." }); + }; + handleFileOperation(deleteBlog, res, filePath); +}); + +// Read a blog +app.get("/blogs/:title", (req, res) => { + const title = req.params.title; + const filePath = path.join(blogsDir, title); + + checkFileExists(filePath, res); + + const encoding = "utf-8"; + const readBlog = () => { + const post = fs.readFileSync(filePath, encoding); + res.setHeader("Content-Type", "text/plain"); + res.status(200).send(post); + }; + + handleFileOperation(readBlog, res, filePath, encoding); +}); + +// BONUS: Get all blogs +app.get("/blogs/", (req, res) => { + const blogs = fs.readdirSync(blogsDir); + const getBlogs = () => { + if (!blogs) { + res.status(500).send({ message: "No blog is found." }); + } + const blogTitles = blogs.map((blog) => ({ title: blog })); + res.status(200).send(blogTitles); + }; + + handleFileOperation(getBlogs, res, blogs); +}); + +/** A helper function that checks if a file operation is a success or failure. */ + +const handleFileOperation = (operation, ...args) => { + try { + return operation(...args); + } catch (err) { + res.status(500).send({ message: `Error occurred while ${msg}` }); + } +}; + +/*Checks if a specified file exists or not. */ +const checkFileExists = (filePath, res) => { + if (!fs.existsSync(filePath)) { + return res.status(404).send("This blog does not exist."); + } +}; + +app.listen(3000);