updated mailbox app password
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mailgoserver/internal/db"
|
||||
)
|
||||
|
||||
func TestAppPasswordExpiryPresets(t *testing.T) {
|
||||
loc := time.UTC
|
||||
now := time.Now()
|
||||
|
||||
t.Run("never expires by default", func(t *testing.T) {
|
||||
got, err := appPasswordExpiry("", "", loc)
|
||||
if err != nil || got != nil {
|
||||
t.Fatalf("got (%v, %v), want (nil, nil)", got, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("7 day preset", func(t *testing.T) {
|
||||
got, err := appPasswordExpiry("7d", "", loc)
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("got (%v, %v), want a non-nil expiry", got, err)
|
||||
}
|
||||
wantAround := now.Add(7 * 24 * time.Hour)
|
||||
if diff := got.Sub(wantAround); diff < -time.Minute || diff > time.Minute {
|
||||
t.Fatalf("expiry %v not within a minute of %v", got, wantAround)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom date is end of day", func(t *testing.T) {
|
||||
got, err := appPasswordExpiry("custom", "2030-01-15", loc)
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("got (%v, %v), want a non-nil expiry", got, err)
|
||||
}
|
||||
want := time.Date(2030, 1, 15, 23, 59, 59, 0, loc)
|
||||
if !got.Equal(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom without a date errors", func(t *testing.T) {
|
||||
if _, err := appPasswordExpiry("custom", "", loc); err == nil {
|
||||
t.Fatal("expected an error for a missing custom date")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid preset errors", func(t *testing.T) {
|
||||
if _, err := appPasswordExpiry("bogus", "", loc); err == nil {
|
||||
t.Fatal("expected an error for an invalid preset")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAddAppPasswordExpiredIsRejectedByAuth confirms an app password created with a
|
||||
// past expiry (simulated directly at the DB layer, since the UI can only pick future
|
||||
// dates) can no longer authenticate, even while still marked active.
|
||||
func TestAddAppPasswordExpiredIsRejectedByAuth(t *testing.T) {
|
||||
app := newTestApp(t)
|
||||
mailboxes, _ := app.DB.ListMailboxes()
|
||||
if len(mailboxes) == 0 {
|
||||
t.Fatal("no seeded mailbox")
|
||||
}
|
||||
mbox := mailboxes[0].Mailbox
|
||||
|
||||
past := time.Now().Add(-time.Hour)
|
||||
hash, err := db.HashPassword("some-secret-app-password-value")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := app.DB.CreateAppPassword(mbox.ID, "expired", hash, &past); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := app.DB.VerifyMailboxAppPassword(mbox.Email, "some-secret-app-password-value")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatal("expired app password must not authenticate")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddAppPasswordSetsRevealCookieNotFlash confirms the create handler no longer
|
||||
// puts the plaintext secret in the toast-driven Flash cookie (easy to miss, no copy
|
||||
// button) and instead sets the dedicated one-time reveal cookie the list page renders
|
||||
// as a modal.
|
||||
func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) {
|
||||
app := newTestApp(t)
|
||||
mux := app.Mux()
|
||||
cookie := loginSession(t, app)
|
||||
mailboxes, _ := app.DB.ListMailboxes()
|
||||
mboxID := mailboxes[0].ID
|
||||
|
||||
form := url.Values{"label": {"laptop"}, "expires_preset": {"never"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords/add", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(cookie)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("add app password: status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var revealCookie, flashCookie *http.Cookie
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
switch c.Name {
|
||||
case appPasswordRevealCookieName:
|
||||
revealCookie = c
|
||||
case flashCookieName:
|
||||
flashCookie = c
|
||||
}
|
||||
}
|
||||
if revealCookie == nil || revealCookie.Value == "" {
|
||||
t.Fatal("expected a non-empty app password reveal cookie")
|
||||
}
|
||||
if flashCookie != nil && flashCookie.Value != "" {
|
||||
t.Fatalf("flash cookie should not carry the secret, got %q", flashCookie.Value)
|
||||
}
|
||||
|
||||
// Following the redirect (as the browser would) should render the reveal modal
|
||||
// with the secret, and clear the one-time cookie.
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords", nil)
|
||||
req2.AddCookie(cookie)
|
||||
req2.AddCookie(revealCookie)
|
||||
rec2 := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec2, req2)
|
||||
if rec2.Code != http.StatusOK {
|
||||
t.Fatalf("apppasswords list: status=%d", rec2.Code)
|
||||
}
|
||||
if !strings.Contains(rec2.Body.String(), "appPasswordRevealModal") {
|
||||
t.Fatal("expected the reveal modal markup in the response")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user