From 3a492d12e5ccb15258f94b653ce3676002519f13 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 16 Dec 2022 15:00:51 +0100 Subject: [PATCH] feat: 617 - new "save packagings V3" feature New files: * `api_saveProductV3_test.dart`: Integration tests around the "save packagings V3" feature. * `ProductPackaging.dart`: Packaging component for a product, e.g. recyclable bottle made of glass. * `ProductPackaging.g.dart`: generated Impacted files: * `AbstractQueryConfiguration.dart`: populated parameter `tags_lc` * `api_getProduct_test.dart`: removed references to now deprecated field `packaging`; added a test about the new packagings field * `api_saveProduct_test.dart`: skipped one 504 test; removed references to deprecated field `packaging` * `HttpHelper.dart`: new method `doPatchRequest`; minor refactoring * `openfoodfacts.dart`: new method `temporarySaveProductV3` * `Product.dart`: deprecated field `packaging`; added field `packagings` * `Product.g.dart`: generated * `ProductFields.dart`: deprecated `PACKAGING`; added `PACKAGINGS` and `RAW` * `ProductSearchQueryConfiguration.dart`: added api version parameter * `UriHelper.dart`: new method `getPatchUri` --- lib/model/Product.dart | 10 +- lib/model/Product.g.dart | 4 + lib/model/ProductPackaging.dart | 38 +++++ lib/model/ProductPackaging.g.dart | 36 +++++ lib/openfoodfacts.dart | 43 ++++++ lib/utils/AbstractQueryConfiguration.dart | 2 + lib/utils/HttpHelper.dart | 46 ++++-- lib/utils/ProductFields.dart | 6 + .../ProductSearchQueryConfiguration.dart | 8 +- lib/utils/UriHelper.dart | 16 ++ test/api_getProduct_test.dart | 139 +++++++++++++++++- test/api_saveProductV3_test.dart | 68 +++++++++ test/api_saveProduct_test.dart | 7 +- 13 files changed, 394 insertions(+), 29 deletions(-) create mode 100644 lib/model/ProductPackaging.dart create mode 100644 lib/model/ProductPackaging.g.dart create mode 100644 test/api_saveProductV3_test.dart diff --git a/lib/model/Product.dart b/lib/model/Product.dart index 2f96c733e4..b2b41829bc 100644 --- a/lib/model/Product.dart +++ b/lib/model/Product.dart @@ -3,6 +3,7 @@ import 'package:openfoodfacts/model/Attribute.dart'; import 'package:openfoodfacts/model/AttributeGroup.dart'; import 'package:openfoodfacts/model/KnowledgePanels.dart'; import 'package:openfoodfacts/model/ProductImage.dart'; +import 'package:openfoodfacts/model/ProductPackaging.dart'; import 'package:openfoodfacts/utils/JsonHelper.dart'; import 'package:openfoodfacts/utils/LanguageHelper.dart'; import 'package:openfoodfacts/utils/ProductFields.dart'; @@ -311,8 +312,14 @@ class Product extends JsonObject { includeIfNull: false) Map>? labelsTagsInLanguages; + // TODO: deprecated from 2022-12-16; remove when old enough + @Deprecated('User packagingS instead') @JsonKey(name: 'packaging', includeIfNull: false) String? packaging; + + @JsonKey(name: 'packagings', includeIfNull: false) + List? packagings; + @JsonKey(name: 'packaging_tags', includeIfNull: false) List? packagingTags; @JsonKey( @@ -468,7 +475,8 @@ class Product extends JsonObject { this.labels, this.labelsTags, this.labelsTagsInLanguages, - this.packaging, + // TODO: deprecated from 2022-12-16; remove when old enough + @Deprecated('Use packagingS field instead') this.packaging, this.packagingTags, this.miscTags, this.statesTags, diff --git a/lib/model/Product.g.dart b/lib/model/Product.g.dart index cca85114cc..ccb3016687 100644 --- a/lib/model/Product.g.dart +++ b/lib/model/Product.g.dart @@ -112,6 +112,9 @@ Product _$ProductFromJson(Map json) => Product( ) ..nutritionData = JsonHelper.checkboxFromJSON(json['nutrition_data']) ..comparedToCategory = json['compared_to_category'] as String? + ..packagings = (json['packagings'] as List?) + ?.map((e) => ProductPackaging.fromJson(e)) + .toList() ..packagingTextInLanguages = LanguageHelper.fromJsonStringMap(json['packaging_text_in_languages']) ..lastModifiedBy = json['last_modified_by'] as String? @@ -202,6 +205,7 @@ Map _$ProductToJson(Product instance) { writeNotNull('labels_tags_in_languages', LanguageHelper.toJsonStringsListMap(instance.labelsTagsInLanguages)); writeNotNull('packaging', instance.packaging); + writeNotNull('packagings', instance.packagings); writeNotNull('packaging_tags', instance.packagingTags); writeNotNull('packaging_text_in_languages', LanguageHelper.toJsonStringMap(instance.packagingTextInLanguages)); diff --git a/lib/model/ProductPackaging.dart b/lib/model/ProductPackaging.dart new file mode 100644 index 0000000000..0ede575adb --- /dev/null +++ b/lib/model/ProductPackaging.dart @@ -0,0 +1,38 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:openfoodfacts/model/LocalizedTag.dart'; +import '../interface/JsonObject.dart'; + +part 'ProductPackaging.g.dart'; + +/// Packaging component for a product, e.g. recyclable bottle made of glass. +@JsonSerializable() +class ProductPackaging extends JsonObject { + @JsonKey(includeIfNull: false) + LocalizedTag? shape; + + @JsonKey(includeIfNull: false) + LocalizedTag? material; + + @JsonKey(includeIfNull: false) + LocalizedTag? recycling; + + @JsonKey( + name: 'number_of_units', + includeIfNull: false, + fromJson: JsonObject.parseInt, + ) + int? numberOfUnits; + + ProductPackaging(); + + factory ProductPackaging.fromJson(dynamic json) => + _$ProductPackagingFromJson(json); + + @override + Map toJson() => _$ProductPackagingToJson(this); + + Map toServerData() => JsonObject.toDataStatic(toJson()); + + @override + String toString() => toServerData().toString(); +} diff --git a/lib/model/ProductPackaging.g.dart b/lib/model/ProductPackaging.g.dart new file mode 100644 index 0000000000..2ce5de3073 --- /dev/null +++ b/lib/model/ProductPackaging.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ProductPackaging.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProductPackaging _$ProductPackagingFromJson(Map json) => + ProductPackaging() + ..shape = json['shape'] == null + ? null + : LocalizedTag.fromJson(json['shape'] as Map) + ..material = json['material'] == null + ? null + : LocalizedTag.fromJson(json['material'] as Map) + ..recycling = json['recycling'] == null + ? null + : LocalizedTag.fromJson(json['recycling'] as Map) + ..numberOfUnits = JsonObject.parseInt(json['number_of_units']); + +Map _$ProductPackagingToJson(ProductPackaging instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('shape', instance.shape); + writeNotNull('material', instance.material); + writeNotNull('recycling', instance.recycling); + writeNotNull('number_of_units', instance.numberOfUnits); + return val; +} diff --git a/lib/openfoodfacts.dart b/lib/openfoodfacts.dart index d721a27bfb..9dfdcce91c 100644 --- a/lib/openfoodfacts.dart +++ b/lib/openfoodfacts.dart @@ -6,6 +6,7 @@ import 'dart:developer'; import 'package:http/http.dart'; import 'package:openfoodfacts/interface/JsonObject.dart'; +import 'package:openfoodfacts/model/ProductPackaging.dart'; import 'package:openfoodfacts/model/ProductResultV3.dart'; import 'package:openfoodfacts/model/KnowledgePanels.dart'; import 'package:openfoodfacts/model/LoginStatus.dart'; @@ -147,6 +148,48 @@ class OpenFoodAPIClient { return Status.fromApiResponse(response.body); } + /// Temporary: saves product packagings V3 style. + /// + /// For the moment that's the only field supported in WRITE by API V3. + /// Long term target is of course more something like [saveProduct]. + static Future temporarySaveProductV3( + final User user, + final String barcode, { + final List? packagings, + final QueryType? queryType, + final OpenFoodFactsCountry? country, + final OpenFoodFactsLanguage? language, + }) async { + final Map parameterMap = {}; + parameterMap.addAll(user.toData()); + if (packagings == null) { + // For the moment it's the only purpose of this method: saving packagings. + throw Exception('packagings cannot be null'); + } + parameterMap['product'] = {}; + parameterMap['product']['packagings'] = packagings; + if (language != null) { + parameterMap['lc'] = language.offTag; + parameterMap['tags_lc'] = language.offTag; + } + if (country != null) { + parameterMap['cc'] = country.offTag; + } + + var productUri = UriHelper.getPatchUri( + path: '/api/v3/product/$barcode', + queryType: queryType, + ); + + final Response response = await HttpHelper().doPatchRequest( + productUri, + parameterMap, + user, + queryType: queryType, + ); + return ProductResultV3.fromJson(jsonDecode(response.body)); + } + /// Send one image to the server. /// The image will be added to the product specified in the SendImage /// Returns a Status object as result. diff --git a/lib/utils/AbstractQueryConfiguration.dart b/lib/utils/AbstractQueryConfiguration.dart index f558c92420..508b071e09 100644 --- a/lib/utils/AbstractQueryConfiguration.dart +++ b/lib/utils/AbstractQueryConfiguration.dart @@ -75,6 +75,8 @@ abstract class AbstractQueryConfiguration { if (queryLanguages.isNotEmpty) { result.putIfAbsent( 'lc', () => queryLanguages.map((e) => e.offTag).join(',')); + // the result looks like crap if we put several languages + result.putIfAbsent('tags_lc', () => queryLanguages.first.offTag); } final String? countryCode = computeCountryCode(); diff --git a/lib/utils/HttpHelper.dart b/lib/utils/HttpHelper.dart index ac13a3cf17..6e0f4c2d92 100644 --- a/lib/utils/HttpHelper.dart +++ b/lib/utils/HttpHelper.dart @@ -29,10 +29,10 @@ class HttpHelper { static const String FROM = 'anonymous'; /// Adds user agent data to parameters, for statistics purpose - static Map? addUserAgentParameters( - Map? map, + static Map? addUserAgentParameters( + Map? map, ) { - map ??= {}; + map ??= {}; if (OpenFoodAPIConfiguration.userAgent?.name != null) { map['app_name'] = OpenFoodAPIConfiguration.userAgent!.name!; } @@ -70,9 +70,7 @@ class HttpHelper { headers: _buildHeaders( user: user, isTestModeActive: - OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD - ? false - : true, + OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD, ), ); @@ -105,9 +103,7 @@ class HttpHelper { headers: _buildHeaders( user: user, isTestModeActive: - OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD - ? false - : true, + OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD, addCredentialsToHeader: addCredentialsToHeader, ), body: addUserAgentParameters(body), @@ -115,6 +111,29 @@ class HttpHelper { return response; } + static const String userInfoForTest = 'off:off'; + + /// Send a http PATCH request to the specified uri. + /// + /// The data / body of the request has to be provided as map. (key, value) + /// The result of the request will be returned as string. + Future doPatchRequest( + final Uri uri, + final Map body, + final User? user, { + final QueryType? queryType, + }) async => + http.patch( + uri, + headers: _buildHeaders( + user: user, + isTestModeActive: OpenFoodAPIConfiguration.getQueryType(queryType) != + QueryType.PROD, + addCredentialsToHeader: false, + ), + body: jsonEncode(addUserAgentParameters(body)), + ); + /// Send a multipart post request to the specified uri. /// The data / body of the request has to be provided as map. (key, value) /// The files to send have to be provided as map containing the source file uri. @@ -132,14 +151,13 @@ class HttpHelper { _buildHeaders( user: user, isTestModeActive: - OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD - ? false - : true, + OpenFoodAPIConfiguration.getQueryType(queryType) != QueryType.PROD, ) as Map, ); request.headers.addAll({'Content-Type': 'multipart/form-data'}); - request.fields.addAll(addUserAgentParameters(body)!); + addUserAgentParameters(body); + request.fields.addAll(body); if (user != null) { request.fields.addAll(user.toData()); } @@ -192,7 +210,7 @@ class HttpHelper { }); if (isTestModeActive && !addCredentialsToHeader) { - var token = 'Basic ${base64Encode(utf8.encode('off:off'))}'; + var token = 'Basic ${base64Encode(utf8.encode(userInfoForTest))}'; headers.addAll({'Authorization': token}); } diff --git a/lib/utils/ProductFields.dart b/lib/utils/ProductFields.dart index 30bba04b89..09a4cb113b 100644 --- a/lib/utils/ProductFields.dart +++ b/lib/utils/ProductFields.dart @@ -51,7 +51,10 @@ enum ProductField implements OffTagged { LABELS(offTag: 'labels'), LABELS_TAGS(offTag: 'labels_tags'), LABELS_TAGS_IN_LANGUAGES(offTag: 'labels_tags_'), + // TODO: deprecated from 2022-12-16; remove when old enough + @Deprecated('Use packagingS field instead') PACKAGING(offTag: 'packaging'), + PACKAGINGS(offTag: 'packagings'), PACKAGING_TAGS(offTag: 'packaging_tags'), PACKAGING_TEXT_IN_LANGUAGES(offTag: 'packaging_text_'), PACKAGING_TEXT_ALL_LANGUAGES(offTag: 'packaging_text_languages'), @@ -85,6 +88,9 @@ enum ProductField implements OffTagged { ORIGINS(offTag: 'origins'), NOVA_GROUP(offTag: 'nova_group'), WEBSITE(offTag: 'link'), + + /// All data as RAW from the server. E.g. packagings are only Strings there. + RAW(offTag: 'raw'), ALL(offTag: ''); const ProductField({ diff --git a/lib/utils/ProductSearchQueryConfiguration.dart b/lib/utils/ProductSearchQueryConfiguration.dart index e1ec4967e3..310e998e73 100644 --- a/lib/utils/ProductSearchQueryConfiguration.dart +++ b/lib/utils/ProductSearchQueryConfiguration.dart @@ -1,8 +1,6 @@ -import 'package:openfoodfacts/interface/Parameter.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:openfoodfacts/utils/AbstractQueryConfiguration.dart'; import 'package:openfoodfacts/utils/CountryHelper.dart'; -import 'package:openfoodfacts/utils/LanguageHelper.dart'; -import 'package:openfoodfacts/utils/ProductFields.dart'; /// Query Configuration for search parameters class ProductSearchQueryConfiguration extends AbstractQueryConfiguration { @@ -14,6 +12,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration { final OpenFoodFactsCountry? country, final List? fields, required List parametersList, + this.version = ProductQueryVersion.v3, }) : super( language: language, languages: languages, @@ -22,6 +21,8 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration { additionalParameters: parametersList, ); + final ProductQueryVersion version; + List getFieldsKeys() { List result = []; @@ -38,6 +39,7 @@ class ProductSearchQueryConfiguration extends AbstractQueryConfiguration { result.putIfAbsent('search_terms', () => ''); // explicit json output result.putIfAbsent('json', () => '1'); + result.putIfAbsent('api_version', () => '${version.version}'); return result; } diff --git a/lib/utils/UriHelper.dart b/lib/utils/UriHelper.dart index 7079101899..652993c5bd 100644 --- a/lib/utils/UriHelper.dart +++ b/lib/utils/UriHelper.dart @@ -18,6 +18,7 @@ class UriHelper { final Map? queryParameters, final QueryType? queryType, final bool addUserAgentParameters = true, + final String? userInfo, }) => Uri( scheme: OpenFoodAPIConfiguration.uriScheme, @@ -28,6 +29,7 @@ class UriHelper { queryParameters: addUserAgentParameters ? HttpHelper.addUserAgentParameters(queryParameters) : queryParameters, + userInfo: userInfo, ); static Uri getPostUri({ @@ -36,6 +38,20 @@ class UriHelper { }) => getUri(path: path, queryType: queryType, addUserAgentParameters: false); + static Uri getPatchUri({ + required final String path, + final QueryType? queryType, + }) => + getUri( + path: path, + queryType: queryType, + addUserAgentParameters: false, + userInfo: + OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD + ? null + : HttpHelper.userInfoForTest, + ); + ///Returns a OFF-Robotoff uri with the in the [OpenFoodAPIConfiguration] specified settings static Uri getRobotoffUri({ required final String path, diff --git a/test/api_getProduct_test.dart b/test/api_getProduct_test.dart index 251de36491..b9e4a60a68 100644 --- a/test/api_getProduct_test.dart +++ b/test/api_getProduct_test.dart @@ -1,11 +1,14 @@ import 'package:http/http.dart' as http; import 'package:openfoodfacts/model/Attribute.dart'; import 'package:openfoodfacts/model/AttributeGroup.dart'; +import 'package:openfoodfacts/model/LocalizedTag.dart'; import 'package:openfoodfacts/model/Nutrient.dart'; import 'package:openfoodfacts/model/NutrientLevels.dart'; import 'package:openfoodfacts/model/Nutriments.dart'; import 'package:openfoodfacts/model/PerSize.dart'; import 'package:openfoodfacts/model/ProductResultV3.dart'; +import 'package:openfoodfacts/model/parameter/SearchTerms.dart'; +import 'package:openfoodfacts/model/ProductPackaging.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:openfoodfacts/utils/CountryHelper.dart'; import 'package:openfoodfacts/utils/InvalidBarcodes.dart'; @@ -1801,7 +1804,6 @@ void main() { lang: OpenFoodFactsLanguage.GERMAN, genericName: 'Softdrink', labels: 'MyTestLabel', - packaging: 'de:In einer Plastikflasche', quantity: '5.5 Liter', ); @@ -1816,8 +1818,6 @@ void main() { fields: [ ProductField.GENERIC_NAME, ProductField.LABELS, - ProductField.PACKAGING, - ProductField.PACKAGING_TAGS, ProductField.QUANTITY, ], version: ProductQueryVersion.v3, @@ -1832,8 +1832,6 @@ void main() { expect(result.product?.barcode, null); expect(result.product?.genericName, 'Softdrink'); expect(result.product?.labels, 'MyTestLabel'); - expect(result.product?.packaging, 'de:In einer Plastikflasche'); - expect(result.product?.packagingTags, ['de:in-einer-plastikflasche']); expect(result.product?.quantity, '5.5 Liter'); }, timeout: Timeout( @@ -1919,6 +1917,137 @@ void main() { ); // value on 2022-12-05 }); + test('get new packagings field', () async { + const String barcode = '3661344723290'; + const String searchTerms = 'skyr les 2 vaches'; + const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; + const ProductQueryVersion version = ProductQueryVersion.v3; + const QueryType queryType = QueryType.PROD; + const User user = TestConstants.PROD_USER; + + late ProductResultV3 productResult; + late SearchResult searchResult; + + 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, 3); + for (final ProductPackaging packaging in product.packagings!) { + checkLocalizedTag(packaging.shape); + checkLocalizedTag(packaging.material); + checkLocalizedTag(packaging.recycling); + expect(packaging.recycling!.id, 'en:recycle'); + } + } + + // checking PACKAGINGS as a single field on a barcode search + productResult = await OpenFoodAPIClient.getProductV3( + ProductQueryConfiguration( + barcode, + fields: [ProductField.PACKAGINGS], + language: language, + version: version, + ), + queryType: queryType, + user: user, + ); + expect(productResult.status, ProductResultV3.statusSuccess); + expect(productResult.product, isNotNull); + checkProduct(productResult.product!); + + // checking PACKAGINGS as a part of ALL fields on a barcode search + productResult = await OpenFoodAPIClient.getProductV3( + ProductQueryConfiguration( + barcode, + fields: [ProductField.ALL], + language: language, + version: version, + ), + queryType: queryType, + user: user, + ); + expect(productResult.status, ProductResultV3.statusSuccess); + expect(productResult.product, isNotNull); + checkProduct(productResult.product!); + + late bool found; + // checking PACKAGINGS as a single field on a search query + searchResult = await OpenFoodAPIClient.searchProducts( + user, + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.PACKAGINGS, ProductField.BARCODE], + language: language, + version: version, + ), + queryType: queryType, + ); + expect(searchResult.products, isNotNull); + expect(searchResult.products, isNotEmpty); + found = false; + for (final Product product in searchResult.products!) { + if (product.barcode != barcode) { + continue; + } + found = true; + checkProduct(product); + } + expect(found, isTrue); + + // checking PACKAGINGS as a part of ALL fields on a search query + searchResult = await OpenFoodAPIClient.searchProducts( + user, + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.ALL], + language: language, + version: version, + ), + queryType: queryType, + ); + expect(searchResult.products, isNotNull); + expect(searchResult.products, isNotEmpty); + found = false; + for (final Product product in searchResult.products!) { + if (product.barcode != barcode) { + continue; + } + found = true; + checkProduct(product); + } + expect(found, isTrue); + + // checking PACKAGINGS as a part of RAW fields on a search query + try { + searchResult = await OpenFoodAPIClient.searchProducts( + user, + ProductSearchQueryConfiguration( + parametersList: [ + SearchTerms(terms: [searchTerms]) + ], + fields: [ProductField.RAW], + language: language, + version: version, + ), + queryType: queryType, + ); + } catch (e) { + // In RAW mode the packagings are mere String's instead of LocalizedTag's. + // Therefore we expect an Exception. + return; + } + fail('On Raw'); + }); + group('no nutrition data', () { // This is barcode refers to a test product const String barcode = '111111555555'; diff --git a/test/api_saveProductV3_test.dart b/test/api_saveProductV3_test.dart new file mode 100644 index 0000000000..7800cea9ff --- /dev/null +++ b/test/api_saveProductV3_test.dart @@ -0,0 +1,68 @@ +import 'package:openfoodfacts/model/LocalizedTag.dart'; +import 'package:openfoodfacts/model/ProductPackaging.dart'; +import 'package:openfoodfacts/model/ProductResultFieldAnswer.dart'; +import 'package:openfoodfacts/model/ProductResultV3.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:openfoodfacts/utils/CountryHelper.dart'; +import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart'; +import 'package:openfoodfacts/utils/QueryType.dart'; +import 'package:test/test.dart'; + +import 'test_constants.dart'; + +/// Integration tests around the "save packagings V3" feature. +void main() { + OpenFoodAPIConfiguration.globalQueryType = QueryType.TEST; + + group('$OpenFoodAPIClient save product V3', () { + const String barcode = '12345678'; + + test('save packagings with unknown recycling', () async { + // Here we put an unknown recycling label, and we expect related warnings. + const String unknownRecycling = 'recyKKlage'; + const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH; + final List inputPackagings = [ + ProductPackaging() + ..shape = (LocalizedTag()..lcName = 'bouteille') + ..material = (LocalizedTag()..lcName = 'verre') + ..recycling = (LocalizedTag()..lcName = unknownRecycling) + ..numberOfUnits = 12, + ]; + final ProductResultV3 status = + await OpenFoodAPIClient.temporarySaveProductV3( + TestConstants.TEST_USER, + barcode, + queryType: QueryType.TEST, + country: OpenFoodFactsCountry.FRANCE, + language: language, + packagings: inputPackagings, + ); + + expect(status.status, ProductResultV3.statusWarning); + expect(status.errors, isEmpty); + expect(status.result, isNull); // result is null for UPDATE queries + expect(status.barcode, barcode); + expect(status.product, isNotNull); + + expect(status.warnings, isNotEmpty); + expect(status.warnings, hasLength(1)); + + final ProductResultFieldAnswer answer = status.warnings!.first; + expect(answer.impact, isNotNull); + expect(answer.impact!.id, 'none'); + expect(answer.impact!.name, isNotNull); + expect(answer.impact!.lcName, isNotNull); + expect(answer.message, isNotNull); + expect(answer.message!.id, 'unrecognized_value'); + expect(answer.message!.name, isNotNull); + expect(answer.message!.lcName, isNotNull); + expect(answer.field, isNotNull); + expect(answer.field!.id, 'recycling'); + expect(answer.field!.value, '${language.offTag}:$unknownRecycling'); + }); + }, + timeout: Timeout( + // some tests can be slow here + Duration(seconds: 90), + )); +} diff --git a/test/api_saveProduct_test.dart b/test/api_saveProduct_test.dart index 897e1f9241..0b7ad18ef8 100644 --- a/test/api_saveProduct_test.dart +++ b/test/api_saveProduct_test.dart @@ -216,7 +216,7 @@ Like that: expect(status.status, 1); expect(status.statusVerbose, 'fields saved'); - }); + }, skip: 'Too often 504 Gateway Time-out'); test('add new product test 3', () async { Product product = Product( @@ -283,7 +283,6 @@ Like that: Product product = Product( barcode: '7340011364184', categories: 'Product categories test 1,Product categories test 2', - packaging: 'Product packaging test 1,Product packaging test 2', labels: 'Product labels test 1,Product labels test 2'); Status status = await OpenFoodAPIClient.saveProduct( @@ -307,10 +306,6 @@ Like that: 'Product labels test 1,Product labels test 2'); expect(result.product!.labelsTags, ['en:product-labels-test-1', 'en:product-labels-test-2']); - expect(result.product!.packaging, - 'Product packaging test 1,Product packaging test 2'); - expect(result.product!.packagingTags, - ['en:product-packaging-test-1', 'en:product-packaging-test-2']); expect(result.product!.categories, 'Product categories test 1,Product categories test 2'); expect(result.product!.categoriesTags,