diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3d061ca..43f954b 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -18,3 +18,24 @@ jobs: with: flutter_channel: stable min_coverage: 0 + + test-clients: + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Dependencies + run: flutter pub get + + - name: Run Integration Tests VM + run: flutter test test/integration/eventflux_test_vm.dart + + - name: Run Integration Tests Browser + run: flutter test test/browser --platform chrome diff --git a/CHANGELOG.md b/CHANGELOG.md index 4237e4a..bc3620e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog 📝 +### v2.3.0-dev.1 🛠️ +This release adds web support 🚀 +- Added `webConfig` parameter to the `connect` method. + - This allows you to configure the web client. + - Refer README for more info. + ### v2.2.2-dev.2 🛠️ - Potential fix for multiple connection issue when using single instance method diff --git a/README.md b/README.md index 7e965e8..0d76690 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ EventFlux is a Dart package designed for efficient handling of server-sent event ## Supported Platforms | Android | iOS | Web | MacOS | Windows | Linux | | ------ | ---- | ---- | ----- | ------- | ----- | -| ✅|✅|🏗️|✅|❓|❓| +| ✅|✅|✅|✅|❓|❓| *Pssst... see those question marks? That's your cue, tech adventurers! Dive in, test, and tell me all about it.* 🚀🛠️ @@ -211,6 +211,7 @@ Connects to a server-sent event stream. | `tag` | `String` | Optional tag for debugging. | - | | `logReceivedData` | `bool` | Whether to log received data. | `false` | | `httpClient` | `HttpClientAdapter?` | Optional Http Client Adapter to allow usage of different http clients. | - | +| `webConfig` | `WebConfig?` | Allows configuring the web client. Ignored for non-web platforms. | - |  
diff --git a/example/pubspec.lock b/example/pubspec.lock index 55b5367..c694bfd 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -47,7 +47,7 @@ packages: path: ".." relative: true source: path - version: "2.2.1" + version: "2.2.2-dev.2" fake_async: dependency: transitive description: @@ -56,6 +56,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fetch_api: + dependency: transitive + description: + name: fetch_api + sha256: "97f46c25b480aad74f7cc2ad7ccba2c5c6f08d008e68f95c1077286ce243d0e6" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + fetch_client: + dependency: transitive + description: + name: fetch_client + sha256: "9666ee14536778474072245ed5cba07db81ae8eb5de3b7bf4a2d1e2c49696092" + url: "https://pub.dev" + source: hosted + version: "1.1.2" flutter: dependency: "direct main" description: flutter diff --git a/lib/client.dart b/lib/client.dart index 62586b7..abe3d54 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -5,13 +5,16 @@ import 'dart:async'; import 'dart:convert'; import 'package:eventflux/enum.dart'; +import 'package:eventflux/extensions/fetch_client_extension.dart'; import 'package:eventflux/http_client_adapter.dart'; import 'package:eventflux/models/base.dart'; import 'package:eventflux/models/data.dart'; import 'package:eventflux/models/exception.dart'; import 'package:eventflux/models/reconnect.dart'; import 'package:eventflux/models/response.dart'; +import 'package:eventflux/models/web_config/web_config.dart'; import 'package:eventflux/utils.dart'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; /// A class for managing event-driven data streams using Server-Sent Events (SSE). @@ -24,7 +27,8 @@ class EventFlux extends EventFluxBase { static final EventFlux _instance = EventFlux._(); static EventFlux get instance => _instance; - Client? _client; + @visibleForTesting + Client? client; StreamController? _streamController; bool _isExplicitDisconnect = false; StreamSubscription? _streamSubscription; @@ -141,7 +145,13 @@ class EventFlux extends EventFluxBase { bool logReceivedData = false, List? files, bool multipartRequest = false, + + /// Optional web config to be used for the connection. Must be provided on web. + /// Will be ignored on non-web platforms. + WebConfig? webConfig, }) { + + assert(!(kIsWeb && webConfig == null), 'WebConfig must be provided on web'); // This check prevents redundant connection requests when a connection is already in progress. // This does not prevent reconnection attempts if autoReconnect is enabled. @@ -191,6 +201,7 @@ class EventFlux extends EventFluxBase { logReceivedData: logReceivedData, files: files, multipartRequest: multipartRequest, + webConfig: webConfig, ); } @@ -209,12 +220,14 @@ class EventFlux extends EventFluxBase { bool logReceivedData = false, List? files, bool multipartRequest = false, + WebConfig? webConfig, }) { /// Initalise variables /// Create a new HTTP client based on the platform /// Uses and internal http client if no http client adapter is present if (httpClient == null) { - _client = Client(); + client = + kIsWeb ? FetchClientExtension.fromWebConfig(webConfig!) : Client(); } /// Set `_isExplicitDisconnect` to `false` before connecting. @@ -272,7 +285,7 @@ class EventFlux extends EventFluxBase { response = httpClient.send(request); } else { // Use internal HTTP client - response = _client!.send(request); + response = client!.send(request); } response.then((data) async { @@ -494,7 +507,7 @@ class EventFlux extends EventFluxBase { try { _streamSubscription?.cancel(); _streamController?.close(); - _client?.close(); + client?.close(); Future.delayed(const Duration(seconds: 1), () {}); eventFluxLog('Disconnected', LogEvent.info, _tag); _status = EventFluxStatus.disconnected; @@ -580,25 +593,30 @@ class EventFlux extends EventFluxBase { break; case ReconnectMode.exponential: - _interval = _interval * 2; - eventFluxLog("Trying again in ${_interval.toString()} seconds", - LogEvent.reconnect, _tag); /// It waits for the specified interval before attempting to reconnect. await Future.delayed(Duration(seconds: _interval), () { - _start( - type, - url, - onSuccessCallback: onSuccessCallback, - autoReconnect: autoReconnect, - onError: onError, - header: header, - onConnectionClose: onConnectionClose, - httpClient: httpClient, - body: body, - files: files, - multipartRequest: multipartRequest, - ); + _interval = _interval * 2; + if (!isExplicitDisconnect) { + eventFluxLog("Trying again in ${_interval.toString()} seconds", + LogEvent.reconnect, _tag); + + _status = EventFluxStatus.connectionInitiated; + _start( + type, + url, + onSuccessCallback: onSuccessCallback, + autoReconnect: autoReconnect, + onError: onError, + header: header, + onConnectionClose: onConnectionClose, + httpClient: httpClient, + body: body, + files: files, + multipartRequest: multipartRequest, + ); + } + }); break; } diff --git a/lib/extensions/fetch_client_extension.dart b/lib/extensions/fetch_client_extension.dart new file mode 100644 index 0000000..fc64e58 --- /dev/null +++ b/lib/extensions/fetch_client_extension.dart @@ -0,0 +1,16 @@ +import 'package:eventflux/models/web_config/web_config.dart'; +import 'package:fetch_client/fetch_client.dart'; + +extension FetchClientExtension on FetchClient { + static FetchClient fromWebConfig(WebConfig webConfig) { + return FetchClient( + mode: webConfig.mode.toRequestMode(), + credentials: webConfig.credentials.toRequestCredentials(), + cache: webConfig.cache.toRequestCache(), + referrer: webConfig.referrer, + referrerPolicy: webConfig.referrerPolicy.toRequestReferrerPolicy(), + redirectPolicy: webConfig.redirectPolicy.toRedirectPolicy(), + streamRequests: webConfig.streamRequests, + ); + } +} diff --git a/lib/models/web_config/redirect_policy.dart b/lib/models/web_config/redirect_policy.dart new file mode 100644 index 0000000..3ea9a32 --- /dev/null +++ b/lib/models/web_config/redirect_policy.dart @@ -0,0 +1,24 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'package:fetch_client/fetch_client.dart'; + +/// How requests should handle redirects. +enum WebConfigRedirectPolicy { + /// Default policy - always follow redirects. + /// If redirect occurs the only way to know about it is via response properties. + alwaysFollow, + + /// Probe via HTTP `GET` request. + probe, + + /// Same as [probe] but using `HEAD` method. + probeHead; + + RedirectPolicy toRedirectPolicy() => switch (this) { + WebConfigRedirectPolicy.alwaysFollow => RedirectPolicy.alwaysFollow, + WebConfigRedirectPolicy.probe => RedirectPolicy.probe, + WebConfigRedirectPolicy.probeHead => RedirectPolicy.probeHead, + }; +} diff --git a/lib/models/web_config/request_cache.dart b/lib/models/web_config/request_cache.dart new file mode 100644 index 0000000..46047b0 --- /dev/null +++ b/lib/models/web_config/request_cache.dart @@ -0,0 +1,68 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'package:eventflux/models/web_config/request_mode.dart'; +import 'package:fetch_client/fetch_client.dart'; + +/// Controls how requests will interact with the browser's HTTP cache. +enum WebConfigRequestCache { + /// The browser looks for a matching request in its HTTP cache. + /// + /// * If there is a match and it is fresh, it will be returned from the cache. + /// * If there is a match but it is stale, the browser will make + /// a conditional request to the remote server. If the server indicates + /// that the resource has not changed, it will be returned from the cache. + /// Otherwise the resource will be downloaded from the server and + /// the cache will be updated. + /// * If there is no match, the browser will make a normal request, + /// and will update the cache with the downloaded resource. + byDefault, + + /// The browser fetches the resource from the remote server + /// without first looking in the cache, and will not update the cache + /// with the downloaded resource. + noStore, + + /// The browser fetches the resource from the remote server + /// without first looking in the cache, but then will update the cache + /// with the downloaded resource. + reload, + + /// The browser looks for a matching request in its HTTP cache. + /// + /// * If there is a match, fresh or stale, the browser will make + /// a conditional request to the remote server. If the server indicates + /// that the resource has not changed, it will be returned from the cache. + /// Otherwise the resource will be downloaded from the server and + /// the cache will be updated. + /// * If there is no match, the browser will make a normal request, + /// and will update the cache with the downloaded resource. + noCache, + + /// The browser looks for a matching request in its HTTP cache. + /// + /// * If there is a match, fresh or stale, it will be returned from the cache. + /// * If there is no match, the browser will make a normal request, + /// and will update the cache with the downloaded resource. + forceCache, + + /// The browser looks for a matching request in its HTTP cache. + /// + /// * If there is a match, fresh or stale, it will be returned from the cache. + /// * If there is no match, the browser will respond + /// with a 504 Gateway timeout status. + /// + /// The [onlyIfCached] mode can only be used if the request's mode + /// is [WebConfigRequestMode.sameOrigin]. + onlyIfCached; + + RequestCache toRequestCache() => switch (this) { + WebConfigRequestCache.byDefault => RequestCache.byDefault, + WebConfigRequestCache.noStore => RequestCache.noStore, + WebConfigRequestCache.reload => RequestCache.reload, + WebConfigRequestCache.noCache => RequestCache.noCache, + WebConfigRequestCache.forceCache => RequestCache.forceCache, + WebConfigRequestCache.onlyIfCached => RequestCache.onlyIfCached, + }; +} diff --git a/lib/models/web_config/request_credentials.dart b/lib/models/web_config/request_credentials.dart new file mode 100644 index 0000000..a9622cd --- /dev/null +++ b/lib/models/web_config/request_credentials.dart @@ -0,0 +1,27 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'package:fetch_client/fetch_client.dart'; + +/// Controls what browsers do with credentials (cookies, HTTP authentication +/// entries, and TLS client certificates). +enum WebConfigRequestCredentials { + /// Tells browsers to include credentials with requests to same-origin URLs, + /// and use any credentials sent back in responses from same-origin URLs. + sameOrigin, + + /// Tells browsers to exclude credentials from the request, and ignore + /// any credentials sent back in the response (e.g., any Set-Cookie header). + omit, + + /// Tells browsers to include credentials in both same- and cross-origin + /// requests, and always use any credentials sent back in responses. + cors; + + RequestCredentials toRequestCredentials() => switch (this) { + WebConfigRequestCredentials.sameOrigin => RequestCredentials.sameOrigin, + WebConfigRequestCredentials.omit => RequestCredentials.omit, + WebConfigRequestCredentials.cors => RequestCredentials.cors, + }; +} diff --git a/lib/models/web_config/request_mode.dart b/lib/models/web_config/request_mode.dart new file mode 100644 index 0000000..945fdba --- /dev/null +++ b/lib/models/web_config/request_mode.dart @@ -0,0 +1,46 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'package:fetch_client/fetch_client.dart'; + +/// The mode used to determine if cross-origin requests lead to valid responses, +/// and which properties of the response are readable. +enum WebConfigRequestMode { + /// If a request is made to another origin with this mode set, + /// the result is an error. You could use this to ensure that + /// a request is always being made to your origin. + sameOrigin, + + /// Prevents the method from being anything other than `HEAD`, `GET` or `POST`, + /// and the headers from being anything other than simple headers. + /// If any `ServiceWorkers` intercept these requests, they may not add + /// or override any headers except for those that are simple headers. + /// In addition, JavaScript may not access any properties of the resulting + /// Response. This ensures that ServiceWorkers do not affect the semantics + /// of the Web and prevents security and privacy issues arising from leaking + /// data across domains. + noCors, + + /// Allows cross-origin requests, for example to access various APIs + /// offered by 3rd party vendors. These are expected to adhere to + /// the CORS protocol. Only a limited set of headers are exposed + /// in the Response, but the body is readable. + cors, + + /// A mode for supporting navigation. The navigate value is intended + /// to be used only by HTML navigation. A navigate request + /// is created only while navigating between documents. + navigate, + + /// A special mode used only when establishing a WebSocket connection. + webSocket; + + RequestMode toRequestMode() => switch (this) { + WebConfigRequestMode.sameOrigin => RequestMode.sameOrigin, + WebConfigRequestMode.noCors => RequestMode.noCors, + WebConfigRequestMode.cors => RequestMode.cors, + WebConfigRequestMode.navigate => RequestMode.navigate, + WebConfigRequestMode.webSocket => RequestMode.webSocket, + }; +} diff --git a/lib/models/web_config/request_referrer_policy.dart b/lib/models/web_config/request_referrer_policy.dart new file mode 100644 index 0000000..8220460 --- /dev/null +++ b/lib/models/web_config/request_referrer_policy.dart @@ -0,0 +1,35 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'package:fetch_client/fetch_client.dart'; + +/// Specifies the referrer policy to use for the request. +enum WebConfigRequestReferrerPolicy { + strictOriginWhenCrossOrigin, + noReferrer, + noReferrerWhenDowngrade, + sameOrigin, + origin, + strictOrigin, + originWhenCrossOrigin, + unsafeUrl; + + RequestReferrerPolicy toRequestReferrerPolicy() => switch (this) { + WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin => + RequestReferrerPolicy.strictOriginWhenCrossOrigin, + WebConfigRequestReferrerPolicy.noReferrer => + RequestReferrerPolicy.noReferrer, + WebConfigRequestReferrerPolicy.noReferrerWhenDowngrade => + RequestReferrerPolicy.noReferrerWhenDowngrade, + WebConfigRequestReferrerPolicy.sameOrigin => + RequestReferrerPolicy.sameOrigin, + WebConfigRequestReferrerPolicy.origin => RequestReferrerPolicy.origin, + WebConfigRequestReferrerPolicy.strictOrigin => + RequestReferrerPolicy.strictOrigin, + WebConfigRequestReferrerPolicy.originWhenCrossOrigin => + RequestReferrerPolicy.originWhenCrossOrigin, + WebConfigRequestReferrerPolicy.unsafeUrl => + RequestReferrerPolicy.unsafeUrl, + }; +} diff --git a/lib/models/web_config/web_config.dart b/lib/models/web_config/web_config.dart new file mode 100644 index 0000000..c66fd28 --- /dev/null +++ b/lib/models/web_config/web_config.dart @@ -0,0 +1,95 @@ +// This code is adapted from fetch_client package +// Copyright (c) 2023-2024 Yaroslav Vorobev and contributors +// Licensed under the MIT License + +import 'redirect_policy.dart'; +import 'request_cache.dart'; +import 'request_credentials.dart'; +import 'request_mode.dart'; +import 'request_referrer_policy.dart'; + +/// Configuration for web requests. +class WebConfig { + /// Create a new web configuration. + WebConfig({ + this.mode = WebConfigRequestMode.noCors, + this.credentials = WebConfigRequestCredentials.sameOrigin, + this.cache = WebConfigRequestCache.byDefault, + this.referrer = '', + this.referrerPolicy = + WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin, + this.redirectPolicy = WebConfigRedirectPolicy.alwaysFollow, + this.streamRequests = false, + }); + + /// The request mode. + final WebConfigRequestMode mode; + + /// The credentials mode, defines what browsers do with credentials. + final WebConfigRequestCredentials credentials; + + /// The cache mode which controls how requests will interact with + /// the browser's HTTP cache. + final WebConfigRequestCache cache; + + /// The referrer. + /// This can be a same-origin URL, `about:client`, or an empty string. + final String referrer; + + /// The referrer policy. + final WebConfigRequestReferrerPolicy referrerPolicy; + + /// The redirect policy, defines how client should handle redirects. + final WebConfigRedirectPolicy redirectPolicy; + + /// Whether to use streaming for requests. + /// + /// **NOTICE**: This feature is supported only in __Chromium 105+__ based browsers + /// and requires server to be HTTP/2 or HTTP/3. + final bool streamRequests; + + /// Creates a copy of this configuration with the given fields replaced with the new values. + WebConfig copyWith({ + WebConfigRequestMode? mode, + WebConfigRequestCredentials? credentials, + WebConfigRequestCache? cache, + String? referrer, + WebConfigRequestReferrerPolicy? referrerPolicy, + WebConfigRedirectPolicy? redirectPolicy, + bool? streamRequests, + }) { + return WebConfig( + mode: mode ?? this.mode, + credentials: credentials ?? this.credentials, + cache: cache ?? this.cache, + referrer: referrer ?? this.referrer, + referrerPolicy: referrerPolicy ?? this.referrerPolicy, + redirectPolicy: redirectPolicy ?? this.redirectPolicy, + streamRequests: streamRequests ?? this.streamRequests, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is WebConfig && + other.mode == mode && + other.credentials == credentials && + other.cache == cache && + other.referrer == referrer && + other.referrerPolicy == referrerPolicy && + other.redirectPolicy == redirectPolicy && + other.streamRequests == streamRequests; + } + + @override + int get hashCode => Object.hash( + mode, + credentials, + cache, + referrer, + referrerPolicy, + redirectPolicy, + streamRequests, + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 77643d1..3f80d0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eventflux description: "Efficient handling of server-sent event streams with easy connectivity and data management." -version: 2.2.2-dev.2 +version: 2.3.0-dev.1 homepage: https://gokula.dev repository: https://github.com/Imgkl/EventFlux issue_tracker: https://github.com/Imgkl/EventFlux/issues @@ -14,6 +14,7 @@ environment: flutter: ">=1.17.0" dependencies: + fetch_client: ^1.1.2 flutter: sdk: flutter http: ^1.2.2 diff --git a/test/browser/client_browser_test.dart b/test/browser/client_browser_test.dart new file mode 100644 index 0000000..b1180dd --- /dev/null +++ b/test/browser/client_browser_test.dart @@ -0,0 +1,119 @@ +@TestOn('browser') +import 'dart:async'; + +import 'package:eventflux/eventflux.dart'; +import 'package:eventflux/models/web_config/redirect_policy.dart'; +import 'package:eventflux/models/web_config/request_cache.dart'; +import 'package:eventflux/models/web_config/request_credentials.dart'; +import 'package:eventflux/models/web_config/request_mode.dart'; +import 'package:eventflux/models/web_config/request_referrer_policy.dart'; +import 'package:eventflux/models/web_config/web_config.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +/// Run this with `flutter test --platform chrome test/browser/eventflux_test_browser.dart` +void main() { + late EventFlux eventFlux; + + setUp(() { + eventFlux = EventFlux.spawn(); + }); + + for (final connectionType in [ + EventFluxConnectionType.get, + EventFluxConnectionType.post, + ]) { + test( + 'uses FetchClient in browser environment with correct config for $connectionType', + () { + final events = []; + final completer = Completer(); + const testUrl = 'https://localhost:4567'; + + fakeAsync((async) { + eventFlux.connect( + connectionType, + testUrl, + webConfig: WebConfig( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + cache: WebConfigRequestCache.noCache, + referrer: 'https://localhost:4567', + referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer, + redirectPolicy: WebConfigRedirectPolicy.probe, + streamRequests: true, + ), + onSuccessCallback: (response) { + response?.stream?.listen( + (event) { + events.add(event.data); + if (events.length >= 3) { + completer.complete(); + } + }, + onError: (error) => completer.completeError(error), + ); + }, + onError: (error) => completer.completeError(error), + ); + }); + + expect( + eventFlux.client, + isA() + .having( + (c) => c.mode, + 'mode', + RequestMode.sameOrigin, + ) + .having( + (c) => c.credentials, + 'credentials', + RequestCredentials.cors, + ) + .having( + (c) => c.cache, + 'cache', + RequestCache.noCache, + ) + .having( + (c) => c.referrer, + 'referrer', + 'https://localhost:4567', + ) + .having( + (c) => c.referrerPolicy, + 'referrerPolicy', + RequestReferrerPolicy.noReferrer, + ) + .having( + (c) => c.redirectPolicy, + 'redirectPolicy', + RedirectPolicy.probe, + ) + .having( + (c) => c.streamRequests, + 'streamRequests', + true, + ), + ); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + } + + test('throws Assertion error if webConfig is not provided', () { + expect( + () => fakeAsync((async) { + eventFlux.connect( + EventFluxConnectionType.get, + 'https://localhost:4567', + onSuccessCallback: (_) {}, + onError: (_) {}, + ); + }), + throwsA(isA()), + ); + }); +} diff --git a/test/browser/fetch_client_extension_test.dart b/test/browser/fetch_client_extension_test.dart new file mode 100644 index 0000000..5b1dded --- /dev/null +++ b/test/browser/fetch_client_extension_test.dart @@ -0,0 +1,115 @@ +@TestOn('browser') +import 'package:eventflux/extensions/fetch_client_extension.dart'; +import 'package:eventflux/models/web_config/redirect_policy.dart'; +import 'package:eventflux/models/web_config/request_cache.dart'; +import 'package:eventflux/models/web_config/request_credentials.dart'; +import 'package:eventflux/models/web_config/request_mode.dart'; +import 'package:eventflux/models/web_config/request_referrer_policy.dart'; +import 'package:eventflux/models/web_config/web_config.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +/// Run this with `flutter test test/browser/fetch_client_extensions_test.dart` +void main() { + group('FetchClientExtension', () { + test('fromWebConfig creates FetchClient with correct parameters', () { + final webConfig = WebConfig( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + cache: WebConfigRequestCache.noCache, + referrer: 'https://test.com', + referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer, + redirectPolicy: WebConfigRedirectPolicy.probe, + streamRequests: true, + ); + + final result = FetchClientExtension.fromWebConfig(webConfig); + + expect( + result, + isA() + .having( + (c) => c.mode, + 'mode', + RequestMode.sameOrigin, + ) + .having( + (c) => c.credentials, + 'credentials', + RequestCredentials.cors, + ) + .having( + (c) => c.cache, + 'cache', + RequestCache.noCache, + ) + .having( + (c) => c.referrer, + 'referrer', + 'https://test.com', + ) + .having( + (c) => c.referrerPolicy, + 'referrerPolicy', + RequestReferrerPolicy.noReferrer, + ) + .having( + (c) => c.redirectPolicy, + 'redirectPolicy', + RedirectPolicy.probe, + ) + .having( + (c) => c.streamRequests, + 'streamRequests', + true, + ), + ); + }); + + test('fromWebConfig creates FetchClient with default WebConfig values', () { + final webConfig = WebConfig(); + + final result = FetchClientExtension.fromWebConfig(webConfig); + + expect( + result, + isA() + .having( + (c) => c.mode, + 'mode', + RequestMode.noCors, + ) + .having( + (c) => c.credentials, + 'credentials', + RequestCredentials.sameOrigin, + ) + .having( + (c) => c.cache, + 'cache', + RequestCache.byDefault, + ) + .having( + (c) => c.referrer, + 'referrer', + '', + ) + .having( + (c) => c.referrerPolicy, + 'referrerPolicy', + RequestReferrerPolicy.strictOriginWhenCrossOrigin, + ) + .having( + (c) => c.redirectPolicy, + 'redirectPolicy', + RedirectPolicy.alwaysFollow, + ) + .having( + (c) => c.streamRequests, + 'streamRequests', + false, + ), + ); + }); + }); +} diff --git a/test/client_vm_test.dart b/test/client_vm_test.dart new file mode 100644 index 0000000..ab20474 --- /dev/null +++ b/test/client_vm_test.dart @@ -0,0 +1,52 @@ +@TestOn('vm') +import 'dart:async'; + +import 'package:eventflux/eventflux.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:http/http.dart'; +import 'package:test/test.dart'; + +/// Run this with `flutter test test/eventflux_test_vm.dart` +void main() { + late EventFlux eventFlux; + + setUp(() { + eventFlux = EventFlux.spawn(); + }); + + for (final connectionType in [ + EventFluxConnectionType.get, + EventFluxConnectionType.post, + ]) { + test( + 'uses Client in non-browser environment for $connectionType', + () { + final events = []; + final completer = Completer(); + const testUrl = 'https://localhost:4567'; + + fakeAsync((async) { + eventFlux.connect( + connectionType, + testUrl, + onSuccessCallback: (response) { + response?.stream?.listen( + (event) { + events.add(event.data); + if (events.length >= 3) { + completer.complete(); + } + }, + onError: (error) => completer.completeError(error), + ); + }, + onError: (error) => completer.completeError(error), + ); + }); + + expect(eventFlux.client, isA()); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + } +} diff --git a/test/models/web_config/redirect_policy_test.dart b/test/models/web_config/redirect_policy_test.dart new file mode 100644 index 0000000..74907d1 --- /dev/null +++ b/test/models/web_config/redirect_policy_test.dart @@ -0,0 +1,28 @@ +import 'package:eventflux/models/web_config/redirect_policy.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfigRedirectPolicy.toRedirectPolicy', () { + test('alwaysFollow converts to RedirectPolicy.alwaysFollow', () { + expect( + WebConfigRedirectPolicy.alwaysFollow.toRedirectPolicy(), + RedirectPolicy.alwaysFollow, + ); + }); + + test('probe converts to RedirectPolicy.probe', () { + expect( + WebConfigRedirectPolicy.probe.toRedirectPolicy(), + RedirectPolicy.probe, + ); + }); + + test('probeHead converts to RedirectPolicy.probeHead', () { + expect( + WebConfigRedirectPolicy.probeHead.toRedirectPolicy(), + RedirectPolicy.probeHead, + ); + }); + }); +} diff --git a/test/models/web_config/request_cache_test.dart b/test/models/web_config/request_cache_test.dart new file mode 100644 index 0000000..44006c8 --- /dev/null +++ b/test/models/web_config/request_cache_test.dart @@ -0,0 +1,49 @@ +import 'package:eventflux/models/web_config/request_cache.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfigRequestCache.toRequestCache', () { + test('byDefault converts to RequestCache.byDefault', () { + expect( + WebConfigRequestCache.byDefault.toRequestCache(), + RequestCache.byDefault, + ); + }); + + test('noStore converts to RequestCache.noStore', () { + expect( + WebConfigRequestCache.noStore.toRequestCache(), + RequestCache.noStore, + ); + }); + + test('reload converts to RequestCache.reload', () { + expect( + WebConfigRequestCache.reload.toRequestCache(), + RequestCache.reload, + ); + }); + + test('noCache converts to RequestCache.noCache', () { + expect( + WebConfigRequestCache.noCache.toRequestCache(), + RequestCache.noCache, + ); + }); + + test('forceCache converts to RequestCache.forceCache', () { + expect( + WebConfigRequestCache.forceCache.toRequestCache(), + RequestCache.forceCache, + ); + }); + + test('onlyIfCached converts to RequestCache.onlyIfCached', () { + expect( + WebConfigRequestCache.onlyIfCached.toRequestCache(), + RequestCache.onlyIfCached, + ); + }); + }); +} diff --git a/test/models/web_config/request_credentials_test.dart b/test/models/web_config/request_credentials_test.dart new file mode 100644 index 0000000..4592b70 --- /dev/null +++ b/test/models/web_config/request_credentials_test.dart @@ -0,0 +1,28 @@ +import 'package:eventflux/models/web_config/request_credentials.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfigRequestCredentials.toRequestCredentials', () { + test('sameOrigin converts to RequestCredentials.sameOrigin', () { + expect( + WebConfigRequestCredentials.sameOrigin.toRequestCredentials(), + RequestCredentials.sameOrigin, + ); + }); + + test('omit converts to RequestCredentials.omit', () { + expect( + WebConfigRequestCredentials.omit.toRequestCredentials(), + RequestCredentials.omit, + ); + }); + + test('cors converts to RequestCredentials.cors', () { + expect( + WebConfigRequestCredentials.cors.toRequestCredentials(), + RequestCredentials.cors, + ); + }); + }); +} diff --git a/test/models/web_config/request_mode_test.dart b/test/models/web_config/request_mode_test.dart new file mode 100644 index 0000000..068b952 --- /dev/null +++ b/test/models/web_config/request_mode_test.dart @@ -0,0 +1,42 @@ +import 'package:eventflux/models/web_config/request_mode.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfigRequestMode.toRequestMode', () { + test('sameOrigin converts to RequestMode.sameOrigin', () { + expect( + WebConfigRequestMode.sameOrigin.toRequestMode(), + RequestMode.sameOrigin, + ); + }); + + test('noCors converts to RequestMode.noCors', () { + expect( + WebConfigRequestMode.noCors.toRequestMode(), + RequestMode.noCors, + ); + }); + + test('cors converts to RequestMode.cors', () { + expect( + WebConfigRequestMode.cors.toRequestMode(), + RequestMode.cors, + ); + }); + + test('navigate converts to RequestMode.navigate', () { + expect( + WebConfigRequestMode.navigate.toRequestMode(), + RequestMode.navigate, + ); + }); + + test('webSocket converts to RequestMode.webSocket', () { + expect( + WebConfigRequestMode.webSocket.toRequestMode(), + RequestMode.webSocket, + ); + }); + }); +} diff --git a/test/models/web_config/request_referrer_policy_test.dart b/test/models/web_config/request_referrer_policy_test.dart new file mode 100644 index 0000000..39364c6 --- /dev/null +++ b/test/models/web_config/request_referrer_policy_test.dart @@ -0,0 +1,66 @@ +import 'package:eventflux/models/web_config/request_referrer_policy.dart'; +import 'package:fetch_client/fetch_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfigRequestReferrerPolicy.toRequestReferrerPolicy', () { + test('strictOriginWhenCrossOrigin converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin + .toRequestReferrerPolicy(), + RequestReferrerPolicy.strictOriginWhenCrossOrigin, + ); + }); + + test('noReferrer converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.noReferrer.toRequestReferrerPolicy(), + RequestReferrerPolicy.noReferrer, + ); + }); + + test('noReferrerWhenDowngrade converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.noReferrerWhenDowngrade + .toRequestReferrerPolicy(), + RequestReferrerPolicy.noReferrerWhenDowngrade, + ); + }); + + test('sameOrigin converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.sameOrigin.toRequestReferrerPolicy(), + RequestReferrerPolicy.sameOrigin, + ); + }); + + test('origin converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.origin.toRequestReferrerPolicy(), + RequestReferrerPolicy.origin, + ); + }); + + test('strictOrigin converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.strictOrigin.toRequestReferrerPolicy(), + RequestReferrerPolicy.strictOrigin, + ); + }); + + test('originWhenCrossOrigin converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.originWhenCrossOrigin + .toRequestReferrerPolicy(), + RequestReferrerPolicy.originWhenCrossOrigin, + ); + }); + + test('unsafeUrl converts correctly', () { + expect( + WebConfigRequestReferrerPolicy.unsafeUrl.toRequestReferrerPolicy(), + RequestReferrerPolicy.unsafeUrl, + ); + }); + }); +} diff --git a/test/models/web_config/web_config_test.dart b/test/models/web_config/web_config_test.dart new file mode 100644 index 0000000..7de2404 --- /dev/null +++ b/test/models/web_config/web_config_test.dart @@ -0,0 +1,75 @@ +import 'package:eventflux/models/web_config/redirect_policy.dart'; +import 'package:eventflux/models/web_config/request_cache.dart'; +import 'package:eventflux/models/web_config/request_credentials.dart'; +import 'package:eventflux/models/web_config/request_mode.dart'; +import 'package:eventflux/models/web_config/request_referrer_policy.dart'; +import 'package:eventflux/models/web_config/web_config.dart'; +import 'package:test/test.dart'; + +void main() { + group('WebConfig', () { + test('constructor has correct default values', () { + expect( + WebConfig(), + WebConfig( + mode: WebConfigRequestMode.noCors, + credentials: WebConfigRequestCredentials.sameOrigin, + cache: WebConfigRequestCache.byDefault, + referrer: '', + referrerPolicy: + WebConfigRequestReferrerPolicy.strictOriginWhenCrossOrigin, + redirectPolicy: WebConfigRedirectPolicy.alwaysFollow, + streamRequests: false, + ), + ); + }); + + group('copyWith', () { + test('returns a copy of itself', () { + final webConfig = WebConfig(); + final copy = webConfig.copyWith(); + expect(copy, webConfig); + }); + + test('returns a copy with the given fields replaced', () { + final webConfig = WebConfig(); + final copy = webConfig.copyWith( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + ); + expect( + copy, + WebConfig( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + ), + ); + }); + + test('returns a copy with all fields replaced', () { + final webConfig = WebConfig(); + final copy = webConfig.copyWith( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + cache: WebConfigRequestCache.noCache, + referrer: 'https://test.com', + referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer, + redirectPolicy: WebConfigRedirectPolicy.probe, + streamRequests: true, + ); + expect( + copy, + WebConfig( + mode: WebConfigRequestMode.sameOrigin, + credentials: WebConfigRequestCredentials.cors, + cache: WebConfigRequestCache.noCache, + referrer: 'https://test.com', + referrerPolicy: WebConfigRequestReferrerPolicy.noReferrer, + redirectPolicy: WebConfigRedirectPolicy.probe, + streamRequests: true, + ), + ); + }); + }); + }); +}