tests(backends): add basic integration tests to mount and unmount all volume types
This commit is contained in:
parent
e1e8d097be
commit
9f10960621
8 changed files with 306 additions and 1 deletions
|
|
@ -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(() => {});
|
||||
|
||||
|
|
|
|||
19
app/test/integration/backends/Dockerfile.tester
Normal file
19
app/test/integration/backends/Dockerfile.tester
Normal 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
|
||||
|
||||
57
app/test/integration/backends/docker-compose.yml
Normal file
57
app/test/integration/backends/docker-compose.yml
Normal 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:
|
||||
|
||||
42
app/test/integration/backends/nfs.test.ts
Normal file
42
app/test/integration/backends/nfs.test.ts
Normal 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);
|
||||
});
|
||||
34
app/test/integration/backends/run.sh
Executable file
34
app/test/integration/backends/run.sh
Executable 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
|
||||
48
app/test/integration/backends/sftp.test.ts
Normal file
48
app/test/integration/backends/sftp.test.ts
Normal 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);
|
||||
});
|
||||
48
app/test/integration/backends/smb.test.ts
Normal file
48
app/test/integration/backends/smb.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
49
app/test/integration/backends/webdav.test.ts
Normal file
49
app/test/integration/backends/webdav.test.ts
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue