mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 03:59:40 +02:00
feat(matrix): add tests, E2EE warning, and dynamic homeserver config
- Make SSO loginToken homeserver configurable via VITE_MATRIX_HOMESERVER - Add vitest setup with 14 unit tests for Matrix client functions (discoverHomeserver, checkHomeserver, loginWithToken) - Show amber warning banner when E2EE is not available Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a4f52df138
commit
416e031f69
7 changed files with 726 additions and 603 deletions
|
|
@ -12,7 +12,9 @@
|
|||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@manacore/shared-pwa": "workspace:*",
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
"typescript": "^5.9.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^4.1.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
136
apps/matrix/apps/web/src/lib/matrix/client.test.ts
Normal file
136
apps/matrix/apps/web/src/lib/matrix/client.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { discoverHomeserver, checkHomeserver, loginWithToken } from './client';
|
||||
|
||||
// Mock matrix-js-sdk to avoid importing the full SDK in tests
|
||||
vi.mock('matrix-js-sdk', () => ({
|
||||
createClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./polyfills', () => ({}));
|
||||
|
||||
describe('discoverHomeserver', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('extracts domain from Matrix user ID', async () => {
|
||||
// Mock .well-known failing so we get the fallback
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
|
||||
|
||||
const result = await discoverHomeserver('@user:example.com');
|
||||
expect(result).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('returns null for invalid user ID without domain', async () => {
|
||||
const result = await discoverHomeserver('@user');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('uses domain directly when no @ prefix', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
|
||||
|
||||
const result = await discoverHomeserver('matrix.org');
|
||||
expect(result).toBe('https://matrix.org');
|
||||
});
|
||||
|
||||
it('strips protocol prefix from domain', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
|
||||
|
||||
const result = await discoverHomeserver('https://matrix.org');
|
||||
expect(result).toBe('https://matrix.org');
|
||||
});
|
||||
|
||||
it('uses .well-known base_url when available', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
'm.homeserver': { base_url: 'https://synapse.example.com/' },
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const result = await discoverHomeserver('example.com');
|
||||
expect(result).toBe('https://synapse.example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkHomeserver', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns ok for reachable server', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
||||
|
||||
const result = await checkHomeserver('matrix.mana.how');
|
||||
expect(result).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('prepends https:// if missing', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
await checkHomeserver('matrix.mana.how');
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://matrix.mana.how/_matrix/client/versions', {
|
||||
method: 'GET',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not double-prepend https://', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
await checkHomeserver('https://matrix.mana.how');
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://matrix.mana.how/_matrix/client/versions', {
|
||||
method: 'GET',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error for non-ok response', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 502 }));
|
||||
|
||||
const result = await checkHomeserver('matrix.mana.how');
|
||||
expect(result).toEqual({ ok: false, error: 'Server returned 502' });
|
||||
});
|
||||
|
||||
it('returns error for network failure', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Failed to fetch')));
|
||||
|
||||
const result = await checkHomeserver('matrix.mana.how');
|
||||
expect(result).toEqual({ ok: false, error: 'Failed to fetch' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('loginWithToken', () => {
|
||||
it('normalizes homeserver URL', async () => {
|
||||
const result = await loginWithToken('matrix.mana.how', 'token123', '@user:matrix.mana.how');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.credentials?.homeserver).toBe('https://matrix.mana.how');
|
||||
});
|
||||
|
||||
it('removes trailing slash from homeserver', async () => {
|
||||
const result = await loginWithToken(
|
||||
'https://matrix.mana.how/',
|
||||
'token123',
|
||||
'@user:matrix.mana.how'
|
||||
);
|
||||
expect(result.credentials?.homeserver).toBe('https://matrix.mana.how');
|
||||
});
|
||||
|
||||
it('preserves provided deviceId', async () => {
|
||||
const result = await loginWithToken(
|
||||
'matrix.mana.how',
|
||||
'token123',
|
||||
'@user:matrix.mana.how',
|
||||
'MYDEVICE'
|
||||
);
|
||||
expect(result.credentials?.deviceId).toBe('MYDEVICE');
|
||||
});
|
||||
|
||||
it('generates deviceId when not provided', async () => {
|
||||
const result = await loginWithToken('matrix.mana.how', 'token123', '@user:matrix.mana.how');
|
||||
expect(result.credentials?.deviceId).toMatch(/^MANA_\d+$/);
|
||||
});
|
||||
});
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
|
||||
const AUTH_URL = import.meta.env.VITE_MANA_AUTH_URL || 'https://auth.mana.how';
|
||||
const MATRIX_HOMESERVER = import.meta.env.VITE_MATRIX_HOMESERVER || 'matrix.mana.how';
|
||||
|
||||
/**
|
||||
* Exchange session cookie for JWT token from mana-core-auth
|
||||
|
|
@ -185,7 +186,7 @@
|
|||
|
||||
if (loginToken) {
|
||||
// Exchange loginToken for Matrix credentials
|
||||
const result = await loginWithLoginToken('matrix.mana.how', loginToken);
|
||||
const result = await loginWithLoginToken(MATRIX_HOMESERVER, loginToken);
|
||||
|
||||
if (result.success && result.credentials) {
|
||||
// Remove loginToken from URL to prevent re-processing on refresh
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import SearchDialog from '$lib/components/chat/SearchDialog.svelte';
|
||||
import ForwardMessageDialog from '$lib/components/chat/ForwardMessageDialog.svelte';
|
||||
import { CallView, IncomingCallDialog } from '$lib/components/call';
|
||||
import { ChatCircle, Plus, Gear } from '@manacore/shared-icons';
|
||||
import { ChatCircle, Plus, Gear, ShieldWarning } from '@manacore/shared-icons';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -218,6 +218,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if !matrixStore.cryptoReady}
|
||||
<div
|
||||
class="flex items-center gap-2 px-4 py-2 bg-amber-500/10 border-b border-amber-500/20 text-amber-600 dark:text-amber-400 text-xs"
|
||||
>
|
||||
<ShieldWarning class="h-3.5 w-3.5 shrink-0" />
|
||||
<span>Verschlüsselung nicht verfügbar</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Room List -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<RoomList onCreateRoom={() => (showCreateRoom = true)} onSelectRoom={handleSelectRoom} />
|
||||
|
|
@ -262,6 +271,15 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{#if !matrixStore.cryptoReady}
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-1.5 bg-amber-500/10 border-b border-amber-500/20 text-amber-600 dark:text-amber-400 text-xs"
|
||||
>
|
||||
<ShieldWarning class="h-3.5 w-3.5 shrink-0" />
|
||||
<span>Verschlüsselung nicht verfügbar</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Room List -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<RoomList onCreateRoom={() => (showCreateRoom = true)} onSelectRoom={handleSelectRoom} />
|
||||
|
|
|
|||
4
apps/matrix/apps/web/src/test/mocks/app-environment.ts
Normal file
4
apps/matrix/apps/web/src/test/mocks/app-environment.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const browser = false;
|
||||
export const building = false;
|
||||
export const dev = true;
|
||||
export const version = 'test';
|
||||
16
apps/matrix/apps/web/vitest.config.ts
Normal file
16
apps/matrix/apps/web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib: resolve('./src/lib'),
|
||||
'$app/environment': resolve('./src/test/mocks/app-environment.ts'),
|
||||
},
|
||||
},
|
||||
});
|
||||
1145
pnpm-lock.yaml
generated
1145
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue