Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract API implementations from Post Login API #12

Merged
merged 13 commits into from
Apr 11, 2024
4 changes: 2 additions & 2 deletions examples/geo-filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ test("Filter access based on continent code", async (t) => {
ok(action.access.denied, "Expected access to be denied");

strictEqual(
action.access.code,
action.access.denied.code,
"invalid_request",
"Unexpected denial code"
);

strictEqual(
action.access.reason,
action.access.denied.reason,
"Access from North America is not allowed.",
"Unexpected denial reason"
);
Expand Down
2 changes: 1 addition & 1 deletion examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions examples/redirect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,26 @@ test("redirect and continue with signed data", async (t) => {
const { redirect } = action;

strict(
redirect.queryParams.theme,
redirect.target.queryParams.theme,
"spiffy",
"Unexpected value for `theme` query parameter"
);

strictEqual(
// You can also use redirect.url.href to get the full URL as a string
redirect.url.origin,
redirect.target.url.origin,
"https://example.com",
"Unexpected redirect URL origin"
);

strictEqual(
redirect.url.pathname,
redirect.target.url.pathname,
"/sandwich-preferences",
"Unexpected redirect URL path"
);

// Test the signed JWT data payload
const { session_token } = redirect.queryParams;
const { session_token } = redirect.target.queryParams;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll revert this in the future (e.g. with a Proxy object) as this just creates an unnecessary property to have to type. See #19.


const decoded = jwt.decodeJWTPayload(session_token);

Expand Down
73 changes: 73 additions & 0 deletions src/mock/api/access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
interface AccessTokenMock {
build: <T>(api: T) => {
addScope: (name: string) => T;
removeScope: (name: string) => T;
setCustomClaim: (name: string, value: unknown) => T;
};
state: {
scopes: string[];
claims: Record<string, unknown>;
};
}

interface CredentialsExchangeAccessTokenMock {
build: <T>(api: T) => {
setCustomClaim: (name: string, value: unknown) => T;
};
state: {
claims: Record<string, unknown>;
};
}

export function accessTokenMock(flow: "CredentialsExchange"): CredentialsExchangeAccessTokenMock;
export function accessTokenMock(flow: string): AccessTokenMock;
export function accessTokenMock(flow: string) {
switch(flow) {
case "CredentialsExchange": {
const state = {
claims: {} as Record<string, unknown>,
};

const build = <T>(api: T) => ({
setCustomClaim: (name: string, value: unknown) => {
state.claims[name] = value;
return api;
},
})

return { build, state };
}

default: {
const state = {
scopes: [] as string[],
claims: {} as Record<string, unknown>,
};

const build = <T>(api: T) => ({
addScope: (name: string) => {
state.scopes = [
...new Set(state.scopes).add(name),
];

return api;
},

removeScope: (name: string) => {
state.scopes = state.scopes.filter(
(value) => value !== name
);

return api;
},

setCustomClaim: (name: string, value: unknown) => {
state.claims[name] = value;
return api;
},
})

return { build, state };
}
}
}
53 changes: 53 additions & 0 deletions src/mock/api/access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
interface AccessMock {
build: <API>(api: API) => {
deny: (reason: string) => API;
};
state: {
denied: false | { reason: string };
};
};

interface CredentialsExchangeAccessMock {
build: <API>(api: API) => {
deny: (code: string, reason: string) => API;
};
state: {
denied: false | { code: string; reason: string };
};
};

export function accessMock(flow: "CredentialsExchange"): CredentialsExchangeAccessMock;
export function accessMock(flow: string): AccessMock;
export function accessMock(flow: string) {
switch(flow) {
case "CredentialsExchange": {
const state = {
denied: false as false | { code: string; reason: string },
};

const build = <API>(api: API) => ({
deny: (code: string, reason: string) => {
state.denied = { code, reason };
return api;
},
});

return { build, state };
}

default: {
const state = {
denied: false as false | { reason: string },
};

const build = <API>(api: API) => ({
deny: (reason: string) => {
state.denied = { reason };
return api;
},
});

return { build, state };
}
}
}
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 };
}
27 changes: 11 additions & 16 deletions src/mock/api/credentials-exchange.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Auth0 from "../../types";
import { cache as mockCache } from "./cache";
import { request as mockRequest } from "../request";
import { accessTokenMock } from "./access-token";
import { accessMock } from "./access";

export interface CredentialsExchangeOptions {
cache?: Record<string, string>;
}

export interface CredentialsExchangeState {
access: { denied: false } | { denied: true; code: string; reason: string };
access: { denied: false } | { denied: { code: string; reason: string } };
accessToken: {
claims: Record<string, unknown>;
};
Expand All @@ -18,28 +19,22 @@ export function credentialsExchange({
cache,
}: CredentialsExchangeOptions = {}) {
const apiCache = mockCache(cache);
const access = accessMock("CredentialsExchange");
const accessToken = accessTokenMock("CredentialsExchange")

const state: CredentialsExchangeState = {
access: { denied: false },
accessToken: {
claims: {},
},
access: access.state,
accessToken: accessToken.state,
cache: apiCache,
};

const api: Auth0.API.CredentialsExchange = {
access: {
deny: (code, reason) => {
state.access = { denied: true, code, reason };
return api;
},
get access() {
return access.build(api);
},

accessToken: {
setCustomClaim: (name, value) => {
state.accessToken.claims[name] = value;
return api;
},
get accessToken() {
return accessToken.build(api);
},

cache: apiCache,
Expand Down
14 changes: 14 additions & 0 deletions src/mock/api/id-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function idTokenMock(flow: string) {
const state = {
claims: {} as Record<string, unknown>,
};

const build = <T>(api: T) => ({
setCustomClaim: (name: string, value: unknown) => {
state.claims[name] = value;
return api;
},
});

return { state, build };
}
16 changes: 16 additions & 0 deletions src/mock/api/multifactor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MultifactorEnableOptions } from "../../types";

export function multifactorMock(flow: string) {
const state = {
enabled: false as false | { provider: string; options?: MultifactorEnableOptions },
};

const build = <T>(api: T) => ({
enable: (provider: string, options?: MultifactorEnableOptions) => {
state.enabled = { provider, options };
return api;
},
});

return { state, build };
}
Loading
Loading