Vydané zdroje
Stáhnout zdroje ZIP
CH-J Server Manager
Procházejte adresáře a soubory konkrétního vydání aplikace.
Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
1
"use strict";3
const test = require("node:test");4
const assert = require("node:assert/strict");5
const crypto = require("node:crypto");6
const fs = require("node:fs");7
const os = require("node:os");8
const path = require("node:path");9
const { HashUrlProvider } = require("../src/main/updates/hashUrlProvider");11
const baseUrl = "https://192.168.10.154/";13
function releasePayload(bytes) {14
return {15
update_available: true,16
current_version: "0.0.1",17
release: {18
id: "alpha-002",19
type: "app",20
name: "CH-J Server Manager",21
version: "0.0.2",22
platform: "mac",23
arch: "arm64",24
channel: "alpha",25
mandatory: false,26
notes: "Test",27
sha512: crypto.createHash("sha512").update(bytes).digest("hex"),28
size: bytes.length,29
filename: "CH-J.dmg",30
download_url: `${baseUrl}files/apps/mac/0.0.2-arm64/CH-J.dmg`,31
signature_url: `${baseUrl}files/apps/mac/0.0.2-arm64/CH-J.dmg.asc`,32
published_at: "2026-08-07T17:30:00+02:00"33
}34
};35
}37
test("provider checks PHP API and independently validates release", async (t) => {38
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));39
t.after(() => fs.rmSync(root, { recursive: true, force: true }));40
const bytes = Buffer.from("release-bytes");41
const provider = new HashUrlProvider({42
baseUrls: [baseUrl],43
downloadRoot: root,44
fetchImpl: async (url) => {45
assert.match(url.toString(), /api\/latest\.php/);46
assert.equal(url.searchParams.get("channel"), "alpha");47
assert.equal(url.searchParams.get("arch"), "arm64");48
return new Response(JSON.stringify(releasePayload(bytes)), { status: 200, headers: { "content-type": "application/json" } });49
}50
});51
const result = await provider.checkForUpdates({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "alpha" });52
assert.equal(result.updateAvailable, true);53
assert.equal(result.release.version, "0.0.2");54
});56
test("provider lists every compatible build and keeps same-version releases distinct", async () => {57
const bytes = Buffer.from("release-bytes");58
const newest = releasePayload(bytes).release;59
newest.id = "same-version-newer";60
newest.version = "0.0.1";61
newest.published_at = "2026-08-07T18:00:00+02:00";62
const older = { ...newest, id: "same-version-older", published_at: "2026-08-07T17:00:00+02:00" };63
const provider = new HashUrlProvider({64
baseUrls: [baseUrl],65
downloadRoot: os.tmpdir(),66
fetchImpl: async (url) => {67
assert.match(url.toString(), /api\/releases\.php/);68
assert.equal(url.searchParams.get("platform"), "mac");69
assert.equal(url.searchParams.get("arch"), "arm64");70
assert.equal(url.searchParams.get("channel"), "alpha");71
return new Response(JSON.stringify({ releases: [older, newest] }), { status: 200 });72
}73
});74
const result = await provider.listReleases({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "alpha" });75
assert.deepEqual(result.releases.map((release) => release.id), ["same-version-newer", "same-version-older"]);76
});78
test("all update channels are fetched separately and merged locally", async () => {79
const bytes = Buffer.from("release-bytes");80
const requestedChannels = [];81
const provider = new HashUrlProvider({82
baseUrls: [baseUrl],83
downloadRoot: os.tmpdir(),84
fetchImpl: async (url) => {85
const channel = url.searchParams.get("channel");86
requestedChannels.push(channel);87
const release = releasePayload(bytes).release;88
release.id = `${channel}-release`;89
release.channel = channel;90
return new Response(JSON.stringify({ releases: [release] }), { status: 200 });91
}92
});93
const result = await provider.listReleases({ currentVersion: "0.0.1", platform: "darwin", arch: "arm64", channel: "all" });94
assert.deepEqual(requestedChannels, ["alpha", "beta", "stable"]);95
assert.deepEqual(new Set(result.releases.map((release) => release.channel)), new Set(["alpha", "beta", "stable"]));96
});98
test("provider rejects unsupported macOS x64 target before contacting server", async () => {99
let contacted = false;100
const provider = new HashUrlProvider({101
baseUrls: [baseUrl],102
downloadRoot: os.tmpdir(),103
fetchImpl: async () => {104
contacted = true;105
return new Response(null, { status: 404 });106
}107
});108
await assert.rejects(109
() => provider.checkForUpdates({ currentVersion: "0.0.1", platform: "darwin", arch: "x64", channel: "alpha" }),110
/Unsupported update architecture/111
);112
assert.equal(contacted, false);113
});115
test("provider downloads exact bytes and verifies SHA-512", async (t) => {116
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));117
t.after(() => fs.rmSync(root, { recursive: true, force: true }));118
const bytes = Buffer.from("verified release bytes");119
const payload = releasePayload(bytes);120
const provider = new HashUrlProvider({121
baseUrls: [baseUrl],122
downloadRoot: root,123
signatureVerifier: {124
async verifyFile() {125
return { valid: true, primaryFingerprint: "0".repeat(40), signingFingerprints: ["1".repeat(40)] };126
}127
},128
fetchImpl: async (url) => String(url).endsWith(".asc")129
? new Response("test-signature", { status: 200 })130
: new Response(bytes, { status: 200, headers: { "content-length": String(bytes.length) } })131
});132
const release = {133
...payload.release,134
downloadUrl: payload.release.download_url,135
signatureUrl: payload.release.signature_url136
};137
const result = await provider.downloadAndVerify(release);138
assert.equal(fs.readFileSync(result.path, "utf8"), bytes.toString("utf8"));139
assert.equal(result.sha512, payload.release.sha512);140
});142
test("provider deletes staging file when SHA-512 does not match", async (t) => {143
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));144
t.after(() => fs.rmSync(root, { recursive: true, force: true }));145
const expected = Buffer.from("expected");146
const actual = Buffer.from("modified");147
const payload = releasePayload(expected);148
payload.release.size = actual.length;149
const provider = new HashUrlProvider({150
baseUrls: [baseUrl],151
downloadRoot: root,152
signatureVerifier: { async verifyFile() { throw new Error("must not be reached"); } },153
fetchImpl: async () => new Response(actual, { status: 200 })154
});155
await assert.rejects(() => provider.downloadAndVerify({156
...payload.release,157
downloadUrl: payload.release.download_url,158
signatureUrl: payload.release.signature_url159
}), /SHA-512/);160
const remaining = fs.readdirSync(path.dirname(path.join(root, `${payload.release.version}-${payload.release.id}`, payload.release.filename)));161
assert.equal(remaining.some((name) => name.endsWith(".part")), false);162
});164
test("provider removes an artifact when mandatory OpenPGP verification fails", async (t) => {165
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));166
t.after(() => fs.rmSync(root, { recursive: true, force: true }));167
const bytes = Buffer.from("unsigned release bytes");168
const payload = releasePayload(bytes);169
const provider = new HashUrlProvider({170
baseUrls: [baseUrl],171
downloadRoot: root,172
signatureVerifier: { async verifyFile() { throw new Error("invalid signature"); } },173
fetchImpl: async (url) => String(url).endsWith(".asc")174
? new Response("bad-signature", { status: 200 })175
: new Response(bytes, { status: 200 })176
});177
const release = {178
...payload.release,179
downloadUrl: payload.release.download_url,180
signatureUrl: payload.release.signature_url181
};182
await assert.rejects(() => provider.downloadAndVerify(release), /invalid signature/);183
const destination = path.join(root, `${release.version}-${release.id}`, release.filename);184
assert.equal(fs.existsSync(destination), false);185
assert.equal(fs.existsSync(`${destination}.asc`), false);186
});188
test("provider re-hashes the same private-cache artifact immediately before installation", async (t) => {189
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-updates-"));190
t.after(() => fs.rmSync(root, { recursive: true, force: true }));191
const bytes = Buffer.from("verified release bytes");192
const payload = releasePayload(bytes);193
const signatureVerifier = {194
calls: 0,195
async verifyFile() {196
this.calls += 1;197
return { valid: true, primaryFingerprint: "0".repeat(40), signingFingerprints: ["1".repeat(40)] };198
}199
};200
const provider = new HashUrlProvider({201
baseUrls: [baseUrl],202
downloadRoot: root,203
signatureVerifier,204
fetchImpl: async (url) => String(url).endsWith(".asc")205
? new Response("test-signature", { status: 200 })206
: new Response(bytes, { status: 200 })207
});208
const release = {209
...payload.release,210
downloadUrl: payload.release.download_url,211
signatureUrl: payload.release.signature_url212
};213
const downloaded = await provider.downloadAndVerify(release);214
fs.chmodSync(downloaded.path, 0o600);215
fs.writeFileSync(downloaded.path, Buffer.from("tampered release bytes"));216
await assert.rejects(() => provider.reverifyForInstall(downloaded, release), /changed after verification|size changed/);217
assert.equal(signatureVerifier.calls, 1, "tampered bytes must be rejected before the second OpenPGP pass");218
});SHA-256: 354fd94a6f6118d45f397d6847f961bc84997fa2947a07e34836d5c5db6391ed
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0