index.ts

import express, { Express } from "express";
import bodyParser from 'body-parser';
import { listAuctions, addAuction, bidInAuction, getAuction } from './routes';


// Configure and start the HTTP server.
const port: number = 8088;
const app: Express = express();
app.use(bodyParser.json());
app.get("/api/list", listAuctions);
app.post("/api/add", addAuction);
app.post("/api/bid", bidInAuction);
app.post("/api/get", getAuction);
app.listen(port, () => console.log(`Server listening on ${port}`));

routes.ts

import { Request, Response } from "express";
import { ParamsDictionary } from "express-serve-static-core";


// Require type checking of request body.
type SafeRequest = Request<ParamsDictionary, {}, Record<string, unknown>>;
type SafeResponse = Response;  // only writing, so no need to check


// Description of an individual auction
// RI: minBid, maxBid >= 0
type Auction = {
  name: string,
  seller: string,
  description: string,
  endTime: number,  // ms since epoch
  maxBid: number,
  maxBidder: string
};


// Map from name to auction details.
const auctions: Map<string, Auction> = new Map();


/** Testing function to remove all the added auctions. */
export const resetForTesting = (): void => {
  auctions.clear();
};

/** Testing function to move all end times forward the given amount (of ms). */
export const advanceTimeForTesting = (ms: number): void => {
  for (const auction of auctions.values()) {
    auction.endTime -= ms;
  }
};


// Sort auctions with the ones finishing soonest first, but with all those that
// are completed after those that are not and in reverse order by end time.
const compareAuctions = (a: Auction, b: Auction): number => {
  const now: number = Date.now();
  const endA = now <= a.endTime ? a.endTime : 1e15 - a.endTime;
  const endB = now <= b.endTime ? b.endTime : 1e15 - b.endTime;
  return endA - endB;
};

/**
 * Returns a list of all the auctions, sorted so that the ongoing auctions come
 * first, with the ones about to end listed first, and the completed ones after,
 * with the ones completed more recently
 * @param _req the request
 * @param res the response
 */
export const listAuctions = (_req: SafeRequest, res: SafeResponse): void => {
  const vals = Array.from(auctions.values());
  vals.sort(compareAuctions);
  res.send({auctions: vals});
};


/**
 * Add the item to the list.
 * @param req the request
 * @param res the response
 */
export const addAuction = (req: SafeRequest, res: SafeResponse): void => {
  const name = req.body.name;
  if (typeof name !== 'string') {
    res.status(400).send("missing 'name' parameter");
    return;
  }

  const seller = req.body.seller;
  if (typeof seller !== 'string') {
    res.status(400).send("missing 'seller' parameter");
    return;
  }

  const description = req.body.description;
  if (typeof description !== 'string') {
    res.status(400).send("missing 'description' parameter");
    return;
  }

  const minBid = req.body.minBid;
  if (typeof minBid !== "number") {
    res.status(400).send(`'minBid' is not a number: ${minBid}`);
    return;
  } else if (isNaN(minBid) || minBid < 1 || Math.round(minBid) !== minBid) {
    res.status(400).send(`'minBid' is not a positive integer: ${minBid}`);
    return;
  }

  const minutes = req.body.minutes;
  if (typeof minutes !== "number") {
    res.status(400).send(`'minutes' is not a number: ${minutes}`);
    return;
  } else if (isNaN(minutes) || minutes < 1 || Math.round(minutes) !== minutes) {
    res.status(400).send(`'minutes' is not a positive integer: ${minutes}`);
    return;
  }

  // Make sure there is no auction with this name already.
  if (auctions.has(name)) {
    res.status(400).send(`auction for '${name}' already exists`);
    return;
  }

  const auction: Auction = {
    name: name,
    description: description,
    seller: seller,
    endTime: Date.now() + minutes * 60 * 1000,  // convert to ms
    maxBid: minBid - 1,
    maxBidder: seller
  };
  auctions.set(auction.name, auction); // add this to the map of auctions
  res.send({auction: auction});  // send the auction we made
}


/**
 * Bids in an auction, increasing the maximum bid if the bid is higher.
 * @param req the request
 * @param req the response
 */
