Skip to content

Commit

Permalink
Test the OIDC callback page
Browse files Browse the repository at this point in the history
Signed-off-by: Aurélien Bompard <aurelien@bompard.org>
  • Loading branch information
abompard committed Apr 7, 2023
1 parent 8b84bfd commit de1f074
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 3 deletions.
15 changes: 15 additions & 0 deletions frontend/src/api/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Contributors to the Fedora Project
//
// SPDX-License-Identifier: MIT

import axios, { type AxiosInstance } from "axios";
import { vi } from "vitest";

export const apiClient = {
defaults: axios.defaults,
get: vi.fn(),
} as unknown as AxiosInstance;

export async function getApiClient() {
return apiClient;
}
179 changes: 179 additions & 0 deletions frontend/src/auth/LoginFedora.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: Contributors to the Fedora Project
//
// SPDX-License-Identifier: MIT

import { apiClient } from "@/api/__mocks__";
import { useToastStore } from "@/stores/toast";
import { useUserStore } from "@/stores/user";
import { getRenderOptions } from "@/util/tests";
import { TokenResponse } from "@openid/appauth";
import {
cleanup,
render,
waitFor,
type RenderOptions,
} from "@testing-library/vue";
import { createPinia, setActivePinia } from "pinia";
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
type Mocked,
} from "vitest";
import router from "../router";
import LoginFedora from "./LoginFedora.vue";
import type Authenticator from "./authenticator";
import type { AuthorizationRedirectListener } from "./authenticator";

vi.mock("@/api");

describe("LoginFedora", () => {
let tokenResponse: TokenResponse;
let renderOptions: RenderOptions;
let auth: Mocked<Authenticator>;

beforeEach(async () => {
await router.replace("/login/fedora");
await router.isReady();
setActivePinia(createPinia());
renderOptions = getRenderOptions();
auth = renderOptions.global?.provide?.auth;
auth.handleAuthorizationRedirect.mockImplementation(
async (listener: AuthorizationRedirectListener) => {
listener(tokenResponse);
}
);
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
sessionStorage.clear();
});

it("renders", () => {
const { getByText } = render(LoginFedora, renderOptions);
expect(getByText("Loading user information...")).toBeInTheDocument();
});

it("redirects if already logged in", async () => {
const store = useUserStore();
store.$patch({
accessToken: "testing",
username: "dummy-user",
fullName: "Dummy User",
email: "dummy@example.com",
});
render(LoginFedora, renderOptions);
await waitFor(() => {
expect(router.currentRoute.value.path).toBe("/");
});
});

it("handles the incoming OIDC data", async () => {
tokenResponse = new TokenResponse({
access_token: "dummy-access-token",
refresh_token: "dummy-refresh-token",
scope: "dummy-serverside-scope",
});
const userInfoResponse = {
sub: "dummy-sub",
nickname: "dummy-username",
};
auth.makeUserInfoRequest.mockResolvedValue(userInfoResponse);
vi.mocked(apiClient.get).mockResolvedValue({ data: { is_admin: false } });

render(LoginFedora, renderOptions);

const store = useUserStore();

await waitFor(() => {
expect(store.loggedIn).toBe(true);
// The call to the API is async
expect(vi.mocked(apiClient.get)).toHaveBeenCalled();
});

expect(auth.makeUserInfoRequest).toHaveBeenCalledWith("dummy-access-token");
expect(vi.mocked(apiClient.get)).toHaveBeenCalledWith("/api/v1/users/me");
expect(store.$state).toStrictEqual({
accessToken: "dummy-access-token",
refreshToken: "dummy-refresh-token",
idToken: null,
tokenExpiresAt: null,
scopes: [],
username: "dummy-username",
fullName: "dummy-username",
email: "",
isAdmin: false,
});
// We must have been redirected
await waitFor(() => {
expect(router.currentRoute.value.path).toBe("/");
});
const toastStore = useToastStore();
expect(toastStore.toasts).toHaveLength(1);
expect(toastStore.toasts[0].title).toBe("Login successful!");
expect(toastStore.toasts[0].content).toBe("Welcome, dummy-username.");
expect(toastStore.toasts[0].color).toBe("success");
});

it("handles the incoming OIDC data about an admin", async () => {
tokenResponse = new TokenResponse({
access_token: "dummy-access-token",
refresh_token: "dummy-refresh-token",
scope: "dummy-serverside-scope",
});
auth.makeUserInfoRequest.mockResolvedValue({
sub: "dummy-sub",
nickname: "dummy-username",
});
vi.mocked(apiClient.get).mockResolvedValue({ data: { is_admin: true } });

render(LoginFedora, renderOptions);

const store = useUserStore();

await waitFor(() => {
expect(vi.mocked(apiClient.get)).toHaveBeenCalled();
});
expect(store.loggedIn).toBe(true);
expect(store.isAdmin).toBe(true);
});

it("displays authentication errors", async () => {
const { getByText } = render(LoginFedora, renderOptions);
auth.handleAuthorizationRedirect.mockRejectedValue("Dummy Error");
await waitFor(() => {
expect(getByText("Dummy Error")).toBeInTheDocument();
});
});

it("redirects to the right place on login", async () => {
tokenResponse = new TokenResponse({
access_token: "dummy-access-token",
});
auth.makeUserInfoRequest.mockResolvedValue({
sub: "dummy-sub",
nickname: "dummy-username",
});
vi.mocked(apiClient.get).mockResolvedValue({ data: { is_admin: false } });
sessionStorage.setItem("redirect_to", "/dummy/page");

render(LoginFedora, renderOptions);

await waitFor(() => {
expect(router.currentRoute.value.path).toBe("/dummy/page");
});
expect(sessionStorage.getItem("redirect_to")).toBeNull();
});

it("displays authentication errors", async () => {
const { getByText } = render(LoginFedora, renderOptions);
auth.handleAuthorizationRedirect.mockRejectedValue("Dummy Error");
await waitFor(() => {
expect(getByText("Dummy Error")).toBeInTheDocument();
});
});
});
1 change: 1 addition & 0 deletions frontend/src/auth/LoginFedora.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ onMounted(async () => {
});
} catch (err) {
error.value = err as string;
loading.value = false;
}
});
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/auth/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
type UserInfoResponseJson,
} from "./userinfo_request";

