UNPKG

@hyper-fetch/core

Version:

Cache, Queue and Persist your requests no matter if you are online or offline!

464 lines (387 loc) 15.6 kB
import type { Client } from "client"; import { createClient } from "client"; import type { HttpAdapterType } from "http-adapter"; import type { RequestInstance, RequestModel } from "request"; import { Request } from "request"; import { createSdk, createConfiguration } from "sdk"; type TestClient = Client<Error, HttpAdapterType>; // Schema does not need to declare `client` per request - the SDK injects it from createSdk(client). type TestSchema = { users: { $get: RequestModel<{ endpoint: "/users"; }>; $userId: { $get: RequestModel<{ response: { id: string; name: string }; endpoint: "/users/:userId"; }>; }; }; posts: { $get: RequestModel<{ endpoint: "/posts"; }>; }; }; describe("SDK [ Configuration ]", () => { let client: TestClient; beforeEach(() => { client = createClient<{ error: Error }>({ url: "http://localhost:3000" }); }); describe("createConfiguration", () => { it("should create a configuration map", () => { const config = createConfiguration<TestSchema>()({ "*": { retry: 3, retryTime: 1000 }, "/users/*": { cache: false }, }); expect(config).toStrictEqual({ "*": { retry: 3, retryTime: 1000 }, "/users/*": { cache: false }, }); }); }); describe("$configure", () => { it("should apply global defaults to all requests", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 5, retryTime: 2000 } }); const request = configured.users.$get; expect(request).toBeInstanceOf(Request); expect(request.retry).toBe(5); expect(request.retryTime).toBe(2000); }); it("should apply endpoint-specific defaults", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "/users": { retry: 3, cache: false }, }); const usersRequest = configured.users.$get; expect(usersRequest.retry).toBe(3); expect(usersRequest.cache).toBe(false); // Posts should not be affected const postsRequest = configured.posts.$get; expect(postsRequest.retry).toBe(0); expect(postsRequest.cache).toBe(true); }); it("should apply wildcard defaults to matching endpoints", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "/users/*": { cancelable: true }, }); const userRequest = configured.users.$userId.$get; expect(userRequest.cancelable).toBe(true); const postsRequest = configured.posts.$get; expect(postsRequest.cancelable).toBe(false); }); it("should merge global and specific defaults (specific wins)", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 1 }, "/users": { retry: 5 }, }); const usersRequest = configured.users.$get; // "/users" matches both "*" and "/users"; both apply in order, so "/users" overwrites expect(usersRequest.retry).toBe(5); const postsRequest = configured.posts.$get; expect(postsRequest.retry).toBe(1); }); it("should return a new SDK instance (immutable)", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 10 } }); expect(sdk.users.$get.retry).toBe(0); expect(configured.users.$get.retry).toBe(10); }); it("should apply headers defaults", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { headers: { "X-Custom": "value" } }, }); const request = configured.users.$get; expect(request.headers).toStrictEqual({ "X-Custom": "value" }); }); }); describe("defaults via createSdk options", () => { it("should apply defaults passed through createSdk options", () => { const sdk = createSdk<TestClient, TestSchema>(client, { defaults: { "*": { retry: 7 }, }, }); const request = sdk.users.$get; expect(request.retry).toBe(7); }); }); describe("getMethod with custom methodTransform", () => { it("should use custom methodTransform when provided", () => { const sdk = createSdk<TestClient, TestSchema>(client, { methodTransform: (method: string) => `CUSTOM_${method}`, }); const request = sdk.users.$get; expect(request.method).toBe("CUSTOM_get"); }); it("should use default toUpperCase when no methodTransform provided", () => { const sdk = createSdk<TestClient, TestSchema>(client); const request = sdk.users.$get; expect(request.method).toBe("GET"); }); }); describe("applyDefaults with all config properties", () => { it("should apply all config properties to the request", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { headers: { "X-Test": "value" }, auth: false, cache: false, cacheTime: 5000, staleTime: 3000, retry: 3, retryTime: 1000, cancelable: true, queued: true, offline: true, deduplicate: true, deduplicateTime: 2000, }, }); const request = configured.users.$get; expect(request.headers).toStrictEqual({ "X-Test": "value" }); expect(request.auth).toBe(false); expect(request.cache).toBe(false); expect(request.cacheTime).toBe(5000); expect(request.staleTime).toBe(3000); expect(request.retry).toBe(3); expect(request.retryTime).toBe(1000); expect(request.cancelable).toBe(true); expect(request.queued).toBe(true); expect(request.offline).toBe(true); expect(request.deduplicate).toBe(true); expect(request.deduplicateTime).toBe(2000); }); it("should not apply deduplicateTime when value is null", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { deduplicateTime: null, retry: 1, }, }); const request = configured.users.$get; // null means "don't change" - deduplicateTime should not be set by null // The branch `config.deduplicateTime !== null` prevents null from being applied expect(request.retry).toBe(1); }); }); describe("endpointMatchesPattern with wildcard edge case", () => { it("should match endpoint equal to prefix without trailing slash", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "/users/*": { retry: 9 }, }); const usersRequest = configured.users.$get; expect(usersRequest.retry).toBe(9); }); }); describe("$configure merging", () => { it("should merge existing defaults with new ones from $configure", () => { const sdk = createSdk<TestClient, TestSchema>(client, { defaults: { "*": { retry: 2 }, }, }); const configured = sdk.$configure({ "/users": { cache: false }, }); const usersRequest = configured.users.$get; expect(usersRequest.retry).toBe(2); expect(usersRequest.cache).toBe(false); }); }); describe("proxy special keys", () => { it("should return undefined for symbol keys", () => { const sdk = createSdk<TestClient, TestSchema>(client); const sym = Symbol("test"); expect((sdk as any)[sym]).toBeUndefined(); }); it("should return undefined for inspect key", () => { const sdk = createSdk<TestClient, TestSchema>(client); expect((sdk as any).inspect).toBeUndefined(); }); }); describe("getMethod default fallback in options without methodTransform", () => { it("should use inline default toUpperCase when options exist but have no methodTransform", () => { const sdk = createSdk<TestClient, TestSchema>(client, { camelCaseToKebabCase: false, }); const request = sdk.users.$get; expect(request.method).toBe("GET"); }); }); describe("method-specific dot-path keys", () => { it("should apply config to a specific method via dot-path key", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": { retry: 7 }, }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(7); const postsGet = configured.posts.$get; expect(postsGet.retry).toBe(0); }); it("should apply config to a nested dot-path key", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$userId.$get": { cancelable: true }, }); const userByIdGet = configured.users.$userId.$get; expect(userByIdGet.cancelable).toBe(true); const usersGet = configured.users.$get; expect(usersGet.cancelable).toBe(false); }); it("should not affect other methods on the same endpoint group", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": { retry: 10 }, }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(10); // The nested $userId.$get shares the /users endpoint group but has a different dot-path const userByIdGet = configured.users.$userId.$get; expect(userByIdGet.retry).toBe(0); }); }); describe("function-based configuration values", () => { it("should apply a function value to configure a request", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": (request: RequestInstance) => request.setRetry(5).setCancelable(true), }); const request = configured.users.$get; expect(request.retry).toBe(5); expect(request.cancelable).toBe(true); }); it("should apply a function value on a dot-path key", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": (request: RequestInstance) => request.setRetry(3).setHeaders({ "X-Custom": "test" }), }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(3); expect(usersGet.headers).toStrictEqual({ "X-Custom": "test" }); const postsGet = configured.posts.$get; expect(postsGet.retry).toBe(0); }); it("should allow setMock through function-based configuration", () => { const mockFn = () => ({ data: [{ id: 1, name: "Test" }], error: null, status: 200 }); const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": (request: RequestInstance) => request.setMock(mockFn), }); const usersGet = configured.users.$get; expect(usersGet.isMockerEnabled).toBe(true); }); it("should allow setResponseMapper through function-based configuration", () => { const mapper = (response: any) => ({ ...response, mapped: true }); const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": (request: RequestInstance) => request.setResponseMapper(mapper), }); const usersGet = configured.users.$get; expect(usersGet.unstable_responseMapper).toBe(mapper); }); }); describe("configuration stacking", () => { it("should stack properties from global and method-specific configs", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { cache: true }, "users.$get": { deduplicate: true }, }); const usersGet = configured.users.$get; expect(usersGet.cache).toBe(true); expect(usersGet.deduplicate).toBe(true); }); it("should stack properties across global, endpoint group, and dot-path", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 3 }, "/users": { cache: false }, "users.$get": { deduplicate: true }, }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(3); expect(usersGet.cache).toBe(false); expect(usersGet.deduplicate).toBe(true); }); it("should stack function-based values across levels", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": (request: RequestInstance) => request.setRetry(5), "users.$get": (request: RequestInstance) => request.setCancelable(true), }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(5); expect(usersGet.cancelable).toBe(true); }); it("should let a later level override a specific property while keeping others", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 1, cache: true, cancelable: false }, "users.$get": { retry: 10 }, }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(10); expect(usersGet.cache).toBe(true); expect(usersGet.cancelable).toBe(false); }); }); describe("mixed plain-object and function values", () => { it("should apply both plain objects and functions in the same configuration", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 2 }, "users.$get": (request: RequestInstance) => request.setCancelable(true), }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(2); expect(usersGet.cancelable).toBe(true); const postsGet = configured.posts.$get; expect(postsGet.retry).toBe(2); expect(postsGet.cancelable).toBe(false); }); }); describe("application order", () => { it("should apply in order: global -> endpoint group -> method-specific", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "users.$get": { retry: 99 }, "*": { retry: 1 }, "/users": { retry: 10 }, }); // Method-specific (dot-path) should win since it's applied last const usersGet = configured.users.$get; expect(usersGet.retry).toBe(99); }); it("should apply endpoint group after global", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "/users": { retry: 10 }, "*": { retry: 1 }, }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(10); const postsGet = configured.posts.$get; expect(postsGet.retry).toBe(1); }); it("should apply function after plain-object in same category", () => { const sdk = createSdk<TestClient, TestSchema>(client); const configured = sdk.$configure({ "*": { retry: 1, cancelable: false }, "users.$get": (request: RequestInstance) => request.setRetry(50), }); const usersGet = configured.users.$get; expect(usersGet.retry).toBe(50); expect(usersGet.cancelable).toBe(false); }); }); });