Repository: google/exposure-notifications-server
Pull request #1264 (2021-01-07 01:30:32)

improve test coverage on authorized app database/model

## Proposed Changes

* Improve test coverage

**Release Note**

```release-note
Improve unit test coverage on authorizedapp database/model packaged.
```

google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/1264#" title="Author self-approved">mikehelmick</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/internal/authorizedapp/database/authorized_app.go b/internal/authorizedapp/database/authorized_app.go
index 7a889fa..426f6a4 100644
--- a/internal/authorizedapp/database/authorized_app.go
+++ b/internal/authorizedapp/database/authorized_app.go
@@ -26,23 +26,28 @@ import (
 	pgx "github.com/jackc/pgx/v4"
 )
 
+// AuthorizedAppDB is a handle to database operations for authorized apps
+// (referred to as healthAuthorityID in v1 publish API).
 type AuthorizedAppDB struct {
 	db *database.DB
 }
 
+// New creates a new authorizedAppDB that wraps a raw database handle.
 func New(db *database.DB) *AuthorizedAppDB {
 	return &AuthorizedAppDB{
 		db: db,
 	}
 }
 
+// InsertAuthorizedApp inserts an authorized app into the database, caling the validate method first
+// and returning any errors.
 func (aa *AuthorizedAppDB) InsertAuthorizedApp(ctx context.Context, m *model.AuthorizedApp) error {
 	if errors := m.Validate(); len(errors) > 0 {
 		return fmt.Errorf("AuthorizedApp invalid: %v", strings.Join(errors, ", "))
 	}
 
 	return aa.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
-		result, err := tx.Exec(ctx, `
+		_, err := tx.Exec(ctx, `
 			INSERT INTO
 				AuthorizedApp
 				(app_package_name, allowed_regions,
@@ -56,13 +61,11 @@ func (aa *AuthorizedAppDB) InsertAuthorizedApp(ctx context.Context, m *model.Aut
 		if err != nil {
 			return fmt.Errorf("inserting authorizedapp: %w", err)
 		}
-		if result.RowsAffected() != 1 {
-			return fmt.Errorf("no rows inserted")
-		}
 		return nil
 	})
 }
 
+// UpdateAuthorizedApp updates the properties of an authorized app, including possibly renaming it.
 func (aa *AuthorizedAppDB) UpdateAuthorizedApp(ctx context.Context, priorKey string, m *model.AuthorizedApp) error {
 	return aa.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
 		result, err := tx.Exec(ctx, `
@@ -86,6 +89,7 @@ func (aa *AuthorizedAppDB) UpdateAuthorizedApp(ctx context.Context, priorKey str
 	})
 }
 
+// DeleteAuthorizedApp removes an authorized app from the database.
 func (aa *AuthorizedAppDB) DeleteAuthorizedApp(ctx context.Context, name string) error {
 	var count int64
 	err := aa.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
@@ -110,6 +114,8 @@ func (aa *AuthorizedAppDB) DeleteAuthorizedApp(ctx context.Context, name string)
 	return nil
 }
 
+// ListAuthorizedApps reads all authorized app, returned in alphabetical order by
+// healthAuthorityID (app_package_name).
 func (aa *AuthorizedAppDB) ListAuthorizedApps(ctx context.Context) ([]*model.AuthorizedApp, error) {
 	var apps []*model.AuthorizedApp
 
diff --git a/internal/authorizedapp/model/authorized_app_model.go b/internal/authorizedapp/model/authorized_app_model.go
index 215250e..e5c979d 100644
--- a/internal/authorizedapp/model/authorized_app_model.go
+++ b/internal/authorizedapp/model/authorized_app_model.go
@@ -16,6 +16,7 @@
 package model
 
 import (
+	"sort"
 	"strings"
 )
 
@@ -40,6 +41,8 @@ type AuthorizedApp struct {
 	BypassRevisionToken bool
 }
 
+// NewAuthorizedApp initializes an AuthorizedApp structure including
+// pre-allocating all included maps.
 func NewAuthorizedApp() *AuthorizedApp {
 	return &AuthorizedApp{
 		AllowedRegions:            make(map[string]struct{}),
@@ -47,6 +50,7 @@ func NewAuthorizedApp() *AuthorizedApp {
 	}
 }
 
+// AllAllowedRegions returns a slice of all allowed region codes.
 func (c *AuthorizedApp) AllAllowedRegions() []string {
 	regions := []string{}
 	for k := range c.AllowedRegions {
@@ -55,6 +59,8 @@ func (c *AuthorizedApp) AllAllowedRegions() []string {
 	return regions
 }
 
+// AllAllowedHealthAuthorityIDs returns a slice of all allowed
+// heauth authority IDs.
 func (c *AuthorizedApp) AllAllowedHealthAuthorityIDs() []int64 {
 	has := []int64{}
 	for k := range c.AllowedHealthAuthorityIDs {
@@ -63,6 +69,7 @@ func (c *AuthorizedApp) AllAllowedHealthAuthorityIDs() []int64 {
 	return has
 }
 
+// Validate checks an authorized app before a save operation.
 func (c *AuthorizedApp) Validate() []string {
 	errors := make([]string, 0)
 	if c.AppPackageName == "" {
@@ -74,17 +81,23 @@ func (c *AuthorizedApp) Validate() []string {
 	return errors
 }
 
+// RegionsOnePerLine returns a string with all authorized
+// regions, one per line. This is a utility method for the
+// admin console.
 func (c *AuthorizedApp) RegionsOnePerLine() string {
-	regions := []string{}
+	var regions sort.StringSlice
 	for r := range c.AllowedRegions {
 		regions = append(regions, r)
 	}
+	regions.Sort()
 	return strings.Join(regions, "\n")
 }
 
 // IsAllowedRegion returns true if the regions list is empty or if the given
 // region is in the list of allowed regions.
 func (c *AuthorizedApp) IsAllowedRegion(s string) bool {
+	// This is a legacy inconsistency. Anything going through the admin
+	// console will have allowed regions set.
 	if len(c.AllowedRegions) == 0 {
 		return true
 	}
diff --git a/internal/efgs/regions.go b/internal/efgs/regions.go
deleted file mode 100644
index fc79589..0000000
--- a/internal/efgs/regions.go
+++ /dev/null
@@ -1,272 +0,0 @@
-// 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 efgs contains utilities for interacting eith the EU Federation Gateway.
-package efgs
-
-var (
-	// TravelerRegions represents regions a key has been in, if the traveler flag is true.
-	// Data source: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
-	TravelerRegions = []string{
-		"AD", //	Andorra
-		"AE", //	United Arab Emirates
-		"AF", //	Afghanistan
-		"AG", //	Antigua and Barbuda
-		"AI", //	Anguilla
-		"AL", //	Albania
-		"AM", //	Armenia
-		"AO", //	Angola
-		"AQ", //	Antarctica
-		"AR", //	Argentina
-		"AS", //	American Samoa
-		"AT", //	Austria
-		"AU", //	Australia
-		"AW", //	Aruba
-		"AX", //	Åland Islands
-		"AZ", //	Azerbaijan
-		"BA", //	Bosnia and Herzegovina
-		"BB", //	Barbados
-		"BD", //	Bangladesh
-		"BE", //	Belgium
-		"BF", //	Burkina Faso
-		"BG", //	Bulgaria
-		"BH", //	Bahrain
-		"BI", //	Burundi
-		"BJ", //	Benin
-		"BL", //	Saint Barthélemy
-		"BM", //	Bermuda
-		"BN", //	Brunei Darussalam
-		"BO", //	Bolivia (Plurinational State of)
-		"BQ", //	Bonaire, Sint Eustatius and Saba
-		"BR", //	Brazil
-		"BS", //	Bahamas
-		"BT", //	Bhutan
-		"BV", //	Bouvet Island
-		"BW", //	Botswana
-		"BY", //	Belarus
-		"BZ", //	Belize
-		"CA", //	Canada
-		"CC", //	Cocos (Keeling) Islands
-		"CD", //	Congo, Democratic Republic of the
-		"CF", //	Central African Republic
-		"CG", //	Congo
-		"CH", //	Switzerland
-		"CI", //	Côte d'Ivoire
-		"CK", //	Cook Islands
-		"CL", //	Chile
-		"CM", //	Cameroon
-		"CN", //	China
-		"CO", //	Colombia
-		"CR", //	Costa Rica
-		"CU", //	Cuba
-		"CV", //	Cabo Verde
-		"CW", //	Curaçao
-		"CX", //	Christmas Island
-		"CY", //	Cyprus
-		"CZ", //	Czechia
-		"DE", //	Germany
-		"DJ", //	Djibouti
-		"DK", //	Denmark
-		"DM", //	Dominica
-		"DO", //	Dominican Republic
-		"DZ", //	Algeria
-		"EC", //	Ecuador
-		"EE", //	Estonia
-		"EG", //	Egypt
-		"EH", //	Western Sahara
-		"ER", //	Eritrea
-		"ES", //	Spain
-		"ET", //	Ethiopia
-		"FI", //	Finland
-		"FJ", //	Fiji
-		"FK", //	Falkland Islands (Malvinas)
-		"FM", //	Micronesia (Federated States of)
-		"FO", //	Faroe Islands
-		"FR", //	France
-		"GA", //	Gabon
-		"GB", //	United Kingdom of Great Britain and Northern Ireland
-		"GD", //	Grenada
-		"GE", //	Georgia
-		"GF", //	French Guiana
-		"GG", //	Guernsey
-		"GH", //	Ghana
-		"GI", //	Gibraltar
-		"GL", //	Greenland
-		"GM", //	Gambia
-		"GN", //	Guinea
-		"GP", //	Guadeloupe
-		"GQ", //	Equatorial Guinea
-		"GR", //	Greece
-		"GS", //	South Georgia and the South Sandwich Islands
-		"GT", //	Guatemala
-		"GU", //	Guam
-		"GW", //	Guinea-Bissau
-		"GY", //	Guyana
-		"HK", //	Hong Kong
-		"HM", //	Heard Island and McDonald Islands
-		"HN", //	Honduras
-		"HR", //	Croatia
-		"HT", //	Haiti
-		"HU", //	Hungary
-		"ID", //	Indonesia
-		"IE", //	Ireland
-		"IL", //	Israel
-		"IM", //	Isle of Man
-		"IN", //	India
-		"IO", //	British Indian Ocean Territory
-		"IQ", //	Iraq
-		"IR", //	Iran (Islamic Republic of)
-		"IS", //	Iceland
-		"IT", //	Italy
-		"JE", //	Jersey
-		"JM", //	Jamaica
-		"JO", //	Jordan
-		"JP", //	Japan
-		"KE", //	Kenya
-		"KG", //	Kyrgyzstan
-		"KH", //	Cambodia
-		"KI", //	Kiribati
-		"KM", //	Comoros
-		"KN", //	Saint Kitts and Nevis
-		"KP", //	Korea (Democratic People's Republic of)
-		"KR", //	Korea, Republic of
-		"KW", //	Kuwait
-		"KY", //	Cayman Islands
-		"KZ", //	Kazakhstan
-		"LA", //	Lao People's Democratic Republic
-		"LB", //	Lebanon
-		"LC", //	Saint Lucia
-		"LI", //	Liechtenstein
-		"LK", //	Sri Lanka
-		"LR", //	Liberia
-		"LS", //	Lesotho
-		"LT", //	Lithuania
-		"LU", //	Luxembourg
-		"LV", //	Latvia
-		"LY", //	Libya
-		"MA", //	Morocco
-		"MC", //	Monaco
-		"MD", //	Moldova, Republic of
-		"ME", //	Montenegro
-		"MF", //	Saint Martin (French part)
-		"MG", //	Madagascar
-		"MH", //	Marshall Islands
-		"MK", //	North Macedonia
-		"ML", //	Mali
-		"MM", //	Myanmar
-		"MN", //	Mongolia
-		"MO", //	Macao
-		"MP", //	Northern Mariana Islands
-		"MQ", //	Martinique
-		"MR", //	Mauritania
-		"MS", //	Montserrat
-		"MT", //	Malta
-		"MU", //	Mauritius
-		"MV", //	Maldives
-		"MW", //	Malawi
-		"MX", //	Mexico
-		"MY", //	Malaysia
-		"MZ", //	Mozambique
-		"NA", //	Namibia
-		"NC", //	New Caledonia
-		"NE", //	Niger
-		"NF", //	Norfolk Island
-		"NG", //	Nigeria
-		"NI", //	Nicaragua
-		"NL", //	Netherlands
-		"NO", //	Norway
-		"NP", //	Nepal
-		"NR", //	Nauru
-		"NU", //	Niue
-		"NZ", //	New Zealand
-		"OM", //	Oman
-		"PA", //	Panama
-		"PE", //	Peru
-		"PF", //	French Polynesia
-		"PG", //	Papua New Guinea
-		"PH", //	Philippines
-		"PK", //	Pakistan
-		"PL", //	Poland
-		"PM", //	Saint Pierre and Miquelon
-		"PN", //	Pitcairn
-		"PR", //	Puerto Rico
-		"PS", //	Palestine, State of
-		"PT", //	Portugal
-		"PW", //	Palau
-		"PY", //	Paraguay
-		"QA", //	Qatar
-		"RE", //	Réunion
-		"RO", //	Romania
-		"RS", //	Serbia
-		"RU", //	Russian Federation
-		"RW", //	Rwanda
-		"SA", //	Saudi Arabia
-		"SB", //	Solomon Islands
-		"SC", //	Seychelles
-		"SD", //	Sudan
-		"SE", //	Sweden
-		"SG", //	Singapore
-		"SH", //	Saint Helena, Ascension and Tristan da Cunha
-		"SI", //	Slovenia
-		"SJ", //	Svalbard and Jan Mayen
-		"SK", //	Slovakia
-		"SL", //	Sierra Leone
-		"SM", //	San Marino
-		"SN", //	Senegal
-		"SO", //	Somalia
-		"SR", //	Suriname
-		"SS", //	South Sudan
-		"ST", //	Sao Tome and Principe
-		"SV", //	El Salvador
-		"SX", //	Sint Maarten (Dutch part)
-		"SY", //	Syrian Arab Republic
-		"SZ", //	Eswatini
-		"TC", //	Turks and Caicos Islands
-		"TD", //	Chad
-		"TF", //	French Southern Territories
-		"TG", //	Togo
-		"TH", //	Thailand
-		"TJ", //	Tajikistan
-		"TK", //	Tokelau
-		"TL", //	Timor-Leste
-		"TM", //	Turkmenistan
-		"TN", //	Tunisia
-		"TO", //	Tonga
-		"TR", //	Turkey
-		"TT", //	Trinidad and Tobago
-		"TV", //	Tuvalu
-		"TW", //	Taiwan, Province of China
-		"TZ", //	Tanzania, United Republic of
-		"UA", //	Ukraine
-		"UG", //	Uganda
-		"UM", //	United States Minor Outlying Islands
-		"US", //	United States of America
-		"UY", //	Uruguay
-		"UZ", //	Uzbekistan
-		"VA", //	Holy See
-		"VC", //	Saint Vincent and the Grenadines
-		"VE", //	Venezuela (Bolivarian Republic of)
-		"VG", //	Virgin Islands (British)
-		"VI", //	Virgin Islands (U.S.)
-		"VN", //	Viet Nam
-		"VU", //	Vanuatu
-		"WF", //	Wallis and Futuna
-		"WS", //	Samoa
-		"YE", //	Yemen
-		"YT", //	Mayotte
-		"ZA", //	South Africa
-		"ZM", //	Zambia
-		"ZW", //	Zimbabwe
-	}
-)


Test patch:
diff --git a/internal/authorizedapp/database/authorized_app_test.go b/internal/authorizedapp/database/authorized_app_test.go
index 8628cae..3ad5bc9 100644
--- a/internal/authorizedapp/database/authorized_app_test.go
+++ b/internal/authorizedapp/database/authorized_app_test.go
@@ -17,6 +17,7 @@ package database
 import (
 	"context"
 	"crypto/ecdsa"
+	"strings"
 	"testing"
 
 	"github.com/google/exposure-notifications-server/internal/authorizedapp/model"
@@ -24,6 +25,33 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
+func TestAuthorizedAppInsert_Errors(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	testDB, _ := testDatabaseInstance.NewDatabase(t)
+	aadb := New(testDB)
+
+	source := &model.AuthorizedApp{}
+
+	validationError := "AuthorizedApp invalid: Health Authority ID cannot be empty"
+	if err := aadb.InsertAuthorizedApp(ctx, source); err == nil {
+		t.Fatal("expected validation error")
+	} else if !strings.Contains(err.Error(), validationError) {
+		t.Fatalf("wrong error, want: %q got: %q", validationError, err.Error())
+	}
+
+	source.AppPackageName = "foo"
+	source.AllowedRegions = map[string]struct{}{"US": {}}
+	if err := aadb.InsertAuthorizedApp(ctx, source); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := aadb.InsertAuthorizedApp(ctx, source); err == nil {
+		t.Fatal("expected error")
+	}
+}
+
 func TestAuthorizedAppLifecycle(t *testing.T) {
 	t.Parallel()
 
@@ -73,6 +101,74 @@ func TestAuthorizedAppLifecycle(t *testing.T) {
 	if readBack != nil {
 		t.Fatal("expected record to be deleted, but it wasn't")
 	}
+
+	// attempt to delete already deleted app.
+	expectedError := "no rows were deleted"
+	if err := aadb.DeleteAuthorizedApp(ctx, source.AppPackageName); err == nil {
+		t.Fatal("expected error deleting already deleted app")
+	} else if !strings.Contains(err.Error(), expectedError) {
+		t.Fatalf("wrong error, want: %q got: %q", expectedError, err.Error())
+	}
+}
+
+func TestUpdateAuthorizedApp_NoRows(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	testDB, _ := testDatabaseInstance.NewDatabase(t)
+	aadb := New(testDB)
+
+	source := &model.AuthorizedApp{
+		AppPackageName:            "myapp",
+		AllowedRegions:            map[string]struct{}{"US": {}},
+		AllowedHealthAuthorityIDs: map[int64]struct{}{1: {}, 2: {}},
+	}
+
+	if err := aadb.InsertAuthorizedApp(ctx, source); err != nil {
+		t.Fatal(err)
+	}
+
+	expectedError := "no rows updated"
+	if err := aadb.UpdateAuthorizedApp(ctx, "wrongKey", source); err == nil {
+		t.Fatal("expected error, but didn't get one")
+	} else if !strings.Contains(err.Error(), expectedError) {
+		t.Fatalf("wrong error, want: %q got: %q", expectedError, err.Error())
+	}
+}
+
+func TestUpdateAuthorizedApp_Errors(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	testDB, _ := testDatabaseInstance.NewDatabase(t)
+	aadb := New(testDB)
+
+	appA := &model.AuthorizedApp{
+		AppPackageName:            "appA",
+		AllowedRegions:            map[string]struct{}{"US": {}},
+		AllowedHealthAuthorityIDs: map[int64]struct{}{1: {}, 2: {}},
+	}
+	if err := aadb.InsertAuthorizedApp(ctx, appA); err != nil {
+		t.Fatal(err)
+	}
+
+	appB := &model.AuthorizedApp{
+		AppPackageName:            "appB",
+		AllowedRegions:            map[string]struct{}{"US": {}},
+		AllowedHealthAuthorityIDs: map[int64]struct{}{1: {}, 2: {}},
+	}
+	if err := aadb.InsertAuthorizedApp(ctx, appB); err != nil {
+		t.Fatal(err)
+	}
+
+	appB.AppPackageName = appA.AppPackageName
+
+	expectedError := "updating authorizedapp: ERROR: duplicate key value violates unique constraint"
+	if err := aadb.UpdateAuthorizedApp(ctx, "appB", appB); err == nil {
+		t.Fatal("expected error, but didn't get one")
+	} else if !strings.Contains(err.Error(), expectedError) {
+		t.Fatalf("wrong error, want: %q got: %q", expectedError, err.Error())
+	}
 }
 
 func TestGetAuthorizedApp(t *testing.T) {
@@ -154,3 +250,51 @@ func TestGetAuthorizedApp(t *testing.T) {
 		})
 	}
 }
+
+func TestListAuthorizedApps(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	testDB, _ := testDatabaseInstance.NewDatabase(t)
+	aadb := New(testDB)
+
+	apps := []*model.AuthorizedApp{
+		{
+			AppPackageName: "apple",
+			AllowedRegions: map[string]struct{}{"US": {}},
+			AllowedHealthAuthorityIDs: map[int64]struct{}{
+				1: {},
+			},
+		},
+		{
+			AppPackageName: "banana",
+			AllowedRegions: map[string]struct{}{"US": {}},
+			AllowedHealthAuthorityIDs: map[int64]struct{}{
+				3: {},
+			},
+		},
+		{
+			AppPackageName: "cherry",
+			AllowedRegions: map[string]struct{}{"US": {}},
+			AllowedHealthAuthorityIDs: map[int64]struct{}{
+				2: {},
+			},
+		},
+	}
+
+	for _, newApp := range apps {
+		if err := aadb.InsertAuthorizedApp(ctx, newApp); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	got, err := aadb.ListAuthorizedApps(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if diff := cmp.Diff(apps, got); diff != "" {
+		t.Errorf("mismatch (-want, +got):\n%s", diff)
+	}
+
+}
diff --git a/internal/authorizedapp/model/authorized_app_model_test.go b/internal/authorizedapp/model/authorized_app_model_test.go
index bb2f97e..c51f9c4 100644
--- a/internal/authorizedapp/model/authorized_app_model_test.go
+++ b/internal/authorizedapp/model/authorized_app_model_test.go
@@ -15,7 +15,11 @@
 package model
 
 import (
+	"strings"
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
 func TestAuthorizedApp_IsAllowedRegion(t *testing.T) {
@@ -32,3 +36,85 @@ func TestAuthorizedApp_IsAllowedRegion(t *testing.T) {
 		t.Errorf("expected CA to not be allowed")
 	}
 }
+
+func TestAllAllowedRegions(t *testing.T) {
+	cfg := NewAuthorizedApp()
+	cfg.AllowedRegions = map[string]struct{}{
+		"US": {},
+		"CA": {},
+	}
+
+	got := cfg.AllAllowedRegions()
+	want := []string{"US", "CA"}
+	sorter := cmpopts.SortSlices(func(a, b string) bool {
+		return strings.Compare(a, b) <= 0
+	})
+	if diff := cmp.Diff(want, got, sorter); diff != "" {
+		t.Fatalf("mismatch (-want, +got):\n%s", diff)
+	}
+}
+
+func TestRegionsOnePerLine(t *testing.T) {
+	cfg := NewAuthorizedApp()
+	cfg.AllowedRegions = map[string]struct{}{
+		"US": {},
+		"CA": {},
+	}
+
+	got := cfg.RegionsOnePerLine()
+	want := "CA\nUS"
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("mismatch (-want, +got):\n%s", diff)
+	}
+}
+
+func TestAllAllowedHealthAuthorityIDs(t *testing.T) {
+	cfg := NewAuthorizedApp()
+	cfg.AllowedHealthAuthorityIDs[12] = struct{}{}
+	cfg.AllowedHealthAuthorityIDs[42] = struct{}{}
+
+	got := cfg.AllAllowedHealthAuthorityIDs()
+	want := []int64{12, 42}
+	sorter := cmpopts.SortSlices(func(a, b int64) bool {
+		return a <= b
+	})
+	if diff := cmp.Diff(want, got, sorter); diff != "" {
+		t.Fatalf("mismatch (-want, +got):\n%s", diff)
+	}
+}
+
+func TestValidate(t *testing.T) {
+	cfg := NewAuthorizedApp()
+
+	got := cfg.Validate()
+	want := []string{
+		"Health Authority ID cannot be empty",
+		"Regions list cannot be empty",
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Fatalf("mismatch (-want, +got):\n%s", diff)
+	}
+}
+
+func TestIsAllowedRegions(t *testing.T) {
+	cfg := NewAuthorizedApp()
+
+	if !cfg.IsAllowedRegion("foo") {
+		t.Errorf("region disallowed when all regions should be allowed")
+	}
+
+	cfg.AllowedRegions = map[string]struct{}{
+		"US": {},
+		"CA": {},
+	}
+
+	if !cfg.IsAllowedRegion("US") {
+		t.Errorf("missing expected allowed region: US")
+	}
+	if !cfg.IsAllowedRegion("CA") {
+		t.Errorf("missing expected allowed region: CA")
+	}
+	if cfg.IsAllowedRegion("GB") {
+		t.Errorf("unexpected allowed region: GB")
+	}
+}