Files
bambuddy/frontend/src/__tests__/components/OrcaCloudView.test.tsx
T
maziggy 18d534c945 feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account
  (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing
  Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub /
  email+password); password defaults. Paste-flow PKCE because Orca's
  Supabase project only allowlists localhost redirect_to — open feature
  request at OrcaSlicer/OrcaSlicer#14028.

  Surfaces:
  - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same
    rich layout (search + 5 filter dropdowns + 3-column grouped grid +
    read-only detail modal)
  - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud >
    standard); separate status banner per cloud; metadata-aware pre-pick
    scores Orca filaments above local (Orca's sync_pull returns full
    content inline so filament_type / filament_colour come for free, no
    per-setting fetch rate-limit dance)
  - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed
    orca_<UUID> to match local_/builtin_); generic Bambu filament-ID
    derivation from parsed material (printer firmware can't grok Orca
    UUIDs); slot mapping persists preset_source='orca_cloud'
  - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the
    cloud preset list via Promise.allSettled (OrcaProfileMeta is
    structurally identical to SlicerSetting)

  Backend:
  - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange /
    single-use refresh rotation / get_user_info / list_profiles via the
    bare /sync/pull bootstrap path
  - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish,
    auth/password, status, logout, profiles, profiles/{id}); router-level
    _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers
    (SpoolBuddy kiosk) properly resolve their owner User; just-in-time
    refresh with atomic persist-before-API-call
  - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu
    Cloud fetcher (status vocabulary, 5min cache, permission shortcut);
    _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains
    orca_cloud + orca_cloud_status
  - services/preset_resolver.py: PresetRef.source extended with
    "orca_cloud"; _resolve_orca_cloud walks list + filters
  - 8 columns on users table for tokens (5 persistent) + transient PKCE
    handshake state with 10-min TTL (3); dialect-branched DATETIME /
    TIMESTAMP; auth-disabled mode falls back to Settings table
  - orca_cloud:auth permission folded into can_access_cloud API-key scope
    (same trust dimension)
2026-06-04 15:50:44 +02:00

212 lines
8.5 KiB
TypeScript

