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

Commit

Permalink
Improve test coverage for database package (#1372)
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo authored Dec 15, 2020
1 parent ab52875 commit 494b99e
Show file tree
Hide file tree
Showing 29 changed files with 1,866 additions and 379 deletions.
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 {
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) {
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

0 comments on commit 494b99e

Please sign in to comment.