37a1af4a1f
- Replaced all emojis with SVG icons (Lucide-style) - Changed color scheme to Landvex blue (#0066FF) - Light theme background (#f5f5f7) - Professional typography (Inter, JetBrains Mono) - Clean, minimal design following Design Constitution - No decorative elements — information first - Consistent with Landvex Enterprise platform
275 lines
7.3 KiB
Go
275 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
baseURL = "http://localhost:9096"
|
|
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMjIyMjIyMjItMjIyMi0yMjIyLTIyMjItMjIyMjIyMjIyMjIyIiwiZW1haWwiOiJlcmlrQGxhbmR2ZXguY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzgyMzQ0MDAwfQ.demo"
|
|
)
|
|
|
|
// BenchmarkHealthCheck - simple health endpoint
|
|
func BenchmarkHealthCheck(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := http.Get(baseURL + "/health")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// BenchmarkLogin - auth endpoint
|
|
func BenchmarkLogin(b *testing.B) {
|
|
payload := map[string]string{
|
|
"email": "erik@landvex.com",
|
|
"password": "password123",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := http.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// BenchmarkDashboard - protected endpoint with analytics
|
|
func BenchmarkDashboard(b *testing.B) {
|
|
req, _ := http.NewRequest("GET", baseURL+"/api/v1/analytics/dashboard", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// BenchmarkQuotesList - database query
|
|
func BenchmarkQuotesList(b *testing.B) {
|
|
req, _ := http.NewRequest("GET", baseURL+"/api/v1/sales/quotes", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// Concurrent load test
|
|
func TestConcurrentLoad(t *testing.T) {
|
|
concurrency := 50
|
|
requests := 100
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, concurrency*requests)
|
|
start := time.Now()
|
|
|
|
for i := 0; i < concurrency; i++ {
|
|
wg.Add(1)
|
|
go func(worker int) {
|
|
defer wg.Done()
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
for j := 0; j < requests; j++ {
|
|
req, _ := http.NewRequest("GET", baseURL+"/health", nil)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
errors <- fmt.Errorf("worker %d req %d: %v", worker, j, err)
|
|
continue
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
errors <- fmt.Errorf("worker %d req %d: status %d", worker, j, resp.StatusCode)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
duration := time.Since(start)
|
|
totalRequests := concurrency * requests
|
|
rps := float64(totalRequests) / duration.Seconds()
|
|
|
|
errCount := 0
|
|
for err := range errors {
|
|
if errCount < 5 {
|
|
t.Logf("Error: %v", err)
|
|
}
|
|
errCount++
|
|
}
|
|
|
|
t.Logf("Total: %d requests in %v (%.0f req/sec)", totalRequests, duration, rps)
|
|
t.Logf("Errors: %d (%.2f%%)", errCount, float64(errCount)/float64(totalRequests)*100)
|
|
|
|
if errCount > totalRequests/10 {
|
|
t.Fatalf("Too many errors: %d", errCount)
|
|
}
|
|
}
|
|
|
|
// TestFullWorkflow - complete business flow
|
|
func TestFullWorkflow(t *testing.T) {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
// 1. Login
|
|
loginPayload := map[string]string{
|
|
"email": "erik@landvex.com",
|
|
"password": "password123",
|
|
}
|
|
body, _ := json.Marshal(loginPayload)
|
|
resp, err := client.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("Login failed: %v", err)
|
|
}
|
|
var loginResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&loginResp)
|
|
resp.Body.Close()
|
|
|
|
authToken, ok := loginResp["token"].(string)
|
|
if !ok {
|
|
t.Fatal("No token in response")
|
|
}
|
|
t.Logf("✓ Login successful")
|
|
|
|
// 2. Create customer
|
|
customerPayload := map[string]interface{}{
|
|
"name": "Stress Test AB",
|
|
"email": "stress@test.com",
|
|
"phone": "+46701234567",
|
|
"address": "Testgatan 1, Stockholm",
|
|
}
|
|
body, _ = json.Marshal(customerPayload)
|
|
req, _ := http.NewRequest("POST", baseURL+"/api/v1/crm/customers", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Create customer failed: %v", err)
|
|
}
|
|
var customerResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&customerResp)
|
|
resp.Body.Close()
|
|
customerID := customerResp["id"].(string)
|
|
t.Logf("✓ Customer created: %s", customerID)
|
|
|
|
// 3. Create quote
|
|
quotePayload := map[string]interface{}{
|
|
"customer_id": customerID,
|
|
"title": "Stress Test Quote",
|
|
"items": []map[string]interface{}{
|
|
{
|
|
"description": "Test Product",
|
|
"quantity": 10,
|
|
"unit_price": 1000.00,
|
|
"tax_rate": 25.0,
|
|
},
|
|
},
|
|
}
|
|
body, _ = json.Marshal(quotePayload)
|
|
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Create quote failed: %v", err)
|
|
}
|
|
var quoteResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode("eResp)
|
|
resp.Body.Close()
|
|
quoteID := quoteResp["id"].(string)
|
|
t.Logf("✓ Quote created: %s", quoteID)
|
|
|
|
// 4. Accept quote
|
|
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes/"+quoteID+"/accept", nil)
|
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Accept quote failed: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
t.Logf("✓ Quote accepted")
|
|
|
|
// 5. Convert to order
|
|
req, _ = http.NewRequest("POST", baseURL+"/api/v1/sales/quotes/"+quoteID+"/convert", nil)
|
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Convert quote failed: %v", err)
|
|
}
|
|
var orderResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&orderResp)
|
|
resp.Body.Close()
|
|
t.Logf("✓ Quote converted to order: %s", orderResp["order_id"])
|
|
|
|
// 6. Get dashboard
|
|
req, _ = http.NewRequest("GET", baseURL+"/api/v1/analytics/dashboard", nil)
|
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Dashboard failed: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
t.Logf("✓ Dashboard loaded")
|
|
|
|
t.Logf("\n=== WORKFLOW COMPLETE ===")
|
|
}
|
|
|
|
// TestPDFGeneration - stress PDF generation
|
|
func TestPDFGeneration(t *testing.T) {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
// Get existing quote
|
|
req, _ := http.NewRequest("GET", baseURL+"/api/v1/sales/quotes", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("List quotes failed: %v", err)
|
|
}
|
|
var quotesResp map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode("esResp)
|
|
resp.Body.Close()
|
|
|
|
quotes := quotesResp["quotes"].([]interface{})
|
|
if len(quotes) == 0 {
|
|
t.Skip("No quotes to test")
|
|
}
|
|
|
|
quoteID := quotes[0].(map[string]interface{})["id"].(string)
|
|
|
|
// Generate PDF
|
|
start := time.Now()
|
|
req, _ = http.NewRequest("GET", baseURL+"/api/v1/sales/quotes/"+quoteID+"/pdf", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("PDF generation failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("PDF generation returned %d", resp.StatusCode)
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
t.Logf("✓ PDF generated in %v (status: %d, content-type: %s)", duration, resp.StatusCode, resp.Header.Get("Content-Type"))
|
|
}
|