diff --git a/app/test/gitea-client.test.js b/app/test/gitea-client.test.js new file mode 100644 index 0000000..1a70a9c --- /dev/null +++ b/app/test/gitea-client.test.js @@ -0,0 +1,60 @@ +import { test, beforeEach, afterEach } from 'node:test' +import assert from 'node:assert/strict' +import { GiteaClient } from '../gitea-client.js' + +const realFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = realFetch +}) + +test('建構子在有 token 時帶上 Authorization 標頭', () => { + const client = new GiteaClient({ token: 'abc' }) + assert.equal(client.headers.Authorization, 'token abc') +}) + +test('建構子在無 token 時不帶 Authorization 標頭', () => { + const client = new GiteaClient({}) + assert.equal(client.headers.Authorization, undefined) +}) + +test('fetchAllPages 逐頁讀取直到空陣列', async () => { + const pages = { + 1: [{ id: 1 }, { id: 2 }], + 2: [{ id: 3 }], + 3: [], + } + const requested = [] + globalThis.fetch = async (url) => { + const page = new URL(url).searchParams.get('page') + requested.push(page) + return { ok: true, json: async () => pages[page] } + } + + const client = new GiteaClient({}) + const items = await client.fetchAllPages('https://example.com/api') + + assert.deepEqual( + items.map((i) => i.id), + [1, 2, 3], + ) + assert.deepEqual(requested, ['1', '2', '3']) +}) + +test('fetchAllPages 在 HTTP 錯誤時丟出例外', async () => { + globalThis.fetch = async () => ({ ok: false, status: 500 }) + const client = new GiteaClient({}) + await assert.rejects( + () => client.fetchAllPages('https://example.com/api'), + /HTTP 500/, + ) +}) + +test('deleteResource 回傳 HTTP 狀態碼', async () => { + globalThis.fetch = async (url, opts) => { + assert.equal(opts.method, 'DELETE') + return { status: 204 } + } + const client = new GiteaClient({}) + assert.equal(await client.deleteResource('https://example.com/api/1'), 204) +}) diff --git a/app/test/releases.test.js b/app/test/releases.test.js new file mode 100644 index 0000000..b438545 --- /dev/null +++ b/app/test/releases.test.js @@ -0,0 +1,34 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { selectReleasesToDelete } from '../releases.js' + +const releases = [ + { id: 1, tag_name: 'v1.0.0', created_at: '2024-01-01T00:00:00Z' }, + { id: 3, tag_name: 'v3.0.0', created_at: '2024-03-01T00:00:00Z' }, + { id: 2, tag_name: 'v2.0.0', created_at: '2024-02-01T00:00:00Z' }, +] + +test('selectReleasesToDelete 保留最新的 keepCount 筆,刪除其餘', () => { + const toDelete = selectReleasesToDelete(releases, 2) + assert.equal(toDelete.length, 1) + assert.equal(toDelete[0].tag_name, 'v1.0.0') +}) + +test('selectReleasesToDelete 在數量不超過保留數時回傳空陣列', () => { + assert.deepEqual(selectReleasesToDelete(releases, 3), []) + assert.deepEqual(selectReleasesToDelete(releases, 5), []) +}) + +test('selectReleasesToDelete 在 keepCount 為 0 時刪除全部(由新到舊)', () => { + const toDelete = selectReleasesToDelete(releases, 0) + assert.deepEqual( + toDelete.map((r) => r.tag_name), + ['v3.0.0', 'v2.0.0', 'v1.0.0'], + ) +}) + +test('selectReleasesToDelete 不改動原陣列', () => { + const original = [...releases] + selectReleasesToDelete(releases, 1) + assert.deepEqual(releases, original) +}) diff --git a/app/test/tags.test.js b/app/test/tags.test.js new file mode 100644 index 0000000..d88abb2 --- /dev/null +++ b/app/test/tags.test.js @@ -0,0 +1,40 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { categorizeTags } from '../tags.js' + +const tags = [ + { name: 'v3.0.0' }, + { name: 'v2.0.0' }, + { name: 'v1.0.0' }, + { name: '' }, +] + +test('categorizeTags 保留仍被 release 指定的 tag', () => { + const result = categorizeTags(tags, ['v3.0.0', 'v2.0.0']) + const byName = Object.fromEntries( + result.map(({ tag, action }) => [tag.name, action]), + ) + assert.equal(byName['v3.0.0'], 'keep') + assert.equal(byName['v2.0.0'], 'keep') +}) + +test('categorizeTags 將未指定 release 的 tag 標記為刪除', () => { + const result = categorizeTags(tags, ['v3.0.0']) + const v1 = result.find(({ tag }) => tag.name === 'v1.0.0') + assert.equal(v1.action, 'delete') +}) + +test('categorizeTags 將無名稱的 tag 標記為略過', () => { + const result = categorizeTags(tags, []) + const empty = result.find(({ tag }) => tag.name === '') + assert.equal(empty.action, 'skip') +}) + +test('categorizeTags 在沒有任何 release 指定時刪除所有具名 tag', () => { + const result = categorizeTags(tags, []) + const deletes = result.filter(({ action }) => action === 'delete') + assert.deepEqual( + deletes.map(({ tag }) => tag.name), + ['v3.0.0', 'v2.0.0', 'v1.0.0'], + ) +}) diff --git a/app/test/validate.test.js b/app/test/validate.test.js new file mode 100644 index 0000000..d4b39d9 --- /dev/null +++ b/app/test/validate.test.js @@ -0,0 +1,37 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { isEmptyOrNull, requireValue, requireInteger } from '../validate.js' + +test('isEmptyOrNull 對空值回傳 true', () => { + assert.equal(isEmptyOrNull(''), true) + assert.equal(isEmptyOrNull(null), true) + assert.equal(isEmptyOrNull(undefined), true) + assert.equal(isEmptyOrNull('null'), true) +}) + +test('isEmptyOrNull 對有效值回傳 false', () => { + assert.equal(isEmptyOrNull('value'), false) + assert.equal(isEmptyOrNull('0'), false) + assert.equal(isEmptyOrNull(0), false) +}) + +test('requireValue 對空值丟出錯誤', () => { + assert.throws(() => requireValue('NAME', ''), /NAME is required/) + assert.throws(() => requireValue('NAME', 'null'), /NAME is required/) +}) + +test('requireValue 對有效值不丟出錯誤', () => { + assert.doesNotThrow(() => requireValue('NAME', 'value')) +}) + +test('requireInteger 接受非負整數', () => { + assert.doesNotThrow(() => requireInteger('KEEP_COUNT', '0')) + assert.doesNotThrow(() => requireInteger('KEEP_COUNT', '10')) + assert.doesNotThrow(() => requireInteger('KEEP_COUNT', 5)) +}) + +test('requireInteger 拒絕非整數或負數', () => { + assert.throws(() => requireInteger('KEEP_COUNT', '-1'), /non-negative integer/) + assert.throws(() => requireInteger('KEEP_COUNT', '1.5'), /non-negative integer/) + assert.throws(() => requireInteger('KEEP_COUNT', 'abc'), /non-negative integer/) +})