From ee53eb8d2cfb8ae6263c0709d75154546c424514 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:58:40 +0100 Subject: [PATCH] test: add a flight booking with a card to the demo --- lib/demo/__tests__/fixtures.test.ts | 27 +++++++++- lib/demo/demo-data.ts | 3 +- lib/demo/fixtures/travel.ts | 83 +++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 lib/demo/fixtures/travel.ts diff --git a/lib/demo/__tests__/fixtures.test.ts b/lib/demo/__tests__/fixtures.test.ts index 5531a479b..0619b748e 100644 --- a/lib/demo/__tests__/fixtures.test.ts +++ b/lib/demo/__tests__/fixtures.test.ts @@ -30,13 +30,16 @@ import { MAILBOX_PRESETS } from '@/lib/jmap/share-rights'; import { principalLabel, shareRecipientNames } from '@/hooks/use-share-recipient-labels'; import { checkAvailabilityAccess, lookUpAvailability } from '@/lib/jmap/free-busy'; import { availabilityFor } from '@/components/calendar/participant-availability'; -import { demoAt } from '../demo-utils'; +import { demoAt, demoISODate } from '../demo-utils'; import type { BusyPeriod } from '@/lib/jmap/types'; import { useRecoveryStore } from '@/stores/recovery-store'; import { SUPPORT_TAGS } from '../fixtures/shared-metadata'; import { SIGNED_EMAIL_ID, SIGNED_TEXT } from '../fixtures/signed'; import { INVOICE_BLOB, INVOICE_FILE, INVOICE_TOTAL, createSpreehostInvoiceXml } from '../fixtures/invoice'; import { INVOICE_EMAIL_ID } from '../fixtures/supplier'; +import { FLIGHT_DAY, FLIGHT_EMAIL_ID, FLIGHT_REFERENCE, FLIGHT_SENDER, createFlightReservation } from '../fixtures/travel'; +import { getStructuredMailTrust, structuredHtmlPart } from '@/lib/structured-mail/trust'; +import { loadAirportZones, readStructuredMailDetailed, resolveCards } from '@/lib/structured-mail'; import { loadSharedTags, sharedTagsKey, useSharedTagsStore, saveSharedTags } from '@/stores/shared-tags-store'; const PUBLIC = path.resolve(__dirname, '..', '..', '..', 'public'); @@ -570,4 +573,26 @@ describe('demo fixtures: audience screenshots', () => { expect(one('IssueDate') < today).toBe(true); expect(one('DueDate') > today).toBe(true); }); + + it('Tagus Air\'s booking mail carries a flight the booking card trusts and shows', async () => { + const client = new DemoJMAPClient('work'); + const email = (await client.getEmail(FLIGHT_EMAIL_ID))!; + expect(email.mailboxIds[MB.inbox]).toBe(true); + expect(email.from?.[0].email).toBe(FLIGHT_SENDER.email); + expect(email.headers?.['Authentication-Results']).toContain('dkim=pass header.d=tagusair.example'); + const html = structuredHtmlPart(email)!.value; + expect(html).toContain(``); + // What the viewer's useStructuredMail does: trust, read, resolve the airport zones. + expect(getStructuredMailTrust(email)).toEqual({ trusted: true }); + const { cards } = readStructuredMailDetailed(html, { mailDate: new Date(email.sentAt ?? email.receivedAt) }); + const [card] = resolveCards(cards, await loadAirportZones()); + expect(card?.kind).toBe('flight'); + if (card?.kind !== 'flight') return; + expect(card.reference).toBe(FLIGHT_REFERENCE); + expect(card.links.checkin?.url).toContain('tagusair.example'); + const [leg] = card.legs; + expect([leg.carrierCode, leg.number, leg.departure.place.code, leg.arrival?.place.code]).toEqual(['TQ', '482', 'LIS', 'BER']); + expect(leg.departure.at).toMatchObject({ date: demoISODate(FLIGHT_DAY).slice(0, 10), time: '17:10:00', zone: 'Europe/Lisbon' }); + expect(leg.arrival?.at).toMatchObject({ time: '21:35:00', zone: 'Europe/Berlin' }); + }); }); diff --git a/lib/demo/demo-data.ts b/lib/demo/demo-data.ts index 8aa3bf016..84763bc66 100644 --- a/lib/demo/demo-data.ts +++ b/lib/demo/demo-data.ts @@ -4,6 +4,7 @@ import { createDemoEmails } from './fixtures/emails'; import { createDemoSupportEmails } from './fixtures/support'; import { createDemoSignedEmails } from './fixtures/signed'; import { createDemoSupplierEmails } from './fixtures/supplier'; +import { createDemoTravelEmails } from './fixtures/travel'; import { createDemoPersonalEmails, createDemoPersonalIdentities } from './fixtures/personal'; import { createDemoContacts, createDemoAddressBooks, createDemoPersonalContacts, createDemoPersonalAddressBooks } from './fixtures/contacts'; import { createDemoCalendars, createDemoCalendarEvents } from './fixtures/calendars'; @@ -108,7 +109,7 @@ export function getDemoData(profile: DemoProfile = 'work'): DemoData { self: { name: NORA.name, email: NORA.email }, mailboxes: withShares(createDemoMailboxes(), createDemoMailboxShares()), sharedMailboxes: { [SUPPORT_ACCOUNT_ID]: { name: SUPPORT_ACCOUNT_NAME, mailboxes: createDemoSupportMailboxes() } }, - emails: [...createDemoEmails(), ...createDemoSignedEmails(), ...createDemoSupplierEmails(), ...createDemoSupportEmails()], + emails: [...createDemoEmails(), ...createDemoSignedEmails(), ...createDemoSupplierEmails(), ...createDemoTravelEmails(), ...createDemoSupportEmails()], contacts: createDemoContacts(), addressBooks: createDemoAddressBooks(), calendars: createDemoCalendars(), diff --git a/lib/demo/fixtures/travel.ts b/lib/demo/fixtures/travel.ts new file mode 100644 index 000000000..963561982 --- /dev/null +++ b/lib/demo/fixtures/travel.ts @@ -0,0 +1,83 @@ +/** + * Travel mail with booking markup: Tagus Air's confirmation of the evening + * flight home from Lisbon on the last day of Infra Summit (days 9 to 12 in + * fixtures/calendars.ts; the out-of-office in fixtures/vacation.ts runs + * until that evening). The HTML carries a schema.org FlightReservation as + * JSON-LD and the receiving server verified the sender, so the viewer shows + * a booking card above the message (lib/structured-mail). + */ +import type { Email } from '@/lib/jmap/types'; +import { demoISODate } from '../demo-utils'; +import { at, authPass, dayLabel, mail } from './mail'; +import { MB } from './mailboxes'; +import { NORA } from './people'; + +export const FLIGHT_EMAIL_ID = 'demo-email-tagus-air'; +export const FLIGHT_SENDER = { name: 'Tagus Air', email: 'booking@tagusair.example' }; +export const FLIGHT_REFERENCE = 'K4TQ9M'; +/** The last summit day, relative to today. */ +export const FLIGHT_DAY = 12; + +const SITE = 'https://www.tagusair.example'; + +/** What the airline's mail system puts into the HTML: a FlightReservation in local airport time. */ +export function createFlightReservation(): Record { + return { + '@context': 'https://schema.org', + '@type': 'FlightReservation', + reservationNumber: FLIGHT_REFERENCE, + reservationStatus: 'https://schema.org/ReservationConfirmed', + underName: { '@type': 'Person', name: NORA.name }, + reservationFor: { + '@type': 'Flight', + flightNumber: '482', + airline: { '@type': 'Airline', name: 'Tagus Air', iataCode: 'TQ' }, + departureAirport: { '@type': 'Airport', name: 'Lisbon Humberto Delgado', iataCode: 'LIS' }, + departureTime: demoISODate(FLIGHT_DAY, 17, 10), + arrivalAirport: { '@type': 'Airport', name: 'Berlin Brandenburg', iataCode: 'BER' }, + arrivalTime: demoISODate(FLIGHT_DAY, 21, 35), + }, + airplaneSeat: '7C', + checkinUrl: `${SITE}/check-in?pnr=${FLIGHT_REFERENCE}`, + modifyReservationUrl: `${SITE}/my-trips/${FLIGHT_REFERENCE}`, + }; +} + +export function createDemoTravelEmails(): Email[] { + const day = dayLabel(FLIGHT_DAY); + const row = (label: string, value: string, first = false) => { + const border = first ? '' : 'border-top:1px solid #f0f0f0;'; + return `${label}${value}`; + }; + const html = `` + + '
' + + '

Tagus Air

' + + '

Your booking is confirmed

' + + `

Lisbon (LIS) to Berlin (BER), ${day}

` + + '' + + row('Flight', 'TQ 482', true) + + row('Departs', `${day}, 17:10, Lisbon (LIS)`) + + row('Arrives', `${day}, 21:35, Berlin (BER)`) + + row('Passenger', `${NORA.name}, seat 7C, Economy`) + + row('Booking reference', `${FLIGHT_REFERENCE}`) + + '
' + + `

Check in online

` + + '

Online check-in opens 24 hours before departure. One cabin bag up to 8 kg is included.

' + + '
'; + return [ + { + ...mail({ + id: FLIGHT_EMAIL_ID, mailbox: MB.inbox, date: at(1, 18, 20), + from: FLIGHT_SENDER, + subject: `Booking confirmed ${FLIGHT_REFERENCE}: Lisbon to Berlin`, + text: `Your booking is confirmed.\n\nTQ 482 · ${day} · Lisbon (LIS) 17:10 to Berlin (BER) 21:35\n\nPassenger: ${NORA.name} · Seat 7C · Economy\nBooking reference: ${FLIGHT_REFERENCE}\n\nCheck in online from 24 hours before departure: ${SITE}/check-in?pnr=${FLIGHT_REFERENCE}\n\nOne cabin bag up to 8 kg is included.\n\nTagus Air`, + html, + keywords: ['$label:travel'], + headers: authPass('tagusair.example'), + }), + // The Authentication-Results header came from the receiving MX, not a + // submission hop; a real fetch reads that from the Received order. + authenticationResultsOnReceipt: true, + }, + ]; +}