import { test, before, after } from 'node:test' import assert from 'node:assert/strict' import { startFakeGitea } from './helpers/fakeGitea.js' let fake let pushesFromFeed let listRecentPushes const now = new Date('2026-09-27T12:00:00Z') let nextFeedId = 1 /** * Baut einen Gitea-1.25-Feed-Eintrag vom Typ `commit_repo` nach: `{ op_type, act_user, * ref_name, created, content }`, wobei `content` das JSON `{Commits:[...], HeadCommit:{...}}` * als String enthaelt (siehe Task-Brief). * * Optionen: `sha` (ein Commit) oder `commits` (mehrere Shas, in Feed-Reihenfolge), `head` * (HeadCommit-Sha; `null` fuer "kein HeadCommit"; Voreinstellung: erster Commit), `at` * (created/Timestamp, ISO-String), `user` (act_user.login), `fullName` (act_user.full_name), * `msg` (Commit-Message, sonst generisch). */ function pushEntry({ sha, commits, head, at, user, fullName, msg } = {}) { const shas = commits || [sha ?? 'c0'] const login = user || 'JUSN' const created = at || '2026-09-27T00:00:00Z' const builtCommits = shas.map((s) => ({ Sha1: s, Message: msg || `Commit ${s}`, AuthorEmail: `${login.toLowerCase()}@example.com`, AuthorName: login, CommitterEmail: `${login.toLowerCase()}@example.com`, CommitterName: login, Timestamp: created, })) const headSha = head === undefined ? shas[0] : head return { id: nextFeedId++, op_type: 'commit_repo', act_user: { login, full_name: fullName || '' }, ref_name: 'refs/heads/main', created, content: JSON.stringify({ Commits: builtCommits, HeadCommit: headSha ? { Sha1: headSha } : null, CompareURL: '', Len: builtCommits.length, }), } } /** Push-Feed-Eintrag `days` Tage vor `base`, ein Commit mit Sha `sha`. */ function daysAgo(base, days, sha) { return pushEntry({ sha, at: new Date(base.getTime() - days * 864e5).toISOString() }) } /** 120 Eintraege, 3 pro Tag ueber 40 Tage, neueste zuerst (fuer die Blaetter-Grenze). */ function manyPushes(base) { const entries = [] for (let d = 0; d < 40; d++) { for (let k = 0; k < 3; k++) { const at = new Date(base.getTime() - d * 864e5 - (1 + 2 * k) * 36e5).toISOString() entries.push(pushEntry({ sha: `d${d}k${k}`, at })) } } return entries } /** * Mehr als 40*50 Eintraege, alle nur Minuten auseinander und damit weit innerhalb jedes * 30-Tage-Fensters - fuer den Test der Blaetter-Obergrenze (die Seite wird nie leer/kurz, * und der "aeltester Eintrag vor since"-Stopp greift nie). */ function packedPushes(base, count) { const entries = [] for (let i = 0; i < count; i++) { entries.push(pushEntry({ sha: `p${i}`, at: new Date(base.getTime() - i * 60000).toISOString() })) } return entries } before(async () => { fake = await startFakeGitea({ repos: [{ full_name: 'knso/viel' }, { full_name: 'knso/endlos' }], feeds: { 'knso/viel': manyPushes(now), 'knso/endlos': packedPushes(now, 2005), }, }) Object.assign(process.env, { GITEA_BASE_URL: fake.baseUrl, GITEA_API_TOKEN: 'test-token', }) // config.js liest die Umgebung beim Import ;({ pushesFromFeed, listRecentPushes } = await import('../server/services/pushHistory.js')) }) after(async () => { await fake.close() }) test('nur commit_repo mit Commits', () => { const feed = [pushEntry({ sha: 'c1', at: '2026-09-27T14:46:34Z' }), { ...pushEntry({ at: '2026-09-27T14:46:33Z' }), content: '' }, { op_type: 'create_repo', created: '2026-09-27T14:02:46Z', act_user: { login: 'JUSN' } }] assert.equal(pushesFromFeed(feed, { since: new Date('2026-09-01') }).length, 1) }) test('Felder wie beim Webhook', () => { const [p] = pushesFromFeed([pushEntry({ sha: 'c1', user: 'JUSN', at: '2026-09-27T14:46:34Z', msg: 'Döner Nation Website hinzufügen' })], { since: new Date('2026-09-01') }) assert.deepEqual(p, { branch: 'main', commitSha: 'c1', pushedAt: new Date('2026-09-27T14:46:34Z'), pusher: { login: 'JUSN', username: 'JUSN', full_name: '' }, commits: [{ id: 'c1', message: 'Döner Nation Website hinzufügen', author: { name: 'JUSN' } }] }) }) test('HeadCommit bestimmt commitSha', () => { const [p] = pushesFromFeed([pushEntry({ commits: ['c2', 'c1'], head: 'c2' })], { since: new Date(0) }) assert.equal(p.commitSha, 'c2'); assert.equal(p.commits.length, 2) }) test('HeadCommit hat Vorrang vor dem ersten Commit', () => { const [p] = pushesFromFeed([pushEntry({ commits: ['c1', 'c2'], head: 'c2' })], { since: new Date(0) }) assert.equal(p.commitSha, 'c2') }) test('ohne HeadCommit gilt der erste Commit', () => { const [p] = pushesFromFeed([pushEntry({ commits: ['c2', 'c1'], head: null })], { since: new Date(0) }) assert.equal(p.commitSha, 'c2') }) test('Anzeigename wird übernommen', () => { const [p] = pushesFromFeed([pushEntry({ user: 'knso', fullName: 'Kenso Grimm' })], { since: new Date(0) }) assert.equal(p.pusher.full_name, 'Kenso Grimm') }) test('älteste zuerst und 30-Tage-Grenze', () => { const now = new Date('2026-09-27T12:00:00Z') const feed = [daysAgo(now, 1, 'n1'), daysAgo(now, 2, 'n2'), daysAgo(now, 31, 'alt')] assert.deepEqual(pushesFromFeed(feed, { since: new Date(now - 30 * 864e5) }).map((p) => p.commitSha), ['n2', 'n1']) }) test('listRecentPushes blättert nur so weit wie nötig', async () => { // 120 Einträge, 3 pro Tag über 40 Tage, neueste zuerst; limit 50 const r = await listRecentPushes('knso/viel', { now }) assert.equal(r.length, 90) assert.equal(fake.calls.filter((c) => c.path.includes('/activities/feeds')).length, 2) }) test('Blättern hat eine Obergrenze', async () => { // Feed liefert auf jeder Seite 50 volle, aktuelle Einträge - ohne Obergrenze würde // listRecentPushes endlos weiterblättern. const originalWarn = console.warn console.warn = () => {} let r try { r = await listRecentPushes('knso/endlos', { now }) } finally { console.warn = originalWarn } assert.equal(r.length, 2000) assert.equal(fake.calls.filter((c) => c.path.includes('/knso/endlos/activities/feeds')).length, 40) })