1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2026-01-03 17:02:28 +03:00

Rename 'Emails' route to 'Profile' (#1567)

* rename /account/emails route to profile

* remove support for /emails

* bad unit test for Layout

* update snapshots, fix layout test

* fix snapshots from old version of compound

* better layout test

* coverage?
This commit is contained in:
Kerry
2023-08-25 17:10:34 +12:00
committed by GitHub
parent 70e6489f17
commit c7311eea79
15 changed files with 382 additions and 23 deletions

View File

@@ -0,0 +1,78 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { describe, it, expect } from "vitest";
import { segmentsToRoute } from "./Router";
describe("Router", () => {
describe("segmentsToRoute", () => {
it("returns home for route with no segments", () => {
const segments: string[] = [];
expect(segmentsToRoute(segments)).toEqual({ type: "home" });
});
it("returns home for route with and empty string segment", () => {
const segments: string[] = [""];
expect(segmentsToRoute(segments)).toEqual({ type: "home" });
});
it("returns profile for route with profile", () => {
const segments: string[] = ["profile"];
expect(segmentsToRoute(segments)).toEqual({ type: "profile" });
});
it("returns browser session list for sessions", () => {
const segments: string[] = ["sessions"];
expect(segmentsToRoute(segments)).toEqual({
type: "browser-session-list",
});
});
it("returns compat session list for compat-sessions", () => {
const segments: string[] = ["compat-sessions"];
expect(segmentsToRoute(segments)).toEqual({
type: "compat-session-list",
});
});
it("returns oauth session list for oauth2-sessions", () => {
const segments: string[] = ["oauth2-sessions"];
expect(segmentsToRoute(segments)).toEqual({
type: "oauth2-session-list",
});
});
it("returns client detail route correctly", () => {
const segments: string[] = ["clients", "client-id"];
expect(segmentsToRoute(segments)).toEqual({
type: "client",
id: "client-id",
});
});
it("returns session detail route correctly", () => {
const segments: string[] = ["sessions", "session-id"];
expect(segmentsToRoute(segments)).toEqual({
type: "session",
id: "session-id",
});
});
it("returns unknown for other segments", () => {
const segments: string[] = ["just", "testing"];
expect(segmentsToRoute(segments)).toEqual({ type: "unknown", segments });
});
});
});

View File

@@ -25,7 +25,7 @@ type Location = {
}; };
type HomeRoute = { type: "home" }; type HomeRoute = { type: "home" };
type EmailListRoute = { type: "email-list" }; type ProfileRoute = { type: "profile" };
type OAuth2ClientRoute = { type: "client"; id: string }; type OAuth2ClientRoute = { type: "client"; id: string };
type OAuth2SessionList = { type: "oauth2-session-list" }; type OAuth2SessionList = { type: "oauth2-session-list" };
type BrowserSessionRoute = { type: "session"; id: string }; type BrowserSessionRoute = { type: "session"; id: string };
@@ -36,7 +36,7 @@ type UnknownRoute = { type: "unknown"; segments: string[] };
export type Route = export type Route =
| HomeRoute | HomeRoute
| EmailListRoute | ProfileRoute
| OAuth2ClientRoute | OAuth2ClientRoute
| OAuth2SessionList | OAuth2SessionList
| BrowserSessionRoute | BrowserSessionRoute
@@ -49,8 +49,8 @@ const routeToSegments = (route: Route): string[] => {
switch (route.type) { switch (route.type) {
case "home": case "home":
return []; return [];
case "email-list": case "profile":
return ["emails"]; return ["profile"];
case "verify-email": case "verify-email":
return ["emails", route.id, "verify"]; return ["emails", route.id, "verify"];
case "client": case "client":
@@ -90,7 +90,7 @@ const segmentMatches = (
return true; return true;
}; };
const segmentsToRoute = (segments: string[]): Route => { export const segmentsToRoute = (segments: string[]): Route => {
const matches = (...pattern: PatternItem[]): boolean => const matches = (...pattern: PatternItem[]): boolean =>
segmentMatches(segments, ...pattern); segmentMatches(segments, ...pattern);
@@ -99,8 +99,8 @@ const segmentsToRoute = (segments: string[]): Route => {
return { type: "home" }; return { type: "home" };
} }
if (matches("emails")) { if (matches("profile")) {
return { type: "email-list" }; return { type: "profile" };
} }
if (matches("sessions")) { if (matches("sessions")) {
@@ -169,7 +169,7 @@ export const routeAtom = atom(
); );
const Home = lazy(() => import("./pages/Home")); const Home = lazy(() => import("./pages/Home"));
const EmailList = lazy(() => import("./pages/EmailList")); const Profile = lazy(() => import("./pages/Profile"));
const OAuth2Client = lazy(() => import("./pages/OAuth2Client")); const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
const BrowserSession = lazy(() => import("./pages/BrowserSession")); const BrowserSession = lazy(() => import("./pages/BrowserSession"));
const BrowserSessionList = lazy(() => import("./pages/BrowserSessionList")); const BrowserSessionList = lazy(() => import("./pages/BrowserSessionList"));
@@ -183,8 +183,8 @@ const InnerRouter: React.FC = () => {
switch (route.type) { switch (route.type) {
case "home": case "home":
return <Home />; return <Home />;
case "email-list": case "profile":
return <EmailList />; return <Profile />;
case "oauth2-session-list": case "oauth2-session-list":
return <OAuth2SessionList />; return <OAuth2SessionList />;
case "browser-session-list": case "browser-session-list":

View File

@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Router > routes 1`] = `"Loading..."`;

View File

@@ -0,0 +1,79 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { Provider } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { Suspense } from "react";
import { describe, expect, it, vi, afterAll, beforeEach } from "vitest";
import { appConfigAtom, locationAtom } from "../Router";
import { currentUserIdAtom, GqlResult } from "../atoms";
import Layout from "./Layout";
beforeEach(async () => {
// For some reason, the locationAtom gets updated with `about:black` on render,
// so we need to set a "real" location and wait for the next tick
window.location.assign("https://example.com/");
// Wait the next tick for the location to update
await new Promise((resolve) => setTimeout(resolve, 0));
});
const HydrateLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
children,
path,
}) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[locationAtom, { pathname: path }],
]);
return <>{children}</>;
};
const WithLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
children,
path,
}) => {
return (
<Provider>
<Suspense>
<HydrateLocation path={path}>{children}</HydrateLocation>
</Suspense>
</Provider>
);
};
describe("<Layout />", () => {
beforeEach(() => {
vi.spyOn(currentUserIdAtom, "read").mockResolvedValue(
"abc123" as unknown as GqlResult<string | null>,
);
});
afterAll(() => {
vi.restoreAllMocks();
});
it("renders app navigation correctly", async () => {
const component = render(
<WithLocation path="/account">
<Layout />
</WithLocation>,
);
expect(await component.findByText("Profile")).toMatchSnapshot();
expect(await component.findByText("Home")).toMatchSnapshot();
});
});

View File

@@ -43,7 +43,7 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
<NavBar> <NavBar>
<NavItem route={{ type: "home" }}>Home</NavItem> <NavItem route={{ type: "home" }}>Home</NavItem>
<NavItem route={{ type: "email-list" }}>Emails</NavItem> <NavItem route={{ type: "profile" }}>Profile</NavItem>
</NavBar> </NavBar>
<main>{children}</main> <main>{children}</main>

View File

@@ -30,7 +30,7 @@ const meta = {
<WithHomePage> <WithHomePage>
<NavBar {...props}> <NavBar {...props}>
<NavItem route={{ type: "home" }}>Home</NavItem> <NavItem route={{ type: "home" }}>Home</NavItem>
<NavItem route={{ type: "email-list" }}>Emails</NavItem> <NavItem route={{ type: "profile" }}>Emails</NavItem>
<ExternalLink href="https://example.com">External</ExternalLink> <ExternalLink href="https://example.com">External</ExternalLink>
</NavBar> </NavBar>
</WithHomePage> </WithHomePage>

View File

@@ -53,7 +53,7 @@ export const Active: Story = {
export const Inactive: Story = { export const Inactive: Story = {
args: { args: {
route: { type: "email-list" }, route: { type: "profile" },
children: "Emails", children: "Profile",
}, },
}; };

View File

@@ -77,7 +77,7 @@ describe("NavItem", () => {
it("renders a different route", () => { it("renders a different route", () => {
const component = create( const component = create(
<WithLocation path="/"> <WithLocation path="/">
<NavItem route={{ type: "email-list" }}>Emails</NavItem> <NavItem route={{ type: "profile" }}>Emails</NavItem>
</WithLocation>, </WithLocation>,
); );
const tree = component.toJSON(); const tree = component.toJSON();

View File

@@ -36,7 +36,7 @@ exports[`NavItem > renders a different route 1`] = `
> >
<a <a
className="_navItem_8603fc" className="_navItem_8603fc"
href="/emails" href="/profile"
onClick={[Function]} onClick={[Function]}
> >
Emails Emails

View File

@@ -65,7 +65,7 @@ const UserHome: React.FC<{
{data.unverifiedEmails.totalCount > 0 && !dismiss && ( {data.unverifiedEmails.totalCount > 0 && !dismiss && (
<Alert type="critical" title="Unverified email" onClose={doDismiss}> <Alert type="critical" title="Unverified email" onClose={doDismiss}>
You have {data.unverifiedEmails.totalCount} unverified email You have {data.unverifiedEmails.totalCount} unverified email
address(es). <Link route={{ type: "email-list" }}>Check</Link> address(es). <Link route={{ type: "profile" }}>Check</Link>
</Alert> </Alert>
)} )}
@@ -78,7 +78,7 @@ const UserHome: React.FC<{
{data.confirmedEmails.totalCount > 1 && ( {data.confirmedEmails.totalCount > 1 && (
<Body> <Body>
{data.confirmedEmails.totalCount - 1} additional emails.{" "} {data.confirmedEmails.totalCount - 1} additional emails.{" "}
<Link route={{ type: "email-list" }}>View all</Link> <Link route={{ type: "profile" }}>View all</Link>
</Body> </Body>
)} )}

View File

@@ -47,7 +47,7 @@ exports[`UserHome > render a <UserHome /> with additional emails 1`] = `
additional emails. additional emails.
<a <a
href="/emails" href="/profile"
onClick={[Function]} onClick={[Function]}
> >
View all View all
@@ -128,7 +128,7 @@ exports[`UserHome > render a <UserHome /> with an unverified email 1`] = `
1 1
unverified email address(es). unverified email address(es).
<a <a
href="/emails" href="/profile"
onClick={[Function]} onClick={[Function]}
> >
Check Check

View File

@@ -127,7 +127,7 @@ const VerifyEmail: React.FC<{
e.currentTarget?.reset(); e.currentTarget?.reset();
if (result.data?.verifyEmail.status === "VERIFIED") { if (result.data?.verifyEmail.status === "VERIFIED") {
setRoute({ type: "email-list" }); setRoute({ type: "profile" });
} else { } else {
fieldRef.current?.focus(); fieldRef.current?.focus();
fieldRef.current?.select(); fieldRef.current?.select();

View File

@@ -0,0 +1,179 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<VerifyEmail /> > renders an active session 1`] = `
[
<header
className="_header_07ded5"
>
<svg
className="_icon_07ded5"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.878.878 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
fill="currentColor"
/>
</svg>
<h1
className="_title_07ded5"
>
Verify your email
</h1>
<p
className="_tagline_07ded5"
>
Enter the 6-digit code sent to
<span
className="_email_07ded5"
>
ernie@sesame.st
</span>
</p>
</header>,
<form
className="_root_xzzw3_23 _form_07ded5"
onInvalid={[Function]}
onReset={[Function]}
onSubmit={[Function]}
>
<div
className="_field_xzzw3_32"
>
<label
className="_label_xzzw3_41"
htmlFor="radix-:r0:"
onMouseDown={[Function]}
>
6-digit code
</label>
<input
className="_control_xzzw3_55"
id="radix-:r0:"
inputMode="numeric"
name="code"
onChange={[Function]}
onInvalid={[Function]}
placeholder="xxxxxx"
title=""
type="text"
/>
</div>
<button
className="_button_lls7s_17 _submitButton_07ded5 _submitButton_07ded5"
data-kind="primary"
data-size="lg"
disabled={false}
role="button"
tabIndex={0}
type="submit"
>
Continue
</button>
<button
className="_button_lls7s_17"
data-kind="tertiary"
data-size="lg"
disabled={false}
onClick={[Function]}
role="button"
tabIndex={0}
>
Resend email
</button>
</form>,
]
`;
exports[`<VerifyEmail /> > renders verify screen for email 1`] = `
[
<header
className="_header_07ded5"
>
<svg
className="_icon_07ded5"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.878.878 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
fill="currentColor"
/>
</svg>
<h1
className="_title_07ded5"
>
Verify your email
</h1>
<p
className="_tagline_07ded5"
>
Enter the 6-digit code sent to
<span
className="_email_07ded5"
>
ernie@sesame.st
</span>
</p>
</header>,
<form
className="_root_xzzw3_23 _form_07ded5"
onInvalid={[Function]}
onReset={[Function]}
onSubmit={[Function]}
>
<div
className="_field_xzzw3_32"
>
<label
className="_label_xzzw3_41"
htmlFor="radix-:r0:"
onMouseDown={[Function]}
>
6-digit code
</label>
<input
className="_control_xzzw3_55"
id="radix-:r0:"
inputMode="numeric"
name="code"
onChange={[Function]}
onInvalid={[Function]}
placeholder="xxxxxx"
title=""
type="text"
/>
</div>
<button
className="_button_lls7s_17 _submitButton_07ded5 _submitButton_07ded5"
data-kind="primary"
data-size="lg"
disabled={false}
role="button"
tabIndex={0}
type="submit"
>
Continue
</button>
<button
className="_button_lls7s_17"
data-kind="tertiary"
data-size="lg"
disabled={false}
onClick={[Function]}
role="button"
tabIndex={0}
>
Resend email
</button>
</form>,
]
`;

View File

@@ -0,0 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Layout /> > renders app navigation correctly 1`] = `
<a
class="_navItem_8603fc"
href="/profile"
>
Profile
</a>
`;
exports[`<Layout /> > renders app navigation correctly 2`] = `
<a
aria-current="page"
class="_navItem_8603fc"
href="/"
>
Home
</a>
`;

View File

@@ -20,7 +20,7 @@ import NotLoggedIn from "../components/NotLoggedIn";
import UserEmailList from "../components/UserEmailList"; import UserEmailList from "../components/UserEmailList";
import { isErr, unwrapErr, unwrapOk } from "../result"; import { isErr, unwrapErr, unwrapOk } from "../result";
const EmailList: React.FC = () => { const Profile: React.FC = () => {
const result = useAtomValue(currentUserIdAtom); const result = useAtomValue(currentUserIdAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />; if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
@@ -30,4 +30,4 @@ const EmailList: React.FC = () => {
return <UserEmailList userId={userId} />; return <UserEmailList userId={userId} />;
}; };
export default EmailList; export default Profile;