/**
* Tests for OrcaCloudView component — covers the four UI phases of the
* paste-based PKCE handshake: disconnected, awaiting-paste, connected,
* and disconnect.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { render } from '../utils';
import { OrcaCloudView } from '../../components/OrcaCloudView';
// JSDOM doesn't implement window.open; the connect flow opens the auth URL
// in a new tab so we stub it to capture the call.
beforeEach(() => {
vi.stubGlobal('open', vi.fn());
});
const noProfilesResponse = { profiles: [] };
describe('OrcaCloudView', () => {
it('shows all four sign-in options when not connected', async () => {
server.use(
http.get('/api/v1/orca-cloud/status', () =>
HttpResponse.json({ connected: false, email: null, user_id: null }),
),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByText(/Connect to Orca Cloud/i)).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: /Sign in with Google/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Sign in with Apple/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Sign in with GitHub/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Sign in with email and password/i })).toBeInTheDocument();
});
it('passes the selected OAuth provider to auth/start', async () => {
let receivedProvider: string | undefined;
server.use(
http.get('/api/v1/orca-cloud/status', () =>
HttpResponse.json({ connected: false, email: null, user_id: null }),
),
http.post('/api/v1/orca-cloud/auth/start', async ({ request }) => {
const body = (await request.json()) as { provider?: string };
receivedProvider = body.provider;
return HttpResponse.json({ auth_url: 'https://auth.orcaslicer.com/auth/v1/authorize?test=1' });
}),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /Sign in with Apple/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Sign in with Apple/i }));
await waitFor(() => {
expect(receivedProvider).toBe('apple');
});
expect(window.open).toHaveBeenCalledWith(
'https://auth.orcaslicer.com/auth/v1/authorize?test=1',
'_blank',
'noopener,noreferrer',
);
});
it('connects via email and password without the paste flow', async () => {
let connected = false;
let receivedCreds: { email?: string; password?: string } = {};
server.use(
http.get('/api/v1/orca-cloud/status', () =>
HttpResponse.json(
connected
? { connected: true, email: 'martin@example.com', user_id: 'u1' }
: { connected: false, email: null, user_id: null },
),
),
http.post('/api/v1/orca-cloud/auth/password', async ({ request }) => {
receivedCreds = (await request.json()) as { email?: string; password?: string };
connected = true;
return HttpResponse.json({ connected: true, email: 'martin@example.com', user_id: 'u1' });
}),
http.get('/api/v1/orca-cloud/profiles', () => HttpResponse.json(noProfilesResponse)),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /Sign in with email and password/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Sign in with email and password/i }));
// The password form replaces the provider picker.
await waitFor(() => {
expect(screen.getByLabelText(/^Email$/i)).toBeInTheDocument();
});
fireEvent.change(screen.getByLabelText(/^Email$/i), { target: { value: 'martin@example.com' } });
fireEvent.change(screen.getByLabelText(/^Password$/i), { target: { value: 'hunter2' } });
// Click the submit button inside the form (not the picker's email button).
const submitButtons = screen.getAllByRole('button', { name: /^Sign in$/i });
fireEvent.click(submitButtons[submitButtons.length - 1]);
await waitFor(() => {
expect(screen.getByText('martin@example.com')).toBeInTheDocument();
});
expect(receivedCreds).toEqual({ email: 'martin@example.com', password: 'hunter2' });
});
it('rejects a URL without a code parameter with a client-side error', async () => {
server.use(
http.get('/api/v1/orca-cloud/status', () =>
HttpResponse.json({ connected: false, email: null, user_id: null }),
),
http.post('/api/v1/orca-cloud/auth/start', () =>
HttpResponse.json({ auth_url: 'https://auth.orcaslicer.com/auth/v1/authorize?test=1' }),
),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /Sign in with Google/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Sign in with Google/i }));
await waitFor(() => {
expect(screen.getByPlaceholderText(/http:\/\/localhost:41172/i)).toBeInTheDocument();
});
const textarea = screen.getByPlaceholderText(/http:\/\/localhost:41172/i);
// Paste something with no ?code= — the client-side guard should fire
// before we hit the server.
fireEvent.change(textarea, { target: { value: 'http://localhost:41172/callback?error=denied' } });
fireEvent.click(screen.getByRole('button', { name: /Finish connecting/i }));
await waitFor(() => {
expect(screen.getByText(/does not look like an Orca Cloud callback/i)).toBeInTheDocument();
});
});
it('shows the connected state with the email after a successful paste', async () => {
// Start disconnected; after a successful finish the status query refetches
// and returns connected. MSW lets us swap handlers mid-test.
let connected = false;
server.use(
http.get('/api/v1/orca-cloud/status', () => {
return HttpResponse.json(
connected
? { connected: true, email: 'martin@example.com', user_id: 'u1' }
: { connected: false, email: null, user_id: null },
);
}),
http.post('/api/v1/orca-cloud/auth/start', () =>
HttpResponse.json({ auth_url: 'https://auth.orcaslicer.com/auth/v1/authorize?test=1' }),
),
http.post('/api/v1/orca-cloud/auth/finish', () => {
connected = true;
return HttpResponse.json({ connected: true, email: 'martin@example.com', user_id: 'u1' });
}),
http.get('/api/v1/orca-cloud/profiles', () => HttpResponse.json(noProfilesResponse)),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /Sign in with Google/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Sign in with Google/i }));
await waitFor(() => {
expect(screen.getByPlaceholderText(/http:\/\/localhost:41172/i)).toBeInTheDocument();
});
fireEvent.change(screen.getByPlaceholderText(/http:\/\/localhost:41172/i), {
target: { value: 'http://localhost:41172/callback?code=ABC&state=XYZ' },
});
fireEvent.click(screen.getByRole('button', { name: /Finish connecting/i }));
// After the finish call resolves, the status query is invalidated and
// refetches connected=true → the connection banner appears with the email.
await waitFor(() => {
expect(screen.getByText('martin@example.com')).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: /Disconnect/i })).toBeInTheDocument();
});
it('clears the connection on Disconnect', async () => {
let connected = true;
server.use(
http.get('/api/v1/orca-cloud/status', () =>
HttpResponse.json(
connected
? { connected: true, email: 'martin@example.com', user_id: 'u1' }
: { connected: false, email: null, user_id: null },
),
),
http.get('/api/v1/orca-cloud/profiles', () => HttpResponse.json(noProfilesResponse)),
http.post('/api/v1/orca-cloud/logout', () => {
connected = false;
return HttpResponse.json({ success: true });
}),
);
render(<OrcaCloudView />);
await waitFor(() => {
expect(screen.getByText('martin@example.com')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Disconnect/i }));
await waitFor(() => {
expect(screen.getByText(/Connect to Orca Cloud/i)).toBeInTheDocument();
});
});
});