diff --git a/frontend/src/index.test.ts b/frontend/src/index.test.ts index ab280e28..94daac59 100644 --- a/frontend/src/index.test.ts +++ b/frontend/src/index.test.ts @@ -5,7 +5,7 @@ import { restore, stub } from "sinon"; import { NavigateController } from "./controllers/navigate"; import { NotifyController } from "./controllers/notify"; import { type AppSettings } from "./utils/app"; -import AuthService from "./utils/AuthService"; +import AuthService, { type LoggedInEventDetail } from "./utils/AuthService"; import { AppStateService } from "./utils/state"; import { formatAPIUser } from "./utils/user"; @@ -27,6 +27,12 @@ const mockAPIUser: APIUser = { ], }; const mockUserInfo = formatAPIUser(mockAPIUser); +const mockAuth = { + headers: { Authorization: self.crypto.randomUUID() }, + tokenExpiresAt: Date.now(), + username: "test-auth@example.com", + user: mockAPIUser, +}; const mockAppSettings: AppSettings = { registrationEnabled: false, @@ -203,4 +209,41 @@ describe("browsertrix-app", () => { expect(el.appState.orgSlug).to.equal(id); }); + + describe(".onLoggedIn()", () => { + describe("routing", () => { + it("routes to redirect URL if specified", async () => { + stub(App.prototype, "routeTo"); + + const event = new CustomEvent("btrix-logged-in", { + detail: { + ...mockAuth, + redirectUrl: "/fake-page", + }, + }); + + const el = await fixture(""); + + el.onLoggedIn(event); + + expect(el.routeTo).to.have.been.calledWith("/fake-page"); + }); + + it("falls back to account settings", async () => { + stub(App.prototype, "routeTo"); + + const event = new CustomEvent("btrix-logged-in", { + detail: { + ...mockAuth, + }, + }); + + const el = await fixture(""); + + el.onLoggedIn(event); + + expect(el.routeTo).to.have.been.calledWith("/account/settings"); + }); + }); + }); }); diff --git a/frontend/src/pages/log-in.test.ts b/frontend/src/pages/log-in.test.ts new file mode 100644 index 00000000..96625b99 --- /dev/null +++ b/frontend/src/pages/log-in.test.ts @@ -0,0 +1,132 @@ +import { expect, fixture, oneEvent } from "@open-wc/testing"; +import { html } from "lit/static-html.js"; +import { match, restore, stub } from "sinon"; + +import type { APIUser } from ".."; + +import { LogInPage } from "./log-in"; + +import { ROUTES } from "@/routes"; +import APIRouter from "@/utils/APIRouter"; +import AuthService from "@/utils/AuthService"; +import { AppStateService } from "@/utils/state"; + +const router = new APIRouter(ROUTES); +const viewState = router.match("/log-in"); +const mockAPIUser: APIUser = { + id: "740d7b63-b257-4311-ba3f-adc46a5fafb8", + email: "test-user@example.com", + name: "Test User", + is_verified: false, + is_superuser: false, + orgs: [ + { + id: "e21ab647-2d0e-489d-97d1-88ac91774942", + name: "test org", + slug: "test-org", + role: 10, + }, + ], +}; +const mockAuth = { + headers: { Authorization: self.crypto.randomUUID() }, + tokenExpiresAt: Date.now(), + username: "test-auth@example.com", + user: mockAPIUser, +}; + +describe("", () => { + beforeEach(() => { + AppStateService.resetAll(); + stub(window.history, "pushState"); + }); + + afterEach(() => { + restore(); + }); + + it("is defined", async () => { + const el = await fixture( + html``, + ); + + expect(el).instanceOf(LogInPage); + }); + + describe("form submit", () => { + it("creates logged in event on success", async () => { + stub(AuthService, "login").callsFake(async () => + Promise.resolve(mockAuth), + ); + + const el = await fixture( + html``, + ); + const form = el.shadowRoot!.querySelector("form")!; + + const loggedInListener = oneEvent(el, "btrix-logged-in"); + const submitListener = oneEvent(form, "submit"); + + form.requestSubmit(); + + await submitListener; + const loggedInEvent = await loggedInListener; + + expect(loggedInEvent.detail.user).to.exist; + }); + + it("updates org slug in state", async () => { + stub(AuthService, "login").callsFake(async () => + Promise.resolve(mockAuth), + ); + stub(AppStateService, "updateUser"); + + const el = await fixture( + html``, + ); + const form = el.shadowRoot!.querySelector("form")!; + + const loggedInListener = oneEvent(el, "btrix-logged-in"); + const submitListener = oneEvent(form, "submit"); + + form.requestSubmit(); + + await submitListener; + await loggedInListener; + + expect(AppStateService.updateUser).to.have.been.calledWith( + match.any, + "test-org", + ); + }); + + it("handles users without org", async () => { + stub(AuthService, "login").callsFake(async () => + Promise.resolve({ + ...mockAuth, + user: { + ...mockAPIUser, + orgs: [], + }, + }), + ); + stub(AppStateService, "updateUser"); + + const el = await fixture( + html``, + ); + const form = el.shadowRoot!.querySelector("form")!; + + const loggedInListener = oneEvent(el, "btrix-logged-in"); + const submitListener = oneEvent(form, "submit"); + + form.requestSubmit(); + + await submitListener; + const loggedInEvent = await loggedInListener; + + expect(AppStateService.updateUser).not.to.have.been.called; + expect(loggedInEvent.detail.user).to.exist; + }); + }); +}); diff --git a/frontend/src/pages/log-in.ts b/frontend/src/pages/log-in.ts index 395e47da..53ef46d6 100644 --- a/frontend/src/pages/log-in.ts +++ b/frontend/src/pages/log-in.ts @@ -398,9 +398,13 @@ export class LogInPage extends BtrixElement { this.orgSlugState && data.user.orgs.some((org) => org.slug === this.orgSlugState) ? this.orgSlugState - : data.user.orgs[0].slug; + : data.user.orgs.length + ? data.user.orgs[0].slug + : ""; - AppStateService.updateUser(formatAPIUser(data.user), slug); + if (slug) { + AppStateService.updateUser(formatAPIUser(data.user), slug); + } await this.updateComplete;