homeio
PublicForked from doctor-io/homeio
app-data-ownership.test.ts
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
classifyServiceBindMountOwnership,
collectOwnershipApplicationTargets,
} from "@/lib/server/modules/docker/app-data-ownership";
const tempRoots: string[] = [];
afterEach(async () => {
await Promise.all(
tempRoots.splice(0).map((rootPath) =>
rm(rootPath, { recursive: true, force: true })
),
);
});
describe("app-data-ownership", () => {
it("prefers an explicit numeric service user over image-specific rules", () => {
expect(
classifyServiceBindMountOwnership({
image: "grafana/grafana:12.1.4",
user: "1234:5678",
env: {},
targetPath: "/var/lib/grafana",
}),
).toEqual({
source: "service-user",
uid: 1234,
gid: 5678,
});
});
it("treats root as an explicit service user", () => {
expect(
classifyServiceBindMountOwnership({
image: "grafana/grafana:12.1.4",
user: "root",
env: {},
targetPath: "/var/lib/grafana",
}),
).toEqual({
source: "service-user",
uid: 0,
gid: 0,
});
});
it("prefers PUID and PGID over image-specific rules", () => {
expect(
classifyServiceBindMountOwnership({
image: "grafana/grafana:12.1.4",
user: null,
env: {
PUID: "1001",
PGID: "1002",
},
targetPath: "/var/lib/grafana",
}),
).toEqual({
source: "puid-pgid",
uid: 1001,
gid: 1002,
});
});
it("falls back to known image rules when no explicit user or PUID/PGID is set", () => {
expect(
classifyServiceBindMountOwnership({
image: "n8nio/n8n:1.123.0",
user: null,
env: {},
targetPath: "/home/node/.n8n",
}),
).toEqual({
source: "image-rule",
uid: 1000,
gid: 1000,
});
});
it("returns fallback when no ownership override can be inferred", () => {
expect(
classifyServiceBindMountOwnership({
image: "nginx:latest",
user: null,
env: {},
targetPath: "/usr/share/nginx/html",
}),
).toEqual({
source: "fallback",
uid: null,
gid: null,
});
});
it("collects nested ownership targets and skips symbolic links", async () => {
const rootPath = await mkdtemp(path.join(os.tmpdir(), "ownership-targets-"));
tempRoots.push(rootPath);
const appDataPath = path.join(rootPath, "AppData", "grafana");
const nestedDir = path.join(appDataPath, "data", "plugins");
const nestedFile = path.join(nestedDir, "plugin.json");
const siblingFile = path.join(appDataPath, "data", "grafana.db");
const symlinkPath = path.join(appDataPath, "data", "linked-config");
await mkdir(nestedDir, { recursive: true });
await writeFile(nestedFile, "{}", "utf8");
await writeFile(siblingFile, "db", "utf8");
await symlink(path.join(rootPath, "elsewhere"), symlinkPath);
const targets = await collectOwnershipApplicationTargets(path.join(appDataPath, "data"));
expect(targets.sort((a, b) => a.localeCompare(b))).toEqual([
path.join(appDataPath, "data"),
path.join(appDataPath, "data", "grafana.db"),
path.join(appDataPath, "data", "plugins"),
path.join(appDataPath, "data", "plugins", "plugin.json"),
]);
});
});