SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
5.9 KB · 58 lines typescript
Raw Blame History
1import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";2import { resetDb, closeDb } from "./setup";3import { setTransportForTests, mockTransport } from "../../src/lib/auth/email";4import { db } from "../../src/db/client";5import { users, userProfiles, listingMedia } from "../../src/db/schema";6import { createListing, updateListing, transition, attest, getListingFull, ConflictError } from "../../src/lib/listings/service";7import { openInquiry, getThread, reply, requestViewing, updateViewing } from "../../src/lib/inquiries/service";8import { searchDirect } from "../../src/lib/listings/public";9import { sanitizeBody } from "../../src/lib/inquiries/service";1011async function mkUser(email: string) { const [u] = await db().insert(users).values({ email, emailVerifiedAt: new Date() }).returning(); await db().insert(userProfiles).values({ userId: u.id }); return u; }12async function fakeMedia(listingId: string) { await db().insert(listingMedia).values({ listingId, storageKey: "2026-10/abcdefabcdefabcdefabcdef", status: "READY", scanStatus: "CLEAN", position: 0 }); }13const base = { address: "123 rue Test", city: "Québec", lat: 46.81, lng: -71.21, bedrooms: 3, bathrooms: 1, areaSqft: 1200, yearBuilt: 1990, title: "Beau logement lumineux près du parc", description: "Grand logement rénové, très lumineux, près des services, du parc et des transports. Disponible rapidement, idéal pour une famille." };1415describe("publications directes : un compte, plusieurs annonces, isolation", () => {16  beforeAll(() => setTransportForTests(mockTransport)); afterAll(closeDb); beforeEach(resetDb);17  it("publie un logement à louer et une maison à vendre depuis le même compte ; visibles dans la recherche", async () => {18    const u = await mkUser("owner@exemple.ca");19    const rent = await createListing(u.id, { listingType: "RENT", propertyType: "appartement", ...base, priceAmount: 1450 });20    const sale = await createListing(u.id, { listingType: "SALE", propertyType: "maison", ...base, title: "Maison familiale avec grand terrain boisé", priceAmount: 489000 });21    const r0 = await transition(rent.id, u.id, [], "publish"); expect(r0.problems).toContain("Au moins une photo requise"); expect(r0.problems).toContain("Attestation du droit de publier manquante");22    await fakeMedia(rent.id); await fakeMedia(sale.id); await attest(rent.id, u.id, []); await attest(sale.id, u.id, []);23    expect((await transition(rent.id, u.id, [], "publish")).listing.status).toBe("PUBLISHED");24    expect((await transition(sale.id, u.id, [], "publish")).listing.status).toBe("PUBLISHED");25    expect((await searchDirect({ market: "rent", city: "Québec" })).map((c) => c.uid)).toContain(`toveli:${rent.slug}`);26    expect((await searchDirect({ market: "sale", priceMax: 500000 })).map((c) => c.uid)).toContain(`toveli:${sale.slug}`);27    await transition(rent.id, u.id, [], "pause"); expect((await searchDirect({ market: "rent" })).length).toBe(0);28    await transition(rent.id, u.id, [], "resume"); await transition(rent.id, u.id, [], "mark-rented"); expect((await getListingFull(rent.id))!.status).toBe("RENTED");29  });30  it("un autre utilisateur ne peut ni modifier, ni publier, ni lire les messages", async () => {31    const a = await mkUser("a@exemple.ca"), b = await mkUser("b@exemple.ca");32    const l = await createListing(a.id, { listingType: "RENT", propertyType: "condo", ...base, priceAmount: 1800 });33    await expect(updateListing(l.id, b.id, [], { title: "pirate" })).rejects.toThrow(); await expect(transition(l.id, b.id, [], "publish")).rejects.toThrow();34    await fakeMedia(l.id); await attest(l.id, a.id, []); await transition(l.id, a.id, [], "publish");35    const inq = await openInquiry(l.id, b.id, "INFO", "Bonjour, est-ce encore disponible ? Mon courriel b@x.ca et mon numéro 418 555-0199.");36    const c = await mkUser("c@exemple.ca"); await expect(getThread(inq.id, c.id)).rejects.toThrow(); await expect(reply(inq.id, c.id, "intrus")).rejects.toThrow();37    const t = await getThread(inq.id, a.id); expect(t.messages[0].body).toContain("[courriel masqué]"); expect(t.messages[0].body).toContain("[téléphone masqué]");38    await expect(openInquiry(l.id, a.id, "INFO", "je m'écris à moi-même ?")).rejects.toThrow();39  });40  it("concurrence optimiste et spam → vérification humaine", async () => {41    const a = await mkUser("d@exemple.ca"); const l = await createListing(a.id, { listingType: "RENT", propertyType: "appartement", ...base, priceAmount: 900 });42    await updateListing(l.id, a.id, [], { title: "Titre un", version: 1 });43    await expect(updateListing(l.id, a.id, [], { title: "Titre périmé", version: 1 })).rejects.toThrow(ConflictError);44    await fakeMedia(l.id); await attest(l.id, a.id, []);45    await updateListing(l.id, a.id, [], { description: "Envoyez un dépôt par Western Union et les clés arrivent par la poste. Contact WhatsApp. Grand logement lumineux." });46    expect((await transition(l.id, a.id, [], "publish")).listing.status).toBe("PENDING_REVIEW");47  });48  it("visites : créneaux, confirmation par le propriétaire seulement, conflit à 30 min", async () => {49    const a = await mkUser("e@exemple.ca"), b = await mkUser("f@exemple.ca"), c = await mkUser("g@exemple.ca");50    const l = await createListing(a.id, { listingType: "SALE", propertyType: "maison", ...base, priceAmount: 400000 }); await fakeMedia(l.id); await attest(l.id, a.id, []); await transition(l.id, a.id, [], "publish");51    const slot = new Date(Date.now() + 2 * 86400000).toISOString();52    const v1 = await requestViewing(l.id, b.id, [slot]); const v2 = await requestViewing(l.id, c.id, [slot]);53    await expect(updateViewing(v1.id, b.id, "confirm", slot)).rejects.toThrow();54    await updateViewing(v1.id, a.id, "confirm", slot); await expect(updateViewing(v2.id, a.id, "confirm", slot)).rejects.toThrow(/Conflit/);55  });56  it("sanitize", () => { expect(sanitizeBody("voir https://evil.example/x et https://www.toveli.ca/annonce/1")).toBe("voir [lien retiré] et https://www.toveli.ca/annonce/1"); });57});58