diff --git a/README.md b/README.md index a157c7a635..8ef1292c3c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,17 @@ We use the ability of the Open Food Facts API to return products results in JSON This plugin also allows you to edit a product or upload a new one to Open Food Facts. Using the same simple product structure you can create a product object or edit an existing one and send it to the API using a single function. +## Migrating from 2.x.x to 3.x.x (breaking changes) + +Starting with version 3.0.0, we now enforce all clients to provide a valid user agent. +For this, please ensure to set the SDK before using any other functionality: + +```dart +OpenFoodAPIConfiguration.userAgent = UserAgent( + name: '', +); +``` + ## Migrating from 1.x.x to 2.x.x (breaking changes) - Now the only entry point is `import 'package:openfoodfacts/openfoodfacts.dart';` diff --git a/lib/src/model/user_agent.dart b/lib/src/model/user_agent.dart index 0e07b52a25..e729f6b308 100644 --- a/lib/src/model/user_agent.dart +++ b/lib/src/model/user_agent.dart @@ -1,19 +1,32 @@ import '../interface/json_object.dart'; class UserAgent extends JsonObject { - final String? name; + /// The name of your application (eg: smooth-app) + final String name; + + /// The version of the application (1.0.0) final String? version; + + /// The system running the application (eg: Android+10) final String? system; + + /// The url of your application (eg: https://example.com) final String? url; + + /// Additional information about your application final String? comment; - const UserAgent({ - this.name, + UserAgent({ + required this.name, this.version, this.system, this.url, this.comment, - }); + }) { + if (name.trim().isEmpty) { + throw Exception('A non empty name is required'); + } + } @override Map toJson() => { diff --git a/lib/src/utils/http_helper.dart b/lib/src/utils/http_helper.dart index 90fcf555c9..6e00f9f277 100644 --- a/lib/src/utils/http_helper.dart +++ b/lib/src/utils/http_helper.dart @@ -27,17 +27,18 @@ class HttpHelper { /// A protected constructor to allow subclasses to create themselves. HttpHelper.internal(); - static const String USER_AGENT = 'Dart API'; static const String FROM = 'anonymous'; /// Adds user agent data to parameters, for statistics purpose - static Map? addUserAgentParameters( + static Map addUserAgentParameters( Map? map, ) { - map ??= {}; - if (OpenFoodAPIConfiguration.userAgent?.name != null) { - map['app_name'] = OpenFoodAPIConfiguration.userAgent!.name!; + if (OpenFoodAPIConfiguration.userAgent == null) { + throw Exception('A User-Agent must be set before calling this method'); } + map ??= {}; + map['app_name'] = OpenFoodAPIConfiguration.userAgent!.name; + if (OpenFoodAPIConfiguration.userAgent?.version != null) { map['app_version'] = OpenFoodAPIConfiguration.userAgent!.version!; } @@ -51,10 +52,6 @@ class HttpHelper { map['comment'] = OpenFoodAPIConfiguration.userAgent?.comment ?? ''; } - if (map.isEmpty) { - return null; - } - return map; } @@ -214,10 +211,13 @@ class HttpHelper { }) { Map? headers = {}; + if (OpenFoodAPIConfiguration.userAgent == null) { + throw Exception('A User-Agent must be set before calling this method'); + } + headers.addAll({ 'Accept': 'application/json', - 'User-Agent': - OpenFoodAPIConfiguration.userAgent?.toValueString() ?? USER_AGENT, + 'User-Agent': OpenFoodAPIConfiguration.userAgent!.toValueString(), 'From': OpenFoodAPIConfiguration.getUser(user)?.userId ?? FROM, }); diff --git a/test/configuration_test.dart b/test/configuration_test.dart index e5422eb291..a4f65957d3 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -4,19 +4,36 @@ import 'package:test/test.dart'; import 'test_constants.dart'; void main() { - OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - OpenFoodAPIConfiguration.globalQueryType = QueryType.PROD; + setUp(() { + OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; + OpenFoodAPIConfiguration.globalQueryType = QueryType.PROD; + }); - test('Get Uri', () { + test('Get Uri with no user agent', () { OpenFoodAPIConfiguration.userAgent = null; + + expect( + () => UriHelper.getUri( + path: '/test/test.pl', + ), + throwsA( + const TypeMatcher(), + ), + ); + }); + + test('Get Uri', () { OpenFoodAPIConfiguration.uuid = null; + Uri uri = UriHelper.getUri( path: '/test/test.pl', ); + expect( - uri.toString(), - 'https://world.openfoodfacts.org/test/test.pl', + uri.replace(query: '').toString(), + 'https://world.openfoodfacts.org/test/test.pl?', ); + expect(uri.queryParameters, HttpHelper.addUserAgentParameters(null)); Uri uri1 = UriHelper.getUri( path: '/test/test.pl', @@ -24,7 +41,7 @@ void main() { ); expect( uri1.toString(), - 'https://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD', + 'https://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD&$_appNameValue', ); }); @@ -75,7 +92,6 @@ void main() { test('Get Uri with uuid', () { const String uuid = 'uuidTest'; - OpenFoodAPIConfiguration.userAgent = null; OpenFoodAPIConfiguration.uuid = uuid; Uri uri; @@ -84,7 +100,7 @@ void main() { ); expect( uri.toString(), - 'https://world.openfoodfacts.org/test/test.pl?app_uuid=$uuid', + 'https://world.openfoodfacts.org/test/test.pl?$_appNameValue&app_uuid=$uuid', ); uri = UriHelper.getUri( @@ -93,7 +109,7 @@ void main() { ); expect( uri.toString(), - 'https://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD&app_uuid=$uuid', + 'https://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD&$_appNameValue&app_uuid=$uuid', ); uri = UriHelper.getUri( @@ -117,7 +133,6 @@ void main() { }); test('Get Test Uri', () { - OpenFoodAPIConfiguration.userAgent = null; OpenFoodAPIConfiguration.uuid = null; Uri uri = UriHelper.getUri( path: '/test/test.pl', @@ -125,7 +140,7 @@ void main() { ); expect( uri.toString(), - 'https://world.openfoodfacts.net/test/test.pl', + 'https://world.openfoodfacts.net/test/test.pl?$_appNameValue', ); Uri uri1 = UriHelper.getUri( @@ -135,7 +150,7 @@ void main() { ); expect( uri1.toString(), - 'https://world.openfoodfacts.net/test/test.pl?test=true&queryType=PROD', + 'https://world.openfoodfacts.net/test/test.pl?test=true&queryType=PROD&$_appNameValue', ); }); @@ -184,7 +199,6 @@ void main() { }); test('Get Uri with different uriScheme', () { - OpenFoodAPIConfiguration.userAgent = null; OpenFoodAPIConfiguration.uuid = null; OpenFoodAPIConfiguration.uriScheme = 'http'; Uri uri = UriHelper.getUri( @@ -192,7 +206,7 @@ void main() { ); expect( uri.toString(), - 'http://world.openfoodfacts.org/test/test.pl', + 'http://world.openfoodfacts.org/test/test.pl?$_appNameValue', ); Uri uri1 = UriHelper.getUri( @@ -201,7 +215,13 @@ void main() { ); expect( uri1.toString(), - 'http://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD', + 'http://world.openfoodfacts.org/test/test.pl?test=true&queryType=PROD&$_appNameValue', ); }); } + +String get _appNameValue => + 'app_name=${OpenFoodAPIConfiguration.userAgent!.name.replaceAll( + ' ', + '+', + )}'; diff --git a/test/test_constants.dart b/test/test_constants.dart index 848e472d43..e1b11c933b 100644 --- a/test/test_constants.dart +++ b/test/test_constants.dart @@ -1,7 +1,8 @@ import 'package:openfoodfacts/openfoodfacts.dart'; class TestConstants { - static const UserAgent TEST_USER_AGENT = UserAgent( + // ignore: non_constant_identifier_names + static final UserAgent TEST_USER_AGENT = UserAgent( name: 'off-dart integration tests', );