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

Test for single issue code #1432

Merged
merged 5 commits into from
Dec 18, 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
6 changes: 6 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
- [API Methods](#api-methods)
- [`/api/verify`](#apiverify)
- [`/api/certificate`](#apicertificate)
- [Admin APIs](#admin-apis)
- [`/api/issue`](#apiissue)
- [Client provided UUID to prevent duplicate SMS](#client-provided-uuid-to-prevent-duplicate-sms)
- [`/api/batch-issue`](#apibatch-issue)
- [Handling batch partial success/failure](#handling-batch-partial-successfailure)
- [`/api/checkcodestatus`](#apicheckcodestatus)
- [`/api/expirecode`](#apiexpirecode)
- [`/api/stats/*` (preview)](#apistats-preview)
Expand Down Expand Up @@ -275,6 +280,7 @@ Possible error code responses. New error codes may be added in future releases.
| `unparsable_request` | 400 | No | Client sent an request the sever cannot parse |
| `invalid_test_type` | 400 | No | The client sent an accept of an unrecognized test type |
| `missing_date` | 400 | No | The realm requires either a test or symptom date, but none was provided. |
| `invalid_date` | 400 | No | The provided test or symptom date, was older or newer than the realm allows. |
| `invalid_test_type` | 400 | No | The test type is not a valid test type (a string that is unknown to the server). |
| `uuid_already_exists` | 409 | No | The UUID has already been used for an issued code |
| `maintenance_mode ` | 429 | Yes | The server is temporarily down for maintenance. Wait and retry later. |
Expand Down
5 changes: 4 additions & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const (
// this could mean a database or RPC connection drop or some other internal outage.
ErrInternal = "internal_server_error"

// Verify API responses
// Verify & Issue API responses

// ErrVerifyCodeInvalid indicates the code entered is unknown or already used.
ErrVerifyCodeInvalid = "code_invalid"
Expand All @@ -60,6 +60,9 @@ const (
ErrInvalidTestType = "invalid_test_type"
// ErrMissingDate indicates the realm requires a date, but none was supplied.
ErrMissingDate = "missing_date"
// ErrInvalidDate indicates the realm requires a date, but the supplied date
// was older or newer than the allowed date ramge.
ErrInvalidDate = "invalid_date"
// ErrUUIDAlreadyExists indicates that the UUID has already been used for an issued code.
ErrUUIDAlreadyExists = "uuid_already_exists"
// ErrMaintenanceMode indicates that the server is read-only for maintenance.
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/issueapi/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (c *Controller) HandleIssue() http.Handler {
if err := controller.BindJSON(w, r, &request); err != nil {
result.obsBlame = observability.BlameClient
result.obsResult = observability.ResultError("FAILED_TO_PARSE_JSON_REQUEST")
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrUnparsableRequest))
return
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/issueapi/issue_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (c *Controller) HandleBatchIssue() http.Handler {
if err := controller.BindJSON(w, r, &request); err != nil {
result.obsBlame = observability.BlameClient
result.obsResult = observability.ResultError("FAILED_TO_PARSE_JSON_REQUEST")
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrUnparsableRequest))
return
}

Expand Down
24 changes: 3 additions & 21 deletions pkg/controller/issueapi/issue_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,6 @@ func TestIssueBatch(t *testing.T) {
t.Fatal(err)
}

existingCode := &database.VerificationCode{
RealmID: realm.ID,
Code: "00000001",
LongCode: "00000001ABC",
Claimed: true,
TestType: "confirmed",
ExpiresAt: time.Now().Add(time.Hour),
LongExpiresAt: time.Now().Add(time.Hour),
}
if err := db.SaveVerificationCode(existingCode, time.Hour); err != nil {
t.Fatal(err)
}

symptomDate := time.Now().UTC().Add(-48 * time.Hour).Format(project.RFC3339Date)
tzMinOffset := 0

Expand Down Expand Up @@ -131,9 +118,8 @@ func TestIssueBatch(t *testing.T) {
},
{
TestType: "confirmed",
SymptomDate: symptomDate,
SymptomDate: "unparsable date",
TZOffset: float32(tzMinOffset),
UUID: existingCode.UUID,
},
},
},
Expand All @@ -146,7 +132,7 @@ func TestIssueBatch(t *testing.T) {
ErrorCode: api.ErrInvalidTestType,
},
{
ErrorCode: api.ErrUUIDAlreadyExists,
ErrorCode: api.ErrUnparsableRequest,
},
},
ErrorCode: api.ErrInvalidTestType,
Expand Down Expand Up @@ -196,11 +182,7 @@ func TestIssueBatch(t *testing.T) {

for i, issuedCode := range resp.Codes {
if issuedCode.ErrorCode != tc.response.Codes[i].ErrorCode {
t.Errorf("did not receive expected inner errorCode. got %s, want %v", issuedCode.ErrorCode, tc.response.Codes[i].ErrorCode)
}

if tc.request.Codes[i].UUID != "" && tc.response.Codes[i].UUID != issuedCode.UUID {
t.Errorf("expected stable client-issued UUID. got %s, want %v", issuedCode.UUID, tc.response.Codes[i].UUID)
t.Errorf("did not receive expected inner errorCode. got %q, want %q", issuedCode.ErrorCode, tc.response.Codes[i].ErrorCode)
}
}
})
Expand Down
164 changes: 126 additions & 38 deletions pkg/controller/issueapi/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,147 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package issueapi
package issueapi_test

import (
"context"
"net/http"
"testing"
"time"

"github.com/google/exposure-notifications-verification-server/internal/project"
"github.com/google/exposure-notifications-verification-server/pkg/api"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/sms"
"github.com/google/exposure-notifications-verification-server/pkg/testsuite"
)

func TestDateValidation(t *testing.T) {
utc, err := time.LoadLocation("UTC")
func TestIssue(t *testing.T) {
t.Parallel()

ctx := context.Background()
testSuite := testsuite.NewIntegrationSuite(t, ctx)
adminClient, err := testSuite.NewAdminAPIClient(ctx, t)
if err != nil {
t.Fatalf("error loading utc")
t.Fatal(err)
}
var aug1 time.Time
aug1, err = time.ParseInLocation(project.RFC3339Date, "2020-08-01", utc)
if err != nil {
t.Fatalf("error parsing date")
db := testSuite.DB
realm := testSuite.Realm

realm.AllowedTestTypes = database.TestTypeConfirmed
if err := db.SaveRealm(realm, database.SystemTest); err != nil {
t.Fatal(err)
}

smsConfig := &database.SMSConfig{
RealmID: realm.ID,
ProviderType: sms.ProviderType(sms.ProviderTypeNoop),
}
if err := db.SaveSMSConfig(smsConfig); err != nil {
t.Fatal(err)
}

existingCode := &database.VerificationCode{
RealmID: realm.ID,
Code: "00000001",
LongCode: "00000001ABC",
Claimed: true,
TestType: "confirmed",
ExpiresAt: time.Now().Add(time.Hour),
LongExpiresAt: time.Now().Add(time.Hour),
}
if err := db.SaveVerificationCode(existingCode, time.Hour); err != nil {
t.Fatal(err)
}

symptomDate := time.Now().UTC().Add(-48 * time.Hour).Format(project.RFC3339Date)
tzMinOffset := 0

tests := []struct {
v string
max time.Time
tzOffset int
shouldErr bool
expected string
cases := []struct {
name string
request api.IssueCodeRequest
responseErr string
httpStatusCode int
}{
{"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"},
{
name: "success",
request: api.IssueCodeRequest{
TestType: "confirmed",
SymptomDate: symptomDate,
TZOffset: float32(tzMinOffset),
},
httpStatusCode: http.StatusOK,
},
{
name: "failure",
request: api.IssueCodeRequest{
TestType: "negative", // this realm only supports confirmed
SymptomDate: symptomDate,
TZOffset: float32(tzMinOffset),
},
responseErr: api.ErrUnsupportedTestType,
httpStatusCode: http.StatusBadRequest,
},
{
name: "no test date",
request: api.IssueCodeRequest{
TestType: "confirmed",
TZOffset: float32(tzMinOffset),
},
responseErr: api.ErrMissingDate,
httpStatusCode: http.StatusBadRequest,
},
{
name: "unparsable test date",
request: api.IssueCodeRequest{
TestType: "confirmed",
SymptomDate: "invalid date",
TZOffset: float32(tzMinOffset),
},
responseErr: api.ErrUnparsableRequest,
httpStatusCode: http.StatusBadRequest,
},
{
name: "really old test date",
request: api.IssueCodeRequest{
TestType: "confirmed",
SymptomDate: "1988-09-14",
TZOffset: float32(tzMinOffset),
},
responseErr: api.ErrInvalidDate,
httpStatusCode: http.StatusBadRequest,
},
{
name: "conflict",
request: api.IssueCodeRequest{
TestType: "confirmed",
SymptomDate: symptomDate,
TZOffset: float32(tzMinOffset),
UUID: existingCode.UUID,
},
responseErr: api.ErrUUIDAlreadyExists,
httpStatusCode: http.StatusConflict,
},
}
for i, test := range tests {
date, err := time.ParseInLocation(project.RFC3339Date, 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 {

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

statusCode, resp, err := adminClient.IssueCode(tc.request)
if err != nil {
if !test.shouldErr {
t.Fatalf("[%d] validateDate returned an unexpected error: %q", i, err)
}
} else {
t.Fatalf("[%d] expected error", i)
t.Fatal(err)
}

// Check outer error
if statusCode != tc.httpStatusCode {
t.Errorf("incorrect error code. got %d, want %d", statusCode, tc.httpStatusCode)
}
if resp.ErrorCode != tc.responseErr {
t.Errorf("did not receive expected errorCode. got %q, want %q", resp.ErrorCode, tc.responseErr)
}
} else if s := newDate.Format(project.RFC3339Date); s != test.expected {
t.Fatalf("[%d] validateDate returned a different date %q != %q", i, s, test.expected)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/controller/issueapi/logic.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (c *Controller) issue(ctx context.Context, authApp *database.AuthorizedApp,
obsBlame: observability.BlameClient,
obsResult: observability.ResultError(dateSettings[i].ValidateError),
httpCode: http.StatusBadRequest,
errorReturn: api.Error(err),
errorReturn: api.Error(err).WithCode(api.ErrInvalidDate),
}, nil
}
parsedDates[i] = validatedDate
Expand Down
70 changes: 70 additions & 0 deletions pkg/controller/issueapi/parse_date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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"

"github.com/google/exposure-notifications-verification-server/internal/project"
)

func TestDateValidation(t *testing.T) {
utc, err := time.LoadLocation("UTC")
if err != nil {
t.Fatalf("error loading utc")
}
var aug1 time.Time
aug1, err = time.ParseInLocation(project.RFC3339Date, "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(project.RFC3339Date, 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(project.RFC3339Date); s != test.expected {
t.Fatalf("[%d] validateDate returned a different date %q != %q", i, s, test.expected)
}
}
}
10 changes: 6 additions & 4 deletions pkg/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,17 @@ func (tc testCase) singleIssue(t *testing.T, adminClient *testsuite.AdminClient,
TZOffset: float32(tzMinOffset),
}

issueResp, err := adminClient.IssueCode(issueRequest)
if issueResp == nil || err != nil || issueResp.UUID == "" {
httpCode, issueResp, err := adminClient.IssueCode(issueRequest)
if issueResp == nil || err != nil || issueResp.UUID == "" || httpCode != http.StatusOK {
t.Fatalf("adminClient.IssueCode(%+v) = expected nil, got resp %+v, err %v", issueRequest, issueResp, err)
}

// Try to issue the same code again (same UUID)
issueRequest.UUID = issueResp.UUID
if _, err = adminClient.IssueCode(issueRequest); err == nil {
t.Fatalf("Expected conflict, got %s", err)
if httpCode, _, err = adminClient.IssueCode(issueRequest); httpCode != http.StatusConflict {
t.Fatalf("Expected 409 conflict, got %d", httpCode)
} else if err != nil {
t.Fatal(err)
}

verifyRequest := api.VerifyCodeRequest{
Expand Down
Loading