174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package ocpp
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/lorenzodonini/ocpp-go/ocppj"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testBootFrame = `[2,"orig-id","BootNotification",{"chargePointVendor":"v","chargePointModel":"m"}]`
|
|
|
|
func TestForwarderWithMessageID(t *testing.T) {
|
|
frame, err := withMessageID([]byte(testBootFrame), "new-id")
|
|
require.NoError(t, err)
|
|
|
|
msgType, msgID, action, err := parseOCPPFrame(frame)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ocppj.CALL, msgType)
|
|
assert.Equal(t, "new-id", msgID)
|
|
assert.Equal(t, "BootNotification", action)
|
|
assert.Contains(t, string(frame), `"chargePointVendor":"v"`)
|
|
|
|
_, err = withMessageID([]byte(`{}`), "new-id")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// fakeChannel implements ws.Channel for hook tests
|
|
type fakeChannel string
|
|
|
|
func (c fakeChannel) ID() string { return string(c) }
|
|
func (c fakeChannel) RemoteAddr() net.Addr { return nil }
|
|
func (c fakeChannel) TLSConnectionState() *tls.ConnectionState { return nil }
|
|
func (c fakeChannel) IsConnected() bool { return true }
|
|
|
|
func TestForwarderBootNotificationCache(t *testing.T) {
|
|
const id = "fwd-boot-cache-test"
|
|
|
|
onChargerMessage(fakeChannel(id), []byte(testBootFrame))
|
|
|
|
bootMu.Lock()
|
|
boot := lastBoot[id]
|
|
bootMu.Unlock()
|
|
assert.Equal(t, testBootFrame, string(boot))
|
|
|
|
onChargerDisconnect(fakeChannel(id))
|
|
|
|
bootMu.Lock()
|
|
boot = lastBoot[id]
|
|
bootMu.Unlock()
|
|
assert.Nil(t, boot)
|
|
}
|
|
|
|
// forwarderTestSetup marks a charger as connected with a cached boot frame and
|
|
// fast reconnect, returning a cleanup function.
|
|
func forwarderTestSetup(t *testing.T, id string) func() {
|
|
t.Helper()
|
|
|
|
delay := reconnectInitialDelay
|
|
reconnectInitialDelay = 10 * time.Millisecond
|
|
|
|
sidecarsMu.Lock()
|
|
connectedChargers[id] = true
|
|
sidecarsMu.Unlock()
|
|
|
|
bootMu.Lock()
|
|
lastBoot[id] = []byte(testBootFrame)
|
|
bootMu.Unlock()
|
|
|
|
return func() {
|
|
ApplyForwarderRules(nil)
|
|
sidecarsMu.Lock()
|
|
delete(connectedChargers, id)
|
|
sidecarsMu.Unlock()
|
|
bootMu.Lock()
|
|
delete(lastBoot, id)
|
|
bootMu.Unlock()
|
|
reconnectInitialDelay = delay
|
|
}
|
|
}
|
|
|
|
// expectBootReplay asserts that a replayed BootNotification with a fresh message id arrives.
|
|
func expectBootReplay(t *testing.T, frames <-chan []byte) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case data := <-frames:
|
|
msgType, msgID, action, err := parseOCPPFrame(data)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ocppj.CALL, msgType)
|
|
assert.Equal(t, "BootNotification", action)
|
|
assert.NotEqual(t, "orig-id", msgID)
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timeout waiting for upstream reconnect")
|
|
}
|
|
}
|
|
|
|
func TestForwarderUpstreamReconnect(t *testing.T) {
|
|
const id = "fwd-reconnect-test"
|
|
|
|
var conns atomic.Int32
|
|
frames := make(chan []byte, 1)
|
|
|
|
srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{"ocpp1.6"}})
|
|
if err != nil {
|
|
return
|
|
}
|
|
if conns.Add(1) == 1 {
|
|
// simulate upstream dropping the connection without close handshake
|
|
c.CloseNow()
|
|
return
|
|
}
|
|
if _, data, err := c.Read(r.Context()); err == nil {
|
|
select {
|
|
case frames <- data:
|
|
default:
|
|
}
|
|
}
|
|
c.CloseNow()
|
|
}))
|
|
srv.Start()
|
|
|
|
cleanup := forwarderTestSetup(t, id)
|
|
defer cleanup()
|
|
|
|
ApplyForwarderRules([]ForwarderRule{{StationID: id, UpstreamURL: "ws" + strings.TrimPrefix(srv.URL, "http")}})
|
|
|
|
expectBootReplay(t, frames)
|
|
assert.GreaterOrEqual(t, conns.Load(), int32(2))
|
|
}
|
|
|
|
func TestForwarderDialRetry(t *testing.T) {
|
|
const id = "fwd-dial-retry-test"
|
|
|
|
var reqs atomic.Int32
|
|
frames := make(chan []byte, 1)
|
|
|
|
srv := httptest.NewTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if reqs.Add(1) <= 2 {
|
|
// reject the websocket upgrade to simulate an unreachable upstream
|
|
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{Subprotocols: []string{"ocpp1.6"}})
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, data, err := c.Read(r.Context()); err == nil {
|
|
select {
|
|
case frames <- data:
|
|
default:
|
|
}
|
|
}
|
|
c.CloseNow()
|
|
}))
|
|
srv.Start()
|
|
|
|
cleanup := forwarderTestSetup(t, id)
|
|
defer cleanup()
|
|
|
|
ApplyForwarderRules([]ForwarderRule{{StationID: id, UpstreamURL: "ws" + strings.TrimPrefix(srv.URL, "http")}})
|
|
|
|
expectBootReplay(t, frames)
|
|
assert.GreaterOrEqual(t, reqs.Load(), int32(3))
|
|
}
|