Skip to content

Commit ceee0d5

Browse files
committed
feat: geolocation controller fix
1 parent 0382a83 commit ceee0d5

File tree

3 files changed

+75
-19
lines changed

3 files changed

+75
-19
lines changed

packages/geolocation-controller/src/GeolocationController.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const controllerName = 'GeolocationController';
2222
* State for the {@link GeolocationController}.
2323
*/
2424
export type GeolocationControllerState = {
25-
/** ISO 3166-1 alpha-2 country code, or "UNKNOWN" if not yet determined. */
25+
/** ISO 3166-2 location code (e.g. "US", "US-NY", "CA-ON"), or "UNKNOWN" if not yet determined. */
2626
location: string;
2727
/** Current status of the geolocation fetch lifecycle. */
2828
status: GeolocationRequestStatus;
@@ -182,11 +182,11 @@ export class GeolocationController extends BaseController<
182182
}
183183

184184
/**
185-
* Returns the geolocation country code. Delegates to the
185+
* Returns the geolocation code. Delegates to the
186186
* {@link GeolocationApiService} for network fetching and caching, then
187187
* updates controller state with the result.
188188
*
189-
* @returns The ISO country code string.
189+
* @returns The ISO 3166-2 location code string.
190190
*/
191191
async getGeolocation(): Promise<string> {
192192
return this.#fetchAndUpdate();
@@ -195,7 +195,7 @@ export class GeolocationController extends BaseController<
195195
/**
196196
* Forces a fresh geolocation fetch, bypassing the service's cache.
197197
*
198-
* @returns The ISO country code string.
198+
* @returns The ISO 3166-2 location code string.
199199
*/
200200
async refreshGeolocation(): Promise<string> {
201201
this.update((draft) => {
@@ -210,7 +210,7 @@ export class GeolocationController extends BaseController<
210210
*
211211
* @param options - Options forwarded to the service.
212212
* @param options.bypassCache - When true, the service skips its TTL cache.
213-
* @returns The ISO country code string.
213+
* @returns The ISO 3166-2 location code string.
214214
*/
215215
async #fetchAndUpdate(options?: { bypassCache?: boolean }): Promise<string> {
216216
this.update((draft) => {

packages/geolocation-controller/src/geolocation-api-service/geolocation-api-service.test.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,34 @@ describe('GeolocationApiService', () => {
246246
expect(result).toBe('US');
247247
});
248248

249-
it('returns UNKNOWN_LOCATION for non-ISO-3166-1 alpha-2 responses', async () => {
249+
it('accepts a 2-letter country code', async () => {
250+
const mockFetch = createMockFetch('GB');
251+
const { service } = getService({ options: { fetch: mockFetch } });
252+
253+
const result = await service.fetchGeolocation();
254+
255+
expect(result).toBe('GB');
256+
});
257+
258+
it('accepts a country code with subdivision (ISO 3166-2)', async () => {
259+
const mockFetch = createMockFetch('US-NY');
260+
const { service } = getService({ options: { fetch: mockFetch } });
261+
262+
const result = await service.fetchGeolocation();
263+
264+
expect(result).toBe('US-NY');
265+
});
266+
267+
it('accepts a subdivision code with alphanumeric part', async () => {
268+
const mockFetch = createMockFetch('CA-ON');
269+
const { service } = getService({ options: { fetch: mockFetch } });
270+
271+
const result = await service.fetchGeolocation();
272+
273+
expect(result).toBe('CA-ON');
274+
});
275+
276+
it('returns UNKNOWN_LOCATION for non-ISO responses', async () => {
250277
const mockFetch = jest
251278
.fn()
252279
.mockImplementation(() =>
@@ -272,7 +299,20 @@ describe('GeolocationApiService', () => {
272299
expect(result).toBe(UNKNOWN_LOCATION);
273300
});
274301

275-
it('returns UNKNOWN_LOCATION for three-letter codes', async () => {
302+
it('returns UNKNOWN_LOCATION for lowercase subdivision codes', async () => {
303+
const mockFetch = jest
304+
.fn()
305+
.mockImplementation(() =>
306+
Promise.resolve(createMockResponse('us-ny', 200)),
307+
);
308+
const { service } = getService({ options: { fetch: mockFetch } });
309+
310+
const result = await service.fetchGeolocation();
311+
312+
expect(result).toBe(UNKNOWN_LOCATION);
313+
});
314+
315+
it('returns UNKNOWN_LOCATION for three-letter country codes', async () => {
276316
const mockFetch = jest
277317
.fn()
278318
.mockImplementation(() =>
@@ -284,6 +324,19 @@ describe('GeolocationApiService', () => {
284324

285325
expect(result).toBe(UNKNOWN_LOCATION);
286326
});
327+
328+
it('returns UNKNOWN_LOCATION for subdivision codes with too many characters', async () => {
329+
const mockFetch = jest
330+
.fn()
331+
.mockImplementation(() =>
332+
Promise.resolve(createMockResponse('US-ABCD', 200)),
333+
);
334+
const { service } = getService({ options: { fetch: mockFetch } });
335+
336+
const result = await service.fetchGeolocation();
337+
338+
expect(result).toBe(UNKNOWN_LOCATION);
339+
});
287340
});
288341

289342
describe('bypassCache', () => {
@@ -480,19 +533,19 @@ function createMockResponse(body: string, status: number): Response {
480533
}
481534

482535
/**
483-
* Creates a mock fetch function that resolves with the given country code.
536+
* Creates a mock fetch function that resolves with the given location code.
484537
* Each call returns a fresh mock Response.
485538
*
486-
* @param countryCode - The country code to return.
539+
* @param locationCode - The location code to return (e.g. `US`, `US-NY`).
487540
* @returns A jest mock function.
488541
*/
489542
function createMockFetch(
490-
countryCode: string,
543+
locationCode: string,
491544
): jest.Mock<Promise<Response>, [string]> {
492545
return jest
493546
.fn()
494547
.mockImplementation(() =>
495-
Promise.resolve(createMockResponse(countryCode, 200)),
548+
Promise.resolve(createMockResponse(locationCode, 200)),
496549
);
497550
}
498551

packages/geolocation-controller/src/geolocation-api-service/geolocation-api-service.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ export type FetchGeolocationOptions = {
8585
};
8686

8787
/**
88-
* Low-level data service that fetches a country code from the geolocation API.
88+
* Low-level data service that fetches a location code from the geolocation API.
8989
*
9090
* Responsibilities:
9191
* - HTTP request to the geolocation endpoint (wrapped in a service policy)
92-
* - ISO 3166-1 alpha-2 response validation
92+
* - ISO 3166-2 response validation (country code with optional subdivision,
93+
* e.g. `US`, `US-NY`, `CA-ON`)
9394
* - TTL-based in-memory cache
9495
* - Promise deduplication (concurrent callers share a single in-flight request)
9596
*
@@ -202,16 +203,16 @@ export class GeolocationApiService {
202203
}
203204

204205
/**
205-
* Returns the geolocation country code. Serves from cache when the TTL has
206-
* not expired, otherwise performs a network fetch. Concurrent callers are
206+
* Returns the geolocation code. Serves from cache when the TTL has not
207+
* expired, otherwise performs a network fetch. Concurrent callers are
207208
* deduplicated to a single in-flight request.
208209
*
209210
* @param options - Optional fetch options.
210211
* @param options.bypassCache - When true, invalidates the TTL cache. If a
211212
* request is already in-flight it will be reused (deduplication always
212213
* applies).
213-
* @returns The ISO 3166-1 alpha-2 country code, or {@link UNKNOWN_LOCATION}
214-
* when the API returns an empty or invalid body.
214+
* @returns An ISO 3166-2 location code (e.g. `US`, `US-NY`, `CA-ON`), or
215+
* {@link UNKNOWN_LOCATION} when the API returns an empty or invalid body.
215216
*/
216217
async fetchGeolocation(options?: FetchGeolocationOptions): Promise<string> {
217218
if (options?.bypassCache) {
@@ -252,7 +253,7 @@ export class GeolocationApiService {
252253
* Performs the actual HTTP fetch, wrapped in the service policy for automatic
253254
* retry and circuit-breaking, and validates the response.
254255
*
255-
* @returns The ISO country code string.
256+
* @returns The ISO 3166-2 location code string.
256257
*/
257258
async #performFetch(): Promise<string> {
258259
const response = await this.#policy.execute(async () => {
@@ -267,7 +268,9 @@ export class GeolocationApiService {
267268
});
268269

269270
const raw = (await response.text()).trim();
270-
const location = /^[A-Z]{2}$/u.test(raw) ? raw : UNKNOWN_LOCATION;
271+
const location = /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/u.test(raw)
272+
? raw
273+
: UNKNOWN_LOCATION;
271274

272275
if (location !== UNKNOWN_LOCATION) {
273276
this.#cachedLocation = location;

0 commit comments

Comments
 (0)