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

Improve test coverage for database package #1372

Merged
merged 1 commit into from
Dec 15, 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
4 changes: 2 additions & 2 deletions cmd/server/assets/realmadmin/_form_codes.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
</div>
<div>
<div class="btn-grou dropright pb-2">
{{if $realm.ErrorsFor "SMSTextTemplate"}}<span class="text-danger oi oi-warning"></span>{{end}}
{{if $realm.ErrorsFor "smsTextTemplate"}}<span class="text-danger oi oi-warning"></span>{{end}}
<button type="button" id="sms-template-dropdown-title" class="btn btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{(index .smsTemplates 0).Label}}
</button>
Expand All @@ -214,7 +214,7 @@
<a class="dropdown-item" id="sms-template-new" href="#">New SMS template</a>
</div>
</div>
{{if $realm.ErrorsFor "SMSTextTemplate"}}
{{if $realm.ErrorsFor "smsTextTemplate"}}
<div class="invalid-feedback d-block mt-n1 pb-2">
Errors found for one or more SMS templates
</div>
Expand Down
29 changes: 29 additions & 0 deletions pkg/database/audit_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
package database

import (
"fmt"
"strings"
"time"

"github.com/google/exposure-notifications-verification-server/pkg/pagination"
"github.com/jinzhu/gorm"
)

// AuditEntry represents an event in the system. These records are purged after
Expand Down Expand Up @@ -63,6 +66,32 @@ type AuditEntry struct {
CreatedAt time.Time
}

// BeforeSave runs validations. If there are errors, the save fails.
func (a *AuditEntry) BeforeSave(tx *gorm.DB) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we worry about this causing runtime failures for other actions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields are NOT NULL in the database. A runtime error would already have existed, just with a pgx_violated_contstraint response instead.

if a.ActorID == "" {
a.AddError("actor_id", "cannot be blank")
}
if a.ActorDisplay == "" {
a.AddError("actor_display", "cannot be blank")
}

if a.Action == "" {
a.AddError("action", "cannot be blank")
}

if a.TargetID == "" {
a.AddError("target_id", "cannot be blank")
}
if a.TargetDisplay == "" {
a.AddError("target_display", "cannot be blank")
}

if msgs := a.ErrorMessages(); len(msgs) > 0 {
return fmt.Errorf("validation failed: %s", strings.Join(msgs, ", "))
}
return nil
}

// SaveAuditEntry saves the audit entry.
func (db *Database) SaveAuditEntry(a *AuditEntry) error {
return db.db.Save(a).Error
Expand Down
130 changes: 130 additions & 0 deletions pkg/database/audit_entry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 database

import (
"testing"
"time"

"github.com/google/exposure-notifications-verification-server/pkg/pagination"
)

func TestAuditEntry_BeforeSave(t *testing.T) {
t.Parallel()

cases := []struct {
structField string
field string
}{
{"ActorID", "actor_id"},
{"ActorDisplay", "actor_display"},
{"Action", "action"},
{"TargetID", "target_id"},
{"TargetDisplay", "target_display"},
}

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

t.Run(tc.field, func(t *testing.T) {
t.Parallel()
exerciseValidation(t, &AuditEntry{}, tc.structField, tc.field)
})
}
}

func TestDatabase_PurgeAuditEntries(t *testing.T) {
t.Parallel()

db, _ := testDatabaseInstance.NewDatabase(t, nil)
for i := 0; i < 5; i++ {
if err := db.SaveAuditEntry(&AuditEntry{
RealmID: 1,
ActorID: "actor:1",
ActorDisplay: "Actor",
Action: "created",
TargetID: "target:1",
TargetDisplay: "Target",
}); err != nil {
t.Fatal(err)
}
}

// Should not purge entries (too young).
{
n, err := db.PurgeAuditEntries(24 * time.Hour)
if err != nil {
t.Fatal(err)
}
if got, want := n, int64(0); got != want {
t.Errorf("expected %d to purge, got %d", want, got)
}
}

// Purges entries.
{
n, err := db.PurgeAuditEntries(1 * time.Nanosecond)
if err != nil {
t.Fatal(err)
}
if got, want := n, int64(5); got != want {
t.Errorf("expected %d to purge, got %d", want, got)
}
}
}

