diff --git a/cgi/display.pl b/cgi/display.pl
index 77d2807789e86..7e19e245a54e8 100755
--- a/cgi/display.pl
+++ b/cgi/display.pl
@@ -40,45 +40,63 @@
use Apache2::RequestRec ();
use Apache2::Const qw(:common);
-# The API V3 write product request uses POST / PUT / PATCH with a JSON body
-# if we have such a request, we need to read the body before CGI.pm tries to read it to get multipart/form-data parameters
-
my $env_query_string = $ENV{QUERY_STRING};
-$log->debug("display.pl - start", {env_query_string => $env_query_string});
+my $request_ref = {};
-my $body_request_ref = {};
+my $r = Apache2::RequestUtil->request();
+$request_ref->{method} = $r->method();
-if ($env_query_string =~ /^\/?api\/v3(\.\d+)?\/product/) {
- read_request_body($body_request_ref);
-}
+$log->debug("display.pl - start", {env_query_string => $env_query_string, request_ref => $request_ref})
+ if $log->is_debug();
-# The nginx reverse proxy turns /somepath?someparam=somevalue to /cgi/display.pl?/somepath?someparam=somevalue
-# so that all non /cgi/ queries are sent to display.pl and that we can get the path in the query string
-# CGI.pm thus adds somepath? at the start of the name of the first parameter.
-# we need to remove it so that we can use the CGI.pm param() function to later access the parameters
-
-my @params = multi_param();
-if (defined $params[0]) {
- my $first_param = $params[0];
- my $first_param_value = single_param($first_param);
- $log->debug(
- "replacing first param to remove path from parameter name",
- {first_param => $first_param, $first_param_value => $first_param_value}
- );
- CGI::delete($first_param);
- $first_param =~ s/^(.*?)\?//;
- param($first_param, $first_param_value);
-}
+# Special behaviors for API v3 requests
+
+if ($env_query_string =~ /^\/?api\/v(3(\.\d+)?)\//) {
+
+ # Record that we have an API v3 request, as errors (e.g. bad userid and password) will be handled differently
+ # (through API::process_api_request instead of returning an error page in HTML)
+ $request_ref->{api_version} = $1;
-my $request_ref = ProductOpener::Display::init_request();
+ # Initialize the api_response field for errors and warnings
+ init_api_response($request_ref);
-# Add the HTTP request body if we have one
-if (defined $body_request_ref->{body}) {
- $request_ref->{body} = $body_request_ref->{body};
+ # The API V3 write product request uses POST / PUT / PATCH with a JSON body
+ # if we have such a request, we need to read the body before CGI.pm tries to read it to get multipart/form-data parameters
+ # We also need to do this before the call to init_request() which calls init_user()
+ # so that authentification credentials user_id and password from the JSON body can be used to authenticate the user
+ if ($request_ref->{method} =~ /^(POST|PUT|PATCH)$/) {
+ read_request_body($request_ref);
+ decode_json_request_body($request_ref);
+ }
}
-$log->debug("before analyze_request", {query_string => $request_ref->{query_string}});
+if (($env_query_string !~ /^\/?api\/v(3(\.\d+)?)\//) or ($request_ref->{method} !~ /^(POST|PUT|PATCH)$/)) {
+ # Not an API v3 POST/PUT/PATCH request: we will use CGI.pm param() method to access query string or multipart/form-data parameters
+
+ # The nginx reverse proxy turns /somepath?someparam=somevalue to /cgi/display.pl?/somepath?someparam=somevalue
+ # so that all non /cgi/ queries are sent to display.pl and that we can get the path in the query string
+ # CGI.pm thus adds somepath? at the start of the name of the first parameter.
+ # we need to remove it so that we can use the CGI.pm param() function to later access the parameters
+
+ my @params = multi_param();
+ if (defined $params[0]) {
+ my $first_param = $params[0];
+ my $first_param_value = single_param($first_param);
+ $log->debug(
+ "replacing first param to remove path from parameter name",
+ {first_param => $first_param, $first_param_value => $first_param_value}
+ );
+ CGI::delete($first_param);
+ $first_param =~ s/^(.*?)\?//;
+ param($first_param, $first_param_value);
+ }
+}
+
+# Initialize the request object, and authenticate the user
+init_request($request_ref);
+
+$log->debug("before analyze_request", {query_string => $request_ref->{query_string}}) if $log->is_debug();
# analyze request will fill request with action and parameters
analyze_request($request_ref);
@@ -101,7 +119,7 @@
user => $request_ref->{user},
query => $request_ref->{query}
}
-);
+) if $log->is_debug();
# Only display texts if products are private and no owner is defined
if (
diff --git a/docs/reference/api-v3.yml b/docs/reference/api-v3.yml
index b22886f2f031c..a744cd4d3ebea 100644
--- a/docs/reference/api-v3.yml
+++ b/docs/reference/api-v3.yml
@@ -59,12 +59,12 @@ paths:
name: fields
description: |-
Comma separated list of fields requested in the response.
-
+
Special values:
* "none": returns no fields
* "raw": returns all fields as stored internally in the database
* "all": returns all fields except generated fields that need to be explicitly requested such as "knowledge_panels".
-
+
Defaults to "all" for READ requests. The "all" value can also be combined with fields like "attribute_groups" and "knowledge_panels".'
responses:
'200':
@@ -176,7 +176,7 @@ paths:
- $ref: ./requestBodies/fields_tags_lc.yaml
- type: object
properties:
- userid:
+ user_id:
type: string
password:
type: string
diff --git a/lib/ProductOpener/API.pm b/lib/ProductOpener/API.pm
index b19b96c60f3aa..a965c2747158b 100644
--- a/lib/ProductOpener/API.pm
+++ b/lib/ProductOpener/API.pm
@@ -43,6 +43,7 @@ use Log::Any qw($log);
BEGIN {
use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS);
@EXPORT_OK = qw(
+ &init_api_response
&get_initialized_response
&add_warning
&add_error
@@ -90,6 +91,8 @@ sub get_initialized_response() {
sub init_api_response ($request_ref) {
$request_ref->{api_response} = get_initialized_response();
+
+ $log->debug("init_api_response - done", {request => $request_ref}) if $log->is_debug();
return $request_ref->{api_response};
}
@@ -170,7 +173,7 @@ sub decode_json_request_body ($request_ref) {
);
}
else {
- eval {$request_ref->{request_body_json} = decode_json($request_ref->{body});};
+ eval {$request_ref->{body_json} = decode_json($request_ref->{body});};
if ($@) {
$log->error("JSON decoding error", {error => $@}) if $log->is_error();
add_error(
@@ -340,41 +343,49 @@ sub process_api_request ($request_ref) {
$log->debug("process_api_request - start", {request => $request_ref}) if $log->is_debug();
- my $response_ref = init_api_response($request_ref);
+ my $response_ref = $request_ref->{api_response};
- # Analyze the request body
+ # Check if we already have errors (e.g. authentification error, invalid JSON body)
+ if ((scalar @{$response_ref->{errors}}) > 0) {
+ $log->warn("process_api_request - we already have errors, skipping processing", {request => $request_ref})
+ if $log->is_warn();
+ }
+ else {
- if ($request_ref->{api_action} eq "product") {
+ # Route the API request to the right processing function, based on API action (from path) and method
- if ($request_ref->{api_method} eq "PATCH") {
- write_product_api($request_ref);
- }
- elsif ($request_ref->{api_method} =~ /^(GET|HEAD|OPTIONS)$/) {
- read_product_api($request_ref);
+ if ($request_ref->{api_action} eq "product") {
+
+ if ($request_ref->{api_method} eq "PATCH") {
+ write_product_api($request_ref);
+ }
+ elsif ($request_ref->{api_method} =~ /^(GET|HEAD|OPTIONS)$/) {
+ read_product_api($request_ref);
+ }
+ else {
+ $log->warn("process_api_request - invalid method", {request => $request_ref}) if $log->is_warn();
+ add_error(
+ $response_ref,
+ {
+ message => {id => "invalid_api_method"},
+ field => {id => "api_method", value => $request_ref->{api_method}},
+ impact => {id => "failure"},
+ }
+ );
+ }
}
else {
- $log->warn("process_api_request - invalid method", {request => $request_ref}) if $log->is_warn();
+ $log->warn("process_api_request - unknown action", {request => $request_ref}) if $log->is_warn();
add_error(
$response_ref,
{
- message => {id => "invalid_api_method"},
- field => {id => "api_method", value => $request_ref->{api_method}},
+ message => {id => "invalid_api_action"},
+ field => {id => "api_action", value => $request_ref->{api_action}},
impact => {id => "failure"},
}
);
}
}
- else {
- $log->warn("process_api_request - unknown action", {request => $request_ref}) if $log->is_warn();
- add_error(
- $response_ref,
- {
- message => {id => "invalid_api_action"},
- field => {id => "api_action", value => $request_ref->{api_action}},
- impact => {id => "failure"},
- }
- );
- }
determine_response_result($response_ref);
@@ -536,7 +547,6 @@ sub customize_packagings ($request_ref, $product_ref) {
}
}
}
-
push @$customized_packagings_ref, $customized_packaging_ref;
}
}
diff --git a/lib/ProductOpener/APIProductWrite.pm b/lib/ProductOpener/APIProductWrite.pm
index 0fcc5ed0aab64..f51cbb6d84e7b 100644
--- a/lib/ProductOpener/APIProductWrite.pm
+++ b/lib/ProductOpener/APIProductWrite.pm
@@ -60,7 +60,7 @@ Update product fields based on input product data.
sub update_product_fields ($request_ref, $product_ref) {
my $response_ref = $request_ref->{api_response};
- my $request_body_ref = $request_ref->{request_body_json};
+ my $request_body_ref = $request_ref->{body_json};
$request_ref->{updated_product_fields} = {};
@@ -149,9 +149,7 @@ sub write_product_api ($request_ref) {
$log->debug("write_product_api - start", {request => $request_ref}) if $log->is_debug();
my $response_ref = $request_ref->{api_response};
-
- decode_json_request_body($request_ref);
- my $request_body_ref = $request_ref->{request_body_json};
+ my $request_body_ref = $request_ref->{body_json};
$log->debug("write_product_api - body", {request_body => $request_body_ref}) if $log->is_debug();
diff --git a/lib/ProductOpener/APITest.pm b/lib/ProductOpener/APITest.pm
index 0925fd193c4b3..734dd43c0a2c5 100644
--- a/lib/ProductOpener/APITest.pm
+++ b/lib/ProductOpener/APITest.pm
@@ -119,7 +119,7 @@ sub wait_server() {
$count++;
if (($count % 3) == 0) {
print("Waiting for backend to be ready since more than $count seconds...\n");
- print("Bad response from website:" . explain({url => $target_url, status => $response->code}) . "\n");
+ diag explain({url => $target_url, status => $response->code, response => $response});
}
confess("Waited too much for backend") if $count > 60;
}
@@ -421,42 +421,47 @@ sub execute_api_tests ($file, $tests_ref) {
$response = $ua->request($request);
}
- # Check if we got the expected response status code
- if (defined $test_ref->{expected_status_code}) {
- is($response->code, $test_ref->{expected_status_code})
- or diag(explain($test_ref), "Response status line: " . $response->status_line);
+ # Check if we got the expected response status code, expect 200 if not provided
+ if (not defined $test_ref->{expected_status_code}) {
+ $test_ref->{expected_status_code} = 200;
}
- # Check that we got a JSON response
- my $json = $response->decoded_content;
-
- my $decoded_json;
- eval {
- $decoded_json = decode_json($json);
- 1;
- } or do {
- my $json_decode_error = $@;
- diag("The $method request to $url returned a response that is not valid JSON: $json_decode_error");
- diag("Response content: " . $json);
- fail($test_case);
- next;
- };
-
- # normalize for comparison
- if (defined $decoded_json->{'products'}) {
- normalize_products_for_test_comparison($decoded_json->{'products'});
- }
- if (defined $decoded_json->{'product'}) {
- normalize_product_for_test_comparison($decoded_json->{'product'});
- }
+ is($response->code, $test_ref->{expected_status_code})
+ or diag(explain($test_ref), "Response status line: " . $response->status_line);
+
+ if (not((defined $test_ref->{expected_type}) and ($test_ref->{expected_type} eq "html"))) {
+
+ # Check that we got a JSON response
+ my $json = $response->decoded_content;
+
+ my $decoded_json;
+ eval {
+ $decoded_json = decode_json($json);
+ 1;
+ } or do {
+ my $json_decode_error = $@;
+ diag("The $method request to $url returned a response that is not valid JSON: $json_decode_error");
+ diag("Response content: " . $json);
+ fail($test_case);
+ next;
+ };
+
+ # normalize for comparison
+ if (defined $decoded_json->{'products'}) {
+ normalize_products_for_test_comparison($decoded_json->{'products'});
+ }
+ if (defined $decoded_json->{'product'}) {
+ normalize_product_for_test_comparison($decoded_json->{'product'});
+ }
- is(
- compare_to_expected_results(
- $decoded_json, "$expected_result_dir/$test_case.json",
- $update_expected_results, $test_ref
- ),
- 1,
- );
+ is(
+ compare_to_expected_results(
+ $decoded_json, "$expected_result_dir/$test_case.json",
+ $update_expected_results, $test_ref
+ ),
+ 1,
+ );
+ }
}
return;
diff --git a/lib/ProductOpener/Display.pm b/lib/ProductOpener/Display.pm
index 71fb0baae9514..d4158e378c62b 100644
--- a/lib/ProductOpener/Display.pm
+++ b/lib/ProductOpener/Display.pm
@@ -518,7 +518,7 @@ A scalar value for the parameter, or undef if the parameter is not defined.
=cut
sub request_param ($request_ref, $param_name) {
- return (scalar param($param_name)) || deep_get($request_ref, "request_body_json", $param_name);
+ return (scalar param($param_name)) || deep_get($request_ref, "body_json", $param_name);
}
=head2 init_request ()
@@ -531,24 +531,34 @@ $lc : language code
It also initializes a request object that is returned.
+=head3 Parameters
+
+=head4 (optional) Request object reference $request_ref
+
+This function may be passed an existing request object reference
+(e.g. pre-containing some fields of the request, like a JSON body).
+
+If not passed, a new request object will be created.
+
+
=head3 Return value
Reference to request object.
=cut
-sub init_request() {
+sub init_request ($request_ref = {}) {
+
+ $log->debug("init_request - start", {request_ref => $request_ref}) if $log->is_debug();
# Clear the context
delete $log->context->{user_id};
delete $log->context->{user_session};
$log->context->{request} = generate_token(16);
- # Create and initialize a request object
- my $request_ref = {
- 'original_query_string' => $ENV{QUERY_STRING},
- 'referer' => referer()
- };
+ # Initialize the request object
+ $request_ref->{referer} = referer();
+ $request_ref->{original_query_string} = $ENV{QUERY_STRING};
# Depending on web server configuration, we may get or not get a / at the start of the QUERY_STRING environment variable
# remove the / to normalize the query string, as we use it to build some redirect urls
@@ -745,13 +755,36 @@ sub init_request() {
my $error = ProductOpener::Users::init_user($request_ref);
if ($error) {
- # TODO: currently we always display an HTML message if we were passed a bad user_id and password combination
- # even if the request is an API request
+ # We were sent bad user_id / password credentials
+ # If it is an API v3 query, the error will be handled by API::process_api_request()
+ if ((defined $request_ref->{api_version}) and ($request_ref->{api_version} >= 3)) {
+ $log->debug(
+ "init_request - init_user error - API v3: continue",
+ {init_user_error => $request_ref->{init_user_error}}
+ ) if $log->is_debug();
+ add_error(
+ $request_ref->{api_response},
+ {
+ message => {id => "invalid_user_id_and_password"},
+ impact => {id => "failure"},
+ }
+ );
+ }
+ # /cgi/auth.pl returns a JSON body
# for requests to /cgi/auth.pl, we will now return a JSON body, set in /cgi/auth.pl
- # but it would be good to later have a more consistent behaviour for all API requests
- if ($r->uri() !~ /\/cgi\/auth\.pl/) {
- print $r->uri();
+ elsif ($r->uri() =~ /\/cgi\/auth\.pl/) {
+ $log->debug(
+ "init_request - init_user error - /cgi/auth.pl: continue",
+ {init_user_error => $request_ref->{init_user_error}}
+ ) if $log->is_debug();
+ }
+ # Otherwise we return an error page in HTML (including for v0 / v1 / v2 API queries)
+ else {
+ $log->debug(
+ "init_request - init_user error - display error page",
+ {init_user_error => $request_ref->{init_user_error}}
+ ) if $log->is_debug();
display_error_and_exit($error, 403);
}
}
diff --git a/lib/ProductOpener/Users.pm b/lib/ProductOpener/Users.pm
index 6bb08d0abef5a..9f41c1ec55c3c 100644
--- a/lib/ProductOpener/Users.pm
+++ b/lib/ProductOpener/Users.pm
@@ -895,7 +895,7 @@ sub init_user ($request_ref) {
%Org = ();
# Remove persistent cookie if user is logging out
- if ((defined single_param('length')) and (single_param('length') eq 'logout')) {
+ if ((defined request_param($request_ref, 'length')) and (request_param($request_ref, 'length') eq 'logout')) {
$log->debug("user logout") if $log->is_debug();
my $session = {};
$request_ref->{cookie} = cookie(
@@ -908,12 +908,12 @@ sub init_user ($request_ref) {
}
# Retrieve user_id and password from form parameters
- elsif ( (defined single_param('user_id'))
- and (single_param('user_id') ne '')
- and (((defined single_param('password')) and (single_param('password') ne ''))))
+ elsif ( (defined request_param($request_ref, 'user_id'))
+ and (request_param($request_ref, 'user_id') ne '')
+ and (((defined request_param($request_ref, 'password')) and (request_param($request_ref, 'password') ne ''))))
{
- $user_id = remove_tags_and_quote(single_param('user_id'));
+ $user_id = remove_tags_and_quote(request_param($request_ref, 'user_id'));
if ($user_id =~ /\@/) {
$log->info("got email while initializing user", {email => $user_id}) if $log->is_info();
@@ -950,7 +950,8 @@ sub init_user ($request_ref) {
$user_id = $user_ref->{'userid'};
$log->context->{user_id} = $user_id;
- my $hash_is_correct = check_password_hash(encode_utf8(decode utf8 => single_param('password')),
+ my $hash_is_correct
+ = check_password_hash(encode_utf8(decode utf8 => request_param($request_ref, 'password')),
$user_ref->{'encrypted_password'});
# We don't have the right password
if (not $hash_is_correct) {
@@ -963,7 +964,8 @@ sub init_user ($request_ref) {
return ($Lang{error_bad_login_password}{$lang});
}
# We have the right login/password
- elsif (not defined single_param('no_log')) # no need to store sessions for internal requests
+ elsif (
+ not defined request_param($request_ref, 'no_log')) # no need to store sessions for internal requests
{
$log->info("correct password for user provided") if $log->is_info();
@@ -1002,9 +1004,6 @@ sub init_user ($request_ref) {
if (defined $user_id) {
my $user_file = "$data_root/users/" . get_string_id_for_lang("no_language", $user_id) . ".sto";
- if ($user_id =~ /f\/(.*)$/) {
- $user_file = "$data_root/facebook_users/" . get_string_id_for_lang("no_language", $1) . ".sto";
- }
if (-e $user_file) {
$user_ref = retrieve($user_file);
diff --git a/po/common/common.pot b/po/common/common.pot
index ff3a9b98bcb7b..0cfda6f49cec4 100644
--- a/po/common/common.pot
+++ b/po/common/common.pot
@@ -6508,3 +6508,7 @@ msgctxt "packagings_complete"
msgid "All the packaging parts of the product are listed."
msgstr "All the packaging parts of the product are listed."
+msgctxt "api_message_invalid_user_id_and_password"
+msgid "Invalid user id and password"
+msgstr "Invalid user id and password"
+
diff --git a/po/common/en.po b/po/common/en.po
index 8dba2bc603ee8..2f46633ba3907 100644
--- a/po/common/en.po
+++ b/po/common/en.po
@@ -6451,3 +6451,8 @@ msgstr "Help categorize more {title} on Hunger Games"
msgctxt "packagings_complete"
msgid "All the packaging parts of the product are listed."
msgstr "All the packaging parts of the product are listed."
+
+msgctxt "api_message_invalid_user_id_and_password"
+msgid "Invalid user id and password"
+msgstr "Invalid user id and password"
+
diff --git a/tests/integration/api_v2_product_read.t b/tests/integration/api_v2_product_read.t
index 640ecbde4e072..345832b37c2fe 100644
--- a/tests/integration/api_v2_product_read.t
+++ b/tests/integration/api_v2_product_read.t
@@ -113,31 +113,49 @@ my $tests_ref = [
{
test_case => 'get-fields-raw',
method => 'GET',
- path => '/api/v3/product/200000000034',
+ path => '/api/v2/product/200000000034',
query_string => '?fields=raw',
expected_status_code => 200,
},
{
test_case => 'get-fields-all',
method => 'GET',
- path => '/api/v3/product/200000000034',
+ path => '/api/v2/product/200000000034',
query_string => '?fields=all',
expected_status_code => 200,
},
{
test_case => 'get-fields-all-knowledge-panels',
method => 'GET',
- path => '/api/v3/product/200000000034',
+ path => '/api/v2/product/200000000034',
query_string => '?fields=all,knowledge_panels',
expected_status_code => 200,
},
{
test_case => 'get-fields-attribute-groups-all-knowledge-panels',
method => 'GET',
- path => '/api/v3/product/200000000034',
+ path => '/api/v2/product/200000000034',
query_string => '?fields=attribute_groups,all,knowledge_panels',
expected_status_code => 200,
},
+ # Test authentication
+ # (currently not needed for READ requests, but it could in the future, for instance to get personalized results)
+ {
+ test_case => 'get-auth-good-password',
+ method => 'GET',
+ path => '/api/v2/product/200000000034',
+ query_string => '?fields=code,product_name&user_id=tests&password=testtest',
+ expected_status_code => 200,
+ },
+ # When authentification fails for a v2 request, we return a HTML page
+ {
+ test_case => 'get-auth-bad-user-password',
+ method => 'GET',
+ path => '/api/v2/product/200000000034',
+ query_string => '?fields=code,product_name&user_id=tests&password=bad_password',
+ expected_status_code => 200,
+ expected_type => "html",
+ },
];
execute_api_tests(__FILE__, $tests_ref);
diff --git a/tests/integration/api_v2_product_write.t b/tests/integration/api_v2_product_write.t
index a126c27ab77eb..f5afc73b1e3e9 100644
--- a/tests/integration/api_v2_product_write.t
+++ b/tests/integration/api_v2_product_write.t
@@ -48,6 +48,61 @@ my $tests_ref = [
method => 'GET',
path => '/api/v2/product/1234567890001',
},
+ # Test authentication
+ {
+ test_case => 'post-product-auth-good-password',
+ method => 'POST',
+ path => '/cgi/product_jqm_multilingual.pl',
+ form => {
+ user_id => "tests",
+ password => "testtest",
+ cc => "be",
+ lc => "fr",
+ code => "1234567890002",
+ product_name => "Product name",
+ categories => "Cookies",
+ quantity => "250 g",
+ serving_size => '20 g',
+ ingredients_text_fr => "Farine de blé, eau, sel, sucre",
+ labels => "Bio, Max Havelaar",
+ nutriment_salt => '50.2',
+ nutriment_salt_unit => 'mg',
+ nutriment_sugars => '12.5',
+ }
+ },
+ {
+ test_case => 'get-product-auth-good-password',
+ method => 'GET',
+ path => '/api/v2/product/1234567890002',
+ },
+ {
+ test_case => 'post-product-auth-bad-user-password',
+ method => 'POST',
+ path => '/cgi/product_jqm_multilingual.pl',
+ form => {
+ user_id => "tests",
+ password => "bad password",
+ cc => "be",
+ lc => "fr",
+ code => "1234567890003",
+ product_name => "Product name",
+ categories => "Cookies",
+ quantity => "250 g",
+ serving_size => '20 g',
+ ingredients_text_fr => "Farine de blé, eau, sel, sucre",
+ labels => "Bio, Max Havelaar",
+ nutriment_salt => '50.2',
+ nutriment_salt_unit => 'mg',
+ nutriment_sugars => '12.5',
+ },
+ expected_type => "html",
+ },
+ {
+ test_case => 'get-product-auth-bad-user-password',
+ method => 'GET',
+ path => '/api/v2/product/1234567890003',
+ expected_status_code => 404,
+ },
];
execute_api_tests(__FILE__, $tests_ref);
diff --git a/tests/integration/api_v3_product_read.t b/tests/integration/api_v3_product_read.t
index 8b9d39630ebc7..5da99d69dc9af 100644
--- a/tests/integration/api_v3_product_read.t
+++ b/tests/integration/api_v3_product_read.t
@@ -143,6 +143,23 @@ my $tests_ref = [
query_string => '?fields=attribute_groups,all,knowledge_panels',
expected_status_code => 200,
},
+ # Test authentication
+ # (currently not needed for READ requests, but it could in the future, for instance to get personalized results)
+ {
+ test_case => 'get-auth-good-password',
+ method => 'GET',
+ path => '/api/v3/product/200000000034',
+ query_string => '?fields=code,product_name&user_id=tests&password=testtest',
+ expected_status_code => 200,
+ },
+ {
+ test_case => 'get-auth-bad-user-password',
+ method => 'GET',
+ path => '/api/v3/product/200000000034',
+ query_string => '?fields=code,product_name&user_id=tests&password=bad_password',
+ expected_status_code => 200,
+ },
+
];
execute_api_tests(__FILE__, $tests_ref);
diff --git a/tests/integration/api_v3_product_write.t b/tests/integration/api_v3_product_write.t
index 9871558f36b88..3c7da823d7998 100644
--- a/tests/integration/api_v3_product_write.t
+++ b/tests/integration/api_v3_product_write.t
@@ -393,7 +393,7 @@ my $tests_ref = [
"product": {
"packagings": [
{
- "number_of_units": 1,
+ "number_of_units": 1,
"shape": {"lc_name": "Bottle"},
"weight_measured": 0.43
},
@@ -406,7 +406,48 @@ my $tests_ref = [
"number_of_units": 3,
"shape": {"lc_name": "Lid"},
"weight_measured": "0,43"
- }
+ }
+ ]
+ }
+ }'
+ },
+ # Test authentication
+ {
+ test_case => 'patch-auth-good-password',
+ method => 'PATCH',
+ path => '/api/v3/product/1234567890014',
+ body => '{
+ "user_id": "tests",
+ "password": "testtest",
+ "fields": "creator,editors_tags,packagings",
+ "tags_lc": "en",
+ "product": {
+ "packagings": [
+ {
+ "number_of_units": 1,
+ "shape": {"lc_name": "can"},
+ "recycling": {"lc_name": "recycle"}
+ }
+ ]
+ }
+ }'
+ },
+ {
+ test_case => 'patch-auth-bad-user-password',
+ method => 'PATCH',
+ path => '/api/v3/product/1234567890015',
+ body => '{
+ "user_id": "tests",
+ "password": "bad password",
+ "fields": "creator,editors_tags,packagings",
+ "tags_lc": "en",
+ "product": {
+ "packagings": [
+ {
+ "number_of_units": 1,
+ "shape": {"lc_name": "can"},
+ "recycling": {"lc_name": "recycle"}
+ }
]
}
}'
diff --git a/tests/integration/expected_test_results/api_v2_product_read/get-auth-good-password.json b/tests/integration/expected_test_results/api_v2_product_read/get-auth-good-password.json
new file mode 100644
index 0000000000000..47cb4242de9b2
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v2_product_read/get-auth-good-password.json
@@ -0,0 +1,9 @@
+{
+ "code" : "200000000034",
+ "product" : {
+ "code" : "200000000034",
+ "product_name" : "Some product"
+ },
+ "status" : 1,
+ "status_verbose" : "product found"
+}
diff --git a/tests/integration/expected_test_results/api_v2_product_read/get-fields-all-knowledge-panels.json b/tests/integration/expected_test_results/api_v2_product_read/get-fields-all-knowledge-panels.json
index a57e633c0dba8..380e302106ca5 100644
--- a/tests/integration/expected_test_results/api_v2_product_read/get-fields-all-knowledge-panels.json
+++ b/tests/integration/expected_test_results/api_v2_product_read/get-fields-all-knowledge-panels.json
@@ -1,6 +1,5 @@
{
"code" : "200000000034",
- "errors" : [],
"product" : {
"_id" : "200000000034",
"_keywords" : [
@@ -1919,55 +1918,31 @@
"packaging_text_en" : "1 wooden box to recycle, 6 25cl glass bottles to reuse, 3 steel lids to recycle, 1 plastic film to discard",
"packagings" : [
{
- "material" : {
- "id" : "en:wood"
- },
+ "material" : "en:wood",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:box"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:box"
},
{
- "material" : {
- "id" : "en:glass"
- },
+ "material" : "en:glass",
"number_of_units" : 6,
"quantity_per_unit" : "25cl",
"quantity_per_unit_unit" : "cl",
"quantity_per_unit_value" : 25,
- "recycling" : {
- "id" : "en:reuse"
- },
- "shape" : {
- "id" : "en:bottle"
- }
+ "recycling" : "en:reuse",
+ "shape" : "en:bottle"
},
{
- "material" : {
- "id" : "en:steel"
- },
+ "material" : "en:steel",
"number_of_units" : 3,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:lid"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:lid"
},
{
- "material" : {
- "id" : "en:plastic"
- },
+ "material" : "en:plastic",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:discard"
- },
- "shape" : {
- "id" : "en:film"
- }
+ "recycling" : "en:discard",
+ "shape" : "en:film"
}
],
"photographers_tags" : [],
@@ -2031,11 +2006,6 @@
"unknown_nutrients_tags" : [],
"vitamins_tags" : []
},
- "result" : {
- "id" : "product_found",
- "lc_name" : "Product found",
- "name" : "Product found"
- },
- "status" : "success",
- "warnings" : []
+ "status" : 1,
+ "status_verbose" : "product found"
}
diff --git a/tests/integration/expected_test_results/api_v2_product_read/get-fields-all.json b/tests/integration/expected_test_results/api_v2_product_read/get-fields-all.json
index 3dabb5af4868d..571989cb6586e 100644
--- a/tests/integration/expected_test_results/api_v2_product_read/get-fields-all.json
+++ b/tests/integration/expected_test_results/api_v2_product_read/get-fields-all.json
@@ -1,6 +1,5 @@
{
"code" : "200000000034",
- "errors" : [],
"product" : {
"_id" : "200000000034",
"_keywords" : [
@@ -775,55 +774,31 @@
"packaging_text_en" : "1 wooden box to recycle, 6 25cl glass bottles to reuse, 3 steel lids to recycle, 1 plastic film to discard",
"packagings" : [
{
- "material" : {
- "id" : "en:wood"
- },
+ "material" : "en:wood",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:box"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:box"
},
{
- "material" : {
- "id" : "en:glass"
- },
+ "material" : "en:glass",
"number_of_units" : 6,
"quantity_per_unit" : "25cl",
"quantity_per_unit_unit" : "cl",
"quantity_per_unit_value" : 25,
- "recycling" : {
- "id" : "en:reuse"
- },
- "shape" : {
- "id" : "en:bottle"
- }
+ "recycling" : "en:reuse",
+ "shape" : "en:bottle"
},
{
- "material" : {
- "id" : "en:steel"
- },
+ "material" : "en:steel",
"number_of_units" : 3,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:lid"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:lid"
},
{
- "material" : {
- "id" : "en:plastic"
- },
+ "material" : "en:plastic",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:discard"
- },
- "shape" : {
- "id" : "en:film"
- }
+ "recycling" : "en:discard",
+ "shape" : "en:film"
}
],
"photographers_tags" : [],
@@ -887,11 +862,6 @@
"unknown_nutrients_tags" : [],
"vitamins_tags" : []
},
- "result" : {
- "id" : "product_found",
- "lc_name" : "Product found",
- "name" : "Product found"
- },
- "status" : "success",
- "warnings" : []
+ "status" : 1,
+ "status_verbose" : "product found"
}
diff --git a/tests/integration/expected_test_results/api_v2_product_read/get-fields-attribute-groups-all-knowledge-panels.json b/tests/integration/expected_test_results/api_v2_product_read/get-fields-attribute-groups-all-knowledge-panels.json
index c3e012990de08..932300f1c38e2 100644
--- a/tests/integration/expected_test_results/api_v2_product_read/get-fields-attribute-groups-all-knowledge-panels.json
+++ b/tests/integration/expected_test_results/api_v2_product_read/get-fields-attribute-groups-all-knowledge-panels.json
@@ -1,6 +1,5 @@
{
"code" : "200000000034",
- "errors" : [],
"product" : {
"_id" : "200000000034",
"_keywords" : [
@@ -2214,55 +2213,31 @@
"packaging_text_en" : "1 wooden box to recycle, 6 25cl glass bottles to reuse, 3 steel lids to recycle, 1 plastic film to discard",
"packagings" : [
{
- "material" : {
- "id" : "en:wood"
- },
+ "material" : "en:wood",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:box"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:box"
},
{
- "material" : {
- "id" : "en:glass"
- },
+ "material" : "en:glass",
"number_of_units" : 6,
"quantity_per_unit" : "25cl",
"quantity_per_unit_unit" : "cl",
"quantity_per_unit_value" : 25,
- "recycling" : {
- "id" : "en:reuse"
- },
- "shape" : {
- "id" : "en:bottle"
- }
+ "recycling" : "en:reuse",
+ "shape" : "en:bottle"
},
{
- "material" : {
- "id" : "en:steel"
- },
+ "material" : "en:steel",
"number_of_units" : 3,
- "recycling" : {
- "id" : "en:recycle"
- },
- "shape" : {
- "id" : "en:lid"
- }
+ "recycling" : "en:recycle",
+ "shape" : "en:lid"
},
{
- "material" : {
- "id" : "en:plastic"
- },
+ "material" : "en:plastic",
"number_of_units" : 1,
- "recycling" : {
- "id" : "en:discard"
- },
- "shape" : {
- "id" : "en:film"
- }
+ "recycling" : "en:discard",
+ "shape" : "en:film"
}
],
"photographers_tags" : [],
@@ -2326,11 +2301,6 @@
"unknown_nutrients_tags" : [],
"vitamins_tags" : []
},
- "result" : {
- "id" : "product_found",
- "lc_name" : "Product found",
- "name" : "Product found"
- },
- "status" : "success",
- "warnings" : []
+ "status" : 1,
+ "status_verbose" : "product found"
}
diff --git a/tests/integration/expected_test_results/api_v2_product_read/get-fields-raw.json b/tests/integration/expected_test_results/api_v2_product_read/get-fields-raw.json
index b39012911bd0d..8a6e89897be84 100644
--- a/tests/integration/expected_test_results/api_v2_product_read/get-fields-raw.json
+++ b/tests/integration/expected_test_results/api_v2_product_read/get-fields-raw.json
@@ -1,6 +1,5 @@
{
"code" : "200000000034",
- "errors" : [],
"product" : {
"_id" : "200000000034",
"_keywords" : [
@@ -858,11 +857,6 @@
"unknown_nutrients_tags" : [],
"vitamins_tags" : []
},
- "result" : {
- "id" : "product_found",
- "lc_name" : "Product found",
- "name" : "Product found"
- },
- "status" : "success",
- "warnings" : []
+ "status" : 1,
+ "status_verbose" : "product found"
}
diff --git a/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-bad-user-password.json b/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-bad-user-password.json
new file mode 100644
index 0000000000000..1e238a6f5760e
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-bad-user-password.json
@@ -0,0 +1,5 @@
+{
+ "code" : "1234567890003",
+ "status" : 0,
+ "status_verbose" : "product not found"
+}
diff --git a/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-good-password.json b/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-good-password.json
new file mode 100644
index 0000000000000..0aed309b3e1dd
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v2_product_write/get-product-auth-good-password.json
@@ -0,0 +1,803 @@
+{
+ "code" : "1234567890002",
+ "product" : {
+ "_id" : "1234567890002",
+ "_keywords" : [
+ "bio",
+ "cookie",
+ "havelaar",
+ "max",
+ "name",
+ "product"
+ ],
+ "added_countries_tags" : [],
+ "additives_n" : 0,
+ "additives_old_n" : 0,
+ "additives_old_tags" : [],
+ "additives_original_tags" : [],
+ "additives_tags" : [],
+ "allergens" : "",
+ "allergens_from_ingredients" : "en:gluten, blé",
+ "allergens_from_user" : "(fr) ",
+ "allergens_hierarchy" : [
+ "en:gluten"
+ ],
+ "allergens_tags" : [
+ "en:gluten"
+ ],
+ "amino_acids_tags" : [],
+ "categories" : "Cookies",
+ "categories_hierarchy" : [
+ "en:snacks",
+ "en:sweet-snacks",
+ "en:biscuits-and-cakes",
+ "en:biscuits",
+ "en:drop-cookies"
+ ],
+ "categories_lc" : "fr",
+ "categories_properties" : {
+ "agribalyse_proxy_food_code:en" : "24000"
+ },
+ "categories_properties_tags" : [
+ "all-products",
+ "categories-known",
+ "agribalyse-food-code-unknown",
+ "agribalyse-proxy-food-code-24000",
+ "agribalyse-proxy-food-code-known",
+ "ciqual-food-code-unknown",
+ "agribalyse-known",
+ "agribalyse-24000"
+ ],
+ "categories_tags" : [
+ "en:snacks",
+ "en:sweet-snacks",
+ "en:biscuits-and-cakes",
+ "en:biscuits",
+ "en:drop-cookies"
+ ],
+ "checkers_tags" : [],
+ "code" : "1234567890002",
+ "codes_tags" : [
+ "code-13",
+ "1234567890xxx",
+ "123456789xxxx",
+ "12345678xxxxx",
+ "1234567xxxxxx",
+ "123456xxxxxxx",
+ "12345xxxxxxxx",
+ "1234xxxxxxxxx",
+ "123xxxxxxxxxx",
+ "12xxxxxxxxxxx",
+ "1xxxxxxxxxxxx"
+ ],
+ "complete" : 0,
+ "completeness" : 0.5,
+ "correctors_tags" : [],
+ "countries" : "en:belgium",
+ "countries_hierarchy" : [
+ "en:belgium"
+ ],
+ "countries_tags" : [
+ "en:belgium"
+ ],
+ "created_t" : "--ignore--",
+ "creator" : "tests",
+ "data_quality_bugs_tags" : [],
+ "data_quality_errors_tags" : [],
+ "data_quality_info_tags" : [
+ "en:no-packaging-data",
+ "en:ingredients-percent-analysis-ok",
+ "en:ecoscore-extended-data-not-computed",
+ "en:food-groups-1-known",
+ "en:food-groups-2-known",
+ "en:food-groups-3-unknown"
+ ],
+ "data_quality_tags" : [
+ "en:no-packaging-data",
+ "en:ingredients-percent-analysis-ok",
+ "en:ecoscore-extended-data-not-computed",
+ "en:food-groups-1-known",
+ "en:food-groups-2-known",
+ "en:food-groups-3-unknown",
+ "en:nutrition-value-under-0-1-g-salt",
+ "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown",
+ "en:ecoscore-packaging-packaging-data-missing",
+ "en:ecoscore-production-system-no-label"
+ ],
+ "data_quality_warnings_tags" : [
+ "en:nutrition-value-under-0-1-g-salt",
+ "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown",
+ "en:ecoscore-packaging-packaging-data-missing",
+ "en:ecoscore-production-system-no-label"
+ ],
+ "ecoscore_data" : {
+ "adjustments" : {
+ "origins_of_ingredients" : {
+ "aggregated_origins" : [
+ {
+ "epi_score" : 0,
+ "origin" : "en:unknown",
+ "percent" : 100,
+ "transportation_score" : null
+ }
+ ],
+ "epi_score" : 0,
+ "epi_value" : -5,
+ "origins_from_origins_field" : [
+ "en:unknown"
+ ],
+ "transportation_score" : 0,
+ "transportation_scores" : {
+ "ad" : 0,
+ "al" : 0,
+ "at" : 0,
+ "ax" : 0,
+ "ba" : 0,
+ "be" : 0,
+ "bg" : 0,
+ "ch" : 0,
+ "cy" : 0,
+ "cz" : 0,
+ "de" : 0,
+ "dk" : 0,
+ "dz" : 0,
+ "ee" : 0,
+ "eg" : 0,
+ "es" : 0,
+ "fi" : 0,
+ "fo" : 0,
+ "fr" : 0,
+ "gg" : 0,
+ "gi" : 0,
+ "gr" : 0,
+ "hr" : 0,
+ "hu" : 0,
+ "ie" : 0,
+ "il" : 0,
+ "im" : 0,
+ "is" : 0,
+ "it" : 0,
+ "je" : 0,
+ "lb" : 0,
+ "li" : 0,
+ "lt" : 0,
+ "lu" : 0,
+ "lv" : 0,
+ "ly" : 0,
+ "ma" : 0,
+ "mc" : 0,
+ "md" : 0,
+ "me" : 0,
+ "mk" : 0,
+ "mt" : 0,
+ "nl" : 0,
+ "no" : 0,
+ "pl" : 0,
+ "ps" : 0,
+ "pt" : 0,
+ "ro" : 0,
+ "rs" : 0,
+ "se" : 0,
+ "si" : 0,
+ "sj" : 0,
+ "sk" : 0,
+ "sm" : 0,
+ "sy" : 0,
+ "tn" : 0,
+ "tr" : 0,
+ "ua" : 0,
+ "uk" : 0,
+ "us" : 0,
+ "va" : 0,
+ "world" : 0,
+ "xk" : 0
+ },
+ "transportation_value" : 0,
+ "transportation_values" : {
+ "ad" : 0,
+ "al" : 0,
+ "at" : 0,
+ "ax" : 0,
+ "ba" : 0,
+ "be" : 0,
+ "bg" : 0,
+ "ch" : 0,
+ "cy" : 0,
+ "cz" : 0,
+ "de" : 0,
+ "dk" : 0,
+ "dz" : 0,
+ "ee" : 0,
+ "eg" : 0,
+ "es" : 0,
+ "fi" : 0,
+ "fo" : 0,
+ "fr" : 0,
+ "gg" : 0,
+ "gi" : 0,
+ "gr" : 0,
+ "hr" : 0,
+ "hu" : 0,
+ "ie" : 0,
+ "il" : 0,
+ "im" : 0,
+ "is" : 0,
+ "it" : 0,
+ "je" : 0,
+ "lb" : 0,
+ "li" : 0,
+ "lt" : 0,
+ "lu" : 0,
+ "lv" : 0,
+ "ly" : 0,
+ "ma" : 0,
+ "mc" : 0,
+ "md" : 0,
+ "me" : 0,
+ "mk" : 0,
+ "mt" : 0,
+ "nl" : 0,
+ "no" : 0,
+ "pl" : 0,
+ "ps" : 0,
+ "pt" : 0,
+ "ro" : 0,
+ "rs" : 0,
+ "se" : 0,
+ "si" : 0,
+ "sj" : 0,
+ "sk" : 0,
+ "sm" : 0,
+ "sy" : 0,
+ "tn" : 0,
+ "tr" : 0,
+ "ua" : 0,
+ "uk" : 0,
+ "us" : 0,
+ "va" : 0,
+ "world" : 0,
+ "xk" : 0
+ },
+ "value" : -5,
+ "values" : {
+ "ad" : -5,
+ "al" : -5,
+ "at" : -5,
+ "ax" : -5,
+ "ba" : -5,
+ "be" : -5,
+ "bg" : -5,
+ "ch" : -5,
+ "cy" : -5,
+ "cz" : -5,
+ "de" : -5,
+ "dk" : -5,
+ "dz" : -5,
+ "ee" : -5,
+ "eg" : -5,
+ "es" : -5,
+ "fi" : -5,
+ "fo" : -5,
+ "fr" : -5,
+ "gg" : -5,
+ "gi" : -5,
+ "gr" : -5,
+ "hr" : -5,
+ "hu" : -5,
+ "ie" : -5,
+ "il" : -5,
+ "im" : -5,
+ "is" : -5,
+ "it" : -5,
+ "je" : -5,
+ "lb" : -5,
+ "li" : -5,
+ "lt" : -5,
+ "lu" : -5,
+ "lv" : -5,
+ "ly" : -5,
+ "ma" : -5,
+ "mc" : -5,
+ "md" : -5,
+ "me" : -5,
+ "mk" : -5,
+ "mt" : -5,
+ "nl" : -5,
+ "no" : -5,
+ "pl" : -5,
+ "ps" : -5,
+ "pt" : -5,
+ "ro" : -5,
+ "rs" : -5,
+ "se" : -5,
+ "si" : -5,
+ "sj" : -5,
+ "sk" : -5,
+ "sm" : -5,
+ "sy" : -5,
+ "tn" : -5,
+ "tr" : -5,
+ "ua" : -5,
+ "uk" : -5,
+ "us" : -5,
+ "va" : -5,
+ "world" : -5,
+ "xk" : -5
+ },
+ "warning" : "origins_are_100_percent_unknown"
+ },
+ "packaging" : {
+ "non_recyclable_and_non_biodegradable_materials" : 1,
+ "value" : -15,
+ "warning" : "packaging_data_missing"
+ },
+ "production_system" : {
+ "labels" : [],
+ "value" : 0,
+ "warning" : "no_label"
+ },
+ "threatened_species" : {}
+ },
+ "agribalyse" : {
+ "agribalyse_proxy_food_code" : "24000",
+ "co2_agriculture" : 2.3889426,
+ "co2_consumption" : 0,
+ "co2_distribution" : 0.019530673,
+ "co2_packaging" : 0.11014808,
+ "co2_processing" : 0.22878446,
+ "co2_total" : 2.882859363,
+ "co2_transportation" : 0.13545355,
+ "code" : "24000",
+ "dqr" : "2.14",
+ "ef_agriculture" : 0.28329233,
+ "ef_consumption" : 0,
+ "ef_distribution" : 0.0048315303,
+ "ef_packaging" : 0.01096965,
+ "ef_processing" : 0.041686082,
+ "ef_total" : 0.3518903043,
+ "ef_transportation" : 0.011110712,
+ "is_beverage" : 0,
+ "name_en" : "Biscuit (cookie)",
+ "name_fr" : "Biscuit sec, sans précision",
+ "score" : 69,
+ "version" : "3.1"
+ },
+ "grade" : "c",
+ "grades" : {
+ "ad" : "c",
+ "al" : "c",
+ "at" : "c",
+ "ax" : "c",
+ "ba" : "c",
+ "be" : "c",
+ "bg" : "c",
+ "ch" : "c",
+ "cy" : "c",
+ "cz" : "c",
+ "de" : "c",
+ "dk" : "c",
+ "dz" : "c",
+ "ee" : "c",
+ "eg" : "c",
+ "es" : "c",
+ "fi" : "c",
+ "fo" : "c",
+ "fr" : "c",
+ "gg" : "c",
+ "gi" : "c",
+ "gr" : "c",
+ "hr" : "c",
+ "hu" : "c",
+ "ie" : "c",
+ "il" : "c",
+ "im" : "c",
+ "is" : "c",
+ "it" : "c",
+ "je" : "c",
+ "lb" : "c",
+ "li" : "c",
+ "lt" : "c",
+ "lu" : "c",
+ "lv" : "c",
+ "ly" : "c",
+ "ma" : "c",
+ "mc" : "c",
+ "md" : "c",
+ "me" : "c",
+ "mk" : "c",
+ "mt" : "c",
+ "nl" : "c",
+ "no" : "c",
+ "pl" : "c",
+ "ps" : "c",
+ "pt" : "c",
+ "ro" : "c",
+ "rs" : "c",
+ "se" : "c",
+ "si" : "c",
+ "sj" : "c",
+ "sk" : "c",
+ "sm" : "c",
+ "sy" : "c",
+ "tn" : "c",
+ "tr" : "c",
+ "ua" : "c",
+ "uk" : "c",
+ "us" : "c",
+ "va" : "c",
+ "world" : "c",
+ "xk" : "c"
+ },
+ "missing" : {
+ "labels" : 1,
+ "origins" : 1,
+ "packagings" : 1
+ },
+ "missing_data_warning" : 1,
+ "missing_key_data" : 1,
+ "score" : 49,
+ "scores" : {
+ "ad" : 49,
+ "al" : 49,
+ "at" : 49,
+ "ax" : 49,
+ "ba" : 49,
+ "be" : 49,
+ "bg" : 49,
+ "ch" : 49,
+ "cy" : 49,
+ "cz" : 49,
+ "de" : 49,
+ "dk" : 49,
+ "dz" : 49,
+ "ee" : 49,
+ "eg" : 49,
+ "es" : 49,
+ "fi" : 49,
+ "fo" : 49,
+ "fr" : 49,
+ "gg" : 49,
+ "gi" : 49,
+ "gr" : 49,
+ "hr" : 49,
+ "hu" : 49,
+ "ie" : 49,
+ "il" : 49,
+ "im" : 49,
+ "is" : 49,
+ "it" : 49,
+ "je" : 49,
+ "lb" : 49,
+ "li" : 49,
+ "lt" : 49,
+ "lu" : 49,
+ "lv" : 49,
+ "ly" : 49,
+ "ma" : 49,
+ "mc" : 49,
+ "md" : 49,
+ "me" : 49,
+ "mk" : 49,
+ "mt" : 49,
+ "nl" : 49,
+ "no" : 49,
+ "pl" : 49,
+ "ps" : 49,
+ "pt" : 49,
+ "ro" : 49,
+ "rs" : 49,
+ "se" : 49,
+ "si" : 49,
+ "sj" : 49,
+ "sk" : 49,
+ "sm" : 49,
+ "sy" : 49,
+ "tn" : 49,
+ "tr" : 49,
+ "ua" : 49,
+ "uk" : 49,
+ "us" : 49,
+ "va" : 49,
+ "world" : 49,
+ "xk" : 49
+ },
+ "status" : "known"
+ },
+ "ecoscore_grade" : "c",
+ "ecoscore_score" : 49,
+ "ecoscore_tags" : [
+ "c"
+ ],
+ "editors_tags" : [
+ "tests"
+ ],
+ "entry_dates_tags" : "--ignore--",
+ "food_groups" : "en:biscuits-and-cakes",
+ "food_groups_tags" : [
+ "en:sugary-snacks",
+ "en:biscuits-and-cakes"
+ ],
+ "grades" : {},
+ "id" : "1234567890002",
+ "informers_tags" : [
+ "tests"
+ ],
+ "ingredients" : [
+ {
+ "id" : "en:wheat-flour",
+ "percent_estimate" : 62.5,
+ "percent_max" : 100,
+ "percent_min" : 25,
+ "text" : "Farine de blé",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:water",
+ "percent_estimate" : 18.75,
+ "percent_max" : 50,
+ "percent_min" : 0,
+ "text" : "eau",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:salt",
+ "percent_estimate" : 9.375,
+ "percent_max" : 33.3333333333333,
+ "percent_min" : 0,
+ "text" : "sel",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ },
+ {
+ "id" : "en:sugar",
+ "percent_estimate" : 9.375,
+ "percent_max" : 25,
+ "percent_min" : 0,
+ "text" : "sucre",
+ "vegan" : "yes",
+ "vegetarian" : "yes"
+ }
+ ],
+ "ingredients_analysis" : {},
+ "ingredients_analysis_tags" : [
+ "en:palm-oil-free",
+ "en:vegan",
+ "en:vegetarian"
+ ],
+ "ingredients_from_or_that_may_be_from_palm_oil_n" : 0,
+ "ingredients_from_palm_oil_n" : 0,
+ "ingredients_from_palm_oil_tags" : [],
+ "ingredients_hierarchy" : [
+ "en:wheat-flour",
+ "en:cereal",
+ "en:flour",
+ "en:wheat",
+ "en:cereal-flour",
+ "en:water",
+ "en:salt",
+ "en:sugar",
+ "en:added-sugar",
+ "en:disaccharide"
+ ],
+ "ingredients_n" : 4,
+ "ingredients_n_tags" : [
+ "4",
+ "1-10"
+ ],
+ "ingredients_original_tags" : [
+ "en:wheat-flour",
+ "en:water",
+ "en:salt",
+ "en:sugar"
+ ],
+ "ingredients_percent_analysis" : 1,
+ "ingredients_tags" : [
+ "en:wheat-flour",
+ "en:cereal",
+ "en:flour",
+ "en:wheat",
+ "en:cereal-flour",
+ "en:water",
+ "en:salt",
+ "en:sugar",
+ "en:added-sugar",
+ "en:disaccharide"
+ ],
+ "ingredients_text" : "Farine de blé, eau, sel, sucre",
+ "ingredients_text_fr" : "Farine de blé, eau, sel, sucre",
+ "ingredients_text_with_allergens" : "Farine de blé, eau, sel, sucre",
+ "ingredients_text_with_allergens_fr" : "Farine de blé, eau, sel, sucre",
+ "ingredients_that_may_be_from_palm_oil_n" : 0,
+ "ingredients_that_may_be_from_palm_oil_tags" : [],
+ "ingredients_with_specified_percent_n" : 0,
+ "ingredients_with_specified_percent_sum" : 0,
+ "ingredients_with_unspecified_percent_n" : 4,
+ "ingredients_with_unspecified_percent_sum" : 100,
+ "interface_version_created" : "20150316.jqm2",
+ "interface_version_modified" : "20150316.jqm2",
+ "known_ingredients_n" : 10,
+ "labels" : "Bio, Max Havelaar",
+ "labels_hierarchy" : [
+ "en:organic",
+ "en:fair-trade",
+ "en:max-havelaar"
+ ],
+ "labels_lc" : "fr",
+ "labels_tags" : [
+ "en:organic",
+ "en:fair-trade",
+ "en:max-havelaar"
+ ],
+ "lang" : "fr",
+ "languages" : {
+ "en:french" : 2
+ },
+ "languages_codes" : {
+ "fr" : 2
+ },
+ "languages_hierarchy" : [
+ "en:french"
+ ],
+ "languages_tags" : [
+ "en:french",
+ "en:1"
+ ],
+ "last_edit_dates_tags" : "--ignore--",
+ "last_editor" : "tests",
+ "last_modified_by" : "tests",
+ "last_modified_t" : "--ignore--",
+ "lc" : "fr",
+ "main_countries_tags" : [],
+ "minerals_tags" : [],
+ "misc_tags" : [
+ "en:nutriscore-not-computed",
+ "en:nutrition-not-enough-data-to-compute-nutrition-score",
+ "en:nutriscore-missing-nutrition-data",
+ "en:nutriscore-missing-nutrition-data-energy",
+ "en:nutriscore-missing-nutrition-data-fat",
+ "en:nutriscore-missing-nutrition-data-saturated-fat",
+ "en:nutriscore-missing-nutrition-data-proteins",
+ "en:nutrition-no-fiber",
+ "en:packagings-not-complete",
+ "en:packagings-empty",
+ "en:ecoscore-extended-data-not-computed",
+ "en:ecoscore-missing-data-warning",
+ "en:ecoscore-missing-data-labels",
+ "en:ecoscore-missing-data-origins",
+ "en:ecoscore-missing-data-packagings",
+ "en:ecoscore-missing-data-no-packagings",
+ "en:ecoscore-computed",
+ "en:ecoscore-changed",
+ "en:ecoscore-grade-changed",
+ "en:main-countries-new-product"
+ ],
+ "nova_group" : 3,
+ "nova_group_debug" : "",
+ "nova_groups" : "3",
+ "nova_groups_markers" : {
+ "3" : [
+ [
+ "ingredients",
+ "en:salt"
+ ],
+ [
+ "ingredients",
+ "en:sugar"
+ ],
+ [
+ "categories",
+ "en:sweet-snacks"
+ ]
+ ]
+ },
+ "nova_groups_tags" : [
+ "en:3-processed-foods"
+ ],
+ "nucleotides_tags" : [],
+ "nutrient_levels" : {
+ "salt" : "low",
+ "sugars" : "moderate"
+ },
+ "nutrient_levels_tags" : [
+ "en:sugars-in-moderate-quantity",
+ "en:salt-in-low-quantity"
+ ],
+ "nutriments" : {
+ "fruits-vegetables-nuts-estimate-from-ingredients_100g" : 0,
+ "fruits-vegetables-nuts-estimate-from-ingredients_serving" : 0,
+ "nova-group" : 3,
+ "nova-group_100g" : 3,
+ "nova-group_serving" : 3,
+ "salt" : 0.0502,
+ "salt_100g" : 0.0502,
+ "salt_serving" : 0.01,
+ "salt_unit" : "mg",
+ "salt_value" : 50.2,
+ "sodium" : 0.02008,
+ "sodium_100g" : 0.02008,
+ "sodium_serving" : 0.00402,
+ "sodium_unit" : "mg",
+ "sodium_value" : 20.08,
+ "sugars" : 12.5,
+ "sugars_100g" : 12.5,
+ "sugars_serving" : 2.5,
+ "sugars_unit" : "g",
+ "sugars_value" : 12.5
+ },
+ "nutrition_data" : "on",
+ "nutrition_data_per" : "100g",
+ "nutrition_data_prepared_per" : "100g",
+ "nutrition_grades_tags" : [
+ "unknown"
+ ],
+ "nutrition_score_beverage" : 0,
+ "nutrition_score_debug" : "missing energy - missing fat - missing saturated-fat - missing proteins",
+ "nutrition_score_warning_no_fiber" : 1,
+ "other_nutritional_substances_tags" : [],
+ "packagings" : [],
+ "photographers_tags" : [],
+ "pnns_groups_1" : "Sugary snacks",
+ "pnns_groups_1_tags" : [
+ "sugary-snacks",
+ "known"
+ ],
+ "pnns_groups_2" : "Biscuits and cakes",
+ "pnns_groups_2_tags" : [
+ "biscuits-and-cakes",
+ "known"
+ ],
+ "popularity_key" : 0,
+ "product_name" : "Product name",
+ "product_name_fr" : "Product name",
+ "product_quantity" : "250",
+ "quantity" : "250 g",
+ "removed_countries_tags" : [],
+ "rev" : 1,
+ "scores" : {},
+ "serving_quantity" : "20",
+ "serving_size" : "20 g",
+ "states" : "en:to-be-completed, en:nutrition-facts-completed, en:ingredients-completed, en:expiration-date-to-be-completed, en:packaging-code-to-be-completed, en:characteristics-to-be-completed, en:origins-to-be-completed, en:categories-completed, en:brands-to-be-completed, en:packaging-to-be-completed, en:quantity-completed, en:product-name-completed, en:photos-to-be-uploaded",
+ "states_hierarchy" : [
+ "en:to-be-completed",
+ "en:nutrition-facts-completed",
+ "en:ingredients-completed",
+ "en:expiration-date-to-be-completed",
+ "en:packaging-code-to-be-completed",
+ "en:characteristics-to-be-completed",
+ "en:origins-to-be-completed",
+ "en:categories-completed",
+ "en:brands-to-be-completed",
+ "en:packaging-to-be-completed",
+ "en:quantity-completed",
+ "en:product-name-completed",
+ "en:photos-to-be-uploaded"
+ ],
+ "states_tags" : [
+ "en:to-be-completed",
+ "en:nutrition-facts-completed",
+ "en:ingredients-completed",
+ "en:expiration-date-to-be-completed",
+ "en:packaging-code-to-be-completed",
+ "en:characteristics-to-be-completed",
+ "en:origins-to-be-completed",
+ "en:categories-completed",
+ "en:brands-to-be-completed",
+ "en:packaging-to-be-completed",
+ "en:quantity-completed",
+ "en:product-name-completed",
+ "en:photos-to-be-uploaded"
+ ],
+ "traces" : "",
+ "traces_from_ingredients" : "",
+ "traces_from_user" : "(fr) ",
+ "traces_hierarchy" : [],
+ "traces_tags" : [],
+ "unknown_ingredients_n" : 0,
+ "unknown_nutrients_tags" : [],
+ "vitamins_tags" : []
+ },
+ "status" : 1,
+ "status_verbose" : "product found"
+}
diff --git a/tests/integration/expected_test_results/api_v2_product_write/post-product-auth-good-password.json b/tests/integration/expected_test_results/api_v2_product_write/post-product-auth-good-password.json
new file mode 100644
index 0000000000000..39d4261a9d3f0
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v2_product_write/post-product-auth-good-password.json
@@ -0,0 +1,4 @@
+{
+ "status" : 1,
+ "status_verbose" : "fields saved"
+}
diff --git a/tests/integration/expected_test_results/api_v3_product_read/get-auth-bad-user-password.json b/tests/integration/expected_test_results/api_v3_product_read/get-auth-bad-user-password.json
new file mode 100644
index 0000000000000..557df6ed3a545
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v3_product_read/get-auth-bad-user-password.json
@@ -0,0 +1,18 @@
+{
+ "errors" : [
+ {
+ "impact" : {
+ "id" : "failure",
+ "lc_name" : "Failure",
+ "name" : "Failure"
+ },
+ "message" : {
+ "id" : "invalid_user_id_and_password",
+ "lc_name" : "Invalid user id and password",
+ "name" : "Invalid user id and password"
+ }
+ }
+ ],
+ "status" : "failure",
+ "warnings" : []
+}
diff --git a/tests/integration/expected_test_results/api_v3_product_read/get-auth-good-password.json b/tests/integration/expected_test_results/api_v3_product_read/get-auth-good-password.json
new file mode 100644
index 0000000000000..7e6b07e248b1c
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v3_product_read/get-auth-good-password.json
@@ -0,0 +1,15 @@
+{
+ "code" : "200000000034",
+ "errors" : [],
+ "product" : {
+ "code" : "200000000034",
+ "product_name" : "Some product"
+ },
+ "result" : {
+ "id" : "product_found",
+ "lc_name" : "Product found",
+ "name" : "Product found"
+ },
+ "status" : "success",
+ "warnings" : []
+}
diff --git a/tests/integration/expected_test_results/api_v3_product_write/patch-auth-bad-user-password.json b/tests/integration/expected_test_results/api_v3_product_write/patch-auth-bad-user-password.json
new file mode 100644
index 0000000000000..557df6ed3a545
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v3_product_write/patch-auth-bad-user-password.json
@@ -0,0 +1,18 @@
+{
+ "errors" : [
+ {
+ "impact" : {
+ "id" : "failure",
+ "lc_name" : "Failure",
+ "name" : "Failure"
+ },
+ "message" : {
+ "id" : "invalid_user_id_and_password",
+ "lc_name" : "Invalid user id and password",
+ "name" : "Invalid user id and password"
+ }
+ }
+ ],
+ "status" : "failure",
+ "warnings" : []
+}
diff --git a/tests/integration/expected_test_results/api_v3_product_write/patch-auth-good-password.json b/tests/integration/expected_test_results/api_v3_product_write/patch-auth-good-password.json
new file mode 100644
index 0000000000000..5c21dfcdba740
--- /dev/null
+++ b/tests/integration/expected_test_results/api_v3_product_write/patch-auth-good-password.json
@@ -0,0 +1,42 @@
+{
+ "code" : "1234567890014",
+ "errors" : [],
+ "product" : {
+ "creator" : "tests",
+ "editors_tags" : [
+ "tests"
+ ],
+ "packagings" : [
+ {
+ "number_of_units" : 1,
+ "recycling" : {
+ "id" : "en:recycle",
+ "lc_name" : "Recycle"
+ },
+ "shape" : {
+ "id" : "en:can",
+ "lc_name" : "Can"
+ }
+ }
+ ]
+ },
+ "status" : "success_with_warnings",
+ "warnings" : [
+ {
+ "field" : {
+ "id" : "material",
+ "value" : null
+ },
+ "impact" : {
+ "id" : "field_ignored",
+ "lc_name" : "Field ignored",
+ "name" : "Field ignored"
+ },
+ "message" : {
+ "id" : "missing_field",
+ "lc_name" : "Missing field",
+ "name" : "Missing field"
+ }
+ }
+ ]
+}