Skip to content

Commit

Permalink
chore: improve atomicity of E2E tests (#650)
Browse files Browse the repository at this point in the history
### Issue
The e2e test runner was setting ENV from multiple sources leading
cognitive complexity when reading, writing, and running tests. In
particular, Github vars were passed as part of `deno task test` while
STRIPE entities were not.

### Observations

1. Tests had different outcomes if run outside of the deno.json `test`
task.
2. Tests were not atomically runnable as they were unaware of their
required ENV vars.
3. A users ENV could alter the outcome of tests

### Improvement
I created a `setupEnv` function which defines the base ENV state our
application expects and provides a mechanism for mutating it into what a
given test requires.

1. All tests start with the same base

2. Tests are atomic
3. Deno task args minimally impact test outcomes (👀 `DENO_KV_PATH`)
6. User ENV minimally impacts test outcomes


### Example Behaviours

#### Pass a computed value as the input for an Environment Variable
```javascript
    const priceId = crypto.randomUUID();
    setupEnv(
      { "STRIPE_PREMIUM_PLAN_PRICE_ID": priceId },
    );
```

#### Ensure an Environment Variable is unset 

```javascript
    setupEnv(
      { "STRIPE_SECRET_KEY": null },
    );
```
  • Loading branch information
azohra authored Jan 2, 2024
1 parent e3385a8 commit ea5abbd
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 18 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"db:migrate": "deno run --allow-read --allow-env --allow-net --unstable tasks/db_migrate.ts",
"db:reset": "deno run --allow-read --allow-env --unstable tasks/db_reset.ts",
"start": "deno run --unstable -A --watch=static/,routes/ --env dev.ts",
"test": "DENO_KV_PATH=:memory: GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx deno test -A --unstable --parallel --coverage",
"test": "DENO_KV_PATH=:memory: deno test -A --parallel --unstable --coverage",
"check:license": "deno run --allow-read --allow-write tasks/check_license.ts",
"check:types": "deno check **/*.ts && deno check **/*.tsx",
"ok": "deno fmt --check && deno lint && deno task check:license --check && deno task check:types && deno task test",
Expand Down
93 changes: 76 additions & 17 deletions e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,31 @@ function assertRedirect(response: Response, location: string) {
assert(response.headers.get("location")?.includes(location));
}

function setupEnv(overrides: Record<string, string | null> = {}) {
const defaults: Record<string, string> = {
"STRIPE_SECRET_KEY": crypto.randomUUID(),
"STRIPE_WEBHOOK_SECRET": crypto.randomUUID(),
"STRIPE_PREMIUM_PLAN_PRICE_ID": crypto.randomUUID(),
"GITHUB_CLIENT_ID": crypto.randomUUID(),
"GITHUB_CLIENT_SECRET": crypto.randomUUID(),
// Add more default values here
};

// Merge defaults and overrides
const combinedEnvVars = { ...defaults, ...overrides };

// Set or delete environment variables
for (const [key, value] of Object.entries(combinedEnvVars)) {
if (value === null) {
Deno.env.delete(key);
} else {
Deno.env.set(key, value);
}
}
}

Deno.test("[e2e] security headers", async () => {
setupEnv();
const resp = await handler(new Request("http://localhost"));

assertEquals(
Expand All @@ -92,13 +116,15 @@ Deno.test("[e2e] security headers", async () => {
});

Deno.test("[e2e] GET /", async () => {
setupEnv();
const resp = await handler(new Request("http://localhost"));

assertEquals(resp.status, STATUS_CODE.OK);
assertHtml(resp);
});

Deno.test("[e2e] GET /callback", async (test) => {
setupEnv();
const login = crypto.randomUUID();
const sessionId = crypto.randomUUID();

Expand Down Expand Up @@ -180,6 +206,7 @@ Deno.test("[e2e] GET /callback", async (test) => {
});

Deno.test("[e2e] GET /blog", async () => {
setupEnv();
const resp = await handler(
new Request("http://localhost/blog"),
);
Expand All @@ -189,14 +216,21 @@ Deno.test("[e2e] GET /blog", async () => {
});

Deno.test("[e2e] GET /pricing", async () => {
const req = new Request("http://localhost/pricing");
const resp = await handler(req);
setupEnv({
"STRIPE_SECRET_KEY": null,
"STRIPE_PREMIUM_PLAN_PRICE_ID": null,
});
const handler = await createHandler(manifest, options);
const resp = await handler(
new Request("http://localhost/pricing"),
);

assertEquals(resp.status, STATUS_CODE.NotFound);
assertHtml(resp);
});

Deno.test("[e2e] GET /signin", async () => {
setupEnv();
const resp = await handler(
new Request("http://localhost/signin"),
);
Expand All @@ -208,6 +242,7 @@ Deno.test("[e2e] GET /signin", async () => {
});

Deno.test("[e2e] GET /signout", async () => {
setupEnv();
const resp = await handler(
new Request("http://localhost/signout"),
);
Expand All @@ -216,6 +251,7 @@ Deno.test("[e2e] GET /signout", async () => {
});

Deno.test("[e2e] GET /dashboard", async (test) => {
setupEnv();
const url = "http://localhost/dashboard";
const user = randomUser();
await createUser(user);
Expand All @@ -238,6 +274,7 @@ Deno.test("[e2e] GET /dashboard", async (test) => {
});

Deno.test("[e2e] GET /dashboard/stats", async (test) => {
setupEnv();
const url = "http://localhost/dashboard/stats";
const user = randomUser();
await createUser(user);
Expand All @@ -262,6 +299,7 @@ Deno.test("[e2e] GET /dashboard/stats", async (test) => {
});

Deno.test("[e2e] GET /dashboard/users", async (test) => {
setupEnv();
const url = "http://localhost/dashboard/users";
const user = randomUser();
await createUser(user);
Expand All @@ -286,6 +324,7 @@ Deno.test("[e2e] GET /dashboard/users", async (test) => {
});

Deno.test("[e2e] GET /submit", async () => {
setupEnv();
const resp = await handler(
new Request("http://localhost/submit"),
);
Expand All @@ -295,6 +334,7 @@ Deno.test("[e2e] GET /submit", async () => {
});

Deno.test("[e2e] GET /feed", async () => {
setupEnv();
const resp = await handler(
new Request("http://localhost/feed"),
);
Expand All @@ -304,6 +344,7 @@ Deno.test("[e2e] GET /feed", async () => {
});

Deno.test("[e2e] GET /api/items", async () => {
setupEnv();
const item1 = randomItem();
const item2 = randomItem();
await createItem(item1);
Expand All @@ -318,6 +359,7 @@ Deno.test("[e2e] GET /api/items", async () => {
});

Deno.test("[e2e] POST /submit", async (test) => {
setupEnv();
const url = "http://localhost/submit";
const user = randomUser();
await createUser(user);
Expand Down Expand Up @@ -385,6 +427,7 @@ Deno.test("[e2e] POST /submit", async (test) => {
});

Deno.test("[e2e] GET /api/items/[id]", async (test) => {
setupEnv();
const item = randomItem();
const req = new Request("http://localhost/api/items/" + item.id);

Expand All @@ -406,6 +449,7 @@ Deno.test("[e2e] GET /api/items/[id]", async (test) => {
});

Deno.test("[e2e] GET /api/users", async () => {
setupEnv();
const user1 = randomUser();
const user2 = randomUser();
await createUser(user1);
Expand All @@ -422,6 +466,7 @@ Deno.test("[e2e] GET /api/users", async () => {
});

Deno.test("[e2e] GET /api/users/[login]", async (test) => {
setupEnv();
const user = randomUser();
const req = new Request("http://localhost/api/users/" + user.login);

Expand All @@ -444,6 +489,7 @@ Deno.test("[e2e] GET /api/users/[login]", async (test) => {
});

Deno.test("[e2e] GET /api/users/[login]/items", async (test) => {
setupEnv();
const user = randomUser();
const item: Item = {
...randomItem(),
Expand Down Expand Up @@ -472,6 +518,7 @@ Deno.test("[e2e] GET /api/users/[login]/items", async (test) => {
});

Deno.test("[e2e] POST /api/vote", async (test) => {
setupEnv();
const item = randomItem();
const user = randomUser();
await createItem(item);
Expand Down Expand Up @@ -550,7 +597,9 @@ Deno.test("[e2e] POST /api/stripe-webhooks", async (test) => {
const url = "http://localhost/api/stripe-webhooks";

await test.step("serves not found response if Stripe is disabled", async () => {
Deno.env.delete("STRIPE_SECRET_KEY");
setupEnv(
{ "STRIPE_SECRET_KEY": null },
);
const resp = await handler(new Request(url, { method: "POST" }));

assertEquals(resp.status, STATUS_CODE.NotFound);
Expand All @@ -559,7 +608,7 @@ Deno.test("[e2e] POST /api/stripe-webhooks", async (test) => {
});

await test.step("serves bad request response if `Stripe-Signature` header is missing", async () => {
Deno.env.set("STRIPE_SECRET_KEY", crypto.randomUUID());
setupEnv();
const resp = await handler(new Request(url, { method: "POST" }));

assertEquals(resp.status, STATUS_CODE.BadRequest);
Expand All @@ -568,7 +617,9 @@ Deno.test("[e2e] POST /api/stripe-webhooks", async (test) => {
});

await test.step("serves internal server error response if `STRIPE_WEBHOOK_SECRET` environment variable is not set", async () => {
Deno.env.delete("STRIPE_WEBHOOK_SECRET");
setupEnv(
{ "STRIPE_WEBHOOK_SECRET": null },
);
const resp = await handler(
new Request(url, {
method: "POST",
Expand All @@ -585,7 +636,7 @@ Deno.test("[e2e] POST /api/stripe-webhooks", async (test) => {
});

await test.step("serves bad request response if the event payload is invalid", async () => {
Deno.env.set("STRIPE_WEBHOOK_SECRET", crypto.randomUUID());
setupEnv();
const resp = await handler(
new Request(url, {
method: "POST",
Expand Down Expand Up @@ -728,6 +779,7 @@ Deno.test("[e2e] POST /api/stripe-webhooks", async (test) => {
});

Deno.test("[e2e] GET /account", async (test) => {
setupEnv();
const url = "http://localhost/account";

await test.step("redirects to sign-in page if the session user is not signed in", async () => {
Expand Down Expand Up @@ -768,8 +820,8 @@ Deno.test("[e2e] GET /account", async (test) => {
});

Deno.test("[e2e] GET /account/manage", async (test) => {
setupEnv();
const url = "http://localhost/account/manage";
Deno.env.set("STRIPE_SECRET_KEY", crypto.randomUUID());

await test.step("redirects to sign-in page if the session user is not signed in", async () => {
const resp = await handler(new Request(url));
Expand Down Expand Up @@ -816,6 +868,7 @@ Deno.test("[e2e] GET /account/manage", async (test) => {
});

Deno.test("[e2e] GET /account/upgrade", async (test) => {
setupEnv();
const url = "http://localhost/account/upgrade";

await test.step("redirects to sign-in page if the session user is not signed in", async () => {
Expand All @@ -828,8 +881,10 @@ Deno.test("[e2e] GET /account/upgrade", async (test) => {
await createUser(user);

await test.step("serves internal server error response if the `STRIPE_PREMIUM_PLAN_PRICE_ID` environment variable is not set", async () => {
Deno.env.set("STRIPE_SECRET_KEY", crypto.randomUUID());
Deno.env.delete("STRIPE_PREMIUM_PLAN_PRICE_ID");
setupEnv(
{ "STRIPE_PREMIUM_PLAN_PRICE_ID": null },
);

const resp = await handler(
new Request(url, {
headers: { cookie: "site-session=" + user.sessionId },
Expand All @@ -841,8 +896,10 @@ Deno.test("[e2e] GET /account/upgrade", async (test) => {
});

await test.step("serves not found response if Stripe is disabled", async () => {
Deno.env.set("STRIPE_PREMIUM_PLAN_PRICE_ID", crypto.randomUUID());
Deno.env.delete("STRIPE_SECRET_KEY");
setupEnv(
{ "STRIPE_SECRET_KEY": null },
);

const resp = await handler(
new Request(url, {
headers: { cookie: "site-session=" + user.sessionId },
Expand All @@ -854,8 +911,7 @@ Deno.test("[e2e] GET /account/upgrade", async (test) => {
});

await test.step("serves not found response if Stripe returns a `null` URL", async () => {
Deno.env.set("STRIPE_PREMIUM_PLAN_PRICE_ID", crypto.randomUUID());
Deno.env.set("STRIPE_SECRET_KEY", crypto.randomUUID());
setupEnv();

const session = { url: null } as Stripe.Response<
Stripe.Checkout.Session
Expand All @@ -880,8 +936,9 @@ Deno.test("[e2e] GET /account/upgrade", async (test) => {

await test.step("redirects to the URL returned by Stripe after creating a checkout session", async () => {
const priceId = crypto.randomUUID();
Deno.env.set("STRIPE_PREMIUM_PLAN_PRICE_ID", priceId);
Deno.env.set("STRIPE_SECRET_KEY", crypto.randomUUID());
setupEnv(
{ "STRIPE_PREMIUM_PLAN_PRICE_ID": priceId },
);

const session = { url: "https://stubbing-returned-url" } as Stripe.Response<
Stripe.Checkout.Session
Expand All @@ -905,6 +962,7 @@ Deno.test("[e2e] GET /account/upgrade", async (test) => {
});

Deno.test("[e2e] GET /api/me/votes", async () => {
setupEnv();
const user = randomUser();
await createUser(user);
const item1 = randomItem();
Expand Down Expand Up @@ -932,8 +990,9 @@ Deno.test("[e2e] GET /api/me/votes", async () => {
});

Deno.test("[e2e] GET /welcome", async () => {
Deno.env.delete("GITHUB_CLIENT_ID");

setupEnv(
{ "GITHUB_CLIENT_ID": null },
);
const req = new Request("http://localhost/");
const resp = await handler(req);

Expand Down

0 comments on commit ea5abbd

Please sign in to comment.