package webui import ( "net/http" "net/http/httptest" "strconv" "strings" "testing" "mailgoserver/internal/db" ) // TestResetAdminMFA confirms a manager can clear another admin's TOTP and passkeys // (e.g. after a lost device), and that only an admin who could otherwise manage that // target (per the existing delegation rule) is allowed to. func TestResetAdminMFA(t *testing.T) { app := newTestApp(t) mux := app.Mux() cookie := loginSession(t, app) targetID, err := app.DB.CreateAdminUser("has-mfa-admin", mustHash(t), false) if err != nil { t.Fatal(err) } if err := app.DB.SetAdminTOTPSecret(targetID, "JBSWY3DPEHPK3PXP", true); err != nil { t.Fatal(err) } if err := app.DB.CreateWebAuthnCredential(targetID, "yubikey", "cred-id-1", "cred-data-1"); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/"+strconv.FormatInt(targetID, 10)+"/reset_mfa", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } target, err := app.DB.GetAdminUserByID(targetID) if err != nil || target == nil { t.Fatal(err) } if target.TOTPEnabled || target.TOTPSecret != "" { t.Error("TOTP should be cleared") } creds, err := app.DB.ListWebAuthnCredentials(targetID) if err != nil || len(creds) != 0 { t.Errorf("expected no passkeys left, got %d (err=%v)", len(creds), err) } } // TestResetAdminMFADeniedOutsideDelegationScope confirms a scoped admin can't reset // MFA for an admin outside their delegation scope (mirrors the existing remove/edit // access checks — resetting someone's MFA is just as sensitive an action). func TestResetAdminMFADeniedOutsideDelegationScope(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, domainB, _, _ := setupTwoTenants(t, app) _ = domainA cookieA := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID}) targetID, err := app.DB.CreateScopedAdminUser("tenant-b-admin", mustHash(t), 0, []int64{domainB.ID}) if err != nil { t.Fatal(err) } if err := app.DB.SetAdminTOTPSecret(targetID, "JBSWY3DPEHPK3PXP", true); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/"+strconv.FormatInt(targetID, 10)+"/reset_mfa", nil) req.AddCookie(cookieA) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("expected 404 for an out-of-scope target, got %d", rec.Code) } target, err := app.DB.GetAdminUserByID(targetID) if err != nil || target == nil { t.Fatal(err) } if !target.TOTPEnabled { t.Error("TOTP should NOT have been reset for an out-of-scope admin") } } // TestAdminCannotRemoveOrResetOwnAccount confirms the existing self-management // blocks (canManageAdmin already rejects target.ID == user.ID) also apply to the new // reset_mfa route, and that the admins.html list hides both actions for your own row. func TestAdminCannotRemoveOrResetOwnAccount(t *testing.T) { app := newTestApp(t) mux := app.Mux() cookie := loginSession(t, app) sess, err := app.DB.GetSession(cookie.Value) if err != nil || sess == nil { t.Fatal(err) } selfID := strconv.FormatInt(sess.UserID, 10) for _, action := range []string{"remove", "reset_mfa"} { req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/"+selfID+"/"+action, nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("%s on own account: status=%d, want 404", action, rec.Code) } } stillThere, err := app.DB.GetAdminUserByID(sess.UserID) if err != nil || stillThere == nil { t.Fatal("own account should not have been removed") } // The admins list must not render a Remove/Reset MFA button for your own row. req := httptest.NewRequest(http.MethodGet, Prefix+"/admins", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("/admins: status=%d", rec.Code) } body := rec.Body.String() if strings.Contains(body, "/admins/"+selfID+"/remove") { t.Error("admins.html should not render a Remove action for the signed-in admin's own row") } if strings.Contains(body, "/admins/"+selfID+"/reset_mfa") { t.Error("admins.html should not render a Reset MFA action for the signed-in admin's own row") } if !strings.Contains(body, "This is you") { t.Error("expected the signed-in admin's own row to be marked, not just have its buttons hidden") } } // TestResetMailboxMFA confirms an admin can clear a mailbox owner's TOTP and passkeys. func TestResetMailboxMFA(t *testing.T) { app := newTestApp(t) mux := app.Mux() cookie := loginSession(t, app) mailboxes, err := app.DB.ListMailboxes() if err != nil || len(mailboxes) == 0 { t.Fatal("no seeded mailbox") } mboxID := mailboxes[0].ID if err := app.DB.SetMailboxTOTPSecret(mboxID, "JBSWY3DPEHPK3PXP", true); err != nil { t.Fatal(err) } if err := app.DB.CreateMailboxWebAuthnCredential(mboxID, "phone", "mcred-1", "mcred-data-1"); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, Prefix+"/mailboxes/"+strconv.FormatInt(mboxID, 10)+"/reset_mfa", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } updated, err := app.DB.GetMailboxByID(mboxID) if err != nil || updated == nil { t.Fatal(err) } if updated.TOTPEnabled || updated.TOTPSecret != "" { t.Error("TOTP should be cleared") } creds, err := app.DB.ListMailboxWebAuthnCredentials(mboxID) if err != nil || len(creds) != 0 { t.Errorf("expected no passkeys left, got %d (err=%v)", len(creds), err) } } func mustHash(t *testing.T) string { t.Helper() hash, err := db.HashPassword("some-strong-password-1!") if err != nil { t.Fatal(err) } return hash }