mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-21 10:06:14 +00:00
- Add RFC-compliant wildcard certificates (*.api.bose.io, *.api.bosecm.com) for automatic API coverage - Include additional Bose production domains (worldwide.bose.com, music.api.bose.com, bose-prod.apigee.net) - Implement TLS certificate request logging and wildcard domain matching logic - Add detailed TLS handshake debugging with connection state tracking - Wrap TLS listener with logging to capture certificate selection and handshake failures - Update documentation with wildcard certificate coverage and debugging features - Normalize test data to use consistent local IP addresses This enables automatic coverage of all current and future Bose API subdomains while providing comprehensive TLS debugging for DNS redirection troubleshooting.
946 lines
26 KiB
Go
946 lines
26 KiB
Go
package proxy
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRecorder_Record_Structure(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
// Compile default patterns
|
|
for i := range r.Patterns {
|
|
re, _ := regexp.Compile(r.Patterns[i].Regexp)
|
|
r.Patterns[i].compiled = re
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
category string
|
|
path string
|
|
expected string // Expected subdirectory after interactions/{sessionID}/{category}/
|
|
}{
|
|
{
|
|
name: "root_path",
|
|
category: "self",
|
|
path: "/",
|
|
expected: "root",
|
|
},
|
|
{
|
|
name: "simple_path",
|
|
category: "self",
|
|
path: "/setup/info",
|
|
expected: "setup/info",
|
|
},
|
|
{
|
|
name: "path_with_ip",
|
|
category: "self",
|
|
path: "/setup/info/192.168.1.100",
|
|
expected: "setup/info/{ip}",
|
|
},
|
|
{
|
|
name: "upstream_path",
|
|
category: "upstream",
|
|
path: "/v1/playback/station/s123",
|
|
expected: "v1/playback/station/s123",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: tt.path,
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
|
|
err := r.Record(tt.category, req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
expectedDir := filepath.Join(tmpDir, "interactions", r.SessionID, tt.category, tt.expected)
|
|
if _, err := os.Stat(expectedDir); os.IsNotExist(err) {
|
|
t.Errorf("Expected directory %s does not exist", expectedDir)
|
|
}
|
|
|
|
// Check if file was created
|
|
files, _ := os.ReadDir(expectedDir)
|
|
if len(files) == 0 {
|
|
t.Errorf("No files created in %s", expectedDir)
|
|
}
|
|
for _, f := range files {
|
|
if !strings.Contains(f.Name(), "-GET.http") {
|
|
t.Errorf("Unexpected filename: %s", f.Name())
|
|
}
|
|
// Verify prefix is 4 digits
|
|
if len(f.Name()) < 5 || !isDigit(f.Name()[0]) || !isDigit(f.Name()[1]) || !isDigit(f.Name()[2]) || !isDigit(f.Name()[3]) || f.Name()[4] != '-' {
|
|
t.Errorf("Filename %s does not have correct 0000- prefix", f.Name())
|
|
}
|
|
|
|
// Verify Host header is present
|
|
content, _ := os.ReadFile(filepath.Join(expectedDir, f.Name()))
|
|
if !strings.Contains(string(content), "Host: ") {
|
|
t.Errorf("Recorded file does not contain Host header:\n%s", string(content))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecorder_Record_Sanitization(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-sanitization-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
// Add a custom pattern
|
|
r.Patterns = append(r.Patterns, PathPattern{
|
|
Name: "CustomDeviceID",
|
|
Regexp: `^A81B\w{8}$`,
|
|
Replacement: "{deviceId}",
|
|
})
|
|
// Compile all patterns
|
|
for i := range r.Patterns {
|
|
re, _ := regexp.Compile(r.Patterns[i].Regexp)
|
|
r.Patterns[i].compiled = re
|
|
}
|
|
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: "/info/192.168.1.100/A81B6A536A98",
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
req.Header.Set("X-Device", "A81B6A536A98")
|
|
|
|
err = r.Record("self", req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
expectedDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "info", "{ip}", "{device_id}")
|
|
if _, err := os.Stat(expectedDir); os.IsNotExist(err) {
|
|
t.Errorf("Expected directory %s does not exist", expectedDir)
|
|
}
|
|
|
|
files, _ := os.ReadDir(expectedDir)
|
|
if len(files) == 0 {
|
|
t.Fatalf("No files created in %s", expectedDir)
|
|
}
|
|
|
|
content, _ := os.ReadFile(filepath.Join(expectedDir, files[0].Name()))
|
|
contentStr := string(content)
|
|
|
|
if !strings.Contains(contentStr, "### GET /info/{{ip}}/{{device_id}}") {
|
|
t.Errorf("Expected sanitized comment in .http file, got:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "GET /info/{{ip}}/{{device_id}}") {
|
|
t.Errorf("Expected sanitized URL in .http file, got:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "X-Device: {{device_id}}") {
|
|
t.Errorf("Expected sanitized Header in .http file, got:\n%s", contentStr)
|
|
}
|
|
}
|
|
|
|
func TestRecorder_Record_Sanitization_Account(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-sanitization-account-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
// Use default patterns which now include AccountID
|
|
r.Patterns = DefaultPatterns()
|
|
// Compile all patterns
|
|
for i := range r.Patterns {
|
|
re, _ := regexp.Compile(r.Patterns[i].Regexp)
|
|
r.Patterns[i].compiled = re
|
|
}
|
|
|
|
accountID := "1234567"
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: "/marge/accounts/" + accountID + "/full",
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
|
|
err = r.Record("self", req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
expectedDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "marge", "accounts", "{accountId}", "full")
|
|
if _, err := os.Stat(expectedDir); os.IsNotExist(err) {
|
|
t.Errorf("Expected directory %s does not exist", expectedDir)
|
|
}
|
|
|
|
files, _ := os.ReadDir(expectedDir)
|
|
if len(files) == 0 {
|
|
t.Fatalf("No files created in %s", expectedDir)
|
|
}
|
|
|
|
content, _ := os.ReadFile(filepath.Join(expectedDir, files[0].Name()))
|
|
contentStr := string(content)
|
|
|
|
if !strings.Contains(contentStr, "### GET /marge/accounts/{{accountId}}/full") {
|
|
t.Errorf("Expected sanitized comment in .http file, got:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "GET /marge/accounts/{{accountId}}/full") {
|
|
t.Errorf("Expected sanitized URL in .http file, got:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "// accountId: "+accountID) {
|
|
t.Errorf("Expected accountId comment in .http file, got:\n%s", contentStr)
|
|
}
|
|
}
|
|
|
|
func TestRecorder_Record_Redaction(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-redaction-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
r.Redact = true
|
|
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: "/test",
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
req.Header.Set("Authorization", "Bearer sensitive-token")
|
|
req.Header.Set("Cookie", "session=secret")
|
|
req.Header.Set("X-Normal", "public-info")
|
|
|
|
err = r.Record("self", req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
expectedDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "test")
|
|
files, _ := os.ReadDir(expectedDir)
|
|
content, _ := os.ReadFile(filepath.Join(expectedDir, files[0].Name()))
|
|
contentStr := string(content)
|
|
|
|
if !strings.Contains(contentStr, "Authorization: [REDACTED]") {
|
|
t.Errorf("Expected Authorization header to be redacted, got:\n%s", contentStr)
|
|
}
|
|
if strings.Contains(contentStr, "sensitive-token") {
|
|
t.Errorf("Sensitive token still present in content:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "Cookie: [REDACTED]") {
|
|
t.Errorf("Expected Cookie header to be redacted, got:\n%s", contentStr)
|
|
}
|
|
if !strings.Contains(contentStr, "X-Normal: public-info") {
|
|
t.Errorf("Expected normal header to be present, got:\n%s", contentStr)
|
|
}
|
|
}
|
|
|
|
func isDigit(c byte) bool {
|
|
return c >= '0' && c <= '9'
|
|
}
|
|
|
|
func TestRecorder_IncreasingPrefix(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-prefix-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
// Compile default patterns
|
|
for i := range r.Patterns {
|
|
re, _ := regexp.Compile(r.Patterns[i].Regexp)
|
|
r.Patterns[i].compiled = re
|
|
}
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: "/test",
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
|
|
for i := 1; i <= 3; i++ {
|
|
err := r.Record("self", req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
}
|
|
|
|
expectedDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "test")
|
|
files, _ := os.ReadDir(expectedDir)
|
|
if len(files) != 3 {
|
|
t.Fatalf("Expected 3 files, got %d", len(files))
|
|
}
|
|
|
|
expectedPrefixes := []string{"0001-", "0002-", "0003-"}
|
|
for i, f := range files {
|
|
if !strings.HasPrefix(f.Name(), expectedPrefixes[i]) {
|
|
t.Errorf("File %d: expected prefix %s, got %s", i, expectedPrefixes[i], f.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecorder_EnvFile(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-env-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
// Compile default patterns
|
|
for i := range r.Patterns {
|
|
re, _ := regexp.Compile(r.Patterns[i].Regexp)
|
|
r.Patterns[i].compiled = re
|
|
}
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Path: "/info/192.168.1.100",
|
|
},
|
|
Header: make(http.Header),
|
|
}
|
|
|
|
err = r.Record("self", req, nil)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
envFile := filepath.Join(tmpDir, "interactions", r.SessionID, "http-client.env.json")
|
|
if _, err := os.Stat(envFile); os.IsNotExist(err) {
|
|
t.Fatalf("Expected env file %s does not exist", envFile)
|
|
}
|
|
|
|
data, _ := os.ReadFile(envFile)
|
|
var content map[string]map[string]string
|
|
if err := json.Unmarshal(data, &content); err != nil {
|
|
t.Fatalf("Failed to unmarshal env file: %v", err)
|
|
}
|
|
|
|
if content["session"]["ip"] != "192.168.1.100" {
|
|
t.Errorf("Expected ip to be 192.168.1.100, got %s", content["session"]["ip"])
|
|
}
|
|
}
|
|
|
|
func TestRecorder_GetInteractionStats(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-stats-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
r.SessionID = "20260215-120000-12345"
|
|
|
|
// Create some dummy interactions
|
|
files := []string{
|
|
"interactions/20260215-120000-12345/self/setup/0001-12-00-01.000-GET.http",
|
|
"interactions/20260215-120000-12345/upstream/marge/0002-12-00-02.000-POST.http",
|
|
"interactions/20260215-130000-67890/self/setup/0001-13-00-01.000-GET.http",
|
|
}
|
|
|
|
for _, f := range files {
|
|
path := filepath.Join(tmpDir, f)
|
|
os.MkdirAll(filepath.Dir(path), 0755)
|
|
os.WriteFile(path, []byte("test"), 0644)
|
|
}
|
|
|
|
stats, err := r.GetInteractionStats()
|
|
if err != nil {
|
|
t.Fatalf("GetInteractionStats failed: %v", err)
|
|
}
|
|
|
|
if stats.TotalRequests != 3 {
|
|
t.Errorf("Expected 3 total requests, got %d", stats.TotalRequests)
|
|
}
|
|
|
|
if stats.ByService["self"] != 2 {
|
|
t.Errorf("Expected 2 self requests, got %d", stats.ByService["self"])
|
|
}
|
|
|
|
if stats.ByService["upstream"] != 1 {
|
|
t.Errorf("Expected 1 upstream request, got %d", stats.ByService["upstream"])
|
|
}
|
|
|
|
if stats.BySession["20260215-120000-12345"] != 2 {
|
|
t.Errorf("Expected 2 requests for session 1, got %d", stats.BySession["20260215-120000-12345"])
|
|
}
|
|
}
|
|
|
|
func TestRecorder_ListInteractions(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-list-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
session1 := "20260215-120000-12345"
|
|
session2 := "20260215-130000-67890"
|
|
|
|
// Create some dummy interactions
|
|
files := []struct {
|
|
path string
|
|
content string
|
|
}{
|
|
{
|
|
path: filepath.Join("interactions", session1, "self", "setup", "0001-12-00-01.555-GET.http"),
|
|
content: "### GET /setup\n\n> {% \n // Response: 200 OK\n%}\n",
|
|
},
|
|
{
|
|
path: filepath.Join("interactions", session1, "upstream", "marge", "0002-12-00-02.000-POST.http"),
|
|
content: "### POST /marge\n\n> {% \n // Response: 201 Created\n%}\n",
|
|
},
|
|
{
|
|
path: filepath.Join("interactions", session2, "self", "info", "0001-13-00-05.000-GET.http"),
|
|
content: "### GET /info\n\n> {% \n // Response: 404 Not Found\n%}\n",
|
|
},
|
|
}
|
|
|
|
for _, f := range files {
|
|
path := filepath.Join(tmpDir, f.path)
|
|
os.MkdirAll(filepath.Dir(path), 0755)
|
|
os.WriteFile(path, []byte(f.content), 0644)
|
|
}
|
|
|
|
t.Run("List_all", func(t *testing.T) {
|
|
list, err := r.ListInteractions("", "", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
if len(list) != 3 {
|
|
t.Errorf("Expected 3 interactions, got %d", len(list))
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_by_session", func(t *testing.T) {
|
|
list, err := r.ListInteractions(session1, "", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Errorf("Expected 2 interactions for session1, got %d", len(list))
|
|
}
|
|
for _, i := range list {
|
|
if i.Session != session1 {
|
|
t.Errorf("Expected session %s, got %s", session1, i.Session)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_by_category", func(t *testing.T) {
|
|
list, err := r.ListInteractions("", "upstream", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Errorf("Expected 1 upstream interaction, got %d", len(list))
|
|
}
|
|
if list[0].Category != "upstream" {
|
|
t.Errorf("Expected category upstream, got %s", list[0].Category)
|
|
}
|
|
})
|
|
|
|
t.Run("Check_enhanced_fields", func(t *testing.T) {
|
|
list, err := r.ListInteractions(session1, "self", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
if len(list) == 0 {
|
|
t.Fatal("Expected at least one interaction")
|
|
}
|
|
i := list[0]
|
|
if i.Counter != 1 {
|
|
t.Errorf("Expected counter 1, got %d", i.Counter)
|
|
}
|
|
if i.Status != 200 {
|
|
t.Errorf("Expected status 200, got %d", i.Status)
|
|
}
|
|
if i.Method != "GET" {
|
|
t.Errorf("Expected method GET, got %s", i.Method)
|
|
}
|
|
if i.Timestamp != "2026-02-15 12:00:01.555" {
|
|
t.Errorf("Expected timestamp 2026-02-15 12:00:01.555, got %s", i.Timestamp)
|
|
}
|
|
if i.Path != "/setup" {
|
|
t.Errorf("Expected path /setup, got %s", i.Path)
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_by_since", func(t *testing.T) {
|
|
// session1 has 2026-02-15 12:00:01.555 and 12:00:02.000
|
|
// session2 has 2026-02-15 13:00:05.000
|
|
list, err := r.ListInteractions("", "", "2026-02-15 12:30:00")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Errorf("Expected 1 interaction since 12:30:00, got %d", len(list))
|
|
}
|
|
if list[0].Session != session2 {
|
|
t.Errorf("Expected session2, got %s", list[0].Session)
|
|
}
|
|
|
|
list, err = r.ListInteractions("", "", "2026-02-15 12:00:01.600")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
// Should include 12:00:02.000 and 13:00:05.000
|
|
if len(list) != 2 {
|
|
t.Errorf("Expected 2 interactions since 12:00:01.600, got %d", len(list))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRecorder_DeleteAndCleanup(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "recorder-delete-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
|
|
// Create 15 dummy sessions
|
|
for i := 1; i <= 15; i++ {
|
|
// Use a format that sorts correctly: YYYYMMDD-HHmmss-PID
|
|
sessionID := fmt.Sprintf("20260215-12%02d00-12345", i)
|
|
sessionDir := filepath.Join(tmpDir, "interactions", sessionID)
|
|
os.MkdirAll(sessionDir, 0755)
|
|
os.WriteFile(filepath.Join(sessionDir, "test.http"), []byte("test"), 0644)
|
|
}
|
|
|
|
t.Run("Delete_specific_session", func(t *testing.T) {
|
|
sessionToDelete := "20260215-120500-12345"
|
|
err := r.DeleteSession(sessionToDelete)
|
|
if err != nil {
|
|
t.Fatalf("DeleteSession failed: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(tmpDir, "interactions", sessionToDelete)); !os.IsNotExist(err) {
|
|
t.Errorf("Session %s still exists after deletion", sessionToDelete)
|
|
}
|
|
})
|
|
|
|
t.Run("Cleanup_sessions", func(t *testing.T) {
|
|
err := r.CleanupSessions(10)
|
|
if err != nil {
|
|
t.Fatalf("CleanupSessions failed: %v", err)
|
|
}
|
|
|
|
entries, _ := os.ReadDir(filepath.Join(tmpDir, "interactions"))
|
|
if len(entries) != 10 {
|
|
t.Errorf("Expected 10 sessions to remain, got %d", len(entries))
|
|
}
|
|
|
|
// Ensure newest sessions are kept
|
|
// We created 1 to 15, deleted 5. Remaining: 1-4, 6-15 (14 sessions)
|
|
// Cleanup(10) should keep 15, 14, 13, 12, 11, 10, 9, 8, 7, 6.
|
|
expectedRemaining := []string{
|
|
"20260215-120600-12345",
|
|
"20260215-120700-12345",
|
|
"20260215-120800-12345",
|
|
"20260215-120900-12345",
|
|
"20260215-121000-12345",
|
|
"20260215-121100-12345",
|
|
"20260215-121200-12345",
|
|
"20260215-121300-12345",
|
|
"20260215-121400-12345",
|
|
"20260215-121500-12345",
|
|
}
|
|
|
|
for _, sessionID := range expectedRemaining {
|
|
if _, err := os.Stat(filepath.Join(tmpDir, "interactions", sessionID)); os.IsNotExist(err) {
|
|
t.Errorf("Expected session %s to remain, but it was deleted", sessionID)
|
|
}
|
|
}
|
|
|
|
// Check one that should be deleted
|
|
if _, err := os.Stat(filepath.Join(tmpDir, "interactions", "20260215-120100-12345")); !os.IsNotExist(err) {
|
|
t.Errorf("Session 20260215-120100-12345 should have been cleaned up")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRecorder_GetInteractionContent(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "recorder-content-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
relPath := filepath.Join(r.SessionID, "self", "test", "0001-GET.http")
|
|
fullPath := filepath.Join(tmpDir, "interactions", relPath)
|
|
os.MkdirAll(filepath.Dir(fullPath), 0755)
|
|
|
|
expectedContent := "test content"
|
|
os.WriteFile(fullPath, []byte(expectedContent), 0644)
|
|
|
|
content, err := r.GetInteractionContent(relPath)
|
|
if err != nil {
|
|
t.Fatalf("GetInteractionContent failed: %v", err)
|
|
}
|
|
|
|
if string(content) != expectedContent {
|
|
t.Errorf("Expected %s, got %s", expectedContent, string(content))
|
|
}
|
|
|
|
_, err = r.GetInteractionContent("non-existent")
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent file, got nil")
|
|
}
|
|
}
|
|
|
|
func TestRecorder_ArchiveSession(t *testing.T) {
|
|
tmpDir, _ := os.MkdirTemp("", "archive-test")
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
|
|
sessionID := "test-session-123"
|
|
sessionDir := filepath.Join(tmpDir, "interactions", sessionID)
|
|
os.MkdirAll(filepath.Join(sessionDir, "category1"), 0755)
|
|
os.WriteFile(filepath.Join(sessionDir, "category1", "file1.http"), []byte("content1"), 0644)
|
|
os.WriteFile(filepath.Join(sessionDir, "file2.http"), []byte("content2"), 0644)
|
|
|
|
var buf bytes.Buffer
|
|
err := r.ArchiveSession(sessionID, &buf)
|
|
if err != nil {
|
|
t.Fatalf("ArchiveSession failed: %v", err)
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
t.Fatal("Archive buffer is empty")
|
|
}
|
|
|
|
// Verify it's a valid tar.gz
|
|
gr, err := gzip.NewReader(&buf)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create gzip reader: %v", err)
|
|
}
|
|
defer gr.Close()
|
|
|
|
tr := tar.NewReader(gr)
|
|
files := make(map[string]string)
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Failed to read tar: %v", err)
|
|
}
|
|
|
|
if header.Typeflag == tar.TypeReg {
|
|
var b bytes.Buffer
|
|
io.Copy(&b, tr)
|
|
files[header.Name] = b.String()
|
|
}
|
|
}
|
|
|
|
if len(files) != 2 {
|
|
t.Errorf("Expected 2 files in archive, got %d", len(files))
|
|
}
|
|
if files["category1/file1.http"] != "content1" {
|
|
t.Errorf("Unexpected content for category1/file1.http: %s", files["category1/file1.http"])
|
|
}
|
|
if files["file2.http"] != "content2" {
|
|
t.Errorf("Unexpected content for file2.http: %s", files["file2.http"])
|
|
}
|
|
}
|
|
|
|
func TestRecorder_Record_FullExchange(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-full-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
|
|
req := &http.Request{
|
|
Method: "POST",
|
|
URL: &url.URL{
|
|
Path: "/test",
|
|
},
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("request body")),
|
|
}
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
|
|
res := &http.Response{
|
|
StatusCode: 200,
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("response body")),
|
|
Request: req,
|
|
}
|
|
res.Header.Set("Content-Type", "application/json")
|
|
|
|
err = r.Record("self", req, res)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
// Verify file content
|
|
interactionsDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "test")
|
|
files, _ := os.ReadDir(interactionsDir)
|
|
if len(files) == 0 {
|
|
t.Fatal("No recording file found")
|
|
}
|
|
|
|
content, _ := os.ReadFile(filepath.Join(interactionsDir, files[0].Name()))
|
|
contentStr := string(content)
|
|
|
|
if !strings.Contains(contentStr, "request body") {
|
|
t.Error("Recording does not contain request body")
|
|
}
|
|
if !strings.Contains(contentStr, "Response: 200 OK") {
|
|
t.Error("Recording does not contain response status")
|
|
}
|
|
if !strings.Contains(contentStr, "response body") {
|
|
t.Error("Recording does not contain response body")
|
|
}
|
|
}
|
|
|
|
func TestRecorder_Record_BinaryResponse(t *testing.T) {
|
|
t.Setenv("RECORDER_ASYNC", "false")
|
|
tmpDir, err := os.MkdirTemp("", "recorder-binary-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
|
|
req := &http.Request{
|
|
Method: "GET",
|
|
URL: &url.URL{Path: "/image"},
|
|
}
|
|
|
|
res := &http.Response{
|
|
StatusCode: 200,
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewBuffer([]byte{0x00, 0x01, 0x02, 0x03})),
|
|
Request: req,
|
|
}
|
|
res.Header.Set("Content-Type", "image/png")
|
|
|
|
err = r.Record("self", req, res)
|
|
if err != nil {
|
|
t.Fatalf("Record failed: %v", err)
|
|
}
|
|
|
|
interactionsDir := filepath.Join(tmpDir, "interactions", r.SessionID, "self", "image")
|
|
files, _ := os.ReadDir(interactionsDir)
|
|
content, _ := os.ReadFile(filepath.Join(interactionsDir, files[0].Name()))
|
|
contentStr := string(content)
|
|
|
|
if !strings.Contains(contentStr, "[Binary response body: 4 bytes]") {
|
|
t.Error("Recording does not correctly report binary response")
|
|
}
|
|
}
|
|
|
|
func TestRecorder_ListInteractions_FullTimestamp(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "recorder-full-ts-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
sessionID := "20260215-100000-12345"
|
|
r.SessionID = sessionID
|
|
|
|
// Create some dummy recordings
|
|
basePath := filepath.Join(tmpDir, "interactions", sessionID, "self", "test")
|
|
os.MkdirAll(basePath, 0755)
|
|
|
|
files := []string{
|
|
"0001-10-00-01.000-GET.http",
|
|
"0002-11-00-00.000-GET.http",
|
|
}
|
|
|
|
for _, f := range files {
|
|
os.WriteFile(filepath.Join(basePath, f), []byte("test"), 0644)
|
|
}
|
|
|
|
t.Run("Check_Full_Timestamp_Display", func(t *testing.T) {
|
|
interactions, err := r.ListInteractions(sessionID, "", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
if len(interactions) != 2 {
|
|
t.Fatalf("Expected 2 interactions, got %d", len(interactions))
|
|
}
|
|
|
|
expectedTS := "2026-02-15 10:00:01.000"
|
|
if interactions[0].Timestamp != expectedTS {
|
|
t.Errorf("Expected timestamp %s, got %s", expectedTS, interactions[0].Timestamp)
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_By_Full_Date_Time", func(t *testing.T) {
|
|
// Filter for interactions since 10:30:00 on that day
|
|
interactions, err := r.ListInteractions(sessionID, "", "2026-02-15 10:30:00")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
if len(interactions) != 1 {
|
|
t.Fatalf("Expected 1 interaction, got %d", len(interactions))
|
|
}
|
|
|
|
if interactions[0].ID != "0002-11-00-00.000-GET.http" {
|
|
t.Errorf("Expected 0002-..., got %s", interactions[0].ID)
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_By_Date_Only", func(t *testing.T) {
|
|
// Filter for interactions since the day before
|
|
interactions, err := r.ListInteractions(sessionID, "", "2026-02-14")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
if len(interactions) != 2 {
|
|
t.Fatalf("Expected 2 interactions, got %d", len(interactions))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRecorder_NewFilenameFormat_WithDate(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("", "recorder-new-format-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
r := NewRecorder(tmpDir)
|
|
sessionID := "20260223-150000-12345"
|
|
r.SessionID = sessionID
|
|
|
|
// Create recordings with the new format that includes date in filename
|
|
basePath := filepath.Join(tmpDir, "interactions", sessionID, "self", "test")
|
|
os.MkdirAll(basePath, 0755)
|
|
|
|
files := []string{
|
|
"0047-20260223-215306.128-GET.http",
|
|
"0048-20260223-215306.417-POST.http",
|
|
"0049-20260223-080034.500-GET.http",
|
|
"0050-20260223-080034.507-PUT.http",
|
|
}
|
|
|
|
for _, f := range files {
|
|
os.WriteFile(filepath.Join(basePath, f), []byte("test content"), 0644)
|
|
}
|
|
|
|
t.Run("Parse_New_Format_Timestamps", func(t *testing.T) {
|
|
interactions, err := r.ListInteractions(sessionID, "", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
if len(interactions) != 4 {
|
|
t.Fatalf("Expected 4 interactions, got %d", len(interactions))
|
|
}
|
|
|
|
// Check that timestamps include both date and time from filename
|
|
expectedTimestamps := []string{
|
|
"2026-02-23 21:53:06.128",
|
|
"2026-02-23 21:53:06.417",
|
|
"2026-02-23 08:00:34.500",
|
|
"2026-02-23 08:00:34.507",
|
|
}
|
|
|
|
for i, interaction := range interactions {
|
|
if interaction.Timestamp != expectedTimestamps[i] {
|
|
t.Errorf("Expected timestamp %s, got %s for interaction %d",
|
|
expectedTimestamps[i], interaction.Timestamp, i)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Parse_Methods_From_New_Format", func(t *testing.T) {
|
|
interactions, err := r.ListInteractions(sessionID, "", "")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
expectedMethods := []string{"GET", "POST", "GET", "PUT"}
|
|
for i, interaction := range interactions {
|
|
if interaction.Method != expectedMethods[i] {
|
|
t.Errorf("Expected method %s, got %s for interaction %d",
|
|
expectedMethods[i], interaction.Method, i)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Filter_By_New_Format_Timestamp", func(t *testing.T) {
|
|
// Filter for interactions after 10:00:00 on that day
|
|
interactions, err := r.ListInteractions(sessionID, "", "2026-02-23 10:00:00")
|
|
if err != nil {
|
|
t.Fatalf("ListInteractions failed: %v", err)
|
|
}
|
|
|
|
// Should get the two evening interactions (21:53:06.xxx)
|
|
if len(interactions) != 2 {
|
|
t.Fatalf("Expected 2 interactions after 10:00:00, got %d", len(interactions))
|
|
}
|
|
|
|
for _, interaction := range interactions {
|
|
if !strings.Contains(interaction.Timestamp, "21:53:06") {
|
|
t.Errorf("Expected evening timestamp, got %s", interaction.Timestamp)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("GetFullTimestamp_New_Format", func(t *testing.T) {
|
|
// Test the getFullTimestamp function directly
|
|
fullTS := r.getFullTimestamp(sessionID, "0047-20260223-215306.128-GET.http")
|
|
expected := "2026-02-23-21-53-06.128"
|
|
if fullTS != expected {
|
|
t.Errorf("Expected full timestamp %s, got %s", expected, fullTS)
|
|
}
|
|
})
|
|
}
|