login: don't set default slug if user not part of any orgs #2491 (#2492)

if logged in user is not part of any orgs, still allow logging in,
instead of throwing an exception due to accessing non-existent org

---------

Co-authored-by: sua yoo <sua@suayoo.com>
This commit is contained in:
Ilya Kreymer 2025-03-19 15:23:16 -07:00 committed by GitHub
parent 0bc210d905
commit c9c32d86e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 182 additions and 3 deletions

View File

@ -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<LoggedInEventDetail>("btrix-logged-in", {
detail: {
...mockAuth,
redirectUrl: "/fake-page",
},
});
const el = await fixture<App>("<browsertrix-app></browsertrix-app>");
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<LoggedInEventDetail>("btrix-logged-in", {
detail: {
...mockAuth,
},
});
const el = await fixture<App>("<browsertrix-app></browsertrix-app>");
el.onLoggedIn(event);
expect(el.routeTo).to.have.been.calledWith("/account/settings");
});
});
});
});

View File

@ -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("<btrix-log-in>", () => {
beforeEach(() => {
AppStateService.resetAll();
stub(window.history, "pushState");
});
afterEach(() => {
restore();
});
it("is defined", async () => {
const el = await fixture<LogInPage>(
html`<btrix-log-in .viewState=${viewState}></btrix-log-in>`,
);
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<LogInPage>(
html`<btrix-log-in .viewState=${viewState}></btrix-log-in>`,
);
const form = el.shadowRoot!.querySelector<HTMLFormElement>("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<LogInPage>(
html`<btrix-log-in .viewState=${viewState}></btrix-log-in>`,
);
const form = el.shadowRoot!.querySelector<HTMLFormElement>("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<LogInPage>(
html`<btrix-log-in .viewState=${viewState}></btrix-log-in>`,
);
const form = el.shadowRoot!.querySelector<HTMLFormElement>("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;
});
});
});

View File

@ -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;