Repository: google/exposure-notifications-server
Pull request #1095 (2020-10-17 02:34:50)

Efgs upload model
Towards #965

## Proposed Changes

* EFGS Upload batches will be attached to existing exports. 
* Introduce necessary database fields to configure this
* Add ISO code slice that represents where traveler keys will go.
  * Hopefully will come to a better solution in: https://github.com/eu-federation-gateway-service/efgs-federation-gateway/issues/216

**Release Note**

```release-note
NONE
```

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/1095#" 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":[]} -->
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/1095#pullrequestreview-511005700" title="Approved">jeremyfaller</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/1095#" 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)~~ [jeremyfaller,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/efgs/regions.go b/internal/efgs/regions.go
new file mode 100644
index 0000000..fc79589
--- /dev/null
+++ b/internal/efgs/regions.go
@@ -0,0 +1,272 @@
+// 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
+	}
+)
diff --git a/internal/export/database/export.go b/internal/export/database/export.go
index 3dc9638..0cab273 100644
--- a/internal/export/database/export.go
+++ b/internal/export/database/export.go
@@ -49,20 +49,27 @@ func (db *ExportDB) AddExportConfig(ctx context.Context, ec *model.ExportConfig)
 
 	thru := db.db.NullableTime(ec.Thru)
 	return db.db.InTx(ctx, pgx.Serializable, func(tx pgx.Tx) error {
+		efgs := &ec.EFGSConfig
 		row := tx.QueryRow(ctx, `
 			INSERT INTO
 				ExportConfig
-				(bucket_name, filename_root, period_seconds, output_region, from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers, exclude_regions, only_non_travelers)
+				(bucket_name, filename_root, period_seconds, output_region, from_timestamp, thru_timestamp,
+				 signature_info_ids, input_regions, include_travelers, exclude_regions, only_non_travelers,
+				  efgs_export, efgs_upload_host, efgs_mtls_cert_secret, efgs_mtls_key_secret,
+				  efgs_signing_cert_secret, efgs_signing_key_secret)
 			VALUES
-				($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
+				($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
 			RETURNING config_id
 		`, ec.BucketName, ec.FilenameRoot, int(ec.Period.Seconds()), ec.OutputRegion,
 			ec.From, thru, ec.SignatureInfoIDs, ec.InputRegions, ec.IncludeTravelers,
-			ec.ExcludeRegions, ec.OnlyNonTravelers)
+			ec.ExcludeRegions, ec.OnlyNonTravelers,
+			efgs.Enabled, efgs.UploadHost, efgs.MTLSCertSecret, efgs.MTLSKeySecret,
+			efgs.SigningCertSecret, efgs.SigningKeySecret)
 
 		if err := row.Scan(&ec.ConfigID); err != nil {
 			return fmt.Errorf("fetching config_id: %w", err)
 		}
+
 		return nil
 	})
 }
