From 2b7562de1d5f81485d7280da83452109ff9260b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zsolt=20Sz=2E=20Sztup=C3=A1k?= Date: Tue, 5 Dec 2023 01:11:49 +0000 Subject: [PATCH] Finish the rest of the features --- .github/workflows/run-features.yml | 2 +- .github/workflows/run-server-tests.yml | 2 +- client/src/App.js | 182 +++++++++++++------------ client/src/components/Video.js | 24 +++- client/src/index.css | 35 +++++ client/test/App.functions.test.js | 169 +++++++++++++++++++++++ client/test/App.test.js | 97 ++++++------- client/test/components/Video.test.js | 6 + db/initdb.sql | 19 +++ server/api.js | 151 +++++++++++++++++++- server/api.test.js | 136 ++++++++++++++++++ 11 files changed, 674 insertions(+), 149 deletions(-) create mode 100644 client/test/App.functions.test.js diff --git a/.github/workflows/run-features.yml b/.github/workflows/run-features.yml index 79a184ae89..73d52a45c0 100644 --- a/.github/workflows/run-features.yml +++ b/.github/workflows/run-features.yml @@ -4,7 +4,7 @@ run-name: Enforce selenium feature tests pass on committed files on: workflow_dispatch: - # pull_request: + pull_request: jobs: run-features: diff --git a/.github/workflows/run-server-tests.yml b/.github/workflows/run-server-tests.yml index 211105d5fd..79ee336fac 100644 --- a/.github/workflows/run-server-tests.yml +++ b/.github/workflows/run-server-tests.yml @@ -4,7 +4,7 @@ run-name: Enforce tests pass on committed files on: workflow_dispatch: - # pull_request: + pull_request: jobs: run-server-tests: diff --git a/client/src/App.js b/client/src/App.js index fc106c55f1..e31790cc82 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -4,120 +4,133 @@ import VideoList from "./components/VideoList"; import VideoSubmission from "./components/VideoSubmission"; import OrderingSelector from "./components/OrderingSelector"; -const VIDEO_LIST = [ - { - id: 523523, - title: "Never Gonna Give You Up", - url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - rating: 23, - }, - { - id: 523427, - title: "The Coding Train", - url: "https://www.youtube.com/watch?v=HerCR8bw_GE", - rating: 230, - }, - { - id: 82653, - title: "Mac & Cheese | Basics with Babish", - url: "https://www.youtube.com/watch?v=FUeyrEN14Rk", - rating: 2111, - }, - { - id: 858566, - title: "Videos for Cats to Watch - 8 Hour Bird Bonanza", - url: "https://www.youtube.com/watch?v=xbs7FT7dXYc", - rating: 11, - }, - { - id: 453538, - title: - "The Complete London 2012 Opening Ceremony | London 2012 Olympic Games", - url: "https://www.youtube.com/watch?v=4As0e4de-rI", - rating: 3211, - }, - { - id: 283634, - title: "Learn Unity - Beginner's Game Development Course", - url: "https://www.youtube.com/watch?v=gB1F9G0JXOo", - rating: 211, - }, - { - id: 562824, - title: "Cracking Enigma in 2021 - Computerphile", - url: "https://www.youtube.com/watch?v=RzWB5jL5RX0", - rating: 111, - }, - { - id: 442452, - title: "Coding Adventure: Chess AI", - url: "https://www.youtube.com/watch?v=U4ogK0MIzqk", - rating: 671, - }, - { - id: 536363, - title: "Coding Adventure: Ant and Slime Simulations", - url: "https://www.youtube.com/watch?v=X-iSQQgOd1A", - rating: 76, - }, - { - id: 323445, - title: "Why the Tour de France is so brutal", - url: "https://www.youtube.com/watch?v=ZacOS8NBK6U", - rating: 73, - }, -]; - const App = () => { let [videos, setVideos] = useState([]); + let [message, setMessage] = useState(null); let [order, setOrder] = useState("id"); - function orderVideos(videos, order, initial) { + function orderVideos(videos, order) { switch (order) { case "rating_asc": return videos.sort((a, b) => a.rating - b.rating); case "rating_desc": return videos.sort((a, b) => b.rating - a.rating); case "random": - if (initial) { - return videos.sort(() => 0.5 - Math.random()); - } else { - return videos; // this should only sort once when getting from backend, then keep it as-is - } + return videos; // this should only sort once when getting from backend, then keep it as-is case "id": return videos.sort((a, b) => a.id - b.id); } } useEffect(() => { - setVideos([...orderVideos(VIDEO_LIST, order, true)]); - }, [setVideos, order]); + const fetchVideos = async () => { + try { + const videoResults = await fetch(`/api/videos?order=${order}`); + const videoResultsJson = await videoResults.json(); + if (videoResultsJson.success) { + setVideos(videoResultsJson.data); + setMessage(null); + } else { + setMessage( + videoResultsJson.message || + "Error while loading video recommendations, please try again by reloading the page!" + ); + } + } catch (error) { + setMessage( + "Error while loading video recommendations, please try again by reloading the page!" + ); + } + }; + fetchVideos(); + }, [setVideos, setMessage, order]); const addVideo = function (title, url) { - const newVideo = { - id: Math.max(...videos.map((v) => v.id)) + 1, - title: title, - url: url, - rating: 0, + const publishToApi = async () => { + try { + const results = await fetch("/api/videos", { + method: "POST", + body: JSON.stringify({ title: title, url: url }), + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + }); + const data = await results.json(); + if (data.success) { + setVideos(orderVideos([...videos, data.data], order)); + } else { + setMessage( + data.message || + "Error while publishing the new video. Please reload the page and try again!" + ); + } + } catch (error) { + setMessage( + "Error while publishing the new video. Please reload the page and try again!" + ); + } }; - VIDEO_LIST.push(newVideo); - setVideos([...orderVideos(VIDEO_LIST, order)]); + publishToApi(); }; const updateVideo = function (video, action) { const deleteVideo = async (selectedVideo) => { - VIDEO_LIST.splice(VIDEO_LIST.indexOf(selectedVideo), 1); - setVideos([...orderVideos(VIDEO_LIST, order)]); + try { + const results = await fetch(`/api/videos/${selectedVideo.id}`, { + method: "DELETE", + headers: { + "Access-Control-Allow-Origin": "*", + }, + }); + const data = await results.json(); + if (data.success) { + videos = videos.filter((e) => e.id !== selectedVideo.id); + } else { + selectedVideo.message = + data.message || + "There was an error while trying to delete the video. Please reload the page and try again!"; + } + } catch (error) { + selectedVideo.message = + "There was an error while trying to delete the video. Please reload the page and try again!"; + } + setVideos(orderVideos([...videos], order)); }; const voteOnVideo = async (selectedVideo, action) => { - selectedVideo.rating += action == "up" ? 1 : -1; - setVideos([...orderVideos(VIDEO_LIST, order)]); + try { + const results = await fetch( + `/api/videos/${selectedVideo.id}/${action}`, + { + method: "POST", + headers: { + "Access-Control-Allow-Origin": "*", + }, + } + ); + const data = await results.json(); + if (data.success) { + selectedVideo.rating = data.data.rating; + selectedVideo.message = null; + } else { + selectedVideo.message = + data.message || + "There was an error while updating rating to the video. Please reload the page and try again!"; + } + } catch (error) { + selectedVideo.message = + "There was an error while updating rating to the video. Please reload the page and try again!"; + } + setVideos(orderVideos([...videos], order)); }; - let selectedVideo = VIDEO_LIST.find((e) => e.id === video.id); + let selectedVideo = videos.find((e) => e.id === video.id); if (selectedVideo) { + selectedVideo.message = " "; + setVideos(orderVideos([...videos], order)); + switch (action) { case "up": case "down": @@ -133,6 +146,7 @@ const App = () => { return ( <>

