feat: Sprint 2 — WordPress plugin (auth/api/site-info/admin) + web SiteConnector
Mirror to GitHub / mirror (push) Canceled after 0s
Mirror to GitHub / mirror (push) Canceled after 0s
- plugin: Wursor_Auth (token hashing, HMAC, scoped tokens), Wursor_API (REST + auth), Wursor_Site_Info (builder/capabilities/preflight), Wursor_Admin - plugin: test-auth.php (10 auth tests, run in a WP+PHP env) - web: SiteConnector pairing UI (code + poll + states) - api: PluginClient signs full REST route (matches WP get_route) - 92 unit tests green
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { SiteConnector } from '../src/components/SiteConnector.tsx';
|
||||
|
||||
describe('SiteConnector', () => {
|
||||
it('shows the pairing code', () => {
|
||||
render(<SiteConnector code="ABCD1234" checkConnected={vi.fn().mockResolvedValue({ connected: false })} />);
|
||||
expect(screen.getByText('ABCD1234')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows success state when connected', async () => {
|
||||
render(<SiteConnector code="ABCD1234" checkConnected={vi.fn().mockResolvedValue({ connected: true })} />);
|
||||
expect(await screen.findByText('Site connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error state when the check fails', async () => {
|
||||
render(<SiteConnector code="ABCD1234" checkConnected={vi.fn().mockRejectedValue(new Error('nope'))} />);
|
||||
expect(await screen.findByText('Connection failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('polls until the site is connected', async () => {
|
||||
const checkConnected = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ connected: false })
|
||||
.mockResolvedValueOnce({ connected: true });
|
||||
render(<SiteConnector code="ABCD1234" checkConnected={checkConnected} pollIntervalMs={5} />);
|
||||
|
||||
expect(await screen.findByText('Site connected')).toBeInTheDocument();
|
||||
expect(checkConnected).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type SiteConnectorProps = {
|
||||
code: string;
|
||||
checkConnected: () => Promise<{ connected: boolean }>;
|
||||
pollIntervalMs?: number;
|
||||
};
|
||||
|
||||
type State = 'pending' | 'connected' | 'error';
|
||||
|
||||
export function SiteConnector({ code, checkConnected, pollIntervalMs = 2000 }: SiteConnectorProps) {
|
||||
const [state, setState] = useState<State>('pending');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const { connected } = await checkConnected();
|
||||
if (cancelled) return;
|
||||
if (connected) {
|
||||
setState('connected');
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(poll, pollIntervalMs);
|
||||
} catch {
|
||||
if (!cancelled) setState('error');
|
||||
}
|
||||
}
|
||||
|
||||
void poll();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
};
|
||||
}, [checkConnected, pollIntervalMs]);
|
||||
|
||||
if (state === 'connected') {
|
||||
return <div className="wursor-connected">Site connected</div>;
|
||||
}
|
||||
if (state === 'error') {
|
||||
return <div className="wursor-error">Connection failed</div>;
|
||||
}
|
||||
return (
|
||||
<div className="wursor-pairing">
|
||||
<p>Enter this code in your Wursor plugin:</p>
|
||||
<code className="wursor-pairing-code">{code}</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user