Skip to content

Commit 2ee7270

Browse files
authored
Merge pull request #314 from SableClient/fix/matrix-to-custom-base
fix: support custom matrix.to base URL via config
2 parents cfe4e33 + f65f126 commit 2ee7270

6 files changed

Lines changed: 397 additions & 22 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Support `matrixToBaseUrl` in `config.json` to override the default `matrix.to` link base URL.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Integration tests: exercises the full config-load → setMatrixToBase → URL
3+
* generation pipeline that App.tsx runs on startup.
4+
*
5+
* The pattern under test mirrors App.tsx:
6+
* <ClientConfigLoader>
7+
* {(config) => { setMatrixToBase(config.matrixToBaseUrl); ... }}
8+
* </ClientConfigLoader>
9+
*
10+
* We mock fetch so we don't need a real config.json or a live matrix.to instance.
11+
*/
12+
import { describe, it, expect, vi, afterEach } from 'vitest';
13+
import { render, screen, waitFor } from '@testing-library/react';
14+
import { setMatrixToBase, getMatrixToRoom, getMatrixToUser } from '$plugins/matrix-to';
15+
import { ClientConfigLoader } from './ClientConfigLoader';
16+
17+
afterEach(() => {
18+
setMatrixToBase(); // reset module state to 'https://matrix.to'
19+
vi.unstubAllGlobals();
20+
});
21+
22+
const mockFetch = (config: object) =>
23+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve(config) }));
24+
25+
describe('ClientConfigLoader + matrix-to wiring', () => {
26+
it('generates a standard matrix.to URL when no custom base is configured', async () => {
27+
mockFetch({});
28+
29+
render(
30+
<ClientConfigLoader>
31+
{(config) => {
32+
setMatrixToBase(config.matrixToBaseUrl);
33+
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
34+
}}
35+
</ClientConfigLoader>
36+
);
37+
38+
await waitFor(() =>
39+
expect(screen.getByTestId('link')).toHaveTextContent('https://matrix.to/#/!room:example.com')
40+
);
41+
});
42+
43+
it('generates a custom-base URL for rooms when matrixToBaseUrl is set', async () => {
44+
mockFetch({ matrixToBaseUrl: 'https://custom.example.org' });
45+
46+
render(
47+
<ClientConfigLoader>
48+
{(config) => {
49+
setMatrixToBase(config.matrixToBaseUrl);
50+
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
51+
}}
52+
</ClientConfigLoader>
53+
);
54+
55+
await waitFor(() =>
56+
expect(screen.getByTestId('link')).toHaveTextContent(
57+
'https://custom.example.org/#/!room:example.com'
58+
)
59+
);
60+
});
61+
62+
it('generates a custom-base URL for users when matrixToBaseUrl is set', async () => {
63+
mockFetch({ matrixToBaseUrl: 'https://custom.example.org' });
64+
65+
render(
66+
<ClientConfigLoader>
67+
{(config) => {
68+
setMatrixToBase(config.matrixToBaseUrl);
69+
return <span data-testid="user">{getMatrixToUser('@alice:example.com')}</span>;
70+
}}
71+
</ClientConfigLoader>
72+
);
73+
74+
await waitFor(() =>
75+
expect(screen.getByTestId('user')).toHaveTextContent(
76+
'https://custom.example.org/#/@alice:example.com'
77+
)
78+
);
79+
});
80+
81+
it('strips a trailing slash from matrixToBaseUrl', async () => {
82+
mockFetch({ matrixToBaseUrl: 'https://custom.example.org/' });
83+
84+
render(
85+
<ClientConfigLoader>
86+
{(config) => {
87+
setMatrixToBase(config.matrixToBaseUrl);
88+
return <span data-testid="link">{getMatrixToRoom('!room:example.com')}</span>;
89+
}}
90+
</ClientConfigLoader>
91+
);
92+
93+
await waitFor(() =>
94+
expect(screen.getByTestId('link')).toHaveTextContent(
95+
'https://custom.example.org/#/!room:example.com'
96+
)
97+
);
98+
});
99+
});

src/app/hooks/useClientConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export type ClientConfig = {
4040
};
4141

4242
hashRouter?: HashRouterConfig;
43+
44+
matrixToBaseUrl?: string;
4345
};
4446

4547
const ClientConfigContext = createContext<ClientConfig | null>(null);

src/app/pages/App.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ErrorBoundary } from 'react-error-boundary';
77

