Skip to content

Commit c5e918b

Browse files
committed
test: add comprehensive test coverage for toRequestyServiceUrl
- Add dedicated test suite for toRequestyServiceUrl function - Test default behavior, custom URLs, service types, and edge cases - Improve robustness of URL handling with proper validation - Add JSDoc documentation for clarity - Address PR feedback about robustness and test coverage
1 parent a47fcd6 commit c5e918b

File tree

2 files changed

+176
-7
lines changed

2 files changed

+176
-7
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { toRequestyServiceUrl } from "../requesty"
3+
4+
describe("toRequestyServiceUrl", () => {
5+
beforeEach(() => {
6+
vi.clearAllMocks()
7+
// Mock console.warn to avoid noise in test output
8+
vi.spyOn(console, "warn").mockImplementation(() => {})
9+
})
10+
11+
describe("with default parameters", () => {
12+
it("should return default router URL when no baseUrl provided", () => {
13+
const result = toRequestyServiceUrl()
14+
expect(result).toBe("https://router.requesty.ai/v1")
15+
})
16+
17+
it("should return default router URL when baseUrl is undefined", () => {
18+
const result = toRequestyServiceUrl(undefined)
19+
expect(result).toBe("https://router.requesty.ai/v1")
20+
})
21+
22+
it("should return default router URL when baseUrl is empty string", () => {
23+
const result = toRequestyServiceUrl("")
24+
expect(result).toBe("https://router.requesty.ai/v1")
25+
})
26+
})
27+
28+
describe("with custom baseUrl", () => {
29+
it("should use custom baseUrl for router service", () => {
30+
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1")
31+
expect(result).toBe("https://custom.requesty.ai/v1")
32+
})
33+
34+
it("should handle baseUrl with trailing slash", () => {
35+
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1/")
36+
expect(result).toBe("https://custom.requesty.ai/v1/")
37+
})
38+
39+
it("should handle baseUrl without path", () => {
40+
const result = toRequestyServiceUrl("https://custom.requesty.ai")
41+
expect(result).toBe("https://custom.requesty.ai/")
42+
})
43+
44+
it("should handle localhost URLs", () => {
45+
const result = toRequestyServiceUrl("http://localhost:8080/v1")
46+
expect(result).toBe("http://localhost:8080/v1")
47+
})
48+
49+
it("should handle URLs with ports", () => {
50+
const result = toRequestyServiceUrl("https://custom.requesty.ai:3000/v1")
51+
expect(result).toBe("https://custom.requesty.ai:3000/v1")
52+
})
53+
})
54+
55+
describe("with different service types", () => {
56+
it("should return router URL for router service", () => {
57+
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "router")
58+
expect(result).toBe("https://router.requesty.ai/v1")
59+
})
60+
61+
it("should replace router with app and remove v1 for app service", () => {
62+
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "app")
63+
expect(result).toBe("https://app.requesty.ai/")
64+
})
65+
66+
it("should replace router with api and remove v1 for api service", () => {
67+
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "api")
68+
expect(result).toBe("https://api.requesty.ai/")
69+
})
70+
71+
it("should handle custom baseUrl with app service", () => {
72+
const result = toRequestyServiceUrl("https://router.custom.ai/v1", "app")
73+
expect(result).toBe("https://app.custom.ai/")
74+
})
75+
76+
it("should handle URLs where router appears multiple times", () => {
77+
const result = toRequestyServiceUrl("https://router.router-requesty.ai/v1", "app")
78+
// This will replace the first occurrence only
79+
expect(result).toBe("https://app.router-requesty.ai/")
80+
})
81+
})
82+
83+
describe("error handling", () => {
84+
it("should fall back to default URL for invalid baseUrl", () => {
85+
const result = toRequestyServiceUrl("not-a-valid-url")
86+
expect(result).toBe("https://router.requesty.ai/v1")
87+
expect(console.warn).toHaveBeenCalledWith('Invalid base URL "not-a-valid-url", falling back to default')
88+
})
89+
90+
it("should fall back to default URL for malformed URL", () => {
91+
const result = toRequestyServiceUrl("ht!tp://[invalid")
92+
expect(result).toBe("https://router.requesty.ai/v1")
93+
expect(console.warn).toHaveBeenCalled()
94+
})
95+
96+
it("should fall back to default app URL for invalid baseUrl with app service", () => {
97+
const result = toRequestyServiceUrl("invalid-url", "app")
98+
expect(result).toBe("https://app.requesty.ai/")
99+
expect(console.warn).toHaveBeenCalled()
100+
})
101+
102+
it("should handle null baseUrl gracefully", () => {
103+
const result = toRequestyServiceUrl(null as any)
104+
expect(result).toBe("https://router.requesty.ai/v1")
105+
})
106+
107+
it("should handle non-string baseUrl gracefully", () => {
108+
const result = toRequestyServiceUrl(123 as any)
109+
expect(result).toBe("https://router.requesty.ai/v1")
110+
})
111+
})
112+
113+
describe("edge cases", () => {
114+
it("should handle protocol-relative URLs by falling back to default", () => {
115+
const result = toRequestyServiceUrl("//custom.requesty.ai/v1")
116+
// Protocol-relative URLs are not valid for URL constructor, will fall back
117+
expect(result).toBe("https://router.requesty.ai/v1")
118+
expect(console.warn).toHaveBeenCalled()
119+
})
120+
121+
it("should preserve query parameters", () => {
122+
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1?key=value")
123+
expect(result).toBe("https://custom.requesty.ai/v1?key=value")
124+
})
125+
126+
it("should preserve URL fragments", () => {
127+
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1#section")
128+
expect(result).toBe("https://custom.requesty.ai/v1#section")
129+
})
130+
131+
it("should handle URLs with authentication", () => {
132+
const result = toRequestyServiceUrl("https://user:[email protected]/v1")
133+
expect(result).toBe("https://user:[email protected]/v1")
134+
})
135+
})
136+
})