export const bidInAuction = (req: SafeRequest, res: SafeResponse): void => {
  const bidder = req.body.bidder;
  if (typeof bidder !== 'string') {
    res.status(400).send("missing or invalid 'bidder' parameter");
    return;
  }

  const name = req.body.name;
  if (typeof name !== "string") {
    res.status(400).send("missing or invalid 'name' parameter");
    return;
  }

  const auction = auctions.get(name);
  if (auction === undefined) {
    res.status(400).send(`no auction with name '${name}'`);
    return;
  }

  const now = Date.now();
  if (now >= auction.endTime) {
    res.status(400).send(`auction for "${auction.name}" has already ended`);
    return;
  }

  const amount = req.body.amount;
  if (typeof amount !== "number") {
    res.status(400).send(`'amount' is not a number: ${amount}`);
    return;
  } else if (isNaN(amount) || amount < 1 || Math.round(amount) !== amount) {
    res.status(400).send(`'amount' is not a positive integer: ${amount}`);
    return;
  } else if (amount <= auction.maxBid) {
    res.status(400).send(
        `'amount' is not more than max bid: ${amount} <= ${auction.maxBid}`);
    return;
  }

  auction.maxBid = amount;
  auction.maxBidder = bidder;
  res.send({auction: auction});  // send back the updated auction state
}


/**
 * Retrieves the current state of a given auction.
 * @param req the request
 * @param req the response
 */
export const getAuction = (req: SafeRequest, res: SafeResponse): void => {
  const name = req.body.name;
  if (typeof name !== "string") {
    res.status(400).send("missing or invalid 'name' parameter");
    return;
  }

  const auction = auctions.get(name);
  if (auction === undefined) {
    res.status(400).send(`no auction with name '${name}'`);
    return;
  }

  res.send({auction: auction});  // send back the current auction state
}

routes_test.ts

import * as assert from 'assert';
import * as httpMocks from 'node-mocks-http';
import { addAuction, bidInAuction, getAuction, listAuctions,
         resetForTesting, advanceTimeForTesting } from './routes';

