package webui import ( "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" ) // TestWebmailContactCreateEditDelete exercises the full self-service contact CRUD // flow through the add/edit popup's shared save endpoint. func TestWebmailContactCreateEditDelete(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() mailboxID := createTestMailboxWithPassword(t, app, "contactowner@example.com", domains[0].ID, "contact-password-1!") cookie := webmailLoginSession(t, app, mailboxID) save := func(id, email, name, phone string) *httptest.ResponseRecorder { form := url.Values{"id": {id}, "email": {email}, "name": {name}, "phone": {phone}} req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/contacts/save", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) return rec } if rec := save("", "jane@example.com", "Jane Doe", "555-1234"); rec.Code != http.StatusFound { t.Fatalf("create: status=%d body=%s", rec.Code, rec.Body.String()) } contacts, err := app.DB.ListContacts(mailboxID) if err != nil || len(contacts) != 1 || contacts[0].Name != "Jane Doe" { t.Fatalf("expected 1 contact named Jane Doe, got %+v (err=%v)", contacts, err) } id := contacts[0].ID if rec := save(strconv.FormatInt(id, 10), "jane@example.com", "Jane D.", ""); rec.Code != http.StatusFound { t.Fatalf("edit: status=%d body=%s", rec.Code, rec.Body.String()) } updated, err := app.DB.GetContactByID(mailboxID, id) if err != nil || updated == nil || updated.Name != "Jane D." || updated.Phone != "" { t.Fatalf("expected updated contact, got %+v (err=%v)", updated, err) } // Missing name/email is rejected rather than silently stored. if rec := save("", "", "No Email", ""); rec.Code != http.StatusFound { t.Fatalf("status=%d", rec.Code) } contacts, err = app.DB.ListContacts(mailboxID) if err != nil || len(contacts) != 1 { t.Fatalf("expected the invalid contact rejected (still 1), got %d (err=%v)", len(contacts), err) } delReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/contacts/"+strconv.FormatInt(id, 10)+"/delete", nil) delReq.AddCookie(cookie) delRec := httptest.NewRecorder() mux.ServeHTTP(delRec, delReq) if delRec.Code != http.StatusFound { t.Fatalf("delete: status=%d body=%s", delRec.Code, delRec.Body.String()) } remaining, err := app.DB.ListContacts(mailboxID) if err != nil || len(remaining) != 0 { t.Fatalf("expected no contacts left, got %+v (err=%v)", remaining, err) } } // TestWebmailContactScopedToOwnMailbox confirms one mailbox owner can't delete // another mailbox's contact by guessing its ID. func TestWebmailContactScopedToOwnMailbox(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() victimID := createTestMailboxWithPassword(t, app, "victim4@example.com", domains[0].ID, "victim-password-1!") attackerID := createTestMailboxWithPassword(t, app, "attacker4@example.com", domains[0].ID, "attacker-password-1!") contactID, err := app.DB.CreateContact(victimID, "friend@example.com", "Friend", "") if err != nil { t.Fatal(err) } attackerCookie := webmailLoginSession(t, app, attackerID) req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/contacts/"+strconv.FormatInt(contactID, 10)+"/delete", nil) req.AddCookie(attackerCookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("status=%d", rec.Code) } stillThere, err := app.DB.GetContactByID(victimID, contactID) if err != nil || stillThere == nil { t.Fatalf("expected the victim's contact untouched, got %+v (err=%v)", stillThere, err) } }