Files
mailgoserver/internal/webui/mailbox_apppasswords_test.go
T

140 lines
4.3 KiB
Go

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")
}
}