From 211c7e5d417dd6be9d832416eb6b98a172b69e62 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 23 Feb 2026 19:57:59 +0100 Subject: [PATCH] test: add tests for GitHub OAuth device flow types and Settings UI - Add serialization tests for DeviceFlowStart, DeviceFlowPollResult, GitHubUser - Add SettingsPanel tests for OAuth section: login button, connected state, disconnect, waiting state with user code, no token field - All 174 Rust + 475 frontend tests pass Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/github.rs | 70 +++++++++++++++++ src/components/SettingsPanel.test.tsx | 105 +++++++++++++++++++++++++- 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/github.rs b/src-tauri/src/github.rs index 879490d7..6c3ff814 100644 --- a/src-tauri/src/github.rs +++ b/src-tauri/src/github.rs @@ -491,4 +491,74 @@ mod tests { assert!(repo.updated_at.is_none()); assert!(!repo.private); } + + #[test] + fn test_device_flow_start_serialization() { + let start = DeviceFlowStart { + device_code: "dc_123".to_string(), + user_code: "ABCD-1234".to_string(), + verification_uri: "https://github.com/login/device".to_string(), + expires_in: 900, + interval: 5, + }; + let json = serde_json::to_string(&start).unwrap(); + let parsed: DeviceFlowStart = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.device_code, "dc_123"); + assert_eq!(parsed.user_code, "ABCD-1234"); + assert_eq!(parsed.verification_uri, "https://github.com/login/device"); + assert_eq!(parsed.expires_in, 900); + assert_eq!(parsed.interval, 5); + } + + #[test] + fn test_device_flow_poll_result_complete() { + let result = DeviceFlowPollResult { + status: "complete".to_string(), + access_token: Some("gho_abc123".to_string()), + error: None, + }; + let json = serde_json::to_string(&result).unwrap(); + let parsed: DeviceFlowPollResult = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.status, "complete"); + assert_eq!(parsed.access_token, Some("gho_abc123".to_string())); + assert!(parsed.error.is_none()); + } + + #[test] + fn test_device_flow_poll_result_pending() { + let result = DeviceFlowPollResult { + status: "pending".to_string(), + access_token: None, + error: Some("authorization_pending".to_string()), + }; + let json = serde_json::to_string(&result).unwrap(); + let parsed: DeviceFlowPollResult = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.status, "pending"); + assert!(parsed.access_token.is_none()); + assert_eq!( + parsed.error, + Some("authorization_pending".to_string()) + ); + } + + #[test] + fn test_github_user_serialization() { + let user = GitHubUser { + login: "lucaong".to_string(), + name: Some("Luca Ongaro".to_string()), + avatar_url: "https://avatars.githubusercontent.com/u/123".to_string(), + }; + let json = serde_json::to_string(&user).unwrap(); + let parsed: GitHubUser = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.login, "lucaong"); + assert_eq!(parsed.name, Some("Luca Ongaro".to_string())); + } + + #[test] + fn test_github_user_deserialization_null_name() { + let json = r#"{"login":"bot","name":null,"avatar_url":"https://x"}"#; + let user: GitHubUser = serde_json::from_str(json).unwrap(); + assert_eq!(user.login, "bot"); + assert!(user.name.is_none()); + } } diff --git a/src/components/SettingsPanel.test.tsx b/src/components/SettingsPanel.test.tsx index 16e1a58e..39cd7789 100644 --- a/src/components/SettingsPanel.test.tsx +++ b/src/components/SettingsPanel.test.tsx @@ -1,8 +1,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { SettingsPanel } from './SettingsPanel' import type { Settings } from '../types' +// Mock the tauri/mock-tauri calls used by GitHubSection +const mockInvokeFn = vi.fn() +vi.mock('@tauri-apps/api/core', () => ({ + invoke: (...args: unknown[]) => mockInvokeFn(...args), +})) +vi.mock('../mock-tauri', () => ({ + isTauri: () => false, + mockInvoke: (...args: unknown[]) => mockInvokeFn(...args), +})) + const emptySettings: Settings = { anthropic_key: null, openai_key: null, @@ -189,4 +199,97 @@ describe('SettingsPanel', () => { const updatedInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement expect(updatedInput.value).toBe('new-key') }) + + describe('GitHub OAuth section', () => { + it('shows Login with GitHub button when not connected', () => { + render( + + ) + expect(screen.getByTestId('github-login')).toBeInTheDocument() + expect(screen.getByText('Login with GitHub')).toBeInTheDocument() + }) + + it('does not show GitHub token input field', () => { + render( + + ) + expect(screen.queryByTestId('settings-key-github-token')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('ghp_... or gho_...')).not.toBeInTheDocument() + }) + + it('shows connected state with username when GitHub is connected', () => { + const connectedSettings: Settings = { + ...emptySettings, + github_token: 'gho_test_token', + github_username: 'lucaong', + } + render( + + ) + expect(screen.getByTestId('github-connected')).toBeInTheDocument() + expect(screen.getByText('lucaong')).toBeInTheDocument() + expect(screen.getByText('Connected')).toBeInTheDocument() + expect(screen.getByTestId('github-disconnect')).toBeInTheDocument() + }) + + it('clears GitHub connection on disconnect', () => { + const connectedSettings: Settings = { + ...emptySettings, + github_token: 'gho_test_token', + github_username: 'lucaong', + } + render( + + ) + fireEvent.click(screen.getByTestId('github-disconnect')) + + // onSave should be called with cleared GitHub fields + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ + github_token: null, + github_username: null, + })) + }) + + it('shows waiting state with user code during OAuth flow', async () => { + mockInvokeFn.mockImplementation(async (cmd: string) => { + if (cmd === 'github_device_flow_start') { + return { + device_code: 'test_device_code', + user_code: 'TEST-1234', + verification_uri: 'https://github.com/login/device', + expires_in: 900, + interval: 5, + } + } + if (cmd === 'github_device_flow_poll') { + return { status: 'pending', access_token: null, error: 'authorization_pending' } + } + return null + }) + + // Mock window.open + const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null) + + render( + + ) + + fireEvent.click(screen.getByTestId('github-login')) + + await waitFor(() => { + expect(screen.getByTestId('github-waiting')).toBeInTheDocument() + expect(screen.getByTestId('github-user-code')).toHaveTextContent('TEST-1234') + }) + + expect(windowOpen).toHaveBeenCalledWith('https://github.com/login/device', '_blank') + windowOpen.mockRestore() + }) + + it('shows GitHub section description about connecting', () => { + render( + + ) + expect(screen.getByText(/Connect your GitHub account/)).toBeInTheDocument() + }) + }) })