From 37fe80796edd941cfaac1b5a3c1a8b9fa4531590 Mon Sep 17 00:00:00 2001 From: gezimbll Date: Wed, 14 Jan 2026 16:35:28 +0100 Subject: [PATCH] updated && added new test for ArinGO/ArinGOV1 --- aringo.go | 3 +- aringo_test.go | 688 ++++++++++++++++++++++++++++++++++++++----------- aringov1.go | 1 - 3 files changed, 533 insertions(+), 159 deletions(-) diff --git a/aringo.go b/aringo.go index f9e897e..0c3b1df 100644 --- a/aringo.go +++ b/aringo.go @@ -74,7 +74,7 @@ type ARInGO struct { reconnects int maxReconnectInterval time.Duration delayFunc func(time.Duration, time.Duration) func() time.Duration // used to create/reset the delay function - evChannel chan map[string]interface{} // Events coming from Asterisk are posted here + evChannel chan map[string]any // Events coming from Asterisk are posted here errChannel chan error // Errors are posted here wsListenerExit <-chan struct{} // Signal dispatcher to stop listening pendingMu sync.Mutex // Protects pending map @@ -193,6 +193,7 @@ func (ari *ARInGO) disconnect() error { return ari.ws.Close(websocket.StatusNormalClosure, "") } +// Call sends a REST request over WebSocket, merging both query and body params into the payload func (ari *ARInGO) Call(method, uri string, queryStr map[string]string, bodyParams map[string]string) (RESTResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/aringo_test.go b/aringo_test.go index 8928488..6494407 100644 --- a/aringo_test.go +++ b/aringo_test.go @@ -8,6 +8,8 @@ Provides Asterisk ARI connector from Go programming language. package aringo import ( + "context" + "encoding/json" "fmt" "io" "net/http" @@ -18,7 +20,8 @@ import ( "testing" "time" - "golang.org/x/net/websocket" + "github.com/coder/websocket" + legacyws "golang.org/x/net/websocket" ) func TestAringoFib(t *testing.T) { @@ -50,6 +53,20 @@ func TestAringoFib(t *testing.T) { } } +func TestFibDurationMaxDuration(t *testing.T) { + maxDur := 3 * time.Second + fib := fibDuration(time.Second, maxDur) + + fib() + fib() + fib() + fib() + f := fib() + if f != maxDur { + t.Errorf("\nExpected: <%+v>,\nReceived: <%+v>", maxDur, f) + } +} + func TestNewErrUnexpectedReplyCode(t *testing.T) { statusCode := 111 expected := fmt.Sprintf("UNEXPECTED_REPLY_CODE: %d", statusCode) @@ -59,20 +76,21 @@ func TestNewErrUnexpectedReplyCode(t *testing.T) { } } -func TestAringoNewARInGONoConnAttempts(t *testing.T) { +func TestAringoNewARInGOV1NoConnAttempts(t *testing.T) { wsUrl := "" wsOrigin := "" username := "" + address := "" password := "" userAgent := "" - evChannel := make(chan map[string]interface{}) + evChannel := make(chan map[string]any) errChannel := make(chan error) stopChan := make(<-chan struct{}) connectAttempts := 0 reconnects := -1 experr := ErrZeroConnectAttempts - received, err := NewARInGOV1(wsUrl, wsOrigin, username, password, userAgent, evChannel, + received, err := NewARInGOV1(wsUrl, wsOrigin, username, password, address, userAgent, evChannel, errChannel, stopChan, connectAttempts, reconnects, 0, fibDuration) if err != experr { @@ -93,27 +111,28 @@ func TestAringoNewARInGOV1(t *testing.T) { reconnects := -1 var srv *httptest.Server - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { })) n := strings.LastIndexByte(srv.URL, ':') wsOrigin := srv.URL[:n] + "/" wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") + "/" - + address := strings.TrimPrefix(srv.URL, "http://") expected := &ARInGOV1{ httpClient: http.DefaultClient, wsURL: wsUrl, wsOrigin: wsOrigin, username: "", password: "", + address: address, userAgent: userAgent, reconnects: -1, evChannel: evChannel, errChannel: errChannel, wsListenerExit: stopChan, } - received, err := NewARInGOV1(wsUrl, wsOrigin, username, password, userAgent, evChannel, + received, err := NewARInGOV1(wsUrl, wsOrigin, username, password, address, userAgent, evChannel, errChannel, stopChan, connectAttempts, reconnects, 0, fibDuration) expected.httpClient = received.httpClient expected.ws = received.ws @@ -127,44 +146,12 @@ func TestAringoNewARInGOV1(t *testing.T) { close(stopChan) srv.Close() - - stopChan = make(chan struct{}) - expected.wsListenerExit = stopChan - - srv2 := &http.Server{ - Addr: strings.TrimPrefix(srv.URL, "http://"), - Handler: websocket.Handler(func(c *websocket.Conn) { - - }), - } - defer srv2.Close() - - go func() { - time.Sleep(10 * time.Millisecond) - srv2.ListenAndServe() - - }() - - received, err = NewARInGOV1(wsUrl, wsOrigin, username, password, userAgent, evChannel, - errChannel, stopChan, connectAttempts, reconnects, 0, fibDuration) - - expected.httpClient = received.httpClient - expected.ws = received.ws - received.delayFunc = nil - - if err != nil { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", nil, err) - } else if !reflect.DeepEqual(received, expected) { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", expected, received) - } - close(stopChan) } -func TestAringowsEventListenerValidJSON(t *testing.T) { +func TestAringoV1wsEventListenerValidJSON(t *testing.T) { var srv *httptest.Server stopChan := make(chan struct{}) - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { - + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { time.Sleep(10 * time.Millisecond) close(stopChan) c.Write([]byte("{\"key\":\"value\"}")) @@ -186,13 +173,13 @@ func TestAringowsEventListenerValidJSON(t *testing.T) { userAgent: "", reconnects: -1, delayFunc: fibDuration, - evChannel: make(chan map[string]interface{}, 1), + evChannel: make(chan map[string]any, 1), errChannel: make(chan error, 1), wsListenerExit: stopChan, } var err error - ari.ws, err = websocket.Dial(ari.wsURL, "", ari.wsOrigin) + ari.ws, err = legacyws.Dial(ari.wsURL, "", ari.wsOrigin) if err != nil { t.Fatal(err) } @@ -206,7 +193,7 @@ func TestAringowsEventListenerValidJSON(t *testing.T) { t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", 1, len(ari.evChannel)) } - exp := map[string]interface{}{ + exp := map[string]any{ "key": "value", } rcv := <-ari.evChannel @@ -216,15 +203,14 @@ func TestAringowsEventListenerValidJSON(t *testing.T) { } } -func TestAringowsEventListenerClosedCh(t *testing.T) { +func TestAringoV1wsEventListenerClosedCh(t *testing.T) { var srv *httptest.Server stopChan := make(chan struct{}) - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { time.Sleep(10 * time.Millisecond) close(stopChan) c.Write([]byte("{key:value}")) c.Close() - })) defer srv.Close() @@ -248,7 +234,7 @@ func TestAringowsEventListenerClosedCh(t *testing.T) { } var err error - ari.ws, err = websocket.Dial(ari.wsURL, "", ari.wsOrigin) + ari.ws, err = legacyws.Dial(ari.wsURL, "", ari.wsOrigin) if err != nil { t.Fatal(err) } @@ -263,10 +249,10 @@ func TestAringowsEventListenerClosedCh(t *testing.T) { } } -func TestAringowsEventListenerReconnect(t *testing.T) { +func TestAringoV1wsEventListenerReconnect(t *testing.T) { var srv *httptest.Server stopChan := make(chan struct{}) - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { })) @@ -291,7 +277,7 @@ func TestAringowsEventListenerReconnect(t *testing.T) { } var err error - ari.ws, err = websocket.Dial(ari.wsURL, "", ari.wsOrigin) + ari.ws, err = legacyws.Dial(ari.wsURL, "", ari.wsOrigin) if err != nil { t.Fatal(err) } @@ -306,7 +292,7 @@ func TestAringowsEventListenerReconnect(t *testing.T) { } } -func TestAringowsEventListenerInvalidJSONReturn(t *testing.T) { +func TestAringoV1wsEventListenerInvalidJSONReturn(t *testing.T) { stopChan := make(chan struct{}) var srv *httptest.Server @@ -321,14 +307,13 @@ func TestAringowsEventListenerInvalidJSONReturn(t *testing.T) { errChannel: make(chan error, 1), wsListenerExit: stopChan, } - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { urll := ari.wsURL ari.wsURL = "invalidURL" c.Write([]byte("invalid")) time.Sleep(20 * time.Millisecond) ari.wsURL = urll c.Close() - })) defer srv.Close() @@ -337,7 +322,7 @@ func TestAringowsEventListenerInvalidJSONReturn(t *testing.T) { ari.wsURL = "ws" + strings.TrimPrefix(srv.URL, "http") + "/" var err error - ari.ws, err = websocket.Dial(ari.wsURL, "", ari.wsOrigin) + ari.ws, err = legacyws.Dial(ari.wsURL, "", ari.wsOrigin) if err != nil { t.Fatal(err) } @@ -354,7 +339,7 @@ func TestAringowsEventListenerInvalidJSONReturn(t *testing.T) { close(stopChan) } -func TestAringowsEventListenerFailReconnect(t *testing.T) { +func TestAringoV1wsEventListenerFailReconnect(t *testing.T) { stopChan := make(chan struct{}) var srv *httptest.Server @@ -369,14 +354,13 @@ func TestAringowsEventListenerFailReconnect(t *testing.T) { errChannel: make(chan error, 1), wsListenerExit: stopChan, } - srv = httptest.NewServer(websocket.Handler(func(c *websocket.Conn) { + srv = httptest.NewServer(legacyws.Handler(func(c *legacyws.Conn) { urll := ari.wsURL ari.wsURL = "invalidURL" c.Write([]byte("invalid")) time.Sleep(20 * time.Millisecond) ari.wsURL = urll c.Close() - })) defer srv.Close() @@ -385,7 +369,7 @@ func TestAringowsEventListenerFailReconnect(t *testing.T) { ari.wsURL = "ws" + strings.TrimPrefix(srv.URL, "http") + "/" var err error - ari.ws, err = websocket.Dial(ari.wsURL, "", ari.wsOrigin) + ari.ws, err = legacyws.Dial(ari.wsURL, "", ari.wsOrigin) if err != nil { t.Fatal(err) } @@ -410,43 +394,100 @@ func TestAringowsEventListenerFailReconnect(t *testing.T) { close(stopChan) } -func TestAringoCallUnrecognizedMethod(t *testing.T) { +func TestAringoV1CallGETSuccess(t *testing.T) { + stopChan := make(chan struct{}) + defer close(stopChan) + + expectedBody := "OK" + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != HTTP_GET { + t.Errorf("Expected GET method, got %s", r.Method) + } + + if r.URL.Query().Get("test") != "string" { + t.Errorf("Expected query param test=string, got %s", r.URL.Query().Get("test")) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(expectedBody)) + })) + defer srv.Close() + ari := &ARInGOV1{ - delayFunc: fibDuration, + httpClient: http.DefaultClient, + address: strings.TrimPrefix(srv.URL, "http://"), + reconnects: -1, + delayFunc: fibDuration, + evChannel: make(chan map[string]interface{}), + errChannel: make(chan error), + wsListenerExit: stopChan, } - var data url.Values - - experr := "Unrecognized method: invalid" - rply, err := ari.Call("invalid", ari.wsURL, data) - if err == nil || err.Error() != experr { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", experr, err) - } else if rply != nil { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", nil, rply) + resp, err := ari.Call(HTTP_GET, "", map[string]string{"test": "string"}, nil) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + if resp.MessageBody != expectedBody { + t.Errorf("Expected body '%s', got '%s'", expectedBody, resp.MessageBody) } } -func TestAringoCallInvalidURL(t *testing.T) { +func TestAringoV1CallPOSTSuccess(t *testing.T) { + stopChan := make(chan struct{}) + defer close(stopChan) + + expectedBody := `{"status":"created"}` + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != HTTP_POST { + t.Errorf("Expected POST method, got %s", r.Method) + } + + body, _ := io.ReadAll(r.Body) + vals, _ := url.ParseQuery(string(body)) + if vals.Get("channel") != "SIP/1000" { + t.Errorf("Expected body param channel=SIP/1000, got %s", vals.Get("channel")) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(expectedBody)) + })) + defer srv.Close() + ari := &ARInGOV1{ - delayFunc: fibDuration, + httpClient: http.DefaultClient, + address: strings.TrimPrefix(srv.URL, "http://"), + reconnects: -1, + delayFunc: fibDuration, + evChannel: make(chan map[string]interface{}), + errChannel: make(chan error), + wsListenerExit: stopChan, } - var data url.Values - experr := "parse \":foo\": missing protocol scheme" - rply, err := ari.Call(HTTP_POST, ":foo", data) - - if err == nil || err.Error() != experr { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", experr, err) - } else if rply != nil { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", nil, rply) + resp, err := ari.Call(HTTP_POST, "", nil, map[string]string{"channel": "SIP/1000"}) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) } } -func TestAringoCallSuccess(t *testing.T) { +func TestAringoV1CallDELETESuccess(t *testing.T) { stopChan := make(chan struct{}) - var srv *httptest.Server + defer close(stopChan) + + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.Method != HTTP_DELETE { + t.Errorf("Expected DELETE method, got %s", r.Method) + } + rw.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + ari := &ARInGOV1{ httpClient: http.DefaultClient, + address: strings.TrimPrefix(srv.URL, "http://"), reconnects: -1, delayFunc: fibDuration, evChannel: make(chan map[string]interface{}), @@ -454,156 +495,489 @@ func TestAringoCallSuccess(t *testing.T) { wsListenerExit: stopChan, } - data := url.Values{} - data.Set("test", "string") - expected := []byte("OK") - srv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + resp, err := ari.Call(HTTP_DELETE, "channels/12345", nil, nil) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Expected status 204, got %d", resp.StatusCode) + } +} - rcv, err := url.ParseQuery(r.URL.RawQuery) - if err != nil { - t.Fatal(err) - } +func TestAringoV1Call404(t *testing.T) { + stopChan := make(chan struct{}) + defer close(stopChan) - if !reflect.DeepEqual(data, rcv) { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", data, rcv) - } - rw.Write(expected) + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusNotFound) + rw.Write([]byte(`{"message":"Channel not found"}`)) })) defer srv.Close() - received, err := ari.Call(HTTP_GET, srv.URL, data) + ari := &ARInGOV1{ + httpClient: http.DefaultClient, + address: strings.TrimPrefix(srv.URL, "http://"), + reconnects: -1, + delayFunc: fibDuration, + evChannel: make(chan map[string]interface{}), + errChannel: make(chan error), + wsListenerExit: stopChan, + } + resp, err := ari.Call(HTTP_GET, "channels/nonexistent", nil, nil) if err != nil { - t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", nil, err) + t.Fatalf("Expected no error, got: %v", err) } - - if !reflect.DeepEqual(received, expected) { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", expected, received) + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected status 404, got %d", resp.StatusCode) } } -func TestAringoCallNoGET(t *testing.T) { +func TestAringoV1CallWithQueryAndBody(t *testing.T) { stopChan := make(chan struct{}) - var srv *httptest.Server + defer close(stopChan) + + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("app") != "myapp" { + t.Errorf("Expected query param app=myapp, got %s", r.URL.Query().Get("app")) + } + body, _ := io.ReadAll(r.Body) + vals, _ := url.ParseQuery(string(body)) + if vals.Get("endpoint") != "PJSIP/1000" { + t.Errorf("Expected body param endpoint=PJSIP/1000, got %s", vals.Get("endpoint")) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"id":"channel123"}`)) + })) + defer srv.Close() + ari := &ARInGOV1{ httpClient: http.DefaultClient, + address: strings.TrimPrefix(srv.URL, "http://"), reconnects: -1, delayFunc: fibDuration, evChannel: make(chan map[string]interface{}), errChannel: make(chan error), wsListenerExit: stopChan, } - data := url.Values{ - "test": {"string"}, + + resp, err := ari.Call(HTTP_POST, "channels", + map[string]string{"app": "myapp"}, + map[string]string{"endpoint": "PJSIP/1000"}) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestNewARInGONoConnAttempts(t *testing.T) { + wsUrl := "" + wsOrigin := "" + username := "" + password := "" + userAgent := "" + evChannel := make(chan map[string]any) + errChannel := make(chan error) + stopChan := make(<-chan struct{}) + connectAttempts := 0 + reconnects := -1 + + experr := ErrZeroConnectAttempts + received, err := NewARInGO(wsUrl, wsOrigin, username, password, userAgent, evChannel, + errChannel, stopChan, connectAttempts, reconnects, 0, fibDuration) + + if err != experr { + t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", experr, err) + } else if received != nil { + t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", nil, received) + } +} + +func TestARInGOCallNoConnection(t *testing.T) { + ari := &ARInGO{ + httpClient: new(http.Client), + delayFunc: fibDuration, + pending: make(map[string]chan RESTResponse), + } + + _, err := ari.Call(HTTP_GET, "channels", nil, nil) + if err == nil { + t.Fatal("Expected error for no websocket connection") + } + if err.Error() != "websocket not connected" { + t.Errorf("Expected 'websocket not connected' error, got: %v", err) } +} - srv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - expdata := []byte(data.Encode()) - rcv, err := io.ReadAll(r.Body) +func createTestWSServer(t *testing.T, handler func(ctx context.Context, conn *websocket.Conn)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, nil) if err != nil { - t.Error(err) + t.Logf("websocket accept error: %v", err) return } + defer conn.Close(websocket.StatusNormalClosure, "") + handler(r.Context(), conn) + })) +} + +func TestARInGOCallSuccess(t *testing.T) { + requestReceived := make(chan RESTRequest, 1) - if !reflect.DeepEqual(expdata, rcv) { - t.Errorf("\nExpected: <%+v>, \nReceived: <%+v>", expdata, rcv) + srv := createTestWSServer(t, func(ctx context.Context, conn *websocket.Conn) { + _, data, err := conn.Read(ctx) + if err != nil { + t.Logf("read error: %v", err) + return } - rw.Write([]byte("OK")) - })) + var req RESTRequest + if err := json.Unmarshal(data, &req); err != nil { + t.Logf("unmarshal error: %v", err) + return + } + requestReceived <- req + + resp := RESTResponse{ + Type: "RESTResponse", + RequestID: req.RequestID, + StatusCode: 200, + MessageBody: `{"channels":[]}`, + } + respData, _ := json.Marshal(resp) + conn.Write(ctx, websocket.MessageText, respData) + }) defer srv.Close() - rcv, err := ari.Call(HTTP_POST, srv.URL, data) + wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") + stopChan := make(chan struct{}) + defer close(stopChan) + ari := &ARInGO{ + httpClient: new(http.Client), + wsURL: wsUrl, + wsOrigin: srv.URL, + username: "user", + password: "pass", + userAgent: "test-agent", + reconnects: -1, + delayFunc: fibDuration, + evChannel: make(chan map[string]any, 10), + errChannel: make(chan error, 1), + wsListenerExit: stopChan, + pending: make(map[string]chan RESTResponse), + } + + err := ari.connect() + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + defer ari.disconnect() + + resp, err := ari.Call(HTTP_GET, "channels", map[string]string{"app": "myapp"}, nil) if err != nil { - t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", nil, err) + t.Fatalf("Call failed: %v", err) } - if len(rcv) != 0 { - t.Errorf("\nExpected empty reply, \nReceived: <%+v>", rcv) + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + select { + case req := <-requestReceived: + if req.Type != "RESTRequest" { + t.Errorf("Expected type RESTRequest, got %s", req.Type) + } + if req.Method != HTTP_GET { + t.Errorf("Expected method GET, got %s", req.Method) + } + if req.URI != "channels" { + t.Errorf("Expected URI channels, got %s", req.URI) + } + case <-time.After(time.Second): + t.Fatal("Timeout waiting for request") } } -func TestAringoCallDoErr(t *testing.T) { +func TestARInGOCallWithQueryStrings(t *testing.T) { + var receivedReq RESTRequest + + srv := createTestWSServer(t, func(ctx context.Context, conn *websocket.Conn) { + _, data, err := conn.Read(ctx) + if err != nil { + return + } + json.Unmarshal(data, &receivedReq) + + resp := RESTResponse{ + Type: "RESTResponse", + RequestID: receivedReq.RequestID, + StatusCode: 200, + } + respData, _ := json.Marshal(resp) + conn.Write(ctx, websocket.MessageText, respData) + }) + defer srv.Close() + + wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") stopChan := make(chan struct{}) + defer close(stopChan) - ari := &ARInGOV1{ - httpClient: http.DefaultClient, + ari := &ARInGO{ + httpClient: new(http.Client), + wsURL: wsUrl, reconnects: -1, delayFunc: fibDuration, - evChannel: make(chan map[string]interface{}), - errChannel: make(chan error), + evChannel: make(chan map[string]any, 10), + errChannel: make(chan error, 1), wsListenerExit: stopChan, + pending: make(map[string]chan RESTResponse), } - data := url.Values{ - "test": {"string"}, + err := ari.connect() + if err != nil { + t.Fatalf("Failed to connect: %v", err) } + defer ari.disconnect() - experr := "Post \"\": unsupported protocol scheme \"\"" - _, err := ari.Call(HTTP_POST, "", data) + _, err = ari.Call(HTTP_POST, "channels", + map[string]string{"app": "myapp", "channelId": "chan123"}, + map[string]string{"endpoint": "PJSIP/1000", "timeout": "30"}) + if err != nil { + t.Fatalf("Call failed: %v", err) + } - if err == nil || err.Error() != experr { - t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", experr, err) + time.Sleep(50 * time.Millisecond) + expectedParams := map[string]bool{ + "app": false, + "channelId": false, + "endpoint": false, + "timeout": false, + } + for _, qs := range receivedReq.QueryStrings { + if _, ok := expectedParams[qs.Name]; ok { + expectedParams[qs.Name] = true + } + } + for param, found := range expectedParams { + if !found { + t.Errorf("Expected param %s not found in request", param) + } } } -func TestAringoCall204(t *testing.T) { +func TestARInGOEventReceiving(t *testing.T) { + srv := createTestWSServer(t, func(ctx context.Context, conn *websocket.Conn) { + event := map[string]interface{}{ + "type": "StasisStart", + "application": "myapp", + "channel": map[string]interface{}{ + "id": "chan123", + "state": "Ring", + }, + } + eventData, _ := json.Marshal(event) + conn.Write(ctx, websocket.MessageText, eventData) + time.Sleep(100 * time.Millisecond) + }) + defer srv.Close() + + wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") stopChan := make(chan struct{}) - var srv *httptest.Server + evChannel := make(chan map[string]any, 10) - ari := &ARInGOV1{ - reconnects: -1, + ari := &ARInGO{ + httpClient: new(http.Client), + wsURL: wsUrl, + reconnects: 0, delayFunc: fibDuration, - evChannel: make(chan map[string]interface{}), - errChannel: make(chan error), + evChannel: evChannel, + errChannel: make(chan error, 1), wsListenerExit: stopChan, + pending: make(map[string]chan RESTResponse), } - var data url.Values - srv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(204) - rw.Write([]byte("OK")) - })) - defer srv.Close() - ari.httpClient = http.DefaultClient + err := ari.connect() + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + defer func() { + close(stopChan) + ari.disconnect() + }() - rcv, err := ari.Call(HTTP_POST, srv.URL, data) + select { + case ev := <-evChannel: + if ev["type"] != "StasisStart" { + t.Errorf("Expected type StasisStart, got %v", ev["type"]) + } + if ev["application"] != "myapp" { + t.Errorf("Expected application myapp, got %v", ev["application"]) + } + case <-time.After(2 * time.Second): + t.Fatal("Timeout waiting for event") + } +} +func TestRESTRequestStruct(t *testing.T) { + req := RESTRequest{ + Type: "RESTRequest", + TransactionID: "txn123", + RequestID: "req456", + Method: "POST", + URI: "channels", + ContentType: "application/json", + QueryStrings: []QueryString{ + {Name: "app", Value: "myapp"}, + {Name: "endpoint", Value: "PJSIP/1000"}, + }, + } + + data, err := json.Marshal(req) if err != nil { - t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", nil, err) + t.Fatalf("Failed to marshal: %v", err) } - if len(rcv) != 0 { - t.Errorf("\nExpected empty slice, \nReceived: <%+v>", rcv) + var decoded RESTRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + + if decoded.Type != req.Type { + t.Errorf("Type mismatch: expected %s, got %s", req.Type, decoded.Type) + } + if decoded.TransactionID != req.TransactionID { + t.Errorf("TransactionID mismatch: expected %s, got %s", req.TransactionID, decoded.TransactionID) + } + if decoded.RequestID != req.RequestID { + t.Errorf("RequestID mismatch: expected %s, got %s", req.RequestID, decoded.RequestID) + } + if len(decoded.QueryStrings) != 2 { + t.Errorf("QueryStrings count mismatch: expected 2, got %d", len(decoded.QueryStrings)) } } -func TestAringoCallNot200(t *testing.T) { +func TestRESTResponseStruct(t *testing.T) { + resp := RESTResponse{ + Type: "RESTResponse", + RequestID: "req456", + StatusCode: 200, + ReasonPhrase: "OK", + ContentType: "application/json", + MessageBody: `{"channels":[]}`, + Timestamp: "2024-01-01T12:00:00Z", + AsteriskID: "asterisk-id", + Application: "myapp", + } + + data, err := json.Marshal(resp) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + + var decoded RESTResponse + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + + if decoded.StatusCode != 200 { + t.Errorf("StatusCode mismatch: expected 200, got %d", decoded.StatusCode) + } + if decoded.MessageBody != resp.MessageBody { + t.Errorf("MessageBody mismatch: expected %s, got %s", resp.MessageBody, decoded.MessageBody) + } +} + +func TestQueryStringStruct(t *testing.T) { + qs := QueryString{ + Name: "endpoint", + Value: "PJSIP/1000", + } + + data, err := json.Marshal(qs) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + + expected := `{"name":"endpoint","value":"PJSIP/1000"}` + if string(data) != expected { + t.Errorf("JSON mismatch: expected %s, got %s", expected, string(data)) + } +} + +func TestARInGODisconnect(t *testing.T) { + connClosed := make(chan struct{}) + srv := createTestWSServer(t, func(ctx context.Context, conn *websocket.Conn) { + select { + case <-ctx.Done(): + case <-connClosed: + } + }) + defer srv.Close() + + wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") stopChan := make(chan struct{}) - var srv *httptest.Server - ari := &ARInGOV1{ - reconnects: -1, + ari := &ARInGO{ + httpClient: new(http.Client), + wsURL: wsUrl, + reconnects: 0, delayFunc: fibDuration, - evChannel: make(chan map[string]interface{}), - errChannel: make(chan error), + evChannel: make(chan map[string]any, 10), + errChannel: make(chan error, 1), wsListenerExit: stopChan, + pending: make(map[string]chan RESTResponse), } - srv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(201) - })) + err := ari.connect() + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + + close(connClosed) + close(stopChan) + + time.Sleep(50 * time.Millisecond) +} + +func TestARInGODisconnectNilConnection(t *testing.T) { + ari := &ARInGO{} + + err := ari.disconnect() + if err != nil { + t.Errorf("Disconnect with nil ws returned error: %v", err) + } +} + +func TestARInGOPendingMapInitialization(t *testing.T) { + srv := createTestWSServer(t, func(ctx context.Context, conn *websocket.Conn) { + <-ctx.Done() + }) defer srv.Close() - ari.httpClient = srv.Client() - var data url.Values - experr := "UNEXPECTED_REPLY_CODE: 201" - _, err := ari.Call(HTTP_POST, srv.URL, data) + wsUrl := "ws" + strings.TrimPrefix(srv.URL, "http") + stopChan := make(chan struct{}) + defer close(stopChan) + + ari := &ARInGO{ + httpClient: new(http.Client), + wsURL: wsUrl, + reconnects: 0, + delayFunc: fibDuration, + evChannel: make(chan map[string]any, 10), + errChannel: make(chan error, 1), + wsListenerExit: stopChan, + } - if err == nil || err.Error() != experr { - t.Fatalf("\nExpected: <%+v>, \nReceived: <%+v>", experr, err) + err := ari.connect() + if err != nil { + t.Fatalf("Failed to connect: %v", err) } + defer ari.disconnect() + if ari.pending == nil { + t.Error("pending map should be initialized after connect") + } } func fibDuration(durationUnit, maxDuration time.Duration) func() time.Duration { diff --git a/aringov1.go b/aringov1.go index 7879d98..c5cf72b 100644 --- a/aringov1.go +++ b/aringov1.go @@ -125,7 +125,6 @@ func (ari *ARInGOV1) disconnect() error { } // Call represents one REST call to Asterisk using httpClient call -// If there is a reply from Asterisk it should be in form map[string]interface{} func (ari *ARInGOV1) Call(method, uri string, queryStr map[string]string, bodyParams map[string]string) (reply RESTResponse, err error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()