Skip to content

Commit

Permalink
Extract mock authentication api
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-wratt committed Apr 11, 2024
1 parent 8ff15c2 commit c9410d7
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 63 deletions.
65 changes: 65 additions & 0 deletions src/mock/api/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Factor } from "../../types";

export interface FactorList {
allOptions: Factor[];
default: Factor | undefined;
}

export function authenticationMock(_flow: string, { userId }: { userId: string }) {
let numCallsToSetPrimaryUser = 0;

const state = {
primaryUserId: userId,
challenge: false as false | FactorList,
enrollment: false as false | FactorList,
newlyRecordedMethods: [] as string[],
};

const build = <T>(api: T) => ({
challengeWith: (factor: Factor, options?: { additionalFactors?: Factor[] }) => {
const additionalFactors = options?.additionalFactors ?? [];

state.challenge = {
allOptions: [factor, ...additionalFactors],
default: factor,
};
},
challengeWithAny(factors: Factor[]) {
state.challenge = {
allOptions: factors,
default: undefined,
};
},
enrollWith: (factor: Factor, options?: { additionalFactors?: Factor[] }) => {
const additionalFactors = options?.additionalFactors ?? [];

state.enrollment = {
allOptions: [factor, ...additionalFactors],
default: factor,
};
},
enrollWithAny(factors: Factor[]) {
state.enrollment = {
allOptions: factors,
default: undefined,
};
},
setPrimaryUser: (primaryUserId: string) => {
numCallsToSetPrimaryUser++;

if (numCallsToSetPrimaryUser > 1) {
throw new Error(
"`authentication.setPrimaryUser` can only be set once per transaction"
);
}

state.primaryUserId = primaryUserId;
},
recordMethod: (providerUrl: string) => {
state.newlyRecordedMethods.push(providerUrl);
return api;
},
});

return { state, build };
}
71 changes: 10 additions & 61 deletions src/mock/api/post-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ok } from "node:assert";
import { encodeHS256JWT, signHS256 } from "../../jwt/hs256";
import { accessTokenMock } from "./access-token";
import { accessMock } from "./access";
import { authenticationMock, FactorList } from "./authentication";

