diff --git a/.gitignore b/.gitignore index 383c7b1..bc82213 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ migrate_working_dir/ .packages build/ coverage/ +.flutter-* +CRIB.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b1f40..1f78788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,42 @@ +## 4.0.0 + +- The flutter version of the SDK packages has been adjusted to global version 4. The SDK has been completely refactored. New data validation has been added to avoid wasting quota. A set of new tests has been added. Improved test code coverage. + +- Added [blockList option](https://www.emailjs.com/docs/sdk/options/#blocklist) to block requests for unwanted variable values +- Added [limitrate option](https://www.emailjs.com/docs/sdk/options/#limitrate) to install the throttle. For persistence, the shared_preferences package is used. + ## 1.3.0 -* Fix closed client [issue #1](https://github.com/emailjs-com/emailjs-flutter/issues/1) + +- Fix closed client [issue #1](https://github.com/emailjs-com/emailjs-flutter/issues/1) ## 1.2.1 -* Fix the environment SDK version + +- Fix the environment SDK version ## 1.2.0 -* Update packages + +- Update packages ## 1.1.0 -* Improve README.md + +- Improve README.md ## 1.0.0 -* Add support for the private key -* Instead of the public key argument, the settings object is passed, where the public and private keys can be set. -Check out the documentation and examples. + +- Add support for the private key +- Instead of the public key argument, the settings object is passed, where the public and private keys can be set. + Check out the documentation and examples. ## 0.0.3 -* re-format examples code + +- re-format examples code ## 0.0.2 -* Score fixes -* Added examples -* Dart formatter +- Score fixes +- Added examples +- Dart formatter ## 0.0.1 -* Official EmailJS SDK for Flutter +- Official EmailJS SDK for Flutter diff --git a/README.md b/README.md index 08e8c12..a7abd56 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Use you EmailJS account for sending emails. This is a flutter-only version, otherwise use - [Browser SDK](https://www.npmjs.com/package/@emailjs/browser) - [Node.js SDK](https://www.npmjs.com/package/@emailjs/nodejs) +- [React Native SDK](https://www.npmjs.com/package/@emailjs/react-native) - [REST API](https://www.emailjs.com/docs/rest-api/send/) ## Links @@ -18,7 +19,7 @@ This is a flutter-only version, otherwise use ## Intro EmailJS helps to send emails directly from your code. -No large knowledge is required – just connect EmailJS to one of the supported +No server is required – just connect EmailJS to one of the supported email services, create an email template, and use our SDK to trigger an email. @@ -44,7 +45,7 @@ through [Account:Security](https://dashboard.emailjs.com/admin/account/security) **send email** ```dart -import package:emailjs/emailjs.dart +import package:emailjs/emailjs.dart as emailjs Map templateParams = { 'name': 'James', @@ -52,39 +53,39 @@ Map templateParams = { }; try { - await EmailJS.send( - '', - '', + await emailjs.send( + 'YOUR_SERVICE_ID', + 'YOUR_TEMPLATE_ID', templateParams, - const Options( - publicKey: '', - privateKey: '', + const emailjs.Options( + publicKey: 'YOUR_PUBLIC_KEY', + privateKey: 'YOUR_PRIVATE_KEY', ), ); print('SUCCESS!'); } catch (error) { - print(error.toString()); + print('$error'); } ``` **init (optional)** ```dart -import package:emailjs/emailjs.dart +import package:emailjs/emailjs.dart as emailjs // set Public Key as global settings -EmailJS.init(const Options( - publicKey: '', - privateKey: '', +emailjs.init(const emailjs.Options( + publicKey: 'YOUR_PUBLIC_KEY', + privateKey: 'YOUR_PRIVATE_KEY', )); try { // send the email without dynamic variables - await EmailJS.send( - '', - '', + await emailjs.send( + 'YOUR_SERVICE_ID', + 'YOUR_TEMPLATE_ID', ); print('SUCCESS!'); } catch (error) { - print(error.toString()); + print('$error'); } ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index 744b729..0815c94 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/emailjs.dart' as emailjs; void main() { runApp(const App()); @@ -31,22 +31,25 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { void _sendEmail() async { try { - await EmailJS.send( - '', - '', + await emailjs.send( + 'YOUR_SERVICE_ID', + 'YOUR_TEMPLATE_ID', { - 'user_email': 'hi@example.com', + 'to_email': 'hi@example.com', 'message': 'Hi', }, - const Options( - publicKey: '', - privateKey: '', - ), + const emailjs.Options( + publicKey: 'YOUR_PUBLIC_KEY', + privateKey: 'YOUR_PRIVATE_KEY', + limitRate: const emailjs.LimitRate( + id: 'app', + throttle: 10000, + )), ); print('SUCCESS!'); } catch (error) { - if (error is EmailJSResponseStatus) { - print('ERROR... ${error.status}: ${error.text}'); + if (error is emailjs.EmailJSResponseStatus) { + print('ERROR... $error'); } print(error.toString()); } diff --git a/lib/emailjs.dart b/lib/emailjs.dart index 34b2fc9..f9f20f0 100644 --- a/lib/emailjs.dart +++ b/lib/emailjs.dart @@ -1,71 +1,9 @@ library emailjs; -import 'dart:convert'; -import 'package:http/http.dart' as http; - -import 'src/models/emailjs_response_status.dart'; -import 'src/utils/validate_params.dart'; -import 'src/api/send_json.dart'; -import 'src/models/options.dart'; - export 'src/models/emailjs_response_status.dart'; +export 'src/models/storage_provider.dart'; export 'src/models/options.dart'; - -class EmailJS { - /// Public Key specified in the [init] method - static String _publicKey = ''; - - /// Private Key specified in the [init] method - static String? _privateKey; - - /// API host specified in the [init] method - static String _host = 'api.emailjs.com'; - - /// HTTP Client specified in the [init] method - static http.Client? _httpClient; - - /// Global configuration for EmailJS - /// - /// Sets globally the EmailJS [options] - static void init( - Options options, [ - String? host, - http.Client? customHttpClient, - ]) { - EmailJS._publicKey = options.publicKey; - EmailJS._privateKey = options.privateKey; - EmailJS._host = host ?? 'api.emailjs.com'; - EmailJS._httpClient = customHttpClient; - } - - /// Sends the email through the [serviceID] using the ready-made [templateID]. - /// - /// It's possible to pass [templatePrams] dynamic variables, - /// and set the [options] for this call. - static Future send( - String serviceID, - String templateID, [ - Map? templatePrams, - Options? options, - ]) async { - final pubKey = options?.publicKey ?? EmailJS._publicKey; - final prKey = options?.privateKey ?? EmailJS._privateKey; - - validateParams(pubKey, serviceID, templateID); - - final Map params = { - 'lib_version': '1.3.0', - 'user_id': pubKey, - 'accessToken': prKey, - 'service_id': serviceID, - 'template_id': templateID, - 'template_params': templatePrams, - }; - - return await sendJSON( - Uri.https(EmailJS._host, 'api/v1.0/email/send'), - json.encode(params), - EmailJS._httpClient, - ); - } -} +export 'src/models/block_list.dart'; +export 'src/models/limit_rate.dart'; +export 'src/methods/init.dart'; +export 'src/methods/send.dart'; diff --git a/lib/src/api/send_json.dart b/lib/src/api/send_json.dart deleted file mode 100644 index 31e65e0..0000000 --- a/lib/src/api/send_json.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:http/http.dart' as http; -import '../models/emailjs_response_status.dart'; - -/// sends JSON object via HTTP POST -Future sendJSON( - Uri uri, String data, http.Client? customClient) async { - final client = customClient ?? http.Client(); - - try { - final response = await client.post( - uri, - headers: { - 'Content-Type': 'application/json', - }, - body: data, - ); - - if (response.statusCode == 200) { - return EmailJSResponseStatus( - status: response.statusCode, - text: response.body, - ); - } else { - throw EmailJSResponseStatus( - status: response.statusCode, - text: response.body, - ); - } - } finally { - client.close(); - } -} diff --git a/lib/src/api/send_post.dart b/lib/src/api/send_post.dart new file mode 100644 index 0000000..ec172ab --- /dev/null +++ b/lib/src/api/send_post.dart @@ -0,0 +1,35 @@ +import 'package:http/http.dart'; +import '../store/store.dart'; +import '../models/emailjs_response_status.dart'; + +/// sends JSON object via HTTPS POST +Future sendPost( + String url, + String data, [ + Client? client, +]) async { + client ??= Client(); + + try { + final response = await client.post( + Uri.https(store.host, url), + headers: { + 'Content-Type': 'application/json', + }, + body: data, + ); + + final responseStatus = EmailJSResponseStatus( + status: response.statusCode, + text: response.body, + ); + + if (response.statusCode == 200) { + return responseStatus; + } else { + throw responseStatus; + } + } finally { + client.close(); + } +} diff --git a/lib/src/errors/blocked_email_error.dart b/lib/src/errors/blocked_email_error.dart new file mode 100644 index 0000000..e19bf1c --- /dev/null +++ b/lib/src/errors/blocked_email_error.dart @@ -0,0 +1,8 @@ +import '../models/emailjs_response_status.dart'; + +EmailJSResponseStatus blockedEmailError() { + return const EmailJSResponseStatus( + status: 403, + text: 'Forbidden', + ); +} diff --git a/lib/src/errors/limit_rate_error.dart b/lib/src/errors/limit_rate_error.dart new file mode 100644 index 0000000..3bc0b68 --- /dev/null +++ b/lib/src/errors/limit_rate_error.dart @@ -0,0 +1,8 @@ +import '../models/emailjs_response_status.dart'; + +EmailJSResponseStatus limitRateError() { + return const EmailJSResponseStatus( + status: 429, + text: 'Too Many Requests', + ); +} diff --git a/lib/src/methods/init.dart b/lib/src/methods/init.dart new file mode 100644 index 0000000..09fc179 --- /dev/null +++ b/lib/src/methods/init.dart @@ -0,0 +1,18 @@ +import 'package:http/http.dart'; +import '../models/options.dart'; +import '../store/store.dart'; + +/// Global configuration for EmailJS +/// +/// Sets globally the EmailJS [options] +void init(Options options, [Client? client]) { + store.publicKey = options.publicKey; + store.privateKey = options.privateKey; + store.blockList = options.blockList; + store.limitRate = options.limitRate; + store.storageProvider = options.storageProvider; + store.origin = options.origin; + + // for testing porpose + store.client = client; +} diff --git a/lib/src/methods/send.dart b/lib/src/methods/send.dart new file mode 100644 index 0000000..46232b4 --- /dev/null +++ b/lib/src/methods/send.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import '../store/store.dart'; +import '../api/send_post.dart'; + +import '../models/emailjs_response_status.dart'; +import '../models/options.dart'; +import '../models/storage_provider.dart'; +import '../models/block_list.dart'; +import '../models/limit_rate.dart'; + +import '../utils/validate_params.dart'; +import '../utils/is_blocked_value_in_params.dart'; +import '../utils/is_limit_rate_hit.dart'; + +import '../errors/blocked_email_error.dart'; +import '../errors/limit_rate_error.dart'; + +/// Sends the email through the [serviceID] using the ready-made [templateID]. +/// +/// It's possible to pass [templateParams] dynamic variables, +/// and set the [options] for this call. +Future send( + String serviceID, + String templateID, [ + Map? templateParams, + Options? options, +]) async { + final publicKey = options?.publicKey ?? store.publicKey; + final privateKey = options?.privateKey ?? store.privateKey; + final StorageProvider storageProvider = + options?.storageProvider ?? store.storageProvider; + + final BlockList blockList = BlockList( + list: options?.blockList?.list ?? store.blockList?.list, + watchVariable: + options?.blockList?.watchVariable ?? store.blockList?.watchVariable, + ); + + final LimitRate limitRate = LimitRate( + id: options?.limitRate?.id ?? store.limitRate?.id, + throttle: store.limitRate?.throttle ?? options?.limitRate?.throttle ?? 0, + ); + + validateParams(publicKey, serviceID, templateID); + + if (templateParams != null && + isBlockedValueInParams(blockList, templateParams)) { + return Future.error(blockedEmailError()); + } + + if (await isLimitRateHit(limitRate, storageProvider)) { + return Future.error(limitRateError()); + } + + final Map params = { + 'lib_version': '4.0.0', + 'user_id': publicKey, + 'accessToken': privateKey, + 'service_id': serviceID, + 'template_id': templateID, + 'template_params': templateParams, + }; + + return sendPost('api/v1.0/email/send', json.encode(params), store.client); +} diff --git a/lib/src/models/block_list.dart b/lib/src/models/block_list.dart new file mode 100644 index 0000000..7d73b2d --- /dev/null +++ b/lib/src/models/block_list.dart @@ -0,0 +1,12 @@ +class BlockList { + /// The list of strings contains suspended values + final List? list; + + /// A name of the variable to be watched + final String? watchVariable; + + const BlockList({ + this.list, + this.watchVariable, + }); +} diff --git a/lib/src/models/default_storage.dart b/lib/src/models/default_storage.dart new file mode 100644 index 0000000..b89e215 --- /dev/null +++ b/lib/src/models/default_storage.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'storage_provider.dart'; + +class DefaultStorage implements StorageProvider { + final Future _storage = SharedPreferences.getInstance(); + + @override + Future get(String key) async { + final SharedPreferences storage = await _storage; + return storage.getInt(key); + } + + @override + Future set(String key, int value) async { + final SharedPreferences storage = await _storage; + await storage.setInt(key, value); + } + + @override + Future remove(String key) async { + final SharedPreferences storage = await _storage; + await storage.remove(key); + } +} diff --git a/lib/src/models/limit_rate.dart b/lib/src/models/limit_rate.dart new file mode 100644 index 0000000..4aaf943 --- /dev/null +++ b/lib/src/models/limit_rate.dart @@ -0,0 +1,12 @@ +class LimitRate { + /// Sets the throttle ID + final String? id; + + /// After how many milliseconds a next request is allowed + final int? throttle; + + const LimitRate({ + this.id, + this.throttle, + }); +} diff --git a/lib/src/models/options.dart b/lib/src/models/options.dart index d06c0a8..8d962d8 100644 --- a/lib/src/models/options.dart +++ b/lib/src/models/options.dart @@ -1,12 +1,32 @@ +import 'block_list.dart'; +import 'limit_rate.dart'; +import 'storage_provider.dart'; + class Options { - /// The public key is passed unmodified to API call. - final String publicKey; + /// The public key is required to identify your account and is a required variable + final String? publicKey; - /// The private key is passed unmodified to API call. + /// Additionally, a private key can be used for authorization final String? privateKey; + /// This configuration controls whether requests are blocked for certain values in the variable + final BlockList? blockList; + + /// The option allows SDK to process requests no more often than specified in the throttle + final LimitRate? limitRate; + + /// Overwrite the API endpoint + final String? origin; + + /// Overwrite the storage provider + final StorageProvider? storageProvider; + const Options({ - this.publicKey = '', + this.publicKey, this.privateKey, + this.blockList, + this.limitRate, + this.origin, + this.storageProvider, }); } diff --git a/lib/src/models/storage_provider.dart b/lib/src/models/storage_provider.dart new file mode 100644 index 0000000..bdad424 --- /dev/null +++ b/lib/src/models/storage_provider.dart @@ -0,0 +1,5 @@ +abstract class StorageProvider { + Future get(String key); + Future set(String key, int value); + Future remove(String key); +} diff --git a/lib/src/store/store.dart b/lib/src/store/store.dart new file mode 100644 index 0000000..f8f431f --- /dev/null +++ b/lib/src/store/store.dart @@ -0,0 +1,39 @@ +import 'package:http/http.dart'; + +import '../models/default_storage.dart'; +import '../models/block_list.dart'; +import '../models/limit_rate.dart'; +import '../models/storage_provider.dart'; + +class Store { + String host; + String? publicKey; + String? privateKey; + BlockList? blockList; + LimitRate? limitRate; + StorageProvider? storeProvider; + Client? client; + + set storageProvider(StorageProvider? provider) { + if (provider != null) { + storeProvider = provider; + } + } + + StorageProvider get storageProvider { + storeProvider = storeProvider ?? DefaultStorage(); + return storeProvider!; + } + + set origin(String? origin) { + if (origin != null) { + host = origin; + } + } + + Store({ + this.host = 'api.emailjs.com', + }); +} + +final Store store = Store(); diff --git a/lib/src/utils/is_blocked_value_in_params.dart b/lib/src/utils/is_blocked_value_in_params.dart new file mode 100644 index 0000000..64d995f --- /dev/null +++ b/lib/src/utils/is_blocked_value_in_params.dart @@ -0,0 +1,17 @@ +import '../models/block_list.dart'; + +bool isBlockListDisabled(BlockList options) { + return options.list == null || + options.list!.isEmpty || + options.watchVariable == null; +} + +bool isBlockedValueInParams( + BlockList options, + Map params, +) { + if (isBlockListDisabled(options)) return false; + + final value = params[options.watchVariable]; + return options.list!.contains(value); +} diff --git a/lib/src/utils/is_limit_rate_hit.dart b/lib/src/utils/is_limit_rate_hit.dart new file mode 100644 index 0000000..4ca33ca --- /dev/null +++ b/lib/src/utils/is_limit_rate_hit.dart @@ -0,0 +1,34 @@ +import '../models/limit_rate.dart'; +import '../models/storage_provider.dart'; +import 'validate_limit_rate_params.dart'; + +Future getLeftTime( + String id, + int throttle, + StorageProvider storage, +) async { + final int lastTime = await storage.get(id) ?? 0; + return throttle - DateTime.now().millisecondsSinceEpoch + lastTime; +} + +Future isLimitRateHit( + LimitRate options, + StorageProvider? storage, +) async { + if (storage == null || options.throttle == null || options.throttle == 0) { + return false; + } + + final id = options.id ?? 'default'; + + validateLimitRateParams(options.throttle!, id); + + final leftTime = await getLeftTime(id, options.throttle!, storage); + + if (leftTime > 0) { + return true; + } + + await storage.set(id, DateTime.now().millisecondsSinceEpoch); + return false; +} diff --git a/lib/src/utils/validate_limit_rate_params.dart b/lib/src/utils/validate_limit_rate_params.dart new file mode 100644 index 0000000..5a071e2 --- /dev/null +++ b/lib/src/utils/validate_limit_rate_params.dart @@ -0,0 +1,9 @@ +void validateLimitRateParams(int throttle, [String? id]) { + if (throttle < 0) { + throw 'The LimitRate throttle has to be a positive number'; + } + + if (id != null && id.isEmpty) { + throw 'The LimitRate ID has to be a non-empty string'; + } +} diff --git a/lib/src/utils/validate_params.dart b/lib/src/utils/validate_params.dart index 08a896d..c038526 100644 --- a/lib/src/utils/validate_params.dart +++ b/lib/src/utils/validate_params.dart @@ -1,20 +1,18 @@ /// Validates required params -bool validateParams( - String publicKey, - String serviceID, - String templateID, +void validateParams( + String? publicKey, + String? serviceID, + String? templateID, ) { - if (publicKey.isEmpty) { + if (publicKey == null || publicKey.isEmpty) { throw 'The public key is required. Visit https://dashboard.emailjs.com/admin/account'; } - if (serviceID.isEmpty) { + if (serviceID == null || serviceID.isEmpty) { throw 'The service ID is required. Visit https://dashboard.emailjs.com/admin'; } - if (templateID.isEmpty) { + if (templateID == null || templateID.isEmpty) { throw 'The template ID is required. Visit https://dashboard.emailjs.com/admin/templates'; } - - return true; } diff --git a/pubspec.yaml b/pubspec.yaml index 0db3db2..fa13e72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: emailjs description: EmailJS helps sending emails directly from the Flutter app. No server is required. -version: 1.3.0 +version: 4.0.0 homepage: https://www.emailjs.com repository: https://github.com/emailjs-com/emailjs-flutter @@ -12,9 +12,10 @@ dependencies: flutter: sdk: flutter http: ^1.1.0 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 - mocktail: ^0.3.0 + flutter_lints: ^4.0.0 + mocktail: ^1.0.3 diff --git a/test/emailjs_test.dart b/test/emailjs_test.dart deleted file mode 100644 index 6699deb..0000000 --- a/test/emailjs_test.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:http/http.dart' as http; - -import 'package:emailjs/emailjs.dart'; - -class MockClient extends Mock implements http.Client {} - -class FakeUri extends Fake implements Uri {} - -void main() { - setUpAll(() { - registerFallbackValue(FakeUri()); - }); - - group('required params validator', () { - test('should send method and fail on the public key', () { - expect( - () => EmailJS.send('default_service', 'my_test_template'), - throwsA(startsWith('The public key is required')), - ); - }); - - test('should send method and fail on the service ID', () { - EmailJS.init(const Options( - publicKey: 'LC2JWGTestKeySomething', - privateKey: 'PrKeyTestKeySomething', - )); - - expect( - () => EmailJS.send('', 'my_test_template'), - throwsA(startsWith('The service ID is required')), - ); - }); - - test('should send method and fail on the template ID', () { - EmailJS.init(const Options( - publicKey: 'LC2JWGTestKeySomething', - privateKey: 'PrKeyTestKeySomething', - )); - - expect( - () => EmailJS.send('default_service', ''), - throwsA(startsWith('The template ID is required')), - ); - }); - }); - - group('EmailJS.send method', () { - test('should init and send method successfully', () async { - final mockHttpClient = MockClient(); - - when(() => mockHttpClient.post( - any(), - headers: any(named: 'headers'), - body: any(named: 'body'), - )).thenAnswer((_) async => http.Response('OK', 200)); - - EmailJS.init( - const Options( - publicKey: 'LC2JWGTestKeySomething', - privateKey: 'PrKeyTestKeySomething', - ), - null, - mockHttpClient, - ); - - try { - final result = await EmailJS.send( - 'default_service', - 'my_test_template', - ); - expect(result.status, 200); - expect(result.text, 'OK'); - } catch (error) { - expect(error, isNull); - } - }); - - test('should send method successfully with 4 params', () async { - final mockHttpClient = MockClient(); - - when(() => mockHttpClient.post( - any(), - headers: any(named: 'headers'), - body: any(named: 'body'), - )).thenAnswer((_) async => http.Response('OK', 200)); - - // pass the mock http client - EmailJS.init( - const Options( - publicKey: '', - ), - null, - mockHttpClient, - ); - - try { - final result = await EmailJS.send( - 'default_service', - 'my_test_template', - null, - const Options( - publicKey: 'LC2JWGTestKeySomething', - privateKey: 'PrKeyTestKeySomething', - )); - expect(result.status, 200); - expect(result.text, 'OK'); - } catch (error) { - expect(error, isNull); - } - }); - - test('should send method and fail', () async { - final mockHttpClient = MockClient(); - - when(() => mockHttpClient.post( - any(), - headers: any(named: 'headers'), - body: any(named: 'body'), - )).thenAnswer((_) async => http.Response('The Public Key is required', 403)); - - // pass the mock http client - EmailJS.init( - const Options( - publicKey: 'LC2JWGTestKeySomething', - privateKey: 'PrKeyTestKeySomething', - ), - null, - mockHttpClient, - ); - - try { - final result = await EmailJS.send( - 'default_service', - 'my_test_template', - ); - expect(result, isNull); - } catch (error) { - if (error is EmailJSResponseStatus) { - expect('$error', '[403] The Public Key is required'); - } - } - }); - }); -} diff --git a/test/errors/blocked_email_error_test.dart b/test/errors/blocked_email_error_test.dart new file mode 100644 index 0000000..eb757aa --- /dev/null +++ b/test/errors/blocked_email_error_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/errors/blocked_email_error.dart'; + +void main() { + test('should return EmailJSResponseStatus', () { + final error = blockedEmailError(); + expect(error.runtimeType, EmailJSResponseStatus); + }); + + test('should return status 403', () { + final error = blockedEmailError(); + expect('$error', '[403] Forbidden'); + }); +} \ No newline at end of file diff --git a/test/errors/limit_rate_error_test.dart b/test/errors/limit_rate_error_test.dart new file mode 100644 index 0000000..2765e4c --- /dev/null +++ b/test/errors/limit_rate_error_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/errors/limit_rate_error.dart'; + +void main() { + test('should return EmailJSResponseStatus', () { + final error = limitRateError(); + expect(error.runtimeType, EmailJSResponseStatus); + }); + + test('should return status 429', () { + final error = limitRateError(); + expect('$error', '[429] Too Many Requests'); + }); +} \ No newline at end of file diff --git a/test/integration_test.dart b/test/integration_test.dart new file mode 100644 index 0000000..0c73917 --- /dev/null +++ b/test/integration_test.dart @@ -0,0 +1,121 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:emailjs/emailjs.dart' as emailjs; + +class MockClient extends Mock implements http.Client {} + +class FakeUri extends Fake implements Uri {} + +class MockStorage extends Mock implements emailjs.StorageProvider {} + +initMockRequest({int? status = 200}) { + final mockHttpClient = MockClient(); + when(() => mockHttpClient.post( + any(), + headers: any(named: 'headers'), + body: any(named: 'body'), + )).thenAnswer((_) async => http.Response('OK', status!)); + + return mockHttpClient; +} + +void main() { + setUpAll(() { + registerFallbackValue(FakeUri()); + }); + + setUp(() async { + final Map values = {}; + SharedPreferences.setMockInitialValues(values); + }); + + group('send method', () { + test('should call the send method and fail on http error', () async { + final mockHttpClient = initMockRequest(status: 400); + + emailjs.init( + const emailjs.Options( + publicKey: 'LC2JWGTestKeySomething', + privateKey: 'PrKeyTestKeySomething', + ), + mockHttpClient, + ); + + try { + final response = await emailjs.send( + 'default_service', + 'my_test_template', + ); + expect(response, isNull); + } catch (error) { + expect('$error', '[400] OK'); + } + }); + + test('should call the send method and fail on http error as future', () async { + final mockHttpClient = initMockRequest(status: 400); + + emailjs.init( + const emailjs.Options( + publicKey: 'LC2JWGTestKeySomething', + privateKey: 'PrKeyTestKeySomething', + ), + mockHttpClient, + ); + + emailjs.send( + 'default_service', + 'my_test_template', + ).then((response) { + expect(response, isNull); + }).catchError((error) { + expect('$error', '[400] OK'); + }); + }); + + test('should call the init and the send method successfully', () async { + final mockHttpClient = initMockRequest(); + + emailjs.init( + const emailjs.Options( + publicKey: 'LC2JWGTestKeySomething', + privateKey: 'PrKeyTestKeySomething', + ), + mockHttpClient, + ); + + try { + final response = await emailjs.send( + 'default_service', + 'my_test_template', + ); + expect('$response', '[200] OK'); + } catch (error) { + expect(error, isNull); + } + }); + + test('should call the init and the send method successfully as future', () { + final mockHttpClient = initMockRequest(); + + emailjs.init( + const emailjs.Options( + publicKey: 'LC2JWGTestKeySomething', + privateKey: 'PrKeyTestKeySomething', + ), + mockHttpClient, + ); + + emailjs.send( + 'default_service', + 'my_test_template', + ).then((response) { + expect('$response', '[200] OK'); + }).catchError((error) { + expect(error, isNull); + }); + }); + }); +} diff --git a/test/methods/init_test.dart b/test/methods/init_test.dart new file mode 100644 index 0000000..11b4353 --- /dev/null +++ b/test/methods/init_test.dart @@ -0,0 +1,53 @@ +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/store/store.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + + test('should call the init method with empty options and get default values', () { + init(const Options()); + + final newStore = Store(); + + expect(store.host, newStore.host); + expect(store.publicKey, newStore.publicKey); + expect(store.privateKey, newStore.privateKey); + expect(store.blockList, newStore.blockList); + expect(store.limitRate, newStore.limitRate); + expect(store.storeProvider, newStore.storeProvider); + expect(store.client, newStore.client); + }); + + test('should call the init method with custom options', () { + init(const Options( + publicKey: 'C2JWGTestKeySomething', + blockList: BlockList( + list: ['block@email.com'], + ), + limitRate: LimitRate( + throttle: 10000, + ), + origin: 'test.com', + )); + + + final newStore = Store(host: 'test.com'); + newStore.publicKey = 'C2JWGTestKeySomething'; + + newStore.blockList = const BlockList( + list: ['block@email.com'], + ); + + newStore.limitRate = const LimitRate( + throttle: 10000, + ); + + expect(store.host, newStore.host); + expect(store.publicKey, newStore.publicKey); + expect(store.privateKey, newStore.privateKey); + expect(store.blockList, newStore.blockList); + expect(store.limitRate, newStore.limitRate); + expect(store.storeProvider, newStore.storeProvider); + expect(store.client, newStore.client); + }); +} diff --git a/test/methods/send_test.dart b/test/methods/send_test.dart new file mode 100644 index 0000000..60cad39 --- /dev/null +++ b/test/methods/send_test.dart @@ -0,0 +1,161 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:emailjs/emailjs.dart'; + +class MockClient extends Mock implements http.Client {} + +class FakeUri extends Fake implements Uri {} + +class MockStorage extends Mock implements StorageProvider {} + +void main() { + setUpAll(() { + registerFallbackValue(FakeUri()); + + final mockHttpClient = MockClient(); + when(() => mockHttpClient.post( + any(), + headers: any(named: 'headers'), + body: any(named: 'body'), + )).thenAnswer((_) async => http.Response('OK', 200)); + init(const Options(), mockHttpClient); + }); + + setUp(() async { + final Map values = {}; + SharedPreferences.setMockInitialValues(values); + }); + + test('should call the send method and fail on the public key', () { + expect( + () => send('default_service', 'my_test_template'), + throwsA(startsWith('The public key is required')), + ); + }); + + test('should call the send method and fail on the service ID', () { + expect( + () => send('', 'my_test_template', null, const Options( + publicKey: 'C2JWGTestKeySomething', + )), + throwsA(startsWith('The service ID is required')), + ); + }); + + test('should call the send method and fail on the template ID', () { + expect( + () => send('default_service', '', null, const Options( + publicKey: 'C2JWGTestKeySomething', + )), + throwsA(startsWith('The template ID is required')), + ); + }); + + test('should call the send method and fail on blocklist', () async { + try { + final response = await send('default_service', 'my_test_template', { + 'email': 'bar@emailjs.com', + }, const Options( + publicKey: 'C2JWGTestKeySomething', + blockList: BlockList( + list: ['foo@emailjs.com', 'bar@emailjs.com'], + watchVariable: 'email', + ), + )); + + expect(response, isNull); + } catch (error) { + expect('$error', '[403] Forbidden'); + } + }); + + test('should call the send method and fail on blocklist as future', () { + send('default_service', 'my_test_template', { + 'email': 'bar@emailjs.com', + }, const Options( + publicKey: 'C2JWGTestKeySomething', + blockList: BlockList( + list: ['foo@emailjs.com', 'bar@emailjs.com'], + watchVariable: 'email', + ), + )).then((result) { + expect(result, isNull); + }).catchError((error) { + expect('$error', '[403] Forbidden'); + }); + }); + + test('should call the send method and fail on limit rate', () async { + sendEmail() { + return send('default_service', 'my_test_template', null, const Options( + publicKey: 'C2JWGTestKeySomething', + limitRate: LimitRate( + id: 'async-send', + throttle: 100, + ), + )); + } + + try { + final response = await sendEmail(); + expect('$response', '[200] OK'); + } catch (error) { + expect(error, isNull); + } + + try { + final response = await sendEmail(); + expect(response, isNull); + } catch (error) { + expect('$error', '[429] Too Many Requests'); + } + }); + + test('should call the send method and fail on limit rate as future', () { + sendEmail() { + return send('default_service', 'my_test_template', null, const Options( + publicKey: 'C2JWGTestKeySomething', + limitRate: LimitRate( + id: 'future-send', + throttle: 1000, + ), + )); + } + + sendEmail().then((response) { + expect('$response', '[200] OK'); + + sendEmail().then((response) { + expect(response, isNull); + }).catchError((error) { + expect('$error', '[429] Too Many Requests'); + }); + }).catchError((error) { + expect(error, isNull); + }); + }); + + test('should call the send method successfully with 4 params', () async { + try { + final response = await send('default_service', 'my_test_template', {}, const Options( + publicKey: 'C2JWGTestKeySomething', + )); + + expect('$response', '[200] OK'); + } catch (error) { + expect(error, isNull); + } + }); + + test('should call the send method successfully with 4 params as future', () { + send('default_service', 'my_test_template', {}, const Options( + publicKey: 'C2JWGTestKeySomething', + )).then((response) { + expect('$response', '[200] OK'); + }).catchError((error) { + expect(error, isNull); + }); + }); +} diff --git a/test/models/default_storage_test.dart b/test/models/default_storage_test.dart new file mode 100644 index 0000000..2dcd58e --- /dev/null +++ b/test/models/default_storage_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:emailjs/src/models/default_storage.dart'; + +void main() { + setUpAll(() async { + final Map values = {'test': 100}; + SharedPreferences.setMockInitialValues(values); + }); + + test('get value', () async { + DefaultStorage storage = DefaultStorage(); + final value = await storage.get('test'); + expect(value, 100); + }); + + test('remove value', () async { + DefaultStorage storage = DefaultStorage(); + await storage.remove('test'); + expect(await storage.get('test'), null); + }); + + test('set value', () async { + DefaultStorage storage = DefaultStorage(); + await storage.set('test', 500); + expect(await storage.get('test'), 500); + }); +} diff --git a/test/models/emailjs_response_status_test.dart b/test/models/emailjs_response_status_test.dart new file mode 100644 index 0000000..edfee28 --- /dev/null +++ b/test/models/emailjs_response_status_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/models/emailjs_response_status.dart'; + +void main() { + test('should handle the success response', () { + const error = EmailJSResponseStatus(status: 200, text: 'OK'); + expect(error.status, 200); + expect(error.text, 'OK'); + }); + + test('should handle the fail response', () { + const error = EmailJSResponseStatus(status: 404, text: 'No Found'); + expect(error.status, 404); + expect(error.text, 'No Found'); + }); + + test('should handle the null response', () { + const error = EmailJSResponseStatus(); + expect(error.status, 0); + expect(error.text, 'Network Error'); + }); +} \ No newline at end of file diff --git a/test/store/store_test.dart b/test/store/store_test.dart new file mode 100644 index 0000000..00033e2 --- /dev/null +++ b/test/store/store_test.dart @@ -0,0 +1,31 @@ +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/store/store.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockStorage extends Mock implements StorageProvider {} + +void main() { + test('origin setter', () { + + final store = Store(); + expect(store.host, 'api.emailjs.com'); + + store.origin = 'test.com'; + expect(store.host, 'test.com'); + + store.origin = null; + expect(store.host, 'test.com'); + }); + + test('storageProvider setter', () { + + final store = Store(); + + store.storageProvider = MockStorage(); + expect(store.storageProvider is MockStorage, true); + + store.storageProvider = null; + expect(store.storageProvider is MockStorage, true); + }); +} \ No newline at end of file diff --git a/test/utils/is_blocked_value_in_params_test.dart b/test/utils/is_blocked_value_in_params_test.dart new file mode 100644 index 0000000..a202022 --- /dev/null +++ b/test/utils/is_blocked_value_in_params_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/utils/is_blocked_value_in_params.dart'; + +void main() { + group('should be disabled', () { + test('get value', () { + const blockList = BlockList(); + expect(isBlockedValueInParams(blockList, {}), false); + }); + + test('without list', () { + const blockList = BlockList(watchVariable: 'email'); + expect(isBlockedValueInParams(blockList, {}), false); + }); + + test('without watchVariable', () { + const blockList = BlockList(list: ['test@emailjs.com']); + expect(isBlockedValueInParams(blockList, {}), false); + }); + + test('without data', () { + const blockList = BlockList( + watchVariable: 'email', + list: ['test@emailjs.com'], + ); + + expect(isBlockedValueInParams(blockList, {}), false); + }); + + test('wrong type', () { + const blockList = BlockList( + watchVariable: 'email', + list: ['test@emailjs.com'], + ); + + expect(isBlockedValueInParams(blockList, { + 'email': ['item', 'item'], + }), false); + }); + + test('not found in the list', () { + const blockList = BlockList( + watchVariable: 'email', + list: ['test@emailjs.com', 'bar@emailjs.com'], + ); + + expect(isBlockedValueInParams(blockList, { + 'email': 'foo@emailjs.com', + }), false); + }); + }); + + group('should be enabled', () { + test('template params', () { + const blockList = BlockList( + watchVariable: 'email', + list: ['test@emailjs.com', 'foo@emailjs.com', 'bar@emailjs.com'], + ); + + expect(isBlockedValueInParams(blockList, { + 'email': 'test@emailjs.com', + 'other': 'other data', + }), true); + }); + }); +} diff --git a/test/utils/is_limit_rate_hit_test.dart b/test/utils/is_limit_rate_hit_test.dart new file mode 100644 index 0000000..b26a348 --- /dev/null +++ b/test/utils/is_limit_rate_hit_test.dart @@ -0,0 +1,68 @@ +import 'package:emailjs/src/models/default_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:emailjs/emailjs.dart'; +import 'package:emailjs/src/utils/is_limit_rate_hit.dart'; + +void main() { + setUp(() { + final Map values = {}; + SharedPreferences.setMockInitialValues(values); + }); + + group('limit rate is disabled', () { + test('empty limit rate options', () async { + DefaultStorage storage = DefaultStorage(); + const limitRate = LimitRate(); + + expect(await isLimitRateHit(limitRate, storage), false); + }); + + test('throttle is 0', () async { + DefaultStorage storage = DefaultStorage(); + const limitRate = LimitRate(throttle: 0); + + expect(await isLimitRateHit(limitRate, storage), false); + }); + + test('no record', () async { + DefaultStorage storage = DefaultStorage(); + const limitRate = LimitRate(throttle: 1000, id: 'app'); + + expect(await isLimitRateHit(limitRate, storage), false); + }); + + test('no hit limit', () async { + DefaultStorage storage = DefaultStorage(); + const limitRate = LimitRate(throttle: 100, id: 'app'); + + expect(await isLimitRateHit(limitRate, storage), false); + + await Future.delayed(const Duration(milliseconds: 150)); + + expect(await isLimitRateHit(limitRate, storage), false); + }); + + test('not same page or ID', () async { + DefaultStorage storage = DefaultStorage(); + LimitRate limitRate = const LimitRate(throttle: 100, id: 'app'); + + expect(await isLimitRateHit(limitRate, storage), false); + + limitRate = const LimitRate(throttle: 100, id: 'new-app'); + + expect(await isLimitRateHit(limitRate, storage), false); + }); + }); + + group('limit rate is enabled', () { + test('hit limit', () async { + DefaultStorage storage = DefaultStorage(); + const limitRate = LimitRate(throttle: 100, id: 'app'); + + expect(await isLimitRateHit(limitRate, storage), false); + expect(await isLimitRateHit(limitRate, storage), true); + }); + }); +} diff --git a/test/utils/validate_limit_rate_params_test.dart b/test/utils/validate_limit_rate_params_test.dart new file mode 100644 index 0000000..aefdfc1 --- /dev/null +++ b/test/utils/validate_limit_rate_params_test.dart @@ -0,0 +1,36 @@ +import 'package:emailjs/src/utils/validate_limit_rate_params.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('should fail', () { + test('throttle is a negative number', () { + expect( + () => validateLimitRateParams(-1000), + throwsA(startsWith('The LimitRate throttle has to be a positive number')), + ); + }); + + test('ID is empty', () { + expect( + () => validateLimitRateParams(1000, ''), + throwsA(startsWith('The LimitRate ID has to be a non-empty string')), + ); + }); + }); + + group('should successfully pass the validation', () { + test('throttle is a positive number', () { + expect( + () => validateLimitRateParams(1000), + returnsNormally, + ); + }); + + test('ID is valid string', () { + expect( + () => validateLimitRateParams(1000, 'app'), + returnsNormally, + ); + }); + }); +} \ No newline at end of file diff --git a/test/utils/validate_params_test.dart b/test/utils/validate_params_test.dart new file mode 100644 index 0000000..6d89640 --- /dev/null +++ b/test/utils/validate_params_test.dart @@ -0,0 +1,59 @@ +import 'package:emailjs/src/utils/validate_params.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('should fail on the public key', () { + test('no key', () { + expect( + () => validateParams('', 'default_service', 'my_test_template'), + throwsA(startsWith('The public key is required')), + ); + }); + + test('no string', () { + expect( + () => validateParams(null, 'default_service', 'my_test_template'), + throwsA(startsWith('The public key is required')), + ); + }); + }); + + group('should fail on the service ID', () { + test('no key', () { + expect( + () => validateParams('d2JWGTestKeySomething', '', 'my_test_template'), + throwsA(startsWith('The service ID is required')), + ); + }); + + test('no string', () { + expect( + () => validateParams('d2JWGTestKeySomething', null, 'my_test_template'), + throwsA(startsWith('The service ID is required')), + ); + }); + }); + + group('should fail on the service ID', () { + test('no key', () { + expect( + () => validateParams('d2JWGTestKeySomething', 'default_service', ''), + throwsA(startsWith('The template ID is required')), + ); + }); + + test('no string', () { + expect( + () => validateParams('d2JWGTestKeySomething', 'default_service', null), + throwsA(startsWith('The template ID is required')), + ); + }); + }); + + test('should successfully pass the validation', () { + expect( + () => validateParams('d2JWGTestKeySomething', 'default_service', 'my_test_template'), + returnsNormally, + ); + }); +}