Add Prettier and ESLint tooling (Refs #77)

This commit is contained in:
2026-10-09 15:56:13 -04:00
parent 8ed059b8d3
commit 16469e2d43
711 changed files with 99737 additions and 28286 deletions
+266 -71
View File
@@ -7,9 +7,22 @@ const content = Buffer.from([0x4d, 0x5a, 1, 2, 3, 4]);
const sha256 = createHash('sha256').update(content).digest('hex');
const url = 'https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0';
const metadata = (overrides: Record<string, unknown> = {}) => ({
id: 100, tag_name: 'v3.1.0', name: 'Utilities 3.1', body: 'Release notes <script>untrusted</script>',
published_at: '2026-07-27T12:00:00Z', draft: false, prerelease: false,
assets: [{ id: 200, name: 't6-gsc-utils.dll', size: content.length, digest: `sha256:${sha256}`, state: 'uploaded' }],
id: 100,
tag_name: 'v3.1.0',
name: 'Utilities 3.1',
body: 'Release notes <script>untrusted</script>',
published_at: '2026-07-27T12:00:00Z',
draft: false,
prerelease: false,
assets: [
{
id: 200,
name: 't6-gsc-utils.dll',
size: content.length,
digest: `sha256:${sha256}`,
state: 'uploaded'
}
],
...overrides
});
let fetchMock: ReturnType<typeof vi.fn>;
@@ -18,38 +31,80 @@ beforeEach(async () => {
fixture = await newFixture();
delete process.env.RCON_PASSWORD;
fetchMock = vi.fn(async (input: string | URL) => String(input).startsWith('https://api.github.com/')
? Response.json(metadata()) : new Response(content));
fetchMock = vi.fn(async (input: string | URL) =>
String(input).startsWith('https://api.github.com/')
? Response.json(metadata())
: new Response(content)
);
vi.stubGlobal('fetch', fetchMock);
});
afterEach(async () => { vi.unstubAllGlobals(); await fixture.cleanup(); });
afterEach(async () => {
vi.unstubAllGlobals();
await fixture.cleanup();
});
describe('GitHub release lookup', () => {
it('supports repository, latest, tag and the supplied release-anchor URLs', async () => {
const { parseGithubReleaseUrl, getGithubRelease } = await import('../../src/lib/server/integrations/github-releases.js');
expect(parseGithubReleaseUrl('https://github.com/alicealys/t6-gsc-utils/releases#release-v3.1.0')).toEqual({ repository: 'alicealys/t6-gsc-utils', tag: 'v3.1.0' });
expect(parseGithubReleaseUrl('https://github.com/alicealys/t6-gsc-utils/releases/latest').tag).toBeNull();
const { parseGithubReleaseUrl, getGithubRelease } =
await import('../../src/lib/server/integrations/github-releases.js');
expect(
parseGithubReleaseUrl(
'https://github.com/alicealys/t6-gsc-utils/releases#release-v3.1.0'
)
).toEqual({ repository: 'alicealys/t6-gsc-utils', tag: 'v3.1.0' });
expect(
parseGithubReleaseUrl('https://github.com/alicealys/t6-gsc-utils/releases/latest').tag
).toBeNull();
expect(parseGithubReleaseUrl('https://github.com/alicealys/t6-gsc-utils').tag).toBeNull();
expect(parseGithubReleaseUrl('https://github.com/alicealys/t6-gsc-utils/releases/tag/release%2F3.1').tag).toBe('release/3.1');
expect(
parseGithubReleaseUrl(
'https://github.com/alicealys/t6-gsc-utils/releases/tag/release%2F3.1'
).tag
).toBe('release/3.1');
const release = await getGithubRelease(url);
expect(release).toMatchObject({ repository: 'alicealys/t6-gsc-utils', tag: 'v3.1.0', assets: [{ name: 't6-gsc-utils.dll' }] });
expect(fetchMock.mock.calls[0][0]).toBe('https://api.github.com/repos/alicealys/t6-gsc-utils/releases/tags/v3.1.0');
expect(release).toMatchObject({
repository: 'alicealys/t6-gsc-utils',
tag: 'v3.1.0',
assets: [{ name: 't6-gsc-utils.dll' }]
});
expect(fetchMock.mock.calls[0][0]).toBe(
'https://api.github.com/repos/alicealys/t6-gsc-utils/releases/tags/v3.1.0'
);
});
it.each(['http://github.com/a/b', 'https://github.com.evil.test/a/b', 'https://github.com@127.0.0.1/a/b',
'https://github.com:444/a/b', 'https://github.com/a/b/issues/1', 'https://github.com/a/b/releases#unknown',
'https://github.com/a/b/releases/tag/', 'https://github.com/a/b/releases/tag/v1%0A'])('rejects unsupported URL %s without fetching it', async (input) => {
const { getGithubRelease } = await import('../../src/lib/server/integrations/github-releases.js');
it.each([
'http://github.com/a/b',
'https://github.com.evil.test/a/b',
'https://github.com@127.0.0.1/a/b',
'https://github.com:444/a/b',
'https://github.com/a/b/issues/1',
'https://github.com/a/b/releases#unknown',
'https://github.com/a/b/releases/tag/',
'https://github.com/a/b/releases/tag/v1%0A'
])('rejects unsupported URL %s without fetching it', async (input) => {
const { getGithubRelease } =
await import('../../src/lib/server/integrations/github-releases.js');
await expect(getGithubRelease(input)).rejects.toThrow();
expect(fetchMock).not.toHaveBeenCalled();
});
it('filters out archives, unsafe filenames and oversized assets; shares concurrent lookups', async () => {
const normal = metadata().assets[0];
fetchMock.mockImplementation(async () => Response.json(metadata({ assets: [normal,
{ ...normal, id: 201, name: 'source.zip' }, { ...normal, id: 202, name: '../evil.dll' },
{ ...normal, id: 203, size: 65 * 1024 * 1024 }, { ...normal, id: 204, state: 'new' }] })));
const { getGithubRelease } = await import('../../src/lib/server/integrations/github-releases.js');
fetchMock.mockImplementation(async () =>
Response.json(
metadata({
assets: [
normal,
{ ...normal, id: 201, name: 'source.zip' },
{ ...normal, id: 202, name: '../evil.dll' },
{ ...normal, id: 203, size: 65 * 1024 * 1024 },
{ ...normal, id: 204, state: 'new' }
]
})
)
);
const { getGithubRelease } =
await import('../../src/lib/server/integrations/github-releases.js');
const [first, second] = await Promise.all([getGithubRelease(url), getGithubRelease(url)]);
expect(first.assets).toHaveLength(1);
expect(second).toEqual(first);
@@ -58,7 +113,8 @@ describe('GitHub release lookup', () => {
it('reports rate limits and retries failed lookups', async () => {
fetchMock.mockResolvedValueOnce(new Response(null, { status: 403 }));
const { getGithubRelease } = await import('../../src/lib/server/integrations/github-releases.js');
const { getGithubRelease } =
await import('../../src/lib/server/integrations/github-releases.js');
await expect(getGithubRelease(url)).rejects.toMatchObject({ status: 429 });
await expect(getGithubRelease(url)).resolves.toMatchObject({ tag: 'v3.1.0' });
});
@@ -66,17 +122,51 @@ describe('GitHub release lookup', () => {
describe('GitHub DLL imports', () => {
it('captures provenance and notes, leaves promotion untouched, and deduplicates concurrent imports', async () => {
const { importPluginRelease, pluginSource } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { getScript, getVersion, listVersions } = await import('../../src/lib/server/fleet/scripts.js');
const imported = await importPluginRelease({ url, assetId: 200, name: 'Utilities', scopeType: 'game', scopeValue: 't6' });
const { importPluginRelease, pluginSource } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { getScript, getVersion, listVersions } =
await import('../../src/lib/server/fleet/scripts.js');
const imported = await importPluginRelease({
url,
assetId: 200,
name: 'Utilities',
scopeType: 'game',
scopeValue: 't6'
});
expect(imported.sha256).toBe(sha256);
expect(imported.betaUrl).toBe(`/betas?pluginId=${imported.pluginId}&versionId=${imported.versionId}`);
expect(getScript(imported.pluginId)).toMatchObject({ kind: 'plugin', target_rel_path: 't6-gsc-utils.dll', scope_value: 't6', current_version_id: null });
expect(getVersion(imported.versionId)).toMatchObject({ release_version: 'v3.1.0', notes: metadata().body, state: 'untested' });
expect((await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(imported.versionId)).toEqual(content);
expect(pluginSource(imported.pluginId)).toMatchObject({ repository: 'alicealys/t6-gsc-utils', release_id: 100, asset_id: 200, release_url: url });
const duplicates = await Promise.all([1, 2].map(() => importPluginRelease({ url, assetId: 200, pluginId: imported.pluginId })));
expect(duplicates.every(r => r.alreadyImported && r.versionId === imported.versionId)).toBe(true);
expect(imported.betaUrl).toBe(
`/betas?pluginId=${imported.pluginId}&versionId=${imported.versionId}`
);
expect(getScript(imported.pluginId)).toMatchObject({
kind: 'plugin',
target_rel_path: 't6-gsc-utils.dll',
scope_value: 't6',
current_version_id: null
});
expect(getVersion(imported.versionId)).toMatchObject({
release_version: 'v3.1.0',
notes: metadata().body,
state: 'untested'
});
expect(
(await import('../../src/lib/server/fleet/scripts.js')).getVersionContent(
imported.versionId
)
).toEqual(content);
expect(pluginSource(imported.pluginId)).toMatchObject({
repository: 'alicealys/t6-gsc-utils',
release_id: 100,
asset_id: 200,
release_url: url
});
const duplicates = await Promise.all(
[1, 2].map(() =>
importPluginRelease({ url, assetId: 200, pluginId: imported.pluginId })
)
);
expect(
duplicates.every((r) => r.alreadyImported && r.versionId === imported.versionId)
).toBe(true);
expect(listVersions(imported.pluginId)).toHaveLength(1);
const { GET } = await import('../../src/routes/api/scripts/[id]/+server.js');
const response = await (await GET({ params: { id: String(imported.pluginId) } })).json();
@@ -86,100 +176,205 @@ describe('GitHub DLL imports', () => {
it('follows GitHub asset redirects and verifies the bytes', async () => {
fetchMock.mockImplementation(async (input) => {
if (String(input).startsWith('https://api.github.com/')) return Response.json(metadata());
if (String(input).startsWith('https://github.com/')) return new Response(null, { status: 302, headers: { location: 'https://release-assets.githubusercontent.com/blob/test' } });
if (String(input).startsWith('https://api.github.com/'))
return Response.json(metadata());
if (String(input).startsWith('https://github.com/'))
return new Response(null, {
status: 302,
headers: { location: 'https://release-assets.githubusercontent.com/blob/test' }
});
return new Response(content);
});
const { importPluginRelease } = await import('../../src/lib/server/fleet/plugin-releases.js');
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).resolves.toMatchObject({ sha256 });
const { importPluginRelease } =
await import('../../src/lib/server/fleet/plugin-releases.js');
await expect(
importPluginRelease({ url, assetId: 200, name: 'Utilities' })
).resolves.toMatchObject({ sha256 });
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it.each(['http://127.0.0.1/internal', 'https://evil.test/file.dll', 'https://user:pass@github.com/file.dll'])('blocks unsafe download redirects to %s', async (location) => {
fetchMock.mockImplementation(async (input) => String(input).startsWith('https://api.github.com/')
? Response.json(metadata()) : new Response(null, { status: 302, headers: { location } }));
const { importPluginRelease } = await import('../../src/lib/server/fleet/plugin-releases.js');
it.each([
'http://127.0.0.1/internal',
'https://evil.test/file.dll',
'https://user:pass@github.com/file.dll'
])('blocks unsafe download redirects to %s', async (location) => {
fetchMock.mockImplementation(async (input) =>
String(input).startsWith('https://api.github.com/')
? Response.json(metadata())
: new Response(null, { status: 302, headers: { location } })
);
const { importPluginRelease } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { listScripts } = await import('../../src/lib/server/fleet/scripts.js');
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(/unsupported host/);
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(
/unsupported host/
);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(listScripts()).toHaveLength(0);
});
it('rejects checksum mismatches and truncated downloads without creating a plugin', async () => {
const { importPluginRelease } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { importPluginRelease } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { listScripts } = await import('../../src/lib/server/fleet/scripts.js');
fetchMock.mockImplementation(async (input) => String(input).startsWith('https://api.github.com/')
? Response.json(metadata()) : new Response(Buffer.alloc(content.length)));
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(/checksum/);
fetchMock.mockImplementation(async (input) => String(input).startsWith('https://api.github.com/')
? Response.json(metadata()) : new Response(Buffer.alloc(1)));
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(/size differs/);
fetchMock.mockImplementation(async (input) =>
String(input).startsWith('https://api.github.com/')
? Response.json(metadata())
: new Response(Buffer.alloc(content.length))
);
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(
/checksum/
);
fetchMock.mockImplementation(async (input) =>
String(input).startsWith('https://api.github.com/')
? Response.json(metadata())
: new Response(Buffer.alloc(1))
);
await expect(importPluginRelease({ url, assetId: 200, name: 'Utilities' })).rejects.toThrow(
/size differs/
);
expect(listScripts()).toHaveLength(0);
});
it('rejects a missing asset and refuses to import into non-plugins or active betas', async () => {
const { importPluginRelease } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { importPluginRelease } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
await expect(importPluginRelease({ url, assetId: 999, name: 'Utilities' })).rejects.toThrow(/no longer available/);
await expect(importPluginRelease({ url, assetId: 999, name: 'Utilities' })).rejects.toThrow(
/no longer available/
);
const script = createScript({ name: 'Script', targetRelPath: 's.gsc', content });
await expect(importPluginRelease({ url, assetId: 200, pluginId: script.script.id })).rejects.toMatchObject({ status: 404 });
const plugin = createScript({ name: 'Plugin', kind: 'plugin', targetRelPath: 'p.dll', content });
const beta = db().prepare("INSERT INTO betas (script_id, script_version_id, trigger_kind, state) VALUES (?, ?, 'version', 'running')")
await expect(
importPluginRelease({ url, assetId: 200, pluginId: script.script.id })
).rejects.toMatchObject({ status: 404 });
const plugin = createScript({
name: 'Plugin',
kind: 'plugin',
targetRelPath: 'p.dll',
content
});
const beta = db()
.prepare(
"INSERT INTO betas (script_id, script_version_id, trigger_kind, state) VALUES (?, ?, 'version', 'running')"
)
.run(plugin.script.id, plugin.version.id);
db().prepare('INSERT INTO beta_scripts (beta_id, script_id, script_version_id, kind) VALUES (?, ?, ?, ?)')
db()
.prepare(
'INSERT INTO beta_scripts (beta_id, script_id, script_version_id, kind) VALUES (?, ?, ?, ?)'
)
.run(beta.lastInsertRowid, plugin.script.id, plugin.version.id, 'plugin');
await expect(importPluginRelease({ url, assetId: 200, pluginId: plugin.script.id })).rejects.toMatchObject({ status: 409 });
await expect(
importPluginRelease({ url, assetId: 200, pluginId: plugin.script.id })
).rejects.toMatchObject({ status: 409 });
});
});
describe('plugin update discovery', () => {
it('finds newer stable releases, imports into the same plugin, and distinguishes imported from promoted', async () => {
const { importPluginRelease, checkPluginUpdate } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { importPluginRelease, checkPluginUpdate } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { getScript, listVersions } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
const first = await importPluginRelease({ url, assetId: 200, name: 'Utilities' });
const next = metadata({ id: 101, tag_name: 'v3.1.1', published_at: '2026-08-11T12:00:00Z',
assets: [{ ...metadata().assets[0], id: 201 }] });
fetchMock.mockImplementation(async (input) => String(input).startsWith('https://api.github.com/') ? Response.json(next) : new Response(content));
const next = metadata({
id: 101,
tag_name: 'v3.1.1',
published_at: '2026-08-11T12:00:00Z',
assets: [{ ...metadata().assets[0], id: 201 }]
});
fetchMock.mockImplementation(async (input) =>
String(input).startsWith('https://api.github.com/')
? Response.json(next)
: new Response(content)
);
const update = await checkPluginUpdate(first.pluginId);
expect(update).toMatchObject({ status: 'available', release: { tag: 'v3.1.1' } });
const second = await importPluginRelease({ url: update.release!.url, assetId: 201, pluginId: first.pluginId });
const second = await importPluginRelease({
url: update.release!.url,
assetId: 201,
pluginId: first.pluginId
});
expect(listVersions(first.pluginId)).toHaveLength(2);
expect(getScript(first.pluginId)!.current_version_id).toBeNull();
expect(await checkPluginUpdate(first.pluginId)).toMatchObject({ status: 'imported', versionId: second.versionId });
db().prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?').run(second.versionId, first.pluginId);
expect(await checkPluginUpdate(first.pluginId)).toMatchObject({ status: 'current', versionId: second.versionId });
expect(await checkPluginUpdate(first.pluginId)).toMatchObject({
status: 'imported',
versionId: second.versionId
});
db()
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
.run(second.versionId, first.pluginId);
expect(await checkPluginUpdate(first.pluginId)).toMatchObject({
status: 'current',
versionId: second.versionId
});
});
it('does not recommend an older stable release after importing a newer prerelease', async () => {
const { importPluginRelease, checkPluginUpdate } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { importPluginRelease, checkPluginUpdate } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const stable = await importPluginRelease({ url, assetId: 200, name: 'Utilities' });
fetchMock.mockImplementation(async (input) => String(input).startsWith('https://api.github.com/') ? Response.json(metadata({ id: 105, tag_name: 'v4.0-rc1', prerelease: true, published_at: '2026-09-01T12:00:00Z' })) : new Response(content));
const imported = await importPluginRelease({ url: url.replace('v3.1.0', 'v4.0-rc1'), assetId: 200, pluginId: stable.pluginId });
fetchMock.mockImplementation(async (input) =>
String(input).startsWith('https://api.github.com/')
? Response.json(
metadata({
id: 105,
tag_name: 'v4.0-rc1',
prerelease: true,
published_at: '2026-09-01T12:00:00Z'
})
)
: new Response(content)
);
const imported = await importPluginRelease({
url: url.replace('v3.1.0', 'v4.0-rc1'),
assetId: 200,
pluginId: stable.pluginId
});
fetchMock.mockImplementation(async () => Response.json(metadata()));
expect(await checkPluginUpdate(imported.pluginId)).toMatchObject({ status: 'ahead' });
});
it('reports untracked and GitHub failures per plugin without breaking the list', async () => {
const { importPluginRelease } = await import('../../src/lib/server/fleet/plugin-releases.js');
const { importPluginRelease } =
await import('../../src/lib/server/fleet/plugin-releases.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
await importPluginRelease({ url, assetId: 200, name: 'Imported' });
createScript({ name: 'Manual', kind: 'plugin', targetRelPath: 'manual.dll', content });
fetchMock.mockResolvedValue(new Response(null, { status: 404 }));
const { GET } = await import('../../src/routes/api/plugins/updates/+server.js');
const result = await (await GET({ url: new URL('http://localhost/api/plugins/updates') })).json();
expect(result.updates.map((u: { status: string }) => u.status)).toEqual(['error', 'untracked']);
const result = await (
await GET({ url: new URL('http://localhost/api/plugins/updates') })
).json();
expect(result.updates.map((u: { status: string }) => u.status)).toEqual([
'error',
'untracked'
]);
});
it('serves the preview and import endpoints and validates bad requests', async () => {
const { GET } = await import('../../src/routes/api/plugins/github/release/+server.js');
const { POST } = await import('../../src/routes/api/plugins/github/import/+server.js');
const preview = await GET({ url: new URL(`http://localhost/api/plugins/github/release?url=${encodeURIComponent(url)}`) });
const preview = await GET({
url: new URL(
`http://localhost/api/plugins/github/release?url=${encodeURIComponent(url)}`
)
});
expect((await preview.json()).release.assets[0].id).toBe(200);
const request = (body: unknown) => new Request('http://localhost/api/plugins/github/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const request = (body: unknown) =>
new Request('http://localhost/api/plugins/github/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
expect((await POST({ request: request(null) })).status).toBe(400);
expect((await POST({ request: request({ url, assetId: 200, name: 'Test', scopeType: 'other' }) })).status).toBe(400);
expect(
(
await POST({
request: request({ url, assetId: 200, name: 'Test', scopeType: 'other' })
})
).status
).toBe(400);
const response = await POST({ request: request({ url, assetId: 200, name: 'Test' }) });
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ ok: true, sha256, releaseVersion: 'v3.1.0' });