refactor: remove proxy API (#1596)

This commit is contained in:
Levi authored and GitHub committed 2026-10-21 22:48:17 +08:00
1 parent 47cafb7575
commit 4214765d39
98 files changed
+5958 -1225

No files matched your search

+105 -40
View File
@@ -8,71 +8,136 @@ import 'package:gopeed/api/gopeed_site_api.dart';
import 'package:gopeed/core/network/gopeed/gopeed_transport.dart';
void main() {
test('summary requests are shared and cached; explicit refresh requests fresh data', () async {
final pending = Completer<Response<String>>();
final transport = _Transport()..onProxy = (_) => pending.future;
late Dio client;
late _SiteAdapter adapter;
late GopeedSiteApi site;
late _Transport transport;
setUp(() {
client = Dio();
adapter = _SiteAdapter();
client.httpClientAdapter = adapter;
site = GopeedSiteApi(client: client);
transport = _Transport();
api.setTransportForTesting(transport);
final site = GopeedSiteApi.instance;
});
tearDown(() {
client.close(force: true);
expect(transport.calls, 0);
for (final request in adapter.requests) {
expect(request.uri.origin, 'https://gopeed.com');
expect(request.headers.keys.map((key) => key.toLowerCase()), isNot(contains('x-api-token')));
}
});
test('install counts are reported directly with JSON and failures remain observable', () async {
await site.reportExtensionInstall('开发者@name');
final request = adapter.requests.single;
expect(request.uri.toString(), 'https://gopeed.com/api/extensions/install');
expect(request.method, 'POST');
expect(request.contentType, Headers.jsonContentType);
expect(request.data, {'id': '开发者@name'});
adapter.statusCode = 503;
await expectLater(site.reportExtensionInstall('test'), throwsA(isA<DioException>()));
});
test('summary requests go directly to the website and remain shared and cached', () async {
final pending = Completer<Map<String, dynamic>>();
final started = Completer<void>();
adapter.onRequest = (_) {
if (!started.isCompleted) started.complete();
return pending.future;
};
final first = site.getExtensions(query: 'cache-test', forceRefresh: true);
final second = site.getExtensions(query: 'cache-test');
expect(transport.urls.length, 1);
expect(Uri.parse(transport.urls.single).queryParameters['view'], 'summary');
pending.complete(
_response({
'data': [
{'id': 'test'},
],
}),
);
await started.future;
expect(adapter.requests.length, 1);
final request = adapter.requests.single;
expect(request.method, 'GET');
expect(request.uri.path, '/api/extensions');
expect(request.uri.queryParameters, {
'view': 'summary',
'page': '1',
'limit': '20',
'sort': 'stars',
'order': 'desc',
'q': 'cache-test',
});
pending.complete({
'data': [
{'id': 'test'},
],
});
expect((await first).data.single.hasDetails, isFalse);
await second;
await site.getExtensions(query: 'cache-test');
expect(transport.urls.length, 1);
expect(adapter.requests.length, 1);
await site.getExtensions(query: 'cache-test', forceRefresh: true);
expect(transport.urls.length, 2);
expect(adapter.requests.length, 2);
});
test('failed lists are not cached', () async {
final transport = _Transport()..onProxy = (_) => Future.error(StateError('offline'));
api.setTransportForTesting(transport);
final site = GopeedSiteApi.instance;
await expectLater(site.getExtensions(query: 'error-test'), throwsStateError);
transport.onProxy = (_) async => _response({
adapter.statusCode = 503;
await expectLater(site.getExtensions(query: 'error-test'), throwsA(isA<DioException>()));
adapter.statusCode = 200;
adapter.onRequest = (_) async => {
'data': [
{'id': 'recovered'},
],
});
};
expect((await site.getExtensions(query: 'error-test')).data.single.id, 'recovered');
expect(transport.urls.length, 2);
expect(adapter.requests.length, 2);
});
test('detail IDs are encoded once and a null README is still loaded detail', () async {
final transport = _Transport()..onProxy = (_) async => _response({'id': '开发者@name', 'readme': null});
api.setTransportForTesting(transport);
final result = await GopeedSiteApi.instance.getExtension('开发者@name', version: '1.2.3');
final uri = Uri.parse(transport.urls.single);
test('detail IDs are encoded once and version remains a query parameter', () async {
adapter.onRequest = (_) async => {'id': '开发者@name', 'readme': null};
final result = await site.getExtension('开发者@name', version: '1.2.3');
final uri = adapter.requests.single.uri;
expect(uri.pathSegments, ['api', 'extensions', '开发者@name']);
expect(uri.queryParameters['version'], '1.2.3');
expect(result.hasDetails, isTrue);
await site.getExtension('literal%2Fname');
expect(adapter.requests.last.uri.pathSegments.last, 'literal%2Fname');
});
}
Response<String> _response(Map<String, dynamic> json) => Response(
requestOptions: RequestOptions(path: '/test'),
data: jsonEncode(json),
statusCode: 200,
);
class _Transport implements GopeedTransport {
final urls = <String>[];
Future<Response<String>> Function(String) onProxy = (_) async => _response({'data': []});
int calls = 0;
@override
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
urls.add(uri);
return onProxy(uri);
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
calls++;
throw StateError('Store requests must not use the Downloader transport');
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
String join(String path) => path;
}
class _SiteAdapter implements HttpClientAdapter {
final requests = <RequestOptions>[];
int statusCode = 200;
Future<Map<String, dynamic>> Function(RequestOptions) onRequest = (_) async => {'data': []};
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<List<int>>? requestStream,
Future<void>? cancelFuture,
) async {
requests.add(options);
return ResponseBody.fromString(
jsonEncode(await onRequest(options)),
statusCode,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
}
@override
void close({bool force = false}) {}
}
@@ -0,0 +1,98 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/api/api.dart' as api;
import 'package:gopeed/api/model/downloader_config.dart';
import 'package:gopeed/core/capabilities/app_capabilities.dart';
import 'package:gopeed/core/capabilities/capability_rpc.dart';
import 'package:gopeed/core/capabilities/gopeed_capability.dart';
import 'package:gopeed/core/network/gopeed/gopeed_transport.dart';
void main() {
test('protocol jobs use the common protocol and job name API', () async {
final transport = _Transport();
api.setTransportForTesting(transport);
await LocalAppCapabilities.instance.capabilities.gopeed.triggerProtocolJob('bt', 'update-trackers');
expect(transport.path, 'api/v1/protocols/bt/jobs/update-trackers/trigger');
expect(transport.method, 'POST');
});
test('mirror settings and tracker settings serialize at their backend-owned locations', () {
final config = DownloaderConfig();
config.githubMirror = GithubMirrorConfig(enabled: false, mirrors: []);
config.protocolConfig.bt.customTrackers = ['udp://example.com:80/announce'];
config.protocolConfig.bt.state.trackerSubscriptionStatuses = {
'https://example.com/list': TrackerSubscriptionStatus(
success: true,
trackerCount: 12,
lastFetchTime: DateTime.utc(2026, 10, 19),
),
};
final json = config.toJson();
expect(json['githubMirror'], {'enabled': false, 'mirrors': []});
expect(json['extra'], isNot(contains('githubMirror')));
expect(json['extra'], isNot(contains('bt')));
expect((json['protocolConfig'] as Map)['bt']['customTrackers'], ['udp://example.com:80/announce']);
final restored = DownloaderConfig.fromJson(
json,
).protocolConfig.bt.state.trackerSubscriptionStatuses['https://example.com/list']!;
expect(restored.success, isTrue);
expect(restored.trackerCount, 12);
expect(restored.lastFetchTime, DateTime.utc(2026, 10, 19));
expect(BtState.fromJson({'lastTrackerUpdateTime': '0001-01-01T00:00:00Z'}).lastTrackerUpdateTime, isNull);
});
test('protocol operations survive the shared capability codec', () {
final codecs = createAppCapabilityCodecs();
final payload = {'protocol': 'bt', 'name': 'update-trackers'};
expect(codecs.decode<Map<String, dynamic>>(codecs.encode(payload)), payload);
expect(codecs.decode<Map<String, dynamic>?>(null), isNull);
expect(GopeedMethods.triggerProtocolJob.name, 'gopeed.protocol.job.trigger');
expect(const RpcUnit(), isA<RpcUnit>());
});
test('mirror candidates use provider dispatch and the shared capability codec', () async {
final transport = _Transport();
api.setTransportForTesting(transport);
const url = 'https://github.com/GopeedLab/gopeed/releases/download/v2.0.0/package.zip';
final result = await LocalAppCapabilities.instance.capabilities.gopeed.getMirrorCandidates('github', url);
expect(transport.path, 'api/v1/mirrors/github/candidates');
expect(transport.method, 'POST');
expect(transport.data, {'url': url});
expect(result, ['https://example.com/package.dmg']);
final codecs = createAppCapabilityCodecs();
expect(codecs.decode<List<String>>(codecs.encode(result)), result);
expect(GopeedMethods.getMirrorCandidates.name, 'gopeed.mirrors.candidates');
final request = {'provider': 'another-resource', 'url': 'https://example.com/resource'};
expect(codecs.decode<Map<String, dynamic>>(codecs.encode(request)), request);
await LocalAppCapabilities.instance.capabilities.gopeed.getMirrorCandidates('another-resource', request['url']!);
expect(transport.path, 'api/v1/mirrors/another-resource/candidates');
expect(transport.data, {'url': request['url']});
});
}
class _Transport implements GopeedTransport {
String? path;
String? method;
dynamic data;
@override
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
this.path = path;
this.method = method;
this.data = data;
return {
'code': 0,
'data': path.endsWith('/trigger')
? null
: {
'urls': ['https://example.com/package.dmg'],
},
};
}
@override
String join(String path) => path;
}
-6
View File
@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:dio/dio.dart' as dio;
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/api/api.dart' as api;
import 'package:gopeed/api/model/create_task.dart';
@@ -77,9 +76,4 @@ class _RecordingTransport implements GopeedTransport {
@override
String join(String path) => path;
@override
Future<dio.Response<String>> proxyRequest(String uri, {dynamic data, dio.Options? options}) {
throw UnimplementedError();
}
}