Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions api/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,28 @@ const PLATFORM_PATTERNS = {
'linux-appimage': (name) => name.endsWith('_amd64.AppImage'),
};

const VARIANT_PREFIXES = {
full: ['world-monitor'],
world: ['world-monitor'],
tech: ['tech-monitor'],
finance: ['finance-monitor'],
const VARIANT_IDENTIFIERS = {
full: ['worldmonitor'],
world: ['worldmonitor'],
tech: ['techmonitor'],
finance: ['financemonitor'],
};

function canonicalAssetName(name) {
return String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '');
}

function findAssetForVariant(assets, variant, platformMatcher) {
const prefixes = VARIANT_PREFIXES[variant] ?? null;
if (!prefixes) return null;
const identifiers = VARIANT_IDENTIFIERS[variant] ?? null;
if (!identifiers) return null;

return assets.find((asset) => {
const assetName = String(asset?.name || '').toLowerCase();
const hasVariantPrefix = prefixes.some((prefix) =>
assetName.startsWith(`${prefix.toLowerCase()}_`) || assetName.startsWith(`${prefix.toLowerCase()}-`)
const assetName = String(asset?.name || '');
const normalizedAssetName = canonicalAssetName(assetName);
const hasVariantIdentifier = identifiers.some((identifier) =>
normalizedAssetName.includes(identifier)
);
return hasVariantPrefix && platformMatcher(String(asset?.name || ''));
return hasVariantIdentifier && platformMatcher(assetName);
}) ?? null;
}

Expand Down
1 change: 1 addition & 0 deletions src-tauri/sidecar/local-api-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ export async function createLocalApiServer(options = {}) {

const address = server.address();
const boundPort = typeof address === 'object' && address?.port ? address.port : context.port;
context.port = boundPort;

const portFile = process.env.LOCAL_API_PORT_FILE;
if (portFile) {
Expand Down
34 changes: 34 additions & 0 deletions src-tauri/sidecar/local-api-server.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1347,3 +1347,37 @@ test('traffic log strips query strings from entries to protect privacy', async (
await localApi.cleanup();
}
});

test('service-status reports bound fallback port after EADDRINUSE recovery', async () => {
const blocker = createServer((_req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('occupied');
});
await listen(blocker, '127.0.0.1', 46123);

const localApi = await setupApiDir({});
const app = await createLocalApiServer({
port: 46123,
apiDir: localApi.apiDir,
logger: { log() {}, warn() {}, error() {} },
});
const { port } = await app.start();

try {
assert.notEqual(port, 46123);

const response = await fetch(`http://127.0.0.1:${port}/api/service-status`);
assert.equal(response.status, 200);
const body = await response.json();

assert.equal(body.local.port, port);
const localService = body.services.find((service) => service.id === 'local-api');
assert.equal(localService.description, `Running on 127.0.0.1:${port}`);
} finally {
await app.close();
await localApi.cleanup();
await new Promise((resolve, reject) => {
blocker.close((error) => (error ? reject(error) : resolve()));
});
}
});
4 changes: 4 additions & 0 deletions src/app/desktop-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ export class DesktopUpdater implements AppModule {
return null;
}

if (normalizedOs === 'linux') {
return normalizedArch === 'x86_64' ? 'linux-appimage' : null;
}

return null;
}

Expand Down
82 changes: 82 additions & 0 deletions tests/download-handler.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { strict as assert } from 'node:assert';
import test from 'node:test';
import handler from '../api/download.js';

const RELEASES_PAGE = 'https://github.com/koala73/worldmonitor/releases/latest';

function makeGitHubReleaseResponse(assets) {
return new Response(JSON.stringify({ assets }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}

test('matches full variant for dotted World.Monitor AppImage asset names', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);

try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=full')
);
assert.equal(response.status, 302);
assert.equal(
response.headers.get('location'),
'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage'
);
} finally {
globalThis.fetch = originalFetch;
}
});

test('matches tech variant for dashed Tech-Monitor AppImage asset names', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'Tech-Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage',
},
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);

try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=tech')
);
assert.equal(response.status, 302);
assert.equal(
response.headers.get('location'),
'https://downloads.example/Tech-Monitor_2.5.7_amd64.AppImage'
);
} finally {
globalThis.fetch = originalFetch;
}
});

test('falls back to release page when requested variant has no matching asset', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => makeGitHubReleaseResponse([
{
name: 'World.Monitor_2.5.7_amd64.AppImage',
browser_download_url: 'https://downloads.example/World.Monitor_2.5.7_amd64.AppImage',
},
]);

try {
const response = await handler(
new Request('https://worldmonitor.app/api/download?platform=linux-appimage&variant=finance')
);
assert.equal(response.status, 302);
assert.equal(response.headers.get('location'), RELEASES_PAGE);
} finally {
globalThis.fetch = originalFetch;
}
});