88
import { ClientConfigLoader } from '$components/ClientConfigLoader';
99
import { ClientConfigProvider } from '$hooks/useClientConfig';
10+
import { setMatrixToBase } from '$plugins/matrix-to';
1011
import { ScreenSizeProvider, useScreenSize } from '$hooks/useScreenSize';
1112
import { useCompositionEndTracking } from '$hooks/useComposingCheck';
1213
import { ErrorPage } from '$components/DefaultErrorPage';
@@ -35,16 +36,19 @@ function App() {
3536
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
3637
)}
3738
>
38-
{(clientConfig) => (
39-
<ClientConfigProvider value={clientConfig}>
40-
<QueryClientProvider client={queryClient}>
41-
<JotaiProvider>
42-
<RouterProvider router={createRouter(clientConfig, screenSize)} />
43-
</JotaiProvider>
44-
<ReactQueryDevtools initialIsOpen={false} />
45-
</QueryClientProvider>
46-
</ClientConfigProvider>
47-
)}
39+
{(clientConfig) => {
40+
setMatrixToBase(clientConfig.matrixToBaseUrl);
41+
return (
42+
<ClientConfigProvider value={clientConfig}>
43+
<QueryClientProvider client={queryClient}>
44+
<JotaiProvider>
45+
<RouterProvider router={createRouter(clientConfig, screenSize)} />
46+
</JotaiProvider>
47+
<ReactQueryDevtools initialIsOpen={false} />
48+
</QueryClientProvider>
49+
</ClientConfigProvider>
50+
);
51+
}}
4852
</ClientConfigLoader>
4953
</FeatureCheck>
5054
</ScreenSizeProvider>

