From 3ac0107abefd7f8ca3bc96b40f7083b5b1f71696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Struck?= <98230431+michalstruck@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:08:33 +0100 Subject: [PATCH] remove external category properly (#1215) * remove external category properly * category validation * typefixes --- web/api/v2/public/app/[app_id]/index.ts | 11 +++- web/api/v2/public/apps/index.ts | 36 +++++++++++- web/lib/categories.ts | 7 ++- web/tests/api/v2/public/apps/app.test.ts | 60 +++++++++++++++++++ web/tests/api/v2/public/apps/apps.test.ts | 71 ++++++++++++++++++++++- 5 files changed, 176 insertions(+), 9 deletions(-) diff --git a/web/api/v2/public/app/[app_id]/index.ts b/web/api/v2/public/app/[app_id]/index.ts index f6247b81f..6a55c7b7d 100644 --- a/web/api/v2/public/app/[app_id]/index.ts +++ b/web/api/v2/public/app/[app_id]/index.ts @@ -1,5 +1,6 @@ import { formatAppMetadata } from "@/api/helpers/app-store"; import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; +import { getAppStoreLocalisedCategoriesWithUrls } from "@/lib/categories"; import { NativeAppToAppIdMapping, NativeApps } from "@/lib/constants"; import { parseLocale } from "@/lib/languages"; import { AppStatsReturnType } from "@/lib/types"; @@ -136,13 +137,21 @@ export async function GET( app_id: nativeAppItem.app_id, }; } + const categories = getAppStoreLocalisedCategoriesWithUrls(locale); + const isCategoryValid = categories.some( + (category) => category?.id === formattedMetadata.category.id, + ); + + if (!isCategoryValid) { + return NextResponse.json({ error: "Invalid category" }, { status: 500 }); + } return NextResponse.json( { app_data: formattedMetadata }, { status: 200, headers: { - "Cache-Control": "public, max-age=86400, stale-if-error=86400", + "Cache-Control": "public, max-age=5, stale-if-error=86400", }, }, ); diff --git a/web/api/v2/public/apps/index.ts b/web/api/v2/public/apps/index.ts index e788970e1..7da1f6c9d 100644 --- a/web/api/v2/public/apps/index.ts +++ b/web/api/v2/public/apps/index.ts @@ -3,7 +3,7 @@ import { getAPIServiceGraphqlClient } from "@/api/helpers/graphql"; import { validateRequestSchema } from "@/api/helpers/validate-request-schema"; import { AllCategory, - getAllLocalisedCategoriesWithUrls, + getAppStoreLocalisedCategoriesWithUrls, getLocalisedCategory, } from "@/lib/categories"; import { NativeApps } from "@/lib/constants"; @@ -32,6 +32,7 @@ const queryParamsSchema = yup.object({ .oneOf(["mini-app", "external", "native"]) .notRequired(), override_country: yup.string().notRequired(), + show_external: yup.boolean().notRequired().default(false), }); export const GET = async (request: NextRequest) => { @@ -132,6 +133,15 @@ export const GET = async (request: NextRequest) => { }); } + if (!parsedParams.show_external) { + topApps = topApps.filter( + (app) => app.category.toLowerCase() !== "external", + ); + highlightsApps = highlightsApps.filter( + (app) => app.category.toLowerCase() !== "external", + ); + } + // ANCHOR: Filter top apps by country if (country && topApps.length > 0) { topApps = topApps.filter((app) => @@ -212,6 +222,26 @@ export const GET = async (request: NextRequest) => { return aIndex - bIndex; }); + // validate all apps have valid categories + const categories = getAppStoreLocalisedCategoriesWithUrls(locale); + const areAppCategoriesValid = + formattedTopApps.every((app) => + categories.some((category) => category?.id === app.category.id), + ) && + highlightedApps.every((app) => + categories.some((category) => category?.id === app.category.id), + ); + + if (!areAppCategoriesValid) { + return errorResponse({ + statusCode: 500, + code: "invalid_categories", + detail: "Some apps have invalid categories", + attribute: null, + req: request, + }); + } + return NextResponse.json( { app_rankings: { @@ -222,13 +252,13 @@ export const GET = async (request: NextRequest) => { ...AllCategory, name: getLocalisedCategory(AllCategory.name, locale).name, }, - categories: getAllLocalisedCategoriesWithUrls(locale), // TODO: Localise + categories, }, { headers: { // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#ExpirationDownloadDist // https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-cloudfront-stale-while-revalidate-stale-if-error-cache-control-directives/ - "Cache-Control": "public, max-age=86400, stale-if-error=86400", + "Cache-Control": "public, max-age=5, stale-if-error=86400", }, }, ); diff --git a/web/lib/categories.ts b/web/lib/categories.ts index c5007d47c..c371ed1ea 100644 --- a/web/lib/categories.ts +++ b/web/lib/categories.ts @@ -297,10 +297,13 @@ export const getLocalisedCategory = ( }; }; -export const getAllLocalisedCategoriesWithUrls = (locale: string) => { +export const getAppStoreLocalisedCategoriesWithUrls = (locale: string) => { const defaultLocale = locale || "en"; return Categories.map((category) => { + if (category.id === "external") { + return null; + } const { id, name } = getLocalisedCategory(category.name, defaultLocale); return { id, name, icon_url: category.icon_url }; - }); + }).filter((category) => category !== null); }; diff --git a/web/tests/api/v2/public/apps/app.test.ts b/web/tests/api/v2/public/apps/app.test.ts index 1948f1cfa..9805e7ce2 100644 --- a/web/tests/api/v2/public/apps/app.test.ts +++ b/web/tests/api/v2/public/apps/app.test.ts @@ -679,4 +679,64 @@ describe("/api/public/app/[app_id]", () => { expect(data.error).toBe("Draft already verified"); }); }); + describe("response integrity", () => { + test("should return 500 when category is invalid", async () => { + jest.mocked(getAppMetadataSdk).mockImplementation(() => ({ + GetAppMetadata: jest.fn().mockResolvedValue({ + app_metadata: [ + { + id: "1", + name: "Example App", + app_id: "test-app", + short_name: "test", + logo_img_url: "logo.png", + showcase_img_urls: ["showcase1.png", "showcase2.png"], + hero_image_url: "hero.png", + world_app_description: + "This is an example app designed to showcase the capabilities of our platform.", + world_app_button_text: "Use Integration", + category: "INVALID!!", + description: + '{"description_overview":"fewf","description_how_it_works":"few","description_connect":"fewf"}', + integration_url: "https://example.com/integration", + app_website_url: "https://example.com", + source_code_url: "https://github.com/example/app", + whitelisted_addresses: ["0x1234", "0x5678"], + app_mode: "mini-app", + support_link: "michal@gmail.com", + supported_countries: ["us"], + associated_domains: ["https://worldcoin.org"], + contracts: ["0x0c892815f0B058E69987920A23FBb33c834289cf"], + permit2_tokens: ["0x0c892815f0B058E69987920A23FBb33c834289cf"], + supported_languages: ["en", "es"], + is_reviewer_world_app_approved: true, + verification_status: "verified", + is_allowed_unlimited_notifications: false, + max_notifications_per_day: 10, + app: { + team: { + name: "Example Team", + }, + rating_sum: 10, + rating_count: 3, + }, + }, + ], + }), + })); + + const request = new NextRequest( + "https://cdn.test.com/api/public/app/test-app", + { + headers: { + host: "cdn.test.com", + }, + }, + ); + const response = await GET(request, { params: { app_id: "test-app" } }); + expect(response.status).toBe(500); + const data = await response.json(); + expect(data).toEqual({ error: "Invalid category" }); + }); + }); }); diff --git a/web/tests/api/v2/public/apps/apps.test.ts b/web/tests/api/v2/public/apps/apps.test.ts index 44f7ff54b..edf05ce25 100644 --- a/web/tests/api/v2/public/apps/apps.test.ts +++ b/web/tests/api/v2/public/apps/apps.test.ts @@ -342,7 +342,7 @@ describe("/api/v2/public/apps", () => { expect(await response.json()).toEqual({ app_rankings: { top_apps: [], highlights: [] }, all_category: AllCategory, - categories: Categories, + categories: Categories.filter((category) => category.id !== "external"), }); }); @@ -573,8 +573,8 @@ describe("/api/v2/public/apps", () => { }, ], }, + categories: Categories.filter((category) => category.id !== "external"), all_category: AllCategory, - categories: Categories, }); }); @@ -685,8 +685,73 @@ describe("/api/v2/public/apps", () => { ], highlights: [], }, + categories: Categories.filter((category) => category.id !== "external"), all_category: AllCategory, - categories: Categories, }); }); + + test("Error on invalid category", async () => { + jest.mocked(getWebHighlightsSdk).mockImplementation(() => ({ + GetHighlights: jest.fn().mockResolvedValue({ + app_rankings: [{ rankings: [] }], + }), + })); + jest.mocked(getHighlightsSdk).mockImplementation(() => ({ + GetHighlights: jest.fn().mockResolvedValue({ + highlights: [], + }), + })); + + jest.mocked(getAppsSdk).mockImplementation(() => ({ + GetApps: jest.fn().mockResolvedValue({ + top_apps: [ + { + name: "Example App", + app_id: "app_test_123", + short_name: "test", + logo_img_url: "logo.png", + showcase_img_urls: ["showcase1.png", "showcase2.png"], + hero_image_url: "hero.png", + world_app_description: + "This is an example app designed to showcase the capabilities of our platform.", + world_app_button_text: "Use Integration", + category: "INVALID!!", + description: + '{"description_overview":"fewf","description_how_it_works":"few","description_connect":"fewf"}', + integration_url: "https://example.com/integration", + app_website_url: "https://example.com", + source_code_url: "https://github.com/example/app", + whitelisted_addresses: ["0x1234", "0x5678"], + app_mode: "mini-app", + support_link: "andy@gmail.com", + associated_domains: ["https://worldcoin.org"], + contracts: ["0x0c892815f0B058E69987920A23FBb33c834289cf"], + permit2_tokens: ["0x0c892815f0B058E69987920A23FBb33c834289cf"], + supported_countries: ["us"], + supported_languages: ["en", "es"], + verification_status: "verified", + is_allowed_unlimited_notifications: false, + max_notifications_per_day: 10, + app: { + team: { + name: "Example Team", + }, + rating_sum: 10, + rating_count: 3, + }, + }, + ], + }), + })); + + const request = new NextRequest("https://cdn.test.com/api/v2/public/apps", { + headers: { + host: "cdn.test.com", + }, + }); + + const response = await GET(request); + + expect(response.status).toEqual(500); + }); });