Repository: google/exposure-notifications-server
Pull request #1469 (2021-03-17 23:08:03)

Add tests for admin export importer
**Release Note**

<!-- Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". -->

```release-note
Add tests for admin export importer
```


Patch:
diff --git a/internal/admin/handle_export_importers.go b/internal/admin/handle_export_importers.go
index 45b5a1b..5142002 100644
--- a/internal/admin/handle_export_importers.go
+++ b/internal/admin/handle_export_importers.go
@@ -63,7 +63,7 @@ func (s *Server) HandleExportImportersSave() func(c *gin.Context) {
 
 		m.AddSuccess("Successfully updated export importer config!")
 
-		c.Redirect(http.StatusFound, fmt.Sprintf("/export-importers/%d", record.ID))
+		c.Redirect(http.StatusSeeOther, fmt.Sprintf("/export-importers/%d", record.ID))
 		c.Abort()
 	}
 }
@@ -137,11 +137,11 @@ type exportImporterFormData struct {
 func (f *exportImporterFormData) BuildExportImporterModel(c *model.ExportImport) error {
 	from, err := CombineDateAndTime(f.FromDate, f.FromTime)
 	if err != nil {
-		return err
+		return fmt.Errorf("invalid from time: %w", err)
 	}
 	thru, err := CombineDateAndTime(f.ThruDate, f.ThruTime)
 	if err != nil {
-		return err
+		return fmt.Errorf("invalid thru time: %w", err)
 	}
 
 	if val := f.IndexFile; val != "" {


Test patch:
diff --git a/internal/admin/handle_export_importers_test.go b/internal/admin/handle_export_importers_test.go
index 60e9b4c..2b18231 100644
--- a/internal/admin/handle_export_importers_test.go
+++ b/internal/admin/handle_export_importers_test.go
@@ -15,9 +15,19 @@
 package admin
 
 import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/google/exposure-notifications-server/internal/exportimport/database"
 	"github.com/google/exposure-notifications-server/internal/exportimport/model"
+	"github.com/google/exposure-notifications-server/internal/project"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
 func TestRenderExportImporters(t *testing.T) {
@@ -29,3 +39,315 @@ func TestRenderExportImporters(t *testing.T) {
 
 	testRenderTemplate(t, "export-importer", m)
 }
+
+func TestBuildExportImporterModel(t *testing.T) {
+	t.Parallel()
+
+	from := time.Unix(1609579380, 0).UTC()
+	thru := time.Unix(1641119640, 0).UTC()
+
+	cases := []struct {
+		name string
+		form *exportImporterFormData
+		exp  *model.ExportImport
+		err  string
+	}{
+		{
+			name: "default",
+			form: &exportImporterFormData{
+				IndexFile:  "index.txt",
+				ExportRoot: "root",
+				Region:     "TEST",
+				Travelers:  true,
+				FromDate:   "2021-01-02",
+				FromTime:   "09:23",
+				ThruDate:   "2022-01-02",
+				ThruTime:   "10:34",
+			},
+			exp: &model.ExportImport{
+				IndexFile:  "index.txt",
+				ExportRoot: "root",
+				Region:     "TEST",
+				Traveler:   true,
+				From:       from,
+				Thru:       &thru,
+			},
+		},
+		{
+			name: "bad_from",
+			form: &exportImporterFormData{
+				FromDate: "banana",
+				FromTime: "apple",
+			},
+			err: "invalid from time",
+		},
+		{
+			name: "zero_time",
+			form: &exportImporterFormData{
+				FromDate: "",
+				FromTime: "",
+			},
+			exp: &model.ExportImport{
+				From: time.Now().UTC().Add(1 * time.Minute),
+			},
+		},
+		{
+			name: "bad_thru",
+			form: &exportImporterFormData{
+				ThruDate: "banana",
+				ThruTime: "apple",
+			},
+			err: "invalid thru time",
+		},
+		{
+			name: "zero_thru",
+			form: &exportImporterFormData{
+				ThruDate: "",
+				ThruTime: "",
+			},
+			exp: &model.ExportImport{
+				From: time.Now().UTC().Add(1 * time.Minute),
+				Thru: nil,
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			var key model.ExportImport
+			err := tc.form.BuildExportImporterModel(&key)
+			if err != nil {
+				if tc.err == "" {
+					t.Fatal(err)
+				}
+				if got, want := err.Error(), tc.err; !strings.Contains(got, want) {
+					t.Errorf("expected %q to contain %q", got, want)
+				}
+			}
+
+			if tc.err == "" {
+				opts := cmp.Options{cmpopts.EquateApproxTime(5 * time.Minute)}
+				if diff := cmp.Diff(tc.exp, &key, opts); diff != "" {
+					t.Errorf("mismatch (-want, +got):\n%s", diff)
+				}
+			}
+		})
+	}
+}
+
+func TestHandleExportImportersShow(t *testing.T) {
+	t.Parallel()
+	ctx := project.TestContext(t)
+
+	env, s := newTestServer(t)
+	db := env.Database()
+	exportimportDB := database.New(db)
+
+	exportImport := &model.ExportImport{
+		IndexFile:  "index.txt",
+		ExportRoot: "root",
+		Region:     "TEST",
+		Traveler:   true,
+		From:       time.Now().UTC().Add(-24 * time.Hour),
+	}
+	if err := exportimportDB.AddConfig(ctx, exportImport); err != nil {
+		t.Fatal(err)
+	}
+
+	cases := []struct {
+		name   string
+		id     string
+		status int
+		want   []string
+	}{
+		{
+			name:   "new",
+			id:     "0",
+			status: 200,
+			want:   []string{"New Export Importer Config"},
+		},
+		{
+			name:   "existing",
+			id:     fmt.Sprintf("%d", exportImport.ID),
+			status: 200,
+			want:   []string{"Edit Export Importer Config", exportImport.ExportRoot},
+		},
+		{
+			name:   "non_existing",
+			id:     "123456",
+			status: 500,
+			want:   []string{"failed to lookup export importer config"},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			server := newHTTPServer(t, http.MethodGet, "/:id", s.HandleExportImportersShow())
+
+			ctx := context.Background()
+			req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s", server.URL, tc.id), nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			client := server.Client()
+
+			resp, err := client.Do(req)
+			if err != nil {
+				t.Fatalf("error making http call: %v", err)
+			}
+			defer resp.Body.Close()
+
+			if got, want := resp.StatusCode, tc.status; got != want {
+				b, err := io.ReadAll(resp.Body)
+				if err != nil {
+					t.Fatal(err)
+				}
+				t.Errorf("expected status %d to be %d; headers: %#v; body: %s", got, want, resp.Header, b)
+			}
+
+			mustFindStrings(t, resp, tc.want...)
+		})
+	}
+}
+
+func TestHandleExportImportersSave(t *testing.T) {
+	t.Parallel()
+	ctx := project.TestContext(t)
+
+	env, s := newTestServer(t)
+	db := env.Database()
+	exportimportDB := database.New(db)
+
+	exportImport := &model.ExportImport{
+		IndexFile:  "index.txt",
+		ExportRoot: "root",
+		Region:     "TEST",
+		Traveler:   true,
+		From:       time.Now().UTC().Add(-24 * time.Hour),
+	}
+	if err := exportimportDB.AddConfig(ctx, exportImport); err != nil {
+		t.Fatal(err)
+	}
+
+	cases := []struct {
+		name   string
+		id     string
+		seed   *model.ExportImport
+		form   *exportImporterFormData
+		status int
+		want   []string
+	}{
+		// create
+		{
+			name: "create_new",
+			id:   "0",
+			form: &exportImporterFormData{
+				IndexFile:  "index.txt",
+				ExportRoot: "root",
+				Region:     "TEST",
+				Travelers:  true,
+				FromDate:   "2021-01-02",
+				FromTime:   "09:23",
+				ThruDate:   "2022-01-02",
+				ThruTime:   "10:34",
+			},
+			status: 303,
+		},
+
+		// update
+		{
+			name: "update_unknown",
+			id:   "123456",
+			form: &exportImporterFormData{
+				IndexFile: "index.txt",
+			},
+			status: 500,
+			want:   []string{"failed to lookup export importer config"},
+		},
+		{
+			name: "update_bad_id",
+			id:   "banana",
+			form: &exportImporterFormData{
+				IndexFile: "index.txt",
+			},
+			status: 500,
+			want:   []string{"failed to parse"},
+		},
+		{
+			name: "update_request",
+			id:   fmt.Sprintf("%d", exportImport.ID),
+			form: &exportImporterFormData{
+				IndexFile: "index.txt",
+				FromDate:  "nope",
+			},
+			status: 500,
+			want:   []string{"failed to build export importer config"},
+		},
+		{
+			name: "update_existing",
+			id:   fmt.Sprintf("%d", exportImport.ID),
+			form: &exportImporterFormData{
+				IndexFile: "index.txt",
+			},
+			status: 303,
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			if tc.seed != nil {
+				if err := exportimportDB.AddConfig(ctx, tc.seed); err != nil {
+					t.Fatalf("failed to add export import config: %v", err)
+				}
+			}
+
+			server := newHTTPServer(t, http.MethodPost, "/:id", s.HandleExportImportersSave())
+
+			// URL values
+			form, err := serializeForm(tc.form)
+			if err != nil {
+				t.Fatalf("unable to serialize form: %v", err)
+			}
+
+			ctx := context.Background()
+			req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/%s", server.URL, tc.id), strings.NewReader(form.Encode()))
+			if err != nil {
+				t.Fatal(err)
+			}
+			req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+			client := server.Client()
+			client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+				return http.ErrUseLastResponse
+			}
+
+			resp, err := client.Do(req)
+			if err != nil {
+				t.Fatalf("error making http call: %v", err)
+			}
+			defer resp.Body.Close()
+
+			if got, want := resp.StatusCode, tc.status; got != want {
+				b, err := io.ReadAll(resp.Body)
+				if err != nil {
+					t.Fatal(err)
+				}
+				t.Errorf("expected status %d to be %d; headers: %#v; body: %s", got, want, resp.Header, b)
+			}
+
+			if len(tc.want) > 0 {
+				mustFindStrings(t, resp, tc.want...)
+			}
+		})
+	}
+}