151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package mtx
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
type fakeMediaMTX struct {
|
|
mu sync.Mutex
|
|
paths map[string]string
|
|
mutations int
|
|
}
|
|
|
|
func (f *fakeMediaMTX) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
prefixes := map[string]string{
|
|
"/v3/config/paths/get/": "get",
|
|
"/v3/config/paths/add/": "add",
|
|
"/v3/config/paths/patch/": "patch",
|
|
"/v3/config/paths/delete/": "delete",
|
|
"/v3/paths/get/": "runtime",
|
|
}
|
|
for prefix, operation := range prefixes {
|
|
if !strings.HasPrefix(request.URL.Path, prefix) {
|
|
continue
|
|
}
|
|
name := strings.TrimPrefix(request.URL.Path, prefix)
|
|
source, exists := f.paths[name]
|
|
switch operation {
|
|
case "get":
|
|
if !exists {
|
|
http.Error(writer, `{"error":"not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(writer).Encode(map[string]any{"name": name, "source": source})
|
|
case "add", "patch":
|
|
var body struct {
|
|
Source string `json:"source"`
|
|
}
|
|
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
|
http.Error(writer, `{}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
f.paths[name] = body.Source
|
|
f.mutations++
|
|
_, _ = writer.Write([]byte(`{}`))
|
|
case "delete":
|
|
if !exists {
|
|
http.Error(writer, `{"error":"not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
delete(f.paths, name)
|
|
f.mutations++
|
|
_, _ = writer.Write([]byte(`{}`))
|
|
case "runtime":
|
|
if !exists {
|
|
http.Error(writer, `{"error":"not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
_, _ = writer.Write([]byte(`{"online":true,"available":true}`))
|
|
}
|
|
return
|
|
}
|
|
http.NotFound(writer, request)
|
|
}
|
|
|
|
func TestGeneratedClientCreateReadDeleteMapping(t *testing.T) {
|
|
t.Parallel()
|
|
fake := &fakeMediaMTX{paths: make(map[string]string)}
|
|
server := httptest.NewServer(fake)
|
|
defer server.Close()
|
|
client, err := NewClient(server.URL, server.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
if err := client.CreatePath(ctx, "camera-1", "rtsp://media.invalid/camera-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path, err := client.GetPath(ctx, "camera-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if path.Source != "rtsp://media.invalid/camera-1" {
|
|
t.Fatalf("unexpected source mapping: %+v", path)
|
|
}
|
|
if err := client.DeletePath(ctx, "camera-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := client.GetPath(ctx, "camera-1"); err != ErrPathNotFound {
|
|
t.Fatalf("expected not found after delete, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsurePathIsIdempotentAndCanPatch(t *testing.T) {
|
|
t.Parallel()
|
|
fake := &fakeMediaMTX{paths: make(map[string]string)}
|
|
server := httptest.NewServer(fake)
|
|
defer server.Close()
|
|
client, err := NewClient(server.URL, server.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
changed, err := client.EnsurePath(ctx, "camera-2", "rtsp://media.invalid/first")
|
|
if err != nil || !changed {
|
|
t.Fatalf("first ensure: changed=%v err=%v", changed, err)
|
|
}
|
|
changed, err = client.EnsurePath(ctx, "camera-2", "rtsp://media.invalid/first")
|
|
if err != nil || changed {
|
|
t.Fatalf("second ensure must be idempotent: changed=%v err=%v", changed, err)
|
|
}
|
|
changed, err = client.EnsurePath(ctx, "camera-2", "rtsp://media.invalid/second")
|
|
if err != nil || !changed {
|
|
t.Fatalf("changed source must patch: changed=%v err=%v", changed, err)
|
|
}
|
|
if fake.mutations != 2 {
|
|
t.Fatalf("expected create + patch, got %d mutations", fake.mutations)
|
|
}
|
|
ready, err := client.PathReady(ctx, "camera-2")
|
|
if err != nil || !ready {
|
|
t.Fatalf("runtime probe: ready=%v err=%v", ready, err)
|
|
}
|
|
}
|
|
|
|
func TestAPIErrorDoesNotLeakSource(t *testing.T) {
|
|
t.Parallel()
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
|
writer.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = fmt.Fprint(writer, `{"error":"upstream included a secret"}`)
|
|
}))
|
|
defer server.Close()
|
|
client, err := NewClient(server.URL, server.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secretSource := "rtsp://" + "user" + ":" + "redacted" + "@camera.invalid/live"
|
|
err = client.CreatePath(context.Background(), "camera", secretSource)
|
|
if err == nil || strings.Contains(err.Error(), secretSource) || strings.Contains(err.Error(), "secret") {
|
|
t.Fatalf("error must be redacted, got %v", err)
|
|
}
|
|
}
|