func TestDatabase_ListAudits(t *testing.T) {
t.Parallel()

t.Run("empty", func(t *testing.T) {
t.Parallel()

db, _ := testDatabaseInstance.NewDatabase(t, nil)

audits, _, err := db.ListAudits(&pagination.PageParams{Limit: 1})
if err != nil {
t.Fatal(err)
}
if got, want := len(audits), 0; got != want {
t.Errorf("expected %d audits, got %d: %v", want, got, audits)
}
})

t.Run("lists", func(t *testing.T) {
t.Parallel()

db, _ := testDatabaseInstance.NewDatabase(t, nil)
for i := 0; i < 5; i++ {
if err := db.SaveAuditEntry(&AuditEntry{
RealmID: 1,
ActorID: "actor:1",
ActorDisplay: "Actor",
Action: "created",
TargetID: "target:1",
TargetDisplay: "Target",
}); err != nil {
t.Fatal(err)
}
}

audits, _, err := db.ListAudits(&pagination.PageParams{Limit: 10})
if err != nil {
t.Fatal(err)
}
if got, want := len(audits), 5; got != want {
t.Errorf("expected %d audits, got %d: %v", want, got, audits)
}
})
}
49 changes: 9 additions & 40 deletions pkg/database/authorized_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ func (a *AuthorizedApp) BeforeSave(tx *gorm.DB) error {
a.AddError("type", "is invalid")
}

if len(a.Errors()) > 0 {
return fmt.Errorf("validation failed")
if msgs := a.ErrorMessages(); len(msgs) > 0 {
return fmt.Errorf("validation failed: %s", strings.Join(msgs, ", "))
}
return nil
}
Expand All @@ -109,51 +109,20 @@ func (a *AuthorizedApp) IsDeviceType() bool {
return a.APIKeyType == APIKeyTypeDevice
}

// Realm returns the associated realm for this app.
// Realm returns the associated realm for this app. If you only need the ID,
// call .RealmID instead of a full database lookup.
func (a *AuthorizedApp) Realm(db *Database) (*Realm, error) {
var realm Realm
if err := db.db.Model(a).Related(&realm).Error; err != nil {
if err := db.db.
Model(&Realm{}).
Where("id = ?", a.RealmID).
First(&realm).
Error; err != nil {
return nil, err
}
return &realm, nil
}

// TableName definition for the authorized apps relation.
func (AuthorizedApp) TableName() string {
return "authorized_apps"
}

// CreateAuthorizedApp generates a new API key and assigns it to the specified
// app. Note that the API key is NOT stored in the database, only a hash. The
// only time the API key is available is as the string return parameter from
// invoking this function.
func (r *Realm) CreateAuthorizedApp(db *Database, app *AuthorizedApp, actor Auditable) (string, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved into Realm

fullAPIKey, err := db.GenerateAPIKey(r.ID)
if err != nil {
return "", fmt.Errorf("failed to generate API key: %w", err)
}

parts := strings.SplitN(fullAPIKey, ".", 3)
if len(parts) != 3 {
return "", fmt.Errorf("internal error, key is invalid")
}
apiKey := parts[0]

hmacedKey, err := db.GenerateAPIKeyHMAC(apiKey)
if err != nil {
return "", fmt.Errorf("failed to create hmac: %w", err)
}

app.RealmID = r.ID
app.APIKey = hmacedKey
app.APIKeyPreview = apiKey[:6]

if err := db.SaveAuthorizedApp(app, actor); err != nil {
return "", err
}
return fullAPIKey, nil
}

// FindAuthorizedApp finds the authorized app by the given id.
func (db *Database) FindAuthorizedApp(id interface{}) (*AuthorizedApp, error) {
var app AuthorizedApp
Expand Down
96 changes: 96 additions & 0 deletions pkg/database/authorized_app_stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 database

import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
)

func TestAuthorizedAppStats_MarshalCSV(t *testing.T) {
t.Parallel()

cases := []struct {
name string
stats AuthorizedAppStats
exp string
}{
{
name: "empty",
stats: nil,
exp: "",
},
{
name: "single",
stats: []*AuthorizedAppStat{
{
Date: time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 10,
AuthorizedAppName: "Appy",
},
},
exp: `date,authorized_app_id,authorized_app_name,codes_issued
2020-02-03,1,Appy,10
`,
},
{
name: "multi",
stats: []*AuthorizedAppStat{
{
Date: time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 10,
AuthorizedAppName: "Appy",
},
{
Date: time.Date(2020, 2, 4, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 45,
AuthorizedAppName: "Mc",
},
{
Date: time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 15,
AuthorizedAppName: "Apperson",
},
},
exp: `date,authorized_app_id,authorized_app_name,codes_issued
2020-02-03,1,Appy,10
2020-02-04,1,Mc,45
2020-02-05,1,Apperson,15
`,
},
}

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

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

b, err := tc.stats.MarshalCSV()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(b), tc.exp); diff != "" {
t.Errorf("bad csv (+got, -want): %s", diff)
}
})
}
}
Loading