UNPKG

@follow-app/client-sdk

Version:

TypeScript client SDK for Follow RSS Server API

396 lines (333 loc) 11.4 kB
/* eslint-disable @typescript-eslint/no-empty-object-type */ import { describe, expect, it } from "vitest" import type { InferParams, ModuleAPI, RouteFunction } from "./define-module" import { defineModule, defineRoute } from "./define-module" describe("define-module", () => { describe("defineRoute", () => { it("should define a simple GET route", () => { const route = defineRoute("GET", "/users") expect(route).toEqual({ method: "GET", path: "/users", params: undefined, query: undefined, body: undefined, input: undefined, response: undefined, requestType: undefined, responseType: undefined, }) }) it("should extract params from path", () => { const route = defineRoute("GET", "/users/{userId}/posts/{postId}") expect(route.params).toEqual(["userId", "postId"]) }) it("should define route with query and body options", () => { const route = defineRoute("POST", "/users/{userId}", { query: ["include", "fields"] as const, body: ["name", "email"] as const, requestType: "json", responseType: "json", }) expect(route).toEqual({ method: "POST", path: "/users/{userId}", params: ["userId"], query: ["include", "fields"], body: ["name", "email"], input: undefined, response: undefined, requestType: "json", responseType: "json", }) }) it("should handle path with no params", () => { const route = defineRoute("GET", "/health") expect(route.params).toBeUndefined() }) it("should handle complex path patterns", () => { const route = defineRoute( "GET", "/api/v1/users/{userId}/posts/{postId}/comments", ) expect(route.params).toEqual(["userId", "postId"]) }) }) describe("defineModule", () => { it("should define a simple module", () => { const routes = { getUsers: defineRoute("GET", "/users"), createUser: defineRoute("POST", "/users"), } const module = defineModule({ name: "users", prefix: "/api/v1", routes, }) expect(module.name).toBe("users") expect(module.prefix).toBe("/api/v1") expect(module.routes).toBe(routes) expect(module.api).toEqual({}) }) it("should define module with nested routes", () => { const routes = { users: { list: defineRoute("GET", "/users"), create: defineRoute("POST", "/users"), byId: { get: defineRoute("GET", "/users/{userId}"), update: defineRoute("PUT", "/users/{userId}"), posts: { list: defineRoute("GET", "/users/{userId}/posts"), create: defineRoute("POST", "/users/{userId}/posts"), }, }, }, } const module = defineModule({ name: "api", routes, }) expect(module.routes).toBe(routes) }) }) describe("Type inference", () => { it("should infer params from path string", () => { type UserPath = "/users/{userId}/posts/{postId}" type Params = InferParams<UserPath> const params: Params = { userId: "123", postId: "456", } expect(params.userId).toBe("123") expect(params.postId).toBe("456") }) it("should handle path with no params", () => { type SimplePath = "/users" type Params = InferParams<SimplePath> const params: Params = {} expect(params).toEqual({}) }) }) describe("RouteFunction types", () => { it("should create route function with required args", async () => { interface UserInput { name: string email: string } interface UserResponse { id: string name: string email: string } type CreateUserFn = RouteFunction<UserInput, UserResponse> // This should compile - required args const createUser: CreateUserFn = async (args, options) => { expect(args.name).toBeDefined() expect(args.email).toBeDefined() expect(options?.headers).toBeUndefined() // Optional second param return { id: "123", name: args.name, email: args.email, } } // Test the function call const result = createUser({ name: "John", email: "john@example.com" }) await expect(result).resolves.toEqual({ id: "123", name: "John", email: "john@example.com", }) }) it("should create route function with optional args", async () => { interface EmptyInput {} interface ListResponse { users: string[] } type ListUsersFn = RouteFunction<EmptyInput, ListResponse> // This should compile - optional args const listUsers: ListUsersFn = async (args, options) => { // When called without options, should be undefined // When called with options, should have the values if (options) { expect(options.timeout).toBe(5000) } return { users: ["user1", "user2"], } } // Both calls should work const result1 = listUsers() const result2 = listUsers({}, { timeout: 5000 }) await expect(result1).resolves.toEqual({ users: ["user1", "user2"] }) await expect(result2).resolves.toEqual({ users: ["user1", "user2"] }) }) it("should separate input args from fetch options", async () => { interface LoginInput { username: string password: string } interface LoginResponse { token: string } type LoginFn = RouteFunction<LoginInput, LoginResponse> const login: LoginFn = async (args, options) => { // First parameter: route input expect(args.username).toBe("testuser") expect(args.password).toBe("testpass") // Second parameter: fetch options expect(options?.headers?.["Authorization"]).toBe( "Bearer existing-token", ) expect(options?.timeout).toBe(10000) expect(options?.signal).toBeInstanceOf(AbortSignal) return { token: "new-token" } } // Test with fetch options const controller = new AbortController() const result = login( { username: "testuser", password: "testpass" }, { headers: { Authorization: "Bearer existing-token" }, timeout: 10000, signal: controller.signal, }, ) await expect(result).resolves.toEqual({ token: "new-token" }) }) }) describe("ModuleAPI types", () => { it("should generate correct API types from route definitions", () => { const _routes = { getUser: defineRoute("GET", "/users/{userId}", { input: {} as { userId: string }, response: {} as { id: string, name: string }, }), createUser: defineRoute("POST", "/users", { input: {} as { name: string, email: string }, response: {} as { id: string, name: string, email: string }, }), listUsers: defineRoute("GET", "/users", { input: {} as {}, response: {} as { users: Array<{ id: string, name: string }> }, }), } type API = ModuleAPI<typeof _routes> // This test ensures the types compile correctly const mockApi: API = { getUser: async (args) => { expect(args.userId).toBeDefined() return { id: args.userId, name: "Test User" } }, createUser: async (args) => { expect(args.name).toBeDefined() expect(args.email).toBeDefined() return { id: "123", name: args.name, email: args.email } }, listUsers: async () => { return { users: [{ id: "1", name: "User 1" }] } }, } expect(mockApi.getUser).toBeDefined() expect(mockApi.createUser).toBeDefined() expect(mockApi.listUsers).toBeDefined() }) it("should handle nested route structures", () => { const _routes = { users: { get: defineRoute("GET", "/users/{userId}", { input: {} as { userId: string }, response: {} as { id: string, name: string }, }), posts: { list: defineRoute("GET", "/users/{userId}/posts", { input: {} as { userId: string }, response: {} as { posts: Array<{ id: string, title: string }> }, }), }, }, } type API = ModuleAPI<typeof _routes> const mockApi: API = { users: { get: async (args) => { return { id: args.userId, name: "Test User" } }, posts: { list: async () => { return { posts: [{ id: "1", title: "Post 1" }] } }, }, }, } expect(mockApi.users.get).toBeDefined() expect(mockApi.users.posts.list).toBeDefined() }) }) describe("FetchOptions separation", () => { it("should handle fetch options independently from route input", async () => { interface TestInput { id: string data: { name: string } } interface TestResponse { success: boolean } type TestFn = RouteFunction<TestInput, TestResponse> const testFunction: TestFn = async (args, options) => { // Route input should not contain fetch options expect(args).toEqual({ id: "123", data: { name: "test" }, }) // Fetch options should be separate expect(options).toEqual({ headers: { "Content-Type": "application/json" }, timeout: 5000, }) return { success: true } } const result = testFunction( { id: "123", data: { name: "test" } }, { headers: { "Content-Type": "application/json" }, timeout: 5000 }, ) await expect(result).resolves.toEqual({ success: true }) }) it("should allow omitting fetch options", async () => { interface TestInput { message: string } interface TestResponse { echo: string } type TestFn = RouteFunction<TestInput, TestResponse> const testFunction: TestFn = async (args, options) => { expect(args.message).toBe("hello") expect(options).toBeUndefined() return { echo: args.message } } const result = testFunction({ message: "hello" }) await expect(result).resolves.toEqual({ echo: "hello" }) }) }) describe("Legacy compatibility", () => { it("should maintain LegacyRouteArgs interface", () => { // This test ensures the legacy interface still exists const legacyArgs = { params: { userId: "123" }, query: { include: "posts" }, body: { name: "John" }, headers: { Authorization: "Bearer token" }, timeout: 5000, signal: new AbortController().signal, } expect(legacyArgs.params?.userId).toBe("123") expect(legacyArgs.query?.include).toBe("posts") expect(legacyArgs.body).toEqual({ name: "John" }) expect(legacyArgs.headers?.Authorization).toBe("Bearer token") expect(legacyArgs.timeout).toBe(5000) expect(legacyArgs.signal).toBeInstanceOf(AbortSignal) }) }) })