UNPKG

@follow-app/client-sdk

Version:

TypeScript client SDK for Follow RSS Server API

478 lines (403 loc) 14.6 kB
import { beforeEach, describe, expect, it, vi } from "vitest" import { FollowClient } from "./core" // Create mock functions that can be tracked const mockHttpClientInstance = { request: vi.fn(), get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), setHeaders: vi.fn(), setFetch: vi.fn(), setConfig: vi.fn(), getConfig: vi.fn().mockReturnValue({ baseURL: "https://api.follow.is", timeout: 30000, headers: {}, credentials: "include", }), getInterceptors: vi.fn().mockReturnValue({ addRequestInterceptor: vi.fn(), addResponseInterceptor: vi.fn(), addErrorInterceptor: vi.fn(), }), } // Mock the HttpClient vi.mock("./base", () => ({ HttpClient: vi.fn().mockImplementation(() => mockHttpClientInstance), })) // Mock the proxy creation vi.mock("./proxy", () => ({ createAPIProxy: vi .fn() .mockImplementation((_httpClient, moduleDefinition) => { // Create mock functions based on module definition routes const createMockRoutes = (routes: any, _prefix = "") => { const result: any = {} for (const [key, value] of Object.entries(routes)) { if ( typeof value === "object" && value !== null && !("method" in value) ) { // Nested routes result[key] = createMockRoutes(value, `${_prefix}/${key}`) } else { // Route definition result[key] = vi.fn().mockResolvedValue({ code: 0, data: {} }) } } return result } return createMockRoutes(moduleDefinition.routes) }), })) // Mock the module registry vi.mock("../modules/registry", () => ({ moduleRegistry: { admin: { name: "admin", prefix: "/admin", routes: { featureFlags: { list: { method: "GET", path: "/admin/feature-flags" }, update: { method: "PUT", path: "/admin/feature-flags/{name}" }, }, }, }, entries: { name: "entries", prefix: "/entries", routes: { get: { method: "GET", path: "/entries" }, list: { method: "POST", path: "/entries/list" }, }, }, feeds: { name: "feeds", prefix: "/feeds", routes: { get: { method: "GET", path: "/feeds" }, refresh: { method: "POST", path: "/feeds/refresh" }, }, }, lists: { name: "lists", prefix: "/lists", routes: { list: { method: "GET", path: "/lists" }, create: { method: "POST", path: "/lists" }, }, }, reads: { name: "reads", prefix: "/reads", routes: { get: { method: "GET", path: "/reads" }, markAsRead: { method: "POST", path: "/reads" }, }, }, settings: { name: "settings", prefix: "/settings", routes: { get: { method: "GET", path: "/settings" }, update: { method: "PATCH", path: "/settings/{tab}" }, }, }, subscriptions: { name: "subscriptions", prefix: "/subscriptions", routes: { get: { method: "GET", path: "/subscriptions" }, create: { method: "POST", path: "/subscriptions" }, }, }, }, })) // Mock the interceptors vi.mock("./interceptors", () => ({ commonInterceptors: { logRequests: vi.fn().mockImplementation((config) => (req: any) => { config.log?.(`Request: ${req.method} ${req.url}`) return req }), logResponses: vi.fn().mockImplementation((config) => (res: any) => { config.log?.(`Response: ${res.status}`) return res }), }, })) describe("FollowClient Core", () => { let client: FollowClient beforeEach(() => { // Clear all mocks before each test vi.clearAllMocks() client = new FollowClient({ baseURL: "https://api.follow.is", authToken: "test-token", }) }) describe("Client Initialization", () => { it("should initialize with default configuration", () => { vi.clearAllMocks() // Clear mocks from beforeEach const defaultClient = new FollowClient() expect(defaultClient).toBeInstanceOf(FollowClient) // Verify that the client is properly constructed (no specific getConfig call expected) expect(defaultClient).toBeDefined() }) it("should initialize with custom configuration", () => { vi.clearAllMocks() // Clear mocks from beforeEach const customClient = new FollowClient({ baseURL: "https://custom.api.com", timeout: 60000, headers: { "Custom-Header": "value" }, credentials: "same-origin", }) expect(customClient).toBeInstanceOf(FollowClient) // Verify that the client is properly constructed (no specific getConfig call expected) expect(customClient).toBeDefined() }) it("should initialize with auth token", () => { vi.clearAllMocks() // Clear mocks from beforeEach const clientWithAuth = new FollowClient({ authToken: "my-auth-token", }) expect(clientWithAuth).toBeInstanceOf(FollowClient) expect(mockHttpClientInstance.setHeaders).toHaveBeenCalledWith({ Authorization: "Bearer my-auth-token", }) }) it("should initialize with default interceptors when enabled", () => { vi.clearAllMocks() // Clear mocks from beforeEach const clientWithInterceptors = new FollowClient({ enableDefaultInterceptors: true, }) expect(clientWithInterceptors).toBeInstanceOf(FollowClient) expect(mockHttpClientInstance.getInterceptors).toHaveBeenCalled() }) }) describe("API Module Initialization", () => { it("should initialize all API modules", () => { expect(client.api.admin).toBeDefined() expect(client.api.entries).toBeDefined() expect(client.api.feeds).toBeDefined() expect(client.api.lists).toBeDefined() expect(client.api.reads).toBeDefined() expect(client.api.settings).toBeDefined() expect(client.api.subscriptions).toBeDefined() }) it("should have functional API methods", async () => { expect(typeof client.api.admin.featureFlags.list).toBe("function") expect(typeof client.api.entries.get).toBe("function") expect(typeof client.api.feeds.get).toBe("function") expect(typeof client.api.lists.list).toBe("function") expect(typeof client.api.reads.get).toBe("function") expect(typeof client.api.settings.get).toBe("function") expect(typeof client.api.subscriptions.get).toBe("function") }) }) describe("Authentication Methods", () => { it("should set auth token", () => { client.setAuthToken("new-token") expect(mockHttpClientInstance.setHeaders).toHaveBeenCalledWith({ Authorization: "Bearer new-token", }) }) it("should remove auth token", () => { mockHttpClientInstance.getConfig.mockReturnValue({ headers: { "Authorization": "Bearer old-token", "Other-Header": "value" }, }) client.removeAuthToken() expect(mockHttpClientInstance.setHeaders).toHaveBeenCalledWith({ "Other-Header": "value", }) }) }) describe("Configuration Methods", () => { it("should set custom headers", () => { const customHeaders = { "X-Custom-Header": "custom-value", "X-Another-Header": "another-value", } client.setHeaders(customHeaders) expect(mockHttpClientInstance.setHeaders).toHaveBeenCalledWith( customHeaders, ) }) it("should set custom fetch instance", () => { const customFetch = vi.fn() client.setFetch(customFetch) expect(mockHttpClientInstance.setFetch).toHaveBeenCalledWith(customFetch) }) it("should update client configuration", () => { const newConfig = { baseURL: "https://new.api.com", timeout: 45000, } client.updateConfig(newConfig) expect(mockHttpClientInstance.setConfig).toHaveBeenCalledWith(newConfig) }) it("should get current configuration", () => { const mockConfig = { baseURL: "https://api.follow.is", timeout: 30000, headers: {}, credentials: "include", } mockHttpClientInstance.getConfig.mockReturnValue(mockConfig) const config = client.getConfig() expect(config).toEqual(mockConfig) expect(mockHttpClientInstance.getConfig).toHaveBeenCalled() }) }) describe("Utility Methods", () => { it("should batch multiple requests", async () => { const request1 = Promise.resolve({ data: "result1" }) const request2 = Promise.resolve({ data: "result2" }) const request3 = Promise.resolve({ data: "result3" }) const results = await client.batch([request1, request2, request3]) expect(results).toEqual([ { data: "result1" }, { data: "result2" }, { data: "result3" }, ]) }) it("should handle batch request failures", async () => { const request1 = Promise.resolve({ data: "result1" }) const request2 = Promise.reject(new Error("Request failed")) const request3 = Promise.resolve({ data: "result3" }) await expect( client.batch([request1, request2, request3]), ).rejects.toThrow("Request failed") }) it("should clone client with new configuration", () => { const originalConfig = { baseURL: "https://api.follow.is", timeout: 30000, headers: {}, credentials: "include", } mockHttpClientInstance.getConfig.mockReturnValue(originalConfig) const clonedClient = client.clone({ baseURL: "https://new.api.com", timeout: 60000, }) expect(clonedClient).toBeInstanceOf(FollowClient) expect(clonedClient).not.toBe(client) }) }) describe("Interceptor Methods", () => { it("should add request interceptor", () => { const requestInterceptor = vi.fn() const removeInterceptor = vi.fn() mockHttpClientInstance.getInterceptors.mockReturnValue({ addRequestInterceptor: vi.fn().mockReturnValue(removeInterceptor), addResponseInterceptor: vi.fn(), addErrorInterceptor: vi.fn(), }) const result = client.addRequestInterceptor(requestInterceptor) expect( mockHttpClientInstance.getInterceptors().addRequestInterceptor, ).toHaveBeenCalledWith(requestInterceptor) expect(result).toBe(removeInterceptor) }) it("should add response interceptor", () => { const responseInterceptor = vi.fn() const removeInterceptor = vi.fn() mockHttpClientInstance.getInterceptors.mockReturnValue({ addRequestInterceptor: vi.fn(), addResponseInterceptor: vi.fn().mockReturnValue(removeInterceptor), addErrorInterceptor: vi.fn(), }) const result = client.addResponseInterceptor(responseInterceptor) expect( mockHttpClientInstance.getInterceptors().addResponseInterceptor, ).toHaveBeenCalledWith(responseInterceptor) expect(result).toBe(removeInterceptor) }) it("should add error interceptor", () => { const errorInterceptor = vi.fn() const removeInterceptor = vi.fn() mockHttpClientInstance.getInterceptors.mockReturnValue({ addRequestInterceptor: vi.fn(), addResponseInterceptor: vi.fn(), addErrorInterceptor: vi.fn().mockReturnValue(removeInterceptor), }) const result = client.addErrorInterceptor(errorInterceptor) expect( mockHttpClientInstance.getInterceptors().addErrorInterceptor, ).toHaveBeenCalledWith(errorInterceptor) expect(result).toBe(removeInterceptor) }) }) describe("Integration Tests", () => { it("should work with real API calls", async () => { // Mock a successful API response client.api.admin.featureFlags.list = vi.fn().mockResolvedValue({ code: 0, data: [{ id: 1, name: "test_flag", enabled: true }], }) const response = await client.api.admin.featureFlags.list() expect(response).toEqual({ code: 0, data: [{ id: 1, name: "test_flag", enabled: true }], }) }) it("should handle API errors", async () => { const apiError = new Error("API Error") client.api.feeds.get = vi.fn().mockRejectedValue(apiError) await expect(client.api.feeds.get()).rejects.toThrow("API Error") }) it("should work with different HTTP methods", async () => { client.api.subscriptions.create = vi.fn().mockResolvedValue({ code: 0, data: { id: "new-sub", feedId: "feed-1" }, }) const response = await (client.api.subscriptions.create as any)({ feedId: "feed-1", }) expect(response).toEqual({ code: 0, data: { id: "new-sub", feedId: "feed-1" }, }) }) }) describe("Error Handling", () => { it("should handle initialization errors gracefully", () => { // Since the current FollowClient implementation doesn't throw during initialization, // we test that it handles configuration gracefully const clientWithInvalidConfig = new FollowClient({ baseURL: undefined as any, timeout: -1, }) expect(clientWithInvalidConfig).toBeInstanceOf(FollowClient) expect(clientWithInvalidConfig).toBeDefined() }) it("should handle interceptor setup errors", () => { // The current implementation doesn't throw errors during interceptor setup // Test that interceptors can be enabled without throwing const clientWithInterceptors = new FollowClient({ enableDefaultInterceptors: true, }) expect(clientWithInterceptors).toBeInstanceOf(FollowClient) expect(clientWithInterceptors).toBeDefined() }) }) describe("Type Safety", () => { it("should have proper TypeScript types", () => { expect(client).toBeInstanceOf(FollowClient) expect(typeof client.setAuthToken).toBe("function") expect(typeof client.removeAuthToken).toBe("function") expect(typeof client.setHeaders).toBe("function") expect(typeof client.setFetch).toBe("function") expect(typeof client.updateConfig).toBe("function") expect(typeof client.getConfig).toBe("function") expect(typeof client.batch).toBe("function") expect(typeof client.clone).toBe("function") expect(typeof client.addRequestInterceptor).toBe("function") expect(typeof client.addResponseInterceptor).toBe("function") expect(typeof client.addErrorInterceptor).toBe("function") }) }) })