140 lines
4.3 KiB
Go
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")
|
|
}
|
|
}
|