import { GET } from "@/app/api/youtube/route" import { describe, it, expect, vi, beforeEach } from "vitest" // Mock fetch globally const mockFetch = vi.fn() vi.stubGlobal("fetch", mockFetch) describe("/api/youtube", () => { beforeEach(() => { vi.clearAllMocks() }) const mockXmlResponse = ` test-video-id-1 Test Video Title 1 This is a test video description that should be truncated if it exceeds 150 characters to ensure proper display in the UI components. 2024-01-01T00:00:00.000Z test-video-id-2 Test Video Title 2 & Special Characters Short description 2024-01-02T00:00:00.000Z ` const expectedVideos = [ { id: "test-video-id-1", title: "Test Video Title 1", description: "This is a test video description that should be truncated if it exceeds 150 characters to ensure proper display in the UI components.", thumbnailUrl: "https://i.ytimg.com/vi/test-video-id-1/hqdefault.jpg", publishedAt: "2024-01-01T00:00:00.000Z", channelTitle: "Privacy Stewards of Ethereum", url: "https://www.youtube.com/watch?v=test-video-id-1", }, { id: "test-video-id-2", title: "Test Video Title 2 & Special Characters", description: "Short description", thumbnailUrl: "https://i.ytimg.com/vi/test-video-id-2/hqdefault.jpg", publishedAt: "2024-01-02T00:00:00.000Z", channelTitle: "Privacy Stewards of Ethereum", url: "https://www.youtube.com/watch?v=test-video-id-2", }, ] describe("GET /api/youtube", () => { it("successfully fetches and parses YouTube videos from RSS feed", async () => { mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(mockXmlResponse), }) const response = await GET() const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ videos: expectedVideos, }) expect(mockFetch).toHaveBeenCalledWith( "https://www.youtube.com/feeds/videos.xml?channel_id=UCh7qkafm95-kRiLMVPlbIcQ", { next: { revalidate: 3600 } } ) }) it("handles YouTube RSS feed HTTP errors", async () => { mockFetch.mockResolvedValue({ ok: false, status: 404, }) const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => {}) const response = await GET() const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ videos: [], }) expect(consoleErrorSpy).toHaveBeenCalledWith( "Error fetching videos from RSS feed:", expect.any(Error) ) consoleErrorSpy.mockRestore() }) it("handles network errors gracefully", async () => { mockFetch.mockRejectedValue(new Error("Network error")) const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementation(() => {}) const response = await GET() const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ videos: [], }) expect(consoleErrorSpy).toHaveBeenCalledWith( "Error fetching videos from RSS feed:", expect.any(Error) ) consoleErrorSpy.mockRestore() }) it("limits videos to maximum count", async () => { // Create XML with more than 6 videos (MAX_VIDEOS = 6) const manyVideosXml = ` ${Array.from( { length: 10 }, (_, i) => ` video-${i} Video ${i} Description ${i} 2024-01-0${(i % 9) + 1}T00:00:00.000Z ` ).join("")} ` mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(manyVideosXml), }) const response = await GET() const data = await response.json() expect(response.status).toBe(200) expect(data.videos).toHaveLength(6) // MAX_VIDEOS = 6 }) it("handles malformed XML gracefully", async () => { mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve("Invalid XML content"), }) const response = await GET() const data = await response.json() expect(response.status).toBe(200) expect(data).toEqual({ videos: [], }) }) it("properly decodes HTML entities in video titles and descriptions", async () => { const xmlWithEntities = ` test-video Title with & <special> "characters" 'test' Description with & <entities> 2024-01-01T00:00:00.000Z ` mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(xmlWithEntities), }) const response = await GET() const data = await response.json() expect(data.videos[0].title).toBe( "Title with & \"characters\" 'test'" ) expect(data.videos[0].description).toBe("Description with & ") }) it("handles entries missing required fields", async () => { const incompleteXml = ` Video without ID Description 2024-01-01T00:00:00.000Z valid-video Valid Video Valid Description 2024-01-01T00:00:00.000Z ` mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(incompleteXml), }) const response = await GET() const data = await response.json() expect(data.videos).toHaveLength(1) expect(data.videos[0].id).toBe("valid-video") }) it("generates correct thumbnail URLs", async () => { mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(mockXmlResponse), }) const response = await GET() const data = await response.json() expect(data.videos[0].thumbnailUrl).toBe( "https://i.ytimg.com/vi/test-video-id-1/hqdefault.jpg" ) }) it("generates correct video URLs", async () => { mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(mockXmlResponse), }) const response = await GET() const data = await response.json() expect(data.videos[0].url).toBe( "https://www.youtube.com/watch?v=test-video-id-1" ) }) it("handles missing published date gracefully", async () => { const xmlWithoutDate = ` test-video Video without date Description ` mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(xmlWithoutDate), }) const response = await GET() const data = await response.json() expect(data.videos[0].publishedAt).toMatch( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ ) }) it("uses correct channel ID in RSS URL", async () => { mockFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve(""), }) await GET() expect(mockFetch).toHaveBeenCalledWith( "https://www.youtube.com/feeds/videos.xml?channel_id=UCh7qkafm95-kRiLMVPlbIcQ", expect.any(Object) ) }) }) })