文件
mynode/packages/app-desktop/gui/MainScreen/handleCallbackUrl.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

79 行
2.9 KiB
TypeScript

import { utils as commandUtils } from '@joplin/lib/services/CommandService';
import executeCallbackUrl from './handleCallbackUrl';
import { withWarningSilenced } from '@joplin/lib/testing/test-utils';
const mockOpenExternal = jest.fn();
jest.mock('../../services/bridge', () => ({
__esModule: true,
default: () => ({
openExternal: (url: string) => mockOpenExternal(url),
}),
}));
const setSelectedNoteId = (noteId: string | null) => {
// A null id makes handleGetCurrentNote take its x-error branch, which reaches
// respond() without touching the database.
commandUtils.store = {
getState: () => ({ selectedNoteIds: noteId ? [noteId] : [] }),
};
};
describe('handleCallbackUrl', () => {
beforeEach(() => {
mockOpenExternal.mockClear();
setSelectedNoteId(null);
});
test.each([
'ms-msdt:/id PCWDiagnostic',
'search-ms:query=secret',
'shell:startup',
'file:///etc/passwd',
'javascript:alert(1)',
'data:text/html,<script>alert(1)</script>',
'mailto:someone@example.com',
'http://example.com/cb',
'https://example.com/cb',
'unknown-app://attacker.example.com/cb',
'unknown-app:///hostless',
'not a url',
])('should not dispatch an unrecognised callback target (%s)', async (target) => {
await withWarningSilenced(/Rejected malformed callback|Rejected callback target with host/, async () => {
await executeCallbackUrl(`joplin://x-callback-url/getCurrentNote?x-error=${encodeURIComponent(target)}`);
}, { requireWarning: true });
expect(mockOpenExternal).not.toHaveBeenCalled();
});
test.each([
'hook://x-callback-url/setCurrentNode',
'drafts://x-callback-url/create',
'bear://x-callback-url/open-note',
'ulysses://x-callback-url/new-sheet',
'new-app://x-callback-url/cb',
'editorial://workflow-callback',
'things:///add',
])('should dispatch a recognised callback target (%s)', async (target) => {
await executeCallbackUrl(`joplin://x-callback-url/getCurrentNote?x-error=${encodeURIComponent(target)}`);
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
expect(mockOpenExternal.mock.calls[0][0]).toContain(`${target}?`);
});
it('should not leak the raw error message on the error path', async () => {
// Force a throw inside the try block so the catch path builds the response.
commandUtils.store = {
getState: () => { throw new Error('secret path /home/victim/.config/joplin/database.sqlite'); },
};
await withWarningSilenced(/Error handling callback URL command "getCurrentNote":.*secret path \/home\/victim/, async () => {
await executeCallbackUrl(`joplin://x-callback-url/getCurrentNote?x-error=${encodeURIComponent('hook://x-callback-url/cb')}`);
}, { requireWarning: true });
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
const responseUrl = mockOpenExternal.mock.calls[0][0];
expect(responseUrl).not.toContain('secret path');
expect(responseUrl).not.toContain('database.sqlite');
expect(new URL(responseUrl).searchParams.get('errorMessage')).toBe('The command could not be completed');
});
});