初回コミット: AltStore PAL向けADPホスティングSaaS
- 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対応
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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 });
|
||||
});
|
||||
Reference in New Issue
Block a user