import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:qiangbao_accounting/api/geo_api.dart'; import 'package:qiangbao_accounting/core/network/api_exception.dart'; /// 用一个假的 HTTP 适配器接管请求,按 URL 分发不同的响应。 /// 这样不用真的联网,也能把「百度失败 → Nominatim 兜底」整条链路跑通。 class _FakeAdapter implements HttpClientAdapter { _FakeAdapter(this.handler); final Future Function(RequestOptions options) handler; /// 记录每次请求,用来断言请求头和参数 final List requests = []; @override Future fetch( RequestOptions options, Stream? requestStream, Future? cancelFuture, ) { requests.add(options); return handler(options); } @override void close({bool force = false}) {} } ResponseBody _json(String body) => ResponseBody.fromString( body, 200, headers: { Headers.contentTypeHeader: ['application/json'], }, ); bool _isBaidu(RequestOptions o) => o.uri.host.contains('baidu'); bool _isNominatim(RequestOptions o) => o.uri.host.contains('nominatim'); void main() { GeoApi apiWith(_FakeAdapter adapter) => GeoApi(dio: Dio()..httpClientAdapter = adapter); group('地址解析:百度优先、Nominatim 兜底', () { test('百度正常时用百度的结果(国内数据最准)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) { return _json( '{"status":0,"result":{"formatted_address":"云南省昆明市五华区护国街道南屏街112"}}', ); } fail('百度成功了就不该再去打 Nominatim'); }); final address = await apiWith(adapter) .reverseAddress(latitude: 25.041234, longitude: 102.712345); expect(address, '云南省昆明市五华区护国街道南屏街112'); expect(adapter.requests.length, 1); }); test('★ 百度配额用完时自动降级到 Nominatim(用户实际撞到的情况)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) { return _json('{"status":4,"msg":"AK 配额校验失败"}'); } return _json( '{"display_name":"庆云街, 护国街道, 五华区, 昆明市, 云南省, 650011, 中国",' '"address":{"road":"庆云街","suburb":"护国街道","district":"五华区",' '"city":"昆明市","state":"云南省","postcode":"650011","country":"中国"}}', ); }); final address = await apiWith(adapter) .reverseAddress(latitude: 25.041234, longitude: 102.712345); // 拼成中文习惯的「大 → 小」,而不是 Nominatim 原生的「小 → 大」 expect(address, '云南省昆明市五华区护国街道庆云街'); expect(adapter.requests.length, 2); }); test('百度网络异常也会降级(不只是配额问题)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) { throw DioException(requestOptions: o, message: 'connection timeout'); } return _json('{"address":{"state":"云南省","city":"昆明市","district":"五华区"}}'); }); final address = await apiWith(adapter) .reverseAddress(latitude: 25.0, longitude: 102.7); expect(address, '云南省昆明市五华区'); }); test('两个都挂时,把百度的原因带出来(而不是一句笼统的失败)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) return _json('{"status":4,"msg":"AK 配额校验失败"}'); throw DioException(requestOptions: o, message: 'timeout'); }); expect( () => apiWith(adapter).reverseAddress(latitude: 25.0, longitude: 102.7), throwsA( isA().having( (e) => e.message, 'message', contains('配额'), ), ), ); }); test('Nominatim 只给 display_name 时也能拼出地址,且滤掉邮编和国家', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) return _json('{"status":1,"msg":"失败"}'); return _json( '{"display_name":"庆云街, 护国街道, 五华区, 昆明市, 云南省, 650011, 中国"}', ); }); final address = await apiWith(adapter) .reverseAddress(latitude: 25.0, longitude: 102.7); expect(address, '云南省昆明市五华区护国街道庆云街'); }); test('Nominatim 请求必须带 User-Agent(它的使用政策要求,不带会被拒)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) return _json('{"status":1,"msg":"失败"}'); return _json('{"address":{"state":"云南省"}}'); }); await apiWith(adapter).reverseAddress(latitude: 25.0, longitude: 102.7); final nominatim = adapter.requests.firstWhere(_isNominatim); expect(nominatim.headers['User-Agent'], isNotNull); expect(nominatim.headers['User-Agent'], contains('Qiangbao')); }); test('百度请求带 coordtype=wgs84ll(geolocator 给的是 WGS84)', () async { final adapter = _FakeAdapter((o) async { if (_isBaidu(o)) { return _json('{"status":0,"result":{"formatted_address":"昆明"}}'); } return _json('{}'); }); await apiWith(adapter).reverseAddress(latitude: 25.0, longitude: 102.7); final baidu = adapter.requests.firstWhere(_isBaidu); expect(baidu.uri.queryParameters['coordtype'], 'wgs84ll'); expect(baidu.uri.queryParameters['location'], '25.0,102.7'); }); }); }