diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 6a095fef2b..041da38a0a 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -56,11 +56,11 @@ sub DATE_FIELDS { update => [] }; - # Add date related custom fields + # Add datetime custom fields. Date-only fields are left out so they are + # passed through as YYYY-MM-DD, since converting them would append a time + # component that _check_date_field rejects. foreach my $field (Bugzilla->active_custom_fields({skip_extensions => 1})) { - next - unless ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE); + next unless $field->type == FIELD_TYPE_DATETIME; push(@{$fields->{create}}, $field->name); push(@{$fields->{update}}, $field->name); } @@ -1893,9 +1893,16 @@ sub _format_cf_value { if ($field->type == FIELD_TYPE_BUG_ID) { return $self->type('int', $value); } - elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + elsif ($field->type == FIELD_TYPE_DATETIME) { return defined($value) ? $self->type('dateTime', $value) : undef; } + elsif ($field->type == FIELD_TYPE_DATE) { + + # Date-only fields are returned as YYYY-MM-DD, the same format they are + # accepted in, like deadline. Converting them to dateTime would append + # a time and time zone that the value does not have. + return defined($value) ? $self->type('string', $value) : undef; + } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { return [map { $self->type('string', $_) } @{$value}]; } diff --git a/docs/en/rst/api/core/v1/bug.rst b/docs/en/rst/api/core/v1/bug.rst index 516d59d907..c53d3c5f31 100644 --- a/docs/en/rst/api/core/v1/bug.rst +++ b/docs/en/rst/api/core/v1/bug.rst @@ -319,6 +319,7 @@ field name in ``include_fields``. * Bug ID Fields: (int) * Multiple-Selection Fields: (array of strings) * Date/Time Fields: (datetime) +* Date Fields: (string) In the format ``YYYY-MM-DD``. **Errors** @@ -1013,7 +1014,8 @@ work_time double The number of hours worked on this bug as part You can also set the value of any custom field by passing its name as a parameter, and the value to set the field to. For multiple-selection -fields, the value should be an array of strings. +fields, the value should be an array of strings. For date fields, the value +should be a string in the format ``YYYY-MM-DD``. Flag change object: diff --git a/qa/config/generate_test_data.pl b/qa/config/generate_test_data.pl index ec65ad3474..16367f9bdf 100644 --- a/qa/config/generate_test_data.pl +++ b/qa/config/generate_test_data.pl @@ -814,6 +814,26 @@ BEGIN obsolete => 0, values => [qw(one two three)], }, + { + name => 'cf_qa_date', + description => 'QA Date', + type => FIELD_TYPE_DATE, + sortkey => 300, + mailhead => 0, + enter_bug => 1, + custom => 1, + obsolete => 0, + }, + { + name => 'cf_qa_datetime', + description => 'QA DateTime', + type => FIELD_TYPE_DATETIME, + sortkey => 400, + mailhead => 0, + enter_bug => 1, + custom => 1, + obsolete => 0, + }, ); print "creating custom fields...\n"; diff --git a/qa/t/rest_bug_date_fields.t b/qa/t/rest_bug_date_fields.t new file mode 100644 index 0000000000..444aca36c2 --- /dev/null +++ b/qa/t/rest_bug_date_fields.t @@ -0,0 +1,120 @@ +#!/usr/bin/env perl +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +##################################################### +# Test for REST Bug.create() and Bug.update() with # +# DATE and DATETIME custom fields # +# POST /rest/bug # +# PUT /rest/bug/ # +##################################################### + +# FIELD_TYPE_DATE custom fields must be passed through to the Bug object as +# YYYY-MM-DD and returned in that same format, so a value read from the API +# can be sent back unchanged. FIELD_TYPE_DATETIME custom fields must still be +# converted from and to ISO 8601 by the REST server. See bug 2074690. + +use 5.10.1; +use strict; +use warnings; +use lib qw(lib ../../lib ../../local/lib/perl5); + +use Bugzilla; +use QA::Util qw(get_config); +use QA::Tests qw(create_bug_fields); +use QA::REST::Util qw(api_headers); + +use Test::Mojo; +use Test::More; + +use constant DATE_FIELD => 'cf_qa_date'; +use constant DATETIME_FIELD => 'cf_qa_datetime'; + +my $config = get_config(); +my $api_key = $config->{editbugs_user_api_key}; +my $url = Bugzilla->localconfig->urlbase; + +my $t = Test::Mojo->new(); + +sub check_bug_dates { + my ($bug_id, $date, $datetime, $desc) = @_; + my $fields = join(',', DATE_FIELD, DATETIME_FIELD); + $t->get_ok( + $url . "rest/bug/$bug_id?include_fields=$fields" => api_headers($api_key)) + ->status_is(200) + ->json_is('/bugs/0/' . DATE_FIELD, $date, + "$desc: date field has the right value") + ->json_is('/bugs/0/' . DATETIME_FIELD, $datetime, + "$desc: datetime field has the right value"); +} + +############################### +# Create with both field types # +############################### + +my $new_bug = create_bug_fields($config); +$new_bug->{+DATE_FIELD} = '2026-01-15'; +$new_bug->{+DATETIME_FIELD} = '2026-01-15T12:15:00Z'; + +$t->post_ok($url . 'rest/bug' => api_headers($api_key) => json => $new_bug) + ->status_is(200)->json_has('/id'); +my $bug_id = $t->tx->res->json->{id}; + +check_bug_dates($bug_id, '2026-01-15', '2026-01-15T12:15:00Z', 'After create'); + +############################### +# Update with both field types # +############################### + +$t->put_ok($url + . "rest/bug/$bug_id" => api_headers($api_key) => json => + {DATE_FIELD, '2026-02-20', DATETIME_FIELD, '2026-02-20T08:45:00Z'}) + ->status_is(200); + +check_bug_dates($bug_id, '2026-02-20', '2026-02-20T08:45:00Z', 'After update'); + +############################################# +# Date fields still reject a time component # +############################################# + +$t->put_ok($url + . "rest/bug/$bug_id" => api_headers($api_key) => json => + {DATE_FIELD, '2026-03-01 10:00:00'})->status_is(400) + ->json_is('/code' => 56) + ->json_like('/message' => qr/is not a legal date/); + +check_bug_dates($bug_id, '2026-02-20', '2026-02-20T08:45:00Z', + 'After rejected update'); + +# ISO 8601 with a time is rejected too. Date fields are returned as plain +# YYYY-MM-DD, so this is not a value a client would read back from the API. +$t->put_ok($url + . "rest/bug/$bug_id" => api_headers($api_key) => json => + {DATE_FIELD, '2026-03-01T00:00:00Z'})->status_is(400) + ->json_is('/code' => 56) + ->json_like('/message' => qr/is not a legal date/); + +check_bug_dates($bug_id, '2026-02-20', '2026-02-20T08:45:00Z', + 'After rejected ISO 8601 update'); + +########################################## +# Read-modify-write round trip is stable # +########################################## + +my $read_url = $url . "rest/bug/$bug_id?include_fields=" . DATE_FIELD; +$t->get_ok($read_url => api_headers($api_key))->status_is(200); +my $read_back = $t->tx->res->json->{bugs}->[0]->{+DATE_FIELD}; +ok(defined $read_back, 'Date field value was read back from the API'); + +$t->put_ok($url + . "rest/bug/$bug_id" => api_headers($api_key) => json => + {DATE_FIELD, $read_back})->status_is(200); + +check_bug_dates($bug_id, '2026-02-20', '2026-02-20T08:45:00Z', + 'After writing back the value read from the API'); + +done_testing();