mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
Review of the notification actions. With a plugin that may stop a move or read change, or a silent tab holding the login, the message now opens instead of the route acting. The route answers 401 under a session limit, and neither path touches a message since moved to Trash or Junk. A tab that thawed declines an old click, Archive follows the gate of the notification's login and files into a shared-out Archive as the app's button does, Reply opens a composer in the Pro shell, the route retries once when another client made the folder first and leaves no empty folder behind, and logins match however the server address is written. Two assertions of this feature's own tests change: the route answers 401, not 403, when a login's scopes can't be worked out, since that now also covers session limits; a tab's message to the page now carries `at`. The Pro shell's mail surface now reads a parked link's query from the handoff: the address bar may have none by its first render, which lost `?slot=` and `?owner=` there too.
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { sanitizeApiPostPaths } from '@/lib/extension-origins';
|
|
|
|
vi.mock('@/stores/toast-store', () => ({
|
|
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
|
|
}));
|
|
|
|
// host-api pulls in the whole store layer; only the pure gate is used here.
|
|
import { isPluginPostDenied } from '@/lib/plugin-sandbox/host-api';
|
|
|
|
// A plugin with `http:post` may POST to the same-origin routes its manifest
|
|
// lists, with the user's cookies. The denylist kept it off the passthrough,
|
|
// admin and sign-in routes but not off the plugin proxy and secrets routes:
|
|
// `/api/plugin-proxy?plugin=<other>&credential=<id>` injects another
|
|
// plugin's connected account or admin key into a request and hands the
|
|
// answer back, and `/api/plugin-secrets` re-points or deletes them. The
|
|
// agenda, fetch-ical and translate routes gave data and reach the plugin's
|
|
// own permissions would not. A manifest entry of the bare `/api/` prefix
|
|
// also passed validation and allowed every route at once.
|
|
|
|
describe('routes no plugin may POST to', () => {
|
|
it.each([
|
|
'/api/plugin-proxy',
|
|
'/api/plugin-secrets',
|
|
'/api/calendar-agenda',
|
|
'/api/fetch-ical',
|
|
'/api/translate',
|
|
'/api/account-archive',
|
|
'/api/%70lugin-proxy',
|
|
'/api//plugin-secrets',
|
|
'/api/account/stalwart/jmap',
|
|
'/api/push/action',
|
|
])('%s is denied', (path) => {
|
|
expect(isPluginPostDenied(path)).toBe(true);
|
|
});
|
|
|
|
it('leaves a plugin sidecar route alone', () => {
|
|
expect(isPluginPostDenied('/api/my-plugin/webhook')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('apiPostPaths manifest entries', () => {
|
|
it('rejects the bare API prefix and keeps named routes', () => {
|
|
expect(sanitizeApiPostPaths(['/api/', '/api//x', '/api/my-plugin/', '/api/my-plugin/hook'])).toEqual([
|
|
'/api/my-plugin/',
|
|
'/api/my-plugin/hook',
|
|
]);
|
|
});
|
|
});
|