feat: replace browser login with imported Reddit sessions
This commit is contained in:
1 parent
2b0545afcf
commit
c483c7b8be
76 files changed
+2048
-2828
No files matched your search
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createDashboardRequest } from '../../apps/dashboard/src/api/request.js';
|
||||
|
||||
describe('dashboard API requests', () => {
|
||||
it('redirects to sign-in when the session is no longer authorized', async () => {
|
||||
const fetchImplementation = vi.fn<typeof fetch>().mockResolvedValue(new Response(
|
||||
JSON.stringify({ error: 'Dashboard authentication required' }),
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
||||
));
|
||||
const redirectToLogin = vi.fn();
|
||||
const request = createDashboardRequest(fetchImplementation, redirectToLogin);
|
||||
|
||||
const response = await request('/api/stats');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(redirectToLogin).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -200,6 +200,15 @@ describe('dashboard foundation', () => {
|
||||
expect(timerRequests).toBe(2);
|
||||
});
|
||||
|
||||
it('styles the recent-messages retry control when its request fails', async () => {
|
||||
installSuccessfulApi();
|
||||
server.use(http.get('/api/messages', () => HttpResponse.json({ error: 'Messages unavailable' }, { status: 503 })));
|
||||
renderDashboard();
|
||||
|
||||
const retryButton = await screen.findByRole('button', { name: 'Retry messages' });
|
||||
expect(retryButton).toHaveClass('button', 'secondary');
|
||||
});
|
||||
|
||||
it('shows refresh progress while preserving the current overview', async () => {
|
||||
installSuccessfulApi();
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -127,6 +127,9 @@ describe('OperationsPage', () => {
|
||||
expect(await within(activityRegion).findByText('Reply from u/alex')).toBeInTheDocument();
|
||||
const jobsRegion = screen.getByRole('region', { name: 'Sync jobs' });
|
||||
expect(await within(jobsRegion).findByText('4 of 10')).toBeInTheDocument();
|
||||
for (const name of ['Sync latest chats', 'Full cache sync', 'Fetch all chats']) {
|
||||
expect(within(jobsRegion).getByRole('button', { name })).toHaveClass('button', 'secondary');
|
||||
}
|
||||
expect(screen.getByRole('status', { name: 'Realtime connection' })).toHaveTextContent('State may be stale');
|
||||
|
||||
const consoleRegion = screen.getByRole('region', { name: 'Live console' });
|
||||
|
||||
@@ -246,46 +246,49 @@ describe('settings and access', () => {
|
||||
expect(updateRequests).toBe(0);
|
||||
});
|
||||
|
||||
it('shows Reddit session and noVNC state without exposing credentials, then starts and rechecks login', async () => {
|
||||
it('waits for and imports an extension dump without exposing credentials', async () => {
|
||||
let statusRequests = 0;
|
||||
let loginRequests = 0;
|
||||
let importRequests = 0;
|
||||
server.use(
|
||||
http.get('/api/config', () => HttpResponse.json(config)),
|
||||
http.get('/api/login/info', ({ request }) => {
|
||||
expect(request.credentials).toBe('same-origin');
|
||||
return HttpResponse.json({
|
||||
vncEnabled: true,
|
||||
novncPort: 6080,
|
||||
publicUrl: 'https://login.example/vnc.html',
|
||||
passwordRequired: true,
|
||||
});
|
||||
}),
|
||||
http.get('/api/login/status', () => {
|
||||
statusRequests += 1;
|
||||
return HttpResponse.json(statusRequests === 1
|
||||
? { ready: true, loggedIn: false, status: 'login-required' }
|
||||
: { ready: true, loggedIn: true, status: 'authenticated' });
|
||||
: { ready: true, loggedIn: true, status: 'authenticated', username: 'example-account' });
|
||||
}),
|
||||
http.post('/api/login/start', ({ request }) => {
|
||||
http.post('/api/login/session-import', async ({ request }) => {
|
||||
expect(request.credentials).toBe('same-origin');
|
||||
loginRequests += 1;
|
||||
return HttpResponse.json({ ok: true });
|
||||
const body = await request.json() as { cookies: unknown[]; matrix: { accessToken: string } };
|
||||
expect(body.cookies).toHaveLength(1);
|
||||
expect(body.matrix.accessToken).toBe('matrix-secret');
|
||||
importRequests += 1;
|
||||
return HttpResponse.json({ ok: true, status: 'authenticated' });
|
||||
}),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderSettings('/settings?section=access');
|
||||
|
||||
expect(await screen.findByText('Login required')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Open noVNC login window' })).toHaveAttribute('href', 'https://login.example/vnc.html');
|
||||
expect(screen.getByText('A VNC password is required.')).toBeInTheDocument();
|
||||
expect(screen.getByText(/waiting for a session dump/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/noVNC/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox', { name: /password/i })).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open Reddit login' }));
|
||||
expect(await screen.findByText('Login page opened.')).toBeInTheDocument();
|
||||
expect(loginRequests).toBe(1);
|
||||
const dump = new File([JSON.stringify({
|
||||
cookies: [{ name: 'reddit_session', value: 'secret', domain: '.reddit.com', path: '/' }],
|
||||
matrix: {
|
||||
homeserver: 'https://matrix.redditspace.com', accessToken: 'matrix-secret',
|
||||
userId: '@t2_bot:reddit.com', capturedAt: 1_700_000_000_000,
|
||||
},
|
||||
})], 'redditbot-session.json', { type: 'application/json' });
|
||||
await user.upload(screen.getByLabelText('Reddit session export'), dump);
|
||||
|
||||
expect(await screen.findByText('Reddit session imported and verified.')).toBeInTheDocument();
|
||||
expect(importRequests).toBe(1);
|
||||
expect(await screen.findByText('Authenticated')).toBeInTheDocument();
|
||||
expect(screen.getByText(/u\/example-account/)).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Recheck session' }));
|
||||
expect(statusRequests).toBeGreaterThanOrEqual(3);
|
||||
expect(statusRequests).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import dashboardViteConfig from '../../apps/dashboard/vite.config.js';
|
||||
|
||||
describe('dashboard development proxy', () => {
|
||||
it('proxies private media requests to the backend', () => {
|
||||
expect(dashboardViteConfig.server?.proxy).toMatchObject({
|
||||
'/media': 'http://localhost:3000',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user