Terminplaner: Staff-Aktivierung nur mit Zustimmung in Textform (Modus on), Portal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b5b4b90eb
commit
bade4edb74
5 files changed
+381
-7
No files matched your search
@@ -8,7 +8,7 @@ import { schemaKeys } from './support/schema.js'
|
||||
// Terminplaner als zweite Funktion (Aufgabe 17): Team-Route /:customerId/:key, GET /api/funktionen mit Schalter,
|
||||
// Kunde loeschen schaltet den Terminplaner ab und entfernt seine Entwuerfe (C6). News verhaelt sich wie bisher (test/funktionenRoutes.test.js).
|
||||
const aw = await startAppwriteMock({
|
||||
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings') },
|
||||
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings'), featureConsents: schemaKeys('featureConsents') },
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
@@ -1363,3 +1363,254 @@ test('Teamansicht: hindernis je Terminplaner in setup, null in jedem anderen Zus
|
||||
assert.equal(view.state, 'setup')
|
||||
assert.equal(view.ticketMissing, false)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------- Staff-Aktivierung mit Zustimmung in Textform (TP2 F13a) ---
|
||||
const { zustimmungFuerSitzung, leseZustimmung } = await import('../server/services/featureConsents.js')
|
||||
const { setLegalDirForTests } = await import('../server/services/legalDocs.js')
|
||||
|
||||
const berlinTagF13 = (ms = Date.now()) => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(ms)
|
||||
const tagMitVersatzF13 = (tag, tage) => new Date(Date.parse(`${tag}T12:00:00Z`) + tage * 86400000).toISOString().slice(0, 10)
|
||||
const ttmmjjjjF13 = (tag) => tag.split('-').reverse().join('.')
|
||||
const consentsF13 = () => [...aw.docs('featureConsents').values()]
|
||||
const consentsDesF13 = (projectId) => consentsF13().filter((c) => c.projectId === projectId)
|
||||
let f13Nr = 0
|
||||
/** Kunde mit eigenem Projekt (ohne Funktionsdokument); E-Mail frei waehlbar, das Wartungs-Abo laeuft. */
|
||||
function legeKundeF13({ email, abo = true } = {}) {
|
||||
f13Nr += 1
|
||||
const customerId = `c_f13_${f13Nr}`
|
||||
const projectId = `p_f13_${f13Nr}`
|
||||
aw.seed('customers', { $id: customerId, email: email ?? `f13-${f13Nr}@example.invalid`, name: `Kunde F13 ${f13Nr}`, status: 'active', portalAccessEnabled: true })
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Projekt F13 ${f13Nr}` })
|
||||
if (abo) aw.seed('subscriptions', subscriptionDoc(`sub_f13_${f13Nr}`, { ...wartungDoc, customerId }))
|
||||
return { customerId, projectId, email: aw.docs('customers').get(customerId).email }
|
||||
}
|
||||
const aktivierenF13 = (k, body = {}, opts = STAFF) => team(k.customerId, 'termine', { projectId: k.projectId, action: 'activate', ...body }, opts)
|
||||
const feldF13 = (k, over = {}) => ({ textformDate: berlinTagF13(), textformChannel: 'chat', ...over })
|
||||
const k16AnF13 = (email) => mails.sent.filter((m) => m.subject === 'Dein Terminplaner ist bereit' && m.to.some((t) => t.address === email))
|
||||
|
||||
test('textform_pflicht_modus_on: ohne Angaben 400, mit gültigen Angaben 200, Nachweis mit Datum, Weg, Fassung, Mitarbeiter und leerer Sitzung, K16 mit LB und AVV als PDF', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', ['pilot@example.invalid'])
|
||||
const k = legeKundeF13()
|
||||
const v = config.termine.avvVersion
|
||||
const mailsVorher = mails.sent.length
|
||||
|
||||
const ohne = await aktivierenF13(k)
|
||||
assert.equal(ohne.status, 400)
|
||||
assert.match(ohne.body.error, /Datum der Zustimmung/)
|
||||
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null, 'nichts angelegt')
|
||||
assert.equal(consentsDesF13(k.projectId).length, 0)
|
||||
assert.equal(mails.sent.length, mailsVorher)
|
||||
// nur Datum ohne Weg
|
||||
const nurDatum = await aktivierenF13(k, { textformDate: berlinTagF13() })
|
||||
assert.equal(nurDatum.status, 400)
|
||||
assert.match(nurDatum.body.error, /Weg der Zustimmung/)
|
||||
|
||||
const tag = berlinTagF13()
|
||||
const ok = await aktivierenF13(k, { textformDate: tag, textformChannel: 'email' })
|
||||
assert.equal(ok.status, 200, JSON.stringify(ok.body))
|
||||
assert.equal(featuresOf(ok, k.projectId).termine.state, 'active')
|
||||
const doc = await sf.getFeatureDoc(k.customerId, k.projectId, 'termine')
|
||||
assert.deepEqual([doc.state, doc.enabled, doc.unlockedByPurchase], ['active', true, false], 'ohne Bestellung freigeschaltet')
|
||||
|
||||
const [nachweis] = consentsDesF13(k.projectId)
|
||||
assert.equal(consentsDesF13(k.projectId).length, 1)
|
||||
assert.deepEqual([nachweis.customerId, nachweis.projectId, nachweis.featureKey], [k.customerId, k.projectId, 'termine'])
|
||||
assert.equal(nachweis.stripeSessionId, '', 'keine Stripe-Sitzung')
|
||||
assert.equal(nachweis.ip, '')
|
||||
assert.equal(nachweis.userAgent, '')
|
||||
assert.equal(nachweis.konto, 'Mitarbeiter: Andrej')
|
||||
assert.ok(Math.abs(Date.parse(nachweis.acceptedAt) - Date.now()) < 10000, 'Zeitpunkt der Erfassung')
|
||||
const gelesen = leseZustimmung(nachweis)
|
||||
assert.deepEqual(gelesen.fassungen, { leistungsbeschreibung: v, avv: v })
|
||||
assert.deepEqual(gelesen.texte, [{
|
||||
id: 'textform',
|
||||
text: `Der Kunde hat der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner (Fassung ${v}) am ${ttmmjjjjF13(tag)} per E-Mail in Textform zugestimmt und bestätigt, dass sein Angebot nicht im Gesundheitsbereich liegt.`,
|
||||
}])
|
||||
assert.match(nachweis.textHash, /^[0-9a-f]{64}$/)
|
||||
// der Weg Chat heißt „Chat im Kundenportal“
|
||||
const chat = legeKundeF13()
|
||||
assert.equal((await aktivierenF13(chat, { textformDate: tag, textformChannel: 'chat' })).status, 200)
|
||||
assert.ok(leseZustimmung(consentsDesF13(chat.projectId)[0]).texte[0].text.includes(`am ${ttmmjjjjF13(tag)} per Chat im Kundenportal in Textform`))
|
||||
|
||||
// K16 an den Kunden mit genau den beiden PDFs (Leistungsbeschreibung, AVV), keine Widerrufsbelehrung
|
||||
const k16 = k16AnF13(k.email)
|
||||
assert.equal(k16.length, 1, mails.sent.map((m) => m.subject).join(' / '))
|
||||
assert.deepEqual(k16[0].attachments.map((a) => a.filename), [`Leistungsbeschreibung-Terminplaner-${v}.pdf`, `AVV-Terminplaner-${v}.pdf`])
|
||||
for (const a of k16[0].attachments) {
|
||||
assert.equal(a.contentType, 'application/pdf')
|
||||
assert.equal(Buffer.from(a.content, 'base64').subarray(0, 4).toString(), '%PDF')
|
||||
}
|
||||
assert.equal(mails.sent.length, mailsVorher + 2, 'je eine K16 für den Kunden und den Chat-Kunden, sonst nichts')
|
||||
})
|
||||
|
||||
test('textform_entfaellt: Modus off und pilot, Modus on mit Pilotadresse, News und Abschalten brauchen keine Angaben und schreiben keinen Nachweis', async (t) => {
|
||||
modusMerken(t)
|
||||
const mailsVorher = mails.sent.length
|
||||
for (const [mode, pilot] of [['off', []], ['pilot', ['*']], ['pilot', []]]) {
|
||||
termineMit(mode, pilot)
|
||||
const k = legeKundeF13()
|
||||
const res = await aktivierenF13(k)
|
||||
assert.equal(res.status, 200, `${mode}: ${JSON.stringify(res.body)}`)
|
||||
assert.equal(featuresOf(res, k.projectId).termine.state, 'active')
|
||||
assert.equal(consentsDesF13(k.projectId).length, 0, mode)
|
||||
}
|
||||
// Modus on, aber die Adresse steht in der Pilotliste
|
||||
const pilotKunde = legeKundeF13({ email: 'Pilot-F13@example.invalid' })
|
||||
termineMit('on', ['pilot-f13@example.invalid'])
|
||||
assert.equal((await aktivierenF13(pilotKunde)).status, 200)
|
||||
assert.equal(consentsDesF13(pilotKunde.projectId).length, 0)
|
||||
// Modus on, andere Adresse: Pflicht; News und Abschalten bleiben frei
|
||||
const k = legeKundeF13()
|
||||
assert.equal((await aktivierenF13(k)).status, 400)
|
||||
const news = await team(k.customerId, 'news', { projectId: k.projectId, action: 'activate' })
|
||||
assert.equal(news.status, 200)
|
||||
assert.equal(featuresOf(news, k.projectId).news.state, 'active')
|
||||
assert.equal((await team(k.customerId, 'news', { projectId: k.projectId, action: 'disable' })).status, 200)
|
||||
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null)
|
||||
const aktiv = legeKundeF13()
|
||||
termineMit('off')
|
||||
assert.equal((await aktivierenF13(aktiv)).status, 200)
|
||||
termineMit('on', [])
|
||||
const aus = await team(aktiv.customerId, 'termine', { projectId: aktiv.projectId, action: 'disable' })
|
||||
assert.equal(aus.status, 200, 'Abschalten braucht keine Angaben')
|
||||
assert.equal(featuresOf(aus, aktiv.projectId).termine.state, 'disabled')
|
||||
assert.equal(consentsF13().filter((c) => [k.projectId, aktiv.projectId, pilotKunde.projectId].includes(c.projectId)).length, 0)
|
||||
assert.equal(k16AnF13('k@example.invalid').length, 0)
|
||||
assert.equal(mails.sent.slice(mailsVorher).filter((m) => m.subject === 'Dein Terminplaner ist bereit').length, 0, 'ohne Textform keine K16 (wie bisher)')
|
||||
})
|
||||
|
||||
test('textform_entfaellt: ein bezahlter oder schon aktiver Terminplaner braucht keine neue Zustimmung in Textform (der Nachweis liegt schon vor)', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', [])
|
||||
// bezahlt (Zustimmung beim Checkout), Zustand setup, Wartungs-Abo läuft
|
||||
const bezahlt = legeKundeF13()
|
||||
aw.seed('portalFeatures', { $id: `f_${bezahlt.projectId}_termine`, customerId: bezahlt.customerId, projectId: bezahlt.projectId, featureKey: 'termine', siteKey: 'qrstuvwxyzabcdef', state: 'setup', enabled: false, unlockedByPurchase: true, purchaseStatus: 'paid' })
|
||||
const res = await aktivierenF13(bezahlt)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(featuresOf(res, bezahlt.projectId).termine.state, 'active')
|
||||
assert.equal(consentsDesF13(bezahlt.projectId).length, 0)
|
||||
// schon aktiv (Doppelklick im WOMS): kein zweiter Nachweis, keine zweite Mail
|
||||
const k = legeKundeF13()
|
||||
termineMit('off')
|
||||
assert.equal((await aktivierenF13(k)).status, 200)
|
||||
termineMit('on', [])
|
||||
const vorher = mails.sent.length
|
||||
assert.equal((await aktivierenF13(k)).status, 200)
|
||||
assert.equal(consentsDesF13(k.projectId).length, 0)
|
||||
assert.equal(mails.sent.length, vorher)
|
||||
})
|
||||
|
||||
test('textform_datum: Datum vor der Fassung, in der Zukunft, ungültig oder fehlend und ungültiger Weg ergeben 400 ohne Nachweis, ohne Dokument, ohne Mail', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', [])
|
||||
const k = legeKundeF13()
|
||||
const v = config.termine.avvVersion
|
||||
const heute = berlinTagF13()
|
||||
const mailsVorher = mails.sent.length
|
||||
const faelle = [
|
||||
[{ textformDate: tagMitVersatzF13(v, -1), textformChannel: 'chat' }, /liegt vor der Fassung/],
|
||||
[{ textformDate: tagMitVersatzF13(heute, 1), textformChannel: 'chat' }, /in der Zukunft/],
|
||||
[{ textformDate: '2026-02-30', textformChannel: 'chat' }, /fehlt oder ist ungültig/],
|
||||
[{ textformDate: '05.10.2026', textformChannel: 'chat' }, /fehlt oder ist ungültig/],
|
||||
[{ textformDate: 20261005, textformChannel: 'chat' }, /fehlt oder ist ungültig/],
|
||||
[{ textformDate: heute }, /Weg der Zustimmung/],
|
||||
[{ textformDate: heute, textformChannel: 'telefon' }, /Weg der Zustimmung/],
|
||||
[{ textformDate: heute, textformChannel: '' }, /Weg der Zustimmung/],
|
||||
[{ textformDate: heute, textformChannel: ['chat'] }, /Weg der Zustimmung/],
|
||||
]
|
||||
for (const [body, meldung] of faelle) {
|
||||
const res = await aktivierenF13(k, body)
|
||||
assert.equal(res.status, 400, JSON.stringify(body))
|
||||
assert.match(res.body.error, meldung, JSON.stringify(body))
|
||||
}
|
||||
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null, 'nichts angelegt')
|
||||
assert.equal(consentsDesF13(k.projectId).length, 0)
|
||||
assert.equal(mails.sent.length, mailsVorher)
|
||||
// genau am Tag der Fassung und heute ist erlaubt
|
||||
assert.equal((await aktivierenF13(k, { textformDate: v, textformChannel: 'email' })).status, 200)
|
||||
assert.equal(consentsDesF13(k.projectId).length, 1)
|
||||
})
|
||||
|
||||
test('textform_pflicht_modus_on: fremdes Projekt, Kundensitzung und nicht lesbare Rechtsunterlagen ändern nichts (die Berechtigung des Team-Wegs bleibt)', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', [])
|
||||
const k = legeKundeF13()
|
||||
const fremd = legeKundeF13()
|
||||
// Projekt eines anderen Kunden: 403 vor jeder Prüfung der Zustimmung
|
||||
const res = await team(k.customerId, 'termine', { projectId: fremd.projectId, action: 'activate', ...feldF13(k) })
|
||||
assert.equal(res.status, 403)
|
||||
const alsKunde = await aktivierenF13(k, feldF13(k), { cookie: customerCookie(k.customerId) })
|
||||
assert.ok(alsKunde.status === 401 || alsKunde.status === 403)
|
||||
assert.equal(consentsF13().filter((c) => [k.projectId, fremd.projectId].includes(c.projectId)).length, 0)
|
||||
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null)
|
||||
// PDFs nicht lesbar: nichts wird freigeschaltet, nichts gespeichert, keine Mail
|
||||
const leer = await (await import('node:fs/promises')).mkdtemp((await import('node:path')).join((await import('node:os')).tmpdir(), 'f13-leer-'))
|
||||
setLegalDirForTests(leer)
|
||||
t.after(() => setLegalDirForTests(null))
|
||||
const mailsVorher = mails.sent.length
|
||||
const fehler = await aktivierenF13(k, feldF13(k))
|
||||
assert.ok(fehler.status >= 500, `Status ${fehler.status}`)
|
||||
assert.match(fehler.body.error, /Rechtsunterlagen/)
|
||||
assert.equal(await sf.getFeatureDoc(k.customerId, k.projectId, 'termine'), null)
|
||||
assert.equal(consentsDesF13(k.projectId).length, 0)
|
||||
assert.equal(mails.sent.length, mailsVorher)
|
||||
setLegalDirForTests(null)
|
||||
assert.equal((await aktivierenF13(k, feldF13(k))).status, 200)
|
||||
})
|
||||
|
||||
test('get_admin_termine_feld: GET /api/team/funktionen/:kunde liefert termine { textformNoetig, avvVersion, links } je Modus', async (t) => {
|
||||
modusMerken(t)
|
||||
const k = legeKundeF13({ email: 'get-f13@example.invalid' })
|
||||
const v = config.termine.avvVersion
|
||||
const feldVon = async () => (await get(`/api/team/funktionen/${k.customerId}`, STAFF)).termine
|
||||
termineMit('off')
|
||||
assert.equal((await feldVon()).textformNoetig, false)
|
||||
termineMit('pilot', ['*'])
|
||||
assert.equal((await feldVon()).textformNoetig, false)
|
||||
termineMit('on', ['get-f13@example.invalid'])
|
||||
assert.equal((await feldVon()).textformNoetig, false, 'Pilotadresse')
|
||||
termineMit('on', [])
|
||||
const f = await feldVon()
|
||||
assert.deepEqual(Object.keys(f).sort(), ['avvVersion', 'links', 'textformNoetig'])
|
||||
assert.equal(f.textformNoetig, true)
|
||||
assert.equal(f.avvVersion, v)
|
||||
assert.deepEqual(Object.keys(f.links).sort(), ['avv', 'lb'])
|
||||
// bisherige Antwort bleibt: projects
|
||||
const ganz = await get(`/api/team/funktionen/${k.customerId}`, STAFF)
|
||||
assert.ok(Array.isArray(ganz.projects))
|
||||
// unbekannter Kunde: Modus on => Textform nötig (ohne Adresse), keine Ausnahme
|
||||
termineMit('on', ['*'])
|
||||
assert.equal((await get('/api/team/funktionen/c_gibtsnicht', STAFF)).termine.textformNoetig, true)
|
||||
})
|
||||
|
||||
test('chat_vorlage_versioniert: die Adressen für die Chat-Vorlage verweisen nur auf Seiten mit Fassung (-<Fassung>.html), absolut, folgen der aktuellen Fassung', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', [])
|
||||
const k = legeKundeF13()
|
||||
const v = config.termine.avvVersion
|
||||
const { links } = (await get(`/api/team/funktionen/${k.customerId}`, STAFF)).termine
|
||||
assert.equal(links.lb, `${config.publicBaseUrl}/rechtliches/leistungsbeschreibung-terminplaner-${v}.html`)
|
||||
assert.equal(links.avv, `${config.publicBaseUrl}/rechtliches/avv-terminplaner-${v}.html`)
|
||||
for (const adresse of Object.values(links)) {
|
||||
assert.ok(adresse.endsWith(`-${v}.html`), adresse)
|
||||
assert.match(adresse, /^https:\/\//)
|
||||
}
|
||||
// eine neue Fassung ändert die Adressen mit
|
||||
const vorher = config.termine.avvVersion
|
||||
t.after(() => { config.termine.avvVersion = vorher })
|
||||
config.termine.avvVersion = '2099-01-02'
|
||||
const neu = (await get(`/api/team/funktionen/${k.customerId}`, STAFF)).termine
|
||||
assert.equal(neu.avvVersion, '2099-01-02')
|
||||
for (const adresse of Object.values(neu.links)) assert.ok(adresse.endsWith('-2099-01-02.html'), adresse)
|
||||
})
|
||||
|
||||
test('zustimmungFuerSitzung(""): der Nachweis der Staff-Aktivierung (leere Sitzung) wird über keine Sitzung gefunden', async (t) => {
|
||||
modusMerken(t)
|
||||
termineMit('on', [])
|
||||
const k = legeKundeF13()
|
||||
assert.equal((await aktivierenF13(k, feldF13(k))).status, 200)
|
||||
assert.equal(consentsDesF13(k.projectId).length, 1)
|
||||
assert.equal(consentsDesF13(k.projectId)[0].stripeSessionId, '')
|
||||
for (const leer of ['', ' ', undefined, null]) assert.equal(await zustimmungFuerSitzung(leer), null, JSON.stringify(leer))
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
import { test, beforeEach, afterEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { config } from '../server/config.js'
|
||||
import { termineAus, termineFuerKunde, termineModus } from '../server/services/termine/schalter.js'
|
||||
import * as schalter from '../server/services/termine/schalter.js'
|
||||
|
||||
const { termineAus, termineFuerKunde, termineModus } = schalter
|
||||
// Namensraum für termineTextformNoetig: fehlt der Export, scheitert nur sein Test
|
||||
const termineTextformNoetig = (email) => schalter.termineTextformNoetig(email)
|
||||
|
||||
const TERMINE_ENV = [
|
||||
'TERMINE_ENABLED',
|
||||
@@ -162,3 +166,29 @@ test('termineFuerKunde: on gilt fuer alle ohne E-Mail-Filter', () => {
|
||||
assert.equal(termineFuerKunde('jemand@example.com'), true)
|
||||
assert.equal(termineFuerKunde(undefined), true)
|
||||
})
|
||||
|
||||
test('termineTextformNoetig: nur im Modus on und nur außerhalb der Pilotliste; „*“ und fehlende Adresse befreien nicht', () => {
|
||||
for (const mode of ['off', 'pilot', undefined, '', 'banane']) {
|
||||
config.termine.mode = mode
|
||||
config.termine.pilotEmails = []
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), false, `mode=${JSON.stringify(mode)}`)
|
||||
config.termine.pilotEmails = ['*']
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), false, `mode=${JSON.stringify(mode)} mit Stern`)
|
||||
}
|
||||
config.termine.mode = 'on'
|
||||
config.termine.pilotEmails = []
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), true)
|
||||
config.termine.pilotEmails = ['kenso.gri@gmail.com']
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), true)
|
||||
assert.equal(termineTextformNoetig('KENSO.gri@gmail.com '), false, 'Pilotadresse, Schreibweise egal')
|
||||
assert.equal(termineTextformNoetig('kenso.gri@gmail.com.evil.de'), true)
|
||||
config.termine.pilotEmails = ['*']
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), true, 'der Stern zählt in on nicht als Pilotadresse')
|
||||
assert.equal(termineTextformNoetig('*'), true)
|
||||
config.termine.pilotEmails = ['*', 'kenso.gri@gmail.com']
|
||||
assert.equal(termineTextformNoetig('kenso.gri@gmail.com'), false)
|
||||
for (const email of [undefined, null, '', ' ']) assert.equal(termineTextformNoetig(email), true, `ohne Adresse: ${JSON.stringify(email)}`)
|
||||
// wirkt sofort wie die übrigen Schalter
|
||||
config.termine.mode = 'pilot'
|
||||
assert.equal(termineTextformNoetig('jemand@example.com'), false)
|
||||
})
|
||||
Reference in new issue
Block a user