@@ -79,11 +86,18 @@ func (db *ExportDB) UpdateExportConfig(ctx context.Context, ec *model.ExportConf
 			UPDATE
 				ExportConfig
 			SET
-				bucket_name = $1, filename_root = $2, period_seconds = $3, output_region = $4, from_timestamp = $5, thru_timestamp = $6, signature_info_ids = $7, input_regions = $8, include_travelers = $9, exclude_regions = $10, only_non_travelers = $11
-			WHERE config_id = $12
+				bucket_name = $1, filename_root = $2, period_seconds = $3, output_region = $4, from_timestamp = $5,
+				thru_timestamp = $6, signature_info_ids = $7, input_regions = $8, include_travelers = $9,
+				exclude_regions = $10, only_non_travelers = $11,
+				efgs_export = $12, efgs_upload_host = $13, efgs_mtls_cert_secret = $14, efgs_mtls_key_secret = $15,
+				efgs_signing_cert_secret = $16, efgs_signing_key_secret = $17
+			WHERE config_id = $18
 		`, ec.BucketName, ec.FilenameRoot, int(ec.Period.Seconds()), ec.OutputRegion,
 			ec.From, thru, ec.SignatureInfoIDs, ec.InputRegions, ec.IncludeTravelers,
-			ec.ExcludeRegions, ec.OnlyNonTravelers, ec.ConfigID)
+			ec.ExcludeRegions, ec.OnlyNonTravelers, ec.EFGSConfig.Enabled, ec.EFGSConfig.UploadHost,
+			ec.EFGSConfig.MTLSCertSecret, ec.EFGSConfig.MTLSKeySecret,
+			ec.EFGSConfig.SigningCertSecret, ec.EFGSConfig.SigningKeySecret,
+			ec.ConfigID)
 		if err != nil {
 			return fmt.Errorf("updating signatureinfo: %w", err)
 		}
@@ -99,7 +113,11 @@ func (db *ExportDB) GetExportConfig(ctx context.Context, id int64) (*model.Expor
 	if err := db.db.InTx(ctx, pgx.ReadCommitted, func(tx pgx.Tx) error {
 		row := tx.QueryRow(ctx, `
 			SELECT
-				config_id, bucket_name, filename_root, period_seconds, output_region, from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers, exclude_regions, only_non_travelers
+				config_id, bucket_name, filename_root, period_seconds, output_region,
+				from_timestamp, thru_timestamp, signature_info_ids, input_regions,
+				include_travelers, exclude_regions, only_non_travelers,
+				efgs_export, efgs_upload_host, efgs_mtls_cert_secret, efgs_mtls_key_secret,
+				efgs_signing_cert_secret, efgs_signing_key_secret
 			FROM
 				ExportConfig
 			WHERE
@@ -125,7 +143,11 @@ func (db *ExportDB) GetAllExportConfigs(ctx context.Context) ([]*model.ExportCon
 	if err := db.db.InTx(ctx, pgx.ReadCommitted, func(tx pgx.Tx) error {
 		rows, err := tx.Query(ctx, `
 			SELECT
-				config_id, bucket_name, filename_root, period_seconds, output_region, from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers, exclude_regions, only_non_travelers
+				config_id, bucket_name, filename_root, period_seconds, output_region,
+				from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers,
+				exclude_regions, only_non_travelers,
+				efgs_export, efgs_upload_host, efgs_mtls_cert_secret, efgs_mtls_key_secret,
+				efgs_signing_cert_secret, efgs_signing_key_secret	
 			FROM
 				ExportConfig
 			ORDER BY config_id
@@ -162,7 +184,11 @@ func (db *ExportDB) IterateExportConfigs(ctx context.Context, t time.Time, f fun
 	if err := db.db.InTx(ctx, pgx.ReadCommitted, func(tx pgx.Tx) error {
 		rows, err := tx.Query(ctx, `
 			SELECT
-				config_id, bucket_name, filename_root, period_seconds, output_region, from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers, exclude_regions, only_non_travelers
+				config_id, bucket_name, filename_root, period_seconds, output_region,
+				from_timestamp, thru_timestamp, signature_info_ids, input_regions, include_travelers,
+				exclude_regions, only_non_travelers,
+				efgs_export, efgs_upload_host, efgs_mtls_cert_secret, efgs_mtls_key_secret,
+				efgs_signing_cert_secret, efgs_signing_key_secret
 			FROM
 				ExportConfig
 			WHERE
@@ -204,7 +230,10 @@ func scanOneExportConfig(row pgx.Row) (*model.ExportConfig, error) {
 		periodSeconds int
 		thru          *time.Time
 	)
-	if err := row.Scan(&m.ConfigID, &m.BucketName, &m.FilenameRoot, &periodSeconds, &outputRegion, &m.From, &thru, &m.SignatureInfoIDs, &m.InputRegions, &m.IncludeTravelers, &m.ExcludeRegions, &m.OnlyNonTravelers); err != nil {
+	if err := row.Scan(&m.ConfigID, &m.BucketName, &m.FilenameRoot, &periodSeconds, &outputRegion, &m.From, &thru,
+		&m.SignatureInfoIDs, &m.InputRegions, &m.IncludeTravelers, &m.ExcludeRegions, &m.OnlyNonTravelers,
+		&m.EFGSConfig.Enabled, &m.EFGSConfig.UploadHost, &m.EFGSConfig.MTLSCertSecret, &m.EFGSConfig.MTLSKeySecret,
+		&m.EFGSConfig.SigningCertSecret, &m.EFGSConfig.SigningKeySecret); err != nil {
 		return nil, err
 	}
 
diff --git a/internal/export/model/export_model.go b/internal/export/model/export_model.go
index ae61db8..89c7e1a 100644
--- a/internal/export/model/export_model.go
+++ b/internal/export/model/export_model.go
@@ -32,6 +32,16 @@ const (
 	oneDay = 24 * time.Hour
 )
 
+// EFGSUploadConfig ties a specific export config to EFGS Upload Config
+type EFGSUploadConfig struct {
+	Enabled           bool
+	UploadHost        string
+	MTLSCertSecret    string
+	MTLSKeySecret     string
+	SigningCertSecret string
+	SigningKeySecret  string
+}
+
 type ExportConfig struct {
 	ConfigID         int64
 	BucketName       string
@@ -45,6 +55,8 @@ type ExportConfig struct {
 	From             time.Time
 	Thru             time.Time
 	SignatureInfoIDs []int64
+	// Data related to EFGS
+	EFGSConfig EFGSUploadConfig
 }
 
 // EffectiveInputRegions either returns `InputRegions` or if that array is
diff --git a/migrations/000047_AddEFGSUploadConfig.down.sql b/migrations/000047_AddEFGSUploadConfig.down.sql
new file mode 100644
index 0000000..954ed05
--- /dev/null
+++ b/migrations/000047_AddEFGSUploadConfig.down.sql
@@ -0,0 +1,27 @@
+-- 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.
+
+BEGIN;
+
+DROP TABLE EFGSUploadConfig;
+
+ALTER TABLE ExportConfig
+    DROP COLUMN efgs_export,
+    DROP COLUMN efgs_upload_host,
+    DROP COLUMN efgs_mtls_cert_secret,
+    DROP COLUMN efgs_mtls_key_secret,
+    DROP COLUMN efgs_signing_cert_secret,
+    DROP COLUMN efgs_signing_key_secret;
+
+END;
diff --git a/migrations/000047_AddEFGSUploadConfig.up.sql b/migrations/000047_AddEFGSUploadConfig.up.sql
new file mode 100644
index 0000000..76dfc1a
--- /dev/null
+++ b/migrations/000047_AddEFGSUploadConfig.up.sql
@@ -0,0 +1,38 @@
+-- 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.
+
+BEGIN;
+
+-- Allow for batches to be federated out. 
+ALTER TABLE ExportBatch
+    ADD COLUMN efgs_status exportbatchstatus DEFAULT 'OPEN';
+
+-- Mark a specific export config as needing to be federated UP to EFGS.
+ALTER TABLE ExportConfig
+    ADD COLUMN efgs_export BOOLEAN DEFAULT false,
+    ADD COLUMN efgs_upload_host VARCHAR(250) DEFAULT '',
+    ADD COLUMN efgs_mtls_cert_secret VARCHAR(250) DEFAULT '',
+    ADD COLUMN efgs_mtls_key_secret VARCHAR(250) DEFAULT '',
+    ADD COLUMN efgs_signing_cert_secret VARCHAR(250) DEFAULT '',
+    ADD COLUMN efgs_signing_key_secret VARCHAR(250) DEFAULT '';
+
+ALTER TABLE ExportConfig
+    ALTER COLUMN efgs_export SET NOT NULL,
+    ALTER COLUMN efgs_upload_host SET NOT NULL,
+    ALTER COLUMN efgs_mtls_cert_secret SET NOT NULL,
+    ALTER COLUMN efgs_mtls_key_secret SET NOT NULL,
+    ALTER COLUMN efgs_signing_cert_secret SET NOT NULL,
+    ALTER COLUMN efgs_signing_key_secret SET NOT NULL;
+
+END;


Test patch:
diff --git a/internal/export/database/export_test.go b/internal/export/database/export_test.go
index 5c42bad..5475ce3 100644
--- a/internal/export/database/export_test.go
+++ b/internal/export/database/export_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/google/exposure-notifications-server/internal/export/model"
 	publishdb "github.com/google/exposure-notifications-server/internal/publish/database"
 	publishmodel "github.com/google/exposure-notifications-server/internal/publish/model"
+
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	pgx "github.com/jackc/pgx/v4"
@@ -166,6 +167,12 @@ func TestAddGetUpdateExportConfig(t *testing.T) {
 	want.Thru = time.Time{}
 	want.SignatureInfoIDs = []int64{1, 2, 3, 4, 5}
 	want.InputRegions = []string{"US", "CA"}
+	want.EFGSConfig.Enabled = true
+	want.EFGSConfig.UploadHost = "https://somwehere.else.com/"
+	want.EFGSConfig.MTLSCertSecret = "secret-ref/mtls/cert"
+	want.EFGSConfig.MTLSKeySecret = "secret-ref/mtls/key"
+	want.EFGSConfig.SigningCertSecret = "secret-ref/signing/cert"
+	want.EFGSConfig.SigningKeySecret = "secret-ref/signing/key"
 
 	if err := exportDB.UpdateExportConfig(ctx, want); err != nil {
 		t.Fatal(err)