export interface PostLoginOptions {
user?: Auth0.User;
Expand Down Expand Up @@ -45,21 +46,16 @@ interface SamlResponseState {
signingCert?: string;
}

interface FactorList {
allOptions: Factor[];
default: Factor | undefined;
}

export interface PostLoginState {
user: Auth0.User;
primaryUserId: string;
cache: Auth0.API.Cache;
access: { denied: false } | { denied: { reason: string } };
accessToken: {
claims: Record<string, unknown>;
scopes: string[];
};
authentication: {
primaryUserId: string;
challenge: FactorList | false;
enrollment: FactorList | false;
newlyRecordedMethods: string[];
Expand Down Expand Up @@ -89,27 +85,22 @@ export function postLogin({
executedRules: optionallyExecutedRules,
now: nowValue,
}: PostLoginOptions = {}) {
const userValue = user ?? mockUser();
const executedRules = optionallyExecutedRules ?? [];
const requestValue = request ?? mockRequest();

const apiCache = mockCache(cache);
const access = accessMock("PostLogin");
const accessToken = accessTokenMock("PostLogin");
const executedRules = optionallyExecutedRules ?? [];
const userValue = user ?? mockUser();
const requestValue = request ?? mockRequest();
const authentication = authenticationMock("PostLogin", { userId: userValue.user_id });

const now = new Date(nowValue || Date.now());

let numCallsToSetPrimaryUser = 0;

const state: PostLoginState = {
user: userValue,
primaryUserId: userValue.user_id,
access: access.state,
accessToken: accessToken.state,
authentication: {
challenge: false,
enrollment: false,
newlyRecordedMethods: [],
},
authentication: authentication.state,
cache: apiCache,
idToken: {
claims: {},
Expand Down Expand Up @@ -187,50 +178,8 @@ export function postLogin({
return accessToken.build(api);
},

authentication: {
challengeWith: (factor, options) => {
const additionalFactors = options?.additionalFactors ?? [];

state.authentication.challenge = {
allOptions: [factor, ...additionalFactors],
default: factor,
};
},
challengeWithAny(factors) {
state.authentication.challenge = {
allOptions: factors,
default: undefined,
};
},
enrollWith(factor, options) {
const additionalFactors = options?.additionalFactors ?? [];

state.authentication.enrollment = {
allOptions: [factor, ...additionalFactors],
default: factor,
};
},
enrollWithAny(factors) {
state.authentication.enrollment = {
allOptions: factors,
default: undefined,
};
},
setPrimaryUser: (primaryUserId) => {
numCallsToSetPrimaryUser++;

if (numCallsToSetPrimaryUser > 1) {
throw new Error(
"`authentication.setPrimaryUser` can only be set once per transaction"
);
}

state.primaryUserId = primaryUserId;
},
recordMethod: (providerUrl) => {
state.authentication.newlyRecordedMethods.push(providerUrl);
return api;
},
get authentication() {
return authentication.build(api);
},

cache: apiCache,
Expand Down
111 changes: 111 additions & 0 deletions src/test/api/authentication.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import test from "node:test";
import { authenticationMock } from "../../mock/api/authentication";
import { ok, strictEqual } from "node:assert";

test("authentication mock", async (t) => {
const baseApi = Symbol("Base API");

await t.test("challengeWith", async (t) => {
await t.test("factor", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.challengeWith({ type: "email" });

ok(state.challenge, "Expected challenge to be set");
strictEqual(state.challenge.default?.type, "email", "Expected default factor to be set");
strictEqual(state.challenge.allOptions?.length, 1, "Expected additional factors to be set");
strictEqual(state.challenge.allOptions?.[0].type, "email", "Expected additional factors to be email");
});

await t.test("additional factors", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.challengeWith({ type: "otp" }, { additionalFactors: [{ type: "email" }] });

ok(state.challenge, "Expected challenge to be set");
strictEqual(state.challenge.default?.type, "otp", "Expected default factor to be set");
strictEqual(state.challenge.allOptions?.length, 2, "Expected additional factors to be set");
strictEqual(state.challenge.allOptions?.[0].type, "otp", "Expected additional factor to be otp");
strictEqual(state.challenge.allOptions?.[1].type, "email", "Expected additional factor to be email");
});
});

await t.test('challengeWithAny', async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.challengeWithAny([{ type: "email" }, { type: "otp" }]);

ok(state.challenge, "Expected challenge to be set");
strictEqual(state.challenge.default, undefined, "Expected default factor to be undefined");
strictEqual(state.challenge.allOptions?.length, 2, "Expected additional factors to be set");
strictEqual(state.challenge.allOptions?.[0].type, "email", "Expected additional factor to be email");
strictEqual(state.challenge.allOptions?.[1].type, "otp", "Expected additional factor to be otp");
});

await t.test("enrollWith", async (t) => {
await t.test("factor", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.enrollWith({ type: "email" });

ok(state.enrollment, "Expected enrollment to be set");
strictEqual(state.enrollment.default?.type, "email", "Expected default factor to be set");
strictEqual(state.enrollment.allOptions?.length, 1, "Expected additional factors to be set");
strictEqual(state.enrollment.allOptions?.[0].type, "email", "Expected additional factors to be email");
});

await t.test("additional factors", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.enrollWith({ type: "otp" }, { additionalFactors: [{ type: "email" }] });

ok(state.enrollment, "Expected enrollment to be set");
strictEqual(state.enrollment.default?.type, "otp", "Expected default factor to be set");
strictEqual(state.enrollment.allOptions?.length, 2, "Expected additional factors to be set");
strictEqual(state.enrollment.allOptions?.[0].type, "otp", "Expected additional factor to be otp");
strictEqual(state.enrollment.allOptions?.[1].type, "email", "Expected additional factor to be email");
});
});

await t.test('enrollWithAny', async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.enrollWithAny([{ type: "email" }, { type: "otp" }]);

ok(state.enrollment, "Expected enrollment to be set");
strictEqual(state.enrollment.default, undefined, "Expected default factor to be undefined");
strictEqual(state.enrollment.allOptions?.length, 2, "Expected additional factors to be set");
strictEqual(state.enrollment.allOptions?.[0].type, "email", "Expected additional factor to be email");
strictEqual(state.enrollment.allOptions?.[1].type, "otp", "Expected additional factor to be otp");
});

await t.test("setPrimaryUser", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.setPrimaryUser("43");

strictEqual(state.primaryUserId, "43", "Expected primary user to be set");
});

await t.test("recordMethod", async (t) => {
const { build, state } = authenticationMock("Another Flow", { userId: "42" });
const api = build(baseApi);

api.recordMethod("https://example.com");

strictEqual(state.newlyRecordedMethods.length, 1, "Expected newly recorded methods to be set");
strictEqual(state.newlyRecordedMethods[0], "https://example.com", "Expected newly recorded method to be set");

api.recordMethod("https://another.example.com");

strictEqual(state.newlyRecordedMethods.length, 2, "Expected newly recorded methods to be set");
strictEqual(state.newlyRecordedMethods[1], "https://another.example.com", "Expected newly recorded method to be set");
});
});
4 changes: 2 additions & 2 deletions src/test/api/post-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,14 @@ test("PostLogin API", async (t) => {
await t.test("can change the primary user ID", async (t) => {
const { implementation: api, state } = postLogin();
const originalUserId = state.user.user_id;
strictEqual(state.primaryUserId, originalUserId);
strictEqual(state.authentication.primaryUserId, originalUserId);

strictEqual(
api.authentication.setPrimaryUser("new-primary-user-id"),
undefined
);

strictEqual(state.primaryUserId, "new-primary-user-id");
strictEqual(state.authentication.primaryUserId, "new-primary-user-id");
strictEqual(state.user.user_id, originalUserId);
});

Expand Down

0 comments on commit c9410d7

Please sign in to comment.