mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
* feat(webhooks): refactor webhook URL handling and enhance API endpoints - Introduced a new utility function `buildWebhookIngressUrl` to standardize the construction of webhook URLs across components. - Updated `WebhooksDebugPanel` and `TunnelList` components to utilize the new URL builder for improved consistency and maintainability. - Refactored API endpoints in `tunnelsApi` to reflect changes in webhook routing, ensuring all references to tunnel management are aligned with the new structure. - Adjusted user API calls to replace `/telegram/me` with `/auth/me` for better clarity and consistency in authentication flows. - Enhanced socket service to normalize channel connection update payloads, improving error handling and data integrity. These changes streamline webhook management and enhance the overall developer experience when working with webhooks. * feat(messaging): enhance Telegram and Discord channel linking functionality - Added support for managed DM linking with Telegram and OAuth flow for Discord. - Introduced `createChannelLinkToken` API to generate short-lived link tokens for messaging channels. - Updated `MessagingPanel` to handle managed linking flows, including building launch URLs and user instructions. - Enhanced configuration to include a Telegram bot username for fallback linking. - Updated tests to mock new configuration values for consistent testing. These changes improve the user experience for linking messaging channels and streamline the authentication process. * feat(api): introduce core command client and refactor API calls - Added a new `coreCommandClient` to facilitate RPC calls to core services. - Refactored existing API endpoints in `authApi`, `billingApi`, `creditsApi`, `tunnelsApi`, and `userApi` to utilize the new command client for improved consistency and maintainability. - Updated the billing API to include new endpoints for fetching balance, transactions, and managing auto-recharge settings. - Enhanced the user API to streamline user data retrieval. - Improved error handling and logging across the refactored API calls. These changes enhance the overall architecture of the API services, making them more modular and easier to maintain. * refactor(api): streamline API calls and enhance error handling - Refactored `creditsApi` and `tunnelsApi` to ensure consistent use of the new core command client. - Updated API methods to improve error handling and response parsing. - Introduced a new `authed_json` method in `BackendOAuthClient` for standardized authenticated requests. - Cleaned up unused imports and optimized code structure across various modules. These changes enhance the maintainability and reliability of the API services. * refactor(api): streamline API calls and improve code readability - Reorganized API calls in `authApi`, `billingApi`, `creditsApi`, and `tunnelsApi` to enhance consistency and maintainability. - Updated function formatting for better readability, ensuring parameters are clearly defined. - Cleaned up import statements and removed unnecessary line breaks to improve code clarity. These changes contribute to a more maintainable codebase and enhance the overall developer experience. * refactor(MessagingPanel): clean up imports and remove unused constants - Reorganized import statements for clarity and consistency. - Removed unused constants related to channel definitions and status styles to streamline the component. - Simplified the `updateField` function call in the rendering logic for better readability. These changes enhance the maintainability of the MessagingPanel component. * fix: restore helper functions removed during merge cleanup buildManagedChannelLaunchUrl and buildManagedChannelInstruction were stripped by the linter after merge resolution, breaking the build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(messaging): enhance MessagingPanel with pending instruction handling - Introduced a new state for pending instructions to improve user feedback during channel connection processes. - Updated error handling to ensure users receive clear instructions, including URLs for manual access if automatic opening fails. - Refactored connection status updates to streamline the dispatch process and improve code readability. - Enhanced the rendering logic to display pending instructions in the UI, providing better context for users during authentication flows. * fix(api): encode channel and ID parameters in webhook and OAuth URLs - Updated the URL construction in the BackendOAuthClient to encode the channel parameter, ensuring proper formatting for requests. - Modified the get_tunnel, update_tunnel, and delete_tunnel functions to encode the ID parameter in webhook URLs, enhancing security and compatibility with special characters. * fix(auth): validate channel input for link token creation - Added validation to ensure the channel is not empty and is one of the supported types (telegram, discord). - Converted the channel string to lowercase for consistent matching. - Updated the call to create_channel_link_token to use a reference to the channel variable. * chore(todos): update TODO list with completed tasks and new feature requests - Marked several tasks as completed, including integration of the custom memory engine and skills registry into the core. - Added new items to the TODO list, including improvements for voiceover functionalities, screen intelligence, and Gmail skill enhancements. - Included tasks for debugging skills from the UI and improving prompts to reduce hallucinations. * refactor(webhooks): improve formatting of get_tunnel function for readability - Reformatted the get_tunnel function to enhance code clarity by adjusting the indentation and line breaks in the call to get_authed_value. - This change improves the maintainability of the code and aligns with the overall code style. * test(userApi): enhance userApi tests with improved mocking and error handling - Added a mock function for core command calls to streamline test setup. - Introduced a helper function to generate mock user data for consistent test cases. - Updated tests to use the mock function for simulating API responses, improving clarity and maintainability. - Enhanced error handling in tests to cover various API error scenarios, ensuring robustness in userApi.getMe functionality. * refactor(billingApi): update tests to use core command mocking and improve error handling - Replaced apiClient mocks with core command mocks for better alignment with the new API structure. - Updated test cases to reflect changes in API call expectations, enhancing clarity and maintainability. - Improved error handling in tests to ensure proper propagation of errors from core command calls. * refactor(billingApi): simplify test cases by consolidating mock function calls - Streamlined mock function calls in billingApi tests for improved readability and consistency. - Removed unnecessary line breaks in mock responses, enhancing clarity in test definitions. - Ensured that error handling and API call expectations are clearly represented in the tests. * refactor(api): rename settings endpoint to current_user for clarity - Updated the endpoint handling authenticated profile fetches to improve clarity in the codebase. - Adjusted the routing to reflect the new function name, enhancing maintainability and understanding of the API's purpose. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
452 lines
15 KiB
TypeScript
452 lines
15 KiB
TypeScript
/* eslint-disable */
|
||
// @ts-nocheck
|
||
/**
|
||
* E2E test: Authentication & Access Control + Billing & Subscriptions (Linux / tauri-driver).
|
||
*
|
||
* Covers:
|
||
* 1.1 User registration via deep link
|
||
* 1.1.1 Duplicate account handling (re-auth same user)
|
||
* 1.2 Multi-device sessions (second JWT accepted)
|
||
* 3.1.1 Default plan allocation (FREE plan on registration)
|
||
* 3.2.1 Upgrade flow (purchase API call)
|
||
* 3.3.1 Active subscription display
|
||
* 3.3.3 Manage subscription (Stripe portal API call)
|
||
* 1.3 Logout via Settings menu
|
||
* 1.3.1 Revoked session auto-logout
|
||
*
|
||
* Onboarding steps (Onboarding.tsx — 5 steps, indices 0–4):
|
||
* Welcome → Local AI → Screen & Accessibility → Enable Tools → Install Skills
|
||
* (each step: primary "Continue"; final step completes onboarding)
|
||
*
|
||
* The mock server runs on http://127.0.0.1:18473 and the .app bundle must
|
||
* have been built with VITE_BACKEND_URL pointing there.
|
||
*/
|
||
import { waitForApp, waitForAppReady, waitForAuthBootstrap } from '../helpers/app-helpers';
|
||
import { triggerAuthDeepLink } from '../helpers/deep-link-helpers';
|
||
import {
|
||
clickButton,
|
||
clickText,
|
||
dumpAccessibilityTree,
|
||
hasAppChrome,
|
||
textExists,
|
||
waitForText,
|
||
waitForWebView,
|
||
waitForWindowVisible,
|
||
} from '../helpers/element-helpers';
|
||
import {
|
||
navigateToBilling,
|
||
navigateToHome,
|
||
navigateToSettings,
|
||
waitForHomePage,
|
||
walkOnboarding,
|
||
} from '../helpers/shared-flows';
|
||
import {
|
||
clearRequestLog,
|
||
getRequestLog,
|
||
resetMockBehavior,
|
||
setMockBehavior,
|
||
startMockServer,
|
||
stopMockServer,
|
||
} from '../mock-server';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Shared helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// waitForHomePage imported from shared-flows
|
||
|
||
async function waitForTextToDisappear(text, timeout = 10_000) {
|
||
const deadline = Date.now() + timeout;
|
||
while (Date.now() < deadline) {
|
||
if (!(await textExists(text))) return true;
|
||
await browser.pause(500);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function waitForRequest(method, urlFragment, timeout = 15_000) {
|
||
const deadline = Date.now() + timeout;
|
||
while (Date.now() < deadline) {
|
||
const log = getRequestLog();
|
||
const match = log.find(r => r.method === method && r.url.includes(urlFragment));
|
||
if (match) return match;
|
||
await browser.pause(500);
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
// walkOnboarding, waitForHomePage imported from shared-flows
|
||
|
||
/**
|
||
* Perform full login via deep link. Walks onboarding. Leaves app on Home page.
|
||
*/
|
||
async function performFullLogin(token = 'e2e-test-token') {
|
||
await triggerAuthDeepLink(token);
|
||
|
||
await waitForWindowVisible(25_000);
|
||
await waitForWebView(15_000);
|
||
await waitForAppReady(15_000);
|
||
await waitForAuthBootstrap(15_000);
|
||
|
||
const consumeCall = await waitForRequest('POST', '/telegram/login-tokens/', 20_000);
|
||
if (!consumeCall) {
|
||
console.log(
|
||
'[AuthAccess] Missing consume call. Request log:',
|
||
JSON.stringify(getRequestLog(), null, 2)
|
||
);
|
||
throw new Error('Auth consume call missing in performFullLogin');
|
||
}
|
||
// The app may call /auth/me or /settings for user profile
|
||
const meCall =
|
||
(await waitForRequest('GET', '/auth/me', 10_000)) ||
|
||
(await waitForRequest('GET', '/settings', 10_000));
|
||
if (!meCall) {
|
||
console.log(
|
||
'[AuthAccess] Missing user profile call. Request log:',
|
||
JSON.stringify(getRequestLog(), null, 2)
|
||
);
|
||
console.log('[AuthAccess] Continuing without user profile call confirmation');
|
||
}
|
||
|
||
// Walk real onboarding steps
|
||
await walkOnboarding('[AuthAccess]');
|
||
|
||
const homeText = await waitForHomePage(15_000);
|
||
if (!homeText) {
|
||
const tree = await dumpAccessibilityTree();
|
||
console.log('[AuthAccess] Home page not reached after login. Tree:\n', tree.slice(0, 4000));
|
||
throw new Error('Full login did not reach Home page');
|
||
}
|
||
console.log(`[AuthAccess] Home page confirmed: found "${homeText}"`);
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Test suite
|
||
// ===========================================================================
|
||
|
||
describe('Auth & Access Control', () => {
|
||
before(async () => {
|
||
await startMockServer();
|
||
await waitForApp();
|
||
clearRequestLog();
|
||
});
|
||
|
||
after(async () => {
|
||
resetMockBehavior();
|
||
await stopMockServer();
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 1. Authentication
|
||
// -------------------------------------------------------------------------
|
||
|
||
it('new user registers via deep link and reaches home', async () => {
|
||
await performFullLogin('e2e-auth-token');
|
||
});
|
||
|
||
it('re-authenticating with a new token for the same user returns to home', async () => {
|
||
clearRequestLog();
|
||
await triggerAuthDeepLink('e2e-auth-reauth-token');
|
||
await browser.pause(5_000);
|
||
|
||
const homeText = await waitForHomePage(15_000);
|
||
if (!homeText) {
|
||
await navigateToHome();
|
||
}
|
||
const finalHome = homeText || (await waitForHomePage(10_000));
|
||
expect(finalHome).not.toBeNull();
|
||
console.log('[AuthAccess] Re-auth completed, on Home');
|
||
});
|
||
|
||
it('second device token is accepted and processed', async () => {
|
||
clearRequestLog();
|
||
await triggerAuthDeepLink('e2e-auth-device2-token');
|
||
await browser.pause(5_000);
|
||
|
||
const homeText = await waitForHomePage(15_000);
|
||
if (!homeText) {
|
||
await navigateToHome();
|
||
}
|
||
const finalHome = homeText || (await waitForHomePage(10_000));
|
||
expect(finalHome).not.toBeNull();
|
||
|
||
const consumeCall = getRequestLog().find(
|
||
r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/')
|
||
);
|
||
expect(consumeCall).toBeDefined();
|
||
console.log('[AuthAccess] Multi-device token accepted');
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 2. Default Plan
|
||
// -------------------------------------------------------------------------
|
||
|
||
it('3.1.1 — new user is assigned FREE plan by default', async () => {
|
||
await navigateToBilling();
|
||
|
||
// BillingPanel heading: "Current Plan — FREE"
|
||
const hasPlan = (await textExists('Current Plan')) || (await textExists('FREE'));
|
||
if (!hasPlan) {
|
||
const tree = await dumpAccessibilityTree();
|
||
console.log('[AuthAccess] Billing page tree:\n', tree.slice(0, 6000));
|
||
}
|
||
expect(hasPlan).toBe(true);
|
||
|
||
const hasUpgrade = await textExists('Upgrade');
|
||
expect(hasUpgrade).toBe(true);
|
||
|
||
console.log('[AuthAccess] 3.1.1 — FREE plan verified in billing');
|
||
await navigateToHome();
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 3. Upgrade Flow
|
||
// -------------------------------------------------------------------------
|
||
|
||
it('3.2.1 — upgrade initiates purchase flow via Stripe', async () => {
|
||
await navigateToBilling();
|
||
clearRequestLog();
|
||
|
||
await clickText('Upgrade', 10_000);
|
||
console.log('[AuthAccess] Clicked Upgrade button');
|
||
await browser.pause(3_000);
|
||
|
||
const purchaseCall = await waitForRequest('POST', '/payments/stripe/purchasePlan', 10_000);
|
||
expect(purchaseCall).toBeDefined();
|
||
|
||
if (purchaseCall?.body) {
|
||
const bodyStr = typeof purchaseCall.body === 'string' ? purchaseCall.body : '';
|
||
console.log('[AuthAccess] Purchase request body:', bodyStr);
|
||
}
|
||
|
||
// Verify purchasing state appears
|
||
const hasWaiting = (await textExists('Waiting')) || (await textExists('Waiting for payment'));
|
||
console.log(`[AuthAccess] Purchasing state visible: ${hasWaiting}`);
|
||
|
||
// Switch mock to BASIC plan so polling clears the waiting state
|
||
setMockBehavior('plan', 'BASIC');
|
||
setMockBehavior('planActive', 'true');
|
||
setMockBehavior('planExpiry', new Date(Date.now() + 30 * 86400000).toISOString());
|
||
|
||
if (hasWaiting) {
|
||
const disappeared = await waitForTextToDisappear('Waiting', 20_000);
|
||
if (!disappeared) {
|
||
throw new Error(
|
||
'3.2.1 — "Waiting" spinner did not clear within 20s after mock plan was set to BASIC'
|
||
);
|
||
}
|
||
}
|
||
|
||
console.log('[AuthAccess] 3.2.1 — Upgrade purchase flow verified');
|
||
await navigateToHome();
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 4. Active Subscription Display
|
||
// -------------------------------------------------------------------------
|
||
|
||
it('3.3.1 — active subscription is displayed correctly', async () => {
|
||
// Seed mock state explicitly so this test is self-contained
|
||
setMockBehavior('plan', 'BASIC');
|
||
setMockBehavior('planActive', 'true');
|
||
setMockBehavior('planExpiry', new Date(Date.now() + 30 * 86400000).toISOString());
|
||
clearRequestLog();
|
||
|
||
await navigateToBilling();
|
||
|
||
// Wait for billing data to load
|
||
await browser.pause(3_000);
|
||
|
||
// Verify currentPlan was fetched
|
||
const planCall = getRequestLog().find(
|
||
r => r.method === 'GET' && r.url.includes('/payments/stripe/currentPlan')
|
||
);
|
||
expect(planCall).toBeDefined();
|
||
|
||
// Check that plan info is displayed (Current Plan heading or tier name)
|
||
const hasPlanInfo =
|
||
(await textExists('Current Plan')) ||
|
||
(await textExists('BASIC')) ||
|
||
(await textExists('Basic'));
|
||
expect(hasPlanInfo).toBe(true);
|
||
|
||
// "Manage" button appears when hasActiveSubscription is true in currentPlan response.
|
||
const hasManage = await textExists('Manage');
|
||
expect(hasManage).toBe(true);
|
||
|
||
console.log('[AuthAccess] 3.3.1 — Active subscription display verified (Manage visible)');
|
||
});
|
||
|
||
it('3.3.3 — manage subscription opens Stripe portal', async () => {
|
||
// Seed mock state explicitly so this test is self-contained
|
||
setMockBehavior('plan', 'BASIC');
|
||
setMockBehavior('planActive', 'true');
|
||
setMockBehavior('planExpiry', new Date(Date.now() + 30 * 86400000).toISOString());
|
||
clearRequestLog();
|
||
|
||
await navigateToBilling();
|
||
await browser.pause(3_000);
|
||
|
||
const hasManage = await textExists('Manage');
|
||
expect(hasManage).toBe(true);
|
||
|
||
await clickText('Manage', 10_000);
|
||
console.log('[AuthAccess] Clicked Manage button');
|
||
await browser.pause(3_000);
|
||
|
||
const portalCall = await waitForRequest('POST', '/payments/stripe/portal', 10_000);
|
||
if (!portalCall) {
|
||
console.log('[AuthAccess] Portal request log:', JSON.stringify(getRequestLog(), null, 2));
|
||
}
|
||
expect(portalCall).toBeDefined();
|
||
|
||
console.log('[AuthAccess] 3.3.3 — Stripe portal API call verified');
|
||
resetMockBehavior();
|
||
await navigateToHome();
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 5. Logout
|
||
// -------------------------------------------------------------------------
|
||
|
||
it('user can log out via Settings and returns to Welcome', async () => {
|
||
// Re-auth to get a clean session for logout
|
||
clearRequestLog();
|
||
await triggerAuthDeepLink('e2e-pre-logout-token');
|
||
await browser.pause(5_000);
|
||
|
||
const homeCheck = await waitForHomePage(10_000);
|
||
if (!homeCheck) {
|
||
await navigateToHome();
|
||
}
|
||
|
||
await navigateToSettings();
|
||
|
||
// Click "Log out" via JS — the settings menu item text is "Log out"
|
||
// with description "Sign out of your account"
|
||
const loggedOut = await browser.execute(() => {
|
||
const allElements = document.querySelectorAll('*');
|
||
for (const el of allElements) {
|
||
const text = el.textContent?.trim() || '';
|
||
if (text === 'Log out') {
|
||
const clickable = el.closest(
|
||
'button, [role="button"], a, [class*="MenuItem"]'
|
||
) as HTMLElement;
|
||
if (clickable) {
|
||
clickable.click();
|
||
return 'clicked-parent';
|
||
}
|
||
(el as HTMLElement).click();
|
||
return 'clicked-self';
|
||
}
|
||
}
|
||
return null;
|
||
});
|
||
|
||
if (!loggedOut) {
|
||
// Fallback: try XPath text search
|
||
const logoutCandidates = ['Log out', 'Logout', 'Sign out'];
|
||
let found = false;
|
||
for (const text of logoutCandidates) {
|
||
if (await textExists(text)) {
|
||
await clickText(text, 10_000);
|
||
console.log(`[AuthAccess] Clicked "${text}" via XPath`);
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
const tree = await dumpAccessibilityTree();
|
||
console.log('[AuthAccess] Logout button not found. Tree:\n', tree.slice(0, 4000));
|
||
throw new Error('Could not find logout button in Settings');
|
||
}
|
||
} else {
|
||
console.log(`[AuthAccess] Logout: ${loggedOut}`);
|
||
}
|
||
|
||
// If a confirmation dialog appears, confirm it
|
||
await browser.pause(2_000);
|
||
const hasConfirm =
|
||
(await textExists('Confirm')) || (await textExists('Yes')) || (await textExists('Log Out'));
|
||
if (hasConfirm) {
|
||
const confirmed = await browser.execute(() => {
|
||
const candidates = document.querySelectorAll('button, [role="button"], a');
|
||
for (const el of candidates) {
|
||
const text = el.textContent?.trim() || '';
|
||
const label = el.getAttribute('aria-label') || '';
|
||
if (['Confirm', 'Yes', 'Log Out'].some(t => text === t || label === t)) {
|
||
(el as HTMLElement).click();
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
});
|
||
expect(confirmed).toBe(true);
|
||
console.log('[AuthAccess] Confirmation dialog: clicked');
|
||
await browser.pause(2_000);
|
||
}
|
||
|
||
// Verify we landed on the logged-out state — assert a specific marker
|
||
await browser.pause(3_000);
|
||
const welcomeCandidates = ['Welcome', 'Sign in', 'Login', 'Get Started'];
|
||
let onWelcome = false;
|
||
for (const text of welcomeCandidates) {
|
||
if (await textExists(text)) {
|
||
console.log(`[AuthAccess] Logged-out state confirmed: found "${text}"`);
|
||
onWelcome = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Also verify auth token was cleared from localStorage
|
||
const hasToken = await browser.execute(() => {
|
||
const persisted = localStorage.getItem('persist:auth');
|
||
if (!persisted) return false;
|
||
try {
|
||
const parsed = JSON.parse(persisted);
|
||
const token = typeof parsed.token === 'string' ? parsed.token.replace(/^"|"$/g, '') : null;
|
||
return !!token && token !== 'null';
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
|
||
// Must see logged-out UI or token must be cleared (or both)
|
||
expect(onWelcome || !hasToken).toBe(true);
|
||
console.log(`[AuthAccess] Logout verified: welcomeUI=${onWelcome}, tokenCleared=${!hasToken}`);
|
||
});
|
||
|
||
it('revoked session auto-logs out the user', async () => {
|
||
// Login fresh
|
||
clearRequestLog();
|
||
resetMockBehavior();
|
||
await performFullLogin('e2e-revoked-session-token');
|
||
|
||
// Set mock to return 401 for user profile requests (revoked session)
|
||
setMockBehavior('session', 'revoked');
|
||
|
||
// Trigger a re-auth which will fail with 401
|
||
await triggerAuthDeepLink('e2e-revoked-check-token');
|
||
await browser.pause(8_000);
|
||
|
||
// The app should auto-log out when it gets a 401
|
||
const stillOnHome = await waitForHomePage(5_000);
|
||
if (!stillOnHome) {
|
||
console.log('[AuthAccess] Revoked session: user was logged out (no home page markers)');
|
||
}
|
||
|
||
// Verify the app is either on Welcome or not on Home
|
||
const welcomeCandidates = ['Welcome', 'Sign in', 'Login', 'Get Started', 'OpenHuman'];
|
||
let onWelcome = false;
|
||
for (const text of welcomeCandidates) {
|
||
if (await textExists(text)) {
|
||
onWelcome = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
expect(onWelcome || !stillOnHome).toBe(true);
|
||
console.log('[AuthAccess] Revoked session auto-logout verified');
|
||
});
|
||
});
|