kit.program/kit/test/remote_test.js

279 lines
11 KiB
JavaScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
import { Readable, Writable } from "stream";
import Remote from "../src/remote";
class MemoryWebdavClient {
constructor() {
this.files = new Map();
this.dirs = new Set(["/"]);
}
normalize(remotePath) {
const normalized = path.posix.normalize("/" + String(remotePath || "").replace(/\\/g, "/"));
return normalized === "/" ? "/" : normalized.replace(/\/$/, "");
}
async exists(remotePath) {
const key = this.normalize(remotePath);
return this.dirs.has(key) || this.files.has(key);
}
async createDirectory(remotePath) {
const key = this.normalize(remotePath);
const parts = key.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current += "/" + part;
this.dirs.add(current);
}
}
async getDirectoryContents(remotePath) {
const dir = this.normalize(remotePath);
const prefix = dir === "/" ? "/" : dir + "/";
const names = new Map();
for (const item of [...this.dirs, ...this.files.keys()]) {
if (item === dir || !item.startsWith(prefix)) {
continue;
}
const rest = item.slice(prefix.length);
const name = rest.split("/")[0];
names.set(name, prefix + name);
}
return [...names].map(([basename, filename]) => ({ basename, filename, type: this.dirs.has(this.normalize(filename)) ? "directory" : "file" }));
}
async putFileContents(remotePath, content) {
const key = this.normalize(remotePath);
await this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.isBuffer(content) ? content : Buffer.from(String(content)));
}
async getFileContents(remotePath, options = {}) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
const data = this.files.get(key);
return options.format === "text" ? data.toString("utf-8") : data;
}
createWriteStream(remotePath) {
const key = this.normalize(remotePath);
const chunks = [];
const stream = new Writable({
write(chunk, encoding, callback) {
chunks.push(Buffer.from(chunk));
callback();
}
});
stream.on("finish", () => {
this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.concat(chunks));
});
return stream;
}
createReadStream(remotePath) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
return Readable.from(this.files.get(key));
}
async stat(remotePath) {
const key = this.normalize(remotePath);
const data = this.files.get(key);
if (!data) {
throw new Error(`missing ${key}`);
}
return { size: data.length };
}
}
function writeFixtureProject(tempDir, version, content, group = "/modules") {
const projectDir = path.resolve(tempDir, `fixture-${version || "auto"}-${Math.random().toString(16).slice(2)}`);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.resolve(projectDir, "myapp.txt");
fs.writeFileSync(filePath, content, "utf-8");
fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({
id: "myapp",
group,
version,
desc: "test app",
official_url: "https://example.test/myapp",
dist: [
{
path: filePath,
os: "any",
platform: "any",
filename: "myapp.txt",
type: "data"
}
]
}, null, 4), "utf-8");
return projectDir;
}
function createRemote() {
const remote = new Remote();
remote.client = new MemoryWebdavClient();
return remote;
}
let tempDir;
let localStore;
let oldLogger;
let oldConfig;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-test-"));
localStore = path.resolve(tempDir, "local-store");
oldLogger = globalThis.$logger;
oldConfig = globalThis.$sConfig;
globalThis.$logger = { info() {}, warn() {}, error() {}, debug() {} };
globalThis.$sConfig = { getLocalStorePath: () => localStore };
});
afterEach(() => {
globalThis.$logger = oldLogger;
globalThis.$sConfig = oldConfig;
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("remote module public API", () => {
test("exposes documented public methods", () => {
const remote = createRemote();
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
test("check validates an available webdav client", async () => {
const remote = createRemote();
expect(await remote.check()).toBe(true);
});
test("publish stores files under default group /modules/id", async () => {
const remote = createRemote();
const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1");
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents("/modules/myapp/1.0/version.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/modules/myapp/1.0/any/any/myapp.txt", { format: "text" });
expect(metadata.project_id).toBe("myapp");
expect(metadata.group).toBe("/modules");
expect(metadata.version).toBe("1.0");
expect(metadata.versions).toBeUndefined();
expect(metadata.latest_version).toBeUndefined();
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
});
test("publish supports custom nested group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", "/docker/net"));
const metadata = JSON.parse(await remote.client.getFileContents("/docker/net/myapp/metadata.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/docker/net/myapp/2.0/any/any/myapp.txt", { format: "text" });
expect(metadata.group).toBe("/docker/net");
expect(metadata.version).toBe("2.0");
expect(artifact).toBe("docker net");
});
test("empty group stores project at repository root", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", ""));
const metadata = JSON.parse(await remote.client.getFileContents("/myapp/metadata.json", { format: "text" }));
expect(metadata.group).toBe("/");
expect(await remote.client.getFileContents("/myapp/1.0/any/any/myapp.txt", { format: "text" })).toBe("root group");
});
test("explicit version fails when remote version already exists", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again"))).rejects.toThrow("远程版本已存在");
});
test("blank version increments remote version tail or starts at 1.0", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "", "first auto"));
let metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.0");
await remote.publish(writeFixtureProject(tempDir, "", "second auto"));
metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.1");
expect(await remote.client.getFileContents("/modules/myapp/1.1/any/any/myapp.txt", { format: "text" })).toBe("second auto");
});
test("install downloads latest version from default group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
const installed = await remote.install("myapp");
const moduleRoot = path.resolve(localStore, "modules", "myapp");
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0");
expect(installJson.installed_versions).toContain("1.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
});
test("install can target a custom group path", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", "/docker/net"));
const installed = await remote.install("/docker/net/myapp");
const installedFile = fs.readFileSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("docker install");
});
test("list use rollback and remove manage installed versions", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await remote.install("myapp");
await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2"));
await remote.install("myapp");
expect(await remote.list("myapp")).toEqual(["1.0", "2.0"]);
await remote.rollback("myapp");
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0");
await remote.use("myapp@2.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0");
await remote.remove("myapp@1.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0"))).toBe(false);
});
});