diff --git a/server/services/previewDeploy.js b/server/services/previewDeploy.js index df437c3..4e1eb52 100644 --- a/server/services/previewDeploy.js +++ b/server/services/previewDeploy.js @@ -3,7 +3,8 @@ import { promisify } from 'node:util' import fs from 'node:fs/promises' import path from 'node:path' import { config } from '../config.js' -import { listDocuments, updateDocument, Query } from './appwriteAdmin.js' +import { listDocuments, updateDocument, getDocument, Query } from './appwriteAdmin.js' +import { getFeatureDoc } from './siteFeatures.js' import { withLocks } from './keyedLock.js' import { giteaFetch, splitRepo } from './giteaClient.js' import { envKey, decryptEnv, maskLog } from './previewEnv.js' @@ -669,7 +670,7 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job, } if (run.result === 'dev') { await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' }) - try { await runnerClient.startDev(subdomain, { ...job, mode: 'dev', displayName: undefined }) } catch (err) { + try { await runnerClient.startDev(subdomain, { ...job, mode: 'dev', displayName: undefined, analytics: undefined }) } catch (err) { console.error('[previewDeploy] Dev-Registrierung fehlgeschlagen:', err.message) } } else { @@ -696,6 +697,36 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job, } } +/** + * Analyse-Feld fuer den Build-Auftrag: nur bei aktiver Funktion 'analyse', oeffentlichem Projekt mit Subdomain + * und vollstaendigen Datenschutz-Angaben (Firma + E-Mail Pflicht, Adresse optional). Sonst null. + * Wirft nie: der Build darf an der Analyse nicht scheitern. + */ +async function buildAnalytics(project) { + try { + if (!project?.$id || project.isPublic !== true || !project.subdomain || !project.customerId) return null + const feature = await getFeatureDoc(project.customerId, project.$id, 'analyse') + if (!feature || feature.state !== 'active' || !feature.siteKey) return null + let customer = null + try { + customer = await getDocument(config.collections.customers, project.customerId) + } catch (err) { + if (err.status !== 404) throw err + } + const company = String(customer?.companyName || customer?.name || '').trim() + const email = String(customer?.email || '').trim() + if (!company || !email) { + console.warn(`[previewDeploy] Analyse fuer Projekt ${project.$id} uebersprungen: Firma oder E-Mail fehlt`) + return null + } + const address = [String(customer.street || '').trim(), `${customer.postalCode || ''} ${customer.city || ''}`.trim()].filter(Boolean).join(', ') + return { siteKey: feature.siteKey, color: feature.accentColor || '#111111', privacy: { company, address, email } } + } catch (err) { + console.warn(`[previewDeploy] Analyse-Abfrage fuer Projekt ${project?.$id} fehlgeschlagen: ${err.message}`) + return null + } +} + async function runDeployViaRunner(repoFullName, branch, previewConfig, { project, sha } = {}) { const subdomain = previewConfig.subdomain || repoFullName.split('/').pop() // Platz fuer diesen Aufruf sofort belegen (vor jedem await): aeltere Verfolger scheiden aus @@ -736,6 +767,7 @@ async function runDeployViaRunner(repoFullName, branch, previewConfig, { project ...(previewConfig.index ? { index: previewConfig.index } : {}), ...(displayName ? { displayName } : {}), env, + analytics: await buildAnalytics(doc), } const { runId } = await runnerClient.submitBuild(job) if (current()) await patchProject(repoFullName, doc, { previewState: 'building', previewError: warning }) diff --git a/test/previewRunner.test.js b/test/previewRunner.test.js index 67c05fa..1edb71a 100644 --- a/test/previewRunner.test.js +++ b/test/previewRunner.test.js @@ -112,7 +112,7 @@ test('an: Auftrag mit entschlüsselten Variablen, sofortige Rückgabe, Ergebnis assert.equal(aw.get('websiteProjects', id).previewState, 'building') const [sub] = submits() assert.equal(sub.auth, 'Bearer sekret') - assert.deepEqual(sub.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'auto', displayName: 'Mein Projekt', env: { API_URL: 'https://x.test' } }) + assert.deepEqual(sub.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'auto', displayName: 'Mein Projekt', env: { API_URL: 'https://x.test' }, analytics: null }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'online') @@ -366,3 +366,78 @@ test('dev-Ergebnis: PUT /dev/ mit vollständigem Auftrag inkl. Variablen', const put = rs.calls.find((c) => c.method === 'PUT') assert.deepEqual(put.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'dev', env: { A: 'wert1234' } }) }) + +// ---- Analyse-Feld im Build-Auftrag (Task 10) ---- +async function analyseProject({ feature = {}, customer = {}, project: pextra = {}, noFeature = false, noCustomer = false } = {}) { + const { createDocument } = await import('../server/services/appwriteAdmin.js') + const cid = `cust-an-${n + 1}` + const p = await project({ customerId: cid, subdomain: `an-${n + 1}`, isPublic: true, ...pextra }) + if (!noCustomer) { + await createDocument('customers', { name: 'Max Muster', companyName: 'Muster GmbH', email: 'info@muster.test', street: 'Hauptstr. 1', postalCode: '12345', city: 'Berlin', ...customer }, cid) + } + if (!noFeature) { + await createDocument('portalFeatures', { customerId: cid, projectId: p.id, featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'abcdef0123456789abcd', accentColor: '#ff0066', ...feature }, `f_${p.id}_analyse`) + } + return p +} +async function submitFor(p) { + await pd.runDeploy(p.repo, 'main', cfg(p.repo), { project: p.doc, sha: SHA }) + await pd.waitForRunFollowers() + return submits().at(-1).body +} + +test('Analyse: aktiv + öffentlich ⇒ analytics mit siteKey, Farbe, Datenschutzangaben', async () => { + const body = await submitFor(await analyseProject()) + assert.deepEqual(body.analytics, { + siteKey: 'abcdef0123456789abcd', + color: '#ff0066', + privacy: { company: 'Muster GmbH', address: 'Hauptstr. 1, 12345 Berlin', email: 'info@muster.test' }, + }) +}) + +test('Analyse: Standardfarbe, Firma fällt auf Name zurück, Adresse optional', async () => { + const p = await analyseProject({ feature: { accentColor: '' }, customer: { companyName: '', street: '', postalCode: '', city: '' } }) + const body = await submitFor(p) + assert.equal(body.analytics.color, '#111111') + assert.equal(body.analytics.privacy.company, 'Max Muster') + assert.equal(body.analytics.privacy.address, '') +}) + +test('Analyse: aus ⇒ null', async () => { + assert.equal((await submitFor(await analyseProject({ feature: { state: 'off', enabled: false } }))).analytics, null) + assert.equal((await submitFor(await analyseProject({ noFeature: true }))).analytics, null) +}) + +test('Analyse: nicht öffentlich oder ohne Subdomain ⇒ null', async () => { + assert.equal((await submitFor(await analyseProject({ project: { isPublic: false } }))).analytics, null) + assert.equal((await submitFor(await analyseProject({ project: { subdomain: '' } }))).analytics, null) +}) + +test('Analyse: fehlende E-Mail oder Firma ⇒ null mit Warnung, Build läuft trotzdem', async (t) => { + const warn = t.mock.method(console, 'warn', () => {}) + const a = await analyseProject({ customer: { email: '' } }) + assert.equal((await submitFor(a)).analytics, null) + assert.ok(warn.mock.calls.some((c) => String(c.arguments[0]).includes(a.id))) + assert.equal((await submitFor(await analyseProject({ customer: { companyName: '', name: '' } }))).analytics, null) + assert.equal((await submitFor(await analyseProject({ noCustomer: true }))).analytics, null) +}) + +test('Analyse: Fehler bei der Funktions-Abfrage ⇒ null, Auftrag wird trotzdem gesendet', async (t) => { + t.mock.method(console, 'warn', () => {}) + const p = await analyseProject() + aw.failNext({ method: 'GET', pathIncludes: 'portalFeatures', status: 500 }) + const before = submits().length + const body = await submitFor(p) + assert.equal(submits().length, before + 1) + assert.equal(body.analytics, null) +}) + +test('Analyse: Dev-PUT nach erfolgreichem Build (Modus dev) enthält kein analytics', async () => { + rs.script = done({ result: 'dev' }) + const p = await analyseProject({ project: { previewMode: 'dev' } }) + const body = await submitFor(p) + assert.ok(body.analytics) + const puts = rs.calls.filter((c) => c.method === 'PUT' && c.url.startsWith('/dev/')) + assert.ok(puts.length >= 1) + for (const c of puts) assert.equal('analytics' in c.body, false) +})