126 lines
5.6 KiB
TypeScript
126 lines
5.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { GET } from "@/app/api/discover/route";
|
|
import { mapDiscover } from "@/lib/discover";
|
|
|
|
const expert = {
|
|
id: 7, slug: "jane-doe", first_name: "Jane", last_name: "Doe", byline: "Coach",
|
|
url_picture: "https://img/jane.jpg", rating: 4.9, reviews_count: 12, available: true,
|
|
city: "Austin", country: "US",
|
|
quick_question_rate: 25, video_call_rate: 100, is_blocking: false, score: 99,
|
|
leaderboard: 3, profile_views: 500, status: "active",
|
|
phone: "555-1234", dob: "1990-01-01", zip: "78701", email: "jane@x.com",
|
|
gender: "f", stripe_account_id: "acct_1", kyc_status: "verified",
|
|
};
|
|
const body = {
|
|
data: {
|
|
featured_experts: [expert],
|
|
trending_conversations: [{
|
|
id: "c1", question_text: "How?", answer_text: "secret text", views: 10,
|
|
made_public_on: "2026-01-01", thumbnail: { url: "https://img/t.jpg" },
|
|
answer: { url: "https://a/1.mp3", duration: 42, waveform_data: [1, 2, 3], file_type: "audio" },
|
|
expert,
|
|
}],
|
|
trending_categories: [
|
|
{ id: 2, name: "Money", icon: "m", sort_order: 2, active: true, search: "x" },
|
|
{ id: 1, name: "Health", icon: "h", sort_order: 1, active: true },
|
|
{ id: 3, name: "Hidden", icon: "x", sort_order: 0, active: false },
|
|
],
|
|
popular_topics: [], wallet: { balance: 5 },
|
|
},
|
|
};
|
|
|
|
async function withUpstream(impl: () => Promise<Response>) {
|
|
const orig = globalThis.fetch;
|
|
const spy = vi.fn(impl);
|
|
globalThis.fetch = spy as unknown as typeof fetch;
|
|
try {
|
|
return { res: await GET(), spy };
|
|
} finally {
|
|
globalThis.fetch = orig;
|
|
}
|
|
}
|
|
|
|
describe("GET /api/discover", () => {
|
|
it("maps public fields, sorts and filters categories, sends no auth", async () => {
|
|
const { res, spy } = await withUpstream(async () => new Response(JSON.stringify(body)));
|
|
expect(res.status).toBe(200);
|
|
const json = await res.json();
|
|
expect(json.featuredExperts[0]).toEqual({
|
|
id: 7, slug: "jane-doe", displayName: "Jane Doe", byline: "Coach",
|
|
pictureUrl: "https://img/jane.jpg", rating: 4.9, reviewsCount: 12, available: true,
|
|
location: { city: "Austin", country: "US" },
|
|
});
|
|
expect(json.trendingMinnects[0]).toEqual({
|
|
id: "c1", question: "How?", views: 10, thumbnailUrl: "https://img/t.jpg",
|
|
media: { kind: "audio", url: "https://a/1.mp3", duration: 42, waveform: [1, 2, 3] },
|
|
expert: { slug: "jane-doe", displayName: "Jane Doe", pictureUrl: "https://img/jane.jpg", byline: "Coach", rating: 4.9 },
|
|
});
|
|
expect(json.categories).toEqual([
|
|
{ id: 1, name: "Health", icon: "h", sortOrder: 1 },
|
|
{ id: 2, name: "Money", icon: "m", sortOrder: 2 },
|
|
]);
|
|
const [url, init] = spy.mock.calls[0] as unknown as [string, RequestInit];
|
|
expect(url).toBe("https://api.minnect.com/api/v1/individual/home");
|
|
expect(JSON.stringify(init.headers)).not.toMatch(/authorization/i);
|
|
});
|
|
|
|
it("never leaks PII, KYC, payment or internal fields", async () => {
|
|
const { res } = await withUpstream(async () => new Response(JSON.stringify(body)));
|
|
const text = JSON.stringify(await res.json());
|
|
for (const bad of [
|
|
"555-1234", "1990-01-01", "78701", "jane@x.com", "acct_1", "kyc", "stripe",
|
|
"is_blocking", "isBlocking", "score", "leaderboard", "profile_views",
|
|
"quick_question_rate", "video_call_rate", "secret text", "wallet", "balance",
|
|
"gender", "phone", "dob", "zip", "email",
|
|
]) {
|
|
expect(text.toLowerCase()).not.toContain(bad.toLowerCase());
|
|
}
|
|
});
|
|
|
|
it("tolerates missing fields", () => {
|
|
const out = mapDiscover({
|
|
data: {
|
|
featured_experts: [{ id: 1 }, {}, null, "x"],
|
|
trending_conversations: [{ id: 2 }, { question_text: "no id" }],
|
|
trending_categories: [{ id: 3, name: "A" }, { id: 4 }],
|
|
},
|
|
});
|
|
expect(out.featuredExperts).toEqual([{
|
|
id: 1, slug: null, displayName: "", byline: null, pictureUrl: null, rating: null,
|
|
reviewsCount: null, available: null, location: { city: null, country: null },
|
|
}]);
|
|
expect(out.trendingMinnects).toHaveLength(1);
|
|
expect(out.trendingMinnects[0].media).toBeNull();
|
|
expect(out.categories).toEqual([{ id: 3, name: "A", icon: null, sortOrder: 0 }]);
|
|
expect(mapDiscover(null)).toEqual({ featuredExperts: [], trendingMinnects: [], categories: [] });
|
|
});
|
|
|
|
it("upstream non-2xx -> 502", async () => {
|
|
const { res } = await withUpstream(async () => new Response("boom", { status: 500 }));
|
|
expect(res.status).toBe(502);
|
|
expect(await res.json()).toEqual({ error: "discover_unavailable" });
|
|
});
|
|
|
|
it("network error or bad JSON -> 502", async () => {
|
|
const a = await withUpstream(async () => { throw new Error("down"); });
|
|
expect(a.res.status).toBe(502);
|
|
const b = await withUpstream(async () => new Response("<html>"));
|
|
expect(b.res.status).toBe(502);
|
|
});
|
|
});
|
|
|
|
describe("trending media kind", () => {
|
|
const conv = (answer: unknown) => mapDiscover({ data: { trending_conversations: [{ id: 1, answer }] } }).trendingMinnects[0].media;
|
|
it.each(["MOV", "mp4", "M4V", "QuickTime", "MO4", "webm"])("maps %s to video and keeps the signed url", (t) => {
|
|
const url = "https://valuentainment-quick-questions-prod.s3.amazonaws.com/a.mov?X-Amz-Signature=abc";
|
|
expect(conv({ url, file_type: t, duration: 5 })).toEqual({ kind: "video", url, duration: 5, waveform: null });
|
|
});
|
|
it("maps other or missing file types to audio", () => {
|
|
expect(conv({ url: "https://a/x", file_type: "aac" })?.kind).toBe("audio");
|
|
expect(conv({ url: "https://a/x" })?.kind).toBe("audio");
|
|
});
|
|
it("has no media without a url", () => {
|
|
expect(conv({ file_type: "MOV" })).toBeNull();
|
|
});
|
|
});
|