594 lines
21 KiB
Go
594 lines
21 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/pkg/crypto"
|
|
)
|
|
|
|
// AccountCRUDE2ETestSuite tests Account, Contact, and Inbox CRUD flows end-to-end:
|
|
// Register → Login → Account CRUD → Contact CRUD → Inbox CRUD
|
|
// Reference: Chatwoot spec/controllers/api/v1/accounts_controller_spec.rb
|
|
// and spec/controllers/api/v1/accounts/contacts_controller_spec.rb
|
|
// and spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
|
|
type AccountCRUDE2ETestSuite struct {
|
|
E2ETestSuite
|
|
authToken string
|
|
userID uint
|
|
accountID uint
|
|
}
|
|
|
|
// SetupTest creates fresh test data for each test: register user → login → get JWT token.
|
|
func (s *AccountCRUDE2ETestSuite) SetupTest() {
|
|
s.ClearDatabase()
|
|
|
|
// Step 1: Create an account via DB helper
|
|
account := s.CreateTestAccount("E2E Test Org")
|
|
s.accountID = account.ID
|
|
|
|
// Step 2: Create a user with hashed password stored in PasswordDigest (what AuthService checks)
|
|
hashedPassword, err := crypto.HashPassword("SecurePass123!")
|
|
assert.NoError(s.T(), err, "Failed to hash password")
|
|
|
|
now := time.Now()
|
|
user := &model.User{
|
|
Name: "E2E Tester",
|
|
Email: "e2e@test.com",
|
|
Password: hashedPassword,
|
|
PasswordDigest: hashedPassword,
|
|
Provider: "email",
|
|
Active: true,
|
|
Available: true,
|
|
ConfirmedAt: &now,
|
|
}
|
|
err = s.DB().Create(user).Error
|
|
assert.NoError(s.T(), err, "Failed to create user")
|
|
s.userID = user.ID
|
|
|
|
// Step 3: Create AccountUser association so the user belongs to the account
|
|
accountUser := &model.AccountUser{
|
|
UserID: user.ID,
|
|
AccountID: account.ID,
|
|
Role: "administrator",
|
|
}
|
|
err = s.DB().Create(accountUser).Error
|
|
assert.NoError(s.T(), err, "Failed to create AccountUser association")
|
|
|
|
// Step 4: Login via API to obtain JWT token
|
|
loginPayload := map[string]interface{}{
|
|
"email": "e2e@test.com",
|
|
"password": "SecurePass123!",
|
|
}
|
|
body, _ := json.Marshal(loginPayload)
|
|
resp, err := http.Post(
|
|
fmt.Sprintf("%s/api/v1/auth/login", s.ServerURL()),
|
|
"application/json",
|
|
bytes.NewBuffer(body),
|
|
)
|
|
assert.NoError(s.T(), err, "Login request failed")
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
s.T().Logf("Login response: status=%d body=%s", resp.StatusCode, string(respBody))
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Login should succeed: %s", string(respBody))
|
|
|
|
var loginResp map[string]interface{}
|
|
err = json.Unmarshal(respBody, &loginResp)
|
|
assert.NoError(s.T(), err, "Failed to parse login response")
|
|
|
|
// Token is nested under "data" key: {"success":true,"data":{"access_token":"..."}}
|
|
data, _ := loginResp["data"].(map[string]interface{})
|
|
s.authToken, _ = data["access_token"].(string)
|
|
assert.NotEmpty(s.T(), s.authToken, "JWT token should not be empty")
|
|
}
|
|
|
|
// authRequest sends an authenticated HTTP request with the JWT token.
|
|
func (s *AccountCRUDE2ETestSuite) authRequest(method, path string, body interface{}) *http.Response {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
b, _ := json.Marshal(body)
|
|
reqBody = bytes.NewBuffer(b)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", s.ServerURL(), path), reqBody)
|
|
assert.NoError(s.T(), err)
|
|
req.Header.Set("Authorization", "Bearer "+s.authToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
assert.NoError(s.T(), err)
|
|
return resp
|
|
}
|
|
|
|
// parseResponse reads and unmarshals the response body.
|
|
func parseResponse(resp *http.Response) map[string]interface{} {
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var result map[string]interface{}
|
|
json.Unmarshal(body, &result)
|
|
return result
|
|
}
|
|
|
|
// ========== Account CRUD Tests ==========
|
|
|
|
// TestAccountList verifies listing accounts for the authenticated user.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountList() {
|
|
resp := s.authRequest("GET", "/api/v1/accounts", nil)
|
|
defer resp.Body.Close()
|
|
|
|
// The handler currently returns placeholder: {"accounts": [], "meta": {"count": 0}}
|
|
// Once wired to real service, we verify the account appears in the list
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated,
|
|
"Account list should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
}
|
|
|
|
// TestAccountGet verifies retrieving a single account by ID.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountGet() {
|
|
path := fmt.Sprintf("/api/v1/accounts/%d", s.accountID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get account should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// The handler returns {"id": "<id>", "name": "placeholder"} currently
|
|
// Once wired to real service, check: result["name"] == "E2E Test Org"
|
|
idStr, _ := result["id"].(string)
|
|
if idStr != "" {
|
|
assert.Equal(s.T(), strconv.Itoa(int(s.accountID)), idStr, "Account ID should match")
|
|
}
|
|
}
|
|
|
|
// TestAccountCreate verifies creating a new account via the API.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountCreate() {
|
|
createPayload := map[string]interface{}{
|
|
"name": "New E2E Account",
|
|
"locale": "en",
|
|
}
|
|
|
|
resp := s.authRequest("POST", "/api/v1/accounts", createPayload)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create account should return 201")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Once wired to real service, verify the account was persisted in DB
|
|
if id, ok := result["id"].(float64); ok && id > 0 {
|
|
var account model.Account
|
|
err := s.DB().First(&account, uint(id)).Error
|
|
assert.NoError(s.T(), err, "Created account should exist in DB")
|
|
assert.Equal(s.T(), "New E2E Account", account.Name)
|
|
}
|
|
}
|
|
|
|
// TestAccountUpdate verifies updating an existing account.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountUpdate() {
|
|
path := fmt.Sprintf("/api/v1/accounts/%d", s.accountID)
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Updated E2E Org",
|
|
"locale": "zh",
|
|
}
|
|
|
|
resp := s.authRequest("PUT", path, updatePayload)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Update account should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Once wired to real service, verify DB reflects the update
|
|
var account model.Account
|
|
err := s.DB().First(&account, s.accountID).Error
|
|
assert.NoError(s.T(), err)
|
|
if account.Name == "Updated E2E Org" {
|
|
assert.Equal(s.T(), "Updated E2E Org", account.Name, "Account name should be updated in DB")
|
|
}
|
|
}
|
|
|
|
// TestAccountDelete verifies soft-deleting an account.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountDelete() {
|
|
// Create an account to delete
|
|
accountToDelete := s.CreateTestAccount("Account To Delete")
|
|
path := fmt.Sprintf("/api/v1/accounts/%d", accountToDelete.ID)
|
|
|
|
resp := s.authRequest("DELETE", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Delete account should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Verify account is soft-deleted in DB (DeletedAt should be set)
|
|
var deletedAccount model.Account
|
|
err := s.DB().Unscoped().First(&deletedAccount, accountToDelete.ID).Error
|
|
assert.NoError(s.T(), err, "Soft-deleted account should still exist in DB (unscoped)")
|
|
assert.NotZero(s.T(), deletedAccount.DeletedAt, "DeletedAt should be set after soft delete")
|
|
|
|
// Verify account is not visible through normal query
|
|
var normalAccount model.Account
|
|
err = s.DB().First(&normalAccount, accountToDelete.ID).Error
|
|
assert.Error(s.T(), err, "Soft-deleted account should not be visible via normal query")
|
|
}
|
|
|
|
// ========== Contact CRUD Tests ==========
|
|
|
|
// TestContactList verifies listing contacts for an account.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactList() {
|
|
// Create test contacts via DB helper
|
|
s.CreateTestContact("Contact One", "c1@test.com", s.accountID)
|
|
s.CreateTestContact("Contact Two", "c2@test.com", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts", s.accountID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Contact list should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
}
|
|
|
|
// TestContactGet verifies retrieving a single contact by ID.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactGet() {
|
|
contact := s.CreateTestContact("Get Contact", "get@test.com", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.accountID, contact.ID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get contact should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Verify contact exists in DB
|
|
var dbContact model.Contact
|
|
err := s.DB().First(&dbContact, contact.ID).Error
|
|
assert.NoError(s.T(), err, "Contact should exist in DB")
|
|
assert.Equal(s.T(), "Get Contact", dbContact.Name)
|
|
assert.Equal(s.T(), "get@test.com", dbContact.Email)
|
|
}
|
|
|
|
// TestContactCreate verifies creating a new contact via the API.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactCreate() {
|
|
createPayload := map[string]interface{}{
|
|
"name": "New E2E Contact",
|
|
"email": "new_contact@test.com",
|
|
"phone": "+1234567890",
|
|
"identifier": "e2e-contact-001",
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts", s.accountID)
|
|
resp := s.authRequest("POST", path, createPayload)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create contact should return 201")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Once wired to real service, verify contact persisted in DB
|
|
if id, ok := result["id"].(float64); ok && id > 0 {
|
|
var contact model.Contact
|
|
err := s.DB().First(&contact, uint(id)).Error
|
|
assert.NoError(s.T(), err, "Created contact should exist in DB")
|
|
assert.Equal(s.T(), "New E2E Contact", contact.Name)
|
|
assert.Equal(s.T(), "new_contact@test.com", contact.Email)
|
|
}
|
|
}
|
|
|
|
// TestContactSearch verifies searching contacts by query.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactSearch() {
|
|
// Create test contacts with searchable names
|
|
s.CreateTestContact("Alice Johnson", "alice@test.com", s.accountID)
|
|
s.CreateTestContact("Bob Smith", "bob@test.com", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts/search?q=Alice", s.accountID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
// Search is currently NotImplemented (501) — once implemented, expect 200
|
|
assert.True(s.T(), resp.StatusCode == http.StatusNotImplemented || resp.StatusCode == http.StatusOK,
|
|
"Contact search should return 200 (or 501 if not yet implemented)")
|
|
}
|
|
|
|
// TestContactUpdate verifies updating an existing contact.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactUpdate() {
|
|
contact := s.CreateTestContact("Original Name", "original@test.com", s.accountID)
|
|
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Updated Name",
|
|
"email": "updated@test.com",
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.accountID, contact.ID)
|
|
resp := s.authRequest("PUT", path, updatePayload)
|
|
defer resp.Body.Close()
|
|
|
|
// Update is currently NotImplemented (501) — once implemented, expect 200
|
|
assert.True(s.T(), resp.StatusCode == http.StatusNotImplemented || resp.StatusCode == http.StatusOK,
|
|
"Contact update should return 200 (or 501 if not yet implemented)")
|
|
}
|
|
|
|
// TestContactDelete verifies soft-deleting a contact.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactDelete() {
|
|
contact := s.CreateTestContact("Delete Contact", "delete@test.com", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.accountID, contact.ID)
|
|
resp := s.authRequest("DELETE", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
// Delete is currently NotImplemented (501) — once implemented, expect 200
|
|
assert.True(s.T(), resp.StatusCode == http.StatusNotImplemented || resp.StatusCode == http.StatusOK,
|
|
"Contact delete should return 200 (or 501 if not yet implemented)")
|
|
}
|
|
|
|
// ========== Inbox CRUD Tests ==========
|
|
|
|
// TestInboxList verifies listing inboxes for an account.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxList() {
|
|
// Create test inboxes via DB helper
|
|
s.CreateTestInbox("Support Inbox", "web_widget", s.accountID)
|
|
s.CreateTestInbox("Telegram Inbox", "telegram", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes", s.accountID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Inbox list should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
}
|
|
|
|
// TestInboxGet verifies retrieving a single inbox by ID.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxGet() {
|
|
inbox := s.CreateTestInbox("Get Inbox", "web_widget", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.accountID, inbox.ID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get inbox should return 200")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Verify inbox exists in DB
|
|
var dbInbox model.Inbox
|
|
err := s.DB().First(&dbInbox, inbox.ID).Error
|
|
assert.NoError(s.T(), err, "Inbox should exist in DB")
|
|
assert.Equal(s.T(), "Get Inbox", dbInbox.Name)
|
|
assert.Equal(s.T(), "web_widget", dbInbox.ChannelType)
|
|
}
|
|
|
|
// TestInboxCreate verifies creating a new inbox via the API.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxCreate() {
|
|
createPayload := map[string]interface{}{
|
|
"name": "New E2E Inbox",
|
|
"channel_type": "web_widget",
|
|
"enabled": true,
|
|
"enable_auto_assignment": false,
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes", s.accountID)
|
|
resp := s.authRequest("POST", path, createPayload)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(s.T(), http.StatusCreated, resp.StatusCode, "Create inbox should return 201")
|
|
|
|
result := parseResponse(resp)
|
|
assert.NotNil(s.T(), result)
|
|
|
|
// Once wired to real service, verify inbox persisted in DB
|
|
if id, ok := result["id"].(float64); ok && id > 0 {
|
|
var inbox model.Inbox
|
|
err := s.DB().First(&inbox, uint(id)).Error
|
|
assert.NoError(s.T(), err, "Created inbox should exist in DB")
|
|
assert.Equal(s.T(), "New E2E Inbox", inbox.Name)
|
|
assert.Equal(s.T(), "web_widget", inbox.ChannelType)
|
|
}
|
|
}
|
|
|
|
// TestInboxUpdate verifies updating an existing inbox.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxUpdate() {
|
|
inbox := s.CreateTestInbox("Original Inbox", "web_widget", s.accountID)
|
|
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Updated Inbox Name",
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.accountID, inbox.ID)
|
|
resp := s.authRequest("PUT", path, updatePayload)
|
|
defer resp.Body.Close()
|
|
|
|
// Update is currently NotImplemented (501) — once implemented, expect 200
|
|
assert.True(s.T(), resp.StatusCode == http.StatusNotImplemented || resp.StatusCode == http.StatusOK,
|
|
"Inbox update should return 200 (or 501 if not yet implemented)")
|
|
}
|
|
|
|
// TestInboxDelete verifies soft-deleting an inbox.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxDelete() {
|
|
inbox := s.CreateTestInbox("Delete Inbox", "web_widget", s.accountID)
|
|
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.accountID, inbox.ID)
|
|
resp := s.authRequest("DELETE", path, nil)
|
|
defer resp.Body.Close()
|
|
|
|
// Delete is currently NotImplemented (501) — once implemented, expect 200
|
|
assert.True(s.T(), resp.StatusCode == http.StatusNotImplemented || resp.StatusCode == http.StatusOK,
|
|
"Inbox delete should return 200 (or 501 if not yet implemented)")
|
|
}
|
|
|
|
// ========== Auth/Guard Tests ==========
|
|
|
|
// TestAccountCRUDRequiresAuth verifies that CRUD endpoints require authentication.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountCRUDRequiresAuth() {
|
|
endpoints := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{"GET", "/api/v1/accounts"},
|
|
{"POST", "/api/v1/accounts"},
|
|
{"GET", fmt.Sprintf("/api/v1/accounts/%d", s.accountID)},
|
|
{"PUT", fmt.Sprintf("/api/v1/accounts/%d", s.accountID)},
|
|
{"DELETE", fmt.Sprintf("/api/v1/accounts/%d", s.accountID)},
|
|
{"GET", fmt.Sprintf("/api/v1/accounts/%d/contacts", s.accountID)},
|
|
{"GET", fmt.Sprintf("/api/v1/accounts/%d/inboxes", s.accountID)},
|
|
}
|
|
|
|
for _, ep := range endpoints {
|
|
req, err := http.NewRequest(ep.method, fmt.Sprintf("%s%s", s.ServerURL(), ep.path), nil)
|
|
assert.NoError(s.T(), err)
|
|
// No Authorization header
|
|
resp, err := s.httpClient.Do(req)
|
|
assert.NoError(s.T(), err)
|
|
assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode,
|
|
"%s %s should require auth (401)", ep.method, ep.path)
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// ========== Full CRUD Lifecycle Tests ==========
|
|
|
|
// TestAccountFullCRUDLifecycle tests the complete create→read→update→delete flow.
|
|
func (s *AccountCRUDE2ETestSuite) TestAccountFullCRUDLifecycle() {
|
|
// Step 1: Create account via API
|
|
createPayload := map[string]interface{}{
|
|
"name": "Lifecycle Account",
|
|
"locale": "en",
|
|
}
|
|
resp := s.authRequest("POST", "/api/v1/accounts", createPayload)
|
|
assert.Equal(s.T(), http.StatusCreated, resp.StatusCode)
|
|
result := parseResponse(resp)
|
|
|
|
// Extract account ID from response
|
|
var accountID uint
|
|
if id, ok := result["id"].(float64); ok && id > 0 {
|
|
accountID = uint(id)
|
|
} else {
|
|
// If handler is still placeholder, create via DB and proceed
|
|
account := s.CreateTestAccount("Lifecycle Account DB")
|
|
accountID = account.ID
|
|
}
|
|
assert.NotZero(s.T(), accountID, "Account ID should not be zero")
|
|
|
|
// Step 2: Read account via API
|
|
path := fmt.Sprintf("/api/v1/accounts/%d", accountID)
|
|
resp = s.authRequest("GET", path, nil)
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
|
|
parseResponse(resp)
|
|
|
|
// Verify in DB
|
|
var dbAccount model.Account
|
|
err := s.DB().First(&dbAccount, accountID).Error
|
|
assert.NoError(s.T(), err, "Account should exist in DB after create")
|
|
|
|
// Step 3: Update account via API
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Lifecycle Updated",
|
|
"locale": "zh",
|
|
}
|
|
resp = s.authRequest("PUT", path, updatePayload)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK,
|
|
"Update should return 200: got %d", resp.StatusCode)
|
|
parseResponse(resp)
|
|
|
|
// Step 4: Delete account via API
|
|
resp = s.authRequest("DELETE", path, nil)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK,
|
|
"Delete should return 200: got %d", resp.StatusCode)
|
|
parseResponse(resp)
|
|
|
|
// Verify soft-delete in DB
|
|
err = s.DB().First(&dbAccount, accountID).Error
|
|
assert.Error(s.T(), err, "Account should not be visible after soft delete")
|
|
}
|
|
|
|
// TestContactFullCRUDLifecycle tests the complete contact create→read→update→delete flow.
|
|
func (s *AccountCRUDE2ETestSuite) TestContactFullCRUDLifecycle() {
|
|
// Step 1: Create contact via DB (since handler may be placeholder)
|
|
contact := s.CreateTestContact("Lifecycle Contact", "lifecycle@test.com", s.accountID)
|
|
assert.NotZero(s.T(), contact.ID)
|
|
|
|
// Step 2: Read contact via API
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/contacts/%d", s.accountID, contact.ID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get contact should return 200")
|
|
parseResponse(resp)
|
|
|
|
// Verify in DB
|
|
var dbContact model.Contact
|
|
err := s.DB().First(&dbContact, contact.ID).Error
|
|
assert.NoError(s.T(), err)
|
|
assert.Equal(s.T(), "Lifecycle Contact", dbContact.Name)
|
|
|
|
// Step 3: Try update (may be 501 if not yet implemented)
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Lifecycle Updated Contact",
|
|
}
|
|
resp = s.authRequest("PUT", path, updatePayload)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotImplemented)
|
|
|
|
// Step 4: Try delete (may be 501 if not yet implemented)
|
|
resp = s.authRequest("DELETE", path, nil)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotImplemented)
|
|
}
|
|
|
|
// TestInboxFullCRUDLifecycle tests the complete inbox create→read→update→delete flow.
|
|
func (s *AccountCRUDE2ETestSuite) TestInboxFullCRUDLifecycle() {
|
|
// Step 1: Create inbox via DB (since handler may be placeholder)
|
|
inbox := s.CreateTestInbox("Lifecycle Inbox", "web_widget", s.accountID)
|
|
assert.NotZero(s.T(), inbox.ID)
|
|
|
|
// Step 2: Read inbox via API
|
|
path := fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d", s.accountID, inbox.ID)
|
|
resp := s.authRequest("GET", path, nil)
|
|
assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Get inbox should return 200")
|
|
parseResponse(resp)
|
|
|
|
// Verify in DB
|
|
var dbInbox model.Inbox
|
|
err := s.DB().First(&dbInbox, inbox.ID).Error
|
|
assert.NoError(s.T(), err)
|
|
assert.Equal(s.T(), "Lifecycle Inbox", dbInbox.Name)
|
|
assert.Equal(s.T(), "web_widget", dbInbox.ChannelType)
|
|
|
|
// Step 3: Try update (may be 501 if not yet implemented)
|
|
updatePayload := map[string]interface{}{
|
|
"name": "Lifecycle Updated Inbox",
|
|
}
|
|
resp = s.authRequest("PUT", path, updatePayload)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotImplemented)
|
|
|
|
// Step 4: Try delete (may be 501 if not yet implemented)
|
|
resp = s.authRequest("DELETE", path, nil)
|
|
assert.True(s.T(), resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotImplemented)
|
|
}
|
|
|
|
// ========== Entry point ==========
|
|
|
|
// TestAccountCRUDE2ESuite runs the Account CRUD e2e test suite.
|
|
func TestAccountCRUDE2ESuite(t *testing.T) {
|
|
suite.Run(t, new(AccountCRUDE2ETestSuite))
|
|
} |