From 561b19201f6ddfb18741f546afe091bc6d1da8f6 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Tue, 20 Oct 2026 18:43:23 +0200 Subject: [PATCH 1/4] Say in the template editor which mail an approved loan actually sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approving a loan that starts today sends «Pronto per il ritiro» (loan_pickup_ready). «Prestito approvato» (loan_approved) only goes out for a loan that starts on a later date, and «Pronto per il ritiro» follows on its start day. The editor described loan_approved as the mail sent whenever an administrator approves a request. A library edited it and kept receiving the untouched pickup mail. Both templates stay editable. Their descriptions now say when each one is sent, and name the other one. The expiry reminder no longer claims three days, since the advance is the one set in the advanced settings. All five locales are updated. tests/mail-template-approval.spec.js edits both templates through the settings form, approves a loan that starts today from the pending list, and checks in Mailpit that the reader gets the edited pickup mail and not the approval one. It also checks that the editor says so. --- app/Support/SettingsMailTemplates.php | 6 +- locale/da_DK.json | 6 +- locale/de_DE.json | 6 +- locale/en_US.json | 6 +- locale/fr_FR.json | 6 +- locale/it_IT.json | 6 +- tests/mail-template-approval.spec.js | 102 ++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 tests/mail-template-approval.spec.js diff --git a/app/Support/SettingsMailTemplates.php b/app/Support/SettingsMailTemplates.php index 71f0ec692..cc0b7d70f 100644 --- a/app/Support/SettingsMailTemplates.php +++ b/app/Support/SettingsMailTemplates.php @@ -130,7 +130,7 @@ public static function all(?string $locale = null): array ], 'loan_expiring_warning' => [ 'label' => __('Promemoria scadenza'), - 'description' => __("Promemoria agli utenti tre giorni prima della scadenza del prestito."), + 'description' => __("Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate."), 'subject' => '⚠️ Il tuo prestito sta per scadere', 'placeholders' => ['utente_nome', 'libro_titolo', 'data_scadenza', 'giorni_rimasti'], 'body' => <<<'HTML' @@ -234,7 +234,7 @@ public static function all(?string $locale = null): array ], 'loan_approved' => [ 'label' => __('Prestito approvato'), - 'description' => __("Inviata all'utente quando un amministratore approva una richiesta di prestito."), + 'description' => __("Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro»."), 'subject' => '✅ La tua richiesta di prestito è stata approvata!', 'placeholders' => ['utente_nome', 'libro_titolo', 'data_inizio', 'data_fine', 'giorni_prestito', 'pickup_instructions', 'sezione_calendario'], 'body' => <<<'HTML' @@ -364,7 +364,7 @@ public static function all(?string $locale = null): array ], 'loan_pickup_ready' => [ 'label' => __('Pronto per il ritiro'), - 'description' => __("Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro."), + 'description' => __("Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo."), 'subject' => '📦 Libro pronto per il ritiro!', 'placeholders' => ['utente_nome', 'libro_titolo', 'data_inizio', 'data_fine', 'giorni_prestito', 'scadenza_ritiro', 'pickup_instructions', 'sezione_calendario'], 'body' => <<<'HTML' diff --git a/locale/da_DK.json b/locale/da_DK.json index 07719c1ac..40a90c54f 100644 --- a/locale/da_DK.json +++ b/locale/da_DK.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Sendes til nye brugere for at indstille deres kontoadgangskode.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Sendes til brugeren ved afslutningen af registreringen for at bekræfte modtagelsen og afventningen af godkendelse.", "Inviata all'utente quando un amministratore approva l'account.": "Sendes til brugeren, når en administrator godkender kontoen.", - "Inviata all'utente quando un amministratore approva una richiesta di prestito.": "Sendes til brugeren, når en administrator godkender en låneanmodning.", + "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sendes, når en administrator godkender en låneanmodning, der starter på en fremtidig dato. For et lån, der starter med det samme, sendes «Klar til afhentning» i stedet.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Sendes til brugeren, når en administrator afviser en låneanmodning.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Sendes, når tiden til at afhente en bog er udløbet, og udlånet er blevet annulleret.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Sendes, når en reserveret bog bliver tilgængelig og konverteres til et afventende udlån.", - "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.": "Sendes, når et udlån er blevet godkendt, og bogen er klar til afhentning.", + "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Sendes, når bogen er klar til afhentning: når et lån, der starter med det samme, godkendes, og på startdagen for et lån, der er godkendt på forhånd.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Sendes, når en afhentning annulleres af administratoren.", "Inviata quando un utente viene invitato come amministratore.": "Sendes, når en bruger inviteres som administrator.", "Inviato": "Sendt", @@ -3823,7 +3823,7 @@ "Programmati": "Planlagte", "Programmato": "Planlagt", "Project Gutenberg": "Project Gutenberg", - "Promemoria agli utenti tre giorni prima della scadenza del prestito.": "Påmindelse til brugere tre dage før lånets udløbsdato.", + "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.": "Påmindelse til brugere før lånets udløb, så mange dage i forvejen som angivet i de avancerede indstillinger.", "Promemoria scadenza": "Udløbspåmindelse", "Pronti per il Ritiro": "Klar til afhentning", "Pronti per il prestito": "Klar til udlån", diff --git a/locale/de_DE.json b/locale/de_DE.json index e025f1102..db60e17f9 100644 --- a/locale/de_DE.json +++ b/locale/de_DE.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Wird an neue Benutzer gesendet, um das Passwort für ihr Konto festzulegen.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Wird nach der Registrierung an den Benutzer gesendet, um den Eingang zu bestätigen und auf die Genehmigung hinzuweisen.", "Inviata all'utente quando un amministratore approva l'account.": "Wird an den Benutzer gesendet, wenn ein Administrator das Konto genehmigt.", - "Inviata all'utente quando un amministratore approva una richiesta di prestito.": "Wird an den Benutzer gesendet, wenn ein Administrator eine Ausleihanfrage genehmigt.", + "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Wird gesendet, wenn ein Administrator eine Ausleihanfrage genehmigt, die an einem späteren Datum beginnt. Bei einer Ausleihe, die sofort beginnt, wird stattdessen «Zur Abholung bereit» gesendet.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Wird an den Benutzer gesendet, wenn ein Administrator eine Ausleihanfrage ablehnt.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Wird gesendet, wenn die Abholfrist abgelaufen ist und die Ausleihe storniert wurde.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Wird gesendet, wenn ein vorgemerktes Buch verfügbar wird und in eine ausstehende Ausleihe umgewandelt wird.", - "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.": "Wird gesendet, wenn eine Ausleihe genehmigt wurde und das Buch zur Abholung bereit ist.", + "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Wird gesendet, wenn das Buch zur Abholung bereitliegt: bei der Genehmigung einer sofort beginnenden Ausleihe und am Starttag einer im Voraus genehmigten Ausleihe.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Wird gesendet, wenn eine Abholung vom Administrator storniert wird.", "Inviata quando un utente viene invitato come amministratore.": "Wird gesendet, wenn ein Benutzer als Administrator eingeladen wird.", "Inviato": "Gesendet", @@ -3823,7 +3823,7 @@ "Programmati": "Geplant", "Programmato": "Geplant", "Project Gutenberg": "Project Gutenberg", - "Promemoria agli utenti tre giorni prima della scadenza del prestito.": "Erinnerung an die Benutzer drei Tage vor dem Fälligkeitsdatum der Ausleihe.", + "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.": "Erinnerung an Benutzer vor dem Ablauf der Ausleihe, so viele Tage im Voraus wie in den erweiterten Einstellungen festgelegt.", "Promemoria scadenza": "Fälligkeitserinnerung", "Pronti per il Ritiro": "Zur Abholung bereit", "Pronti per il prestito": "Zur Ausleihe bereit", diff --git a/locale/en_US.json b/locale/en_US.json index ffcce127e..d93c8b56a 100644 --- a/locale/en_US.json +++ b/locale/en_US.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Sent to new users to set up their account password.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Sent to the user after registration to confirm receipt and waiting for approval.", "Inviata all'utente quando un amministratore approva l'account.": "Sent to the user when an administrator approves the account.", - "Inviata all'utente quando un amministratore approva una richiesta di prestito.": "Sent to the user when an administrator approves a loan request.", + "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sent when an administrator approves a loan request that starts on a future date. For a loan that starts right away, «Ready for Pickup» is sent instead.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Sent to the user when an administrator rejects a loan request.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Sent when the pickup deadline has passed and the loan has been cancelled.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Sent when a reserved book becomes available and is converted to a pending loan.", - "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.": "Sent when a loan has been approved and the book is ready for pickup.", + "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Sent when the book is ready for pickup: when a loan that starts right away is approved, and on the start day of a loan approved in advance.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Sent when a pickup is cancelled by an administrator.", "Inviata quando un utente viene invitato come amministratore.": "Sent when a user is invited as an administrator.", "Inviato": "Sent", @@ -3823,7 +3823,7 @@ "Programmati": "Scheduled", "Programmato": "Scheduled", "Project Gutenberg": "Project Gutenberg", - "Promemoria agli utenti tre giorni prima della scadenza del prestito.": "Reminder to users three days before the loan due date.", + "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.": "Reminder sent to users before a loan is due, as many days ahead as set in the advanced settings.", "Promemoria scadenza": "Due-date reminder", "Pronti per il Ritiro": "Ready for Pickup", "Pronti per il prestito": "Ready for loan", diff --git a/locale/fr_FR.json b/locale/fr_FR.json index 9236cf5dc..3aaa08a51 100644 --- a/locale/fr_FR.json +++ b/locale/fr_FR.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Envoyée aux nouveaux utilisateurs pour définir le mot de passe de leur compte.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Envoyée à l'utilisateur à la fin de l'inscription pour confirmer la réception et l'attente d'approbation.", "Inviata all'utente quando un amministratore approva l'account.": "Envoyée à l'utilisateur lorsqu'un administrateur approuve le compte.", - "Inviata all'utente quando un amministratore approva una richiesta di prestito.": "Envoyée à l'utilisateur lorsqu'un administrateur approuve une demande d'emprunt.", + "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Envoyé lorsqu'un administrateur approuve une demande d'emprunt qui commence à une date future. Pour un emprunt qui commence tout de suite, c'est «Prêt à récupérer» qui est envoyé.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Envoyée à l'utilisateur lorsqu'un administrateur refuse une demande d'emprunt.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Envoyée lorsque le délai de récupération est dépassé et que l'emprunt a été annulé.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Envoyée lorsqu'un livre réservé devient disponible et est converti en emprunt en attente.", - "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.": "Envoyée lorsqu'un emprunt a été approuvé et que le livre est prêt à être récupéré.", + "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Envoyé lorsque le livre est prêt à être retiré : à l'approbation d'un emprunt qui commence tout de suite, et le jour de début d'un emprunt approuvé à l'avance.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Envoyée lorsqu'une récupération est annulée par l'administrateur.", "Inviata quando un utente viene invitato come amministratore.": "Envoyée lorsqu'un utilisateur est invité en tant qu'administrateur.", "Inviato": "Envoyé", @@ -3823,7 +3823,7 @@ "Programmati": "Programmés", "Programmato": "Programmé", "Project Gutenberg": "Project Gutenberg", - "Promemoria agli utenti tre giorni prima della scadenza del prestito.": "Rappel aux utilisateurs trois jours avant la date d'échéance de l'emprunt.", + "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.": "Rappel envoyé aux utilisateurs avant l'échéance de l'emprunt, avec le nombre de jours de préavis défini dans les paramètres avancés.", "Promemoria scadenza": "Rappel d'échéance", "Pronti per il Ritiro": "Prêts à récupérer", "Pronti per il prestito": "Prêts à l'emprunt", diff --git a/locale/it_IT.json b/locale/it_IT.json index 7999fda19..b2e1ec783 100644 --- a/locale/it_IT.json +++ b/locale/it_IT.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Inviata ai nuovi utenti per impostare la password del loro account.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.", "Inviata all'utente quando un amministratore approva l'account.": "Inviata all'utente quando un amministratore approva l'account.", - "Inviata all'utente quando un amministratore approva una richiesta di prestito.": "Inviata all'utente quando un amministratore approva una richiesta di prestito.", + "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.", - "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.": "Inviata quando un prestito è stato approvato e il libro è pronto per il ritiro.", + "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Inviata quando un ritiro viene annullato dall'amministratore.", "Inviata quando un utente viene invitato come amministratore.": "Inviata quando un utente viene invitato come amministratore.", "Inviato": "Inviato", @@ -3823,7 +3823,7 @@ "Programmati": "Programmati", "Programmato": "Programmato", "Project Gutenberg": "Project Gutenberg", - "Promemoria agli utenti tre giorni prima della scadenza del prestito.": "Promemoria agli utenti tre giorni prima della scadenza del prestito.", + "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.": "Promemoria agli utenti prima della scadenza del prestito, con i giorni di preavviso impostati nelle impostazioni avanzate.", "Promemoria scadenza": "Promemoria scadenza", "Pronti per il Ritiro": "Pronti per il Ritiro", "Pronti per il prestito": "Pronti per il prestito", diff --git a/tests/mail-template-approval.spec.js b/tests/mail-template-approval.spec.js new file mode 100644 index 000000000..57dd9b231 --- /dev/null +++ b/tests/mail-template-approval.spec.js @@ -0,0 +1,102 @@ +// @ts-check +// An admin who edits a mail template must be able to tell which template a reader +// receives. Approving a loan that starts today sends «Pronto per il ritiro» +// (loan_pickup_ready), not «Prestito approvato» (loan_approved), which only goes +// out for a loan that starts on a later date. The editor said otherwise, so a +// library edited loan_approved and kept receiving the untouched pickup mail. +// Both templates are edited through the settings form, the loan is approved from +// the pending list, and the mail is read from Mailpit. +const { test, expect } = require('@playwright/test'); +const { execFileSync } = require('child_process'); + +const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081'; +const MAILPIT = 'http://localhost:8025'; +const RUN = Date.now().toString(36); + +function db(sql) { + return execFileSync('mysql', ['-S', process.env.E2E_DB_SOCKET, '-u', process.env.E2E_DB_USER, process.env.E2E_DB_NAME, '-N', '-B', '-e', sql], { + encoding: 'utf-8', env: { ...process.env, MYSQL_PWD: process.env.E2E_DB_PASS }, + }).trim(); +} +const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + +test.skip(!process.env.E2E_ADMIN_EMAIL || !process.env.E2E_DB_USER || !process.env.E2E_DB_NAME, 'E2E credentials not configured'); + +test('approving a loan that starts today sends the edited «Pronto per il ritiro», as the editor says', async ({ page }) => { + const mailpitUp = await fetch(`${MAILPIT}/api/v1/info`).then((r) => r.ok).catch(() => false); + test.skip(!mailpitUp, 'Mailpit is not running on :8025'); + test.setTimeout(120000); + const emailBackup = db("SELECT setting_key, IFNULL(setting_value,'') FROM system_settings WHERE category='email'"); + db(`DROP TABLE IF EXISTS zz_tpl_backup_${RUN}`); + db(`CREATE TABLE zz_tpl_backup_${RUN} AS SELECT * FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready')`); + const userEmail = `probe-${RUN}@test.local`; + let userId = '', bookId = '', loanId = ''; + try { + db(`INSERT INTO system_settings (category, setting_key, setting_value) VALUES + ('email','type','smtp'),('email','driver_mode','smtp'),('email','smtp_host','localhost'),('email','smtp_port','1025'), + ('email','smtp_security',''),('email','smtp_username',''),('email','smtp_password',''),('email','from_email','test@biblioteca.local'),('email','from_name','Probe') + ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value)`); + await page.request.get(`${BASE}/_e2e/flush-cache`); + + await page.goto(`${BASE}/accedi`); + await page.fill('input[name="email"]', process.env.E2E_ADMIN_EMAIL || ''); + await page.fill('input[name="password"]', process.env.E2E_ADMIN_PASS || ''); + await page.locator('button[type="submit"]').click(); + await page.waitForURL(/admin|\/$/, { timeout: 15000 }).catch(() => {}); + + for (const name of ['loan_approved', 'loan_pickup_ready']) { + await page.goto(`${BASE}/admin/settings?tab=templates&template_locale=it_IT&template=${name}`); + const form = page.locator(`form:has(textarea[data-template="${name}"])`); + await form.scrollIntoViewIfNeeded(); + await form.locator('input[name="subject"]').fill(`SUBJ-${name}-${RUN}`); + const frame = form.frameLocator('iframe').first(); + await frame.locator('body').click(); + await page.keyboard.press('ControlOrMeta+End'); + await page.keyboard.type(` BODY-${name}-${RUN}`); + await form.getByRole('button', { name: /Salva template/ }).click(); + await page.waitForLoadState('domcontentloaded'); + expect(db(`SELECT LOCATE('BODY-${name}-${RUN}', body) > 0 FROM email_templates WHERE name='${name}' AND locale='it_IT'`)).toBe('1'); + } + + await page.goto(`${BASE}/admin/settings?tab=templates&template_locale=it_IT`); + const approvedCard = page.locator('div.overflow-hidden:has(textarea[data-template="loan_approved"])').last(); + await expect(approvedCard).toContainText('data futura'); + await expect(approvedCard).toContainText('Pronto per il ritiro'); + + db(`INSERT INTO utenti (nome, cognome, email, password, codice_tessera, stato, tipo_utente, email_verificata, locale) VALUES ('Probe','Reader','${userEmail}','x','PRB${RUN}','attivo','standard',1,'it_IT')`); + userId = db(`SELECT id FROM utenti WHERE email='${userEmail}'`); + bookId = db(`INSERT INTO libri (titolo, copie_totali, copie_disponibili, stato) VALUES ('Probe Book ${RUN}',1,1,'disponibile'); SELECT LAST_INSERT_ID()`); + db(`INSERT INTO copie (libro_id, numero_inventario, stato) VALUES (${bookId}, 'PRB-${RUN}', 'disponibile')`); + loanId = db(`INSERT INTO prestiti (libro_id, utente_id, stato, data_prestito, data_scadenza, attivo, created_at) VALUES (${bookId}, ${userId}, 'pendente', CURDATE(), CURDATE() + INTERVAL 30 DAY, 1, NOW()); SELECT LAST_INSERT_ID()`); + await fetch(`${MAILPIT}/api/v1/messages`, { method: 'DELETE' }); + + await page.goto(`${BASE}/admin/loans/pending`); + await page.locator(`.approve-btn[data-loan-id="${loanId}"]`).click(); + await page.locator('.swal2-confirm').click(); + await expect.poll(() => db(`SELECT stato FROM prestiti WHERE id=${loanId}`), { timeout: 10000 }).toBe('da_ritirare'); + + let messages = []; + for (let i = 0; i < 20 && messages.length === 0; i++) { + const list = await (await fetch(`${MAILPIT}/api/v1/search?query=${encodeURIComponent('to:' + userEmail)}`)).json(); + messages = list.messages || []; + if (messages.length === 0) await page.waitForTimeout(500); + } + expect(messages).toHaveLength(1); + const mail = await (await fetch(`${MAILPIT}/api/v1/message/${messages[0].ID}`)).json(); + expect(mail.Subject).toBe(`SUBJ-loan_pickup_ready-${RUN}`); + expect(mail.HTML).toContain(`BODY-loan_pickup_ready-${RUN}`); + expect(mail.HTML).not.toContain(`BODY-loan_approved-${RUN}`); + } finally { + if (loanId) db(`DELETE FROM prestiti WHERE id=${loanId}`); + if (bookId) { db(`DELETE FROM copie WHERE libro_id=${bookId}`); db(`DELETE FROM libri WHERE id=${bookId}`); } + if (userId) db(`DELETE FROM utenti WHERE id=${userId}`); + for (const line of emailBackup.split('\n').filter(Boolean)) { + const [k, v = ''] = line.split('\t'); + db(`UPDATE system_settings SET setting_value='${esc(v)}' WHERE category='email' AND setting_key='${esc(k)}'`); + } + db(`DELETE FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready')`); + db(`INSERT INTO email_templates SELECT * FROM zz_tpl_backup_${RUN}`); + db(`DROP TABLE zz_tpl_backup_${RUN}`); + await page.request.get(`${BASE}/_e2e/flush-cache`); + } +}); From 01628c33e2a14f805977e39fc76dfdf66fc458a4 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Tue, 20 Oct 2026 17:39:11 +0000 Subject: [PATCH 2/4] Name the directly created loans in the approval mail descriptions, restore the mail settings whole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An administrator who creates a loan that starts later also sends «Prestito approvato» (PrestitiController, state prenotato), and one created to be picked up sends «Pronto per il ritiro» (da_ritirare). The two descriptions spoke only of approving a request; they now say "approves or creates" and name the scheduled loan's start day, in all five locales. tests/mail-template-approval.spec.js restored the mail settings by updating the keys that existed before it: the keys its INSERT added (type, driver_mode, smtp_host...) stayed behind and pointed later suites at localhost:1025, NULLs came back as '' and backslashes doubled. It now copies the email rows into a table, as it already does for the templates, and puts them back with DELETE + INSERT ... SELECT. Ran it against a local install with Mailpit: it passes, and the email settings are identical before and after. --- app/Support/SettingsMailTemplates.php | 4 ++-- locale/da_DK.json | 4 ++-- locale/de_DE.json | 4 ++-- locale/en_US.json | 4 ++-- locale/fr_FR.json | 4 ++-- locale/it_IT.json | 4 ++-- tests/mail-template-approval.spec.js | 15 ++++++++------- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/Support/SettingsMailTemplates.php b/app/Support/SettingsMailTemplates.php index cc0b7d70f..6d335a44c 100644 --- a/app/Support/SettingsMailTemplates.php +++ b/app/Support/SettingsMailTemplates.php @@ -234,7 +234,7 @@ public static function all(?string $locale = null): array ], 'loan_approved' => [ 'label' => __('Prestito approvato'), - 'description' => __("Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro»."), + 'description' => __("Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro»."), 'subject' => '✅ La tua richiesta di prestito è stata approvata!', 'placeholders' => ['utente_nome', 'libro_titolo', 'data_inizio', 'data_fine', 'giorni_prestito', 'pickup_instructions', 'sezione_calendario'], 'body' => <<<'HTML' @@ -364,7 +364,7 @@ public static function all(?string $locale = null): array ], 'loan_pickup_ready' => [ 'label' => __('Pronto per il ritiro'), - 'description' => __("Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo."), + 'description' => __("Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato."), 'subject' => '📦 Libro pronto per il ritiro!', 'placeholders' => ['utente_nome', 'libro_titolo', 'data_inizio', 'data_fine', 'giorni_prestito', 'scadenza_ritiro', 'pickup_instructions', 'sezione_calendario'], 'body' => <<<'HTML' diff --git a/locale/da_DK.json b/locale/da_DK.json index 40a90c54f..c9ff8b2dd 100644 --- a/locale/da_DK.json +++ b/locale/da_DK.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Sendes til nye brugere for at indstille deres kontoadgangskode.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Sendes til brugeren ved afslutningen af registreringen for at bekræfte modtagelsen og afventningen af godkendelse.", "Inviata all'utente quando un amministratore approva l'account.": "Sendes til brugeren, når en administrator godkender kontoen.", - "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sendes, når en administrator godkender en låneanmodning, der starter på en fremtidig dato. For et lån, der starter med det samme, sendes «Klar til afhentning» i stedet.", + "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sendes, når en administrator godkender eller opretter et lån, der starter på en fremtidig dato. For et lån, der starter med det samme, sendes «Klar til afhentning» i stedet.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Sendes til brugeren, når en administrator afviser en låneanmodning.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Sendes, når tiden til at afhente en bog er udløbet, og udlånet er blevet annulleret.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Sendes, når en reserveret bog bliver tilgængelig og konverteres til et afventende udlån.", - "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Sendes, når bogen er klar til afhentning: når et lån, der starter med det samme, godkendes, og på startdagen for et lån, der er godkendt på forhånd.", + "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.": "Sendes, når bogen er klar til afhentning: når en administrator godkender et lån, der starter med det samme, eller opretter et lån til afhentning, og på startdagen for et planlagt lån.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Sendes, når en afhentning annulleres af administratoren.", "Inviata quando un utente viene invitato come amministratore.": "Sendes, når en bruger inviteres som administrator.", "Inviato": "Sendt", diff --git a/locale/de_DE.json b/locale/de_DE.json index db60e17f9..60433b038 100644 --- a/locale/de_DE.json +++ b/locale/de_DE.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Wird an neue Benutzer gesendet, um das Passwort für ihr Konto festzulegen.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Wird nach der Registrierung an den Benutzer gesendet, um den Eingang zu bestätigen und auf die Genehmigung hinzuweisen.", "Inviata all'utente quando un amministratore approva l'account.": "Wird an den Benutzer gesendet, wenn ein Administrator das Konto genehmigt.", - "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Wird gesendet, wenn ein Administrator eine Ausleihanfrage genehmigt, die an einem späteren Datum beginnt. Bei einer Ausleihe, die sofort beginnt, wird stattdessen «Zur Abholung bereit» gesendet.", + "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Wird gesendet, wenn ein Administrator eine Ausleihe genehmigt oder anlegt, die an einem späteren Datum beginnt. Bei einer Ausleihe, die sofort beginnt, wird stattdessen «Zur Abholung bereit» gesendet.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Wird an den Benutzer gesendet, wenn ein Administrator eine Ausleihanfrage ablehnt.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Wird gesendet, wenn die Abholfrist abgelaufen ist und die Ausleihe storniert wurde.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Wird gesendet, wenn ein vorgemerktes Buch verfügbar wird und in eine ausstehende Ausleihe umgewandelt wird.", - "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Wird gesendet, wenn das Buch zur Abholung bereitliegt: bei der Genehmigung einer sofort beginnenden Ausleihe und am Starttag einer im Voraus genehmigten Ausleihe.", + "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.": "Wird gesendet, wenn das Buch zur Abholung bereitliegt: wenn ein Administrator eine sofort beginnende Ausleihe genehmigt oder eine abzuholende Ausleihe anlegt, und am Starttag einer geplanten Ausleihe.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Wird gesendet, wenn eine Abholung vom Administrator storniert wird.", "Inviata quando un utente viene invitato come amministratore.": "Wird gesendet, wenn ein Benutzer als Administrator eingeladen wird.", "Inviato": "Gesendet", diff --git a/locale/en_US.json b/locale/en_US.json index d93c8b56a..114f36767 100644 --- a/locale/en_US.json +++ b/locale/en_US.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Sent to new users to set up their account password.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Sent to the user after registration to confirm receipt and waiting for approval.", "Inviata all'utente quando un amministratore approva l'account.": "Sent to the user when an administrator approves the account.", - "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sent when an administrator approves a loan request that starts on a future date. For a loan that starts right away, «Ready for Pickup» is sent instead.", + "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Sent when an administrator approves or creates a loan that starts on a future date. For a loan that starts right away, «Ready for Pickup» is sent instead.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Sent to the user when an administrator rejects a loan request.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Sent when the pickup deadline has passed and the loan has been cancelled.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Sent when a reserved book becomes available and is converted to a pending loan.", - "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Sent when the book is ready for pickup: when a loan that starts right away is approved, and on the start day of a loan approved in advance.", + "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.": "Sent when the book is ready for pickup: when an administrator approves a loan that starts right away or creates one to be picked up, and on the start day of a scheduled loan.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Sent when a pickup is cancelled by an administrator.", "Inviata quando un utente viene invitato come amministratore.": "Sent when a user is invited as an administrator.", "Inviato": "Sent", diff --git a/locale/fr_FR.json b/locale/fr_FR.json index 3aaa08a51..de529e362 100644 --- a/locale/fr_FR.json +++ b/locale/fr_FR.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Envoyée aux nouveaux utilisateurs pour définir le mot de passe de leur compte.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Envoyée à l'utilisateur à la fin de l'inscription pour confirmer la réception et l'attente d'approbation.", "Inviata all'utente quando un amministratore approva l'account.": "Envoyée à l'utilisateur lorsqu'un administrateur approuve le compte.", - "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Envoyé lorsqu'un administrateur approuve une demande d'emprunt qui commence à une date future. Pour un emprunt qui commence tout de suite, c'est «Prêt à récupérer» qui est envoyé.", + "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Envoyé lorsqu'un administrateur approuve ou crée un emprunt qui commence à une date future. Pour un emprunt qui commence tout de suite, c'est «Prêt à récupérer» qui est envoyé.", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Envoyée à l'utilisateur lorsqu'un administrateur refuse une demande d'emprunt.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Envoyée lorsque le délai de récupération est dépassé et que l'emprunt a été annulé.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Envoyée lorsqu'un livre réservé devient disponible et est converti en emprunt en attente.", - "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Envoyé lorsque le livre est prêt à être retiré : à l'approbation d'un emprunt qui commence tout de suite, et le jour de début d'un emprunt approuvé à l'avance.", + "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.": "Envoyé lorsque le livre est prêt à être retiré : quand un administrateur approuve un emprunt qui commence tout de suite ou en crée un à retirer, et le jour de début d'un emprunt programmé.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Envoyée lorsqu'une récupération est annulée par l'administrateur.", "Inviata quando un utente viene invitato come amministratore.": "Envoyée lorsqu'un utilisateur est invité en tant qu'administrateur.", "Inviato": "Envoyé", diff --git a/locale/it_IT.json b/locale/it_IT.json index b2e1ec783..1c73a5189 100644 --- a/locale/it_IT.json +++ b/locale/it_IT.json @@ -2683,11 +2683,11 @@ "Inviata ai nuovi utenti per impostare la password del loro account.": "Inviata ai nuovi utenti per impostare la password del loro account.", "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.": "Inviata all'utente al termine della registrazione per confermare la ricezione e l'attesa di approvazione.", "Inviata all'utente quando un amministratore approva l'account.": "Inviata all'utente quando un amministratore approva l'account.", - "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Inviata quando un amministratore approva una richiesta di prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».", + "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».": "Inviata quando un amministratore approva o crea un prestito che inizia in una data futura. Per un prestito che inizia subito parte invece «Pronto per il ritiro».", "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.": "Inviata all'utente quando un amministratore rifiuta una richiesta di prestito.", "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.": "Inviata quando il tempo per ritirare un libro è scaduto e il prestito è stato annullato.", "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.": "Inviata quando un libro prenotato diventa disponibile e viene convertito in prestito pendente.", - "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.": "Inviata quando il libro è pronto per il ritiro: all'approvazione di un prestito che inizia subito, e il giorno d'inizio di un prestito approvato in anticipo.", + "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.": "Inviata quando il libro è pronto per il ritiro: quando un amministratore approva un prestito che inizia subito o ne crea uno da ritirare, e il giorno d'inizio di un prestito programmato.", "Inviata quando un ritiro viene annullato dall'amministratore.": "Inviata quando un ritiro viene annullato dall'amministratore.", "Inviata quando un utente viene invitato come amministratore.": "Inviata quando un utente viene invitato come amministratore.", "Inviato": "Inviato", diff --git a/tests/mail-template-approval.spec.js b/tests/mail-template-approval.spec.js index 57dd9b231..99c887139 100644 --- a/tests/mail-template-approval.spec.js +++ b/tests/mail-template-approval.spec.js @@ -18,7 +18,6 @@ function db(sql) { encoding: 'utf-8', env: { ...process.env, MYSQL_PWD: process.env.E2E_DB_PASS }, }).trim(); } -const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); test.skip(!process.env.E2E_ADMIN_EMAIL || !process.env.E2E_DB_USER || !process.env.E2E_DB_NAME, 'E2E credentials not configured'); @@ -26,7 +25,10 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro const mailpitUp = await fetch(`${MAILPIT}/api/v1/info`).then((r) => r.ok).catch(() => false); test.skip(!mailpitUp, 'Mailpit is not running on :8025'); test.setTimeout(120000); - const emailBackup = db("SELECT setting_key, IFNULL(setting_value,'') FROM system_settings WHERE category='email'"); + // Whole rows, kept in the database: the restore must also drop the keys the + // INSERT below adds, and keep NULLs and backslashes exactly as they were. + db(`DROP TABLE IF EXISTS zz_mail_backup_${RUN}`); + db(`CREATE TABLE zz_mail_backup_${RUN} AS SELECT * FROM system_settings WHERE category='email'`); db(`DROP TABLE IF EXISTS zz_tpl_backup_${RUN}`); db(`CREATE TABLE zz_tpl_backup_${RUN} AS SELECT * FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready')`); const userEmail = `probe-${RUN}@test.local`; @@ -60,7 +62,7 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro await page.goto(`${BASE}/admin/settings?tab=templates&template_locale=it_IT`); const approvedCard = page.locator('div.overflow-hidden:has(textarea[data-template="loan_approved"])').last(); - await expect(approvedCard).toContainText('data futura'); + await expect(approvedCard).toContainText('approva o crea un prestito che inizia in una data futura'); await expect(approvedCard).toContainText('Pronto per il ritiro'); db(`INSERT INTO utenti (nome, cognome, email, password, codice_tessera, stato, tipo_utente, email_verificata, locale) VALUES ('Probe','Reader','${userEmail}','x','PRB${RUN}','attivo','standard',1,'it_IT')`); @@ -90,10 +92,9 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro if (loanId) db(`DELETE FROM prestiti WHERE id=${loanId}`); if (bookId) { db(`DELETE FROM copie WHERE libro_id=${bookId}`); db(`DELETE FROM libri WHERE id=${bookId}`); } if (userId) db(`DELETE FROM utenti WHERE id=${userId}`); - for (const line of emailBackup.split('\n').filter(Boolean)) { - const [k, v = ''] = line.split('\t'); - db(`UPDATE system_settings SET setting_value='${esc(v)}' WHERE category='email' AND setting_key='${esc(k)}'`); - } + db(`DELETE FROM system_settings WHERE category='email'`); + db(`INSERT INTO system_settings SELECT * FROM zz_mail_backup_${RUN}`); + db(`DROP TABLE zz_mail_backup_${RUN}`); db(`DELETE FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready')`); db(`INSERT INTO email_templates SELECT * FROM zz_tpl_backup_${RUN}`); db(`DROP TABLE zz_tpl_backup_${RUN}`); From bae4e1657d042f75eaa5e2b30cd2abe2591784e8 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Tue, 20 Oct 2026 18:33:41 +0000 Subject: [PATCH 3/4] Reach the database and Mailpit the way CI provides them in the mail template spec The spec's db() helper always passed -S "$E2E_DB_SOCKET". The deep-regression shards connect over TCP (E2E_DB_HOST/E2E_DB_PORT) and set no socket, so its first query ran "mysql -S -u root" and failed with "Can't connect ... through socket 'undefined'" on both attempts, in every run of shard 1. It now uses host and port when they are set and the socket otherwise, like loan-request-default-end.spec.js, and reads Mailpit from MAILPIT_API like the other mail specs. Ran it locally with only E2E_DB_HOST/E2E_DB_PORT set, as in CI: it passes. --- tests/mail-template-approval.spec.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/mail-template-approval.spec.js b/tests/mail-template-approval.spec.js index 99c887139..4525175fc 100644 --- a/tests/mail-template-approval.spec.js +++ b/tests/mail-template-approval.spec.js @@ -10,11 +10,20 @@ const { test, expect } = require('@playwright/test'); const { execFileSync } = require('child_process'); const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081'; -const MAILPIT = 'http://localhost:8025'; +const MAILPIT_API = process.env.MAILPIT_API || 'http://localhost:8025/api/v1'; const RUN = Date.now().toString(36); function db(sql) { - return execFileSync('mysql', ['-S', process.env.E2E_DB_SOCKET, '-u', process.env.E2E_DB_USER, process.env.E2E_DB_NAME, '-N', '-B', '-e', sql], { + // CI connects over TCP (E2E_DB_HOST/E2E_DB_PORT, no socket); a local run may use a socket. + const args = []; + if (process.env.E2E_DB_HOST) { + args.push('-h', process.env.E2E_DB_HOST); + if (process.env.E2E_DB_PORT) args.push('-P', process.env.E2E_DB_PORT); + } else if (process.env.E2E_DB_SOCKET) { + args.push('-S', process.env.E2E_DB_SOCKET); + } + args.push('-u', process.env.E2E_DB_USER || '', process.env.E2E_DB_NAME || '', '-N', '-B', '-e', sql); + return execFileSync('mysql', args, { encoding: 'utf-8', env: { ...process.env, MYSQL_PWD: process.env.E2E_DB_PASS }, }).trim(); } @@ -22,7 +31,7 @@ function db(sql) { test.skip(!process.env.E2E_ADMIN_EMAIL || !process.env.E2E_DB_USER || !process.env.E2E_DB_NAME, 'E2E credentials not configured'); test('approving a loan that starts today sends the edited «Pronto per il ritiro», as the editor says', async ({ page }) => { - const mailpitUp = await fetch(`${MAILPIT}/api/v1/info`).then((r) => r.ok).catch(() => false); + const mailpitUp = await fetch(`${MAILPIT_API}/info`).then((r) => r.ok).catch(() => false); test.skip(!mailpitUp, 'Mailpit is not running on :8025'); test.setTimeout(120000); // Whole rows, kept in the database: the restore must also drop the keys the @@ -70,7 +79,7 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro bookId = db(`INSERT INTO libri (titolo, copie_totali, copie_disponibili, stato) VALUES ('Probe Book ${RUN}',1,1,'disponibile'); SELECT LAST_INSERT_ID()`); db(`INSERT INTO copie (libro_id, numero_inventario, stato) VALUES (${bookId}, 'PRB-${RUN}', 'disponibile')`); loanId = db(`INSERT INTO prestiti (libro_id, utente_id, stato, data_prestito, data_scadenza, attivo, created_at) VALUES (${bookId}, ${userId}, 'pendente', CURDATE(), CURDATE() + INTERVAL 30 DAY, 1, NOW()); SELECT LAST_INSERT_ID()`); - await fetch(`${MAILPIT}/api/v1/messages`, { method: 'DELETE' }); + await fetch(`${MAILPIT_API}/messages`, { method: 'DELETE' }); await page.goto(`${BASE}/admin/loans/pending`); await page.locator(`.approve-btn[data-loan-id="${loanId}"]`).click(); @@ -79,12 +88,12 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro let messages = []; for (let i = 0; i < 20 && messages.length === 0; i++) { - const list = await (await fetch(`${MAILPIT}/api/v1/search?query=${encodeURIComponent('to:' + userEmail)}`)).json(); + const list = await (await fetch(`${MAILPIT_API}/search?query=${encodeURIComponent('to:' + userEmail)}`)).json(); messages = list.messages || []; if (messages.length === 0) await page.waitForTimeout(500); } expect(messages).toHaveLength(1); - const mail = await (await fetch(`${MAILPIT}/api/v1/message/${messages[0].ID}`)).json(); + const mail = await (await fetch(`${MAILPIT_API}/message/${messages[0].ID}`)).json(); expect(mail.Subject).toBe(`SUBJ-loan_pickup_ready-${RUN}`); expect(mail.HTML).toContain(`BODY-loan_pickup_ready-${RUN}`); expect(mail.HTML).not.toContain(`BODY-loan_approved-${RUN}`); From 37f995c61a45a55c85cac69475d8fe32aeaca823 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Tue, 20 Oct 2026 21:24:39 +0000 Subject: [PATCH 4/4] Restore the mail settings and templates in one transaction after the spec The four DELETE/INSERT statements of the cleanup now run in one mysql call inside a transaction. If any fails, the client exits without COMMIT, so the install is never left half restored and the backup tables stay. db() gets the JSDoc the other spec helpers have. --- tests/mail-template-approval.spec.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/mail-template-approval.spec.js b/tests/mail-template-approval.spec.js index 4525175fc..c5442612c 100644 --- a/tests/mail-template-approval.spec.js +++ b/tests/mail-template-approval.spec.js @@ -13,8 +13,14 @@ const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081'; const MAILPIT_API = process.env.MAILPIT_API || 'http://localhost:8025/api/v1'; const RUN = Date.now().toString(36); +/** + * Runs one statement against the test database and returns its tab-separated output, + * trimmed. CI connects over TCP (E2E_DB_HOST/E2E_DB_PORT, no socket); a local run may + * use a socket. + * @param {string} sql + * @returns {string} + */ function db(sql) { - // CI connects over TCP (E2E_DB_HOST/E2E_DB_PORT, no socket); a local run may use a socket. const args = []; if (process.env.E2E_DB_HOST) { args.push('-h', process.env.E2E_DB_HOST); @@ -101,12 +107,15 @@ test('approving a loan that starts today sends the edited «Pronto per il ritiro if (loanId) db(`DELETE FROM prestiti WHERE id=${loanId}`); if (bookId) { db(`DELETE FROM copie WHERE libro_id=${bookId}`); db(`DELETE FROM libri WHERE id=${bookId}`); } if (userId) db(`DELETE FROM utenti WHERE id=${userId}`); - db(`DELETE FROM system_settings WHERE category='email'`); - db(`INSERT INTO system_settings SELECT * FROM zz_mail_backup_${RUN}`); - db(`DROP TABLE zz_mail_backup_${RUN}`); - db(`DELETE FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready')`); - db(`INSERT INTO email_templates SELECT * FROM zz_tpl_backup_${RUN}`); - db(`DROP TABLE zz_tpl_backup_${RUN}`); + // One transaction in one mysql call: on any error the client stops and disconnects + // without COMMIT, so nothing is half restored and the backups are still there. + db(`START TRANSACTION; + DELETE FROM system_settings WHERE category='email'; + INSERT INTO system_settings SELECT * FROM zz_mail_backup_${RUN}; + DELETE FROM email_templates WHERE locale='it_IT' AND name IN ('loan_approved','loan_pickup_ready'); + INSERT INTO email_templates SELECT * FROM zz_tpl_backup_${RUN}; + COMMIT`); + db(`DROP TABLE zz_mail_backup_${RUN}, zz_tpl_backup_${RUN}`); await page.request.get(`${BASE}/_e2e/flush-cache`); } });