diff --git a/package.json b/package.json index f91e42b..df1678e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,6 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "test": "node --test tests/*.test.mjs" + "test": "node --test tests/*.test.mjs tests/unit/*.test.mjs" } } diff --git a/src/lib/projectAssign.js b/src/lib/projectAssign.js new file mode 100644 index 0000000..9f0b7f9 --- /dev/null +++ b/src/lib/projectAssign.js @@ -0,0 +1,45 @@ +export const GITEA_ORG = 'WEBklar' + +export function isOutsideOrg(repoFullName) { + if (!repoFullName) return false + const org = repoFullName.split('/')[0] || '' + return org.toLowerCase() !== GITEA_ORG.toLowerCase() +} + +export function needsPreviewButton(project) { + return !project.previewUrl || project.status !== 'deployed' +} + +export function assignSummary(projectName, result) { + if (result && result.error) { + return { text: `${projectName}: Zuweisen fehlgeschlagen – ${result.error}`, warnings: [] } + } + + const { moved, backfilled, warnings = [] } = result + const parts = [] + if (moved) { + parts.push('nach WEBklar verschoben') + } + if (backfilled === 1) { + parts.push('1 Push nachgetragen') + } else if (backfilled > 1) { + parts.push(`${backfilled} Pushes nachgetragen`) + } else { + parts.push('keine neuen Pushes') + } + + return { text: `${projectName}: ${parts.join(' · ')}`, warnings } +} + +export async function assignSequentially(projectIds, ctx, assignFn) { + const results = [] + for (const projectId of projectIds) { + try { + const result = await assignFn(projectId, ctx) + results.push({ projectId, ok: true, result }) + } catch (err) { + results.push({ projectId, ok: false, error: err.message }) + } + } + return results +} diff --git a/src/lib/subdomain.js b/src/lib/subdomain.js new file mode 100644 index 0000000..0a5b2bd --- /dev/null +++ b/src/lib/subdomain.js @@ -0,0 +1,22 @@ +const UMLAUT_MAP = { + ä: 'ae', + ö: 'oe', + ü: 'ue', + ß: 'ss', +} + +export function suggestSubdomain(customerName, projectName) { + const base = (customerName || '').trim() || (projectName || '').trim() + if (!base) return '' + + let value = base.toLowerCase() + value = value.replace(/[äöüß]/g, (ch) => UMLAUT_MAP[ch]) + value = value.normalize('NFD').replace(/[̀-ͯ]/g, '') + value = value.replace(/['’‘`]/g, '') + value = value.replace(/[^a-z0-9]+/g, '-') + value = value.replace(/^-+|-+$/g, '') + value = value.slice(0, 63) + value = value.replace(/-+$/g, '') + + return value +} diff --git a/tests/unit/load.mjs b/tests/unit/load.mjs new file mode 100644 index 0000000..a174879 --- /dev/null +++ b/tests/unit/load.mjs @@ -0,0 +1,20 @@ +import esbuild from 'esbuild' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(__dirname, '../..') + +export async function loadModule(relPathFromRepoRoot) { + const entryPoint = path.join(repoRoot, relPathFromRepoRoot) + const result = await esbuild.build({ + entryPoints: [entryPoint], + bundle: true, + format: 'esm', + write: false, + platform: 'neutral', + }) + const code = result.outputFiles[0].text + const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString('base64')}` + return import(dataUrl) +} diff --git a/tests/unit/projectAssign.test.mjs b/tests/unit/projectAssign.test.mjs new file mode 100644 index 0000000..7a3dfe7 --- /dev/null +++ b/tests/unit/projectAssign.test.mjs @@ -0,0 +1,61 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { isOutsideOrg, needsPreviewButton, assignSummary, assignSequentially } = await loadModule( + 'src/lib/projectAssign.js' +) + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +test('isOutsideOrg', () => { + assert.equal(isOutsideOrg('JUSN/D-ner-Nation'), true) + assert.equal(isOutsideOrg('webklar/x'), false) + assert.equal(isOutsideOrg(''), false) +}) + +test('needsPreviewButton', () => { + assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'deployed' }), false) + assert.equal(needsPreviewButton({ previewUrl: '', status: 'synced' }), true) + assert.equal(needsPreviewButton({ previewUrl: 'https://x', status: 'pending' }), true) +}) + +test('assignSummary', () => { + assert.deepEqual( + assignSummary('Döner Nation', { + moved: { from: 'JUSN/D-ner-Nation', to: 'WEBklar/D-ner-Nation' }, + backfilled: 1, + warnings: [], + }), + { text: 'Döner Nation: nach WEBklar verschoben · 1 Push nachgetragen', warnings: [] } + ) + assert.deepEqual( + assignSummary('X', { moved: null, backfilled: 0, warnings: ['Internes Repo, bleibt wo es ist.'] }), + { text: 'X: keine neuen Pushes', warnings: ['Internes Repo, bleibt wo es ist.'] } + ) + assert.equal(assignSummary('X', { moved: null, backfilled: 3, warnings: [] }).text, 'X: 3 Pushes nachgetragen') + assert.deepEqual(assignSummary('X', { error: 'boom' }), { text: 'X: Zuweisen fehlgeschlagen – boom', warnings: [] }) +}) + +test('nacheinander und weiter nach Fehler', async () => { + const log = [] + const fn = async (id) => { + log.push(id + ':start') + await sleep(10) + log.push(id + ':end') + if (id === 'b') throw new Error('boom') + return { backfilled: 0 } + } + const r = await assignSequentially(['a', 'b', 'c'], { ticketId: 't1' }, fn) + assert.deepEqual(log, ['a:start', 'a:end', 'b:start', 'b:end', 'c:start', 'c:end']) + assert.deepEqual( + r.map((x) => [x.projectId, x.ok, x.error]), + [ + ['a', true, undefined], + ['b', false, 'boom'], + ['c', true, undefined], + ] + ) +}) diff --git a/tests/unit/subdomain.test.mjs b/tests/unit/subdomain.test.mjs new file mode 100644 index 0000000..bff1f6d --- /dev/null +++ b/tests/unit/subdomain.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { suggestSubdomain } = await loadModule('src/lib/subdomain.js') + +test('Kundenname vor Projektname', () => { + assert.equal(suggestSubdomain('Döner Nation', 'D-ner-Nation'), 'doener-nation') +}) + +test('Apostroph', () => { + assert.equal(suggestSubdomain("Emma's Schatzkiste", ''), 'emmas-schatzkiste') +}) + +test('Sonderzeichen', () => { + assert.equal(suggestSubdomain('', 'Café Müller & Söhne + Straße'), 'cafe-mueller-soehne-strasse') +}) + +test('leer', () => { + assert.equal(suggestSubdomain('', ''), '') +}) + +test('63 Zeichen ohne Bindestrich am Ende', () => { + assert.equal(suggestSubdomain('a'.repeat(62) + ' b', ''), 'a'.repeat(62)) +})