Skip to content

Commit

Permalink
Merge pull request #109 from PermanentOrg/per-9751_add_get_promos_end…
Browse files Browse the repository at this point in the history
…point

Add GET /promo endpoint
  • Loading branch information
cecilia-donnelly authored Aug 1, 2024
2 parents bd15442 + be0d0a6 commit d875ad3
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 3 deletions.
21 changes: 21 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,27 @@ MixPanel](https://permanent.atlassian.net/wiki/spaces/EN/pages/2086862849/Mixpan
{}
```

### GET `/promo`

- Headers: Authorization Bearer \<JWT rom FusionAuth> (admin user)
- Response

```
[
{
id: string,
code: string,
storageInMB: number (integer > 0),
expirationTimestamp: string (ISO 8601 format timestamp),
remainingUses: number (integer >= 0)
status: string
type: string
createdDt: string (ISO 8601 format timestamp),
updatedDt: string (ISO 8601 format timestamp)
}
]
```

## FusionAuth Users and Two-Factor Methods

(Note: this section is not yet implemented)
Expand Down
40 changes: 40 additions & 0 deletions packages/api/src/fixtures/create_test_promos.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
INSERT INTO promo (
code,
sizeinmb,
expiresdt,
remaininguses,
status,
type,
createddt,
updateddt
) VALUES
(
'PROMO1',
1024,
'2030-12-31',
100,
'status.promo.valid',
'type.generic.ok',
'2020-01-01',
'2020-01-01'
),
(
'PROMO2',
2048,
'2031-12-31',
200,
'status.promo.valid',
'type.generic.ok',
'2020-01-02',
'2020-01-02'
),
(
'PROMO3',
4096,
'2032-12-31',
300,
'status.promo.valid',
'type.generic.ok',
'2020-01-03',
'2020-01-03'
);
93 changes: 92 additions & 1 deletion packages/api/src/promo/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import createError from "http-errors";
import { logger } from "@stela/logger";
import { app } from "../app";
import { verifyAdminAuthentication } from "../middleware/authentication";
import type { CreatePromoRequest } from "./models";
import type { CreatePromoRequest, Promo } from "./models";
import { db } from "../database";

jest.mock("../middleware/authentication");
jest.mock("../database");
jest.mock("@stela/logger");

const loadFixtures = async (): Promise<void> => {
await db.sql("fixtures.create_test_promos");
};

const clearDatabase = async (): Promise<void> => {
await db.query("TRUNCATE promo CASCADE");
};
Expand Down Expand Up @@ -333,3 +337,90 @@ describe("POST /promo", () => {
expect(logger.error).toHaveBeenCalled();
});
});

describe("GET /promo", () => {
const agent = request(app);

beforeEach(async () => {
(verifyAdminAuthentication as jest.Mock).mockImplementation(
(req: Request, __, next: NextFunction) => {
(req.body as CreatePromoRequest).emailFromAuthToken =
"test@permanent.org";
next();
}
);

jest.restoreAllMocks();
jest.clearAllMocks();
await loadFixtures();
});

afterEach(async () => {
jest.restoreAllMocks();
jest.clearAllMocks();
await clearDatabase();
});

test("should respond with a 200 status code", async () => {
await agent.get("/api/v2/promo").expect(200);
});

test("should respond with 401 status code if lacking admin authentication", async () => {
(verifyAdminAuthentication as jest.Mock).mockImplementation(
(_: Request, __, next: NextFunction) => {
next(new createError.Unauthorized("You aren't logged in"));
}
);
await agent.get("/api/v2/promo").expect(401);
});

test("should return all promo codes", async () => {
const response = await agent.get("/api/v2/promo").expect(200);

const promos = response.body as Promo[];
const promoOne = promos.find((promo: Promo) => promo.code === "PROMO1");
expect(promoOne).not.toBeUndefined();
expect(promoOne?.storageInMB).toEqual(1024);
expect(promoOne?.expirationTimestamp).toEqual("2030-12-31T00:00:00.000Z");
expect(promoOne?.remainingUses).toEqual(100);
expect(promoOne?.status).toEqual("status.promo.valid");
expect(promoOne?.type).toEqual("type.generic.ok");
expect(promoOne?.createdAt).toEqual("2020-01-01T00:00:00.000Z");
expect(promoOne?.updatedAt).toEqual("2020-01-01T00:00:00.000Z");

const promoTwo = promos.find((promo: Promo) => promo.code === "PROMO2");
expect(promoTwo).not.toBeUndefined();
expect(promoTwo?.storageInMB).toEqual(2048);
expect(promoTwo?.expirationTimestamp).toEqual("2031-12-31T00:00:00.000Z");
expect(promoTwo?.remainingUses).toEqual(200);
expect(promoTwo?.status).toEqual("status.promo.valid");
expect(promoTwo?.type).toEqual("type.generic.ok");
expect(promoTwo?.createdAt).toEqual("2020-01-02T00:00:00.000Z");
expect(promoTwo?.updatedAt).toEqual("2020-01-02T00:00:00.000Z");

const promoThree = promos.find((promo: Promo) => promo.code === "PROMO3");
expect(promoThree).not.toBeUndefined();
expect(promoThree?.storageInMB).toEqual(4096);
expect(promoThree?.expirationTimestamp).toEqual("2032-12-31T00:00:00.000Z");
expect(promoThree?.remainingUses).toEqual(300);
expect(promoThree?.status).toEqual("status.promo.valid");
expect(promoThree?.type).toEqual("type.generic.ok");
expect(promoThree?.createdAt).toEqual("2020-01-03T00:00:00.000Z");
expect(promoThree?.updatedAt).toEqual("2020-01-03T00:00:00.000Z");
});

test("should response with 500 status code if the database call fails", async () => {
const testError = new Error("SQL error");
jest.spyOn(db, "sql").mockRejectedValueOnce(testError);

await agent.get("/api/v2/promo").expect(500);
});

test("should log the error if the database call fails", async () => {
const testError = new Error("SQL error");
jest.spyOn(db, "sql").mockRejectedValueOnce(testError);

await agent.get("/api/v2/promo").expect(500);
expect(logger.error).toHaveBeenCalled();
});
});
15 changes: 14 additions & 1 deletion packages/api/src/promo/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Request, Response, NextFunction } from "express";
import { verifyAdminAuthentication } from "../middleware/authentication";
import { validateCreatePromoRequest } from "./validators";
import { isValidationError } from "../validators/validator_util";
import { createPromo } from "./service";
import { createPromo, getPromos } from "./service";

export const promoController = Router();

Expand All @@ -24,3 +24,16 @@ promoController.post(
}
}
);

promoController.get(
"/",
verifyAdminAuthentication,
async (_: Request, res: Response, next: NextFunction) => {
try {
const promos = await getPromos();
res.status(200).send(promos);
} catch (err) {
next(err);
}
}
);
24 changes: 24 additions & 0 deletions packages/api/src/promo/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,27 @@ export interface CreatePromoRequest {
expirationTimestamp: string;
totalUses: Date;
}

export interface Promo {
id: string;
code: string;
storageInMB: number;
expirationTimestamp: string;
remainingUses: number;
status: string;
type: string;
createdAt: Date;
updatedAt: Date;
}

export interface PromoRow {
id: string;
code: string;
storageInMB: string;
expirationTimestamp: string;
remainingUses: string;
status: string;
type: string;
createdAt: Date;
updatedAt: Date;
}
12 changes: 12 additions & 0 deletions packages/api/src/promo/queries/get_promos.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
SELECT
promoid AS "id",
code,
sizeinmb AS "storageInMB",
expiresdt AS "expirationTimestamp",
remaininguses AS "remainingUses",
status,
type,
createddt AS "createdAt",
updateddt AS "updatedAt"
FROM
promo;
20 changes: 19 additions & 1 deletion packages/api/src/promo/service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import createError from "http-errors";
import { logger } from "@stela/logger";
import { db } from "../database";
import type { CreatePromoRequest } from "./models";
import type { CreatePromoRequest, Promo, PromoRow } from "./models";

export const createPromo = async (
promoData: CreatePromoRequest
Expand All @@ -18,3 +18,21 @@ export const createPromo = async (
throw new createError.InternalServerError("Failed to create promo");
});
};

export const getPromos = async (): Promise<Promo[]> => {
const result = await db
.sql<PromoRow>("promo.queries.get_promos")
.catch((err) => {
logger.error(err);
throw new createError.InternalServerError("Failed to retrieve promos");
});
const promos = result.rows.map<Promo>(
(row: PromoRow): Promo => ({
...row,
storageInMB: +row.storageInMB,
remainingUses: +row.remainingUses,
})
);

return promos;
};

0 comments on commit d875ad3

Please sign in to comment.