Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Use local time zone when issuing a code. #340

Merged
merged 4 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions cmd/get-code/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import (
)

var (
testFlag = flag.String("type", "", "diagnosis test type: confirmed, likely, negative")
onsetFlag = flag.String("onset", "", "Symptom onset date, YYYY-MM-DD format")
apikeyFlag = flag.String("apikey", "", "API Key to use")
addrFlag = flag.String("addr", "http://localhost:8080", "protocol, address and port on which to make the API call")
timeoutFlag = flag.Duration("timeout", 5*time.Second, "request time out duration in the format: 0h0m0s")
testFlag = flag.String("type", "", "diagnosis test type: confirmed, likely, negative")
onsetFlag = flag.String("onset", "", "Symptom onset date, YYYY-MM-DD format")
tzOffsetFlag = flag.Int("tzOffset", 0, "timezone adjustment (minutes) from UTC for request")
apikeyFlag = flag.String("apikey", "", "API Key to use")
addrFlag = flag.String("addr", "http://localhost:8080", "protocol, address and port on which to make the API call")
timeoutFlag = flag.Duration("timeout", 5*time.Second, "request time out duration in the format: 0h0m0s")
)

func main() {
Expand All @@ -58,7 +59,7 @@ func main() {
func realMain(ctx context.Context) error {
logger := logging.FromContext(ctx)

request, response, err := clients.IssueCode(ctx, *addrFlag, *apikeyFlag, *testFlag, *onsetFlag, *timeoutFlag)
request, response, err := clients.IssueCode(ctx, *addrFlag, *apikeyFlag, *testFlag, *onsetFlag, *tzOffsetFlag, *timeoutFlag)
logger.Infow("sent request", "request", request)
if err != nil {
return fmt.Errorf("failed to get token: %w", err)
Expand Down
15 changes: 3 additions & 12 deletions cmd/server/assets/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,25 +102,15 @@ <h1>Create verification code</h1>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-6">
<label for="testDate">Testing date</label>
<label for="testDate">Testing date (local time)</label>
<input type="date" id="test-date" name="testDate" min="{{.minDate}}" max="{{.maxDate}}" class="form-control" />
<small class="form-text text-muted">
<strong>Recommended.</strong>
This is the <a href="https://www.timeanddate.com/worldclock/timezone/utc" target="_blank">UTC date</a> when the test was performed.
Click here to <a href="#" data-fill-target="test-date" data-fill-value="{{.maxDate}}">use today's UTC date</a>.
</small>
</div>

<div class="form-group col-md-6">
<label for="symptomDate">Symptoms onset</label>
<label for="symptomDate">Symptoms onset (local time)</label>
<div class="input-group">
<input type="date" id="symptom-date" name="symptomDate" min="{{.minDate}}" max="{{.maxDate}}" class="form-control" />
</div>
<small class="form-text text-muted">
<strong>Recommended.</strong>
This is the <a href="https://www.timeanddate.com/worldclock/timezone/utc" target="_blank">UTC date</a> when symptoms began. It cannot be more than {{.maxSymptomDays}} days ago.
Click here to <a href="#" data-fill-target="symptom-date" data-fill-value="{{.maxDate}}">use today's UTC date</a>.
</small>
</div>
</div>
</div>
Expand Down Expand Up @@ -274,6 +264,7 @@ <h1>Create verification code</h1>
$($form.serializeArray()).each(function(i, obj) {
data[obj.name] = obj.value
});
data.tzOffset = new Date().getTimezoneOffset();

getCode(data);
});
Expand Down
Binary file added e2e-test
Binary file not shown.
5 changes: 4 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ type IssueCodeRequest struct {
SymptomDate string `json:"symptomDate"` // ISO 8601 formatted date, YYYY-MM-DD
TestDate string `json:"testDate"`
TestType string `json:"testType"`
Phone string `json:"phone"`
// Offset in minutes of the user's timezone. Positive, negative, 0, or omitted
// (using the default of 0) are all valid. 0 is considered to be UTC.
TZOffset int `json:"tzOffset"`
Phone string `json:"phone"`
}

// IssueCodeResponse defines the response type for IssueCodeRequest.
Expand Down
3 changes: 2 additions & 1 deletion pkg/clients/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import (

// IssueCode uses the ADMIN API to issue a verification code.
// Currently does not accept the SMS param.
func IssueCode(ctx context.Context, hostname string, apiKey, testType, symptomDate string, timeout time.Duration) (*api.IssueCodeRequest, *api.IssueCodeResponse, error) {
func IssueCode(ctx context.Context, hostname string, apiKey, testType, symptomDate string, tzMinOffset int, timeout time.Duration) (*api.IssueCodeRequest, *api.IssueCodeResponse, error) {
url := hostname + "/api/issue"
request := api.IssueCodeRequest{
TestType: testType,
SymptomDate: symptomDate,
TZOffset: tzMinOffset,
}
client := &http.Client{
Timeout: timeout,
Expand Down
63 changes: 54 additions & 9 deletions pkg/controller/issueapi/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package issueapi

import (
"errors"
"fmt"
"net/http"
"strings"
Expand All @@ -27,6 +28,49 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/sms"
)

// Cache the UTC time.Location, to speed runtime.
var utc *time.Location

func init() {
var err error
utc, err = time.LoadLocation("UTC")
if err != nil {
panic("should have found UTC")
}
}

// validateDate validates the date given -- returning the time or an error.
func validateDate(date, minDate, maxDate time.Time, tzOffset int) (*time.Time, error) {
// Check that all our dates are utc.
if date.Location() != utc || minDate.Location() != utc || maxDate.Location() != utc {
return nil, errors.New("dates weren't in UTC")
}

// If we're dealing with a timezone where the offset is earlier than this one,
// we loosen up the lower bound. We might have the following circumstance:
//
// Server time: UTC Aug 1, 12:01 AM
// Client time: UTC July 30, 11:01 PM (ie, tzOffset = -30)
//
// In this circumstance, we'll have the following:
//
// minTime: UTC July 31, maxTime: Aug 1, clientTime: July 30.
//
// which would be an error. Loosening up the lower bound, by a day, keeps us
// all ok.
if tzOffset < 0 {
if m := minDate.Add(-24 * time.Hour); m.After(date) {
return nil, fmt.Errorf("date %v before min %v", date, m)
}
} else if minDate.After(date) {
return nil, fmt.Errorf("date %v before min %v", date, minDate)
}
if date.After(maxDate) {
return nil, fmt.Errorf("date %v after max %v", date, maxDate)
}
return &date, nil
}

func (c *Controller) HandleIssue() http.Handler {
logger := c.logger.Named("issueapi.HandleIssue")

Expand Down Expand Up @@ -96,25 +140,26 @@ func (c *Controller) HandleIssue() http.Handler {
return
}

// Max date is today (local time) and min date is AllowedTestAge ago, truncated.
maxDate := time.Now().Local()
minDate := maxDate.Add(-1 * c.config.GetAllowedSymptomAge()).Truncate(24 * time.Hour)

var symptomDate *time.Time
if request.SymptomDate != "" {
if parsed, err := time.Parse("2006-01-02", request.SymptomDate); err != nil {
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("failed to process symptom onset date: %v", err))
return
} else {
parsed = parsed.Local()
if minDate.After(parsed) || parsed.After(maxDate) {
err := fmt.Errorf("symptom onset date must be on/after %v and on/before %v",
// Max date is today (UTC time) and min date is AllowedTestAge ago, truncated.
maxDate := time.Now().UTC().Truncate(24 * time.Hour)
minDate := maxDate.Add(-1 * c.config.GetAllowedSymptomAge()).Truncate(24 * time.Hour)

symptomDate, err = validateDate(parsed, minDate, maxDate, request.TZOffset)
if err != nil {
err := fmt.Errorf("symptom onset date must be on/after %v and on/before %v %v",
minDate.Format("2006-01-02"),
maxDate.Format("2006-01-02"))
maxDate.Format("2006-01-02"),
parsed.Format("2006-01-02"),
)
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
return
}
symptomDate = &parsed
}
}

Expand Down
68 changes: 68 additions & 0 deletions pkg/controller/issueapi/issue_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package issueapi

import (
"testing"
"time"
)

func TestDateValidationn(t *testing.T) {
utc, err := time.LoadLocation("UTC")
if err != nil {
t.Fatalf("error loading utc")
}
var aug1 time.Time
aug1, err = time.ParseInLocation("2006-01-02", "2020-08-01", utc)
if err != nil {
t.Fatalf("error parsing date")
}

tests := []struct {
v string
max time.Time
tzOffset int
shouldErr bool
expected string
}{
{"2020-08-01", aug1, 0, false, "2020-08-01"},
{"2020-08-01", aug1, 60, false, "2020-08-01"},
{"2020-08-01", aug1, 60 * 12, false, "2020-08-01"},
{"2020-07-31", aug1, 60, false, "2020-07-31"},
{"2020-08-01", aug1, -60, false, "2020-08-01"},
{"2020-07-31", aug1, -60, false, "2020-07-31"},
{"2020-07-30", aug1, -60, false, "2020-07-30"},
{"2020-07-29", aug1, -60, true, "2020-07-30"},
}
for i, test := range tests {
date, err := time.ParseInLocation("2006-01-02", test.v, utc)
if err != nil {
t.Fatalf("[%d] error parsing date %q", i, test.v)
}
min := test.max.Add(-24 * time.Hour)
var newDate *time.Time
if newDate, err = validateDate(date, min, test.max, test.tzOffset); newDate == nil {
if err != nil {
if !test.shouldErr {
t.Fatalf("[%d] validateDate returned an unexpected error: %q", i, err)
}
} else {
t.Fatalf("[%d] expected error", i)
}
} else if s := newDate.Format("2006-01-02"); s != test.expected {
t.Fatalf("[%d] validateDate returned a different date %q != %q", i, s, test.expected)
}
}
}
2 changes: 1 addition & 1 deletion tools/e2e-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func realMain(ctx context.Context) error {
for i := 0; i < iterations; i++ {
// Issue the verification code.
logger.Infof("Issuing verification code, iteration %d", i)
codeRequest, code, err := clients.IssueCode(ctx, config.VerificationAdminAPIServer, config.VerificationAdminAPIKey, testType, symptomDate, timeout)
codeRequest, code, err := clients.IssueCode(ctx, config.VerificationAdminAPIServer, config.VerificationAdminAPIKey, testType, symptomDate, 0, timeout)
if err != nil {
return fmt.Errorf("error issuing verification code: %w", err)
} else if code.Error != "" {
Expand Down