diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index c770495064..1c5905debe 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -145,6 +145,8 @@ export 'src/utils/server_type.dart'; export 'src/utils/suggestion_manager.dart'; export 'src/utils/tag_type.dart'; export 'src/utils/tag_type_autocompleter.dart'; +export 'src/utils/too_many_requests_exception.dart'; +export 'src/utils/too_many_requests_manager.dart'; export 'src/utils/unit_helper.dart'; export 'src/utils/uri_helper.dart'; export 'src/utils/uri_reader.dart'; diff --git a/lib/src/open_food_api_client.dart b/lib/src/open_food_api_client.dart index 52520f24da..49c1cc78e5 100644 --- a/lib/src/open_food_api_client.dart +++ b/lib/src/open_food_api_client.dart @@ -47,6 +47,7 @@ import 'utils/product_query_configurations.dart'; import 'utils/product_search_query_configuration.dart'; import 'utils/tag_type.dart'; import 'utils/taxonomy_query_configuration.dart'; +import 'utils/too_many_requests_exception.dart'; import 'utils/uri_helper.dart'; /// Client calls of the Open Food Facts API @@ -356,6 +357,7 @@ class OpenFoodAPIClient { final UriProductHelper uriHelper = uriHelperFoodProd, }) async { final Response response = await configuration.getResponse(user, uriHelper); + TooManyRequestsException.check(response); return response.body; } @@ -519,6 +521,7 @@ class OpenFoodAPIClient { final UriProductHelper uriHelper = uriHelperFoodProd, }) async { final Response response = await configuration.getResponse(user, uriHelper); + TooManyRequestsException.check(response); final String jsonStr = _replaceQuotes(response.body); final SearchResult result = SearchResult.fromJson( HttpHelper().jsonDecode(jsonStr), diff --git a/lib/src/prices/get_locations_order.dart b/lib/src/prices/get_locations_order.dart index 69886dbb89..ec51e163d5 100644 --- a/lib/src/prices/get_locations_order.dart +++ b/lib/src/prices/get_locations_order.dart @@ -2,6 +2,7 @@ import 'order_by.dart'; /// Field for the "order by" clause of "get locations". enum GetLocationsOrderField implements OrderByField { + priceCount(offTag: 'price_count'), created(offTag: 'created'), updated(offTag: 'updated'); diff --git a/lib/src/prices/get_proofs_order.dart b/lib/src/prices/get_proofs_order.dart index 4efd306b1e..b3f581746a 100644 --- a/lib/src/prices/get_proofs_order.dart +++ b/lib/src/prices/get_proofs_order.dart @@ -2,6 +2,7 @@ import 'order_by.dart'; /// Field for the "order by" clause of "get proofs". enum GetProofsOrderField implements OrderByField { + priceCount(offTag: 'price_count'), created(offTag: 'created'); const GetProofsOrderField({required this.offTag}); diff --git a/lib/src/utils/too_many_requests_exception.dart b/lib/src/utils/too_many_requests_exception.dart new file mode 100644 index 0000000000..97e37d7df0 --- /dev/null +++ b/lib/src/utils/too_many_requests_exception.dart @@ -0,0 +1,19 @@ +import 'package:http/http.dart'; + +/// Exception when the server returns "Too many requests". +class TooManyRequestsException implements Exception { + const TooManyRequestsException(); + + /// Start of the response body when the server received too many requests. + static const String _tooManyRequestsError = + '

TOO MANY REQUESTS

