mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@/auth-state", () => ({
|
|
clearAccessToken: vi.fn(),
|
|
}));
|
|
|
|
import { clearAccessToken } from "@/auth-state";
|
|
import { redirectOnAuthFailure } from "@/utils/auth-redirect";
|
|
|
|
const mockedClearAccessToken = vi.mocked(clearAccessToken);
|
|
|
|
type NavigationStub = { replace: ReturnType<typeof vi.fn>; href: string };
|
|
|
|
function installLocation(href: string): NavigationStub {
|
|
const url = new URL(href);
|
|
const replace = vi.fn((next: string) => {
|
|
// Mirror real navigation: update the mutable href on subsequent inspection.
|
|
location.href = new URL(next, url).toString();
|
|
});
|
|
const location: NavigationStub = { replace, href: url.toString() };
|
|
|
|
Object.defineProperty(window, "location", {
|
|
configurable: true,
|
|
value: {
|
|
get href() {
|
|
return location.href;
|
|
},
|
|
set href(value: string) {
|
|
location.href = value;
|
|
},
|
|
pathname: url.pathname,
|
|
search: url.search,
|
|
hash: url.hash,
|
|
origin: url.origin,
|
|
replace,
|
|
},
|
|
});
|
|
|
|
return location;
|
|
}
|
|
|
|
describe("redirectOnAuthFailure", () => {
|
|
let originalLocation: Location;
|
|
|
|
beforeEach(() => {
|
|
originalLocation = window.location;
|
|
});
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(window, "location", {
|
|
configurable: true,
|
|
value: originalLocation,
|
|
});
|
|
});
|
|
|
|
it("does nothing when the user is already on an /auth page", () => {
|
|
const nav = installLocation("http://localhost/auth?foo=bar");
|
|
|
|
redirectOnAuthFailure();
|
|
|
|
expect(nav.replace).not.toHaveBeenCalled();
|
|
expect(mockedClearAccessToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does nothing on a public route by default", () => {
|
|
const nav = installLocation("http://localhost/explore");
|
|
|
|
redirectOnAuthFailure();
|
|
|
|
expect(nav.replace).not.toHaveBeenCalled();
|
|
expect(mockedClearAccessToken).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("clears the token and redirects to /auth on a protected route", () => {
|
|
const nav = installLocation("http://localhost/home?tab=pins#latest");
|
|
|
|
redirectOnAuthFailure();
|
|
|
|
expect(mockedClearAccessToken).toHaveBeenCalledTimes(1);
|
|
expect(nav.replace).toHaveBeenCalledWith("/auth?redirect=%2Fhome%3Ftab%3Dpins%23latest");
|
|
});
|
|
|
|
it("honours forceRedirect even on a public route", () => {
|
|
const nav = installLocation("http://localhost/explore");
|
|
|
|
redirectOnAuthFailure(true);
|
|
|
|
expect(mockedClearAccessToken).toHaveBeenCalledTimes(1);
|
|
expect(nav.replace).toHaveBeenCalledWith("/auth?redirect=%2Fexplore");
|
|
});
|
|
|
|
it("embeds the reason parameter when provided", () => {
|
|
const nav = installLocation("http://localhost/home");
|
|
|
|
redirectOnAuthFailure(false, { reason: "protected-memo" });
|
|
|
|
expect(nav.replace).toHaveBeenCalledWith("/auth?redirect=%2Fhome&reason=protected-memo");
|
|
});
|
|
|
|
it("prefers an explicitly provided redirect target over the current location", () => {
|
|
const nav = installLocation("http://localhost/home");
|
|
|
|
redirectOnAuthFailure(false, { redirect: "/setting" });
|
|
|
|
expect(nav.replace).toHaveBeenCalledWith("/auth?redirect=%2Fsetting");
|
|
});
|
|
|
|
it("drops an unsafe redirect target silently", () => {
|
|
const nav = installLocation("http://localhost/home");
|
|
|
|
redirectOnAuthFailure(false, { redirect: "//evil.example/phish" });
|
|
|
|
expect(nav.replace).toHaveBeenCalledWith("/auth");
|
|
});
|
|
});
|