export type AuthorizationRedirectListener = (
result: TokenResponse
) => TokenResponse | Promise<TokenResponse>;

export class NoHashQueryStringUtils extends BasicQueryStringUtils {
parse(input: LocationLike) {
return super.parse(input, false /* never use hash */);
Expand Down Expand Up @@ -186,9 +190,7 @@ export default class Authenticator {
);
}

handleAuthorizationRedirect(
listener: (result: TokenResponse) => TokenResponse | Promise<TokenResponse>
) {
handleAuthorizationRedirect(listener: AuthorizationRedirectListener) {
this.notifier.setAuthorizationListener((request, response, error) => {
console.log("Authorization request complete ", request, response, error);
if (response) {
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/util/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Contributors to the Fedora Project
//
// SPDX-License-Identifier: MIT

import { vueQueryPluginOptions } from "@/api";
import type { UserInfoResponseJson } from "@/auth/userinfo_request";
import { config as formkitConfig } from "@/forms/index";
import { plugin as FormKitPlugin } from "@formkit/vue";
import type { RenderOptions } from "@testing-library/vue";
import { getActivePinia, type Pinia } from "pinia";
import { vi } from "vitest";
import { createI18n } from "vue-i18n";
import { VueQueryPlugin } from "vue-query";
import router from "../router";

export const getRenderOptions = (): RenderOptions => {
const pinia = getActivePinia() as Pinia;
const i18n = createI18n({
legacy: false,
locale: navigator.language,
fallbackLocale: "en-US",
messages: {},
});
return {
global: {
plugins: [
router,
pinia,
i18n,
[FormKitPlugin, formkitConfig],
[VueQueryPlugin, vueQueryPluginOptions],
],
provide: {
auth: {
fetchServiceConfiguration: vi.fn().mockResolvedValue(null),
handleAuthorizationRedirect: vi.fn().mockResolvedValue(null),
makeUserInfoRequest: vi
.fn()
.mockResolvedValue({} as UserInfoResponseJson),
},
},
},
};
};

0 comments on commit de1f074

Please sign in to comment.