diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd3630fdf..6ed841d9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -458,7 +458,10 @@ jobs: # tsconfig.strict.json is the same program with strictNullChecks and # noImplicitAny; its errors per file may not rise above # scripts/strict-baseline.json, and a file without an entry, every new - # one included, has to be clean (scripts/strict-lint.mjs). + # one included, has to be clean (scripts/strict-lint.mjs). It holds the + # whole program with strict checks, past Node's default ~4 GB heap. + env: + NODE_OPTIONS: --max-old-space-size=8192 run: cd client && npm run lint:strict - name: Page pattern check diff --git a/client/src/api/mailIngest.test.ts b/client/src/api/mailIngest.test.ts new file mode 100644 index 000000000..425b3997e --- /dev/null +++ b/client/src/api/mailIngest.test.ts @@ -0,0 +1,21 @@ +// FE-MAILINGEST-API-001 +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('./client', () => ({ + default: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, + postMultipart: vi.fn(async () => ({ duplicate: false })), +})); + +import apiClient, { postMultipart } from './client'; +import { mailIngestApi } from './mailIngest'; + +describe('mailIngestApi.upload', () => { + it('FE-MAILINGEST-API-001: sends the .eml as a multipart form, never through the JSON post', async () => { + const file = new File(['From: a@b.c\r\n\r\nhi'], 'booking.eml', { type: 'message/rfc822' }); + expect(await mailIngestApi.upload(file)).toEqual({ duplicate: false }); + const [url, form] = vi.mocked(postMultipart).mock.calls[0]; + expect(url).toBe('/mail-ingest/upload'); + expect((form as FormData).get('file')).toBe(file); + expect(apiClient.post).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/api/mailIngest.ts b/client/src/api/mailIngest.ts index 321ba22df..92787e3ac 100644 --- a/client/src/api/mailIngest.ts +++ b/client/src/api/mailIngest.ts @@ -1,4 +1,4 @@ -import apiClient from './client'; +import apiClient, { postMultipart } from './client'; /** * The booking mail import (server nest/mail-ingest): the person's settings and @@ -43,6 +43,7 @@ export const mailIngestApi = { upload: (file: File): Promise => { const form = new FormData(); form.append('file', file); - return apiClient.post('/mail-ingest/upload', form).then(data); + // As multipart: under the client's default JSON content type the form went as JSON, without the file. + return postMultipart('/mail-ingest/upload', form); }, }; diff --git a/server/src/nest/booking-import/booking-import.service.ts b/server/src/nest/booking-import/booking-import.service.ts index f8205563f..089340dfe 100644 --- a/server/src/nest/booking-import/booking-import.service.ts +++ b/server/src/nest/booking-import/booking-import.service.ts @@ -14,7 +14,7 @@ import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { PlaceOriginsService } from '../places/place-origins.service'; import { PlacesService } from '../places/places.service'; import { RealtimeService } from '../realtime/realtime.service'; -import { withoutOrigin } from '../reservations/reservation-origin'; +import { withoutOrigin } from '../reservations/reservation-origin.contract'; import { ReservationsService, type CreateAccommodation } from '../reservations/reservations.service'; import { IMPORT_SOURCE, importExternalId } from './booking-origin'; import { KitineraryExtractorService } from './kitinerary-extractor.service'; diff --git a/server/src/nest/reservations/reservation-inputs.ts b/server/src/nest/reservations/reservation-inputs.ts index 3244ebf23..6b480103e 100644 --- a/server/src/nest/reservations/reservation-inputs.ts +++ b/server/src/nest/reservations/reservation-inputs.ts @@ -1,4 +1,4 @@ -import type { ReservationOrigin } from './reservation-origin'; +import type { ReservationOrigin } from './reservation-origin.contract'; /* * The shapes the reservations service takes in: a booking's endpoints, the diff --git a/server/src/nest/reservations/reservation-origin.ts b/server/src/nest/reservations/reservation-origin.contract.ts similarity index 86% rename from server/src/nest/reservations/reservation-origin.ts rename to server/src/nest/reservations/reservation-origin.contract.ts index 7c198c29c..dcd80da4a 100644 --- a/server/src/nest/reservations/reservation-origin.ts +++ b/server/src/nest/reservations/reservation-origin.contract.ts @@ -1,3 +1,9 @@ +/* + * The origin an imported booking carries, published by the reservations + * domain: the importers (booking import, mail import) set it, and every other + * write path strips it from what a client sends. + */ + /** * Where an imported booking came from (`import`, `mail`) and its key there, * unique per trip. Both or neither; without them the row is a hand-made diff --git a/server/src/nest/reservations/reservations.controller.ts b/server/src/nest/reservations/reservations.controller.ts index 3ff8c240e..63a09784f 100644 --- a/server/src/nest/reservations/reservations.controller.ts +++ b/server/src/nest/reservations/reservations.controller.ts @@ -2,7 +2,7 @@ import type { User } from '../../types'; import { CurrentUser } from '../auth-core/current-user.decorator'; import { JwtAuthGuard } from '../auth-core/jwt-auth.guard'; import { RequirePermission, TripAccessGuard } from '../permissions/trip-access.guard'; -import { withoutOrigin } from './reservation-origin'; +import { withoutOrigin } from './reservation-origin.contract'; import { ReservationWritesService } from './reservation-writes.service'; import { ReservationCreateDto, diff --git a/server/src/nest/reservations/reservations.rpc.ts b/server/src/nest/reservations/reservations.rpc.ts index fcec9cb3b..a6506a242 100644 --- a/server/src/nest/reservations/reservations.rpc.ts +++ b/server/src/nest/reservations/reservations.rpc.ts @@ -4,7 +4,7 @@ import { PluginController, PluginMethod } from '../../nest-rpc/rpc-kit/decorator import type { PluginRpcContext } from '../../nest-rpc/rpc-kit/types'; import { num, schemaMessage } from '../../nest-rpc/rpc-params'; import { RealtimeService } from '../realtime/realtime.service'; -import { withoutOrigin } from './reservation-origin'; +import { withoutOrigin } from './reservation-origin.contract'; import { ReservationsService } from './reservations.service'; import { reservationCreateRequestSchema, diff --git a/server/src/nest/reservations/reservations.service.ts b/server/src/nest/reservations/reservations.service.ts index e6ed8f2a7..e3cf0205c 100644 --- a/server/src/nest/reservations/reservations.service.ts +++ b/server/src/nest/reservations/reservations.service.ts @@ -1,4 +1,4 @@ -import { originColumns } from './reservation-origin'; +import { originColumns } from './reservation-origin.contract'; import type { CreateAccommodation, CreateReservationData, EndpointInput, ReservationEndpoint, UpdateReservationData } from './reservation-inputs'; export type { CreateAccommodation, CreateReservationData, EndpointInput, ReservationEndpoint, UpdateReservationData } from './reservation-inputs'; import { BadRequestException, Injectable } from '@nestjs/common'; diff --git a/server/tests/unit/nest/reservations.service.test.ts b/server/tests/unit/nest/reservations.service.test.ts index af6c62379..15e5fb370 100644 --- a/server/tests/unit/nest/reservations.service.test.ts +++ b/server/tests/unit/nest/reservations.service.test.ts @@ -33,7 +33,7 @@ import { HttpException } from '@nestjs/common'; import { resetTestDb } from '../../helpers/test-db'; import { createUser, createTrip, createReservation, createBudgetItem, createPlace, createDay, createDayAccommodation, createDayAssignment, addTripMember } from '../../helpers/factories'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; -import { withoutOrigin } from '../../../src/nest/reservations/reservation-origin'; +import { withoutOrigin } from '../../../src/nest/reservations/reservation-origin.contract'; import { ReservationsService, type UpdateReservationData } from '../../../src/nest/reservations/reservations.service'; import { ReservationsReadService } from '../../../src/nest/reservations/reservations-read.service'; import type { BudgetService } from '../../../src/nest/budget/budget.service';