import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hepan_everyday/features/feedback/feedback_http_sender.dart'; import 'package:hepan_everyday/features/feedback/feedback_submission.dart'; class FakeFeedbackHttpTransport implements FeedbackHttpTransport { int calls = 0; Uri? endpoint; Map? headers; Uint8List? body; FeedbackHttpResponse response = FeedbackHttpResponse( 200, utf8.encode('{"success":true,"submission_id":"same-id","status":"sent"}'), ); Completer? pending; @override Future post({ required Uri endpoint, required Map headers, required Uint8List body, required Duration timeout, }) async { calls++; this.endpoint = endpoint; this.headers = headers; this.body = body; return pending == null ? response : await pending!.future; } } void main() { test( 'matching uncertain delivery is distinct and never exposes server detail', () async { final transport = FakeFeedbackHttpTransport(); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, ); final submission = FeedbackSubmission.problem( id: 'same-id', message: '问题', ); for (final code in ['delivery_uncertain', 'storage_unavailable']) { transport.response = FeedbackHttpResponse( 503, utf8.encode( jsonEncode({ 'success': false, 'error': code, 'detail': 'private SMTP error', 'submission_id': 'same-id', 'status': 'uncertain', }), ), ); await expectLater( sender.send(submission), throwsA( isA() .having((error) => error.submissionId, 'original ID', 'same-id') .having( (error) => error.message, 'safe instructions', allOf( contains('请勿修改'), isNot(contains('private SMTP error')), ), ), ), ); } transport.response = FeedbackHttpResponse( 503, utf8.encode( jsonEncode({ 'success': false, 'error': 'delivery_uncertain', 'submission_id': 'other-id', 'status': 'uncertain', }), ), ); await expectLater( sender.send(submission), throwsA( allOf( isA(), isNot(isA()), ), ), ); }, ); test( 'permanent media/validation failures give actionable safe messages', () async { final transport = FakeFeedbackHttpTransport(); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, ); final submission = FeedbackSubmission.problem( id: 'same-id', message: '问题', ); for (final rejection in [ (status: 415, code: 'unsupported_media', expected: '移除附件'), (status: 422, code: 'validation_error', expected: '必填内容'), ]) { transport.response = FeedbackHttpResponse( rejection.status, utf8.encode( jsonEncode({ 'success': false, 'error': rejection.code, 'detail': 'untrusted detail', }), ), ); await expectLater( sender.send(submission), throwsA( isA().having( (error) => error.message, 'actionable message', allOf( contains(rejection.expected), isNot(contains('untrusted detail')), ), ), ), ); } transport.response = FeedbackHttpResponse( 503, utf8.encode( jsonEncode({ 'success': false, 'error': 'smtp_unavailable', 'submission_id': 'same-id', 'status': 'retryable', }), ), ); await expectLater( sender.send(submission), throwsA( allOf( isA().having( (error) => error.message, 'safe retry', contains('原提交'), ), isNot(isA()), ), ), ); }, ); test('requires HTTPS before contacting transport', () async { final transport = FakeFeedbackHttpTransport(); final submission = FeedbackSubmission.problem(id: 'same-id', message: '问题'); for (final endpoint in [ '', 'http://example.com/v1/feedback', 'https://user:pass@example.com/v1/feedback', ]) { await expectLater( HttpFeedbackSender( endpoint: endpoint, transport: transport, ).send(submission), throwsA(isA()), ); } expect(transport.calls, 0); }); test( 'problem multipart contains relevant fields and repeated media parts', () async { final transport = FakeFeedbackHttpTransport(); final file = await FeedbackAttachment.read( name: 'x.png', stream: Stream.value([1, 2, 3]), ); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, ); await sender.send( FeedbackSubmission.problem( id: 'same-id', message: ' 问题 ', attachments: [file, file], ), ); final body = utf8.decode(transport.body!, allowMalformed: true); expect(body, contains('name="submission_id"\r\n\r\nsame-id')); expect(body, contains('name="type"\r\n\r\nproblem')); expect(body, contains('name="message"\r\n\r\n问题')); expect(body, isNot(contains('name="school"'))); expect(body, isNot(contains('name="contact"'))); expect('name="attachments"'.allMatches(body), hasLength(2)); expect(body, contains('Content-Type: image/png')); expect( transport.headers!['Content-Type'], startsWith('multipart/form-data; boundary='), ); expect(transport.headers!.keys, isNot(contains('Authorization'))); }, ); test('development multipart sends school and contact without problem/media fields', () async { final transport = FakeFeedbackHttpTransport(); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, ); await sender.send( FeedbackSubmission.development( id: 'same-id', school: '某大学', contact: 'QQ 123', ), ); final body = utf8.decode(transport.body!); expect(body, contains('name="type"\r\n\r\ndevelopment')); expect(body, contains('name="school"\r\n\r\n某大学')); expect(body, contains('name="contact"\r\n\r\nQQ 123')); expect(body, isNot(contains('name="message"'))); expect(body, isNot(contains('name="attachments"'))); }); test('only matching confirmed sent response is successful', () async { final transport = FakeFeedbackHttpTransport(); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, ); final submission = FeedbackSubmission.problem(id: 'same-id', message: '问题'); for (final response in [ FeedbackHttpResponse(200, utf8.encode('{"success":true}')), FeedbackHttpResponse( 200, utf8.encode('{"success":true,"submission_id":"other","status":"sent"}'), ), FeedbackHttpResponse( 200, utf8.encode( '{"success":true,"submission_id":"same-id","status":"queued"}', ), ), FeedbackHttpResponse( 200, utf8.encode( '{"success":false,"submission_id":"same-id","status":"sent"}', ), ), FeedbackHttpResponse( 500, utf8.encode( '{"success":true,"submission_id":"same-id","status":"sent"}', ), ), FeedbackHttpResponse(200, utf8.encode('bad JSON')), FeedbackHttpResponse(200, Uint8List(65537)), ]) { transport.response = response; await expectLater( sender.send(submission), throwsA(isA()), ); } }); test( 'timeout exposes safe error and repeated request keeps submission ID', () async { final transport = FakeFeedbackHttpTransport() ..pending = Completer(); final sender = HttpFeedbackSender( endpoint: 'https://example.com/v1/feedback', transport: transport, timeout: const Duration(milliseconds: 1), ); final submission = FeedbackSubmission.problem( id: 'same-id', message: '问题', ); await expectLater( sender.send(submission), throwsA(isA()), ); transport.pending = null; await sender.send(submission); expect(transport.calls, 2); expect( utf8.decode(transport.body!), contains('name="submission_id"\r\n\r\nsame-id'), ); }, ); }