package onvif import ( "context" "encoding/json" "fmt" "os" "sync" "time" ) type FakeScenario struct { Result ProbeResult `json:"result"` ProbeError ErrorCode `json:"probe_error,omitempty"` ClockError ErrorCode `json:"clock_error,omitempty"` DelayMillis int `json:"delay_millis,omitempty"` } type fakeFixture struct { Scenarios map[string]FakeScenario `json:"scenarios"` } // Fake is deterministic and intended only for tests and offline development. type Fake struct { mu sync.Mutex scenarios map[string]FakeScenario probeCalls map[string]int clockSyncCalls map[string]int } func NewFake(scenarios map[string]FakeScenario) *Fake { copyOfScenarios := make(map[string]FakeScenario, len(scenarios)) for key, value := range scenarios { copyOfScenarios[key] = value } return &Fake{ scenarios: copyOfScenarios, probeCalls: make(map[string]int), clockSyncCalls: make(map[string]int), } } func LoadFakeFixture(path string) (*Fake, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read ONVIF fixture: %w", err) } var fixture fakeFixture if err := json.Unmarshal(data, &fixture); err != nil { return nil, fmt.Errorf("decode ONVIF fixture: %w", err) } return NewFake(fixture.Scenarios), nil } func (f *Fake) Probe(ctx context.Context, target Target) (ProbeResult, error) { scenario, ok := f.scenario(target.EndpointRef) if !ok { return ProbeResult{}, &Error{Code: ErrorUnavailable, Err: fmt.Errorf("fixture endpoint is not configured")} } if err := waitForFakeDelay(ctx, scenario.DelayMillis); err != nil { return ProbeResult{}, &Error{Code: ErrorTimeout, Err: err} } f.mu.Lock() f.probeCalls[target.EndpointRef]++ f.mu.Unlock() if scenario.ProbeError != "" { return ProbeResult{}, &Error{Code: scenario.ProbeError, Err: fmt.Errorf("fixture probe failure")} } if scenario.Result.StreamURI == "" || len(scenario.Result.Profiles) == 0 { return ProbeResult{}, &Error{Code: ErrorInvalidReply, Err: fmt.Errorf("fixture lacks profile or stream URI")} } return scenario.Result, nil } func (f *Fake) SetSystemDateAndTime(ctx context.Context, target Target, _ time.Time) error { scenario, ok := f.scenario(target.EndpointRef) if !ok { return &Error{Code: ErrorUnavailable, Err: fmt.Errorf("fixture endpoint is not configured")} } if err := waitForFakeDelay(ctx, scenario.DelayMillis); err != nil { return &Error{Code: ErrorTimeout, Err: err} } f.mu.Lock() f.clockSyncCalls[target.EndpointRef]++ f.mu.Unlock() if scenario.ClockError != "" { return &Error{Code: scenario.ClockError, Err: fmt.Errorf("fixture clock failure")} } return nil } func (f *Fake) ProbeCalls(endpointRef string) int { f.mu.Lock() defer f.mu.Unlock() return f.probeCalls[endpointRef] } func (f *Fake) ClockSyncCalls(endpointRef string) int { f.mu.Lock() defer f.mu.Unlock() return f.clockSyncCalls[endpointRef] } func (f *Fake) scenario(endpointRef string) (FakeScenario, bool) { f.mu.Lock() defer f.mu.Unlock() scenario, ok := f.scenarios[endpointRef] return scenario, ok } func waitForFakeDelay(ctx context.Context, milliseconds int) error { if milliseconds <= 0 { select { case <-ctx.Done(): return ctx.Err() default: return nil } } timer := time.NewTimer(time.Duration(milliseconds) * time.Millisecond) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: return nil } }