fix(geo): stop calling ip-api.com from the browser (FH-1.1)

detectLocation() fetched http://ip-api.com over plaintext from an HTTPS
storefront. Browsers block mixed active content, so the request never
completed and auto-detect only ever took its error branch - region
detection has been dead in production, not merely insecure. The attempt
also handed every visitor's IP to a third party from the page itself.

Geo now resolves through the tenant API at {baseUrl}/geo/resolve, the
same base /regions already uses. The server reads the client IP; the
browser sends nothing and receives no third-party payload.

The endpoint is specified in BACKEND-API-REFERENCE.md and is not built
yet. Until it ships the client falls back to the manual region picker -
identical to the behaviour production already had.

Adds location.service.spec.ts: geo goes to the tenant API, no request
leaves that origin or uses http://, failure degrades to the manual
picker, and detection is not retried once attempted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-21 10:30:27 +04:00
parent bc5f7c7a64
commit 04272ae673
4 changed files with 112 additions and 4 deletions

View File

@@ -0,0 +1,89 @@
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { ApiConfigService } from '../core/config/api-config.service';
import { LocalStorageService } from '../core/storage/local-storage.service';
import { LocationService } from './location.service';
/**
* FH-1.1. detectLocation() used to call http://ip-api.com directly. On an
* HTTPS storefront the browser blocks mixed active content, so the request
* never completed and region auto-detect silently did nothing in production
* - and the attempt still exposed the visitor's IP to a third party.
*
* These tests pin both halves of the fix: geo resolution goes to our own
* tenant API, and nothing in this service reaches a foreign origin.
*/
describe('LocationService', () => {
const baseUrl = 'https://api.gorbushka.market';
let service: LocationService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
LocationService,
provideHttpClient(),
provideHttpClientTesting(),
{ provide: ApiConfigService, useValue: { getBaseUrl: () => baseUrl } },
{
provide: LocalStorageService,
useValue: { getJSON: () => null, setJSON: () => {}, removeItem: () => {} },
},
],
});
service = TestBed.inject(LocationService);
httpTesting = TestBed.inject(HttpTestingController);
// The constructor loads regions; flush it so each test starts clean.
httpTesting.expectOne(`${baseUrl}/regions`).flush([]);
});
afterEach(() => httpTesting.verify());
it('resolves geo through the tenant API, not a third-party host', () => {
service.detectLocation();
const request = httpTesting.expectOne(`${baseUrl}/geo/resolve`);
expect(request.request.method).toBe('GET');
request.flush({ city: 'Москва', country: 'Россия', countryCode: 'RU' });
});
it('issues no request to a foreign or plaintext origin', () => {
service.detectLocation();
for (const request of httpTesting.match(() => true)) {
expect(request.request.url.startsWith(baseUrl))
.withContext(`unexpected off-origin request: ${request.request.url}`)
.toBe(true);
expect(request.request.url.startsWith('http://'))
.withContext(`plaintext request: ${request.request.url}`)
.toBe(false);
request.flush({});
}
});
it('degrades to the manual picker when geo resolution fails', () => {
service.detectLocation();
httpTesting
.expectOne(`${baseUrl}/geo/resolve`)
.flush(null, { status: 503, statusText: 'Service Unavailable' });
expect(service.region()).toBeNull();
expect(service.detecting()).toBe(false);
expect(service.autoDetected()).toBe(true);
});
it('does not re-request geo once detection has been attempted', () => {
service.detectLocation();
httpTesting.expectOne(`${baseUrl}/geo/resolve`).flush({
city: 'Ереван',
country: 'Армения',
countryCode: 'AM',
});
service.detectLocation();
httpTesting.expectNone(`${baseUrl}/geo/resolve`);
});
});