'; + + static void check(final Response response) { + if (response.body.startsWith(_tooManyRequestsError)) { + throw TooManyRequestsException(); + } + } + + @override + String toString() => 'Too many requests'; +} diff --git a/lib/src/utils/too_many_requests_manager.dart b/lib/src/utils/too_many_requests_manager.dart new file mode 100644 index 0000000000..f5bd250c2f --- /dev/null +++ b/lib/src/utils/too_many_requests_manager.dart @@ -0,0 +1,46 @@ +/// Manager dedicated to "too many requests" server response. +/// +/// Typically, the server may limit the number of requests to a [maxCount] +/// during a specific [duration]. +class TooManyRequestsManager { + TooManyRequestsManager({ + required this.maxCount, + required this.duration, + }); + + final int maxCount; + final Duration duration; + + final List _requestTimestamps = []; + + /// Waits the needed duration in order to avoid "too many requests" error. + Future waitIfNeeded() async { + while (_requestTimestamps.length >= maxCount) { + final int previousInMillis = _requestTimestamps.first; + final int nowInMillis = DateTime.now().millisecondsSinceEpoch; + final int waitingInMillis = + duration.inMilliseconds - nowInMillis + previousInMillis; + if (waitingInMillis > 0) { + await Future.delayed(Duration(milliseconds: waitingInMillis)); + } + _requestTimestamps.removeAt(0); + } + final DateTime now = DateTime.now(); + final int nowInMillis = now.millisecondsSinceEpoch; + _requestTimestamps.add(nowInMillis); + } +} + +/// [TooManyRequestsManager] dedicated to "searchProducts" queries in PROD. +final TooManyRequestsManager searchProductsTooManyRequestsManager = + TooManyRequestsManager( + maxCount: 10, + duration: Duration(minutes: 1), +); + +/// [TooManyRequestsManager] dedicated to "getProduct" queries in PROD. +final TooManyRequestsManager getProductTooManyRequestsManager = + TooManyRequestsManager( + maxCount: 100, + duration: Duration(minutes: 1), +); diff --git a/test/api_get_localized_product_test.dart b/test/api_get_localized_product_test.dart index dac8bad6e6..be985d0052 100644 --- a/test/api_get_localized_product_test.dart +++ b/test/api_get_localized_product_test.dart @@ -7,6 +7,13 @@ void main() { OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; + Future getProductV3InProd( + ProductQueryConfiguration configuration, + ) async { + await getProductTooManyRequestsManager.waitIfNeeded(); + return OpenFoodAPIClient.getProductV3(configuration); + } + group('$OpenFoodAPIClient get localized product fields', () { test('get packaging text in languages (Coca-Cola)', () async { const String barcode = '5449000000996'; @@ -22,7 +29,7 @@ void main() { fields: [ProductField.PACKAGING_TEXT_IN_LANGUAGES], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -42,8 +49,7 @@ void main() { OpenFoodFactsLanguage.FRENCH, ]; - final ProductResultV3 productResult = - await OpenFoodAPIClient.getProductV3( + final ProductResultV3 productResult = await getProductV3InProd( ProductQueryConfiguration( BARCODE_DANISH_BUTTER_COOKIES, languages: languages, @@ -107,7 +113,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -265,7 +271,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -325,7 +331,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -396,7 +402,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -447,7 +453,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); diff --git a/test/api_get_product_image_ids_test.dart b/test/api_get_product_image_ids_test.dart index 4ba797f842..1873a68367 100644 --- a/test/api_get_product_image_ids_test.dart +++ b/test/api_get_product_image_ids_test.dart @@ -8,6 +8,7 @@ void main() { test('get product images (all, main and raw)', () async { const String barcode = '3019081238643'; + await getProductTooManyRequestsManager.waitIfNeeded(); final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( ProductQueryConfiguration( barcode, diff --git a/test/api_get_product_test.dart b/test/api_get_product_test.dart index f0126d96c1..6ebd4ea274 100644 --- a/test/api_get_product_test.dart +++ b/test/api_get_product_test.dart @@ -13,6 +13,13 @@ void main() { OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; + Future getProductV3InProd( + ProductQueryConfiguration configuration, + ) async { + await getProductTooManyRequestsManager.waitIfNeeded(); + return OpenFoodAPIClient.getProductV3(configuration); + } + void findExpectedIngredients( final List ingredients, final List labels, @@ -56,7 +63,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -85,7 +92,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.status, ProductResultV3.statusSuccess); @@ -119,7 +126,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -186,7 +193,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -210,7 +217,7 @@ void main() { ProductResultV3 result; late Nutriments nutriments; - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( '5060517883638', language: language, @@ -229,7 +236,7 @@ void main() { isNull, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( '7612100018477', language: language, @@ -245,7 +252,7 @@ void main() { ); expect(nutriments.getValue(Nutrient.biotin, PerSize.serving), isNull); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( '3057640257773', language: language, @@ -264,7 +271,7 @@ void main() { .015, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( '4260556630007', language: language, @@ -307,7 +314,7 @@ void main() { .00002, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( '3155251205319', language: language, @@ -344,7 +351,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -448,7 +455,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( configurations, ); expect(result.product, isNull); @@ -463,7 +470,7 @@ void main() { fields: [ProductField.ALL], version: ProductQueryVersion.v3, ); - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -483,7 +490,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -509,7 +516,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -539,7 +546,7 @@ void main() { fields: [ProductField.NAME, ProductField.LANGUAGE], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configurations, ); @@ -559,7 +566,7 @@ void main() { fields: [ProductField.NAME, ProductField.COUNTRIES], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configurations, ); @@ -581,7 +588,7 @@ void main() { fields: [ProductField.NAME, ProductField.COUNTRIES_TAGS], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configurations, ); @@ -606,7 +613,7 @@ void main() { fields: [ProductField.NAME, ProductField.ATTRIBUTE_GROUPS], version: ProductQueryVersion.v3, ); - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( configurations, ); @@ -656,7 +663,7 @@ void main() { const int numberOfImages = 53; // was 53 in 20231125 //Get product without setting OpenFoodFactsLanguage or ProductField - ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + ProductResultV3 result = await getProductV3InProd( ProductQueryConfiguration( barcode, version: ProductQueryVersion.v3, @@ -700,7 +707,7 @@ void main() { 'https://images.openfoodfacts.org/images/products/500/011/254/8167/ingredients_de.7.400.jpg'); //Get product without setting ProductField - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( barcode, language: OpenFoodFactsLanguage.GERMAN, @@ -753,7 +760,7 @@ void main() { 'https://images.openfoodfacts.org/images/products/500/011/254/8167/ingredients_de.7.400.jpg'); //Get product without setting OpenFoodFactsLanguage - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( ProductQueryConfiguration( barcode, fields: [ProductField.ALL], @@ -834,7 +841,7 @@ void main() { test( 'vegan, vegetarian and palm oil ingredients of Danish Butter Cookies & Chocolate Chip Cookies', () async { - final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( + final ProductResultV3 result = await getProductV3InProd( ProductQueryConfiguration( '3017620429484', language: OpenFoodFactsLanguage.FRENCH, @@ -860,8 +867,7 @@ void main() { 'nutriscore_2023', 'root', }; - final ProductResultV3 productResult = - await OpenFoodAPIClient.getProductV3( + final ProductResultV3 productResult = await getProductV3InProd( ProductQueryConfiguration( BARCODE_DANISH_BUTTER_COOKIES, language: OpenFoodFactsLanguage.FRENCH, @@ -1017,7 +1023,7 @@ void main() { fields: [ProductField.COMPARED_TO_CATEGORY], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configuration, ); expect(result.status, ProductResultV3.statusSuccess); @@ -1032,7 +1038,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configuration, ); expect(result.status, ProductResultV3.statusSuccess); @@ -1047,7 +1053,7 @@ void main() { fields: [ProductField.OBSOLETE], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configuration, ); expect(result.status, ProductResultV3.statusSuccess); @@ -1060,7 +1066,7 @@ void main() { fields: [ProductField.OBSOLETE], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configuration, ); expect(result.status, ProductResultV3.statusSuccess); @@ -1087,7 +1093,7 @@ void main() { ], version: ProductQueryVersion.v3, ); - result = await OpenFoodAPIClient.getProductV3( + result = await getProductV3InProd( configuration, ); expect(result.status, ProductResultV3.statusSuccess); @@ -1136,7 +1142,6 @@ void main() { group('$OpenFoodAPIClient get new packagings field', () { const String barcode = '3661344723290'; - const String searchTerms = 'skyr les 2 vaches'; const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; const OpenFoodFactsCountry country = OpenFoodFactsCountry.FRANCE; const ProductQueryVersion version = ProductQueryVersion.v3; @@ -1159,8 +1164,7 @@ void main() { } test('as a single field on a barcode search', () async { - final ProductResultV3 productResult = - await OpenFoodAPIClient.getProductV3( + final ProductResultV3 productResult = await getProductV3InProd( ProductQueryConfiguration( barcode, fields: [ProductField.PACKAGINGS], @@ -1175,8 +1179,7 @@ void main() { }); test('as a part of ALL fields on a barcode search', () async { - final ProductResultV3 productResult = - await OpenFoodAPIClient.getProductV3( + final ProductResultV3 productResult = await getProductV3InProd( ProductQueryConfiguration( barcode, fields: [ProductField.ALL], @@ -1189,79 +1192,5 @@ void main() { expect(productResult.product, isNotNull); checkProduct(productResult.product!); }); - - test('as a single field on a search query', () async { - final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( - null, - ProductSearchQueryConfiguration( - parametersList: [ - SearchTerms(terms: [searchTerms]) - ], - fields: [ProductField.PACKAGINGS, ProductField.BARCODE], - language: language, - country: country, - version: version, - ), - ); - expect(searchResult.products, isNotNull); - expect(searchResult.products, isNotEmpty); - bool found = false; - for (final Product product in searchResult.products!) { - if (product.barcode != barcode) { - continue; - } - found = true; - checkProduct(product); - } - expect(found, isTrue); - }); - - test('as a part of ALL fields on a search query', () async { - final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( - null, - ProductSearchQueryConfiguration( - parametersList: [ - SearchTerms(terms: [searchTerms]) - ], - fields: [ProductField.ALL], - language: language, - country: country, - version: version, - ), - ); - expect(searchResult.products, isNotNull); - expect(searchResult.products, isNotEmpty); - bool found = false; - for (final Product product in searchResult.products!) { - if (product.barcode != barcode) { - continue; - } - found = true; - checkProduct(product); - } - expect(found, isTrue); - }); - - test('as a part of RAW fields on a search query', () async { - try { - await OpenFoodAPIClient.searchProducts( - null, - ProductSearchQueryConfiguration( - parametersList: [ - SearchTerms(terms: [searchTerms]) - ], - fields: [ProductField.RAW], - language: language, - country: country, - version: version, - ), - ); - } catch (e) { - // In RAW mode the packagings are mere String's instead of LocalizedTag's. - // Therefore we expect an Exception. - return; - } - fail('On Raw'); - }); }); } diff --git a/test/api_get_to_be_completed_products_test.dart b/test/api_get_to_be_completed_products_test.dart deleted file mode 100644 index 2e46cf95b1..0000000000 --- a/test/api_get_to_be_completed_products_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:test/test.dart'; - -import 'test_constants.dart'; - -/// Integration tests related to the "to-be-completed" products -void main() { - OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; - - group('$OpenFoodAPIClient get all to-be-completed products', () { - Future getCount( - final OpenFoodFactsCountry country, - final OpenFoodFactsLanguage language, - final String store, - ) async { - final String reason = '($country, $language)'; - final ProductSearchQueryConfiguration configuration = - ProductSearchQueryConfiguration( - country: country, - language: language, - fields: [ - ProductField.BARCODE, - ProductField.STATES_TAGS, - ], - parametersList: [ - StatesTagsParameter(map: {ProductState.COMPLETED: false}), - TagFilter.fromType( - tagFilterType: TagFilterType.STORES, - tagName: store, - ), - ], - version: ProductQueryVersion.v3, - ); - - final SearchResult result; - try { - result = await OpenFoodAPIClient.searchProducts( - OpenFoodAPIConfiguration.globalUser, - configuration, - ); - } catch (e) { - fail('Could not retrieve data for $reason: $e'); - } - expect(result.page, 1, reason: reason); // default - expect(result.products, isNotNull, reason: reason); - for (final Product product in result.products!) { - expect(product.statesTags, isNotNull); - expect(product.statesTags!, contains('en:to-be-completed')); - } - return result.count; - } - - Future getCountForAllLanguages( - final OpenFoodFactsCountry country, - final String store, - ) async { - final List languages = [ - OpenFoodFactsLanguage.ENGLISH, - OpenFoodFactsLanguage.FRENCH, - OpenFoodFactsLanguage.ITALIAN, - ]; - int? result; - for (final OpenFoodFactsLanguage language in languages) { - final int? count = await getCount(country, language, store); - if (result != null) { - expect(count, result, reason: language.toString()); - } - result = count; - } - return result!; - } - - Future checkTypeCount( - final OpenFoodFactsCountry country, - final String store, - final int minimalExpectedCount, - ) async { - final int count = await getCountForAllLanguages(country, store); - expect(count, greaterThanOrEqualTo(minimalExpectedCount)); - } - - test( - 'in France', - () async => checkTypeCount( - OpenFoodFactsCountry.FRANCE, - 'Carrefour', - // 2023-08-12: was 14778 - 10000, - )); - - test( - 'in Italy', - () async => checkTypeCount( - OpenFoodFactsCountry.ITALY, - 'Carrefour', - // 2023-07-09: was 2394 - 1500, - )); - - test( - 'in Spain', - () async => checkTypeCount( - OpenFoodFactsCountry.SPAIN, - 'El Corte Inglès', - // 2023-07-09: was 608 - 500, - )); - }, - timeout: Timeout( - // some tests can be slow here - Duration(seconds: 180), - )); -} diff --git a/test/api_get_user_products_test.dart b/test/api_get_user_products_test.dart deleted file mode 100644 index a1803d0403..0000000000 --- a/test/api_get_user_products_test.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:test/test.dart'; - -import 'test_constants.dart'; - -void main() { - OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; - - group('$OpenFoodAPIClient get user products', () { - const String userId = 'monsieurtanuki'; - // should be big enough to get everything on page1 - const int pageSize = 100; - final String toBeCompletedTag = ProductState.COMPLETED.toBeCompletedTag; - - Future getCount( - final TagFilterType type, - final OpenFoodFactsLanguage language, - final bool toBeCompleted, { - final void Function(Product)? additionalCheck, - }) async { - final String reason = '($language, $type)'; - final ProductSearchQueryConfiguration configuration = - ProductSearchQueryConfiguration( - parametersList: [ - TagFilter.fromType(tagFilterType: type, tagName: userId), - PageSize(size: pageSize), - if (toBeCompleted) - TagFilter.fromType( - tagFilterType: TagFilterType.STATES, - tagName: toBeCompletedTag, - ), - ], - language: language, - fields: [ - ProductField.BARCODE, - ProductField.STATES_TAGS, - ], - version: ProductQueryVersion.v3, - ); - - final SearchResult result; - try { - result = await OpenFoodAPIClient.searchProducts( - OpenFoodAPIConfiguration.globalUser, - configuration, - ); - } catch (e) { - fail('Could not retrieve data for $reason: $e'); - } - expect(result.page, 1, reason: reason); // default - expect(result.pageSize, pageSize, reason: reason); - expect(result.products, isNotNull, reason: reason); - expect(result.products!.length, result.pageCount, reason: reason); - if (additionalCheck != null) { - for (final Product product in result.products!) { - additionalCheck(product); - } - } - return result.pageCount!; - } - - Future getCountForAllLanguages( - final TagFilterType type, - final bool toBeCompleted, { - final void Function(Product)? additionalCheck, - }) async { - final List languages = [ - OpenFoodFactsLanguage.ENGLISH, - OpenFoodFactsLanguage.FRENCH, - OpenFoodFactsLanguage.ITALIAN, - ]; - int? result; - for (final OpenFoodFactsLanguage language in languages) { - final int count = await getCount( - type, - language, - toBeCompleted, - additionalCheck: additionalCheck, - ); - if (result != null) { - expect(count, result, reason: language.toString()); - } - result = count; - } - return result!; - } - - Future checkTypeCount( - final TagFilterType type, - final int minimalExpectedCount, { - final void Function(Product)? additionalCheck, - final bool toBeCompleted = false, - }) async { - final int count = await getCountForAllLanguages( - type, - toBeCompleted, - additionalCheck: additionalCheck, - ); - expect(count, greaterThanOrEqualTo(minimalExpectedCount)); - } - - test( - 'contributor', - () async => checkTypeCount(TagFilterType.CREATOR, 2) // as of 20221229 - , - ); - - test( - 'informer', - () async => - await checkTypeCount(TagFilterType.INFORMERS, 73) // as of 20221229 - , - ); - - test( - 'photographer', - () async => - checkTypeCount(TagFilterType.PHOTOGRAPHERS, 48) // as of 20221229 - , - ); - - test( - 'to be completed', - () async => checkTypeCount( - TagFilterType.INFORMERS, 0, // you never know... - toBeCompleted: true, - additionalCheck: (final Product product) { - expect(product.statesTags, isNotNull); - expect(product.statesTags, contains(toBeCompletedTag)); - }, - ), - ); - }); -} diff --git a/test/api_json_to_from_test.dart b/test/api_json_to_from_test.dart index 3c18bb56b2..5d1841d73c 100644 --- a/test/api_json_to_from_test.dart +++ b/test/api_json_to_from_test.dart @@ -10,6 +10,7 @@ void main() { group('$OpenFoodAPIClient json to/from conversions', () { test('images', () async { + await getProductTooManyRequestsManager.waitIfNeeded(); final ProductResultV3 productResult = await OpenFoodAPIClient.getProductV3( ProductQueryConfiguration( diff --git a/test/api_matched_product_v1_test.dart b/test/api_matched_product_v1_test.dart index 8b17245cbf..bd37a28f1a 100644 --- a/test/api_matched_product_v1_test.dart +++ b/test/api_matched_product_v1_test.dart @@ -53,6 +53,7 @@ void main() { fields: [ProductField.NAME, ProductField.ATTRIBUTE_GROUPS], version: ProductQueryVersion.v3, ); + await getProductTooManyRequestsManager.waitIfNeeded(); final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( configurations, user: TestConstants.PROD_USER, diff --git a/test/api_matched_product_v2_test.dart b/test/api_matched_product_v2_test.dart deleted file mode 100644 index ecdd0d33a9..0000000000 --- a/test/api_matched_product_v2_test.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:http/http.dart' as http; -import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:test/test.dart'; - -import 'test_constants.dart'; - -class _Score { - _Score(this.score, this.status); - - final double score; - final MatchedProductStatusV2 status; -} - -void main() { - const int HTTP_OK = 200; - - const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; - OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; - OpenFoodAPIConfiguration.globalCountry = OpenFoodFactsCountry.FRANCE; - OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; - OpenFoodAPIConfiguration.globalLanguages = [language]; - - const String BARCODE_KNACKI = '7613035937420'; - const String BARCODE_CORDONBLEU = '4000405005026'; - const String BARCODE_ORIENTALES = '4032277007211'; - const String BARCODE_HACK = '7613037672756'; - const String BARCODE_SCHNITZEL = '4061458069878'; - const String BARCODE_CHIPOLATA = '3770016162098'; - const String BARCODE_FLEISCHWURST = '4003171036379'; // now veggie! - const String BARCODE_POULET = '40897837'; - const String BARCODE_SAUCISSON = '20045456'; - const String BARCODE_PIZZA = '4260414150470'; - const String BARCODE_ARDECHE = '20712570'; - const String BARCODE_CHORIZO = '8480000591074'; - - final List inputBarcodes = [ - BARCODE_CHIPOLATA, - BARCODE_FLEISCHWURST, - BARCODE_KNACKI, - BARCODE_CORDONBLEU, - BARCODE_SAUCISSON, - BARCODE_PIZZA, - BARCODE_ORIENTALES, - BARCODE_ARDECHE, - BARCODE_HACK, - BARCODE_CHORIZO, - BARCODE_SCHNITZEL, - BARCODE_POULET, - ]; - final Map expectedScores = { - BARCODE_KNACKI: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_CORDONBLEU: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_ORIENTALES: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_HACK: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_SCHNITZEL: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_CHIPOLATA: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_FLEISCHWURST: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), - BARCODE_POULET: _Score(0, MatchedProductStatusV2.UNKNOWN_MATCH), - BARCODE_SAUCISSON: _Score(0, MatchedProductStatusV2.UNKNOWN_MATCH), - BARCODE_PIZZA: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), - BARCODE_ARDECHE: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), - BARCODE_CHORIZO: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), - }; - final List expectedBarcodeOrder = [ - BARCODE_CHIPOLATA, - BARCODE_FLEISCHWURST, - BARCODE_KNACKI, - BARCODE_CORDONBLEU, - BARCODE_ORIENTALES, - BARCODE_HACK, - BARCODE_SCHNITZEL, - BARCODE_SAUCISSON, - BARCODE_POULET, - BARCODE_PIZZA, - BARCODE_ARDECHE, - BARCODE_CHORIZO, - ]; - - Future> downloadProducts() async { - final SearchResult result = await OpenFoodAPIClient.searchProducts( - OpenFoodAPIConfiguration.globalUser, - ProductSearchQueryConfiguration( - parametersList: [BarcodeParameter.list(inputBarcodes)], - language: language, - fields: [ProductField.BARCODE, ProductField.ATTRIBUTE_GROUPS], - version: ProductQueryVersion.v3, - ), - ); - expect(result.count, expectedScores.keys.length); - expect(result.page, 1); - expect(result.products, isNotNull); - final List products = result.products!; - // sorting them again by the input order - products.sort( - (final Product a, final Product b) => inputBarcodes - .indexOf(a.barcode!) - .compareTo(inputBarcodes.indexOf(b.barcode!)), - ); - expect(products.length, inputBarcodes.length); - return products; - } - - Future getManager() async { - final Map attributeImportances = {}; - final ProductPreferencesManager manager = ProductPreferencesManager( - ProductPreferencesSelection( - setImportance: (String attributeId, String importanceIndex) async { - attributeImportances[attributeId] = importanceIndex; - }, - getImportance: (String attributeId) => - attributeImportances[attributeId] ?? - PreferenceImportance.ID_NOT_IMPORTANT, - ), - ); - final String languageCode = language.code; - final String importanceUrl = - AvailablePreferenceImportances.getUrl(languageCode); - final String attributeGroupUrl = - AvailableAttributeGroups.getUrl(languageCode); - http.Response response; - response = await http.get(Uri.parse(importanceUrl)); - expect(response.statusCode, HTTP_OK); - final String preferenceImportancesString = response.body; - response = await http.get(Uri.parse(attributeGroupUrl)); - expect(response.statusCode, HTTP_OK); - final String attributeGroupsString = response.body; - manager.availableProductPreferences = - AvailableProductPreferences.loadFromJSONStrings( - preferenceImportancesString: preferenceImportancesString, - attributeGroupsString: attributeGroupsString, - ); - await manager.setImportance( - Attribute.ATTRIBUTE_VEGETARIAN, - PreferenceImportance.ID_MANDATORY, - ); - return manager; - } - - /// Tests around Matched Product v2. - group('$OpenFoodAPIClient matched product v2', () { - test('matched product', () async { - final ProductPreferencesManager manager = await getManager(); - - final List products = await downloadProducts(); - - final List actuals = - MatchedProductV2.sort(products, manager); - - expect(actuals.length, expectedBarcodeOrder.length); - for (int i = 0; i < actuals.length; i++) { - final MatchedProductV2 matched = actuals[i]; - final String barcode = expectedBarcodeOrder[i]; - expect(matched.product.barcode, barcode); - expect(matched.barcode, barcode); - expect(expectedScores[barcode], isNotNull); - final _Score score = expectedScores[barcode]!; - expect(matched.status, score.status); - expect(matched.score, score.score); - } - }); - - test('matched score', () async { - final ProductPreferencesManager manager = await getManager(); - - final List products = await downloadProducts(); - - final List actuals = []; - for (final Product product in products) { - actuals.add(MatchedScoreV2(product, manager)); - } - MatchedScoreV2.sort(actuals); - - expect(actuals.length, expectedBarcodeOrder.length); - for (int i = 0; i < actuals.length; i++) { - final MatchedScoreV2 matched = actuals[i]; - final String barcode = expectedBarcodeOrder[i]; - expect(matched.barcode, barcode); - expect(expectedScores[barcode], isNotNull); - final _Score score = expectedScores[barcode]!; - expect(matched.status, score.status); - expect(matched.score, score.score); - } - }); - }); -} diff --git a/test/api_not_food_get_product_test.dart b/test/api_not_food_get_product_test.dart index 4be4e4d682..41a44db4e5 100644 --- a/test/api_not_food_get_product_test.dart +++ b/test/api_not_food_get_product_test.dart @@ -34,6 +34,7 @@ void main() { fields: [ProductField.BARCODE], version: ProductQueryVersion(2), ); + await getProductTooManyRequestsManager.waitIfNeeded(); final OldProductResult result = await OpenFoodAPIClient.getOldProduct( configurations, uriHelper: uriHelper, diff --git a/test/api_ocr_ingredients_test.dart b/test/api_ocr_ingredients_test.dart index 19a4111d24..1251e8464f 100644 --- a/test/api_ocr_ingredients_test.dart +++ b/test/api_ocr_ingredients_test.dart @@ -125,6 +125,7 @@ void main() { fields: [ProductField.INGREDIENTS_TEXT], version: ProductQueryVersion.v3, ); + await getProductTooManyRequestsManager.waitIfNeeded(); final ProductResultV3 result = await OpenFoodAPIClient.getProductV3( configurations, user: TestConstants.PROD_USER, diff --git a/test/api_search_products_test.dart b/test/api_search_products_test.dart index e2edd196ec..21e0620248 100644 --- a/test/api_search_products_test.dart +++ b/test/api_search_products_test.dart @@ -1,16 +1,34 @@ import 'dart:math'; +import 'package:http/http.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:test/test.dart'; import 'test_constants.dart'; +class _Score { + _Score(this.score, this.status); + + final double score; + final MatchedProductStatusV2 status; +} + void main() { OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; const ProductQueryVersion version = ProductQueryVersion.v3; const int defaultPageSize = 50; + Future searchProductsInProd( + final AbstractQueryConfiguration configuration, + ) async { + await searchProductsTooManyRequestsManager.waitIfNeeded(); + return OpenFoodAPIClient.searchProducts( + TestConstants.PROD_USER, + configuration, + ); + } + // additional parameter for faster response time const Parameter optimParameter = SearchTerms(terms: ['pizza']); @@ -56,8 +74,7 @@ void main() { if (currentOption != null) SortBy(option: currentOption) ]; - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( ProductSearchQueryConfiguration( parametersList: parameters, fields: [ProductField.BARCODE], @@ -137,8 +154,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -165,8 +181,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -213,8 +228,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -341,8 +355,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -369,8 +382,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -400,8 +412,7 @@ void main() { version: version, ); - SearchResult result = await OpenFoodAPIClient.searchProducts( - null, + SearchResult result = await searchProductsInProd( configuration, ); @@ -430,8 +441,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -466,8 +476,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -551,8 +560,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -600,8 +608,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -629,8 +636,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -694,8 +700,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -711,6 +716,7 @@ void main() { }); test('product freshness', () async { + await searchProductsTooManyRequestsManager.waitIfNeeded(); final Map result = await OpenFoodAPIClient.getProductFreshness( barcodes: BARCODES, @@ -746,8 +752,9 @@ void main() { version: version, ); - final result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, configuration); + final result = await searchProductsInProd( + configuration, + ); if (result.products == null || result.products!.isEmpty) { break; } @@ -775,8 +782,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -804,8 +810,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -829,8 +834,7 @@ void main() { // single filters for (final int novaGroup in novaMinCounts.keys) { - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( ProductSearchQueryConfiguration( parametersList: [ TagFilter.fromType( @@ -867,7 +871,7 @@ void main() { }, timeout: Timeout( // some tests can be slow here - Duration(seconds: 90), + Duration(seconds: 300), )); /// Returns random and different int's. @@ -911,8 +915,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -1021,8 +1024,7 @@ void main() { version: version, ); - final SearchResult result = await OpenFoodAPIClient.searchProducts( - TestConstants.PROD_USER, + final SearchResult result = await searchProductsInProd( configuration, ); @@ -1111,4 +1113,499 @@ void main() { // some tests can be slow here Duration(seconds: 300), )); + + group('$OpenFoodAPIClient get all to-be-completed products', () { + Future getCount( + final OpenFoodFactsCountry country, + final OpenFoodFactsLanguage language, + final String store, + ) async { + final String reason = '($country, $language)'; + final ProductSearchQueryConfiguration configuration = + ProductSearchQueryConfiguration( + country: country, + language: language, + fields: [ + ProductField.BARCODE, + ProductField.STATES_TAGS, + ], + parametersList: [ + StatesTagsParameter(map: {ProductState.COMPLETED: false}), + TagFilter.fromType( + tagFilterType: TagFilterType.STORES, + tagName: store, + ), + ], + version: ProductQueryVersion.v3, + ); + + final SearchResult result; + try { + result = await searchProductsInProd( + configuration, + ); + } catch (e) { + fail('Could not retrieve data for $reason: $e'); + } + expect(result.page, 1, reason: reason); // default + expect(result.products, isNotNull, reason: reason); + for (final Product product in result.products!) { + expect(product.statesTags, isNotNull); + expect(product.statesTags!, contains('en:to-be-completed')); + } + return result.count; + } + + Future getCountForAllLanguages( + final OpenFoodFactsCountry country, + final String store, + ) async { + final List languages = [ + OpenFoodFactsLanguage.ENGLISH, + OpenFoodFactsLanguage.FRENCH, + OpenFoodFactsLanguage.ITALIAN, + ]; + int? result; + for (final OpenFoodFactsLanguage language in languages) { + final int? count = await getCount(country, language, store); + if (result != null) { + expect(count, result, reason: language.toString()); + } + result = count; + } + return result!; + } + + Future checkTypeCount( + final OpenFoodFactsCountry country, + final String store, + final int minimalExpectedCount, + ) async { + final int count = await getCountForAllLanguages(country, store); + expect(count, greaterThanOrEqualTo(minimalExpectedCount)); + } + + test( + 'in France', + () async => checkTypeCount( + OpenFoodFactsCountry.FRANCE, + 'Carrefour', + // 2023-08-12: was 14778 + 10000, + )); + + test( + 'in Italy', + () async => checkTypeCount( + OpenFoodFactsCountry.ITALY, + 'Carrefour', + // 2023-07-09: was 2394 + 1500, + )); + + test( + 'in Spain', + () async => checkTypeCount( + OpenFoodFactsCountry.SPAIN, + 'El Corte Inglès', + // 2023-07-09: was 608 + 500, + )); + }, + timeout: Timeout( + // some tests can be slow here + Duration(seconds: 180), + )); + + group('$OpenFoodAPIClient get user products', () { + const String userId = 'monsieurtanuki'; + // should be big enough to get everything on page1 + const int pageSize = 100; + final String toBeCompletedTag = ProductState.COMPLETED.toBeCompletedTag; + + Future getCount( + final TagFilterType type, + final OpenFoodFactsLanguage language, + final bool toBeCompleted, { + final void Function(Product)? additionalCheck, + }) async { + final String reason = '($language, $type)'; + final ProductSearchQueryConfiguration configuration = + ProductSearchQueryConfiguration( + parametersList: [ + TagFilter.fromType(tagFilterType: type, tagName: userId), + PageSize(size: pageSize), + if (toBeCompleted) + TagFilter.fromType( + tagFilterType: TagFilterType.STATES, + tagName: toBeCompletedTag, + ), + ], + language: language, + fields: [ + ProductField.BARCODE, + ProductField.STATES_TAGS, + ], + version: ProductQueryVersion.v3, + ); + + final SearchResult result; + try { + result = await searchProductsInProd( + configuration, + ); + } catch (e) { + fail('Could not retrieve data for $reason: $e'); + } + expect(result.page, 1, reason: reason); // default + expect(result.pageSize, pageSize, reason: reason); + expect(result.products, isNotNull, reason: reason); + expect(result.products!.length, result.pageCount, reason: reason); + if (additionalCheck != null) { + for (final Product product in result.products!) { + additionalCheck(product); + } + } + return result.pageCount!; + } + + Future getCountForAllLanguages( + final TagFilterType type, + final bool toBeCompleted, { + final void Function(Product)? additionalCheck, + }) async { + final List languages = [ + OpenFoodFactsLanguage.ENGLISH, + OpenFoodFactsLanguage.FRENCH, + OpenFoodFactsLanguage.ITALIAN, + ]; + int? result; + for (final OpenFoodFactsLanguage language in languages) { + final int count = await getCount( + type, + language, + toBeCompleted, + additionalCheck: additionalCheck, + ); + if (result != null) { + expect(count, result, reason: language.toString()); + } + result = count; + } + return result!; + } + + Future checkTypeCount( + final TagFilterType type, + final int minimalExpectedCount, { + final void Function(Product)? additionalCheck, + final bool toBeCompleted = false, + }) async { + final int count = await getCountForAllLanguages( + type, + toBeCompleted, + additionalCheck: additionalCheck, + ); + expect(count, greaterThanOrEqualTo(minimalExpectedCount)); + } + + test( + 'contributor', + () async => checkTypeCount(TagFilterType.CREATOR, 2) // as of 20221229 + , + ); + + test( + 'informer', + () async => + await checkTypeCount(TagFilterType.INFORMERS, 73) // as of 20221229 + , + ); + + test( + 'photographer', + () async => + checkTypeCount(TagFilterType.PHOTOGRAPHERS, 48) // as of 20221229 + , + ); + + test( + 'to be completed', + () async => checkTypeCount( + TagFilterType.INFORMERS, 0, // you never know... + toBeCompleted: true, + additionalCheck: (final Product product) { + expect(product.statesTags, isNotNull); + expect(product.statesTags, contains(toBeCompletedTag)); + }, + ), + ); + }, timeout: Timeout(Duration(seconds: 300))); + + group('$OpenFoodAPIClient get new packagings field', () { + const String barcode = '3661344723290'; + const String searchTerms = 'skyr les 2 vaches'; + const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; + const OpenFoodFactsCountry country = OpenFoodFactsCountry.FRANCE; + const ProductQueryVersion version = ProductQueryVersion.v3; + + void checkProduct(final Product product) { + void checkLocalizedTag(final LocalizedTag? tag) { + expect(tag, isNotNull); + expect(tag!.id, isNotNull); + expect(tag.lcName, isNotNull); + } + + expect(product.packagings, isNotNull); + expect(product.packagings!.length, greaterThanOrEqualTo(3)); + for (final ProductPackaging packaging in product.packagings!) { + checkLocalizedTag(packaging.shape); + checkLocalizedTag(packaging.material); + checkLocalizedTag(packaging.recycling); + expect(packaging.recycling!.id, 'en:recycle'); + } + } + + test('as a single field on a search query', () async { + final SearchResult searchResult = await searchProductsInProd( + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.PACKAGINGS, ProductField.BARCODE], + language: language, + country: country, + version: version, + ), + ); + expect(searchResult.products, isNotNull); + expect(searchResult.products, isNotEmpty); + bool found = false; + for (final Product product in searchResult.products!) { + if (product.barcode != barcode) { + continue; + } + found = true; + checkProduct(product); + } + expect(found, isTrue); + }); + + test('as a part of ALL fields on a search query', () async { + final SearchResult searchResult = await searchProductsInProd( + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.ALL], + language: language, + country: country, + version: version, + ), + ); + expect(searchResult.products, isNotNull); + expect(searchResult.products, isNotEmpty); + bool found = false; + for (final Product product in searchResult.products!) { + if (product.barcode != barcode) { + continue; + } + found = true; + checkProduct(product); + } + expect(found, isTrue); + }); + + test('as a part of RAW fields on a search query', () async { + try { + await searchProductsInProd( + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.RAW], + language: language, + country: country, + version: version, + ), + ); + } catch (e) { + // In RAW mode the packagings are mere String's instead of LocalizedTag's. + // Therefore we expect an Exception. + return; + } + fail('On Raw'); + }); + }, timeout: Timeout(Duration(minutes: 2))); + + group('$OpenFoodAPIClient matched product v2', () { + const int HTTP_OK = 200; + + const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; + OpenFoodAPIConfiguration.userAgent = TestConstants.TEST_USER_AGENT; + OpenFoodAPIConfiguration.globalCountry = OpenFoodFactsCountry.FRANCE; + OpenFoodAPIConfiguration.globalUser = TestConstants.PROD_USER; + OpenFoodAPIConfiguration.globalLanguages = [ + language + ]; + + const String BARCODE_KNACKI = '7613035937420'; + const String BARCODE_CORDONBLEU = '4000405005026'; + const String BARCODE_ORIENTALES = '4032277007211'; + const String BARCODE_HACK = '7613037672756'; + const String BARCODE_SCHNITZEL = '4061458069878'; + const String BARCODE_CHIPOLATA = '3770016162098'; + const String BARCODE_FLEISCHWURST = '4003171036379'; // now veggie! + const String BARCODE_POULET = '40897837'; + const String BARCODE_SAUCISSON = '20045456'; + const String BARCODE_PIZZA = '4260414150470'; + const String BARCODE_ARDECHE = '20712570'; + const String BARCODE_CHORIZO = '8480000591074'; + + final List inputBarcodes = [ + BARCODE_CHIPOLATA, + BARCODE_FLEISCHWURST, + BARCODE_KNACKI, + BARCODE_CORDONBLEU, + BARCODE_SAUCISSON, + BARCODE_PIZZA, + BARCODE_ORIENTALES, + BARCODE_ARDECHE, + BARCODE_HACK, + BARCODE_CHORIZO, + BARCODE_SCHNITZEL, + BARCODE_POULET, + ]; + final Map expectedScores = { + BARCODE_KNACKI: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_CORDONBLEU: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_ORIENTALES: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_HACK: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_SCHNITZEL: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_CHIPOLATA: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_FLEISCHWURST: _Score(100, MatchedProductStatusV2.VERY_GOOD_MATCH), + BARCODE_POULET: _Score(0, MatchedProductStatusV2.UNKNOWN_MATCH), + BARCODE_SAUCISSON: _Score(0, MatchedProductStatusV2.UNKNOWN_MATCH), + BARCODE_PIZZA: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), + BARCODE_ARDECHE: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), + BARCODE_CHORIZO: _Score(0, MatchedProductStatusV2.DOES_NOT_MATCH), + }; + final List expectedBarcodeOrder = [ + BARCODE_CHIPOLATA, + BARCODE_FLEISCHWURST, + BARCODE_KNACKI, + BARCODE_CORDONBLEU, + BARCODE_ORIENTALES, + BARCODE_HACK, + BARCODE_SCHNITZEL, + BARCODE_SAUCISSON, + BARCODE_POULET, + BARCODE_PIZZA, + BARCODE_ARDECHE, + BARCODE_CHORIZO, + ]; + + Future> downloadProducts() async { + final SearchResult result = await searchProductsInProd( + ProductSearchQueryConfiguration( + parametersList: [BarcodeParameter.list(inputBarcodes)], + language: language, + fields: [ProductField.BARCODE, ProductField.ATTRIBUTE_GROUPS], + version: ProductQueryVersion.v3, + ), + ); + expect(result.count, expectedScores.keys.length); + expect(result.page, 1); + expect(result.products, isNotNull); + final List products = result.products!; + // sorting them again by the input order + products.sort( + (final Product a, final Product b) => inputBarcodes + .indexOf(a.barcode!) + .compareTo(inputBarcodes.indexOf(b.barcode!)), + ); + expect(products.length, inputBarcodes.length); + return products; + } + + Future getManager() async { + final Map attributeImportances = {}; + final ProductPreferencesManager manager = ProductPreferencesManager( + ProductPreferencesSelection( + setImportance: (String attributeId, String importanceIndex) async { + attributeImportances[attributeId] = importanceIndex; + }, + getImportance: (String attributeId) => + attributeImportances[attributeId] ?? + PreferenceImportance.ID_NOT_IMPORTANT, + ), + ); + final String languageCode = language.code; + final String importanceUrl = + AvailablePreferenceImportances.getUrl(languageCode); + final String attributeGroupUrl = + AvailableAttributeGroups.getUrl(languageCode); + Response response; + response = await get(Uri.parse(importanceUrl)); + expect(response.statusCode, HTTP_OK); + final String preferenceImportancesString = response.body; + response = await get(Uri.parse(attributeGroupUrl)); + expect(response.statusCode, HTTP_OK); + final String attributeGroupsString = response.body; + manager.availableProductPreferences = + AvailableProductPreferences.loadFromJSONStrings( + preferenceImportancesString: preferenceImportancesString, + attributeGroupsString: attributeGroupsString, + ); + await manager.setImportance( + Attribute.ATTRIBUTE_VEGETARIAN, + PreferenceImportance.ID_MANDATORY, + ); + return manager; + } + + test('matched product', () async { + final ProductPreferencesManager manager = await getManager(); + + final List products = await downloadProducts(); + + final List actuals = + MatchedProductV2.sort(products, manager); + + expect(actuals.length, expectedBarcodeOrder.length); + for (int i = 0; i < actuals.length; i++) { + final MatchedProductV2 matched = actuals[i]; + final String barcode = expectedBarcodeOrder[i]; + expect(matched.product.barcode, barcode); + expect(matched.barcode, barcode); + expect(expectedScores[barcode], isNotNull); + final _Score score = expectedScores[barcode]!; + expect(matched.status, score.status); + expect(matched.score, score.score); + } + }); + + test('matched score', () async { + final ProductPreferencesManager manager = await getManager(); + + final List products = await downloadProducts(); + + final List actuals = []; + for (final Product product in products) { + actuals.add(MatchedScoreV2(product, manager)); + } + MatchedScoreV2.sort(actuals); + + expect(actuals.length, expectedBarcodeOrder.length); + for (int i = 0; i < actuals.length; i++) { + final MatchedScoreV2 matched = actuals[i]; + final String barcode = expectedBarcodeOrder[i]; + expect(matched.barcode, barcode); + expect(expectedScores[barcode], isNotNull); + final _Score score = expectedScores[barcode]!; + expect(matched.status, score.status); + expect(matched.score, score.score); + } + }); + }); }