diff --git a/NOTES.md b/NOTES.md index 6a164cf..ea30c99 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1 +1,23 @@ -Please add any additional notes here… \ No newline at end of file +Please add any additional notes here… + + +high level slices: +run server +load and inspect json data +expose two endpoints +return appropriate Json responses + +slices of slices: +- Save the url as consts here in the server file? +- add a function(s) to get the data (could write just one that takes a url as an arg, and then I can call that in more specific functions later?) +These could then be called in the GET. +- save the correct props/target the correct props, to a var. + + +Decisions: +seperate concrns with different functions, but use a resusable function for fetching- though caching would be important here in the real world. + +The mapping of the data is something I would have liked to extract into mapping functions that could be imported to make the code cleaner, and increase resusability if required. + +Should have included pagination to gaurd against larger payloads. + diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 0ddbe51..85055ba 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,6 +1,16 @@ import request from 'supertest' import { app } from '../server' + +test('returns all cards with correct structure', async () => { + const response = await request(app).get('/cards') + + expect(response.status).toBe(200) + expect(response.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'card 1 title', url: '/cards/card001' }) + ])) +}) test('returns matching card title', async () => { const response = await request(app).get('/cards/card001') @@ -9,3 +19,30 @@ test('returns matching card title', async () => { title: 'card 1 title', })) }) +test('returns 404 if card not found', async () => { + const response = await request(app).get('/cards/invalidCard') + + expect(response.status).toBe(404) + expect(response.body).toEqual(expect.objectContaining({ + "error": "Card not found" + })) +}) + +test('returns matching card size', async () => { + const response = await request(app).get('/cards/card001/sm') + + expect(response.status).toBe(200) + expect(response.body).toEqual(expect.objectContaining({ + title: 'card 1 title', + price: "£1.60", + imageUrl: '/front-cover-portrait-1.jpg' + })) +}) +test('returns 404 if card size not found', async () => { + const response = await request(app).get('/cards/card001/invalidSize') + + expect(response.status).toBe(404) + expect(response.body).toEqual(expect.objectContaining({ + "error": "Card size not found" + })) +}) \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 88068f7..6d213a0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,10 +4,80 @@ export const app = express() app.set('json spaces', 2); -app.get('/cards', async () => { - // respond with a list of cards +const cardsUrl = "https://moonpig.github.io/tech-test-node-backend/cards.json" +const sizesUrl = "https://moonpig.github.io/tech-test-node-backend/sizes.json" +const templatesUrl = "https://moonpig.github.io/tech-test-node-backend/templates.json" + +async function fetchJson(url: string) { + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}`); + return res.json(); +} + +async function getAllCards() { + return fetchJson(cardsUrl); +} +async function getTemplates() { + return fetchJson(templatesUrl); +} +async function getSizes() { + return fetchJson(sizesUrl); +} + + +app.get('/cards', async (req, res) => { + try{ + const cards = await getAllCards(); + const templates = await getTemplates(); + + const result = cards.map((card) => { + const firstPage = card.pages[0]; +const firstPageTemplate = templates.find(t => t.id === firstPage.templateId); + +return { + title: card.title, + url: `/cards/${card.id}`, + imageUrl: firstPageTemplate ? firstPageTemplate.imageUrl : "No image available", + }; + }) + res.json(result); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to fetch cards" }); + } }) -app.get('/cards/:cardId/:sizeId?', () => { - // respond with card by id +app.get('/cards/:cardId/:sizeId?', async (req, res) => { + try{ + const { cardId, sizeId } = req.params; + const cards = await getAllCards(); + const templates = await getTemplates(); + const sizes = await getSizes(); + + const card = cards.find(card => card.id === cardId); + if (!card) { + return res.status(404).json({ error: "Card not found" }); + } + const selectedSize = sizeId + ? sizes.find(s => s.id === sizeId) + : undefined; + + if (sizeId && !selectedSize) { + return res.status(404).json({ error: "Card size not found" }); + } + const pricePence = selectedSize ? card.basePrice * selectedSize.priceMultiplier : card.basePrice; + const price = `£${(pricePence / 100).toFixed(2)}`; + + const template = templates.find(t => t.id === card.pages[0].templateId); + + res.json({ + title: card.title, + price, + imageUrl: template ? template.imageUrl : "No image available", + }); + + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to fetch cards" }); + } })