Be much, much clearer about room server ops. Closes #78.

This commit is contained in:
Jack Kingsman
2026-03-27 12:58:00 -07:00
parent 6e5256acce
commit 7151cf3846
12 changed files with 410 additions and 113 deletions
@@ -11,6 +11,7 @@ const mockHook: {
loggedIn: false,
loginLoading: false,
loginError: null,
lastLoginAttempt: null,
paneData: {
status: null,
nodeInfo: null,
+66 -4
View File
@@ -56,22 +56,84 @@ describe('RoomServerPanel', () => {
status: 'timeout',
authenticated: false,
message:
'No login confirmation was heard from the room server. The control panel is still available; try logging in again if authenticated actions fail.',
"No login confirmation was heard from the room server. You're free to try sending messages; try logging in again if authenticated actions fail.",
});
const onAuthenticatedChange = vi.fn();
render(<RoomServerPanel contact={roomContact} onAuthenticatedChange={onAuthenticatedChange} />);
fireEvent.click(screen.getByText('Login with ACL / Guest'));
fireEvent.click(screen.getByText('Login with Existing Access / Guest'));
await waitFor(() => {
expect(screen.getByText('Show Tools')).toBeInTheDocument();
});
expect(screen.getByText('Show Tools')).toBeInTheDocument();
expect(mockToast.warning).toHaveBeenCalledWith('Room login not confirmed', {
expect(screen.getByText('Retry Existing-Access Login')).toBeInTheDocument();
expect(mockToast.warning).toHaveBeenCalledWith("Couldn't confirm room login", {
description:
'No login confirmation was heard from the room server. The control panel is still available; try logging in again if authenticated actions fail.',
"No login confirmation was heard from the room server. You're free to try sending messages; try logging in again if authenticated actions fail.",
});
expect(onAuthenticatedChange).toHaveBeenLastCalledWith(true);
});
it('retains the last password for one-click retry after unlocking the panel', async () => {
mockApi.roomLogin
.mockResolvedValueOnce({
status: 'timeout',
authenticated: false,
message: 'No reply heard',
})
.mockResolvedValueOnce({
status: 'ok',
authenticated: true,
message: null,
});
render(<RoomServerPanel contact={roomContact} />);
fireEvent.change(screen.getByLabelText('Repeater password'), {
target: { value: 'secret-room-password' },
});
fireEvent.click(screen.getByText('Login with Password'));
await waitFor(() => {
expect(screen.getByText('Retry Password Login')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Retry Password Login'));
await waitFor(() => {
expect(mockApi.roomLogin).toHaveBeenNthCalledWith(
1,
roomContact.public_key,
'secret-room-password'
);
expect(mockApi.roomLogin).toHaveBeenNthCalledWith(
2,
roomContact.public_key,
'secret-room-password'
);
});
});
it('shows only a success toast after a confirmed login', async () => {
mockApi.roomLogin.mockResolvedValueOnce({
status: 'ok',
authenticated: true,
message: null,
});
render(<RoomServerPanel contact={roomContact} />);
fireEvent.click(screen.getByText('Login with Existing Access / Guest'));
await waitFor(() => {
expect(screen.getByText('Show Tools')).toBeInTheDocument();
});
expect(screen.queryByText('Login confirmed by the room server.')).not.toBeInTheDocument();
expect(screen.queryByText('Retry Password Login')).not.toBeInTheDocument();
expect(screen.queryByText('Retry Existing-Access Login')).not.toBeInTheDocument();
expect(mockToast.success).toHaveBeenCalledWith('Login confirmed by the room server.');
});
});
@@ -8,70 +8,24 @@ describe('useRememberedServerPassword', () => {
localStorage.clear();
});
it('loads remembered passwords from localStorage', () => {
localStorage.setItem(
'remoteterm-server-password:repeater:abc123',
JSON.stringify({ password: 'stored-secret' })
it('restores the last in-memory password when local remember is disabled', () => {
const { result, unmount } = renderHook(() =>
useRememberedServerPassword('room', 'aa'.repeat(32))
);
const { result } = renderHook(() => useRememberedServerPassword('repeater', 'abc123'));
expect(result.current.password).toBe('stored-secret');
expect(result.current.rememberPassword).toBe(true);
});
it('stores passwords after login when remember is enabled', () => {
const { result } = renderHook(() => useRememberedServerPassword('room', 'room-key'));
act(() => {
result.current.setRememberPassword(true);
result.current.setPassword('room-secret');
result.current.persistAfterLogin('room-secret');
});
act(() => {
result.current.persistAfterLogin(' hello ');
});
expect(result.current.password).toBe('room-secret');
unmount();
expect(localStorage.getItem('remoteterm-server-password:room:room-key')).toBe(
JSON.stringify({ password: 'hello' })
);
expect(result.current.password).toBe('hello');
});
it('clears stored passwords when login is done with remember disabled', () => {
localStorage.setItem(
'remoteterm-server-password:repeater:abc123',
JSON.stringify({ password: 'stored-secret' })
const { result: remounted } = renderHook(() =>
useRememberedServerPassword('room', 'aa'.repeat(32))
);
const { result } = renderHook(() => useRememberedServerPassword('repeater', 'abc123'));
act(() => {
result.current.setRememberPassword(false);
});
act(() => {
result.current.persistAfterLogin('new-secret');
});
expect(localStorage.getItem('remoteterm-server-password:repeater:abc123')).toBeNull();
expect(result.current.password).toBe('');
});
it('preserves remembered passwords on guest login when remember stays enabled', () => {
localStorage.setItem(
'remoteterm-server-password:room:room-key',
JSON.stringify({ password: 'stored-secret' })
);
const { result } = renderHook(() => useRememberedServerPassword('room', 'room-key'));
act(() => {
result.current.persistAfterLogin('');
});
expect(localStorage.getItem('remoteterm-server-password:room:room-key')).toBe(
JSON.stringify({ password: 'stored-secret' })
);
expect(result.current.password).toBe('stored-secret');
expect(remounted.current.password).toBe('room-secret');
expect(remounted.current.rememberPassword).toBe(false);
});
});
@@ -74,6 +74,8 @@ describe('useRepeaterDashboard', () => {
expect(result.current.loggedIn).toBe(true);
expect(result.current.loginError).toBe(null);
expect(result.current.lastLoginAttempt?.heardBack).toBe(true);
expect(result.current.lastLoginAttempt?.outcome).toBe('confirmed');
expect(mockApi.repeaterLogin).toHaveBeenCalledWith(REPEATER_KEY, 'secret');
});
@@ -92,6 +94,8 @@ describe('useRepeaterDashboard', () => {
expect(result.current.loggedIn).toBe(true);
expect(result.current.loginError).toBe('Auth failed');
expect(result.current.lastLoginAttempt?.heardBack).toBe(true);
expect(result.current.lastLoginAttempt?.outcome).toBe('not_confirmed');
expect(mockToast.error).toHaveBeenCalledWith('Login not confirmed', {
description: 'Auth failed',
});
@@ -125,6 +129,8 @@ describe('useRepeaterDashboard', () => {
expect(result.current.loggedIn).toBe(true);
expect(result.current.loginError).toBe('Network error');
expect(result.current.lastLoginAttempt?.heardBack).toBe(false);
expect(result.current.lastLoginAttempt?.outcome).toBe('request_failed');
expect(mockToast.error).toHaveBeenCalledWith('Login request failed', {
description:
'Network error. The dashboard is still available, but repeater operations may fail until a login succeeds.',