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);
+ }
+ });
+ });
}