mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-15 15:10:31 -08:00
Chore/upgrade (#646)
* docs: deps * Update dependencies and type usage for AI features Upgraded multiple dependencies in package.json and pnpm-lock.yaml, including @ai-sdk, @mui, react, and others for improved compatibility and performance. Changed type usage from CoreMessage to ModelMessage in mockOpenAI.test.ts to align with updated ai package. No functional changes to application logic. * feat: i18n * feat: test oauth login and use PKCE * fix: use ollama-ai-provider-v2 * test: github and mock oauth2 login * test: gitea login * Refactor context menu cleanup and error message Moved context menu cleanup for OAuth window to a single closed event handler in Authentication service. Simplified error message formatting in ContextService for missing keys. * lint: AI fix * Add tsx as a dev dependency and update scripts Replaced usage of 'pnpm dlx tsx' with direct 'tsx' command in development and test scripts for improved reliability. Added 'tsx' to devDependencies in package.json.
This commit is contained in:
parent
19ef74a4a6
commit
b76fc17794
75 changed files with 5863 additions and 3733 deletions
218
features/oauthLogin.feature
Normal file
218
features/oauthLogin.feature
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
Feature: OAuth Login Flow
|
||||
As a user
|
||||
I want to login via OAuth with PKCE using GitHub OAuth
|
||||
So that I can securely authenticate and sync my wiki
|
||||
|
||||
Background:
|
||||
Given I launch the TidGi application
|
||||
And I wait for the page to load completely
|
||||
And I should see a "page body" element with selector "body"
|
||||
|
||||
@oauth @pkce
|
||||
Scenario: Login with Custom OAuth Server using PKCE
|
||||
# Step 1: Start Mock OAuth Server
|
||||
When I start Mock OAuth Server on port 8888
|
||||
|
||||
# Step 2: Open preferences window
|
||||
When I click on a "settings button" element with selector "#open-preferences-button"
|
||||
When I switch to "preferences" window
|
||||
|
||||
# Step 2: Navigate to Sync section
|
||||
When I click on a "sync section" element with selector "[data-testid='preference-section-sync']"
|
||||
|
||||
# Step 3: Click Custom Server tab
|
||||
When I click on a "custom server tab" element with selector "[data-testid='custom-server-tab']"
|
||||
|
||||
# Step 4: Verify Custom Server form is visible
|
||||
Then I should see "server url input and client id input" elements with selectors:
|
||||
| [data-testid='custom-server-url-input'] |
|
||||
| [data-testid='custom-client-id-input'] |
|
||||
|
||||
# Step 5: Trigger OAuth login
|
||||
# Note: oauth2-mock-server automatically redirects without showing login UI
|
||||
# This tests the token exchange logic which is the most critical part
|
||||
When I click on a "login button" element with selector "[data-testid='custom-oauth-login-button']"
|
||||
And I wait for 3 seconds
|
||||
|
||||
# Step 6: After OAuth completes, page reloads and defaults to GitHub tab
|
||||
# Need to click Custom Server tab again to see the filled token
|
||||
When I click on a "custom server tab" element with selector "[data-testid='custom-server-tab']"
|
||||
|
||||
# Step 7: The token should be filled in the form after OAuth completes
|
||||
Then I should see a "token input with non-empty value" element with selector "[data-testid='custom-token-input'] input:not([value=''])"
|
||||
|
||||
# Step 8: Verify logout button appears
|
||||
Then I should see a "logout button" element with selector "[data-testid='custom-oauth-logout-button']"
|
||||
|
||||
# Step 9: Close preferences window
|
||||
When I close "preferences" window
|
||||
|
||||
# Cleanup
|
||||
And I stop Mock OAuth Server
|
||||
|
||||
# For Github login debugging. Need human to fill in the real password of an one-time test account.
|
||||
# @oauth @github @real @manual
|
||||
# Scenario: Login with Real GitHub OAuth
|
||||
# # NOTE: This test requires GITHUB_CLIENT_SECRET environment variable to be set
|
||||
# # GitHub OAuth Apps don't support PKCE and require client_secret
|
||||
# # Step 1: Open preferences window
|
||||
# When I click on a "settings button" element with selector "#open-preferences-button"
|
||||
# And I wait for 1 seconds
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 2: Navigate to Sync section
|
||||
# When I click on a "sync section" element with selector "[data-testid='preference-section-sync']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 3: Click GitHub login button (this will open a new OAuth window)
|
||||
# When I click on a "GitHub login button" element with selector "[data-testid='github-login-button']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 4: Switch to the OAuth popup window
|
||||
# When I switch to the newest window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 5: Fill in GitHub credentials in the OAuth popup
|
||||
# # GitHub's login page uses 'login' and 'password' as field names
|
||||
# When I type "tiddlygit@gmail.com" in "GitHub email input" element with selector "input[name='login']"
|
||||
# And I wait for 0.5 seconds
|
||||
# When I type "PASSWORD HERE" in "GitHub password input" element with selector "input[name='password']"
|
||||
# And I wait for 0.5 seconds
|
||||
# When I click on a "GitHub sign in button" element with selector "input[type='submit'][name='commit']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 6: Click Authorize button on the OAuth authorization page
|
||||
# # GitHub App requires user to authorize access to their account
|
||||
# # The button is usually green and says "Authorize [AppName]"
|
||||
# # When I click on a "GitHub authorize button" element with selector "button[type='submit'].btn-primary, button[id*='authorize'], button.js-oauth-authorize-btn"
|
||||
# # And I wait for 3 seconds
|
||||
|
||||
# # Step 7: Switch back to preferences window
|
||||
# # The OAuth window should close automatically after authorization
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 7: Verify token is filled in the form with actual value
|
||||
# Then I should see a "GitHub token field with value" element with selector "[data-testid='github-token-input'] input:not([value=''])"
|
||||
|
||||
# # Step 8: Verify user info is populated with actual value
|
||||
# Then I should see a "GitHub username field with value" element with selector "[data-testid='github-userName-input'] input:not([value=''])"
|
||||
|
||||
# # Step 9: Close preferences window
|
||||
# When I close "preferences" window
|
||||
|
||||
|
||||
# @oauth @codeberg @real @manual
|
||||
# Scenario: Login with Real Codeberg OAuth
|
||||
# # Step 1: Open preferences window
|
||||
# When I click on a "settings button" element with selector "#open-preferences-button"
|
||||
# And I wait for 1 seconds
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 2: Navigate to Sync section
|
||||
# When I click on a "sync section" element with selector "[data-testid='preference-section-sync']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 3: Click Codeberg tab
|
||||
# When I click on a "Codeberg tab" element with selector "[data-testid='codeberg-tab']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 4: Click Codeberg login button (this will open a new OAuth window)
|
||||
# When I click on a "Codeberg login button" element with selector "[data-testid='codeberg-login-button']"
|
||||
# And I wait for 2 seconds
|
||||
|
||||
# # Step 4: Switch to the OAuth popup window
|
||||
# When I switch to the newest window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 5: Fill in Codeberg credentials in the OAuth popup
|
||||
# # Codeberg uses id-based selectors
|
||||
# When I type "USERNAME HERE" in "Codeberg email input" element with selector "#user_name"
|
||||
# When I type "TEMPORARY pswd" in "Codeberg password input" element with selector "#password"
|
||||
# When I click on a "Codeberg sign in button" element with selector "button.ui.primary.button.tw-w-full"
|
||||
# And I wait for 2 seconds
|
||||
|
||||
# # Step 6: Authorize the application (Codeberg requires authorization every time)
|
||||
# # The authorization page has two buttons:
|
||||
# # - Red "Authorize Application" button (id="authorize-app", name="granted" value="true")
|
||||
# # - Gray "Cancel" button (name="granted" value="false")
|
||||
# # We need to click the red one
|
||||
# When I click on a "authorize button" element with selector "#authorize-app"
|
||||
# And I wait for 2 seconds
|
||||
|
||||
# # Step 7: After OAuth completes, switch back to preferences window
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 8: Switch to Codeberg tab to verify the filled token
|
||||
# When I click on a "codeberg tab" element with selector "[data-testid='codeberg-tab']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 9: Verify token is filled in the form with actual value
|
||||
# Then I should see a "Codeberg token field with value" element with selector "[data-testid='codeberg-token-input'] input:not([value=''])"
|
||||
|
||||
# # Step 10: Verify user info is populated with actual value
|
||||
# Then I should see a "Codeberg username field with value" element with selector "[data-testid='codeberg-userName-input'] input:not([value=''])"
|
||||
|
||||
# # Step 11: Close preferences window
|
||||
# When I close "preferences" window
|
||||
|
||||
# @oauth @gitea @real @manual
|
||||
# Scenario: Login with Real Gitea OAuth
|
||||
# # NOTE: This test uses real Gitea.com OAuth with test credentials
|
||||
# # Gitea supports OAuth Apps with PKCE
|
||||
|
||||
# # Step 1: Open preferences window
|
||||
# When I click on a "settings button" element with selector "#open-preferences-button"
|
||||
# And I wait for 1 seconds
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 2: Navigate to Sync section
|
||||
# When I click on a "sync section" element with selector "[data-testid='preference-section-sync']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 3: Click Gitea tab
|
||||
# When I click on a "Gitea tab" element with selector "[data-testid='gitea-tab']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 4: Click Gitea login button (this will open a new OAuth window)
|
||||
# When I click on a "Gitea login button" element with selector "[data-testid='gitea-login-button']"
|
||||
# And I wait for 2 seconds
|
||||
|
||||
# # Step 5: Switch to the OAuth popup window
|
||||
# When I switch to the newest window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 6: Fill in Gitea credentials in the OAuth popup
|
||||
# # Gitea uses similar id-based selectors as Codeberg
|
||||
# When I type "USERNAME HERE" in "Gitea username input" element with selector "#user_name"
|
||||
# When I type "TEMPORARY pswd" in "Gitea password input" element with selector "#password"
|
||||
# When I click on a "Gitea sign in button" element with selector "button.ui.primary.button.tw-w-full"
|
||||
# And I wait for 2 seconds
|
||||
|
||||
# # Step 7: Authorize the application (Gitea requires authorization every time like Codeberg)
|
||||
# # The authorization page has two buttons similar to Codeberg:
|
||||
# # - Red "Authorize Application" button (id="authorize-app")
|
||||
# # - Gray "Cancel" button
|
||||
# # When I click on a "authorize button" element with selector "#authorize-app"
|
||||
# # And I wait for 2 seconds
|
||||
|
||||
# # Step 8: After OAuth completes, switch back to preferences window
|
||||
# When I switch to "preferences" window
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 9: Switch to Gitea tab to verify the filled token
|
||||
# When I click on a "gitea tab" element with selector "[data-testid='gitea-tab']"
|
||||
# And I wait for 1 seconds
|
||||
|
||||
# # Step 10: Verify token is filled in the form with actual value
|
||||
# Then I should see a "Gitea token field with value" element with selector "[data-testid='gitea-token-input'] input:not([value=''])"
|
||||
|
||||
# # Step 11: Verify user info is populated with actual value
|
||||
# Then I should see a "Gitea username field with value" element with selector "[data-testid='gitea-userName-input'] input:not([value=''])"
|
||||
|
||||
# # Step 12: Close preferences window
|
||||
# When I close "preferences" window
|
||||
|
|
@ -4,6 +4,7 @@ import path from 'path';
|
|||
import { _electron as electron } from 'playwright';
|
||||
import type { ElectronApplication, Page } from 'playwright';
|
||||
import { windowDimension, WindowNames } from '../../src/services/windows/WindowProperties';
|
||||
import { MockOAuthServer } from '../supports/mockOAuthServer';
|
||||
import { MockOpenAIServer } from '../supports/mockOpenAI';
|
||||
import { logsDirectory, makeSlugPath, screenshotsDirectory } from '../supports/paths';
|
||||
import { getPackedAppPath } from '../supports/paths';
|
||||
|
|
@ -33,6 +34,7 @@ export class ApplicationWorld {
|
|||
mainWindow: Page | undefined; // Keep for compatibility during transition
|
||||
currentWindow: Page | undefined; // New state-managed current window
|
||||
mockOpenAIServer: MockOpenAIServer | undefined;
|
||||
mockOAuthServer: MockOAuthServer | undefined;
|
||||
|
||||
// Helper method to check if window is visible
|
||||
async isWindowVisible(page: Page): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { DataTable, Then } from '@cucumber/cucumber';
|
||||
import { After, DataTable, Then, When } from '@cucumber/cucumber';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { MockOAuthServer } from '../supports/mockOAuthServer';
|
||||
import { logsDirectory } from '../supports/paths';
|
||||
import { ApplicationWorld } from './application';
|
||||
|
||||
|
|
@ -17,3 +18,35 @@ Then('I should find log entries containing', async function(this: ApplicationWor
|
|||
throw new Error(`Missing expected log messages "${missing.map(item => item.slice(0, 10)).join('...", "')}..." on latest log file: ${latestLogFilePath}`);
|
||||
}
|
||||
});
|
||||
|
||||
// OAuth Server Steps
|
||||
When('I start Mock OAuth Server on port {int}', async function(this: ApplicationWorld, port: number) {
|
||||
this.mockOAuthServer = new MockOAuthServer(
|
||||
{ clientId: 'test-client-id' },
|
||||
port,
|
||||
);
|
||||
await this.mockOAuthServer.start();
|
||||
});
|
||||
|
||||
When('I stop Mock OAuth Server', async function(this: ApplicationWorld) {
|
||||
if (this.mockOAuthServer) {
|
||||
await this.mockOAuthServer.stop();
|
||||
this.mockOAuthServer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up Mock OAuth Server after @oauth tests
|
||||
After({ tags: '@oauth' }, async function(this: ApplicationWorld) {
|
||||
if (this.mockOAuthServer) {
|
||||
try {
|
||||
await Promise.race([
|
||||
this.mockOAuthServer.stop(),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, 2000)),
|
||||
]);
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
} finally {
|
||||
this.mockOAuthServer = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -300,6 +300,20 @@ When('I switch to {string} window', async function(this: ApplicationWorld, windo
|
|||
}
|
||||
});
|
||||
|
||||
// Switch to the newest/latest window (useful for OAuth popups)
|
||||
When('I switch to the newest window', async function(this: ApplicationWorld) {
|
||||
if (!this.app) {
|
||||
throw new Error('Application is not available');
|
||||
}
|
||||
const allWindows = this.app.windows().filter(p => !p.isClosed());
|
||||
if (allWindows.length === 0) {
|
||||
throw new Error('No windows available');
|
||||
}
|
||||
// The newest window is the last one in the array
|
||||
const newestWindow = allWindows[allWindows.length - 1];
|
||||
this.currentWindow = newestWindow;
|
||||
});
|
||||
|
||||
// Generic window closing
|
||||
When('I close {string} window', async function(this: ApplicationWorld, windowType: string) {
|
||||
if (!this.app) {
|
||||
|
|
|
|||
87
features/supports/mockOAuthServer.ts
Normal file
87
features/supports/mockOAuthServer.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Mock OAuth Server using oauth2-mock-server
|
||||
*
|
||||
* Replaced custom implementation with professional OAuth 2 mock server.
|
||||
* Key benefits:
|
||||
* - Standards-compliant OAuth 2.0 + PKCE
|
||||
* - Automatic JWT token generation
|
||||
* - Proper error handling
|
||||
* - Less code to maintain (from 400+ lines to ~100 lines)
|
||||
*
|
||||
* Note: oauth2-mock-server automatically handles authorization.
|
||||
* It doesn't provide a login UI - it immediately redirects with a code.
|
||||
* This is perfect for testing token exchange logic without UI complexity.
|
||||
*
|
||||
* Standard OAuth 2 endpoints:
|
||||
* - /authorize (authorization endpoint)
|
||||
* - /token (token exchange endpoint)
|
||||
* - /userinfo (user info endpoint)
|
||||
*/
|
||||
import { OAuth2Server } from 'oauth2-mock-server';
|
||||
import type { MutableResponse, MutableToken } from 'oauth2-mock-server';
|
||||
|
||||
export class MockOAuthServer {
|
||||
private server: OAuth2Server | null = null;
|
||||
public port = 0;
|
||||
public baseUrl = '';
|
||||
|
||||
constructor(
|
||||
private config: { clientId: string },
|
||||
private fixedPort?: number,
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.server = new OAuth2Server();
|
||||
|
||||
// Generate RSA key for signing JWT tokens
|
||||
await this.server.issuer.keys.generate('RS256');
|
||||
|
||||
// Start server on specified or random port
|
||||
await this.server.start(this.fixedPort || 0, '127.0.0.1');
|
||||
|
||||
const address = this.server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
throw new Error('Failed to get server address');
|
||||
}
|
||||
|
||||
this.port = address.port;
|
||||
this.baseUrl = `http://127.0.0.1:${this.port}`;
|
||||
|
||||
// Configure issuer URL
|
||||
this.server.issuer.url = this.baseUrl;
|
||||
|
||||
// Setup custom behavior to match real OAuth servers
|
||||
this.setupCustomBehavior();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
this.server = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize server behavior to match GitHub/GitLab/Gitea OAuth servers
|
||||
*/
|
||||
private setupCustomBehavior(): void {
|
||||
if (!this.server) return;
|
||||
|
||||
// Customize access token to include GitHub-like claims
|
||||
this.server.service.on('beforeTokenSigning', (token: MutableToken, _request) => {
|
||||
token.payload.scope = 'user:email,read:user,repo,workflow';
|
||||
token.payload.token_type = 'bearer';
|
||||
});
|
||||
|
||||
// Simulate user info endpoint (matches GitHub API response)
|
||||
this.server.service.on('beforeUserinfo', (userInfoResponse: MutableResponse, _request) => {
|
||||
userInfoResponse.body = {
|
||||
login: 'testuser',
|
||||
id: 12345,
|
||||
email: 'testuser@example.com',
|
||||
name: 'Test User',
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { CoreMessage } from 'ai';
|
||||
import type { ModelMessage } from 'ai';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import type { AiAPIConfig } from '../../src/services/agentInstance/promptConcat/promptConcatSchema';
|
||||
import { streamFromProvider } from '../../src/services/externalAPI/callProviderAPI';
|
||||
|
|
@ -265,7 +265,7 @@ describe('Mock OpenAI Server', () => {
|
|||
enabled: true,
|
||||
};
|
||||
|
||||
const messages: CoreMessage[] = [
|
||||
const messages: ModelMessage[] = [
|
||||
{ role: 'user', content: 'Start streaming' },
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue