-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
41 changed files
with
232 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/** | ||
* @vitest-environment happy-dom | ||
*/ | ||
import { describe, expect, it, vi } from 'vitest' | ||
import { getGeo, getGeoFree, setUserGeolocation } from '../geo' | ||
import { fetchWithTimeout, isLocalhostIp } from '../../utils' | ||
|
||
// Mock the utilities used in the geo functions | ||
vi.mock('../../utils', () => ({ | ||
fetchWithTimeout: vi.fn(), | ||
isLocalhostIp: vi.fn(), | ||
})) | ||
|
||
describe('geo functions', () => { | ||
describe('getGeoFree', () => { | ||
it('should fetch free geo data successfully', async () => { | ||
const mockIp = '123.123.123.123' | ||
const mockApiResponse = { | ||
query: mockIp, | ||
country: 'United States', | ||
countryCode: 'US', | ||
city: 'San Francisco', | ||
lat: 37.7749, | ||
lon: -122.4194, | ||
timezone: 'America/Los_Angeles', | ||
regionName: 'California', | ||
isp: 'ISP Name', | ||
org: 'Organization Name', | ||
} | ||
|
||
vi.mocked(fetchWithTimeout).mockResolvedValueOnce({ | ||
json: () => Promise.resolve(mockApiResponse), | ||
} as Response) | ||
|
||
const expectedGeoData = { | ||
ip: mockIp, | ||
countryCode: 'US', | ||
cityName: 'San Francisco', | ||
latitude: 37.7749, | ||
longitude: -122.4194, | ||
timezone: 'America/Los_Angeles', | ||
regionName: 'California', | ||
ipOrganization: 'Organization Name', | ||
} | ||
|
||
const result = await getGeoFree(mockIp) | ||
|
||
expect(result).toEqual(expectedGeoData) | ||
expect(fetchWithTimeout).toHaveBeenCalledWith(`http://ip-api.com/json/${mockIp}`, expect.any(Object)) | ||
}) | ||
}) | ||
|
||
describe('getGeo', () => { | ||
it('should fetch detailed geo data successfully', async () => { | ||
const mockIp = '123.123.123.123' | ||
const mockApiResponse = { | ||
ipAddress: mockIp, | ||
countryCode: 'US', | ||
stateProv: 'California', | ||
city: 'San Francisco', | ||
latitude: 37.7749, | ||
longitude: -122.4194, | ||
timeZone: 'America/Los_Angeles', | ||
isCrawler: false, | ||
isProxy: false, | ||
threatLevel: 'low', | ||
org: 'ISP Name', | ||
} | ||
|
||
vi.mocked(fetchWithTimeout).mockResolvedValueOnce({ | ||
json: () => Promise.resolve(mockApiResponse), | ||
} as Response) | ||
|
||
const expectedGeoData = { | ||
ip: mockIp, | ||
countryCode: 'US', | ||
cityName: 'San Francisco', | ||
latitude: 37.7749, | ||
longitude: -122.4194, | ||
timezone: 'America/Los_Angeles', | ||
regionName: 'California', | ||
ipIsCrawler: false, | ||
ipIsProxy: false, | ||
ipThreatLevel: 'low', | ||
ipOrganization: 'ISP Name', | ||
} | ||
|
||
const result = await getGeo(mockIp) | ||
|
||
expect(result).toEqual(expectedGeoData) | ||
expect(fetchWithTimeout).toHaveBeenCalled() | ||
}) | ||
|
||
|
||
}) | ||
|
||
describe('setUserGeolocation', () => { | ||
it('should fetch user geolocation data successfully', async () => { | ||
const mockApiResponse = { | ||
ip: '123.123.123.123', | ||
country_code: 'US', | ||
country_name: 'United States', | ||
region_code: 'CA', | ||
region_name: 'California', | ||
city: 'San Francisco', | ||
zip_code: '94016', | ||
time_zone: 'America/Los_Angeles', | ||
latitude: 37.7749, | ||
longitude: -122.4194, | ||
} | ||
|
||
globalThis.fetch = vi.fn().mockResolvedValueOnce({ | ||
json: () => Promise.resolve(mockApiResponse), | ||
}) | ||
|
||
const expectedGeoData = { | ||
ip: '123.123.123.123', | ||
countryCode: 'US', | ||
countryName: 'United States', | ||
regionCode: 'CA', | ||
regionName: 'California', | ||
city: 'San Francisco', | ||
zip: '94016', | ||
timeZone: 'America/Los_Angeles', | ||
latitude: 37.7749, | ||
longitude: -122.4194, | ||
metroCode: undefined, | ||
name: 'San Francisco, United States', | ||
} | ||
|
||
const result = await setUserGeolocation() | ||
|
||
expect(result).toEqual(expectedGeoData) | ||
expect(globalThis.fetch).toHaveBeenCalledWith('https://freegeoip.app/json/') | ||
}) | ||
|
||
|
||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,17 @@ | ||
/** | ||
* Advanced fetch function that adds a timeout and format option to native fetch | ||
*/ | ||
export async function fetchAdvanced<T = unknown>(resource: string, options?: { timeout?: number, format?: 'json' | 'text' }): Promise<T> { | ||
const { timeout = 8000, format = 'json' } = options ?? {} | ||
export async function fetchWithTimeout(url: string, options?: RequestInit & { timeout?: number }): Promise<Response> { | ||
const { timeout = 5000, ...fetchOptions } = options || {} | ||
|
||
const controller = new AbortController() | ||
const id = setTimeout(() => controller.abort(), timeout) | ||
|
||
const response = await window.fetch(resource, { | ||
...options, | ||
signal: controller.signal, | ||
const timeoutPromise = new Promise<never>((_, reject) => { | ||
setTimeout(() => { | ||
reject(new Error(`Request timed out after ${timeout} ms`)) | ||
}, timeout) | ||
}) | ||
|
||
const out = (await response[format]()) as T | ||
|
||
clearTimeout(id) | ||
|
||
return out | ||
return Promise.race([ | ||
fetch(url, fetchOptions), | ||
timeoutPromise, | ||
]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,49 @@ | ||
/** | ||
* @vitest-environment happy-dom | ||
*/ | ||
import { describe, expect, it } from 'vitest' | ||
import { fetchAdvanced } from '@fiction/core/utils/fetch' | ||
|
||
describe('fetch', () => { | ||
it('has window and fetch', () => { | ||
expect(typeof window).toMatchInlineSnapshot('"object"') | ||
expect(typeof window.fetch).toMatchInlineSnapshot('"function"') | ||
expect(typeof fetch).toMatchInlineSnapshot('"function"') | ||
import type { MockInstance } from 'vitest' | ||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' | ||
import { fetchWithTimeout } from '@fiction/core/utils/fetch' | ||
|
||
describe('fetchWithTimeout', () => { | ||
let fetchMock: MockInstance | ||
beforeEach(() => { | ||
fetchMock = vi.spyOn(globalThis, 'fetch') | ||
}) | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks() | ||
}) | ||
|
||
it('should complete the fetch operation successfully before the timeout', async () => { | ||
const mockResponse = new Response(JSON.stringify({ key: 'value' }), { | ||
status: 200, | ||
headers: { 'Content-Type': 'application/json' }, | ||
}) | ||
|
||
fetchMock.mockResolvedValueOnce(mockResponse) | ||
|
||
const response = await fetchWithTimeout('https://jsonplaceholder.typicode.com/posts/1', { timeout: 3000 }) | ||
const data = await response.json() | ||
|
||
expect(data).toEqual({ key: 'value' }) | ||
expect(fetchMock).toHaveBeenCalledTimes(1) | ||
}) | ||
it('fetch advanced with timeout', async () => { | ||
const result = await fetchAdvanced<Record<string, any>>( | ||
'https://jsonplaceholder.typicode.com/todos/1', | ||
) | ||
|
||
expect(result).toMatchInlineSnapshot(` | ||
{ | ||
"completed": false, | ||
"id": 1, | ||
"title": "delectus aut autem", | ||
"userId": 1, | ||
} | ||
`) | ||
|
||
it('should abort the fetch operation after the timeout', async () => { | ||
// Mock a fetch implementation that will not resolve or reject within the test timeout, | ||
// simulating a long-running request that will be aborted. | ||
fetchMock.mockImplementationOnce(() => new Promise(() => {})) | ||
|
||
const fetchPromise = fetchWithTimeout('https://jsonplaceholder.typicode.com/posts/1', { timeout: 1000 }) | ||
|
||
await expect(fetchPromise).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Request timed out after 1000 ms]`) | ||
|
||
expect(fetchMock).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('should handle network or other fetch related errors gracefully', async () => { | ||
const errorMessage = 'Network error' | ||
fetchMock.mockRejectedValueOnce(new Error(errorMessage)) | ||
|
||
await expect(fetchWithTimeout('https://jsonplaceholder.typicode.com/posts/1')).rejects.toThrow(errorMessage) | ||
expect(fetchMock).toHaveBeenCalledTimes(1) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.