All checks were successful
CI / test (pull_request) Successful in 2m45s
Endpoints automatisables par cron externe (Hermes, GitHub Actions,
ou crontab système) pour gérer les tâches récurrentes de la
plateforme.
src/lib/cron-auth.ts (NEW) : isAuthorizedCronRequest(req) vérifie
l'en-tête Authorization Bearer ${CRON_TOKEN}. Le token est déjà dans
.env.production.
GET /api/cron/reminders :
- Itère bookings carbet CONFIRMED + rentalBookings CONFIRMED dont
startDate ∈ [now+22h, now+26h] (fenêtre 4h pour absorber les
éventuels retards de cron).
- Envoie sendBookingReminder (carbet) ou sendRentalReminder (rental).
- Compte bookingSent/bookingErrors et rentalSent/rentalErrors.
- Audit log scope=cron event=cron.reminders.run avec stats.
- Retourne JSON {ok, window, booking:{candidates,sent,errors},
rental:{candidates,sent,errors}}.
GET /api/cron/cleanup :
- Purge OrgInviteToken expirés depuis > 30j.
- Booking PENDING + paymentStatus≠SUCCEEDED + createdAt > 7j →
status=CANCELLED + paymentStatus=FAILED (libère le créneau).
- RentalBooking idem + delete RentalItemAvailability associée
(libère stock) en transaction.
- Audit log scope=cron event=cron.cleanup.run avec compteurs.
src/lib/email.ts :
- sendBookingReminder(to, firstName, bookingId, title, startDate,
slug) : email rappel J-1 avec CTA vers /reservations/[id].
- sendRentalReminder(to, firstName, rbId, providerName, startDate,
contactInfo) : rappel pour récup matériel, affiche contacts
provider (phone + email).
tests/lib/cron-auth.test.ts (6 cas) :
- Refus si CRON_TOKEN absent, header absent, format incorrect (Basic
ou Token), token mismatch.
- Accept si match exact, accept avec espaces autour du token (defensive).
Total tests : 89/89 ✓.
Schedule recommandé (à brancher côté Hermes ou crontab) :
- GET /api/cron/reminders : 1× par jour à 9h (Authorization: Bearer
$CRON_TOKEN)
- GET /api/cron/cleanup : 1× par semaine
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
|
|
vi.mock("server-only", () => ({}));
|
|
|
|
const { isAuthorizedCronRequest } = await import("@/lib/cron-auth");
|
|
|
|
function mkReq(authHeader: string | null): Request {
|
|
return new Request("https://example.invalid/", {
|
|
headers: authHeader ? { authorization: authHeader } : {},
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
delete process.env.CRON_TOKEN;
|
|
});
|
|
|
|
describe("isAuthorizedCronRequest", () => {
|
|
it("refuse si CRON_TOKEN absent côté serveur", () => {
|
|
expect(isAuthorizedCronRequest(mkReq("Bearer anything"))).toBe(false);
|
|
});
|
|
|
|
it("refuse si pas d'en-tête Authorization", () => {
|
|
process.env.CRON_TOKEN = "secret";
|
|
expect(isAuthorizedCronRequest(mkReq(null))).toBe(false);
|
|
});
|
|
|
|
it("refuse si format incorrect (pas Bearer)", () => {
|
|
process.env.CRON_TOKEN = "secret";
|
|
expect(isAuthorizedCronRequest(mkReq("Basic secret"))).toBe(false);
|
|
expect(isAuthorizedCronRequest(mkReq("Token secret"))).toBe(false);
|
|
});
|
|
|
|
it("refuse si token différent", () => {
|
|
process.env.CRON_TOKEN = "secret";
|
|
expect(isAuthorizedCronRequest(mkReq("Bearer wrong"))).toBe(false);
|
|
});
|
|
|
|
it("accepte si token exact", () => {
|
|
process.env.CRON_TOKEN = "secret";
|
|
expect(isAuthorizedCronRequest(mkReq("Bearer secret"))).toBe(true);
|
|
});
|
|
|
|
it("trim les espaces autour du token (defensive)", () => {
|
|
process.env.CRON_TOKEN = "secret";
|
|
expect(isAuthorizedCronRequest(mkReq("Bearer secret "))).toBe(true);
|
|
});
|
|
});
|