diff --git a/.env.example b/.env.example deleted file mode 100644 index 60f3a816..00000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -DATABASE_URL="YOUR_DB_URL" - -# We need the following URL environment variable for test purposes: -# - TEST_DATABASE_URL must be a **completely separate** database from any other used in this file - -TEST_DATABASE_URL="YOUR_TEST_DB_URL" diff --git a/package-lock.json b/package-lock.json index 0fc727e5..a9b8b9e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" }, "devDependencies": { @@ -2146,6 +2147,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index ebaf03be..0962ebbb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,14 @@ "start": "npx nodemon ./src/index.js", "test:migration": "node test/testDbMigration.js", "test": "npx jest -i test/api/routes", - "test-extensions": "npx jest -i test/api/extensions --forceExit" + "test-extensions": "npx jest -i test/api/extensions --forceExit", + "test-movies": "npx jest -i test/api/routes/movies.spec.js", + "test-customer": "npx jest -i test/api/routes/customer.spec.js", + "test-movies-extensions": "npx jest -i test/api/extensions/movies-ext.spec.js --forceExit", + "test-customer-extensions": "npx jest -i test/api/extensions/customer-ext.spec.js --forceExit", + "test-screens-extensions": "npx jest -i test/api/extensions/screens-ext.spec.js --forceExit", + "test-tickets-extensions": "npx jest -i test/api/extensions/tickets-ext.spec.js --forceExit", + "test-reviews-extensions": "npx jest -i test/api/extensions/reviews-ext.spec.js --forceExit" }, "prisma": { "seed": "node prisma/seed.js" @@ -34,6 +41,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "morgan": "^1.10.0" } } diff --git a/prisma/migrations/20240702092124_added_unique_contraint_screen_number/migration.sql b/prisma/migrations/20240702092124_added_unique_contraint_screen_number/migration.sql new file mode 100644 index 00000000..e6b1f2d7 --- /dev/null +++ b/prisma/migrations/20240702092124_added_unique_contraint_screen_number/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[number]` on the table `Screen` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Screen_number_key" ON "Screen"("number"); diff --git a/prisma/migrations/20240702162530_movie_title_unique_field/migration.sql b/prisma/migrations/20240702162530_movie_title_unique_field/migration.sql new file mode 100644 index 00000000..f5af5dc9 --- /dev/null +++ b/prisma/migrations/20240702162530_movie_title_unique_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[title]` on the table `Movie` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Movie_title_key" ON "Movie"("title"); diff --git a/prisma/migrations/20240703083007_reviews/migration.sql b/prisma/migrations/20240703083007_reviews/migration.sql new file mode 100644 index 00000000..0add6c75 --- /dev/null +++ b/prisma/migrations/20240703083007_reviews/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "Review" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "title" VARCHAR(100) NOT NULL, + "customerId" INTEGER NOT NULL, + "movieId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_movieId_fkey" FOREIGN KEY ("movieId") REFERENCES "Movie"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dd9b27f1..1790580f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,6 +16,7 @@ model Customer { name String contact Contact? tickets Ticket[] + reviews Review[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -33,15 +34,16 @@ model Contact { model Movie { id Int @id @default(autoincrement()) screenings Screening[] - title String + title String @unique runtimeMins Int + reviews Review[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Screen { - id Int @id @default(autoincrement()) - number Int + id Int @id @default(autoincrement()) + number Int @unique screenings Screening[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -68,3 +70,15 @@ model Ticket { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Review { + id Int @id @default(autoincrement()) + content String @db.Text + title String @db.VarChar(100) + customerId Int + customer Customer @relation(fields: [customerId], references: [id]) + movieId Int + movie Movie @relation(fields: [movieId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} \ No newline at end of file diff --git a/src/controllers/customer.js b/src/controllers/customer.js index 775cfb42..a62b2f37 100644 --- a/src/controllers/customer.js +++ b/src/controllers/customer.js @@ -1,48 +1,62 @@ -const { PrismaClientKnownRequestError } = require("@prisma/client") -const { createCustomerDb } = require('../domains/customer.js') +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { + createCustomerDb, + updateCustomerByIdDb, +} = require("../domains/customer.js"); +const { DataNotFoundError, MissingFieldsError } = require("../errors/errors.js"); const createCustomer = async (req, res) => { - const { - name, - phone, - email - } = req.body + const { name, phone, email } = req.body; if (!name || !phone || !email) { return res.status(400).json({ - error: "Missing fields in request body" - }) + error: "Missing fields in request body", + }); } - // Try-catch is a very common way to handle errors in JavaScript. - // It allows us to customise how we want errors that are thrown to be handled. - // Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch - - // Here, if Prisma throws an error in the process of trying to create a new customer, - // instead of the Prisma error being thrown (and the app potentially crashing) we exit the - // `try` block (bypassing the `res.status` code) and enter the `catch` block. try { - const createdCustomer = await createCustomerDb(name, phone, email) - - res.status(201).json({ customer: createdCustomer }) + const createdCustomer = await createCustomerDb(name, phone, email); + res.status(201).json({ customer: createdCustomer }); } catch (e) { - // In this catch block, we are able to specify how different Prisma errors are handled. - // Prisma throws errors with its own codes. P2002 is the error code for - // "Unique constraint failed on the {constraint}". In our case, the {constraint} is the - // email field which we have set as needing to be unique in the prisma.schema. - // To handle this, we return a custom 409 (conflict) error as a response to the client. - // Prisma error codes: https://www.prisma.io/docs/orm/reference/error-reference#common - // HTTP error codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses if (e instanceof PrismaClientKnownRequestError) { if (e.code === "P2002") { - return res.status(409).json({ error: "A customer with the provided email already exists" }) + return res + .status(409) + .json({ error: "A customer with the provided email already exists" }); } } - res.status(500).json({ error: e.message }) + res.status(500).json({ error: e.message }); + } +}; + +async function updateCustomerById(req, res) { + const id = Number(req.params.id); + if (isNaN(id)) { + throw new DataNotFoundError('No customer found with that ID') + } + + const newProps = req.body; + if (!newProps.name) { + throw new MissingFieldsError('Customers require a name') + } + + try { + const customer = await updateCustomerByIdDb(id, newProps); + res.status(201).json({ customer }); + } catch (e) { + if (e.code === 'P2025') { + return res.status(404).json({ error: e.message }) + } } } module.exports = { - createCustomer -} + createCustomer, + updateCustomerById, +}; + + + + // Prisma error codes: https://www.prisma.io/docs/orm/reference/error-reference#common + // HTTP error codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses \ No newline at end of file diff --git a/src/controllers/movies.js b/src/controllers/movies.js new file mode 100644 index 00000000..53a385a5 --- /dev/null +++ b/src/controllers/movies.js @@ -0,0 +1,81 @@ +const { PrismaClientKnownRequestError } = require("@prisma/client"); +const { + getMoviesDb, + createMovieDb, + getMovieByIdDb, + updateMovieByIdDb, + getMoviesWithQueryDb, +} = require("../domains/movies.js"); +const { + MissingFieldsError, +} = require("../errors/errors.js"); + +async function getMovies(req, res) { + let movies; + if (Object.keys(req.query).length > 0) { + movies = await getMoviesWithQueryDb(req.query); + } else { + + movies = await getMoviesDb(); + } + res.status(200).json({ movies }); +} + +async function createMovie(req, res) { + const newMovie = req.body; + + const requiredFields = ["title", "runtimeMins"]; + if (!requiredFields.every((field) => newMovie[field])) { + throw new MissingFieldsError("Movies require a title and runtime"); + } + + try { + const movie = await createMovieDb(newMovie); + res.status(201).json({ movie }); + } catch (e) { + if (e.code === "P2002") { + res.status(409).json({ error: "A movie with that title already exists" }); + } + } +} + +async function getMovieById(req, res) { + const id = Number(req.params.id); + + try { + const movie = await getMovieByIdDb(id); + res.status(200).json({ movie }); + } catch (e) { + if (e.code === "P2025") { + res.status(404).json({ error: "No movie found with that ID" }); + } + } +} + +async function updateMovieById(req, res) { + const id = Number(req.params.id); + const updatedProps = req.body; + + if (!updatedProps.title && !updatedProps.runtimeMins) { + throw new MissingFieldsError("Updating movies requires a title or runtime"); + } + + try { + const movie = await updateMovieByIdDb(id, updatedProps); + res.status(201).json({ movie }); + } catch (e) { + if (e.code === "P2025") { + res.status(404).json({ error: "No movie found with that ID" }); + } + if (e.code === "P2002") { + res.status(409).json({ error: "A movie with that title already exists" }); + } + } +} + +module.exports = { + getMovies, + createMovie, + getMovieById, + updateMovieById, +}; diff --git a/src/controllers/reviews.js b/src/controllers/reviews.js new file mode 100644 index 00000000..5517df35 --- /dev/null +++ b/src/controllers/reviews.js @@ -0,0 +1,21 @@ +const { getAllReviewsDb, createReviewDb } = require("../domains/reviews") +const { MissingFieldsError } = require("../errors/errors") + +async function getAllReviews(req, res) { + const reviews = await getAllReviewsDb() + res.status(200).json( {reviews} ) +} + +async function createReview(req, res) { + const { customerId, movieId, title, content } = req.body + + if (!customerId || !movieId || !title || !content) { + throw new MissingFieldsError("Reviews require a customer ID, movie ID, title, and content") + } + + const review = await createReviewDb(customerId, movieId, title, content ) + + res.status(201).json({ review }) +} + +module.exports = { getAllReviews, createReview } \ No newline at end of file diff --git a/src/controllers/screens.js b/src/controllers/screens.js new file mode 100644 index 00000000..2ce620f9 --- /dev/null +++ b/src/controllers/screens.js @@ -0,0 +1,21 @@ +const { createScreenDb } = require('../domains/screens') +const { MissingFieldsError, DataAlreadyExistsError } = require('../errors/errors') + +async function createScreen(req, res) { + const props = req.body + + if (!props.number) { + throw new MissingFieldsError('All screens require a screen number') + } + + try { + const screen = await createScreenDb(props) + res.status(201).json({ screen }) + } catch (e) { + if (e.code === 'P2002') { + throw new DataAlreadyExistsError('There is already a screen with that number') + } + } +} + +module.exports = { createScreen } \ No newline at end of file diff --git a/src/controllers/tickets.js b/src/controllers/tickets.js new file mode 100644 index 00000000..4d07b7ce --- /dev/null +++ b/src/controllers/tickets.js @@ -0,0 +1,54 @@ +const { createTicketDb } = require("../domains/tickets"); +const { MissingFieldsError } = require("../errors/errors"); + +async function createTicket(req, res) { + const { screeningId, customerId } = req.body; + + if (!screeningId || !customerId) { + throw new MissingFieldsError( + "Tickets require a screening ID and a customer ID" + ); + } + + let ticket + try { + ticket = await createTicketDb(screeningId, customerId); + } catch (e) { + if(e.message === 'P2003') { + res.status(400).json({ error: 'Screen or customer ID cannot be found'}) + } + } + + const result = { + id: ticket.id, + screening: { + id: ticket.screening.id, + movieId: ticket.screening.movieId, + startsAt: ticket.screening.startsAt, + createdAt: ticket.screening.createdAt, + updatedAt: ticket.screening.updatedAt, + }, + customer: { + id: ticket.customer.id, + name: ticket.customer.name, + createdAt: ticket.customer.createdAt, + updatedAt: ticket.customer.updatedAt, + }, + screen: { + id: ticket.screening.screen.id, + number: ticket.screening.screen.number, + createdAt: ticket.screening.screen.createdAt, + updatedAt: ticket.screening.screen.updatedAt, + }, + movie: { + id: ticket.screening.movie.id, + runtimeMins: ticket.screening.movie.runtimeMins, + createdAt: ticket.screening.movie.createdAt, + updatedAt: ticket.screening.movie.updatedAt, + }, + }; + + res.status(201).json({ ticket: result }); +} + +module.exports = { createTicket }; diff --git a/src/domains/customer.js b/src/domains/customer.js index c7f315fd..54c300df 100644 --- a/src/domains/customer.js +++ b/src/domains/customer.js @@ -1,9 +1,6 @@ const prisma = require('../utils/prisma') -/** - * This will create a Customer AND create a new Contact, then automatically relate them with each other - * @tutorial https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#create-a-related-record - */ + const createCustomerDb = async (name, phone, email) => await prisma.customer.create({ data: { name, @@ -14,13 +11,32 @@ const createCustomerDb = async (name, phone, email) => await prisma.customer.cre } } }, - // We add an `include` outside of the `data` object to make sure the new contact is returned in the result - // This is like doing RETURNING in SQL + include: { contact: true } }) +async function updateCustomerByIdDb(customerId, newProps) { + + const customer = await prisma.customer.update({ + where: { + id: customerId + }, + data: { ...newProps, + contact: newProps.contact ? { + update: newProps.contact } : undefined, + }, + include: { + contact: true + } + }) + return customer +} + + + module.exports = { - createCustomerDb + createCustomerDb, + updateCustomerByIdDb } diff --git a/src/domains/movies.js b/src/domains/movies.js new file mode 100644 index 00000000..3648cfb7 --- /dev/null +++ b/src/domains/movies.js @@ -0,0 +1,80 @@ +const prisma = require("../utils/prisma"); + +async function getMoviesDb() { + let now = new Date(); + + const movies = await prisma.movie.findMany({ + where: { + screenings: { + some: { + startsAt: { + gt: now, + }, + }, + }, + }, + include: { + screenings: true + } + }); + return movies; +} + +async function createMovieDb(newMovie) { + const movie = await prisma.movie.create({ + data: newMovie, + include: { + screenings: true, + }, + }); + return movie; +} + +async function getMovieByIdDb(movieId) { + const movie = await prisma.movie.findUniqueOrThrow({ + where: { + id: movieId, + }, + include: { + screenings: true, + }, + }); + return movie; +} + +async function updateMovieByIdDb(movieId, updatedProps) { + const movie = await prisma.movie.update({ + where: { + id: movieId, + }, + data: updatedProps, + include: { + screenings: true, + }, + }); + return movie; +} + +async function getMoviesWithQueryDb(query) { + const { runtimeLt, runtimeGt } = query; + + const runTimeLimits = { + ...(runtimeLt && { lt: Number(runtimeLt) }), + ...(runtimeGt && { gt: Number(runtimeGt) }), + }; + + const movies = await prisma.movie.findMany({ + where: { + runtimeMins: runTimeLimits, + }, + }); + return movies; +} + +module.exports = { + getMoviesDb, + createMovieDb, + getMovieByIdDb, + updateMovieByIdDb, + getMoviesWithQueryDb, +}; diff --git a/src/domains/reviews.js b/src/domains/reviews.js new file mode 100644 index 00000000..575158f0 --- /dev/null +++ b/src/domains/reviews.js @@ -0,0 +1,27 @@ +const prisma = require("../utils/prisma"); + +async function getAllReviewsDb() { + return await prisma.review.findMany({ + include: { + customer: true, + movie: true + } + }) +} + +async function createReviewDb(customerId, movieId, title, content) { + return await prisma.review.create({ + data: { + movieId: movieId, + customerId: customerId, + title: title, + content: content + }, + include: { + movie: true, + customer: true + } + }) +} + +module.exports = { getAllReviewsDb, createReviewDb } \ No newline at end of file diff --git a/src/domains/screens.js b/src/domains/screens.js new file mode 100644 index 00000000..fad5d462 --- /dev/null +++ b/src/domains/screens.js @@ -0,0 +1,10 @@ +const prisma = require('../utils/prisma') + +async function createScreenDb(props) { + const screen = await prisma.screen.create({ + data: props + }) + return screen +} + +module.exports = { createScreenDb } \ No newline at end of file diff --git a/src/domains/tickets.js b/src/domains/tickets.js new file mode 100644 index 00000000..dcc98bed --- /dev/null +++ b/src/domains/tickets.js @@ -0,0 +1,35 @@ +const prisma = require("../utils/prisma"); + +async function createTicketDb(screeningId, customerId) { + + const request = { + data: { + screeningId: Number(screeningId), + customerId: Number(customerId), + }, + include: { + screening: { + include: { + screen: true, + movie: true, + }, + }, + customer: { + include: { + contact: true, + }, + }, + }, + }; + + let ticket; + try { + ticket = await prisma.ticket.create(request); + } catch (e) { + throw Error('P2003') + } + + return ticket; +} + +module.exports = { createTicketDb }; diff --git a/src/errors/errors.js b/src/errors/errors.js new file mode 100644 index 00000000..d2e2eb0c --- /dev/null +++ b/src/errors/errors.js @@ -0,0 +1,15 @@ +class MissingFieldsError extends Error { + +} + +class DataAlreadyExistsError extends Error { + +} + +class DataNotFoundError extends Error { + +} + + + +module.exports = { MissingFieldsError, DataAlreadyExistsError, DataNotFoundError } \ No newline at end of file diff --git a/src/routers/customer.js b/src/routers/customer.js index f14a87fc..aa982040 100644 --- a/src/routers/customer.js +++ b/src/routers/customer.js @@ -1,13 +1,14 @@ const express = require("express"); const { - createCustomer + createCustomer, + updateCustomerById } = require('../controllers/customer'); const router = express.Router(); -// In index.js, we told express that the /customer route should use this router file -// The below /register route extends that, so the end result will be a URL -// that looks like http://localhost:4040/customer/register + router.post("/register", createCustomer); +router.put("/:id", updateCustomerById) + module.exports = router; diff --git a/src/routers/movies.js b/src/routers/movies.js new file mode 100644 index 00000000..f941dc51 --- /dev/null +++ b/src/routers/movies.js @@ -0,0 +1,16 @@ +const express = require("express"); +const router = express.Router() +const { + getMovies, + createMovie, + getMovieById, + updateMovieById +} = require('../controllers/movies'); + +router.get("/", getMovies) +router.post("/", createMovie) +router.get("/:id", getMovieById) +router.put("/:id", updateMovieById) + + +module.exports = router; diff --git a/src/routers/reviews.js b/src/routers/reviews.js new file mode 100644 index 00000000..53914e75 --- /dev/null +++ b/src/routers/reviews.js @@ -0,0 +1,8 @@ +const express = require('express') +const router = express.Router() +const { getAllReviews, createReview } = require("../controllers/reviews.js") + +router.get("/", getAllReviews) +router.post("/", createReview) + +module.exports = router \ No newline at end of file diff --git a/src/routers/screens.js b/src/routers/screens.js new file mode 100644 index 00000000..1a2ed088 --- /dev/null +++ b/src/routers/screens.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const { createScreen } = require('../controllers/screens') + +router.post('/', createScreen) + + +module.exports = router; diff --git a/src/routers/tickets.js b/src/routers/tickets.js new file mode 100644 index 00000000..648c1b54 --- /dev/null +++ b/src/routers/tickets.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const { createTicket } = require('../controllers/tickets') + +router.post('/', createTicket) + + +module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index 93d47a16..ca25b2f5 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,6 @@ const express = require('express'); const app = express(); +require("express-async-errors") const cors = require('cors'); const morgan = require('morgan'); @@ -15,7 +16,38 @@ app.use(express.urlencoded({ extended: true })); // Tell express to use your routers here const customerRouter = require('./routers/customer'); +const movieRouter = require('./routers/movies') +const screenRouter = require('./routers/screens') +const ticketsRouter = require('./routers/tickets.js') +const reviewsRouter = require("./routers/reviews.js") app.use('/customers', customerRouter); +app.use('/movies', movieRouter); +app.use('/screens', screenRouter); +app.use('/tickets', ticketsRouter); +app.use('/reviews', reviewsRouter) +const { MissingFieldsError, DataAlreadyExistsError, DataNotFoundError } = require("./errors/errors.js") + + +//Errors +app.use((error, req, res, next) => { + if (error instanceof MissingFieldsError) { + return res.status(400).json({ + error: error.message + }) + } + + if (error instanceof DataAlreadyExistsError) { + return res.status(409).json({ + error: error.message + }) + } + + if (error instanceof DataNotFoundError) { + return res.status(404).json({ + error: error.message + }) + } +}) module.exports = app diff --git a/test/api/extensions/movies-ext.spec.js b/test/api/extensions/movies-ext.spec.js new file mode 100644 index 00000000..4ced224e --- /dev/null +++ b/test/api/extensions/movies-ext.spec.js @@ -0,0 +1,145 @@ +const supertest = require("supertest"); +const app = require("../../../src/server.js"); + +const { createMovie } = require("../../helpers/createMovie.js"); +const { createScreen } = require("../../helpers/createScreen.js"); +const { createScreening } = require("../../helpers/createScreening.js") + +describe("Movies Endpoint", () => { + + describe('GET /movies', () => { + it("will only return movies with a screening that has a start time in the future", async () => { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + + + const screen = await createScreen(1) + const movie = await createMovie("Dodgeball", 120, screen) + const screening = await createScreening(movie, screen, yesterday) + + const response = await supertest(app).get("/movies") + expect(response.body.movies.length).toEqual(0) + + }) + }) + + describe("GET /movies/runtimeLt", () => { + it("will return movies less than a certain runtime", async () => { + const screen = await createScreen(1); + await createMovie("Dodgeball", 120, screen); + await createMovie("Scream", 113, screen); + + const response = await supertest(app).get("/movies?runtimeLt=115"); + + expect(response.status).toEqual(200); + expect(response.body.movies.length).toEqual(1); + expect(response.body.movies[0].runtimeMins).toBeLessThan(115); + + }); + }); + + describe("GET /movies/runtimeGt", () => { + it("will return movies over a certain runtime", async () => { + const screen = await createScreen(1); + await createMovie("The Exorcist", 150, screen); + await createMovie("Spaceballs", 140, screen); + + const response = await supertest(app).get("/movies?runtimeGt=145"); + + expect(response.status).toEqual(200); + expect(response.body.movies.length).toEqual(1); + expect(response.body.movies[0].runtimeMins).toBeGreaterThan(115); + }); + + describe("GET /movies/:id", () => { + it("will throw an error if no movie exists with given id", async () => { + const response = await supertest(app).get("/movies/450"); + + expect(response.status).toEqual(404); + expect(response.body.error).toEqual("No movie found with that ID"); + }); + }); + }); + + describe("POST /movies/", () => { + it("will throw an error if fields missing from body", async () => { + const request = { + title: "Top Gun", + runtimeMins: null, + }; + const response = await supertest(app).post("/movies").send(request); + + expect(response.status).toEqual(400); + expect(response.body.error).toEqual("Movies require a title and runtime"); + }); + + it("will throw an error if a movie already exists with that name", async () => { + const screen = await createScreen(1); + await createMovie("The Exorcist", 150, screen); + await createMovie("Spaceballs", 140, screen); + + const request = { + title: "The Exorcist", + runtimeMins: 140, + }; + + const response = await supertest(app).post("/movies").send(request); + + expect(response.status).toEqual(409); + expect(response.body.error).toEqual( + "A movie with that title already exists" + ); + }); + + describe("PUT /movies/:id", () => { + it("will throw an error if no film exists with given ID", async () => { + const request = { + title: "Top Gun", + runtimeMins: 146, + }; + + const response = await supertest(app).put("/movies/30").send(request); + + expect(response.status).toEqual(404); + expect(response.body.error).toEqual("No movie found with that ID"); + }); + + it("will throw an error if film body is missing fields", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); + + const request = { + cheese: "cheddar", + }; + + const response = await supertest(app) + .put(`/movies/${created.id}`) + .send(request); + + expect(response.status).toEqual(400); + expect(response.body.error).toEqual( + "Updating movies requires a title or runtime" + ); + }); + + it("will throw an error if a movie already exists with that name", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); + const created2 = await createMovie("Signs", 109, screen); + + const request = { + title: "Dodgeball", + }; + + const response = await supertest(app) + .put(`/movies/${created2.id}`) + .send(request); + + expect(response.status).toEqual(409); + expect(response.body.error).toEqual( + "A movie with that title already exists" + ); + }); + }); + }); +}); diff --git a/test/api/extensions/reviews-ext.spec.js b/test/api/extensions/reviews-ext.spec.js new file mode 100644 index 00000000..6d2e3ca1 --- /dev/null +++ b/test/api/extensions/reviews-ext.spec.js @@ -0,0 +1,58 @@ +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createReview } = require("../../helpers/createReview.js"); +const { createCustomer } = require("../../helpers/createCustomer.js"); +const { createMovie } = require("../../helpers/createMovie.js"); + +describe("Reviews Endpoint", () => { + describe("GET reviews", () => { + it("should return all reviews", async () => { + const customer = await createCustomer( + "Johnny", + "07949969106", + "jonny@monny.com" + ); + const movie = await createMovie("The Hunt for Red October", 230); + const review = await createReview( + customer, + movie, + "A really sorry affair", + "It just insists upon itself" + ); + const review2 = await createReview( + customer, + movie, + "An update...", + "I still think it just insists upon itself" + ); + + const response = await supertest(app).get(`/reviews`); + expect(response.body.reviews.length).toEqual(2); + expect(response.body.reviews[0].title).toEqual("A really sorry affair"); + }); + }); + + describe("POST review", () => { + it("should allow customers to create reviews", async () => { + const customer = await createCustomer( + "Johnny", + "07949969106", + "jonny@monny.com" + ); + const movie = await createMovie("The Hunt for Red October", 230); + + const request = { + customerId: customer.id, + movieId: movie.id, + title: "Why I dislike this film", + content: "It's just poor", + }; + + const response = await supertest(app).post(`/reviews`).send(request); + + expect(response.body.review.content).toEqual("It's just poor") + expect(response.body.review.movie).not.toEqual(undefined) + expect(response.body.review.customer).not.toEqual(undefined) + }); + }); +}); diff --git a/test/api/extensions/screens-ext.spec.js b/test/api/extensions/screens-ext.spec.js new file mode 100644 index 00000000..03e151e2 --- /dev/null +++ b/test/api/extensions/screens-ext.spec.js @@ -0,0 +1,28 @@ +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createScreen } = require("../../helpers/createScreen.js"); + +describe("Screens Endpoint", () => { + describe("POST /screens", () => { + it('should reject post requests where body does not contain screen number', async () => { + const request = {capacity: 41} + + const response = await supertest(app).post(`/screens/`).send(request); + + expect(response.status).toEqual(400) + expect(response.body.error).toEqual('All screens require a screen number') + }) + + it('should reject post requests where given screen number already exists', async () => { + const screen = await createScreen(1); + const request = {number: screen.number} + + const response = await supertest(app).post("/screens").send(request); + + expect(response.status).toEqual(409) + expect(response.body.error).toEqual('There is already a screen with that number') + + }) + + }) +}) \ No newline at end of file diff --git a/test/api/extensions/tickets-ext.spec.js b/test/api/extensions/tickets-ext.spec.js new file mode 100644 index 00000000..0d299e9c --- /dev/null +++ b/test/api/extensions/tickets-ext.spec.js @@ -0,0 +1,56 @@ +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createCustomer } = require("../../helpers/createCustomer.js"); +const { createScreening } = require("../../helpers/createScreening.js"); +const { createScreen } = require("../../helpers/createScreen.js"); +const { createMovie } = require("../../helpers/createMovie.js"); + +describe("Tickets Endpoint", () => { + describe("POST /tickets", () => { + it("should throw an error when creating a ticket without a screening or customer ID", async () => { + const request = {}; + + const response = await supertest(app).post(`/tickets`).send(request); + expect(response.status).toEqual(400); + expect(response.body.error).toEqual( + "Tickets require a screening ID and a customer ID" + ); + }); + + it("should create new tickets", async () => { + const date = new Date(); + const customer = await createCustomer( + "Will Baxter", + "999", + "will@baxter.com" + ); + const screen = await createScreen(5); + const movie = await createMovie("The Legend of Bagger Vance", 120); + const screening = await createScreening(movie, screen, date); + + request = { + screeningId: screening.id, + customerId: customer.id, + }; + + const response = await supertest(app).post(`/tickets`).send(request); + + expect(response.status).toEqual(201) + expect(response.body.ticket.id).not.toBe(undefined) + expect(response.body.ticket.customer).not.toBe(undefined) + expect(response.body.ticket.screen).not.toBe(undefined) + expect(response.body.ticket.movie).not.toBe(undefined) + }); + + it('should throw an error if screening or customer ID cannot be found', async () => { + request = { + screeningId: 2343452, + customerId: 123313 + } + + const response = await supertest(app).post(`/tickets`).send(request); + expect(response.status).toEqual(400) + expect(response.body.error).toEqual('Screen or customer ID cannot be found') + }) + }); +}); diff --git a/test/api/routes/movies.spec.js b/test/api/routes/movies.spec.js index 209c4bc3..8ce7b735 100644 --- a/test/api/routes/movies.spec.js +++ b/test/api/routes/movies.spec.js @@ -1,90 +1,97 @@ -const supertest = require("supertest") -const app = require("../../../src/server.js") -const { createMovie } = require("../../helpers/createMovie.js") -const { createScreen } = require("../../helpers/createScreen.js") +const supertest = require("supertest"); +const app = require("../../../src/server.js"); +const { createMovie } = require("../../helpers/createMovie.js"); +const { createScreen } = require("../../helpers/createScreen.js"); +const { createScreening } = require("../../helpers/createScreening.js"); describe("Movies Endpoint", () => { - describe("GET /movies", () => { - it("will retrieve a list of movies", async () => { - const screen = await createScreen(1) - await createMovie('Dodgeball', 120, screen) - await createMovie('Scream', 113, screen) - - const response = await supertest(app).get('/movies') - - expect(response.status).toEqual(200) - expect(response.body.movies).not.toEqual(undefined) - expect(response.body.movies.length).toEqual(2) - - const [movie1, movie2] = response.body.movies - expect(movie1.title).toEqual('Dodgeball') - expect(movie1.runtimeMins).toEqual(120) - expect(movie1.screenings).not.toEqual(undefined) - expect(movie1.screenings.length).toEqual(1) - - expect(movie2.title).toEqual('Scream') - expect(movie2.runtimeMins).toEqual(113) - expect(movie2.screenings).not.toEqual(undefined) - expect(movie2.screenings.length).toEqual(1) - }) - }) - - describe("POST /movies", () => { - it("will create a movie", async () => { - const request = { - title: "Top Gun", - runtimeMins: 110 - } - - const response = await supertest(app) - .post("/movies") - .send(request) - - expect(response.status).toEqual(201) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Top Gun') - expect(response.body.movie.runtimeMins).toEqual(110) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(0) - }) - }) - - describe("GET /movies/:id", () => { - it("will get a movie by id", async () => { - const screen = await createScreen(1) - const created = await createMovie('Dodgeball', 120, screen) - - const response = await supertest(app).get(`/movies/${created.id}`) - - expect(response.status).toEqual(200) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Dodgeball') - expect(response.body.movie.runtimeMins).toEqual(120) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(1) - }) - }) - - describe("PUT /movies/:id", () => { - it("will update a movie by id", async () => { - const screen = await createScreen(1) - const created = await createMovie('Dodgeball', 120, screen) - - const request = { - title: 'Scream', - runtimeMins: 113 - } - - const response = await supertest(app) - .put(`/movies/${created.id}`) - .send(request) - - expect(response.status).toEqual(201) - expect(response.body.movie).not.toEqual(undefined) - expect(response.body.movie.title).toEqual('Scream') - expect(response.body.movie.runtimeMins).toEqual(113) - expect(response.body.movie.screenings).not.toEqual(undefined) - expect(response.body.movie.screenings.length).toEqual(1) - }) - }) -}) + describe("GET /movies", () => { + it("will retrieve a list of movies", async () => { + const screen = await createScreen(1); + + const firstMovie = await createMovie("Dodgeball", 120, screen); + const secondMovie = await createMovie("Scream", 113, screen); + + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1); + + await createScreening(firstMovie, screen, tomorrow); + await createScreening(secondMovie, screen, tomorrow); + + const response = await supertest(app).get("/movies"); + + expect(response.status).toEqual(200); + expect(response.body.movies).not.toEqual(undefined); + expect(response.body.movies.length).toEqual(2); + + const [movie1, movie2] = response.body.movies; + expect(movie1.title).toEqual(firstMovie.title); + expect(movie1.runtimeMins).toEqual(120); + expect(movie1.screenings).not.toEqual(undefined); + console.log(movie1) + expect(movie1.screenings.length).toEqual(2); + + expect(movie2.title).toEqual(secondMovie.title); + expect(movie2.runtimeMins).toEqual(113); + expect(movie2.screenings).not.toEqual(undefined); + expect(movie2.screenings.length).toEqual(2); + }); + }); + + describe("POST /movies", () => { + it("will create a movie", async () => { + const request = { + title: "Top Gun", + runtimeMins: 110, + }; + + const response = await supertest(app).post("/movies").send(request); + + expect(response.status).toEqual(201); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Top Gun"); + expect(response.body.movie.runtimeMins).toEqual(110); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(0); + }); + }); + + describe("GET /movies/:id", () => { + it("will get a movie by id", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); + + const response = await supertest(app).get(`/movies/${created.id}`); + + expect(response.status).toEqual(200); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Dodgeball"); + expect(response.body.movie.runtimeMins).toEqual(120); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(1); + }); + }); + + describe("PUT /movies/:id", () => { + it("will update a movie by id", async () => { + const screen = await createScreen(1); + const created = await createMovie("Dodgeball", 120, screen); + + const request = { + title: "Scream", + runtimeMins: 113, + }; + + const response = await supertest(app) + .put(`/movies/${created.id}`) + .send(request); + + expect(response.status).toEqual(201); + expect(response.body.movie).not.toEqual(undefined); + expect(response.body.movie.title).toEqual("Scream"); + expect(response.body.movie.runtimeMins).toEqual(113); + expect(response.body.movie.screenings).not.toEqual(undefined); + expect(response.body.movie.screenings.length).toEqual(1); + }); + }); +}); diff --git a/test/helpers/createReview.js b/test/helpers/createReview.js new file mode 100644 index 00000000..bb30274a --- /dev/null +++ b/test/helpers/createReview.js @@ -0,0 +1,19 @@ +const prisma = require("../../src/utils/prisma"); + + +async function createReview(customer, movie, title, content) { + return await prisma.review.create({ + data: { + movieId: movie.id, + customerId: customer.id, + title: title, + content: content + }, + include: { + movie: true, + customer: true + } + }) +} + +module.exports = { createReview } \ No newline at end of file diff --git a/test/helpers/createScreening.js b/test/helpers/createScreening.js new file mode 100644 index 00000000..25553856 --- /dev/null +++ b/test/helpers/createScreening.js @@ -0,0 +1,19 @@ +const prisma = require("../../src/utils/prisma"); + +async function createScreening(movie, screen, startTime) { + const screeningData = { + movieId: movie.id, + screenId: screen.id, + startsAt: startTime, + }; + + const screening = await prisma.screening.create({ + data: screeningData, + }); + + return screening +} + +module.exports = { + createScreening, +}; diff --git a/test/helpers/createTicket.js b/test/helpers/createTicket.js new file mode 100644 index 00000000..cfa8d453 --- /dev/null +++ b/test/helpers/createTicket.js @@ -0,0 +1,22 @@ +const prisma = require("../../src/utils/prisma"); + +async function createTicket(screening, customer) { + const ticketData = { + screeningId: screening.id, + customerId: customer.id, + include: { + screening: true, + customer: true, + screen: true, + movie: true, + }, + }; + + const ticket = await prisma.tickets.create({ + data: ticketData, + }); +} + +module.exports = { + createTicket, +}; diff --git a/test/setupTests.js b/test/setupTests.js index 2be95321..f422dbb6 100644 --- a/test/setupTests.js +++ b/test/setupTests.js @@ -2,6 +2,7 @@ const prisma = require("../src/utils/prisma") const deleteTables = () => { const deleteTables = [ + prisma.review.deleteMany(), prisma.ticket.deleteMany(), prisma.screening.deleteMany(), prisma.movie.deleteMany(), @@ -10,8 +11,6 @@ const deleteTables = () => { prisma.customer.deleteMany(), ]; - // Conditionally delete this table as this will only exist if "Extensions to the Extensions" bullet 2 is implemented - prisma.reviews && deleteTables.push(prisma.reviews.deleteMany()) return prisma.$transaction(deleteTables) }