Bugnik
← All problems

45Product Questions API

Node.js · ExpressEasyBug fixSuggested time: 60 min

Product questions REST API (customer questions, community and seller answers, open-question limit)

New to repository-based OA questions? Read the guide.

Open in IDE

Before you start

You'll get more out of this problem if you're comfortable with:

Language

  • CommonJS modules
  • async / await with database calls

Libraries and frameworks

  • Express 5 routers and handlers
  • Mongoose schemas, queries and counts

Build and test tools

  • Reading Jest + Supertest failures

Reasoning

  • Customer questions and answers on a product page

Background

The Product Pages team at a large online retailer runs the "Questions and answers" box under every product: shoppers ask about a product, and other shoppers or the seller answer. The service is a Node.js REST API (Express + MongoDB via Mongoose). A recent release went out with a few problems that shoppers and sellers noticed within a day.

How the system works

Callers

  • Every request needs an x-user-id header (already authenticated upstream). A missing or blank header is 401 with { "error": "sign in required" }.
  • The catalog team also sends x-role: catalog (roles are compared case-insensitively).

Errors

Every error response is { "error": "<message>" }: 400 invalid input, 401 no x-user-id, 403 not allowed, 404 unknown product or question, 409 a rule was broken.

Products

POST /products (catalog team only, else 403): body { "sku", "title", "sellerId" }.

  • sku is trimmed and upper-cased and must be 3–20 letters, digits or hyphens. A SKU that already exists is 409.
  • title is trimmed, 1–120 characters; sellerId is trimmed, 1–40 characters. sellerId is the user id of the seller who lists the product.
  • Returns 201 with { "sku", "title", "sellerId" }.

In paths, :sku is matched case-insensitively (/products/bk-2207 is BK-2207); an unknown SKU is 404.

GET /products/:sku: 200 with { "sku", "title", "sellerId", "questions": { "total", "open", "unanswered", "sellerAnswered" } }: all of the product's questions, the open ones, the open ones with no answer yet, and the ones the seller has answered.

Questions

A question as the API returns it: { "id", "sku", "text", "askedAt", "status", "answerCount", "sellerAnswered" }, where status is OPEN or CLOSED and askedAt is an ISO timestamp in UTC. Questions are anonymous: no response includes the user id of the person who asked.

POST /products/:sku/questions: body { "text" }.

  • text is trimmed and must be 10–300 characters (else 400).
  • The product's seller can't ask about their own product (403).
  • A shopper may have at most 3 open questions with no answers yet on the same product. Asking another one while 3 are waiting is 409; once one of them is answered or closed, the shopper may ask again.
  • Returns 201 with the question (status OPEN, answerCount 0, sellerAnswered false).

GET /products/:sku/questions?answered=…&limit=N: 200 with { "questions": [...] }, newest askedAt first (questions asked at the same moment: the later-created one first).

  • answered is optional: true keeps only questions with at least one answer, false only questions with none; without it, every question is listed. It is read case-insensitively (FALSE, True); any other value is 400.
  • limit is optional (default 20) and must be a whole number from 1 to 50 (else 400).

GET /questions/:id: 200 with the question plus "answers": [...]: the seller's answers first, then the oldest answer first. An id that doesn't belong to a question is 404.

PATCH /questions/:id: body { "text" } (same rules as when asking). Only the person who asked may reword the question (else 403), and only while it has no answers (else 409). Returns 200 with the question.

POST /questions/:id/close: the person who asked or the product's seller may close a question (anyone else gets 403). Closing a closed question is 409. Returns 200 with the question (status CLOSED).

Answers

POST /questions/:id/answers: body { "text" }.

  • text is trimmed, 2–500 characters (else 400).
  • Anyone except the person who asked may answer (403). A closed question takes no answers (409).
  • An answer from the product's seller has fromSeller true; it also sets the question's sellerAnswered. Every answer adds one to the question's answerCount.
  • Returns 201 with { "id", "questionId", "text", "answeredAt", "fromSeller" } (no user id either).

Project structure

src/
  app.js                         createApp(): JSON parsing, routes, error handler
  server.js                      connects to MongoDB and starts the server
  routes/
    products.js                  /products
    questions.js                 /questions
  controllers/
    productController.js         add a product, product page counts, ask, list questions
    questionController.js        question page, edit, close, answer
  middleware/
    auth.js                      x-user-id and role checks
    errorHandler.js              errors → status + { error }
  models/
    Product.js                   a product page and its seller
    Question.js                  a shopper's question
    Answer.js                    an answer to a question
  services/
    productService.js            product lookups and question counts
    questionService.js           asking, listing, editing, closing and answering rules
  utils/
    AppError.js                  an error with an HTTP status
    input.js                     reading text, SKUs and query values
    present.js                   documents → API JSON
tests/
  input.test.js                  input helpers
  questions.test.js              product and question endpoints
  answers.test.js                answer, close and edit endpoints
  setup/                         in-memory MongoDB for the tests

Your task

Shoppers and sellers report these problems:

  • A shopper says someone found out which questions they asked.
  • Filtering a product's questions by whether they've been answered shows the wrong questions.
  • Settled questions keep collecting new answers.

Find and fix the defects so that all tests pass.

  • Do not modify tests/, package.json or jest.config.js.
  • Keep your changes minimal. Fix the defects; don't rewrite modules.
  • Additional hidden tests check the behaviour described in this README.