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 3 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
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.ErrUUIDAlreadyExists,
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