Subdomain-Vorschlag und Zuweisungs-Logik mit Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c24b3d43c8
commit
cc59e7978e
6 files changed
+174
-1
No files matched your search
+1
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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],
|
||||
]
|
||||
)
|
||||
})
|
||||
@@ -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))
|
||||
})
|
||||
Reference in new issue
Block a user