desktop-upload-plan.spec.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { createHash } from 'node:crypto'
  2. import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { createRequire } from 'node:module'
  5. import { join } from 'node:path'
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { load } from 'js-yaml'
  8. import { createDesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
  9. import { desktopUpdateMetadataFilename } from '../scripts/desktop-auto-update-environment.mjs'
  10. import type { DesktopPackageTargetName } from '../scripts/package-target.ts'
  11. const temporaryDirectories: string[] = []
  12. const TEST_ORIGIN = 'https://desktop-updates.example.com'
  13. const TEST_BUCKET = 'test-download-bucket'
  14. const PRODUCTION_BUCKET = 'production-download-bucket'
  15. const require = createRequire(import.meta.url)
  16. const { createBlockmap } = require('app-builder-lib/out/targets/differentialUpdateInfoBuilder.js') as {
  17. createBlockmap: (file: string, target: object, packager: { info: { emitArtifactBuildCompleted(event: object): Promise<void> } },
  18. safeArtifactName: string) => Promise<{ size: number; sha512: string }>
  19. }
  20. interface Fixture {
  21. readonly repositoryRoot: string
  22. readonly appRoot: string
  23. readonly artifactsRoot: string
  24. readonly environment: NodeJS.ProcessEnv
  25. }
  26. function digest(contents: string): string {
  27. return createHash('sha512').update(contents).digest('base64')
  28. }
  29. async function fixture(
  30. target: DesktopPackageTargetName,
  31. version = '1.2.3',
  32. environment: 'test' | 'production' = 'test',
  33. ): Promise<Fixture> {
  34. const root = await mkdtemp(join(tmpdir(), 'dsh-desktop-upload-'))
  35. temporaryDirectories.push(root)
  36. const repositoryRoot = join(root, 'repository')
  37. const appRoot = join(repositoryRoot, 'apps', 'desktop')
  38. const artifactsRoot = join(appRoot, '.desktop-build', 'artifacts')
  39. await mkdir(artifactsRoot, { recursive: true })
  40. await writeFile(join(repositoryRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
  41. await writeFile(join(appRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
  42. const [os, arch] = target.split('-') as ['mac' | 'win', 'arm64' | 'x64']
  43. const base = `deepseek-harness-${version}-${os}-${arch}`
  44. const origin = environment === 'test'
  45. ? TEST_ORIGIN
  46. : 'https://download.deepseek.com'
  47. await writeFile(join(artifactsRoot, `${target}-release.json`), `${JSON.stringify({
  48. schemaVersion: 1,
  49. target,
  50. version,
  51. environment,
  52. publicUrl: `${origin}/dsh-desk/feeds/${target}/`,
  53. })}\n`)
  54. if (os === 'mac') {
  55. const zip = 'signed macOS ZIP fixture'
  56. await writeFile(join(artifactsRoot, `${base}.zip`), zip)
  57. await writeFile(join(artifactsRoot, `${base}.zip.blockmap`), 'blockmap')
  58. await writeFile(join(artifactsRoot, `${base}.dmg`), 'notarized DMG fixture')
  59. await writeFile(join(artifactsRoot, desktopUpdateMetadataFilename(version, 'darwin')), `${JSON.stringify({
  60. version,
  61. files: [{ url: `${base}.zip`, size: Buffer.byteLength(zip), sha512: digest(zip) }],
  62. })}\n`)
  63. }
  64. else {
  65. const executable = 'signed NSIS executable fixture'
  66. await writeFile(join(artifactsRoot, `${base}.exe`), executable)
  67. const info = await createBlockmap(join(artifactsRoot, `${base}.exe`), {},
  68. { info: { emitArtifactBuildCompleted: async () => {} } }, `${base}.exe`)
  69. expect(Object.hasOwn(info, 'blockMapSize')).toBe(false)
  70. await writeFile(join(artifactsRoot, desktopUpdateMetadataFilename(version, 'win32')), `${JSON.stringify({
  71. version,
  72. files: [{
  73. url: `${base}.exe`,
  74. ...info,
  75. }],
  76. })}\n`)
  77. }
  78. return {
  79. repositoryRoot,
  80. appRoot,
  81. artifactsRoot,
  82. environment: environment === 'test'
  83. ? {
  84. DSH_DESKTOP_AUTO_UPDATE_ENV: 'test',
  85. DOWNLOAD_TEST_ORIGIN: TEST_ORIGIN,
  86. DOWNLOAD_TEST_COS_BUCKET: TEST_BUCKET,
  87. }
  88. : {
  89. DSH_DESKTOP_AUTO_UPDATE_ENV: 'production',
  90. DOWNLOAD_PROD_COS_BUCKET: PRODUCTION_BUCKET,
  91. },
  92. }
  93. }
  94. afterEach(async () => {
  95. await Promise.all(temporaryDirectories.splice(0).map(async path => rm(path, {
  96. recursive: true,
  97. force: true,
  98. })))
  99. })
  100. describe('desktop upload plan', () => {
  101. it('publishes fixed feeds referencing versioned binaries without overriding CDN cache policy', async () => {
  102. const paths = await fixture('win-x64', '1.2.3', 'production')
  103. const plan = await createDesktopUploadPlan('win-x64', paths)
  104. expect(plan.artifacts.map(artifact => artifact.key)).toEqual([
  105. 'dsh-desk/bin/win-x64/deepseek-harness-1.2.3-win-x64.exe',
  106. 'dsh-desk/bin/win-x64/deepseek-harness-1.2.3-win-x64.exe.blockmap',
  107. 'dsh-desk/feeds/win-x64/nightly.yml',
  108. 'dsh-desk/feeds/win-x64/latest.yml',
  109. ])
  110. expect(load(plan.artifacts[2]!.contents!)).toMatchObject({
  111. version: '1.2.3',
  112. files: [{
  113. url: 'https://download.deepseek.com/dsh-desk/bin/win-x64/deepseek-harness-1.2.3-win-x64.exe',
  114. sha512: digest('signed NSIS executable fixture'),
  115. }],
  116. })
  117. expect(plan.artifacts[2]!.contents).toBe(plan.artifacts[3]!.contents)
  118. expect(plan.artifacts.every(artifact => !('cacheControl' in artifact))).toBe(true)
  119. })
  120. it('validates macOS artifacts and puts channel metadata last', async () => {
  121. const paths = await fixture('mac-arm64')
  122. const plan = await createDesktopUploadPlan('mac-arm64', paths)
  123. expect(plan).toMatchObject({
  124. environment: 'test',
  125. version: '1.2.3',
  126. publicUrl: 'https://desktop-updates.example.com/dsh-desk/feeds/mac-arm64/',
  127. bucket: TEST_BUCKET,
  128. })
  129. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  130. 'deepseek-harness-1.2.3-mac-arm64.dmg',
  131. 'deepseek-harness-1.2.3-mac-arm64.zip',
  132. 'deepseek-harness-1.2.3-mac-arm64.zip.blockmap',
  133. 'nightly-mac.yml',
  134. 'latest-mac.yml',
  135. ])
  136. expect(plan.artifacts.at(-1)).toMatchObject({
  137. channelMetadata: true,
  138. })
  139. })
  140. it('uploads the prerelease channel metadata emitted by electron-builder', async () => {
  141. const paths = await fixture('mac-arm64', '1.2.3-alpha.4')
  142. const plan = await createDesktopUploadPlan('mac-arm64', paths)
  143. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  144. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.dmg',
  145. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.zip',
  146. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.zip.blockmap',
  147. 'nightly-mac.yml',
  148. ])
  149. })
  150. it('validates the Windows installer with the emitted external blockmap and production destination', async () => {
  151. const paths = await fixture('win-x64', '2.0.0', 'production')
  152. const plan = await createDesktopUploadPlan('win-x64', paths)
  153. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  154. 'deepseek-harness-2.0.0-win-x64.exe',
  155. 'deepseek-harness-2.0.0-win-x64.exe.blockmap',
  156. 'nightly.yml',
  157. 'latest.yml',
  158. ])
  159. expect(plan).toMatchObject({
  160. publicUrl: 'https://download.deepseek.com/dsh-desk/feeds/win-x64/',
  161. bucket: PRODUCTION_BUCKET,
  162. })
  163. })
  164. it.each(['missing', 'empty'])('rejects a %s Windows blockmap before publishing its feed', async (condition) => {
  165. const paths = await fixture('win-x64')
  166. const path = join(paths.artifactsRoot, 'deepseek-harness-1.2.3-win-x64.exe.blockmap')
  167. if (condition === 'missing') await rm(path)
  168. else await writeFile(path, '')
  169. await expect(createDesktopUploadPlan('win-x64', paths)).rejects.toThrow(/missing or empty artifact.*\.exe\.blockmap/u)
  170. })
  171. it('rejects a completed build from another dsh version or deployment', async () => {
  172. const paths = await fixture('mac-x64')
  173. await writeFile(join(paths.repositoryRoot, 'package.json'), '{"version":"1.2.4"}\n')
  174. await writeFile(join(paths.appRoot, 'package.json'), '{"version":"1.2.4"}\n')
  175. await expect(createDesktopUploadPlan('mac-x64', paths)).rejects.toThrow(/completion record.*1\.2\.4/u)
  176. const productionPaths = await fixture('mac-x64', '1.2.3', 'production')
  177. await expect(createDesktopUploadPlan('mac-x64', {
  178. ...productionPaths,
  179. environment: {
  180. DSH_DESKTOP_AUTO_UPDATE_ENV: 'test',
  181. DOWNLOAD_TEST_ORIGIN: TEST_ORIGIN,
  182. DOWNLOAD_TEST_COS_BUCKET: TEST_BUCKET,
  183. },
  184. })).rejects.toThrow(/completion record.*test/u)
  185. })
  186. it('rejects stale architecture metadata and modified updater bytes', async () => {
  187. const paths = await fixture('mac-arm64')
  188. const metadataPath = join(paths.artifactsRoot, 'nightly-mac.yml')
  189. const zipPath = join(paths.artifactsRoot, 'deepseek-harness-1.2.3-mac-arm64.zip')
  190. await writeFile(zipPath, 'modified')
  191. await expect(createDesktopUploadPlan('mac-arm64', paths)).rejects.toThrow(/size.*metadata/u)
  192. const x64 = 'wrong architecture'
  193. await writeFile(metadataPath, `${JSON.stringify({
  194. version: '1.2.3',
  195. files: [{
  196. url: 'deepseek-harness-1.2.3-mac-x64.zip',
  197. size: Buffer.byteLength(x64),
  198. sha512: digest(x64),
  199. }],
  200. })}\n`)
  201. await expect(createDesktopUploadPlan('mac-arm64', paths)).rejects.toThrow(/mac-arm64\.zip/u)
  202. })
  203. })