feat(federation): account-settings notice for detached accounts (detach spec §4.7)
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import type { User } from '@backspace/shared';
|
||||
|
||||
// ── Store mocks ─────────────────────────────────────────────────────────────
|
||||
// AccountPanel reads its self user from `useAuthStore((s) => s.user)`. We drive
|
||||
// that user through a mutable fixture and mock the store with a selector-aware
|
||||
// callable (mirrors the selector-mock idiom used across the web test suite).
|
||||
let currentUser: User | null = null;
|
||||
const noop = vi.fn();
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
updateProfile: (...args: unknown[]) => unknown;
|
||||
changePassword: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
vi.mock('../../../stores/authStore', () => ({
|
||||
useAuthStore: Object.assign(
|
||||
(selector: (s: AuthState) => unknown) =>
|
||||
selector({ user: currentUser, updateProfile: noop, changePassword: noop }),
|
||||
{
|
||||
getState: (): AuthState => ({ user: currentUser, updateProfile: noop, changePassword: noop }),
|
||||
setState: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/uiStore', () => ({
|
||||
useUIStore: Object.assign(
|
||||
(selector: (s: { addToast: (...args: unknown[]) => void }) => unknown) =>
|
||||
selector({ addToast: noop }),
|
||||
{ getState: () => ({ addToast: noop }), setState: vi.fn(), subscribe: vi.fn() },
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/instanceStore', () => ({
|
||||
useInstanceStore: Object.assign(
|
||||
(selector: (s: { instances: unknown[] }) => unknown) => selector({ instances: [] }),
|
||||
{ getState: () => ({ instances: [] }), setState: vi.fn(), subscribe: vi.fn() },
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/transferStore', () => ({
|
||||
useTransferStore: Object.assign(
|
||||
(selector: (s: unknown) => unknown) => selector({}),
|
||||
{ getState: () => ({ startUpload: noop, transfers: new Map() }), setState: vi.fn(), subscribe: vi.fn() },
|
||||
),
|
||||
}));
|
||||
|
||||
// api.uploads.url is referenced during render for avatar/banner sources.
|
||||
vi.mock('../../../api/client', () => ({
|
||||
api: { uploads: { url: (f: string) => `/api/uploads/${f}` } },
|
||||
}));
|
||||
|
||||
// Child modals are closed in these render cases; stub them so their transitive
|
||||
// store imports don't participate in this isolated component render.
|
||||
vi.mock('../../ui/ImageCropModal', () => ({ ImageCropModal: () => null }));
|
||||
vi.mock('../DeleteAccountModal', () => ({ DeleteAccountModal: () => null }));
|
||||
|
||||
import { AccountPanel } from './AccountPanel';
|
||||
|
||||
// ── Fixtures ────────────────────────────────────────────────────────────────
|
||||
function makeUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
id: 'user-self',
|
||||
username: 'me',
|
||||
displayName: 'Me',
|
||||
avatar: null,
|
||||
banner: null,
|
||||
accentColor: null,
|
||||
avatarColor: null,
|
||||
bio: null,
|
||||
status: 'online',
|
||||
customStatus: null,
|
||||
isAdmin: false,
|
||||
createdAt: 0,
|
||||
homeInstance: null,
|
||||
homeUserId: null,
|
||||
replicatedInstances: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const NOTICE = /This account is detached from its home instance\./i;
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
currentUser = null;
|
||||
});
|
||||
|
||||
describe('AccountPanel detached-account notice', () => {
|
||||
it('renders the notice when the account is detached and carries a home instance', () => {
|
||||
currentUser = makeUser({ federationHomeOrphaned: true, homeInstance: 'old.example.net' });
|
||||
render(<AccountPanel />);
|
||||
expect(screen.getByText(NOTICE)).toBeInTheDocument();
|
||||
// Names the lost home instance so the owner understands what happened.
|
||||
expect(screen.getByText(/old\.example\.net/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the notice for a non-detached federated account', () => {
|
||||
currentUser = makeUser({ federationHomeOrphaned: false, homeInstance: 'live.example.net' });
|
||||
render(<AccountPanel />);
|
||||
expect(screen.queryByText(NOTICE)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the notice for a normal local account (no home instance)', () => {
|
||||
currentUser = makeUser({ federationHomeOrphaned: true, homeInstance: null });
|
||||
render(<AccountPanel />);
|
||||
expect(screen.queryByText(NOTICE)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -264,6 +264,13 @@ export function AccountPanel() {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h2 className="text-lg font-semibold text-txt-primary mb-6">My Account</h2>
|
||||
{user?.federationHomeOrphaned && user?.homeInstance && (
|
||||
<div className="rounded-lg bg-accent-amber/10 border border-accent-amber/25 px-3.5 py-3 text-xs text-txt-secondary leading-relaxed mb-4">
|
||||
<span className="font-medium text-txt-primary">This account is detached from its home instance.</span>{' '}
|
||||
{user.homeInstance} was reset or is no longer available, so this account now operates locally on
|
||||
this instance — your profile and password are managed here.
|
||||
</div>
|
||||
)}
|
||||
{/* ── Profile Customization ── */}
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">
|
||||
|
||||
Reference in New Issue
Block a user