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);
+ });
+ });
+ });
});
});
});