diff --git a/.gitignore b/.gitignore index 1985397..676e730 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ build/ !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 + +**/coverage/** diff --git a/lib/flutter_client_sse.dart b/lib/flutter_client_sse.dart index 57db0b3..427cb9d 100644 --- a/lib/flutter_client_sse.dart +++ b/lib/flutter_client_sse.dart @@ -2,8 +2,10 @@ library flutter_client_sse; import 'dart:async'; import 'dart:convert'; + import 'package:flutter_client_sse/constants/sse_request_type_enum.dart'; import 'package:http/http.dart' as http; + part 'sse_event_model.dart'; /// A client for subscribing to Server-Sent Events (SSE). @@ -41,6 +43,10 @@ class SSEClient { /// [url] is the URL of the SSE endpoint. /// [header] is a map of request headers. /// [body] is an optional request body for POST requests. + /// [oldStreamController] stream controller, used to retry to persist the + /// stream from the old connection. + /// [client] is an optional http client used for testing purpose + /// or custom client. /// /// Returns a [Stream] of [SSEModel] representing the SSE events. static Stream subscribeToSSE( @@ -48,6 +54,7 @@ class SSEClient { required String url, required Map header, StreamController? oldStreamController, + http.Client? client, Map? body}) { StreamController streamController = StreamController(); if (oldStreamController != null) { @@ -58,7 +65,7 @@ class SSEClient { print("--SUBSCRIBING TO SSE---"); while (true) { try { - _client = http.Client(); + _client = client ?? http.Client(); var request = new http.Request( method == SSERequestType.GET ? "GET" : "POST", Uri.parse(url), diff --git a/pubspec.lock b/pubspec.lock index 8d66057..615f727 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.1" fake_async: dependency: transitive description: @@ -83,62 +83,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - leak_tracker: + js: dependency: transitive description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "0.6.7" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.2.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.9.1" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" sky_engine: dependency: transitive description: flutter @@ -148,26 +140,26 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -188,10 +180,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.5.1" typed_data: dependency: transitive description: @@ -208,14 +200,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index c7fd9bf..8ccc9c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^1.0.4 flutter: diff --git a/test/flutter_client_sse_test.dart b/test/flutter_client_sse_test.dart new file mode 100644 index 0000000..a1dc91e --- /dev/null +++ b/test/flutter_client_sse_test.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_client_sse/constants/sse_request_type_enum.dart'; +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; + +class MockHttp extends Mock implements http.Client {} + +void main() { + group('SSE Client', () { + setUpAll(() { + registerFallbackValue( + http.Request('GET', Uri.parse('http://localhost:3001'))); + }); + + test('GET', () async { + var mockHttp = MockHttp(); + var elements = [ + utf8.encode('id: 1\nevent: message\ndata: Hello\n\n'), + utf8.encode('id: 2\nevent: message\ndata: World\n\n'), + utf8.encode('id: 3\nevent: message\ndata: World\n\n'), + ]; + + when(() => mockHttp.send(any())).thenAnswer((_) async { + return http.StreamedResponse(Stream.fromIterable(elements), 200, + headers: { + HttpHeaders.contentTypeHeader: 'text/event-stream', + HttpHeaders.cacheControlHeader: 'no-cache', + HttpHeaders.connectionHeader: 'keep-alive', + }); + }); + int lines = 0; + SSEClient.subscribeToSSE( + client: mockHttp, + method: SSERequestType.GET, + url: 'http://localhost:3001', + header: { + "Cookie": + 'jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2NDMyMTAyMzEsImV4cCI6MTY0MzgxNTAzMX0.U0aCAM2fKE1OVnGFbgAU_UVBvNwOMMquvPY8QaLD138; Path=/; Expires=Wed, 02 Feb 2022 15:17:11 GMT; HttpOnly; SameSite=Strict', + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + }).listen( + (event) { + lines++; + print('Id: ' + (event.id ?? "")); + print('Event: ' + (event.event ?? "")); + print('Data: ' + (event.data ?? "")); + }, + ); + await Future.delayed(const Duration(seconds: 1)); + expect(lines, 3); + }); + test('POST', () async { + int lines = 0; + + var mockHttp = MockHttp(); + var elements = [ + utf8.encode('id: 1\nevent: message\ndata: Hello\n\n'), + utf8.encode('id: 2\nevent: message\ndata: World\n\n'), + ]; + + when(() => mockHttp.send(any())).thenAnswer((_) async { + return http.StreamedResponse(Stream.fromIterable(elements), 200, + headers: { + HttpHeaders.contentTypeHeader: 'text/event-stream', + HttpHeaders.cacheControlHeader: 'no-cache', + HttpHeaders.connectionHeader: 'keep-alive', + }); + }); + SSEClient.subscribeToSSE( + client: mockHttp, + method: SSERequestType.POST, + url: + 'http://192.168.1.2:3000/api/activity-stream?historySnapshot=FIVE_MINUTE', + header: { + "Cookie": + 'jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2NDMyMTAyMzEsImV4cCI6MTY0MzgxNTAzMX0.U0aCAM2fKE1OVnGFbgAU_UVBvNwOMMquvPY8QaLD138; Path=/; Expires=Wed, 02 Feb 2022 15:17:11 GMT; HttpOnly; SameSite=Strict', + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + }, + body: { + "name": "Hello", + "customerInfo": {"age": 25, "height": 168} + }).listen( + (event) { + lines++; + print('Id: ' + event.id!); + print('Event: ' + event.event!); + print('Data: ' + event.data!); + }, + ); + await Future.delayed(const Duration(seconds: 2)); + expect(lines, 2); + }); + test('Should retry when receive field undefined', () async { + var mockHttp = MockHttp(); + var elements = [ + utf8.encode('anothe: 1\n\n'), + utf8.encode('id: 3\nevent: message\ndata: World\n\n'), + ]; + + when(() => mockHttp.send(any())).thenAnswer((_) async { + return http.StreamedResponse(Stream.fromIterable(elements), 200, + headers: { + HttpHeaders.contentTypeHeader: 'text/event-stream', + HttpHeaders.cacheControlHeader: 'no-cache', + HttpHeaders.connectionHeader: 'keep-alive', + }); + }); + int lines = 0; + SSEClient.subscribeToSSE( + client: mockHttp, + method: SSERequestType.GET, + url: 'http://localhost:3001', + header: { + "Cookie": + 'jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2NDMyMTAyMzEsImV4cCI6MTY0MzgxNTAzMX0.U0aCAM2fKE1OVnGFbgAU_UVBvNwOMMquvPY8QaLD138; Path=/; Expires=Wed, 02 Feb 2022 15:17:11 GMT; HttpOnly; SameSite=Strict', + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + }).listen( + (event) { + lines++; + print('Id: ' + (event.id ?? "")); + print('Event: ' + (event.event ?? "")); + print('Data: ' + (event.data ?? "")); + }, + ); + await Future.delayed(const Duration(seconds: 7)); + expect(lines, 2); + }); + + test('Should retry when throw exception the client', () async { + var mockHttpExpection = MockHttp(); + + when(() => mockHttpExpection.send(any())).thenThrow(Exception()); + int lines = 0; + SSEClient.subscribeToSSE( + client: mockHttpExpection, + method: SSERequestType.GET, + url: 'http://localhost:3001', + header: { + "Cookie": + 'jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJpYXQiOjE2NDMyMTAyMzEsImV4cCI6MTY0MzgxNTAzMX0.U0aCAM2fKE1OVnGFbgAU_UVBvNwOMMquvPY8QaLD138; Path=/; Expires=Wed, 02 Feb 2022 15:17:11 GMT; HttpOnly; SameSite=Strict', + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + }).listen( + (event) { + lines++; + print('Id: ' + (event.id ?? "")); + print('Event: ' + (event.event ?? "")); + print('Data: ' + (event.data ?? "")); + }, + ); + await Future.delayed(const Duration(seconds: 7)); + expect(lines, 0); + }); + }); +} diff --git a/test/sse_event_model_test.dart b/test/sse_event_model_test.dart new file mode 100644 index 0000000..f1bc866 --- /dev/null +++ b/test/sse_event_model_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_test/flutter_test.dart'; + +main() { + test('SSE Event Model Test', () { + final sseModel = SSEModel(data: 'data', id: 'id', event: 'event'); + expect(sseModel.id, 'id'); + expect(sseModel.event, 'event'); + expect(sseModel.data, 'data'); + }); + test('SSE Event Model Test', () { + final sseModel = SSEModel.fromData('id: id\nevent: event\ndata: data'); + expect(sseModel.id, ' id'); + expect(sseModel.event, ' event'); + expect(sseModel.data, ' data'); + }); +}