import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { resetAndroidReleaseCache } from '../src/services/appUpdates'; import { json, routeFetch, useTestApp } from './helpers'; const app = useTestApp(); beforeEach(resetAndroidReleaseCache); afterEach(() => vi.unstubAllGlobals()); const SHA = 'ab'.repeat(32); const release = (over: object = {}) => ({ tag_name: 'v1.2.0', body: 'Lyrics and notifications.\n\n---\nmusicshare-*-foss.apk has no closed-source code.', draft: false, prerelease: false, published_at: '2026-10-15T15:33:31Z', created_at: '2026-10-15T15:33:00Z', assets: [ { name: 'musicshare-1.2.0.apk', size: 77_000_000, browser_download_url: 'https://git.test/me/musicshare/releases/download/v1.2.0/musicshare-1.2.0.apk' }, { name: 'musicshare-1.2.0-foss.apk', size: 67_000_000, browser_download_url: 'https://git.test/me/musicshare/releases/download/v1.2.0/musicshare-1.2.0-foss.apk' }, { name: 'SHA256SUMS', size: 200, browser_download_url: 'https://git.test/me/musicshare/releases/download/v1.2.0/SHA256SUMS' }, ], ...over, }); const sums = `${'cd'.repeat(32)} musicshare-1.2.0-foss.apk\n${SHA} musicshare-1.2.0.apk\n`; describe('app updates', () => { it('reports the latest release with its checksum and notes, asking the release host only once', async () => { const auth: (string | null)[] = []; const seen = (init?: RequestInit) => auth.push(new Headers(init?.headers).get('authorization')); vi.stubGlobal( 'fetch', routeFetch([ [/releases\/latest$/, (_url, init) => (seen(init), json(release()))], [/SHA256SUMS$/, (_url, init) => (seen(init), new Response(sums))], ]), ); const res = await app().inject({ url: '/app/android/latest' }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual({ version: '1.2.0', apkUrl: 'https://api.test/app/android/apk/musicshare-1.2.0.apk', sha256: SHA, size: 77_000_000, notes: 'Lyrics and notifications.', publishedAt: '2026-10-15T15:33:31Z', }); expect(auth).toEqual(Array(2).fill(`Basic ${Buffer.from('test-release-token:x-oauth-basic').toString('base64')}`)); await app().inject({ url: '/app/android/latest' }); expect(auth).toHaveLength(2); }); it('offers nothing for drafts, pre-releases or releases without a checksum', async () => { for (const over of [{ prerelease: true }, { draft: true }, { assets: release().assets.slice(0, 2) }]) { resetAndroidReleaseCache(); vi.stubGlobal('fetch', routeFetch([[/releases\/latest$/, () => json(release(over))]])); expect((await app().inject({ url: '/app/android/latest' })).json()).toBeNull(); } }); it('streams only the latest APK, never sending the token to another host', async () => { let apkAuth: string | null = 'unset'; vi.stubGlobal( 'fetch', routeFetch([ [ /releases\/latest$/, () => json( release({ assets: [ { name: 'musicshare-1.2.0.apk', size: 4, browser_download_url: 'https://cdn.example/musicshare-1.2.0.apk' }, { name: 'SHA256SUMS', size: 200, browser_download_url: 'https://git.test/SHA256SUMS' }, ], }), ), ], [/SHA256SUMS$/, () => new Response(sums)], [/cdn\.example/, (_url, init) => ((apkAuth = new Headers(init?.headers).get('authorization')), new Response('APK!'))], ]), ); expect((await app().inject({ url: '/app/android/apk/musicshare-1.1.0.apk' })).statusCode).toBe(404); const res = await app().inject({ url: '/app/android/apk/musicshare-1.2.0.apk' }); expect(res.statusCode).toBe(200); expect(res.headers['content-type']).toBe('application/vnd.android.package-archive'); expect(res.body).toBe('APK!'); expect(apkAuth).toBeNull(); }); it('says 503 when the release host is down', async () => { vi.stubGlobal('fetch', routeFetch([[/releases\/latest$/, () => json({ message: 'oops' }, 500)]])); expect((await app().inject({ url: '/app/android/latest' })).statusCode).toBe(503); }); });