test: better sync coverage

This commit is contained in:
zhom
2026-08-26 02:13:00 +04:00
parent e873a72387
commit abe210eda3
23 changed files with 582 additions and 84 deletions
+52 -31
View File
@@ -26,7 +26,7 @@ import {
} from "@/components/ui/tooltip";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { SyncSettings } from "@/types";
import type { SyncServerCheck, SyncSettings } from "@/types";
const DEVICE_LINK_URL = "https://donutbrowser.com/auth/link";
@@ -78,49 +78,51 @@ export function SyncConfigDialog({
const [, setLiveProxyUsage] = useState<ProxyUsage | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "testing" | "connected" | "error"
"unknown" | "testing" | "connected" | "error" | "storage-unreachable"
>("unknown");
const [storageEndpoint, setStorageEndpoint] = useState<string | null>(null);
const hasConfig = Boolean(serverUrl && token);
// `/health` is a bare liveness probe: it answers ok on a server whose storage
// is unreachable or misconfigured, which is how a green "connected" could sit
// next to a sync where every single file failed. `/readyz` checks storage and
// reports the endpoint clients are handed in presigned URLs, so surface that
// too — when transfers fail, it is the value worth checking first.
const probeServer = useCallback(async (url: string) => {
const base = url.replace(/\/$/, "");
const response = await fetch(`${base}/readyz`);
// Probing the sync server alone is what let a broken setup look correct.
// Files never travel through that server: the client is handed a presigned
// URL and uploads straight to storage, so a server whose storage address is
// reachable only from its own network answers every probe while every single
// transfer fails at connect. The check runs in the backend because that is
// the client that performs the transfers — same DNS, proxy and TLS trust, so
// a setup that passes here can actually move bytes.
const probeServer = useCallback(
(url: string) =>
invoke<SyncServerCheck>("check_sync_server_connection", {
serverUrl: url,
}),
[],
);
// A server old enough to predate /readyz is still a working server, so
// fall back rather than reporting a healthy setup as broken.
if (response.status === 404) {
const health = await fetch(`${base}/health`);
return { ok: health.ok, storageEndpoint: undefined };
const applyProbeResult = useCallback((result: SyncServerCheck) => {
setStorageEndpoint(result.storage_endpoint ?? null);
if (!result.server_reachable || result.storage_ready === false) {
setConnectionStatus("error");
return "error" as const;
}
if (!response.ok) {
return { ok: false as const, storageEndpoint: undefined };
if (result.storage_reachable === false) {
setConnectionStatus("storage-unreachable");
return "storage-unreachable" as const;
}
const body = (await response.json()) as {
storageEndpoint?: string;
} | null;
return { ok: true as const, storageEndpoint: body?.storageEndpoint };
setConnectionStatus("connected");
return "connected" as const;
}, []);
const testConnection = useCallback(
async (url: string) => {
setConnectionStatus("testing");
try {
const result = await probeServer(url);
setStorageEndpoint(result.storageEndpoint ?? null);
setConnectionStatus(result.ok ? "connected" : "error");
applyProbeResult(await probeServer(url));
} catch {
setStorageEndpoint(null);
setConnectionStatus("error");
}
},
[probeServer],
[probeServer, applyProbeResult],
);
const loadSettings = useCallback(async () => {
@@ -173,12 +175,18 @@ export function SyncConfigDialog({
setConnectionStatus("testing");
try {
const result = await probeServer(serverUrl);
setStorageEndpoint(result.storageEndpoint ?? null);
if (result.ok) {
setConnectionStatus("connected");
const outcome = applyProbeResult(result);
if (outcome === "connected") {
showSuccessToast(t("sync.config.connectionSuccess"));
} else if (outcome === "storage-unreachable") {
// Deliberately an error, not a warning. Nothing will sync in this
// state, and reporting it as success is the bug being fixed.
showErrorToast(
t("sync.config.storageUnreachable", {
endpoint: result.storage_endpoint ?? "",
}),
);
} else {
setConnectionStatus("error");
showErrorToast(t("sync.config.serverError"));
}
} catch {
@@ -188,7 +196,7 @@ export function SyncConfigDialog({
} finally {
setIsTesting(false);
}
}, [serverUrl, t, probeServer]);
}, [serverUrl, t, probeServer, applyProbeResult]);
const handleSave = useCallback(async () => {
setIsSaving(true);
@@ -485,6 +493,19 @@ export function SyncConfigDialog({
)}
</div>
)}
{connectionStatus === "storage-unreachable" && (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-2 rounded-full bg-destructive" />
{t("sync.config.storageUnreachableStatus")}
</div>
<span className="text-xs text-muted-foreground break-all">
{t("sync.config.storageUnreachable", {
endpoint: storageEndpoint ?? "",
})}
</span>
</div>
)}
{connectionStatus === "error" && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-2 rounded-full bg-destructive" />
+3 -1
View File
@@ -636,6 +636,8 @@
"serverError": "Server responded with an error",
"connectFailed": "Failed to connect to server",
"storageEndpoint": "Storage: {{endpoint}}",
"storageUnreachableStatus": "Storage unreachable",
"storageUnreachable": "The server is reachable, but its storage address {{endpoint}} cannot be reached from this device. File transfers will fail. If you self-host, set S3_PUBLIC_ENDPOINT to an address this device can reach.",
"settingsSaved": "Sync settings saved",
"saveFailed": "Failed to save settings",
"disconnected": "Sync disconnected",
@@ -1983,7 +1985,7 @@
"importSourceBrowserRunning": "Close {{browser}} first, or choose to import anyway",
"wayfernFingerprintApplyFailed": "Could not apply this profile's fingerprint, so the browser was not started. {{detail}}",
"wayfernFingerprintGenerationFailed": "Could not create a fingerprint for this profile. {{detail}}",
"wayfernGenerationLimitReached": "The fingerprint generation limit for this account has been reached. New fingerprints are unavailable for up to 24 hours. This limit applies to this computer, not to one profile.",
"wayfernGenerationLimitReached": "The fingerprint generation limit for this account has been reached. Your existing profiles will keep launching normally — only new fingerprints are paused, and they become available again a little later.",
"wayfernCrossOsRequiresPlan": "This profile claims {{detail}}, which needs a paid plan and an active sign-in. Sign in or switch the profile to your own operating system."
},
"rail": {
+2
View File
@@ -637,6 +637,8 @@
"serverError": "El servidor respondió con un error",
"connectFailed": "Error al conectar con el servidor",
"storageEndpoint": "Almacenamiento: {{endpoint}}",
"storageUnreachableStatus": "Almacenamiento inaccesible",
"storageUnreachable": "Se puede acceder al servidor, pero no a su dirección de almacenamiento {{endpoint}} desde este dispositivo. Las transferencias de archivos fallarán. Si usas un servidor propio, configura S3_PUBLIC_ENDPOINT con una dirección accesible desde este dispositivo.",
"settingsSaved": "Ajustes de sincronización guardados",
"saveFailed": "Error al guardar los ajustes",
"disconnected": "Sincronización desconectada",
+2
View File
@@ -637,6 +637,8 @@
"serverError": "Le serveur a répondu avec une erreur",
"connectFailed": "Échec de la connexion au serveur",
"storageEndpoint": "Stockage : {{endpoint}}",
"storageUnreachableStatus": "Stockage inaccessible",
"storageUnreachable": "Le serveur est accessible, mais son adresse de stockage {{endpoint}} est inaccessible depuis cet appareil. Les transferts de fichiers échoueront. Si vous l'hébergez vous-même, définissez S3_PUBLIC_ENDPOINT sur une adresse accessible depuis cet appareil.",
"settingsSaved": "Paramètres de synchronisation enregistrés",
"saveFailed": "Échec de lenregistrement des paramètres",
"disconnected": "Synchronisation déconnectée",
+2
View File
@@ -636,6 +636,8 @@
"serverError": "サーバーがエラーで応答しました",
"connectFailed": "サーバーへの接続に失敗しました",
"storageEndpoint": "ストレージ: {{endpoint}}",
"storageUnreachableStatus": "ストレージに接続できません",
"storageUnreachable": "サーバーには接続できますが、ストレージのアドレス {{endpoint}} にこのデバイスから接続できません。ファイル転送は失敗します。セルフホストの場合は、S3_PUBLIC_ENDPOINT にこのデバイスから接続できるアドレスを設定してください。",
"settingsSaved": "同期設定を保存しました",
"saveFailed": "設定の保存に失敗しました",
"disconnected": "同期を切断しました",
+2
View File
@@ -636,6 +636,8 @@
"serverError": "서버가 오류로 응답했습니다",
"connectFailed": "서버에 연결하지 못했습니다",
"storageEndpoint": "스토리지: {{endpoint}}",
"storageUnreachableStatus": "스토리지에 연결할 수 없음",
"storageUnreachable": "서버에는 연결되지만 스토리지 주소 {{endpoint}}에 이 기기에서 연결할 수 없습니다. 파일 전송이 실패합니다. 자체 호스팅 중이라면 S3_PUBLIC_ENDPOINT를 이 기기에서 연결할 수 있는 주소로 설정하세요.",
"settingsSaved": "동기화 설정이 저장되었습니다",
"saveFailed": "설정 저장 실패",
"disconnected": "동기화 연결 끊김",
+2
View File
@@ -637,6 +637,8 @@
"serverError": "O servidor respondeu com um erro",
"connectFailed": "Falha ao conectar ao servidor",
"storageEndpoint": "Armazenamento: {{endpoint}}",
"storageUnreachableStatus": "Armazenamento inacessível",
"storageUnreachable": "O servidor está acessível, mas o endereço de armazenamento {{endpoint}} não pode ser acessado deste dispositivo. As transferências de arquivos vão falhar. Se você hospeda o servidor, defina S3_PUBLIC_ENDPOINT com um endereço acessível deste dispositivo.",
"settingsSaved": "Configurações de sincronização salvas",
"saveFailed": "Falha ao salvar as configurações",
"disconnected": "Sincronização desconectada",
+2
View File
@@ -638,6 +638,8 @@
"serverError": "Сервер вернул ошибку",
"connectFailed": "Не удалось подключиться к серверу",
"storageEndpoint": "Хранилище: {{endpoint}}",
"storageUnreachableStatus": "Хранилище недоступно",
"storageUnreachable": "Сервер доступен, но адрес хранилища {{endpoint}} недоступен с этого устройства. Передача файлов работать не будет. Если вы используете собственный сервер, укажите в S3_PUBLIC_ENDPOINT адрес, доступный с этого устройства.",
"settingsSaved": "Настройки синхронизации сохранены",
"saveFailed": "Не удалось сохранить настройки",
"disconnected": "Синхронизация отключена",
+2
View File
@@ -636,6 +636,8 @@
"serverError": "Sunucu bir hatayla yanıt verdi",
"connectFailed": "Sunucuya bağlanılamadı",
"storageEndpoint": "Depolama: {{endpoint}}",
"storageUnreachableStatus": "Depolamaya erişilemiyor",
"storageUnreachable": "Sunucuya erişilebiliyor, ancak depolama adresine {{endpoint}} bu cihazdan erişilemiyor. Dosya aktarımları başarısız olacak. Kendi sunucunuzu barındırıyorsanız S3_PUBLIC_ENDPOINT değerini bu cihazdan erişilebilen bir adres olarak ayarlayın.",
"settingsSaved": "Eşitleme ayarları kaydedildi",
"saveFailed": "Ayarlar kaydedilemedi",
"disconnected": "Eşitleme bağlantısı kesildi",
+2
View File
@@ -636,6 +636,8 @@
"serverError": "Máy chủ trả về lỗi",
"connectFailed": "Kết nối máy chủ thất bại",
"storageEndpoint": "Bộ nhớ: {{endpoint}}",
"storageUnreachableStatus": "Không thể kết nối tới bộ nhớ",
"storageUnreachable": "Máy chủ có thể kết nối được, nhưng thiết bị này không truy cập được địa chỉ bộ nhớ {{endpoint}}. Việc truyền tệp sẽ thất bại. Nếu bạn tự lưu trữ, hãy đặt S3_PUBLIC_ENDPOINT thành địa chỉ mà thiết bị này truy cập được.",
"settingsSaved": "Đã lưu cài đặt đồng bộ",
"saveFailed": "Lưu cài đặt thất bại",
"disconnected": "Đã ngắt kết nối đồng bộ",
+2
View File
@@ -636,6 +636,8 @@
"serverError": "服务器返回了错误",
"connectFailed": "连接服务器失败",
"storageEndpoint": "存储: {{endpoint}}",
"storageUnreachableStatus": "无法连接存储",
"storageUnreachable": "服务器可以连接,但此设备无法访问其存储地址 {{endpoint}}。文件传输将会失败。如果你自建服务器,请将 S3_PUBLIC_ENDPOINT 设置为此设备可以访问的地址。",
"settingsSaved": "同步设置已保存",
"saveFailed": "保存设置失败",
"disconnected": "已断开同步",
+18
View File
@@ -88,6 +88,24 @@ export interface SyncSettings {
sync_token?: string;
}
/**
* Result of `check_sync_server_connection`. Files upload straight to the
* storage host named in the presigned URL rather than through the sync server,
* so a healthy server is not evidence that sync works: `storage_reachable`
* false means every transfer will fail at connect.
*
* `null` means "not known", which is not the same as false a server that
* predates `/readyz`, or a cloud deployment that withholds its storage host,
* discloses nothing to probe.
*/
export interface SyncServerCheck {
server_reachable: boolean;
storage_ready: boolean | null;
storage_endpoint: string | null;
storage_reachable: boolean | null;
storage_error: string | null;
}
/**
* Capability/limit set derived from the plan by the backend. Features are gated
* on these flags instead of a single "is paid?" check, so a plan like "solo"