src/shared/utils/requesty.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,55 @@ const REQUESTY_BASE_URL = "https://router.requesty.ai/v1"
22

33
type URLType = "router" | "app" | "api"
44

5+
/**
6+
* Replaces the service type in the URL (router -> app/api) and removes version suffix for non-router services
7+
* @param baseUrl The base URL to transform
8+
* @param type The service type to use
9+
* @returns The transformed URL
10+
*/
511
const replaceCname = (baseUrl: string, type: URLType): string => {
612
if (type === "router") {
713
return baseUrl
8-
} else {
9-
return baseUrl.replace("router", type).replace("v1", "")
14+
}
15+
16+
// Parse the URL to safely replace the subdomain
17+
try {
18+
const url = new URL(baseUrl)
19+
// Replace 'router' in the hostname with the service type
20+
if (url.hostname.includes("router")) {
21+
url.hostname = url.hostname.replace("router", type)
22+
}
23+
// Remove '/v1' from the pathname for non-router services
24+
if (url.pathname.endsWith("/v1")) {
25+
url.pathname = url.pathname.slice(0, -3)
26+
}
27+
return url.toString()
28+
} catch {
29+
// Fallback to simple string replacement if URL parsing fails
30+
return baseUrl.replace("router", type).replace("/v1", "")
1031
}
1132
}
1233

13-
export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "router"): string => {
14-
let url = replaceCname(baseUrl ?? REQUESTY_BASE_URL, service)
34+
/**
35+
* Converts a base URL to a Requesty service URL with proper validation and fallback
36+
* @param baseUrl Optional custom base URL. Falls back to default if invalid or not provided
37+
* @param service The service type (router, app, or api). Defaults to 'router'
38+
* @returns A valid Requesty service URL
39+
*/
40+
export const toRequestyServiceUrl = (baseUrl?: string | null, service: URLType = "router"): string => {
41+
// Handle null, undefined, empty string, or non-string values
42+
const urlToUse = baseUrl && typeof baseUrl === "string" && baseUrl.trim() ? baseUrl.trim() : REQUESTY_BASE_URL
1543

1644
try {
17-
return new URL(url).toString()
45+
// Validate the URL first
46+
const validatedUrl = new URL(urlToUse).toString()
47+
// Apply service type transformation
48+
return replaceCname(validatedUrl, service)
1849
} catch (error) {
1950
// If the provided baseUrl is invalid, fall back to the default
20-
console.warn(`Invalid base URL "${baseUrl}", falling back to default`)
21-
return new URL(replaceCname(REQUESTY_BASE_URL, service)).toString()
51+
if (baseUrl && baseUrl !== REQUESTY_BASE_URL) {
52+
console.warn(`Invalid base URL "${baseUrl}", falling back to default`)
53+
}
54+
return replaceCname(REQUESTY_BASE_URL, service)
2255
}
2356
}

0 commit comments

Comments
 (0)