mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
fix: send the WOPI status and launch requests under the base path
Behind a reverse proxy at a sub-path (NEXT_PUBLIC_BASE_PATH, e.g.
/webmail) both WOPI requests used a bare fetch('/api/wopi/...') and
reached the mail server instead of the webmail. The status probe came
back 404, so office editing always looked unavailable there - and since
#1047 the probe runs for every opened message, logging the 404 each
time. Route both through apiFetch like every other API call, and cover
the sub-path mount with a test.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { render, renderHook, waitFor, cleanup } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useWopiStatus } from '@/hooks/use-wopi-status';
|
||||
import { WopiEditor } from '@/components/files/wopi-editor';
|
||||
|
||||
// Behind a reverse proxy at a sub-path (e.g. /webmail) a bare
|
||||
// fetch('/api/...') reaches the mail server instead of the webmail and comes
|
||||
// back 404, which left office editing permanently "unavailable" there. Both
|
||||
// WOPI requests have to carry the mount prefix.
|
||||
|
||||
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
|
||||
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.pushState({}, '', '/webmail/de/mail');
|
||||
fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404, json: async () => ({}) });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
window.history.pushState({}, '', '/');
|
||||
});
|
||||
|
||||
describe('WOPI requests under a sub-path mount', () => {
|
||||
it('probes the editor status under the mount prefix', async () => {
|
||||
renderHook(() => useWopiStatus(true));
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('/webmail/api/wopi/status');
|
||||
});
|
||||
|
||||
it('launches the editor under the mount prefix', async () => {
|
||||
render(<WopiEditor target={{ kind: 'file', id: 'f1', name: 'report.docx' }} onClose={() => {}} />);
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('/webmail/api/wopi/launch');
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Loader2, X } from "@/components/icons";
|
||||
import { getActiveAccountSlotHeaders } from "@/lib/auth/active-account-slot";
|
||||
import { apiFetch } from "@/lib/browser-navigation";
|
||||
|
||||
/** What the overlay opens: a Files node, or a mail attachment (read-only, #1047). */
|
||||
export type WopiDocument =
|
||||
@@ -55,7 +56,7 @@ export function WopiEditor({ target, accountId, slot, onClose }: WopiEditorProps
|
||||
try {
|
||||
const slotHeaders: Record<string, string> =
|
||||
typeof slot === "number" ? { "X-JMAP-Cookie-Slot": String(slot) } : getActiveAccountSlotHeaders();
|
||||
const res = await fetch("/api/wopi/launch", {
|
||||
const res = await apiFetch("/api/wopi/launch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...slotHeaders },
|
||||
body: launchBody,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IS_LITE } from "@/lib/lite";
|
||||
import { apiFetch } from "@/lib/browser-navigation";
|
||||
|
||||
/**
|
||||
* Whether a WOPI document editor is configured for this deployment and which
|
||||
@@ -22,7 +23,7 @@ function fetchWopiStatus(): Promise<WopiStatus> {
|
||||
// The WOPI bridge is a server feature; the static build never has an editor.
|
||||
if (IS_LITE) return Promise.resolve(DISABLED);
|
||||
if (!statusPromise) {
|
||||
statusPromise = fetch("/api/wopi/status")
|
||||
statusPromise = apiFetch("/api/wopi/status")
|
||||
.then((res) => (res.ok ? res.json() : DISABLED))
|
||||
.then((data) => ({
|
||||
enabled: !!data?.enabled,
|
||||
|
||||
Reference in New Issue
Block a user