diff --git a/packages/frontend/electron/test/main/fixtures/feeds.txt b/packages/frontend/electron/test/main/fixtures/feeds.txt
new file mode 100644
index 0000000000..315f9574f1
--- /dev/null
+++ b/packages/frontend/electron/test/main/fixtures/feeds.txt
@@ -0,0 +1,50 @@
+
+
+ tag:github.com,2008:https://github.com/toeverything/AFFiNE/releases
+
+
+ Release notes from AFFiNE
+ 2023-12-28T16:36:36+08:00
+
+ tag:github.com,2008:Repository/519859998/0.11.0-nightly-202312280901-e11e827
+ 2023-12-28T17:23:15+08:00
+
+ 0.11.0-nightly-202312280901-e11e827
+ No content.
+
+ github-actions[bot]
+
+
+
+
+ tag:github.com,2008:Repository/519859998/v0.11.1
+ 2023-12-27T19:40:15+08:00
+
+ 0.11.1
+
+ github-actions[bot]
+
+
+
+
+ tag:github.com,2008:Repository/519859998/v0.11.1-beta.1
+ 2023-12-27T18:30:52+08:00
+
+ 0.11.1-beta.1
+
+ github-actions[bot]
+
+
+
+
+ tag:github.com,2008:Repository/519859998/v0.11.1-canary.1
+ 2023-12-27T10:47:52+08:00
+
+ 0.11.1-canary.1
+
+
+ github-actions[bot]
+
+
+
+
diff --git a/packages/frontend/electron/test/main/fixtures/releases/0.11.1-beta.1.txt b/packages/frontend/electron/test/main/fixtures/releases/0.11.1-beta.1.txt
new file mode 100644
index 0000000000..fd9a7434cf
--- /dev/null
+++ b/packages/frontend/electron/test/main/fixtures/releases/0.11.1-beta.1.txt
@@ -0,0 +1,20 @@
+version: 0.11.1-beta.1
+files:
+ - url: affine-beta-windows-x64.exe
+ sha512: uQdF7bEZteCMp/bT7vwCjlEcAf6osW9zZ+Q5grEkmbHPpcqCCzLudguXqHIwohO4GGq9pS8H4kJzG0LZc+SmXg==
+ size: 179515752
+ - url: affine-beta-macos-arm64.dmg
+ sha512: gRsi4XO4+kREQuLX2CnS2V9vvUmBMmoGR6MvoB6TEFm1WiC8k8v69DRKYQ0Vjlom/j9HZlBEYUTqcW7IsMgrpw==
+ size: 169726061
+ - url: affine-beta-macos-arm64.zip
+ sha512: +aXkjJfQnp2dUz3Y0i5cL6V5Hm1OkYVlwM/KAcZfLlvLoU3zQ0zSZvJ6+H2IlIhOebSq56Ip76H5NP8fe7UnOw==
+ size: 169007175
+ - url: affine-beta-macos-x64.dmg
+ sha512: i5V95dLx3iWFpNj89wFU40THT3Oeow8g706Z6/mG1zYOIR3kXUkIpp6+wJmlfe9g4iwNmRd0rgI4HAG5LaQagg==
+ size: 175712730
+ - url: affine-beta-macos-x64.zip
+ sha512: DnRUHcj+4FluII5kTbUuEAQI2CIRufd1Z0P98pwa/uX5hk2iOj1QzMD8WM+MTbFNC6rZvMtMlos8GyVLsZmK0w==
+ size: 175235583
+path: affine-beta-windows-x64.exe
+sha512: uQdF7bEZteCMp/bT7vwCjlEcAf6osW9zZ+Q5grEkmbHPpcqCCzLudguXqHIwohO4GGq9pS8H4kJzG0LZc+SmXg==
+releaseDate: 2023-12-27T08:59:31.826Z
diff --git a/packages/frontend/electron/test/main/fixtures/releases/0.11.1-canary.1.txt b/packages/frontend/electron/test/main/fixtures/releases/0.11.1-canary.1.txt
new file mode 100644
index 0000000000..36dc8f50c8
--- /dev/null
+++ b/packages/frontend/electron/test/main/fixtures/releases/0.11.1-canary.1.txt
@@ -0,0 +1,20 @@
+version: 0.11.1-canary.1
+files:
+ - url: affine-canary-windows-x64.exe
+ sha512: qbK4N6+axVO2dA/iPzfhANWxCZXY1S3ci9qYIT1v/h0oCjc6vqpXU+2KRGL5mplL6wmVgJAOpqrfnq9gHMsfDg==
+ size: 179526504
+ - url: affine-canary-macos-arm64.dmg
+ sha512: ++LAGuxTmFAVd65k8UpKKfU19iisvXHKDDfPkGlTVC000QP3foeS21BmTgYnM1ZuhEC6KGzSGrqvUDVDNYnRmA==
+ size: 169903530
+ - url: affine-canary-macos-arm64.zip
+ sha512: IAWbCpVqPPVVzDowGKGnKZzHN2jPgAW40v+bUZR2tdgDrqIAVy4YdamYz8WmEwpg1TXmi0ueSsWgGFPgBIr0iA==
+ size: 169085665
+ - url: affine-canary-macos-x64.dmg
+ sha512: 4y4/KkmkmFmZ94ntRAN0lSX7aZzgEd4Wg7f85Tff296P3x85sbPF4FFIp++Zx/cgBZBUQwMWe9xeGlefompQ/g==
+ size: 175920978
+ - url: affine-canary-macos-x64.zip
+ sha512: S1MuMHooMOQ9eJ+coRYmyz6k5lnWIMqHotSrywxGGo7sFXBY+O5F4PeKgNREJtwXjAIxv0GxZVvbe5jc+onw9w==
+ size: 175315484
+path: affine-canary-windows-x64.exe
+sha512: qbK4N6+axVO2dA/iPzfhANWxCZXY1S3ci9qYIT1v/h0oCjc6vqpXU+2KRGL5mplL6wmVgJAOpqrfnq9gHMsfDg==
+releaseDate: 2023-12-26T13:24:28.221Z
diff --git a/packages/frontend/electron/test/main/fixtures/releases/0.11.1.txt b/packages/frontend/electron/test/main/fixtures/releases/0.11.1.txt
new file mode 100644
index 0000000000..843b632287
--- /dev/null
+++ b/packages/frontend/electron/test/main/fixtures/releases/0.11.1.txt
@@ -0,0 +1,20 @@
+version: 0.11.1
+files:
+ - url: affine-stable-windows-x64.exe
+ sha512: qHRO31Fb8F+Q/hiGiJJ2WH+PpSC5iUIPtWujUoI+XNMz7UfhCGxoVW9U38CTE9LecILS119SZN0rrHkmu+nQiw==
+ size: 179504488
+ - url: affine-stable-macos-arm64.dmg
+ sha512: uDS7bZusoU5p2t4bi1k/IdvChj3BRIWbOLanbhAfIjwBmf9FM3553wgeUzQLRMRuD5wavsw/aA1BaqFJIbwkyQ==
+ size: 169793193
+ - url: affine-stable-macos-arm64.zip
+ sha512: n4CrOgNPd70WqPfe0ZEKzmyOdqOlVnFvQqylIlt92eqKrUb8jcxVThQY+GU2Jy3jVjvKqUvubodDbIkQhXQ1xQ==
+ size: 169014676
+ - url: affine-stable-macos-x64.dmg
+ sha512: xFy1kt1025h1wqBjHt+IoPweC40UqAZvZLI2XD3LIlPG60jZ03+QM75UwaXYuVYEqe3kO9a3WeFcrfGdoj4Hzw==
+ size: 175720872
+ - url: affine-stable-macos-x64.zip
+ sha512: FfgX22ytleb8fv35odzhDyFsmiUVPdI4XQjTB4uDmkhQ719i+W4yctUnP5TSCpymYC0HgVRFSVg4Jkuuli+8ug==
+ size: 175244411
+path: affine-stable-windows-x64.exe
+sha512: qHRO31Fb8F+Q/hiGiJJ2WH+PpSC5iUIPtWujUoI+XNMz7UfhCGxoVW9U38CTE9LecILS119SZN0rrHkmu+nQiw==
+releaseDate: 2023-12-27T11:04:53.014Z
diff --git a/packages/frontend/electron/test/main/mocks/app-adapter.ts b/packages/frontend/electron/test/main/mocks/app-adapter.ts
new file mode 100644
index 0000000000..cf18d6b314
--- /dev/null
+++ b/packages/frontend/electron/test/main/mocks/app-adapter.ts
@@ -0,0 +1,34 @@
+import type { AppAdapter } from 'electron-updater/out/AppAdapter';
+
+/**
+ * For testing and same as:
+ * https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/ElectronAppAdapter.ts
+ */
+export class MockedAppAdapter implements AppAdapter {
+ version: string;
+ name = 'AFFiNE-testing';
+ isPackaged = true;
+ appUpdateConfigPath = '';
+ userDataPath = '';
+ baseCachePath = '';
+
+ constructor(version: string) {
+ this.version = version;
+ }
+
+ whenReady() {
+ return Promise.resolve();
+ }
+
+ relaunch() {
+ return;
+ }
+
+ quit() {
+ return;
+ }
+
+ onQuit(_handler: (exitCode: number) => void) {
+ return;
+ }
+}
diff --git a/packages/frontend/electron/test/main/mocks/http-executor.ts b/packages/frontend/electron/test/main/mocks/http-executor.ts
new file mode 100644
index 0000000000..b803eb7c6d
--- /dev/null
+++ b/packages/frontend/electron/test/main/mocks/http-executor.ts
@@ -0,0 +1,26 @@
+import http from 'node:https';
+
+import { HttpExecutor } from 'builder-util-runtime';
+import type { ClientRequest } from 'electron';
+
+/**
+ * For testing and same as:
+ * https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/electronHttpExecutor.ts
+ */
+export class MockedHttpExecutor extends HttpExecutor {
+ createRequest(
+ options: any,
+ callback: (response: any) => void
+ ): ClientRequest {
+ if (options.headers && options.headers.Host) {
+ // set host value from headers.Host
+ options.host = options.headers.Host;
+ // remove header property 'Host', if not removed causes net::ERR_INVALID_ARGUMENT exception
+ delete options.headers.Host;
+ }
+
+ const request = http.request(options);
+ request.on('response', callback);
+ return request as unknown as ClientRequest;
+ }
+}
diff --git a/packages/frontend/electron/test/main/mocks/index.ts b/packages/frontend/electron/test/main/mocks/index.ts
new file mode 100644
index 0000000000..2ba1e3996c
--- /dev/null
+++ b/packages/frontend/electron/test/main/mocks/index.ts
@@ -0,0 +1,3 @@
+export * from './app-adapter';
+export * from './http-executor';
+export * from './updater';
diff --git a/packages/frontend/electron/test/main/mocks/updater.ts b/packages/frontend/electron/test/main/mocks/updater.ts
new file mode 100644
index 0000000000..f3fd31bd61
--- /dev/null
+++ b/packages/frontend/electron/test/main/mocks/updater.ts
@@ -0,0 +1,37 @@
+import 'electron-updater'; // Prevent BaseUpdater is undefined.
+
+import { type AllPublishOptions, UUID } from 'builder-util-runtime';
+import { randomBytes } from 'crypto';
+import type { AppAdapter } from 'electron-updater/out/AppAdapter';
+import type { DownloadUpdateOptions } from 'electron-updater/out/AppUpdater';
+import type { InstallOptions } from 'electron-updater/out/BaseUpdater';
+import { BaseUpdater } from 'electron-updater/out/BaseUpdater';
+
+import { MockedHttpExecutor } from './http-executor';
+
+/**
+ * For testing, like:
+ * https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/MacUpdater.ts
+ */
+export class MockedUpdater extends BaseUpdater {
+ httpExecutor: MockedHttpExecutor;
+
+ constructor(options?: AllPublishOptions | null, app?: AppAdapter) {
+ super(options, app);
+
+ this.httpExecutor = new MockedHttpExecutor();
+ Object.assign(this, {
+ getOrCreateStagingUserId: () => {
+ const id = UUID.v5(randomBytes(4096), UUID.OID);
+ return id;
+ },
+ });
+ }
+
+ doInstall(_options: InstallOptions) {
+ return true;
+ }
+ doDownloadUpdate(_options: DownloadUpdateOptions): Promise {
+ return Promise.resolve([]);
+ }
+}
diff --git a/packages/frontend/electron/test/main/updater.spec.ts b/packages/frontend/electron/test/main/updater.spec.ts
new file mode 100644
index 0000000000..51c2864479
--- /dev/null
+++ b/packages/frontend/electron/test/main/updater.spec.ts
@@ -0,0 +1,104 @@
+import nodePath from 'node:path';
+
+import fs from 'fs-extra';
+import { flatten } from 'lodash-es';
+import { http, HttpResponse } from 'msw';
+import { setupServer } from 'msw/node';
+import { compare } from 'semver';
+import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
+
+import { CustomGitHubProvider } from '../../src/main/updater/custom-github-provider';
+import { MockedAppAdapter, MockedUpdater } from './mocks';
+
+const platformTail = (() => {
+ // https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/providers/Provider.ts#L30
+ const platform = process.platform;
+ if (platform === 'linux') {
+ const arch = process.env['TEST_UPDATER_ARCH'] || process.arch;
+ const archSuffix = arch === 'x64' ? '' : `-${arch}`;
+ return '-linux' + archSuffix;
+ } else {
+ return platform === 'darwin' ? '-mac' : '';
+ }
+})();
+
+describe('testing for client update', () => {
+ const expectReleaseList = [
+ { buildType: 'stable', version: '0.11.1' },
+ { buildType: 'beta', version: '0.11.1-beta.1' },
+ { buildType: 'canary', version: '0.11.1-canary.1' },
+ ];
+
+ const restHandlers = [
+ http.get(
+ 'https://github.com/toeverything/AFFiNE/releases.atom',
+ async () => {
+ const buffer = await fs.readFile(
+ nodePath.join(__dirname, 'fixtures', 'feeds.txt')
+ );
+ const content = buffer.toString();
+ return HttpResponse.xml(content);
+ }
+ ),
+ ...flatten(
+ expectReleaseList.map(({ version, buildType }) => {
+ function response404() {
+ return HttpResponse.text('Not Found', { status: 404 });
+ }
+
+ return [
+ http.get(
+ `https://github.com/toeverything/AFFiNE/releases/download/v${version}/latest${platformTail}.yml`,
+ async function responseContent() {
+ const buffer = await fs.readFile(
+ nodePath.join(
+ __dirname,
+ 'fixtures',
+ 'releases',
+ `${version}.txt`
+ )
+ );
+ const content = buffer.toString();
+ return HttpResponse.text(content);
+ }
+ ),
+ http.get(
+ `https://github.com/toeverything/AFFiNE/releases/download/v${version}/${buildType}${platformTail}.yml`,
+ response404
+ ),
+ ];
+ })
+ ),
+ ];
+
+ const server = setupServer(...restHandlers);
+
+ beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
+ afterAll(() => server.close());
+ afterEach(() => server.resetHandlers());
+
+ for (const { buildType } of expectReleaseList) {
+ it(`check update for ${buildType} channel successfully`, async () => {
+ const app = new MockedAppAdapter('0.10.0');
+ const updater = new MockedUpdater(null, app);
+ updater.allowPrerelease = buildType !== 'stable';
+
+ const feedUrl: Parameters[0] = {
+ channel: buildType,
+ // hack for custom provider
+ provider: 'custom' as 'github',
+ repo: 'AFFiNE',
+ owner: 'toeverything',
+ releaseType: buildType === 'stable' ? 'release' : 'prerelease',
+ // @ts-expect-error hack for custom provider
+ updateProvider: CustomGitHubProvider,
+ };
+
+ updater.setFeedURL(feedUrl);
+
+ const info = await updater.checkForUpdates();
+ expect(info).not.toBe(null);
+ expect(compare(info!.updateInfo.version, '0.10.0')).toBe(1);
+ });
+ }
+});