tests(backends): add basic integration tests to mount and unmount all volume types

This commit is contained in:
Nicolas Meienberger 2026-01-17 10:53:10 +01:00
parent e1e8d097be
commit 9f10960621
8 changed files with 306 additions and 1 deletions

View file

@ -9,6 +9,7 @@ import { withTimeout } from "../../../utils/timeout";
import type { VolumeBackend } from "../backend";
import { executeMount, executeUnmount } from "../utils/backend-utils";
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
import { exec } from "~/server/utils/spawn";
const mount = async (config: BackendConfig, path: string) => {
logger.debug(`Mounting WebDAV volume ${path}...`);
@ -116,7 +117,14 @@ const unmount = async (path: string) => {
return { status: BACKEND_STATUS.unmounted };
}
await executeUnmount(path);
// Try fusermount first (proper way to unmount FUSE), fallback to umount
try {
logger.debug(`Executing fusermount -uz ${path}`);
await exec({ command: "fusermount", args: ["-uz", path], timeout: 5000 });
} catch (err) {
logger.debug(`fusermount failed, falling back to umount: ${toMessage(err)}`);
await executeUnmount(path);
}
await fs.rmdir(path).catch(() => {});

View file

@ -0,0 +1,19 @@
FROM oven/bun:latest
RUN apt-get update && apt-get install -y \
cifs-utils \
nfs-common \
davfs2 \
sshfs \
kmod \
iputils-ping \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m davuser
# Enable user_allow_other for FUSE mounts
RUN echo "user_allow_other" >> /etc/fuse.conf
WORKDIR /app

View file

@ -0,0 +1,57 @@
services:
# The Test Runner
tester:
build:
context: .
dockerfile: Dockerfile.tester
privileged: true # Required for mount operations
volumes:
- ../../../../:/app
- /lib/modules:/lib/modules:ro # Required for some kernel-level mount drivers (CIFS/NFS)
working_dir: /app
environment:
- NODE_ENV=test
- LOG_LEVEL=debug
command: tail -f /dev/null # Keep alive for exec
# Mock SMB Server
smb-server:
image: dperson/samba
environment:
- USER=testuser;testpass
- SHARE=testshare;/tmp/samba;yes;no;no;testuser
tmpfs:
- /tmp/samba
# Mock WebDAV Server
webdav-server:
image: bytemark/webdav
environment:
- AUTH_TYPE=Basic
- USERNAME=testuser
- PASSWORD=testpass
volumes:
- webdav-data:/var/lib/dav/data
# Mock NFS Server
nfs-server:
image: erichough/nfs-server
privileged: true
environment:
- NFS_EXPORT_0=/exports *(rw,sync,no_subtree_check,no_root_squash,fsid=0)
- NFS_VERSION=4
volumes:
- nfs-data:/exports
# Mock SFTP Server
sftp-server:
image: atmoz/sftp
command: testuser:testpass:1001
volumes:
- sftp-data:/home/testuser
volumes:
webdav-data:
nfs-data:
sftp-data:

View file

@ -0,0 +1,42 @@
import { describe, expect, it, beforeAll } from "bun:test";
import { makeNfsBackend } from "../../../server/modules/backends/nfs/nfs-backend";
import { BACKEND_STATUS } from "../../../schemas/volumes";
import * as fs from "node:fs/promises";
describe("NFS Backend Integration", () => {
const mountPath = "/tmp/test-mount-nfs";
const config = {
backend: "nfs" as const,
server: "nfs-server",
exportPath: "/",
port: 2049,
version: "4" as const,
};
beforeAll(async () => {
await fs.rm(mountPath, { recursive: true, force: true }).catch(() => {});
});
it("should mount, check health, and unmount successfully", async () => {
const backend = makeNfsBackend(config, mountPath);
// 1. Mount
const mountResult = await backend.mount();
expect(mountResult.status).toBe(BACKEND_STATUS.mounted);
// 2. Health Check
const healthResult = await backend.checkHealth();
expect(healthResult.status).toBe(BACKEND_STATUS.mounted);
// 3. Write/Read test
const testFile = `${mountPath}/test-nfs-${Date.now()}.txt`;
await fs.writeFile(testFile, "hello from nfs integration");
const content = await fs.readFile(testFile, "utf-8");
expect(content).toBe("hello from nfs integration");
// 4. Unmount
const unmountResult = await backend.unmount();
expect(unmountResult.status).toBe(BACKEND_STATUS.unmounted);
}, 60000);
});

View file

@ -0,0 +1,34 @@
#!/bin/bash
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR"
echo "Starting integration sandbox..."
docker compose up -d --build
while ! docker compose exec tester nc -z smb-server 445; do
echo "Waiting for SMB..."
sleep 2
done
while ! docker compose exec tester nc -z nfs-server 2049; do
echo "Waiting for NFS..."
sleep 2
done
while ! docker compose exec tester nc -z webdav-server 80; do
echo "Waiting for WebDAV..."
sleep 2
done
while ! docker compose exec tester nc -z sftp-server 22; do
echo "Waiting for SFTP..."
sleep 2
done
sleep 5
docker compose exec tester bun test --timeout 30000 ./app/test/integration/backends/
docker compose down

View file

@ -0,0 +1,48 @@
import { describe, expect, it, beforeAll } from "bun:test";
import { makeSftpBackend } from "../../../server/modules/backends/sftp/sftp-backend";
import { BACKEND_STATUS } from "../../../schemas/volumes";
import * as fs from "node:fs/promises";
describe("SFTP Backend Integration", () => {
const mountPath = "/tmp/test-mount-sftp";
const config = {
backend: "sftp" as const,
host: "sftp-server",
username: "testuser",
password: "testpass",
path: "",
port: 22,
skipHostKeyCheck: true,
};
beforeAll(async () => {
await fs.rm(mountPath, { recursive: true, force: true }).catch(() => {});
});
it("should mount, check health, and unmount successfully", async () => {
// SKIPPED: The atmoz/sftp Docker image sets restrictive permissions on the home
// directory that prevent writes even when mounting as root. This is a test infrastructure
// limitation, not an issue with the SFTP backend code itself. In production,
// SFTP servers typically have properly configured writable directories.
const backend = makeSftpBackend(config, mountPath);
// 1. Mount
const mountResult = await backend.mount();
expect(mountResult.status).toBe(BACKEND_STATUS.mounted);
// 2. Health Check
const healthResult = await backend.checkHealth();
expect(healthResult.status).toBe(BACKEND_STATUS.mounted);
// 3. Write/Read test - Write to writable directory
const testFile = `${mountPath}/test-sftp-${Date.now()}.txt`;
await fs.writeFile(testFile, "hello from sftp integration");
const content = await fs.readFile(testFile, "utf-8");
expect(content).toBe("hello from sftp integration");
// 4. Unmount
const unmountResult = await backend.unmount();
expect(unmountResult.status).toBe(BACKEND_STATUS.unmounted);
}, 60000);
});

View file

@ -0,0 +1,48 @@
import { describe, expect, it, beforeAll } from "bun:test";
import { makeSmbBackend } from "../../../server/modules/backends/smb/smb-backend";
import { BACKEND_STATUS } from "../../../schemas/volumes";
import * as fs from "node:fs/promises";
describe("SMB Backend Integration", () => {
const mountPath = "/tmp/test-mount-smb";
const config = {
backend: "smb" as const,
server: "smb-server",
share: "testshare",
username: "testuser",
password: "testpass",
vers: "3.0" as const,
port: 445,
};
beforeAll(async () => {
await fs.rm(mountPath, { recursive: true, force: true }).catch(() => {});
});
it("should mount, check health, and unmount successfully", async () => {
const backend = makeSmbBackend(config, mountPath);
// 1. Mount
const mountResult = await backend.mount();
expect(mountResult.status).toBe(BACKEND_STATUS.mounted);
// 2. Health Check
const healthResult = await backend.checkHealth();
expect(healthResult.status).toBe(BACKEND_STATUS.mounted);
// 3. Write/Read test (Verify it's actually usable)
const testFile = `${mountPath}/test-file-${Date.now()}.txt`;
await fs.writeFile(testFile, "hello from integration test");
const content = await fs.readFile(testFile, "utf-8");
expect(content).toBe("hello from integration test");
// 4. Unmount
const unmountResult = await backend.unmount();
expect(unmountResult.status).toBe(BACKEND_STATUS.unmounted);
// 5. Verify unmounted
const postUnmountHealth = await backend.checkHealth();
expect(postUnmountHealth.status).toBe(BACKEND_STATUS.error);
});
});

View file

@ -0,0 +1,49 @@
import { describe, expect, it, beforeAll } from "bun:test";
import { makeWebdavBackend } from "../../../server/modules/backends/webdav/webdav-backend";
import { BACKEND_STATUS } from "../../../schemas/volumes";
import * as fs from "node:fs/promises";
describe("WebDAV Backend Integration", () => {
const mountPath = "/tmp/test-mount-webdav";
const config = {
backend: "webdav" as const,
server: "webdav-server",
path: "/",
username: "testuser",
password: "testpass",
port: 80,
ssl: false,
};
beforeAll(async () => {
await fs.rm(mountPath, { recursive: true, force: true }).catch(() => {});
await fs.mkdir("/etc/davfs2", { recursive: true }).catch(() => {});
const davConfig = "trust_server_cert 1\nuse_locks 0\n";
await fs.writeFile("/etc/davfs2/davfs2.conf", davConfig);
});
it("should mount, check health, and unmount successfully", async () => {
const backend = makeWebdavBackend(config, mountPath);
// 1. Mount
const mountResult = await backend.mount();
expect(mountResult.status).toBe(BACKEND_STATUS.mounted);
// 2. Health Check
const healthResult = await backend.checkHealth();
expect(healthResult.status).toBe(BACKEND_STATUS.mounted);
// 3. Write/Read test
const testFile = `${mountPath}/test-webdav-${Date.now()}.txt`;
await fs.writeFile(testFile, "hello from webdav integration");
const content = await fs.readFile(testFile, "utf-8");
expect(content).toBe("hello from webdav integration");
// 4. Unmount
const unmountResult = await backend.unmount();
expect(unmountResult.status).toBe(BACKEND_STATUS.unmounted);
}, 60000);
});