describe('routes', function() {

  it('add', function() {
    // Separate domain for each branch:
    // 1. Missing name
    const req1 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add', body: {}});
    const res1 = httpMocks.createResponse();
    addAuction(req1, res1);
    assert.strictEqual(res1._getStatusCode(), 400);
    assert.deepStrictEqual(res1._getData(),
        "missing 'name' parameter");

    // 2. Missing seller
    const req2 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add', body: {name: "couch"}});
    const res2 = httpMocks.createResponse();
    addAuction(req2, res2);
    assert.strictEqual(res2._getStatusCode(), 400);
    assert.deepStrictEqual(res2._getData(),
        "missing 'seller' parameter");

    // 3. Missing description
    const req3 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred"}});
    const res3 = httpMocks.createResponse();
    addAuction(req3, res3);
    assert.strictEqual(res3._getStatusCode(), 400);
    assert.deepStrictEqual(res3._getData(),
        "missing 'description' parameter");

    // 4. Missing min-bid
    const req4 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch"}});
    const res4 = httpMocks.createResponse();
    addAuction(req4, res4);
    assert.strictEqual(res4._getStatusCode(), 400);
    assert.deepStrictEqual(res4._getData(),
        "'minBid' is not a number: undefined");

    // 5. Invalid min-bid
    const req5 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: -1}});
    const res5 = httpMocks.createResponse();
    addAuction(req5, res5);
    assert.strictEqual(res5._getStatusCode(), 400);
    assert.deepStrictEqual(res5._getData(),
        "'minBid' is not a positive integer: -1");

    const req6 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 2.5}});
    const res6 = httpMocks.createResponse();
    addAuction(req6, res6);
    assert.strictEqual(res6._getStatusCode(), 400);
    assert.deepStrictEqual(res6._getData(),
        "'minBid' is not a positive integer: 2.5");

    // 6. Missing minutes
    const req7 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 7}});
    const res7 = httpMocks.createResponse();
    addAuction(req7, res7);
    assert.strictEqual(res7._getStatusCode(), 400);
    assert.deepStrictEqual(res7._getData(),
        "'minutes' is not a number: undefined");

    // 7. Invalid minutes
    const req8 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 5, minutes: 0}});
    const res8 = httpMocks.createResponse();
    addAuction(req8, res8);
    assert.strictEqual(res8._getStatusCode(), 400);
    assert.deepStrictEqual(res8._getData(),
        "'minutes' is not a positive integer: 0");

    const req9 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 2, minutes: 3.5}});
    const res9 = httpMocks.createResponse();
    addAuction(req9, res9);
    assert.strictEqual(res9._getStatusCode(), 400);
    assert.deepStrictEqual(res9._getData(),
        "'minutes' is not a positive integer: 3.5");

    // 8. Correctly added
    const req10 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 2, minutes: 4}});
    const res10 = httpMocks.createResponse();
    addAuction(req10, res10);
    assert.strictEqual(res10._getStatusCode(), 200);
    assert.deepStrictEqual(res10._getData().auction.name, "couch");
    assert.deepStrictEqual(res10._getData().auction.seller, "Fred");
    assert.deepStrictEqual(res10._getData().auction.description, "a couch");
    assert.deepStrictEqual(res10._getData().auction.maxBid, 1);
    assert.deepStrictEqual(res10._getData().auction.maxBidder, "Fred");
    const endTime10 = res10._getData().auction.endTime;
    assert.ok(Math.abs(endTime10 - Date.now() - 4 * 60 * 1000) < 50);

    const req11 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "chair", seller: "Barney", description: "comfy chair",
                minBid: 3, minutes: 2}});
    const res11 = httpMocks.createResponse();
    addAuction(req11, res11);
    assert.strictEqual(res11._getStatusCode(), 200);
    assert.deepStrictEqual(res11._getData().auction.name, "chair");
    assert.deepStrictEqual(res11._getData().auction.seller, "Barney");
    assert.deepStrictEqual(res11._getData().auction.description, "comfy chair");
    assert.deepStrictEqual(res11._getData().auction.maxBid, 2);
    assert.deepStrictEqual(res11._getData().auction.maxBidder, "Barney");
    const endTime11 = res11._getData().auction.endTime;
    assert.ok(Math.abs(endTime11 - Date.now() - 2 * 60 * 1000) < 50);

    resetForTesting();
  });

  it('bid', function() {
    const req1 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 10, minutes: 5}});
    const res1 = httpMocks.createResponse();
    addAuction(req1, res1);
    assert.strictEqual(res1._getStatusCode(), 200);
    assert.deepStrictEqual(res1._getData().auction.name, "couch");
    assert.deepStrictEqual(res1._getData().auction.maxBid, 9);

    // Separate domain for each branch:
    // 1. Missing bidder
    const req2 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid', body: {}});
    const res2 = httpMocks.createResponse();
    bidInAuction(req2, res2);
    assert.strictEqual(res2._getStatusCode(), 400);
    assert.deepStrictEqual(res2._getData(),
        "missing or invalid 'bidder' parameter");

    // 2. Missing name
    const req3 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid', body: {bidder: "Barney"}});
    const res3 = httpMocks.createResponse();
    bidInAuction(req3, res3);
    assert.strictEqual(res3._getStatusCode(), 400);
    assert.deepStrictEqual(res3._getData(),
        "missing or invalid 'name' parameter");

    // 3. Invalid name
    const req4 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "chair"}});
    const res4 = httpMocks.createResponse();
    bidInAuction(req4, res4);
    assert.strictEqual(res4._getStatusCode(), 400);
    assert.deepStrictEqual(res4._getData(), "no auction with name 'chair'");

    const req5 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "stool"}});
    const res5 = httpMocks.createResponse();
    bidInAuction(req5, res5);
    assert.strictEqual(res5._getStatusCode(), 400);
    assert.deepStrictEqual(res5._getData(), "no auction with name 'stool'");

    // 4. Amount missing
    const req6 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch"}});
    const res6 = httpMocks.createResponse();
    bidInAuction(req6, res6);
    assert.strictEqual(res6._getStatusCode(), 400);
    assert.deepStrictEqual(res6._getData(),
        "'amount' is not a number: undefined");

    // 5. Amount invalid
    const req7 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch", amount: -1}});
    const res7 = httpMocks.createResponse();
    bidInAuction(req7, res7);
    assert.strictEqual(res7._getStatusCode(), 400);
    assert.deepStrictEqual(res7._getData(),
        "'amount' is not a positive integer: -1");

    const req8 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch", amount: 2.5}});
    const res8 = httpMocks.createResponse();
    bidInAuction(req8, res8);
    assert.strictEqual(res8._getStatusCode(), 400);
    assert.deepStrictEqual(res8._getData(),
        "'amount' is not a positive integer: 2.5");

    // 6. Amount too small
    const req9 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch", amount: 3}});
    const res9 = httpMocks.createResponse();
    bidInAuction(req9, res9);
    assert.strictEqual(res9._getStatusCode(), 400);
    assert.deepStrictEqual(res9._getData(),
        "'amount' is not more than max bid: 3 <= 9");

    // 7. Bid made
    const req10 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch", amount: 10}});
    const res10 = httpMocks.createResponse();
    bidInAuction(req10, res10);
    assert.strictEqual(res10._getStatusCode(), 200);
    assert.deepStrictEqual(res10._getData().auction.name, "couch");
    assert.deepStrictEqual(res10._getData().auction.maxBid, 10);
    assert.deepStrictEqual(res10._getData().auction.maxBidder, "Barney");

    const req11 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Fred", name: "couch", amount: 15}});
    const res11 = httpMocks.createResponse();
    bidInAuction(req11, res11);
    assert.strictEqual(res11._getStatusCode(), 200);
    assert.deepStrictEqual(res11._getData().auction.name, "couch");
    assert.deepStrictEqual(res11._getData().auction.maxBid, 15);
    assert.deepStrictEqual(res11._getData().auction.maxBidder, "Fred");

    // Push time forward by over 5 minutes
    advanceTimeForTesting(5 * 60 * 1000 + 50);

    // 8. Auction over (advanceTimeForTesting) [separate test]
    const req12 = httpMocks.createRequest(
        {method: 'POST', url: '/api/bid',
         body: {bidder: "Barney", name: "couch", amount: 16}});
    const res12 = httpMocks.createResponse();
    bidInAuction(req12, res12);
    assert.strictEqual(res12._getStatusCode(), 400);
    assert.deepStrictEqual(res12._getData(),
        "auction for \"couch\" has already ended");

    resetForTesting();
  });

  it('get', function() {
    const req1 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "the cozy couch",
                minBid: 10, minutes: 5}});
    const res1 = httpMocks.createResponse();
    addAuction(req1, res1);
    assert.strictEqual(res1._getStatusCode(), 200);
    assert.deepStrictEqual(res1._getData().auction.name, "couch");
    assert.deepStrictEqual(res1._getData().auction.maxBid, 9);

    const req2 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "chair", seller: "Barney", description: "the comfy chair",
                minBid: 5, minutes: 10}});
    const res2 = httpMocks.createResponse();
    addAuction(req2, res2);
    assert.strictEqual(res2._getStatusCode(), 200);
    assert.deepStrictEqual(res2._getData().auction.name, "chair");
    assert.deepStrictEqual(res2._getData().auction.maxBid, 4);

    // Separate domain for each branch:
    // 1. Missing name
    // 1. Missing name
    const req3 = httpMocks.createRequest(
        {method: 'POST', url: '/api/get', body: {bidder: "Barney"}});
    const res3 = httpMocks.createResponse();
    getAuction(req3, res3);
    assert.strictEqual(res3._getStatusCode(), 400);
    assert.deepStrictEqual(res3._getData(),
        "missing or invalid 'name' parameter");

    // 2. Invalid name
    const req4 = httpMocks.createRequest(
        {method: 'POST', url: '/api/get',
         body: {bidder: "Barney", name: "fridge"}});
    const res4 = httpMocks.createResponse();
    getAuction(req4, res4);
    assert.strictEqual(res4._getStatusCode(), 400);
    assert.deepStrictEqual(res4._getData(), "no auction with name 'fridge'");

    const req5 = httpMocks.createRequest(
        {method: 'POST', url: '/api/get',
         body: {bidder: "Barney", name: "stool"}});
    const res5 = httpMocks.createResponse();
    getAuction(req5, res5);
    assert.strictEqual(res5._getStatusCode(), 400);
    assert.deepStrictEqual(res5._getData(), "no auction with name 'stool'");

    // 3. Auction found
    const req6 = httpMocks.createRequest(
        {method: 'POST', url: '/api/get', body: {name: "couch"}});
    const res6 = httpMocks.createResponse();
    getAuction(req6, res6);
    assert.strictEqual(res6._getStatusCode(), 200);
    assert.deepStrictEqual(res6._getData().auction.name, "couch");
    assert.deepStrictEqual(res6._getData().auction.maxBid, 9);
    assert.deepStrictEqual(res6._getData().auction.maxBidder, "Fred");

    const req7 = httpMocks.createRequest(
        {method: 'POST', url: '/api/get', body: {name: "chair"}});
    const res7 = httpMocks.createResponse();
    getAuction(req7, res7);
    assert.strictEqual(res7._getStatusCode(), 200);
    assert.deepStrictEqual(res7._getData().auction.name, "chair");
    assert.deepStrictEqual(res7._getData().auction.maxBid, 4);
    assert.deepStrictEqual(res7._getData().auction.maxBidder, "Barney");

    resetForTesting();
  });

  it('list', function() {
    const req1 = httpMocks.createRequest(
        {method: 'GET', url: '/api/list', query: {}});
    const res1 = httpMocks.createResponse();
    listAuctions(req1, res1);
    assert.strictEqual(res1._getStatusCode(), 200);
    assert.deepStrictEqual(res1._getData(), {auctions: []});

    const req2 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "couch", seller: "Fred", description: "a couch",
                minBid: 10, minutes: 10}});
    const res2 = httpMocks.createResponse();
    addAuction(req2, res2);
    assert.strictEqual(res2._getStatusCode(), 200);
    assert.deepStrictEqual(res2._getData().auction.name, "couch");
    assert.deepStrictEqual(res2._getData().auction.maxBid, 9);

    const req3 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "chair", seller: "Barney", description: "comfy couch",
                minBid: 5, minutes: 5}});
    const res3 = httpMocks.createResponse();
    addAuction(req3, res3);
    assert.strictEqual(res3._getStatusCode(), 200);
    assert.deepStrictEqual(res3._getData().auction.name, "chair");
    assert.deepStrictEqual(res3._getData().auction.maxBid, 4);

    const req4 = httpMocks.createRequest(
        {method: 'POST', url: '/api/add',
         body: {name: "stool", seller: "Kevin", description: "correctness stool",
                minBid: 15, minutes: 15}});
    const res4 = httpMocks.createResponse();
    addAuction(req4, res4);
    assert.strictEqual(res4._getStatusCode(), 200);
    assert.deepStrictEqual(res4._getData().auction.name, "stool");
    assert.deepStrictEqual(res4._getData().auction.maxBid, 14);

    // NOTE: chair goes first because it finishes sooner
    const req5 = httpMocks.createRequest(
        {method: 'GET', url: '/api/list', query: {}});
    const res5 = httpMocks.createResponse();
    listAuctions(req5, res5);
    assert.strictEqual(res5._getStatusCode(), 200);
    assert.deepStrictEqual(res5._getData().auctions.length, 3);
    assert.deepStrictEqual(res5._getData().auctions[0].name, "chair");
    assert.deepStrictEqual(res5._getData().auctions[1].name, "couch");
    assert.deepStrictEqual(res5._getData().auctions[2].name, "stool");

   // Push time forward by over 5 minutes
   advanceTimeForTesting(5 * 60 * 1000 + 50); 
         
   // NOTE: chair goes after because it has finished
   const req6 = httpMocks.createRequest(
       {method: 'GET', url: '/api/list', query: {}});
   const res6 = httpMocks.createResponse();
   listAuctions(req6, res6);
   assert.strictEqual(res6._getStatusCode(), 200);
   assert.deepStrictEqual(res6._getData().auctions.length, 3);
   assert.deepStrictEqual(res6._getData().auctions[0].name, "couch");
   assert.deepStrictEqual(res6._getData().auctions[1].name, "stool");
   assert.deepStrictEqual(res6._getData().auctions[2].name, "chair");
       
   // Push time forward by another 5 minutes
   advanceTimeForTesting(5 * 60 * 1000);
   
   // NOTE: chair stays after because it finished first
   const req7 = httpMocks.createRequest(
       {method: 'GET', url: '/api/list', query: {}});
   const res7 = httpMocks.createResponse();
   listAuctions(req7, res7);
   assert.strictEqual(res7._getStatusCode(), 200);
   assert.deepStrictEqual(res7._getData().auctions.length, 3);
   assert.deepStrictEqual(res7._getData().auctions[0].name, "stool");
   assert.deepStrictEqual(res7._getData().auctions[1].name, "couch");
   assert.deepStrictEqual(res7._getData().auctions[2].name, "chair");

   // Push time forward by another 20 minutes (all are completed)
   advanceTimeForTesting(20 * 60 * 1000);
   
   // NOTE: chair stays after because it finished first
   const req8 = httpMocks.createRequest(
       {method: 'GET', url: '/api/list', query: {}});
   const res8 = httpMocks.createResponse();
   listAuctions(req8, res8);
   assert.strictEqual(res8._getStatusCode(), 200);
   assert.deepStrictEqual(res8._getData().auctions.length, 3);
   assert.deepStrictEqual(res8._getData().auctions[0].name, "stool");
   assert.deepStrictEqual(res8._getData().auctions[1].name, "couch");
   assert.deepStrictEqual(res8._getData().auctions[2].name, "chair");

    resetForTesting();
  });

});

Full Code