Skip to content

Commit ff8b8a2

Browse files
authored
fix: Server in subdirectory (#1820)
* Reduce the number of parent directories in server URL resolution * Add tests to resolveServerUrl * Add tests to "servers/fetch-info" * Remove side-effects from renderer modules * Remove extra console.log()
1 parent c349651 commit ff8b8a2

7 files changed

Lines changed: 144 additions & 21 deletions

File tree

jest.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ module.exports = {
55
errorOnDeprecated: true,
66
runner: '@jest-runner/electron',
77
testEnvironment: '@jest-runner/electron/environment',
8-
testMatch: ['<rootDir>/src/*/!(main)/**/*.(spec|test).{js,ts,tsx}'],
8+
testMatch: [
9+
'<rootDir>/src/*/!(main)/**/*.(spec|test).{js,ts,tsx}',
10+
'<rootDir>/src/**/renderer.(spec|test).{js,ts,tsx}',
11+
],
912
globals: {
1013
'ts-jest': {
1114
tsConfig: {

src/notifications/renderer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const inferContentTypeFromImageData = (data: ArrayBuffer): string | null => {
1919
}
2020
};
2121

22-
handle('notifications/fetch-icon', async (urlHref: string) => {
22+
const fetchIcon = async (urlHref: string): Promise<string> => {
2323
if (iconCache.has(urlHref)) {
2424
return iconCache.get(urlHref);
2525
}
@@ -31,4 +31,8 @@ handle('notifications/fetch-icon', async (urlHref: string) => {
3131
const dataUri = `data:${ contentType };base64,${ base64String }`;
3232
iconCache.set(urlHref, dataUri);
3333
return dataUri;
34-
});
34+
};
35+
36+
export default (): void => {
37+
handle('notifications/fetch-icon', fetchIcon);
38+
};

src/rootWindow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ const start = async (): Promise<void> => {
1515
setupRendererErrorHandling('rootWindow');
1616
await setupI18n();
1717

18-
await Promise.all([
18+
(await Promise.all([
1919
import('./notifications/renderer'),
2020
import('./servers/renderer'),
21-
]);
21+
])).forEach((module) => module.default());
2222

2323
const container = document.getElementById('root');
2424

src/servers/main.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { convertToURL } from './main';
1+
import { ServerUrlResolutionStatus } from './common';
2+
import { convertToURL, resolveServerUrl } from './main';
23

34
describe('convertToUrl', () => {
45
it.each([
@@ -17,3 +18,38 @@ describe('convertToUrl', () => {
1718
expect(result.href).toBe(expected);
1819
});
1920
});
21+
22+
jest.mock('../ui/main/rootWindow', () => ({
23+
__esModule: true,
24+
getRootWindow: jest.fn(() => ({ webContents: null })),
25+
}));
26+
27+
jest.mock('../ipc/main', () => ({
28+
__esModule: true,
29+
invoke: jest.fn(async (_webContents, channel, ...args) => {
30+
if (channel === 'servers/fetch-info') {
31+
return [args[0], '3.8.0'];
32+
}
33+
34+
return null;
35+
}),
36+
}));
37+
38+
describe('resolveServerUrl', () => {
39+
it.each([
40+
['localhost', 'https://localhost/'],
41+
['localhost:3000', 'https://localhost:3000/'],
42+
['https://localhost', 'https://localhost/'],
43+
['http://localhost', 'http://localhost/'],
44+
['https://localhost/', 'https://localhost/'],
45+
['http://localhost/', 'http://localhost/'],
46+
['https://localhost/subdir', 'https://localhost/subdir/'],
47+
['http://localhost:80', 'http://localhost/'],
48+
['https://localhost:443', 'https://localhost/'],
49+
])('resolves %s as %s', async (input, expected) => {
50+
const [serverUrl, status, error] = await resolveServerUrl(input);
51+
expect(serverUrl).toBe(expected);
52+
expect(status).toBe(ServerUrlResolutionStatus.OK);
53+
expect(error).toBe(undefined);
54+
});
55+
});

src/servers/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const resolveServerUrl = async (input: string): Promise<ServerUrlResoluti
6363
return resolveServerUrl(`https://${ input }.rocket.chat`);
6464
}
6565

66-
if (error.name === 'AbortError') {
66+
if (error?.name === 'AbortError') {
6767
return [url.href, ServerUrlResolutionStatus.TIMEOUT, error];
6868
}
6969

src/servers/renderer.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createServer, Server } from 'http';
2+
3+
import { fetchInfo } from './renderer';
4+
5+
describe('servers/fetch-info', () => {
6+
const serverVersion = Array.from({ length: 3 }, () => Math.round(Math.random() * 9)).join('.');
7+
let server: Server;
8+
9+
beforeEach(() => {
10+
server = createServer((req, res) => {
11+
if (req.url === '/' || /^(\/subdir)+\/$/.test(req.url)) {
12+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
13+
res.write('Home');
14+
res.end();
15+
return;
16+
}
17+
18+
if (req.url === '/api/info' || /^(\/subdir)+\/api\/info$/.test(req.url)) {
19+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
20+
res.write(JSON.stringify({
21+
success: true,
22+
version: serverVersion,
23+
}));
24+
res.end();
25+
return;
26+
}
27+
28+
if (req.url === '/redirect') {
29+
res.writeHead(302, { Location: 'http://localhost:3000/subdir/' });
30+
res.end();
31+
return;
32+
}
33+
34+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
35+
res.write('Not found!');
36+
res.end();
37+
});
38+
39+
server.listen(3000);
40+
});
41+
42+
afterEach(() => {
43+
server.close();
44+
server = null;
45+
});
46+
47+
it('reaches the server at root directory', async () => {
48+
const [effectiveUrl, version] = await fetchInfo('http://localhost:3000/');
49+
expect(effectiveUrl).toStrictEqual('http://localhost:3000/');
50+
expect(version).toStrictEqual(serverVersion);
51+
});
52+
53+
it('reaches the server at subdirectory', async () => {
54+
const [effectiveUrl, version] = await fetchInfo('http://localhost:3000/subdir/');
55+
expect(effectiveUrl).toStrictEqual('http://localhost:3000/subdir/');
56+
expect(version).toStrictEqual(serverVersion);
57+
});
58+
59+
it('reaches the server at deep subdirectory', async () => {
60+
const [effectiveUrl, version] = await fetchInfo('http://localhost:3000/subdir/subdir/subdir/');
61+
expect(effectiveUrl).toStrictEqual('http://localhost:3000/subdir/subdir/subdir/');
62+
expect(version).toStrictEqual(serverVersion);
63+
});
64+
65+
it('reaches the server after redirection', async () => {
66+
const [effectiveUrl, version] = await fetchInfo('http://localhost:3000/redirect');
67+
expect(effectiveUrl).toStrictEqual('http://localhost:3000/subdir/');
68+
expect(version).toStrictEqual(serverVersion);
69+
});
70+
});

src/servers/renderer.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { dispatch, watch } from '../store';
55
import { RootState } from '../store/rootReducer';
66
import { ROOT_WINDOW_ICON_CHANGED } from '../ui/actions';
77

8-
handle('servers/fetch-info', async (urlHref): Promise<[urlHref: string, version: string]> => {
8+
export const fetchInfo = async (urlHref: string): Promise<[urlHref: string, version: string]> => {
99
const url = new URL(urlHref);
1010

1111
const { username, password } = url;
@@ -15,25 +15,31 @@ handle('servers/fetch-info', async (urlHref): Promise<[urlHref: string, version:
1515
headers.append('Authorization', `Basic ${ btoa(`${ username }:${ password }`) }`);
1616
}
1717

18-
const endpoint = new URL('api/info', url);
18+
const homeResponse = await fetch(url.href, { headers });
1919

20-
const response = await fetch(endpoint.href, { headers });
20+
if (!homeResponse.ok) {
21+
throw new Error(homeResponse.statusText);
22+
}
23+
24+
const endpoint = new URL('api/info', homeResponse.url);
25+
26+
const apiInfoResponse = await fetch(endpoint.href, { headers });
2127

22-
if (!response.ok) {
23-
throw new Error(response.statusText);
28+
if (!apiInfoResponse.ok) {
29+
throw new Error(apiInfoResponse.statusText);
2430
}
2531

2632
const responseBody: {
2733
success: boolean;
2834
version: string;
29-
} = await response.json();
35+
} = await apiInfoResponse.json();
3036

3137
if (!responseBody.success) {
3238
throw new Error();
3339
}
3440

35-
return [new URL('../..', response.url).href, responseBody.version];
36-
});
41+
return [new URL('..', apiInfoResponse.url).href, responseBody.version];
42+
};
3743

3844
type RootWindowIconParams = {
3945
badge: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '+9' | '•' | undefined;
@@ -240,10 +246,14 @@ const updateRootWindowIconForWindows = async ({ badge, favicon }: RootWindowIcon
240246
});
241247
};
242248

243-
if (process.platform === 'linux') {
244-
watch(selectBadgeAndFavicon, updateRootWindowIconForLinux);
245-
}
249+
export default (): void => {
250+
handle('servers/fetch-info', fetchInfo);
246251

247-
if (process.platform === 'win32') {
248-
watch(selectBadgeAndFavicon, updateRootWindowIconForWindows);
249-
}
252+
if (process.platform === 'linux') {
253+
watch(selectBadgeAndFavicon, updateRootWindowIconForLinux);
254+
}
255+
256+
if (process.platform === 'win32') {
257+
watch(selectBadgeAndFavicon, updateRootWindowIconForWindows);
258+
}
259+
};

0 commit comments

Comments
 (0)