Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-corners-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": patch
---

Fix PAM users being unable to log in to the UI
8 changes: 4 additions & 4 deletions e2e/features/auth/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import { basePage } from "../../support/pages/basePage";

export class LoginPage extends basePage {
readonly page: Page;
readonly emailInput: Locator;
readonly identifierInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;

constructor(page: Page) {
super(page);
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.identifierInput = page.locator('input[name="identifier"]');
this.passwordInput = page.locator('input[name="password"]');
this.loginButton = page.locator("[type=submit]", { hasText: "Sign in" });
}

async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
async login(identifier: string, password: string): Promise<void> {
await this.identifierInput.fill(identifier);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
Expand Down
4 changes: 2 additions & 2 deletions e2e/support/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { navigateTo } from "./navigation";

export async function login(
page: Page,
email: string,
identifier: string,
password: string,
params?: Record<string, string>,
): Promise<void> {
await navigateTo(page, "/login", params);

await page.fill('input[name="email"]', email);
await page.fill('input[name="identifier"]', identifier);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');
}
7 changes: 3 additions & 4 deletions src/features/auth/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ export type AuthStateResponse =
invitation_id: string | null;
});

export interface LoginRequestParams {
email: string;
password: string;
}
export type LoginRequestParams =
| { email: string; password: string }
| { identity: string; password: string };

export interface GetUbuntuOneUrlParams {
external?: boolean;
Expand Down
37 changes: 35 additions & 2 deletions src/features/auth/components/LoginForm/LoginForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("LoginForm", () => {
renderWithProviders(<Component isIdentityAvailable={false} />);

await userEvent.type(
screen.getByTestId("email"),
screen.getByTestId("identifier"),
taskId > 0 ? user.email : user.email.slice(1),
);

Expand Down Expand Up @@ -95,6 +95,39 @@ describe("LoginForm", () => {
});
});

describe("with PAM auth (isIdentityAvailable)", () => {
beforeEach(async () => {
vi.doUnmock("react-router");
vi.doUnmock("@/hooks/useAuth");
vi.resetModules();
vi.clearAllMocks();

loginSpy.mockResolvedValue({ data: authUser });

mockTestParams();

const { default: Component } = await import("./LoginForm");

renderWithProviders(<Component isIdentityAvailable={true} />);

await userEvent.type(screen.getByTestId("identifier"), "john");
await userEvent.type(screen.getByTestId("password"), user.password);
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
Comment on lines +113 to +115
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Most tests in the repo use a userEvent.setup() instance rather than calling userEvent.type/click directly (e.g. src/features/auth/components/consent-banner/ConsentBannerModal.test.tsx:9). For consistency and to avoid subtle async/flakiness issues with the global userEvent, consider switching this suite to const user = userEvent.setup() and using that instance for interactions.

Copilot uses AI. Check for mistakes.
});

it("should sign in with identity field instead of email", async () => {
expect(loginSpy).toHaveBeenCalledWith({
identity: "john",
password: user.password,
});
expect(setUser).toHaveBeenCalledWith(authUser);
expect(safeRedirect).toHaveBeenCalledWith(HOMEPAGE_PATH, {
external: false,
replace: true,
});
});
});

describe("with additional test params", () => {
const testSearchParams = [
{ "redirect-to": "/dashboard" },
Expand All @@ -121,7 +154,7 @@ describe("LoginForm", () => {

renderWithProviders(<Component isIdentityAvailable={false} />);

await userEvent.type(screen.getByTestId("email"), user.email);
await userEvent.type(screen.getByTestId("identifier"), user.email);

await userEvent.type(screen.getByTestId("password"), user.password);

Expand Down
23 changes: 12 additions & 11 deletions src/features/auth/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import classes from "./LoginForm.module.scss";
import { getFormikError } from "@/utils/formikErrors";

interface FormProps {
email: string;
identifier: string;
password: string;
}

Expand All @@ -28,7 +28,7 @@ const LoginForm: FC<LoginFormProps> = ({ isIdentityAvailable }) => {
const [searchParams] = useSearchParams();

const debug = useDebug();
const { login: signInWithEmailAndPassword, isLoggingIn } = useLogin();
const { login, isLoggingIn } = useLogin();

const { safeRedirect, setUser } = useAuth();

Expand All @@ -37,11 +37,11 @@ const LoginForm: FC<LoginFormProps> = ({ isIdentityAvailable }) => {

const formik = useFormik<FormProps>({
initialValues: {
email: "",
identifier: "",
password: "",
},
validationSchema: Yup.object().shape({
email: isIdentityAvailable
identifier: isIdentityAvailable
? Yup.string().required("This field is required")
: Yup.string()
.required("This field is required")
Expand Down Expand Up @@ -83,10 +83,11 @@ const LoginForm: FC<LoginFormProps> = ({ isIdentityAvailable }) => {
}),
onSubmit: async (values) => {
try {
const { data } = await signInWithEmailAndPassword({
email: values.email,
password: values.password,
});
const { identifier, password } = values;
const credentials = isIdentityAvailable
? { identity: identifier, password }
: { email: identifier, password };
const { data } = await login(credentials);
Comment thread
rubinaga marked this conversation as resolved.

if ("current_account" in data) {
setUser(data);
Expand All @@ -107,9 +108,9 @@ const LoginForm: FC<LoginFormProps> = ({ isIdentityAvailable }) => {
<Input
type="text"
label={isIdentityAvailable ? "Identity" : "Email"}
error={getFormikError(formik, "email")}
{...formik.getFieldProps("email")}
data-testid="email"
error={getFormikError(formik, "identifier")}
{...formik.getFieldProps("identifier")}
data-testid="identifier"
/>

<PasswordToggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ describe("LoginMethodsLayout", () => {
renderWithProviders(<LoginMethods methods={methods} />);

const inputLabel = screen.getByText(/identity/i);
const emailInput = screen.getByRole("textbox", { name: /identity/i });
const identityInput = screen.getByRole("textbox", { name: /identity/i });

expect(inputLabel).toBeInTheDocument();
expect(emailInput).toBeInTheDocument();
expect(identityInput).toBeInTheDocument();

await userEvent.type(emailInput, "testinputvalue");
await userEvent.type(identityInput, "testinputvalue");

await waitFor(() => {
emailInput.blur();
identityInput.blur();
});

expect(
Expand Down
Loading