fix(security): JWT require env, remove *** token, WS auth disabled
fix(automation): implement all 6 actions + real cron parser fix(db): pq.Array for TEXT[], add sqlmock tests fix(schema): single source migrations docs: v2 architecture + frontend refactor proposals
This commit is contained in:
+12
-5
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type CRMHandler struct {
|
||||
@@ -72,10 +73,12 @@ func (h *CRMHandler) ListCustomers(w http.ResponseWriter, r *http.Request) {
|
||||
customers := []Customer{}
|
||||
for rows.Next() {
|
||||
var c Customer
|
||||
var tags pq.StringArray
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
&c.Status, &c.Source, &tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
c.Tags = []string(tags)
|
||||
customers = append(customers, c)
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ func (h *CRMHandler) CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
INSERT INTO boc_customers (name, email, phone, company, org_number, status, source, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber, req.Status, req.Source, req.Tags).Scan(&id)
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber, req.Status, req.Source, pq.Array(req.Tags)).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create customer")
|
||||
@@ -114,11 +117,13 @@ func (h *CRMHandler) GetCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var c Customer
|
||||
var tags pq.StringArray
|
||||
err := h.DB.QueryRow(`
|
||||
SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at
|
||||
FROM boc_customers WHERE id = $1
|
||||
`, id).Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt)
|
||||
&c.Status, &c.Source, &tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt)
|
||||
c.Tags = []string(tags)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
writeError(w, http.StatusNotFound, "customer not found")
|
||||
@@ -147,7 +152,7 @@ func (h *CRMHandler) UpdateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
status = $6, source = $7, tags = $8, assigned_to = $9
|
||||
WHERE id = $10
|
||||
`, req.Name, req.Email, req.Phone, req.Company, req.OrgNumber,
|
||||
req.Status, req.Source, req.Tags, req.AssignedTo, id)
|
||||
req.Status, req.Source, pq.Array(req.Tags), req.AssignedTo, id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update customer")
|
||||
@@ -189,10 +194,12 @@ func (h *CRMHandler) ListLeads(w http.ResponseWriter, r *http.Request) {
|
||||
leads := []Customer{}
|
||||
for rows.Next() {
|
||||
var c Customer
|
||||
var tags pq.StringArray
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Email, &c.Phone, &c.Company, &c.OrgNumber,
|
||||
&c.Status, &c.Source, &c.Tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
&c.Status, &c.Source, &tags, &c.AssignedTo, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
c.Tags = []string(tags)
|
||||
leads = append(leads, c)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCRMHandler_ListCustomers(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
handler := NewCRMHandler(db)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at FROM boc_customers").
|
||||
WithArgs("active").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id", "name", "email", "phone", "company", "org_number", "status", "source", "tags", "assigned_to", "created_at", "updated_at",
|
||||
}).AddRow(
|
||||
"cust-1", "Test AB", "test@test.com", "+46701234567", "Test AB", "559141-7042", "active", "web", "{tag1,tag2}", nil, time.Now(), time.Now(),
|
||||
))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/crm/customers?status=active", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.ListCustomers(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
customers := response["customers"].([]interface{})
|
||||
assert.Len(t, customers, 1)
|
||||
assert.Equal(t, float64(1), response["total"])
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCRMHandler_CreateCustomer(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
handler := NewCRMHandler(db)
|
||||
|
||||
mock.ExpectQuery("INSERT INTO boc_customers").
|
||||
WithArgs("Test AB", "test@test.com", "+46701234567", "Test AB", "", "lead", "", nil).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("cust-1"))
|
||||
// NOTE: pq.Array([]string(nil)) becomes nil argument — sqlmock matches nil
|
||||
|
||||
payload := Customer{
|
||||
Name: "Test AB",
|
||||
Email: "test@test.com",
|
||||
Phone: "+46701234567",
|
||||
Company: "Test AB",
|
||||
Status: "lead",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/crm/customers", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.CreateCustomer(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "cust-1", response["id"])
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCRMHandler_GetCustomer(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
_ = NewCRMHandler(db)
|
||||
|
||||
mock.ExpectQuery("SELECT id, name, email, phone, company, org_number, status, source, tags, assigned_to, created_at, updated_at").
|
||||
WithArgs("cust-1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id", "name", "email", "phone", "company", "org_number", "status", "source", "tags", "assigned_to", "created_at", "updated_at",
|
||||
}).AddRow(
|
||||
"cust-1", "Test AB", "test@test.com", "+46701234567", "Test AB", "559141-7042", "active", "web", nil, nil, time.Now(), time.Now(),
|
||||
))
|
||||
|
||||
// Requires chi router context for URL params — test via router in integration tests
|
||||
t.Skip("Requires chi router context for URL params")
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCRMHandler_GetPipeline(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
handler := NewCRMHandler(db)
|
||||
|
||||
mock.ExpectQuery("SELECT stage, COUNT\\(\\*\\), COALESCE\\(SUM\\(value\\), 0\\)").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"stage", "count", "sum"}).
|
||||
AddRow("prospect", 5, 100000.00).
|
||||
AddRow("qualified", 3, 75000.00).
|
||||
AddRow("proposal", 2, 50000.00))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/crm/pipeline", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
handler.GetPipeline(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
pipeline := response["pipeline"].([]interface{})
|
||||
assert.Len(t, pipeline, 3)
|
||||
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
@@ -57,10 +57,8 @@ func TestWriteError(t *testing.T) {
|
||||
assert.Equal(t, "test error", response["error"])
|
||||
}
|
||||
|
||||
func TestCRMHandler_CreateCustomer(t *testing.T) {
|
||||
// This would need a real or mocked DB connection
|
||||
// For now, just test the request parsing
|
||||
|
||||
func TestCRMHandler_CreateCustomer_RequestParsing(t *testing.T) {
|
||||
// Test request parsing only — DB tests are in crm_test.go
|
||||
payload := map[string]interface{}{
|
||||
"name": "Test Customer",
|
||||
"email": "test@example.com",
|
||||
@@ -68,12 +66,11 @@ func TestCRMHandler_CreateCustomer(t *testing.T) {
|
||||
"company": "Test AB",
|
||||
"status": "lead",
|
||||
}
|
||||
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/crm/customers", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Without DB, this will fail, but we test the request structure
|
||||
|
||||
assert.NotNil(t, req)
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user