Video Recommendations

+ {message &&

{message}

} diff --git a/client/src/components/Video.js b/client/src/components/Video.js index de8cd1b3f1..7f5bbad4c3 100644 --- a/client/src/components/Video.js +++ b/client/src/components/Video.js @@ -25,17 +25,35 @@ export default function Video({ video, updateVideo }) { allowFullScreen > )} +

Recommended since

+
+ {new Date(video.created_at).toLocaleString()} +

Rating

{video.rating}

Controls

+
{video.message}
- - - + +
); diff --git a/client/src/index.css b/client/src/index.css index 049ca11599..95c43abf02 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -7,6 +7,15 @@ h1 { text-align: center; } +.message { + width: auto; + padding: 1em; + margin: 1em; + border: 1px solid red; + border-radius: 5px; + background-color: lightsalmon; +} + #videos { display: grid; grid-auto-flow: row; @@ -83,6 +92,25 @@ h1 { background-color: bisque; } +.video .recommended-since:before { + content: "🕒 "; +} + +.video .recommended-since { + max-width: fit-content; + padding: 0.5em; + + display: block; + font-size: 1em; + + margin: 0.5em auto; + + border: 1px solid black; + border-radius: 5px; + + background-color: bisque; +} + .video h3 { position: absolute; left: -10000px; @@ -118,6 +146,13 @@ h1 { grid-template-columns: 1fr 1fr 1fr; } +.control-message { + position: absolute; + bottom: 0; + background-color: white; + border: 1px solid red; +} + #submit-video { text-align: center; width: auto; diff --git a/client/test/App.functions.test.js b/client/test/App.functions.test.js new file mode 100644 index 0000000000..144c486018 --- /dev/null +++ b/client/test/App.functions.test.js @@ -0,0 +1,169 @@ +import { render, screen, act } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import fetchMock from "fetch-mock-jest"; + +import App from "../src/App"; + +let updateVideoFunction = null; +let addVideoFunction = null; +let videosList = null; + +jest.mock( + "../src/components/VideoList", + () => + function VideoList({ videos, updateVideo }) { + updateVideoFunction = updateVideo; + videosList = videos; + return ( + + {videos.map((v) => ( +
{v.title}
+ ))} +
+ ); + } +); +jest.mock( + "../src/components/VideoSubmission", + () => + function VideoSubmission({ addVideo }) { + addVideoFunction = addVideo; + return ; + } +); + +describe("AddVideo", () => { + beforeEach(async () => { + fetchMock.reset(); + fetchMock.get("/api/videos?order=id", { + success: true, + data: [], + }); + render(); + + await screen.findByText("Video Recommendations"); + }); + + it("Calls the relevant endpoint to create a new video", async () => { + fetchMock.post("/api/videos", { + success: true, + data: { id: 1, title: "Title", url: "Url", rating: 0 }, + }); + + await act(async () => await addVideoFunction("Title", "Url")); + + expect(fetchMock).toHaveFetched("matched", { + method: "post", + url: "/api/videos", + body: { title: "Title", url: "Url" }, + }); + }); + + it("Adds the new video to the list", async () => { + fetchMock.post("/api/videos", { + success: true, + data: { id: 1, title: "TheNewTitle", url: "Url", rating: 0 }, + }); + + await act(async () => await addVideoFunction("TheNewTitle", "Url")); + + expect(videosList).toHaveLength(1); + expect(videosList[0].title).toBe("TheNewTitle"); + }); + + it("Shows an error message in case something went wrong", async () => { + fetchMock.post("/api/videos", { success: false, message: "The Message" }); + + await act(async () => await addVideoFunction("Title", "Url")); + + expect(screen.getByText("The Message")).toBeInTheDocument(); + }); +}); + +describe("UpdateVideo", () => { + beforeEach(async () => { + fetchMock.reset(); + fetchMock.get("/api/videos?order=id", { + success: true, + data: [{ id: 1, title: "The Title", url: "Url", rating: 10 }], + }); + render(); + + await screen.findByText("The Title"); + }); + + ["up", "down"].forEach((action) => { + describe(`Using action ${action}`, () => { + it("Calls the relevant endpoint to rank up the video", async () => { + fetchMock.post("/api/videos/1/" + action, { + success: true, + data: { id: 1, rating: 11 }, + }); + + await act(async () => await updateVideoFunction({ id: 1 }, action)); + + expect(fetchMock).toHaveFetched("matched", { + method: "post", + url: "/api/videos/1/up", + }); + }); + + it("Updates the rank of the video", async () => { + fetchMock.post("/api/videos/1/" + action, { + success: true, + data: { id: 1, rating: 11 }, + }); + + await act(async () => await updateVideoFunction({ id: 1 }, action)); + + expect(videosList).toHaveLength(1); + expect(videosList[0].rating).toBe(11); + }); + + it("Sets an error message in case there are issues", async () => { + fetchMock.post("/api/videos/1/" + action, { + success: false, + message: "Not okay", + }); + + await act(async () => await updateVideoFunction({ id: 1 }, action)); + + expect(videosList).toHaveLength(1); + expect(videosList[0].message).toBe("Not okay"); + }); + }); + }); + + describe("Using action delete", () => { + it("Calls the relevant endpoint to rank up the video", async () => { + fetchMock.delete("/api/videos/1", { success: true }); + + await act(async () => await updateVideoFunction({ id: 1 }, "delete")); + + expect(fetchMock).toHaveFetched("matched", { + method: "delete", + url: "/api/videos/1", + }); + }); + + it("Removes the video from the list", async () => { + fetchMock.delete("/api/videos/1", { success: true }); + + await act(async () => await updateVideoFunction({ id: 1 }, "delete")); + + expect(videosList).toHaveLength(0); + }); + + it("Sets an error message in case there are issues", async () => { + fetchMock.delete("/api/videos/1", { + success: false, + message: "Not okay", + }); + + await act(async () => await updateVideoFunction({ id: 1 }, "delete")); + + expect(videosList).toHaveLength(1); + expect(videosList[0].message).toBe("Not okay"); + }); + }); +}); diff --git a/client/test/App.test.js b/client/test/App.test.js index d8e7773860..37934dd290 100644 --- a/client/test/App.test.js +++ b/client/test/App.test.js @@ -1,31 +1,29 @@ import { render, screen, fireEvent, act } from "@testing-library/react"; import "@testing-library/jest-dom"; -// Uncomment when you link the backend and frontend together -// import fetchMock from "fetch-mock-jest"; +import fetchMock from "fetch-mock-jest"; import App from "../src/App"; describe("Main Page", () => { beforeEach(async () => { - // Uncomment when you link the backend and frontend together - // fetchMock.reset(); - // fetchMock.get("/api/videos?order=id", { - // success: true, - // data: [ - // { - // id: 1, - // title: "Never Gonna Give You Up", - // url: "https://www.youtube.com/watch?v=ABCDEFGHIJK", - // rating: 1234, - // }, - // { - // id: 2, - // title: "Other Title", - // url: "https://www.youtube.com/watch?v=KJIHGFEDCBA", - // rating: 4321, - // }, - // ], - // }); + fetchMock.reset(); + fetchMock.get("/api/videos?order=id", { + success: true, + data: [ + { + id: 1, + title: "Never Gonna Give You Up", + url: "https://www.youtube.com/watch?v=ABCDEFGHIJK", + rating: 1234, + }, + { + id: 2, + title: "Other Title", + url: "https://www.youtube.com/watch?v=KJIHGFEDCBA", + rating: 4321, + }, + ], + }); render(); @@ -37,16 +35,11 @@ describe("Main Page", () => { (_, e) => e.tagName.toLowerCase() === "iframe" ); - expect(videoContainers.length).toBe(10); + expect(videoContainers.length).toBe(2); }); it("Removes the video when asked to do", async () => { - // Uncomment when you link the backend and frontend together - // fetchMock.delete("/api/videos/1", { success: true }); - - const videoContainersBeforeDelete = screen.getAllByText( - (_, e) => e.tagName.toLowerCase() === "iframe" - ); + fetchMock.delete("/api/videos/1", { success: true }); const deleteButton = screen.getAllByText("Remove video")[0]; @@ -58,24 +51,19 @@ describe("Main Page", () => { (_, e) => e.tagName.toLowerCase() === "iframe" ); - expect(videoContainers.length).toBe(videoContainersBeforeDelete.length - 1); + expect(videoContainers.length).toBe(1); }); it("Adds a new video when asked to do", async () => { - // Uncomment when you link the backend and frontend together - // fetchMock.post("/api/videos", { - // success: true, - // data: { - // id: 3, - // title: "New Title", - // url: "https://www.youtube.com/watch?v=CDEYRFUTURE", - // rating: 0, - // }, - // }); - - const videoContainersBeforeAdd = screen.getAllByText( - (_, e) => e.tagName.toLowerCase() === "iframe" - ); + fetchMock.post("/api/videos", { + success: true, + data: { + id: 3, + title: "New Title", + url: "https://www.youtube.com/watch?v=CDEYRFUTURE", + rating: 0, + }, + }); fireEvent.change(screen.getByRole("textbox", { name: "Title:" }), { target: { value: "New Title" }, @@ -88,22 +76,19 @@ describe("Main Page", () => { fireEvent.click(screen.getByRole("button", { name: "Submit" })); }); - await screen.findByText("New Title"); - const videoContainers = screen.getAllByText( (_, e) => e.tagName.toLowerCase() === "iframe" ); - expect(videoContainers.length).toBe(videoContainersBeforeAdd.length + 1); - - // Uncomment when you link the backend and frontend together - // expect(fetchMock).toHaveFetched("matched", { - // method: "post", - // url: "/api/videos", - // body: { - // title: "New Title", - // url: "https://www.youtube.com/watch?v=CDEYRFUTURE", - // }, - // }); + expect(videoContainers.length).toBe(3); + + expect(fetchMock).toHaveFetched("matched", { + method: "post", + url: "/api/videos", + body: { + title: "New Title", + url: "https://www.youtube.com/watch?v=CDEYRFUTURE", + }, + }); }); }); diff --git a/client/test/components/Video.test.js b/client/test/components/Video.test.js index 69a3b53d5b..1ae7890240 100644 --- a/client/test/components/Video.test.js +++ b/client/test/components/Video.test.js @@ -35,6 +35,12 @@ describe("Video component", () => { it("Renders the rating", async () => { expect(screen.getByText(1234)).toBeInTheDocument(); }); + + it("Renders the recommended since date in the user's locale", async () => { + expect( + screen.getByText(new Date(video.created_at).toLocaleString()) + ).toBeInTheDocument(); + }); }); describe("Actions", () => { diff --git a/db/initdb.sql b/db/initdb.sql index 0453588fe4..31ff662545 100644 --- a/db/initdb.sql +++ b/db/initdb.sql @@ -1 +1,20 @@ DROP TABLE IF EXISTS videos CASCADE; + +CREATE TABLE videos ( + id SERIAL, + title TEXT, + url TEXT, + rating INT, + created_at TIMESTAMP DEFAULT NOW() +); + +INSERT INTO videos (title,url,rating) VALUES ('Never Gonna Give You Up','https://www.youtube.com/watch?v=dQw4w9WgXcQ',23); +INSERT INTO videos (title,url,rating) VALUES ('The Coding Train','https://www.youtube.com/watch?v=HerCR8bw_GE',230); +INSERT INTO videos (title,url,rating) VALUES ('Mac & Cheese | Basics with Babish','https://www.youtube.com/watch?v=FUeyrEN14Rk',2111); +INSERT INTO videos (title,url,rating) VALUES ('Videos for Cats to Watch - 8 Hour Bird Bonanza','https://www.youtube.com/watch?v=xbs7FT7dXYc',11); +INSERT INTO videos (title,url,rating) VALUES ('The Complete London 2012 Opening Ceremony | London 2012 Olympic Games','https://www.youtube.com/watch?v=4As0e4de-rI',3211); +INSERT INTO videos (title,url,rating) VALUES ('Learn Unity - Beginner''s Game Development Course','https://www.youtube.com/watch?v=gB1F9G0JXOo',211); +INSERT INTO videos (title,url,rating) VALUES ('Cracking Enigma in 2021 - Computerphile','https://www.youtube.com/watch?v=RzWB5jL5RX0',111); +INSERT INTO videos (title,url,rating) VALUES ('Coding Adventure: Chess AI','https://www.youtube.com/watch?v=U4ogK0MIzqk',671); +INSERT INTO videos (title,url,rating) VALUES ('Coding Adventure: Ant and Slime Simulations','https://www.youtube.com/watch?v=X-iSQQgOd1A',76); +INSERT INTO videos (title,url,rating) VALUES ('Why the Tour de France is so brutal','https://www.youtube.com/watch?v=ZacOS8NBK6U',73); diff --git a/server/api.js b/server/api.js index 891b1df31a..209cd50b5e 100644 --- a/server/api.js +++ b/server/api.js @@ -1,10 +1,153 @@ import { Router } from "express"; -//import db from "./db"; - +import db from "./db"; const router = Router(); -router.get("/videos", async (_, res) => { - res.status(200).json([]); +// source: https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url +function youtubeLinkParser(url) { + let regExp = + /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; + let match = url.match(regExp); + return match && match[7].length == 11 ? match[7] : false; +} + +router.get("/videos", async (req, res) => { + try { + let orderString = "ORDER BY id ASC"; + switch (req.query.order) { + case "rating_asc": + orderString = "ORDER BY rating ASC"; + break; + case "rating_desc": + orderString = "ORDER BY rating DESC"; + break; + case "random": + orderString = "ORDER BY random()"; + break; + } + const result = await db.query("SELECT * FROM videos " + orderString); + res.status(200).json({ + success: true, + total: result.rows.length, + data: result.rows, + }); + } catch (error) { + res + .status(500) + .json({ success: false, message: "Could not download the video list!" }); + } +}); + +router.get("/videos/:id", async (req, res) => { + try { + const result = await db.query("SELECT * FROM videos WHERE id = $1", [ + req.params.id, + ]); + if (result.rows.length !== 1) { + return res + .status(404) + .json({ success: false, message: "Could not find video" }); + } + + res.status(200).json({ success: true, data: result.rows[0] }); + } catch (error) { + res + .status(500) + .json({ success: false, message: "Could not obtain video!" }); + } +}); + +router.post("/videos/:id/:action", async (req, res) => { + try { + if (req.params.action !== "up" && req.params.action !== "down") { + return res + .status(422) + .json({ success: false, message: "Invalid action" }); + } + + const result = await db.query("SELECT * FROM videos WHERE id = $1", [ + req.params.id, + ]); + if (result.rows.length !== 1) { + return res + .status(404) + .json({ success: false, message: "Could not find video" }); + } + + const operator = req.params.action == "up" ? "+" : "-"; + + const updateResult = await db.query( + `UPDATE videos SET rating = rating ${operator} 1 WHERE id = $1 RETURNING *`, + [req.params.id] + ); + + if (updateResult.rows.length !== 1) { + return res + .status(500) + .json({ success: false, message: "Error while updating rating" }); + } + + res.status(200).json({ success: true, data: updateResult.rows[0] }); + } catch (error) { + res + .status(500) + .json({ success: false, message: "Could not update video!" }); + } +}); + +router.delete("/videos/:id", async (req, res) => { + try { + const result = await db.query("DELETE FROM videos WHERE id = $1", [ + req.params.id, + ]); + if (result.rowCount !== 1) { + return res + .status(404) + .json({ success: false, message: "Could not find video" }); + } + + res.status(200).json({ success: true, message: "Video deleted" }); + } catch (error) { + res + .status(500) + .json({ success: false, message: "Could not delete video!" }); + } +}); + +router.post("/videos", async (req, res) => { + try { + const body = req.body; + if (!body.url || !body.title || !youtubeLinkParser(body.url)) { + return res + .status(422) + .json({ success: false, message: "Missing or invalid input!" }); + } + + const insertResult = await db.query( + "INSERT INTO videos (title,url,rating) VALUES ($1,$2,$3) RETURNING id", + [body.title, body.url, 0] + ); + if (insertResult.rows.length !== 1) { + return res.status(500).json({ + success: false, + message: "Server error during video creation, please reload the page!", + }); + } + + const id = insertResult.rows[0].id; + const result = await db.query("SELECT * FROM videos WHERE id = $1", [id]); + if (result.rows.length !== 1) { + return res.status(500).json({ + success: false, + message: "Server error during video creation, please reload the page!", + }); + } + + res.status(201).json({ success: true, data: result.rows[0] }); + } catch (error) { + res + .status(500) + .json({ success: false, message: "Could not create video!" }); + } }); export default router; diff --git a/server/api.test.js b/server/api.test.js index 66c85203bf..758dbd4e0d 100644 --- a/server/api.test.js +++ b/server/api.test.js @@ -17,6 +17,38 @@ describe("/api", () => { ); expect(response.body.data[0].rating).toBe(23); }); + + it("Returns the list of videos ordered by rating ascending", async () => { + const response = await request(app).get("/api/videos?order=rating_asc"); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(10); + expect(response.body.success).toBe(true); + expect(response.body.data[0].title).toBe( + "Videos for Cats to Watch - 8 Hour Bird Bonanza" + ); + expect(response.body.data[0].url).toBe( + "https://www.youtube.com/watch?v=xbs7FT7dXYc" + ); + expect(response.body.data[0].rating).toBe(11); + }); + + it("Returns the list of videos ordered by rating descending", async () => { + const response = await request(app).get( + "/api/videos?order=rating_desc" + ); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(10); + expect(response.body.success).toBe(true); + expect(response.body.data[9].title).toBe( + "Videos for Cats to Watch - 8 Hour Bird Bonanza" + ); + expect(response.body.data[9].url).toBe( + "https://www.youtube.com/watch?v=xbs7FT7dXYc" + ); + expect(response.body.data[9].rating).toBe(11); + }); }); describe("POST", () => { @@ -50,6 +82,110 @@ describe("/api", () => { "https://www.youtube.com/watch?v=ABCDEFGHIJK" ); }); + + it("Does not create a video for invalid urls", async () => { + const response = await request(app).post("/api/videos").send({ + title: "New Title", + url: "https://www.youtube.com/watch?v=ABCDEFGHIJ", + }); + + expect(response.statusCode).toBe(422); + expect(response.body.success).toBe(false); + }); + }); + + describe("/:id", () => { + describe("GET", () => { + it("Gets the video from the database if the id exists", async () => { + const response = await request(app).get("/api/videos/1"); + + expect(response.statusCode).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.title).toBe("Never Gonna Give You Up"); + expect(response.body.data.url).toBe( + "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + ); + expect(response.body.data.rating).toBe(23); + }); + + it("Returns 404 if the id doesn't exist", async () => { + const response = await request(app).get("/api/videos/999999"); + + expect(response.statusCode).toBe(404); + expect(response.body.success).toBe(false); + }); + }); + + describe("DELETE", () => { + it("Returns a successful response if the id exists", async () => { + const response = await request(app).delete("/api/videos/1"); + + expect(response.statusCode).toBe(200); + expect(response.body.success).toBe(true); + }); + + it("Deletes the video from the database if the id exists", async () => { + await request(app).delete("/api/videos/1"); + + const dbResponse = await db.query( + "SELECT * FROM videos WHERE id = $1", + [1] + ); + expect(dbResponse.rows.length).toBe(0); + }); + + it("Returns 404 if the id doesn't exist", async () => { + const response = await request(app).delete("/api/videos/999999"); + + expect(response.statusCode).toBe(404); + expect(response.body.success).toBe(false); + }); + }); + + describe("/:action", () => { + describe("POST", () => { + it("Updates the rating up", async () => { + const response = await request(app).post("/api/videos/1/up"); + + expect(response.statusCode).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.rating).toBe(24); + }); + + it("Updates the rating down", async () => { + const response = await request(app).post("/api/videos/1/down"); + + expect(response.statusCode).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.rating).toBe(22); + }); + + it("Updates the rating in the database", async () => { + await request(app).post("/api/videos/1/up"); + + const dbResponse = await db.query( + "SELECT * FROM videos WHERE id = $1", + [1] + ); + + expect(dbResponse.rows[0].rating).toBe(24); + }); + + it("Returns 422 if the action is invalid", async () => { + const response = await request(app).post("/api/videos/1/left"); + + expect(response.statusCode).toBe(422); + expect(response.body.success).toBe(false); + }); + + it("Returns 404 if the id doesn't exist", async () => { + const response = await request(app).post("/api/videos/999999/up"); + + expect(response.statusCode).toBe(404); + expect(response.body.success).toBe(false); + }); + }); + }); }); }); });