diff --git a/cmd/server/assets/apikeys/index.html b/cmd/server/assets/apikeys/index.html
index 7d325ce4f..c43e51308 100644
--- a/cmd/server/assets/apikeys/index.html
+++ b/cmd/server/assets/apikeys/index.html
@@ -16,6 +16,7 @@
diff --git a/cmd/server/assets/mobileapps/show.html b/cmd/server/assets/mobileapps/show.html
index 676d86f91..f590b554f 100644
--- a/cmd/server/assets/mobileapps/show.html
+++ b/cmd/server/assets/mobileapps/show.html
@@ -9,7 +9,7 @@
{{template "head" .}}
-
+
{{template "navbar" .}}
diff --git a/pkg/controller/mobileapps/create.go b/pkg/controller/mobileapps/create.go
index 4a4b8e050..bc51e245e 100644
--- a/pkg/controller/mobileapps/create.go
+++ b/pkg/controller/mobileapps/create.go
@@ -100,6 +100,7 @@ func (c *Controller) HandleCreate() http.Handler {
// renderNew renders the new page.
func (c *Controller) renderNew(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) {
m := templateMap(ctx)
+ m["title"] = fmt.Sprintf("New mobile app - %s", m["title"])
m["app"] = app
c.h.RenderHTML(w, "mobileapps/new", m)
}
diff --git a/pkg/controller/mobileapps/create_test.go b/pkg/controller/mobileapps/create_test.go
new file mode 100644
index 000000000..9c301dbeb
--- /dev/null
+++ b/pkg/controller/mobileapps/create_test.go
@@ -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 mobileapps_test
+
+import (
+ "context"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/google/exposure-notifications-verification-server/internal/browser"
+ "github.com/google/exposure-notifications-verification-server/internal/envstest"
+ "github.com/google/exposure-notifications-verification-server/pkg/controller"
+ "github.com/google/exposure-notifications-verification-server/pkg/database"
+
+ "github.com/chromedp/chromedp"
+)
+
+func TestHandleCreate(t *testing.T) {
+ t.Parallel()
+
+ harness := envstest.NewServer(t)
+
+ // Get the default realm
+ realm, err := harness.Database.FindRealm(1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a user
+ admin := &database.User{
+ Email: "admin@example.com",
+ Name: "Admin",
+ Realms: []*database.Realm{realm},
+ AdminRealms: []*database.Realm{realm},
+ }
+ if err := harness.Database.SaveUser(admin, database.System); err != nil {
+ t.Fatal(err)
+ }
+
+ // Log in the user.
+ session, err := harness.LoggedInSession(nil, admin.Email)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Set the current realm.
+ controller.StoreSessionRealm(session, realm)
+
+ // Mint a cookie for the session.
+ cookie, err := harness.SessionCookie(session)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Create a browser runner.
+ browserCtx := browser.New(t)
+ taskCtx, done := context.WithTimeout(browserCtx, 30*time.Second)
+ defer done()
+
+ if err := chromedp.Run(taskCtx,
+ // Pre-authenticate the user.
+ browser.SetCookie(cookie),
+
+ // Visit /apikeys/new.
+ chromedp.Navigate(`http://`+harness.Server.Addr()+`/mobile-apps/new`),
+
+ // Wait for render.
+ chromedp.WaitVisible(`body#mobileapps-new`, chromedp.ByQuery),
+
+ // Fill out the form.
+ chromedp.SetValue(`input#name`, "Example mobile app", chromedp.ByQuery),
+ chromedp.SetValue(`input#url`, "https://example.com", chromedp.ByQuery),
+ chromedp.SetValue(`select#os`, strconv.Itoa(int(database.OSTypeIOS)), chromedp.ByQuery),
+ chromedp.SetValue(`input#app-id`, "com.example.app", chromedp.ByQuery),
+
+ // Click the submit button.
+ chromedp.Click(`#submit`, chromedp.ByQuery),
+
+ // Wait for the page to reload.
+ chromedp.WaitVisible(`body#mobileapps-show`, chromedp.ByQuery),
+ ); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/pkg/controller/mobileapps/index.go b/pkg/controller/mobileapps/index.go
index b7051009e..0c01cf5ea 100644
--- a/pkg/controller/mobileapps/index.go
+++ b/pkg/controller/mobileapps/index.go
@@ -16,10 +16,12 @@ package mobileapps
import (
"context"
+ "fmt"
"net/http"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
+ "github.com/google/exposure-notifications-verification-server/pkg/pagination"
)
func (c *Controller) HandleIndex() http.Handler {
@@ -32,20 +34,28 @@ func (c *Controller) HandleIndex() http.Handler {
return
}
+ pageParams, err := pagination.FromRequest(r)
+ if err != nil {
+ controller.InternalError(w, r, c.h, err)
+ return
+ }
+
// Perform the lazy load on authorized apps for the realm.
- apps, err := realm.ListMobileApps(c.db)
+ apps, paginator, err := realm.ListMobileApps(c.db, pageParams)
if err != nil {
controller.InternalError(w, r, c.h, err)
return
}
- c.renderIndex(ctx, w, apps)
+ c.renderIndex(ctx, w, apps, paginator)
})
}
// renderIndex renders the index page.
-func (c *Controller) renderIndex(ctx context.Context, w http.ResponseWriter, apps []*database.MobileApp) {
+func (c *Controller) renderIndex(ctx context.Context, w http.ResponseWriter, apps []*database.MobileApp, paginator *pagination.Paginator) {
m := templateMap(ctx)
+ m["title"] = fmt.Sprintf("Mobile apps - %s", m["title"])
m["apps"] = apps
+ m["paginator"] = paginator
c.h.RenderHTML(w, "mobileapps/index", m)
}
diff --git a/pkg/controller/mobileapps/show.go b/pkg/controller/mobileapps/show.go
index 8e535ad5d..66f0bc248 100644
--- a/pkg/controller/mobileapps/show.go
+++ b/pkg/controller/mobileapps/show.go
@@ -16,6 +16,7 @@ package mobileapps
import (
"context"
+ "fmt"
"net/http"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
@@ -61,6 +62,7 @@ func (c *Controller) HandleShow() http.Handler {
// renderShow renders the edit page.
func (c *Controller) renderShow(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) {
m := templateMap(ctx)
+ m["title"] = fmt.Sprintf("%s - Mobile apps - %s", app.Name, m["title"])
m["app"] = app
c.h.RenderHTML(w, "mobileapps/show", m)
}
diff --git a/pkg/controller/mobileapps/update.go b/pkg/controller/mobileapps/update.go
index ea8ca03da..96264c824 100644
--- a/pkg/controller/mobileapps/update.go
+++ b/pkg/controller/mobileapps/update.go
@@ -16,6 +16,7 @@ package mobileapps
import (
"context"
+ "fmt"
"net/http"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
@@ -110,6 +111,7 @@ func (c *Controller) HandleUpdate() http.Handler {
// renderEdit renders the edit page.
func (c *Controller) renderEdit(ctx context.Context, w http.ResponseWriter, app *database.MobileApp) {
m := templateMap(ctx)
+ m["title"] = fmt.Sprintf("%s - Edit mobile app - %s", app.Name, m["title"])
m["app"] = app
c.h.RenderHTML(w, "mobileapps/edit", m)
}
diff --git a/pkg/database/realm.go b/pkg/database/realm.go
index ca84faa08..bc408a804 100644
--- a/pkg/database/realm.go
+++ b/pkg/database/realm.go
@@ -730,20 +730,27 @@ func (r *Realm) FindAuthorizedApp(db *Database, id interface{}) (*AuthorizedApp,
}
// ListMobileApps gets all the mobile apps for the realm.
-func (r *Realm) ListMobileApps(db *Database) ([]*MobileApp, error) {
- var apps []*MobileApp
- if err := db.db.
+func (r *Realm) ListMobileApps(db *Database, p *pagination.PageParams) ([]*MobileApp, *pagination.Paginator, error) {
+ var mobileApps []*MobileApp
+ query := db.db.
Unscoped().
- Model(r).
- Order("mobile_apps.deleted_at DESC, LOWER(mobile_apps.name)").
- Related(&apps).
- Error; err != nil {
+ Model(&MobileApp{}).
+ Where("realm_id = ?", r.ID).
+ Order("mobile_apps.deleted_at DESC, LOWER(mobile_apps.name)")
+
+ if p == nil {
+ p = new(pagination.PageParams)
+ }
+
+ paginator, err := Paginate(query, &mobileApps, p.Page, p.Limit)
+ if err != nil {
if IsNotFound(err) {
- return apps, nil
+ return mobileApps, nil, nil
}
- return nil, err
+ return nil, nil, err
}
- return apps, nil
+
+ return mobileApps, paginator, nil
}
// FindMobileApp finds the mobile app by the given id associated with the realm.