diff --git a/__tests__/contexts/wave/utils/wave-messages-utils.additional.test.ts b/__tests__/contexts/wave/utils/wave-messages-utils.additional.test.ts index c0889d74b4..a86e47a23d 100644 --- a/__tests__/contexts/wave/utils/wave-messages-utils.additional.test.ts +++ b/__tests__/contexts/wave/utils/wave-messages-utils.additional.test.ts @@ -13,10 +13,51 @@ jest.mock("@/services/api/common-api"); const drop = { id: "d1", serial_no: 1, - created_at: "2020", - wave: { id: "w" }, + created_at: 1000, + updated_at: null, + is_signed: false, + hide_link_preview: false, + title: "Drop", + content: "Drop content", + media: [], + attachments: [], + parts_count: 1, + author: { + id: "author-id", + handle: "author", + primary_address: "0xauthor", + pfp: null, + level: 1, + classification: "PSEUDONYM", + badges: {}, + }, + drop_type: "CHAT", + referenced_nfts: [], + mentioned_users: [], + mentioned_groups: [], + mentioned_waves: [], + nft_links: [], + reactions: [], + boosts: 0, + context_profile_context: { + reaction: null, + boosted: false, + bookmarked: false, + }, } as any; +const wave = { + id: "w", + name: "Wave", + pfp: null, + last_drop_time: 100, + is_private: false, + context_profile_context: { + can_chat: true, + pinned: false, + }, +}; + const mockFetch = commonApiFetch as jest.Mock; const mockFetchRetry = commonApiFetchWithRetry as jest.Mock; @@ -32,12 +73,12 @@ beforeEach(() => { describe("wave-messages-utils additional", () => { it("fetchWaveMessages returns mapped drops", async () => { - mockFetch.mockResolvedValue({ drops: [drop], wave: { id: "w" } }); + mockFetch.mockResolvedValue({ drops: [drop], wave }); const res = await fetchWaveMessages("w", null); expect(mockFetch).toHaveBeenCalledWith( - expect.objectContaining({ endpoint: "waves/w/drops" }) + expect.objectContaining({ endpoint: "v2/waves/w/drops" }) ); - expect(res?.[0]?.wave).toEqual({ id: "w" }); + expect(res?.[0]?.wave).toEqual(expect.objectContaining({ id: "w" })); }); it("fetchWaveMessages rethrows abort errors", async () => { @@ -47,7 +88,7 @@ describe("wave-messages-utils additional", () => { }); it("fetchAroundSerialNoWaveMessages uses retry fetch", async () => { - mockFetchRetry.mockResolvedValue({ drops: [drop], wave: { id: "w" } }); + mockFetchRetry.mockResolvedValue({ drops: [drop], wave }); const res = await fetchAroundSerialNoWaveMessages("w", 5); expect(mockFetchRetry).toHaveBeenCalled(); expect(res?.[0]?.serial_no).toBe(1); @@ -62,17 +103,16 @@ describe("wave-messages-utils additional", () => { return batch; } - if (options.endpoint === "waves/w/drops") { + if (options.endpoint === "v2/waves/w/drops") { return { drops: [ { + ...drop, serial_no: 5, id: "full-5", - created_at: "2020-01-01", - wave: { id: "w" }, }, ], - wave: { id: "w" }, + wave, }; } @@ -101,8 +141,8 @@ describe("wave-messages-utils additional", () => { return []; } - if (options.endpoint === "waves/w/drops") { - return { drops: [], wave: { id: "w" } }; + if (options.endpoint === "v2/waves/w/drops") { + return { drops: [], wave }; } throw new Error(`Unexpected endpoint: ${options.endpoint}`); diff --git a/__tests__/contexts/wave/utils/wave-messages-utils.test.ts b/__tests__/contexts/wave/utils/wave-messages-utils.test.ts index cf8b127cbb..773d3291ab 100644 --- a/__tests__/contexts/wave/utils/wave-messages-utils.test.ts +++ b/__tests__/contexts/wave/utils/wave-messages-utils.test.ts @@ -23,6 +23,56 @@ const sampleDrop = { metadata: [], }; +const sampleIdentity = { + id: "author-id", + handle: "author", + primary_address: "0xauthor", + pfp: null, + level: 1, + classification: "PSEUDONYM", + badges: {}, +}; + +const sampleWaveOverview = { + id: "wave-1", + name: "Wave 1", + pfp: null, + last_drop_time: 100, + is_private: false, + context_profile_context: { + can_chat: true, + pinned: false, + }, +}; + +const sampleV2Drop = { + id: "drop-1", + serial_no: 5, + created_at: 1000, + updated_at: null, + is_signed: false, + hide_link_preview: false, + title: "Drop 1", + content: "Drop content", + media: [], + attachments: [], + parts_count: 1, + author: sampleIdentity, + drop_type: "CHAT", + referenced_nfts: [], + mentioned_users: [], + mentioned_groups: [], + mentioned_waves: [], + nft_links: [], + reactions: [], + boosts: 0, + context_profile_context: { + reaction: null, + boosted: false, + bookmarked: false, + }, +}; + afterEach(() => { jest.clearAllMocks(); }); @@ -88,15 +138,15 @@ describe("mergeDrops", () => { describe("fetchNewestWaveMessages", () => { it("fetches latest drops and annotates wave data", async () => { (commonApiFetchWithRetry as jest.Mock).mockResolvedValue({ - drops: [sampleDrop], - wave: { id: "wave-1", authenticated_user_admin: true }, + drops: [sampleV2Drop], + wave: sampleWaveOverview, }); const result = await fetchNewestWaveMessages("wave-1", null, 10); expect(commonApiFetchWithRetry).toHaveBeenCalledWith( expect.objectContaining({ - endpoint: "waves/wave-1/drops", + endpoint: "v2/waves/wave-1/drops", params: { limit: "10" }, signal: undefined, retryOptions: expect.objectContaining({ maxRetries: 2 }), @@ -104,7 +154,7 @@ describe("fetchNewestWaveMessages", () => { ); expect(result.drops?.[0]).toMatchObject({ id: "drop-1", - wave: { authenticated_user_admin: true }, + wave: { id: "wave-1", authenticated_user_admin: false }, }); expect(result.highestSerialNo).toBe(5); }); diff --git a/__tests__/services/api/wave-drops-v2-api.test.ts b/__tests__/services/api/wave-drops-v2-api.test.ts index 749651033d..8211df3b38 100644 --- a/__tests__/services/api/wave-drops-v2-api.test.ts +++ b/__tests__/services/api/wave-drops-v2-api.test.ts @@ -1,9 +1,13 @@ import { ApiDropMainType } from "@/generated/models/ApiDropMainType"; import type { ApiDropV2 } from "@/generated/models/ApiDropV2"; import { ApiProfileClassification } from "@/generated/models/ApiProfileClassification"; +import { ApiSubmissionDropStatus } from "@/generated/models/ApiSubmissionDropStatus"; import type { ApiWaveMin } from "@/generated/models/ApiWaveMin"; import { commonApiFetch } from "@/services/api/common-api"; import { + fetchBoostedDropsV2, + fetchDropRepliesV2, + fetchDropV2ById, fetchWaveDropsFeedV2, mapLeaderboardDropV2, } from "@/services/api/wave-drops-v2-api"; @@ -80,6 +84,54 @@ const createDrop = (partsCount: number) => ({ }, }); +const priorityMetadata = [ + { + data_key: "additional_media", + data_value: JSON.stringify({ + preview_image: "ipfs://preview-image", + }), + }, +]; + +const createEnrichableDrop = (overrides: Partial = {}) => ({ + ...createDrop(1), + drop_type: ApiDropMainType.Submission, + priority_metadata: priorityMetadata, + submission_context: { + status: ApiSubmissionDropStatus.Active, + has_metadata: true, + voting: { + is_open: true, + total_votes_given: 0, + current_calculated_vote: 10, + predicted_final_vote: 12, + voters_count: 7, + place: 2, + }, + }, + ...overrides, +}); + +const waveMin = { + id: "wave-1", + name: "Wave 1", + picture: null, + voting_credit_type: "TDH", +} as unknown as ApiWaveMin; + +const expectNoListEnrichmentCalls = () => { + expect(commonApiFetchMock).not.toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining("/metadata"), + }) + ); + expect(commonApiFetchMock).not.toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining("/votes"), + }) + ); +}; + describe("fetchWaveDropsFeedV2", () => { beforeEach(() => { jest.clearAllMocks(); @@ -148,14 +200,6 @@ describe("fetchWaveDropsFeedV2", () => { }); it("maps V2 priority metadata into hydrated legacy drops", async () => { - const priorityMetadata = [ - { - data_key: "additional_media", - data_value: JSON.stringify({ - preview_image: "ipfs://preview-image", - }), - }, - ]; commonApiFetchMock.mockResolvedValueOnce({ wave, drops: [ @@ -175,6 +219,24 @@ describe("fetchWaveDropsFeedV2", () => { expect(result.drops[0]?.metadata).toEqual(priorityMetadata); }); + it("does not fetch full metadata or top raters for list drops", async () => { + commonApiFetchMock.mockResolvedValueOnce({ + wave, + drops: [createEnrichableDrop()], + }); + + const result = await fetchWaveDropsFeedV2({ + waveId: "wave-1", + limit: 20, + }); + + expect(commonApiFetchMock).toHaveBeenCalledTimes(1); + expectNoListEnrichmentCalls(); + expect(result.drops[0]?.metadata).toEqual(priorityMetadata); + expect(result.drops[0]?.top_raters).toEqual([]); + expect(result.drops[0]?.raters_count).toBe(7); + }); + it("maps V2 priority metadata into leaderboard legacy drops", () => { const priorityMetadata = [ { @@ -254,3 +316,105 @@ describe("fetchWaveDropsFeedV2", () => { ).rejects.toBe(abortError); }); }); + +describe("fetchDropRepliesV2", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("does not fetch full metadata or top raters for reply lists", async () => { + commonApiFetchMock.mockResolvedValueOnce({ + data: [createEnrichableDrop()], + count: 1, + page: 1, + next: false, + }); + + const result = await fetchDropRepliesV2({ + parentDropId: "parent-drop", + page: 1, + pageSize: 20, + wave: waveMin, + }); + + expect(commonApiFetchMock).toHaveBeenCalledTimes(1); + expect(commonApiFetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: "v2/drops", + }) + ); + expectNoListEnrichmentCalls(); + expect(result.drops[0]?.metadata).toEqual(priorityMetadata); + expect(result.drops[0]?.top_raters).toEqual([]); + }); +}); + +describe("fetchBoostedDropsV2", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("does not fetch full metadata or top raters for boosted drop lists", async () => { + commonApiFetchMock.mockResolvedValueOnce({ + data: [createEnrichableDrop()], + count: 1, + page: 1, + next: false, + }); + + const result = await fetchBoostedDropsV2({ + waveId: "wave-1", + wave: waveMin, + limit: 10, + }); + + expect(commonApiFetchMock).toHaveBeenCalledTimes(1); + expect(commonApiFetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: "v2/boosted-drops", + }) + ); + expectNoListEnrichmentCalls(); + expect(result[0]?.metadata).toEqual(priorityMetadata); + expect(result[0]?.top_raters).toEqual([]); + }); +}); + +describe("fetchDropV2ById", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("keeps full metadata hydration for single drop details while allowing top raters to be skipped", async () => { + const fullMetadata = [{ data_key: "artist", data_value: "Alice" }]; + commonApiFetchMock + .mockResolvedValueOnce({ + wave, + drop: createEnrichableDrop(), + }) + .mockResolvedValueOnce(fullMetadata); + + const result = await fetchDropV2ById("drop-1", undefined, { + includeTopRaters: false, + }); + + expect(commonApiFetchMock).toHaveBeenCalledTimes(2); + expect(commonApiFetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: "v2/drops/drop-1", + }) + ); + expect(commonApiFetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: "v2/drops/drop-1/metadata", + }) + ); + expect(commonApiFetchMock).not.toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: expect.stringContaining("/votes"), + }) + ); + expect(result.metadata).toEqual([...priorityMetadata, ...fullMetadata]); + expect(result.top_raters).toEqual([]); + }); +}); diff --git a/services/api/wave-drops-v2-api.ts b/services/api/wave-drops-v2-api.ts index e8a3a4e76c..cd1a7f77c9 100644 --- a/services/api/wave-drops-v2-api.ts +++ b/services/api/wave-drops-v2-api.ts @@ -209,11 +209,12 @@ const mergeMetadata = ( const fetchDropMetadataV2 = async ( drop: ApiDropV2, - signal?: AbortSignal + signal?: AbortSignal, + includeFullMetadata = true ): Promise => { const priorityMetadata = mapPriorityMetadataV2ToDropMetadata(drop); - if (!drop.submission_context?.has_metadata) { + if (!includeFullMetadata || !drop.submission_context?.has_metadata) { return priorityMetadata; } @@ -291,16 +292,18 @@ const hydrateDropV2 = async ({ drop, wave, signal, + includeFullMetadata = true, includeTopRaters = true, }: { readonly drop: ApiDropV2; readonly wave: ApiWaveMin; readonly signal?: AbortSignal | undefined; + readonly includeFullMetadata?: boolean | undefined; readonly includeTopRaters?: boolean | undefined; }): Promise => { const [parts, metadata, topRaters] = await Promise.all([ hydrateDropParts(drop, signal), - fetchDropMetadataV2(drop, signal), + fetchDropMetadataV2(drop, signal, includeFullMetadata), includeTopRaters ? fetchTopRatersV2(drop, signal) : Promise.resolve([]), ]); const voting = drop.submission_context?.voting; @@ -391,12 +394,26 @@ const hydrateDropsV2 = async ({ drops, wave, signal, + includeFullMetadata = false, + includeTopRaters = false, }: { readonly drops: ApiDropV2[]; readonly wave: ApiWaveMin; readonly signal?: AbortSignal | undefined; + readonly includeFullMetadata?: boolean | undefined; + readonly includeTopRaters?: boolean | undefined; }): Promise => - Promise.all(drops.map((drop) => hydrateDropV2({ drop, wave, signal }))); + Promise.all( + drops.map((drop) => + hydrateDropV2({ + drop, + wave, + signal, + includeFullMetadata, + includeTopRaters, + }) + ) + ); const getNormalizedDropId = (dropId: string): string => { const normalizedDropId = dropId.trim(); @@ -520,7 +537,10 @@ export async function fetchWaveDropsSearchV2({ export async function fetchDropV2ById( dropId: string, signal?: AbortSignal, - options?: { readonly includeTopRaters?: boolean | undefined } + options?: { + readonly includeFullMetadata?: boolean | undefined; + readonly includeTopRaters?: boolean | undefined; + } ): Promise { const data = await fetchDropAndWaveV2(dropId, signal); const wave = mapApiWaveOverviewToApiWaveMin(data.wave); @@ -528,6 +548,7 @@ export async function fetchDropV2ById( drop: data.drop, wave, signal, + includeFullMetadata: options?.includeFullMetadata, includeTopRaters: options?.includeTopRaters, }); }