- Fastify API・Node Worker・PostgreSQL・S3互換ストレージ構成
- ADP ZIPのマルチパートアップロードとManifest検証(パストラバーサル・ZIP爆弾等を拒否)
- manifest.json/signatureは再シリアライズせず元バイト列を保持
- Cloudflare風の運用向け管理ダッシュボード(shadcn/Radix・日本語UI・4ルート)
概要/Sources/アプリ/リリース + ルートランディングページ
- 汎用OIDC SSO・Workspace単位の認可・demo mode
- 匿名配布: /sources/{slug}/source.json, /artifacts/{releaseId}/manifest.json
- Docker Compose対応
93 lines
5.1 KiB
JavaScript
93 lines
5.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import test from "node:test";
|
|
|
|
process.env.AUTH_MODE = "demo";
|
|
process.env.STORAGE_MODE = "local";
|
|
process.env.PUBLIC_BASE_URL = "http://localhost:4000";
|
|
process.env.WEB_ORIGIN = "http://localhost:3000";
|
|
process.env.LOCAL_STORAGE_DIR = ".data/test-storage";
|
|
process.env.PROCESS_INLINE = "true";
|
|
delete process.env.DATABASE_URL;
|
|
|
|
const { createServer } = await import("../apps/api/src/server.ts");
|
|
const runtime = await createServer();
|
|
const app = runtime.app;
|
|
|
|
test.after(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
test("creates a source and returns a dashboard in demo mode", async () => {
|
|
const sourceResponse = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/sources",
|
|
headers: { "x-demo-user": "test@example.com", "content-type": "application/json" },
|
|
payload: { name: "Test Source", subtitle: "Test apps", visibility: "draft" },
|
|
});
|
|
assert.equal(sourceResponse.statusCode, 201);
|
|
const source = sourceResponse.json().source;
|
|
assert.equal(source.visibility, "draft");
|
|
|
|
const dashboardResponse = await app.inject({ method: "GET", url: "/api/v1/dashboard", headers: { "x-demo-user": "test@example.com" } });
|
|
assert.equal(dashboardResponse.statusCode, 200);
|
|
const dashboard = dashboardResponse.json();
|
|
assert.ok(dashboard.sources.some((entry) => entry.source.id === source.id));
|
|
});
|
|
|
|
test("does not expose draft sources publicly", async () => {
|
|
const response = await app.inject({ method: "GET", url: "/sources/test-source/source.json" });
|
|
assert.equal(response.statusCode, 404);
|
|
});
|
|
|
|
test("validates, stores, publishes, and serves an ADP package", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "altdock-test-adp-"));
|
|
const fixtureRoot = join(root, "fixture");
|
|
await (await import("node:fs/promises")).mkdir(join(fixtureRoot, "variant"), { recursive: true });
|
|
const manifest = Buffer.from(JSON.stringify({
|
|
distributionPackageRevision: 1,
|
|
appleItemId: "123456789",
|
|
bundleId: "com.example.fixture",
|
|
shortVersionString: "1.0",
|
|
bundleVersion: "1",
|
|
platforms: ["ios"],
|
|
minimumSystemVersions: { ios: "17.4" },
|
|
variants: [{ assetPath: "variant/fixture.ipa", installTargets: [] }],
|
|
deltas: [],
|
|
}));
|
|
const signature = Buffer.from("signed-fixture");
|
|
const ipa = Buffer.from("not-an-installable-ipa-fixture");
|
|
await writeFile(join(fixtureRoot, "manifest.json"), manifest);
|
|
await writeFile(join(fixtureRoot, "signature"), signature);
|
|
await writeFile(join(fixtureRoot, "variant/fixture.ipa"), ipa);
|
|
const archivePath = join(root, "fixture.zip");
|
|
await promisify(execFile)("zip", ["-q", "-r", archivePath, "manifest.json", "signature", "variant"], { cwd: fixtureRoot });
|
|
const archive = await readFile(archivePath);
|
|
const sourceResponse = await app.inject({ method: "POST", url: "/api/v1/sources", headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: { name: "Fixture Source", visibility: "public" } });
|
|
const source = sourceResponse.json().source;
|
|
const uploadResponse = await app.inject({ method: "POST", url: "/api/v1/uploads", headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: { sourceId: source.id, filename: "fixture.zip", sizeBytes: archive.length } });
|
|
assert.equal(uploadResponse.statusCode, 201);
|
|
const upload = uploadResponse.json();
|
|
const putResponse = await app.inject({ method: "PUT", url: new URL(upload.uploadPlan.uploadUrl).pathname, headers: { "content-type": "application/zip" }, payload: archive });
|
|
assert.equal(putResponse.statusCode, 200, putResponse.body);
|
|
const completeResponse = await app.inject({ method: "POST", url: `/api/v1/uploads/${upload.upload.id}/complete`, headers: { "x-demo-user": "fixture@example.com", "content-type": "application/json" }, payload: {} });
|
|
assert.equal(completeResponse.statusCode, 202, completeResponse.body);
|
|
const release = completeResponse.json().release;
|
|
assert.equal(release.status, "ready");
|
|
const publishResponse = await app.inject({ method: "POST", url: `/api/v1/releases/${release.id}/publish`, headers: { "x-demo-user": "fixture@example.com" } });
|
|
assert.equal(publishResponse.statusCode, 200, publishResponse.body);
|
|
const sourceJsonResponse = await app.inject({ method: "GET", url: `/sources/${source.slug}/source.json` });
|
|
assert.equal(sourceJsonResponse.statusCode, 200, sourceJsonResponse.body);
|
|
const sourceJson = sourceJsonResponse.json();
|
|
assert.equal(sourceJson.apps[0].bundleIdentifier, "com.example.fixture");
|
|
assert.match(sourceJson.apps[0].versions[0].downloadURL, new RegExp(`/artifacts/${release.id}/manifest\\.json$`));
|
|
const artifactResponse = await app.inject({ method: "GET", url: `/artifacts/${release.id}/manifest.json` });
|
|
assert.equal(artifactResponse.statusCode, 200, artifactResponse.body);
|
|
assert.deepEqual(artifactResponse.rawPayload, manifest);
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|