Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ the interface unless an available tool actually supports that action.
unavailable until you sign in. Sign-in, sign-out and when the current
sign-in ends also live under
**Settings → Buzz → Identity → Organization sign-in**.
- When a community requires organization sign-in, the sidebar or join dialog
explains why and offers **Sign in with** the organization’s name or server
address. Complete sign-in to reconnect; in the join dialog, choose **Continue**.
- Signed in to more than one organization server? **Organization sign-in**
lists each one you're signed in to, with the communities it's used by, and
each **Sign out** ends only that server's sign-in. Your organization's own
Expand Down
21 changes: 16 additions & 5 deletions src/app/shell/PrimarySidebar.sign-in.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,18 @@ it("offers explicit sign-in when the live socket refuses a protected community",
const h = await refusedLive("Federated sign-in is required");
// The cached channel list stays; nothing signs in on its own.
expect(screen.getByRole("button", { name: "Chats" })).toBeVisible();
expect(
await screen.findByText(
"This community requires builderlab.example sign-in.",
),
).toBeVisible();
expect(h.identity.service.signIn).not.toHaveBeenCalled();
expect(h.connects()).toBe(1);
await userEvent
.setup()
.click(await screen.findByRole("button", { name: "Sign in" }));
await userEvent.setup().click(
await screen.findByRole("button", {
name: "Sign in with builderlab.example",
}),
);
expect(h.identity.service.signIn).toHaveBeenCalledOnce();
// A completed sign-in reconnects the live socket with the new session.
await waitFor(() => expect(h.connects()).toBe(2));
Expand All @@ -76,7 +83,9 @@ it.each([
])("shows %s recovery from the live socket", async (refusal, text) => {
await refusedLive(refusal);
expect(await screen.findByRole("alert")).toHaveTextContent(text);
expect(screen.queryByRole("button", { name: "Sign in" })).toBeNull();
expect(
screen.queryByRole("button", { name: "Sign in with builderlab.example" }),
).toBeNull();
});

it.each(["adapter-refused", "grant-refused", "invalid-response"])(
Expand All @@ -87,7 +96,9 @@ it.each(["adapter-refused", "grant-refused", "invalid-response"])(
"Restart Buzz to try again",
);
expect(screen.getByRole("button", { name: "Chats" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Sign in" })).toBeNull();
expect(
screen.queryByRole("button", { name: "Sign in with builderlab.example" }),
).toBeNull();
expect(h.connects()).toBe(1);
},
);
19 changes: 16 additions & 3 deletions src/features/communities/CommunityDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,22 @@ it("offers federated sign-in when joining refuses for it, then lets Continue run
"wss://relay.example",
);
await user.click(screen.getByRole("button", { name: "Continue" }));
const signIn = await screen.findByRole("button", { name: "Sign in" });
expect(
await screen.findByText(
"This community requires builderlab.example sign-in.",
),
).toBeVisible();
const signIn = await screen.findByRole("button", {
name: "Sign in with builderlab.example",
});
refuse = false;
const requests = vi.mocked(fetch).mock.calls.length;
await user.click(signIn);
expect(h.service.signIn).toHaveBeenCalledOnce();
await waitFor(() =>
expect(screen.queryByRole("button", { name: "Sign in" })).toBeNull(),
expect(
screen.queryByRole("button", { name: "Sign in with builderlab.example" }),
).toBeNull(),
);
// Completing login replays nothing captured before it.
expect(vi.mocked(fetch).mock.calls.length).toBe(requests);
Expand Down Expand Up @@ -748,7 +757,11 @@ it.each(["edit", "close"])(
const field = screen.getByLabelText("Community URL");
await user.type(field, "wss://old.example");
await user.click(screen.getByRole("button", { name: "Continue" }));
await user.click(await screen.findByRole("button", { name: "Sign in" }));
await user.click(
await screen.findByRole("button", {
name: "Sign in with builderlab.example",
}),
);
if (change === "close") view.unmount();
else {
await user.clear(field);
Expand Down
42 changes: 26 additions & 16 deletions src/features/federated-identity/CommunitySignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,32 @@ it.each(["storage-unavailable", "storage-denied"])(
expect(h.service.signIn).not.toHaveBeenCalled();
},
);
it("requires explicit sign-in before retrying protected community admission", async () => {
const h = federatedFixture();
const retry = vi.fn();
render(
<CommunitySignIn
identity={h.service}
error="Federated sign-in is required"
retry={retry}
/>,
);
expect(h.service.signIn).not.toHaveBeenCalled();
await userEvent
.setup()
.click(screen.getByRole("button", { name: "Sign in" }));
expect(retry).toHaveBeenCalledOnce();
});
it.each([null, "Builderlab"])(
"names server %s and requires explicit sign-in before retrying admission",
async (organization) => {
const h = federatedFixture();
const authority = h.service.snapshot().authority;
if (!authority) throw new Error("Fixture authority missing");
h.publish({ authority: { ...authority, organization } });
const server = organization ?? "builderlab.example";
const retry = vi.fn();
render(
<CommunitySignIn
identity={h.service}
error="Federated sign-in is required"
retry={retry}
/>,
);
expect(
screen.getByText(`This community requires ${server} sign-in.`),
).toBeVisible();
expect(h.service.signIn).not.toHaveBeenCalled();
await userEvent
.setup()
.click(screen.getByRole("button", { name: `Sign in with ${server}` }));
expect(retry).toHaveBeenCalledOnce();
},
);
it.each(["binding-mismatch", "authorization-denied"])(
"explains %s without a session to switch",
(failure) => {
Expand Down
6 changes: 5 additions & 1 deletion src/features/federated-identity/CommunitySignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ function Action({
)
return null;
if (state.status === "signed-in") return null;
const server = serverName(state.authority);
return (
<>
<p className="text-body-sm">This community requires {server} sign-in.</p>
<Button
onClick={() => {
setFailure("");
Expand All @@ -147,7 +149,9 @@ function Action({
);
}}
>
{state.status === "signing-in" ? "Cancel sign-in" : "Sign in"}
{state.status === "signing-in"
? "Cancel sign-in"
: `Sign in with ${server}`}
</Button>
{(failure || state.failure) && (
<p role="alert" className="text-body-sm text-danger">
Expand Down
Loading