src/app/plugins/matrix-to.test.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import {
3+
getMatrixToRoom,
4+
getMatrixToRoomEvent,
5+
getMatrixToUser,
6+
parseMatrixToRoom,
7+
parseMatrixToRoomEvent,
8+
parseMatrixToUser,
9+
setMatrixToBase,
10+
testMatrixTo,
11+
} from './matrix-to';
12+
13+
// Reset to default after each test so state doesn't leak between tests.
14+
afterEach(() => {
15+
setMatrixToBase(undefined);
16+
});
17+
18+
// ---------------------------------------------------------------------------
19+
// Link generation
20+
// ---------------------------------------------------------------------------
21+
22+
describe('getMatrixToUser', () => {
23+
it('generates a standard matrix.to user link', () => {
24+
expect(getMatrixToUser('@alice:example.com')).toBe('https://matrix.to/#/@alice:example.com');
25+
});
26+
27+
it('uses custom base when configured', () => {
28+
setMatrixToBase('https://matrix.example.org');
29+
expect(getMatrixToUser('@alice:example.com')).toBe(
30+
'https://matrix.example.org/#/@alice:example.com'
31+
);
32+
});
33+
34+
it('strips trailing slash from custom base', () => {
35+
setMatrixToBase('https://matrix.example.org/');
36+
expect(getMatrixToUser('@alice:example.com')).toBe(
37+
'https://matrix.example.org/#/@alice:example.com'
38+
);
39+
});
40+
});
41+
42+
describe('getMatrixToRoom', () => {
43+
it('generates a standard matrix.to room link', () => {
44+
expect(getMatrixToRoom('!room:example.com')).toBe('https://matrix.to/#/!room:example.com');
45+
});
46+
47+
it('appends via servers', () => {
48+
expect(getMatrixToRoom('!room:example.com', ['s1.org', 's2.org'])).toBe(
49+
'https://matrix.to/#/!room:example.com?via=s1.org&via=s2.org'
50+
);
51+
});
52+
53+
it('uses custom base when configured', () => {
54+
setMatrixToBase('https://matrix.example.org');
55+
expect(getMatrixToRoom('#general:example.com')).toBe(
56+
'https://matrix.example.org/#/#general:example.com'
57+
);
58+
});
59+
});
60+
61+
describe('getMatrixToRoomEvent', () => {
62+
it('generates a standard matrix.to event link', () => {
63+
expect(getMatrixToRoomEvent('!room:example.com', '$event123')).toBe(
64+
'https://matrix.to/#/!room:example.com/$event123'
65+
);
66+
});
67+
68+
it('appends via servers', () => {
69+
expect(getMatrixToRoomEvent('!room:example.com', '$event123', ['s1.org'])).toBe(
70+
'https://matrix.to/#/!room:example.com/$event123?via=s1.org'
71+
);
72+
});
73+
74+
it('uses custom base when configured', () => {
75+
setMatrixToBase('https://matrix.example.org');
76+
expect(getMatrixToRoomEvent('!room:example.com', '$event123')).toBe(
77+
'https://matrix.example.org/#/!room:example.com/$event123'
78+
);
79+
});
80+
});
81+
82+
// ---------------------------------------------------------------------------
83+
// testMatrixTo
84+
// ---------------------------------------------------------------------------
85+
86+
describe('testMatrixTo', () => {
87+
it('matches standard matrix.to URLs', () => {
88+
expect(testMatrixTo('https://matrix.to/#/@alice:example.com')).toBe(true);
89+
expect(testMatrixTo('https://matrix.to/#/!room:example.com')).toBe(true);
90+
expect(testMatrixTo('https://matrix.to/#/!room:example.com/$event')).toBe(true);
91+
expect(testMatrixTo('http://matrix.to/#/@alice:example.com')).toBe(true);
92+
});
93+
94+
it('rejects non-matrix.to URLs', () => {
95+
expect(testMatrixTo('https://example.com')).toBe(false);
96+
expect(testMatrixTo('https://notmatrix.to/#/@alice:example.com')).toBe(false);
97+
});
98+
99+
it('matches custom base URLs after setMatrixToBase', () => {
100+
setMatrixToBase('https://matrix.example.org');
101+
expect(testMatrixTo('https://matrix.example.org/#/@alice:example.com')).toBe(true);
102+
});
103+
104+
it('still matches standard matrix.to after setMatrixToBase (cross-client compat)', () => {
105+
setMatrixToBase('https://matrix.example.org');
106+
expect(testMatrixTo('https://matrix.to/#/@alice:example.com')).toBe(true);
107+
});
108+
});
109+
110+
// ---------------------------------------------------------------------------
111+
// parseMatrixToUser
112+
// ---------------------------------------------------------------------------
113+
114+
describe('parseMatrixToUser', () => {
115+
it('parses a standard matrix.to user link', () => {
116+
expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe('@alice:example.com');
117+
});
118+
119+
it('returns undefined for non-user links', () => {
120+
expect(parseMatrixToUser('https://matrix.to/#/!room:example.com')).toBeUndefined();
121+
});
122+
123+
it('parses user links from custom base', () => {
124+
setMatrixToBase('https://matrix.example.org');
125+
expect(parseMatrixToUser('https://matrix.example.org/#/@alice:example.com')).toBe(
126+
'@alice:example.com'
127+
);
128+
});
129+
130+
it('parses standard matrix.to user links even after custom base is set', () => {
131+
setMatrixToBase('https://matrix.example.org');
132+
expect(parseMatrixToUser('https://matrix.to/#/@alice:example.com')).toBe('@alice:example.com');
133+
});
134+
});
135+
136+
// ---------------------------------------------------------------------------
137+
// parseMatrixToRoom
138+
// ---------------------------------------------------------------------------
139+
140+
describe('parseMatrixToRoom', () => {
141+
it('parses a room ID link', () => {
142+
expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com')).toEqual({
143+
roomIdOrAlias: '!room:example.com',
144+
viaServers: undefined,
145+
});
146+
});
147+
148+
it('parses a room alias link', () => {
149+
expect(parseMatrixToRoom('https://matrix.to/#/#general:example.com')).toEqual({
150+
roomIdOrAlias: '#general:example.com',
151+
viaServers: undefined,
152+
});
153+
});
154+
155+
it('parses via servers', () => {
156+
expect(
157+
parseMatrixToRoom('https://matrix.to/#/!room:example.com?via=s1.org&via=s2.org')
158+
).toEqual({
159+
roomIdOrAlias: '!room:example.com',
160+
viaServers: ['s1.org', 's2.org'],
161+
});
162+
});
163+
164+
it('returns undefined for event links (too many segments)', () => {
165+
expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com/$event123')).toBeUndefined();
166+
});
167+
168+
it('parses room links from custom base', () => {
169+
setMatrixToBase('https://matrix.example.org');
170+
expect(parseMatrixToRoom('https://matrix.example.org/#/!room:example.com')).toEqual({
171+
roomIdOrAlias: '!room:example.com',
172+
viaServers: undefined,
173+
});
174+
});
175+
176+
it('still parses standard matrix.to room links after custom base is set', () => {
177+
setMatrixToBase('https://matrix.example.org');
178+
expect(parseMatrixToRoom('https://matrix.to/#/!room:example.com')).toEqual({
179+
roomIdOrAlias: '!room:example.com',
180+
viaServers: undefined,
181+
});
182+
});
183+
});
184+
185+
// ---------------------------------------------------------------------------
186+
// parseMatrixToRoomEvent
187+
// ---------------------------------------------------------------------------
188+
189+
describe('parseMatrixToRoomEvent', () => {
190+
it('parses a room event link', () => {
191+
expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123')).toEqual({
192+
roomIdOrAlias: '!room:example.com',
193+
eventId: '$event123',
194+
viaServers: undefined,
195+
});
196+
});
197+
198+
it('parses via servers', () => {
199+
expect(
200+
parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123?via=s1.org')
201+
).toEqual({
202+
roomIdOrAlias: '!room:example.com',
203+
eventId: '$event123',
204+
viaServers: ['s1.org'],
205+
});
206+
});
207+
208+
it('returns undefined for room-only links', () => {
209+
expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com')).toBeUndefined();
210+
});
211+
212+
it('parses event links from custom base', () => {
213+
setMatrixToBase('https://matrix.example.org');
214+
expect(
215+
parseMatrixToRoomEvent('https://matrix.example.org/#/!room:example.com/$event123')
216+
).toEqual({
217+
roomIdOrAlias: '!room:example.com',
218+
eventId: '$event123',
219+
viaServers: undefined,
220+
});
221+
});
222+
223+
it('still parses standard matrix.to event links after custom base is set', () => {
224+
setMatrixToBase('https://matrix.example.org');
225+
expect(parseMatrixToRoomEvent('https://matrix.to/#/!room:example.com/$event123')).toEqual({
226+
roomIdOrAlias: '!room:example.com',
227+
eventId: '$event123',
228+
viaServers: undefined,
229+
});
230+
});
231+
});

0 commit comments

Comments
 (0)