326 lines
8.4 KiB
Go
326 lines
8.4 KiB
Go
// Package onviftesting provides testing utilities for ONVIF client testing.
|
|
package onviftesting
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// GoldenManifest describes a camera's golden file set.
|
|
type GoldenManifest struct {
|
|
Version string `json:"version"`
|
|
Camera CameraInfo `json:"camera"`
|
|
CaptureDate string `json:"capture_date"`
|
|
Capabilities []string `json:"capabilities"`
|
|
OperationCount map[string]int `json:"operation_count"`
|
|
Notes string `json:"notes,omitempty"`
|
|
}
|
|
|
|
// GoldenFile represents a single operation's expected result.
|
|
type GoldenFile struct {
|
|
Operation string `json:"operation"`
|
|
Service string `json:"service"`
|
|
Parameters map[string]string `json:"parameters,omitempty"`
|
|
Request string `json:"request"`
|
|
Response string `json:"response"`
|
|
ExpectedFields map[string]interface{} `json:"expected_fields,omitempty"`
|
|
VariableFields []string `json:"variable_fields,omitempty"`
|
|
}
|
|
|
|
// GoldenFileSet holds all golden files for a camera.
|
|
type GoldenFileSet struct {
|
|
Manifest *GoldenManifest
|
|
Files map[string]*GoldenFile // key is operation + params
|
|
BasePath string
|
|
}
|
|
|
|
// LoadGoldenManifest loads a manifest.json from a golden directory.
|
|
func LoadGoldenManifest(goldenDir string) (*GoldenManifest, error) {
|
|
manifestPath := filepath.Join(goldenDir, "manifest.json")
|
|
data, err := os.ReadFile(manifestPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read manifest: %w", err)
|
|
}
|
|
|
|
var manifest GoldenManifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
|
|
}
|
|
|
|
return &manifest, nil
|
|
}
|
|
|
|
// LoadGoldenFiles loads all golden files from a camera directory.
|
|
func LoadGoldenFiles(goldenDir string) (*GoldenFileSet, error) {
|
|
set := &GoldenFileSet{
|
|
Files: make(map[string]*GoldenFile),
|
|
BasePath: goldenDir,
|
|
}
|
|
|
|
// Load manifest if it exists
|
|
manifest, err := LoadGoldenManifest(goldenDir)
|
|
if err == nil {
|
|
set.Manifest = manifest
|
|
}
|
|
|
|
// Walk through all JSON files in the directory
|
|
err = filepath.Walk(goldenDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip directories and non-JSON files
|
|
if info.IsDir() || filepath.Ext(path) != ".json" {
|
|
return nil
|
|
}
|
|
|
|
// Skip manifest.json
|
|
if info.Name() == "manifest.json" {
|
|
return nil
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s: %w", path, err)
|
|
}
|
|
|
|
var golden GoldenFile
|
|
if err := json.Unmarshal(data, &golden); err != nil {
|
|
return fmt.Errorf("failed to unmarshal %s: %w", path, err)
|
|
}
|
|
|
|
// Build key from operation and parameters
|
|
key := buildGoldenKey(&golden)
|
|
set.Files[key] = &golden
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return set, nil
|
|
}
|
|
|
|
// buildGoldenKey creates a unique key for a golden file.
|
|
func buildGoldenKey(g *GoldenFile) string {
|
|
key := g.Operation
|
|
if g.Parameters != nil {
|
|
// Sort parameters for consistent keys
|
|
for k, v := range g.Parameters {
|
|
key += "_" + k + "_" + v
|
|
}
|
|
}
|
|
return key
|
|
}
|
|
|
|
// GetGoldenFile retrieves a golden file by operation name and parameters.
|
|
func (s *GoldenFileSet) GetGoldenFile(operation string, params map[string]string) *GoldenFile {
|
|
// Try exact match first
|
|
golden := &GoldenFile{Operation: operation, Parameters: params}
|
|
key := buildGoldenKey(golden)
|
|
if g, ok := s.Files[key]; ok {
|
|
return g
|
|
}
|
|
|
|
// Fall back to operation-only match
|
|
for _, g := range s.Files {
|
|
if g.Operation == operation {
|
|
return g
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOperations returns all unique operations in the golden file set.
|
|
func (s *GoldenFileSet) GetOperations() []string {
|
|
seen := make(map[string]bool)
|
|
var ops []string
|
|
|
|
for _, g := range s.Files {
|
|
if !seen[g.Operation] {
|
|
seen[g.Operation] = true
|
|
ops = append(ops, g.Operation)
|
|
}
|
|
}
|
|
|
|
return ops
|
|
}
|
|
|
|
// ValidateResponse validates a response against expected fields in a golden file.
|
|
func ValidateResponse(response interface{}, golden *GoldenFile) []string {
|
|
if golden.ExpectedFields == nil {
|
|
return nil
|
|
}
|
|
|
|
var errors []string
|
|
|
|
// Convert response to map for comparison
|
|
responseData, err := toMap(response)
|
|
if err != nil {
|
|
return []string{fmt.Sprintf("failed to convert response: %v", err)}
|
|
}
|
|
|
|
// Check each expected field
|
|
for field, expected := range golden.ExpectedFields {
|
|
actual, ok := responseData[field]
|
|
if !ok {
|
|
errors = append(errors, fmt.Sprintf("missing field: %s", field))
|
|
continue
|
|
}
|
|
|
|
// Skip variable fields (like timestamps)
|
|
if isVariableField(field, golden.VariableFields) {
|
|
continue
|
|
}
|
|
|
|
// Compare values
|
|
if !valuesEqual(expected, actual) {
|
|
errors = append(errors, fmt.Sprintf("field %s: expected %v, got %v", field, expected, actual))
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
// toMap converts a struct to a map for field comparison.
|
|
func toMap(v interface{}) (map[string]interface{}, error) {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// isVariableField checks if a field should be skipped during validation.
|
|
func isVariableField(field string, variableFields []string) bool {
|
|
for _, v := range variableFields {
|
|
if v == field {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// valuesEqual compares two values for equality.
|
|
func valuesEqual(expected, actual interface{}) bool {
|
|
// Handle nil comparison
|
|
if expected == nil && actual == nil {
|
|
return true
|
|
}
|
|
if expected == nil || actual == nil {
|
|
return false
|
|
}
|
|
|
|
// Convert to JSON for deep comparison
|
|
e, err1 := json.Marshal(expected)
|
|
a, err2 := json.Marshal(actual)
|
|
if err1 != nil || err2 != nil {
|
|
return false
|
|
}
|
|
|
|
return string(e) == string(a)
|
|
}
|
|
|
|
// SaveGoldenFile saves a golden file to disk.
|
|
func SaveGoldenFile(golden *GoldenFile, outputPath string) error {
|
|
data, err := json.MarshalIndent(golden, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal golden file: %w", err)
|
|
}
|
|
|
|
// Create directory if needed
|
|
dir := filepath.Dir(outputPath)
|
|
if err := os.MkdirAll(dir, 0750); err != nil { //nolint:mnd
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(outputPath, data, 0600); err != nil { //nolint:mnd
|
|
return fmt.Errorf("failed to write file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SaveGoldenManifest saves a manifest file to disk.
|
|
func SaveGoldenManifest(manifest *GoldenManifest, outputPath string) error {
|
|
data, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal manifest: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(outputPath, data, 0600); err != nil { //nolint:mnd
|
|
return fmt.Errorf("failed to write manifest: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateGoldenFileName generates a filename for a golden file.
|
|
func GenerateGoldenFileName(operation string, params map[string]string) string {
|
|
name := operation
|
|
for k, v := range params {
|
|
// Sanitize parameter value for filename
|
|
v = strings.ReplaceAll(v, "/", "_")
|
|
v = strings.ReplaceAll(v, "\\", "_")
|
|
name += "_" + k + "_" + v
|
|
}
|
|
return name + ".json"
|
|
}
|
|
|
|
// CreateGoldenFromCapture creates a golden file from a captured exchange.
|
|
func CreateGoldenFromCapture(exchange *CapturedExchangeV2) *GoldenFile {
|
|
params := make(map[string]string)
|
|
if exchange.Parameters != nil {
|
|
for k, v := range exchange.Parameters {
|
|
if s, ok := v.(string); ok {
|
|
params[k] = s
|
|
}
|
|
}
|
|
}
|
|
|
|
return &GoldenFile{
|
|
Operation: exchange.OperationName,
|
|
Service: string(exchange.ServiceType),
|
|
Parameters: params,
|
|
Request: exchange.RequestBody,
|
|
Response: exchange.ResponseBody,
|
|
}
|
|
}
|
|
|
|
// GoldenTestRunner helps run tests against golden files.
|
|
type GoldenTestRunner struct {
|
|
GoldenSet *GoldenFileSet
|
|
}
|
|
|
|
// NewGoldenTestRunner creates a new golden test runner.
|
|
func NewGoldenTestRunner(goldenDir string) (*GoldenTestRunner, error) {
|
|
set, err := LoadGoldenFiles(goldenDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &GoldenTestRunner{GoldenSet: set}, nil
|
|
}
|
|
|
|
// ValidateOperation validates a response against the golden file for an operation.
|
|
func (r *GoldenTestRunner) ValidateOperation(operation string, params map[string]string, response interface{}) []string {
|
|
golden := r.GoldenSet.GetGoldenFile(operation, params)
|
|
if golden == nil {
|
|
return []string{fmt.Sprintf("no golden file found for operation: %s", operation)}
|
|
}
|
|
|
|
return ValidateResponse(response, golden)
|
|
}
|