package ocpp import ( "context" "net/http" "strings" "sync" "testing" "time" ) // wsURL turns an httptest http:// base URL into a ws:// URL with a path. func wsURL(base, path string) string { return "ws" + strings.TrimPrefix(base, "http") + path } // simCP is a simulated OCPP 1.6J charge point used by the CSMS and proxy tests. // It auto-answers inbound control CALLs with "Accepted" (recording them for // assertions) and can itself send CALLs (BootNotification, StatusNotification, …) // and await their result. type simCP struct { conn *Conn mu sync.Mutex pending map[string]chan Message recv chan Message // inbound CALLs (CSMS → CP), for assertions } func dialSimCP(t *testing.T, url string, header http.Header) *simCP { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() conn, err := Dial(ctx, url, []string{"ocpp1.6"}, header) if err != nil { t.Fatalf("dial sim charge point: %v", err) } cp := &simCP{conn: conn, pending: map[string]chan Message{}, recv: make(chan Message, 64)} go cp.loop() return cp } func (cp *simCP) loop() { for { data, err := cp.conn.ReadMessage() if err != nil { return } msg, err := DecodeMessage(data) if err != nil { continue } switch msg.Type { case MessageTypeCall: select { case cp.recv <- msg: default: } out, _ := EncodeCallResult(msg.ID, cp.responseFor(msg.Action)) _ = cp.conn.WriteMessage(out) case MessageTypeCallResult, MessageTypeCallError: cp.mu.Lock() ch := cp.pending[msg.ID] delete(cp.pending, msg.ID) cp.mu.Unlock() if ch != nil { ch <- msg } } } } // responseFor returns the payload the sim answers an inbound control CALL with. func (cp *simCP) responseFor(action string) any { switch action { case "GetConfiguration": return map[string]any{ "configurationKey": []any{ map[string]any{"key": "HeartbeatInterval", "readonly": false, "value": "300"}, }, } default: // RemoteStart/Stop, Reset, ChangeAvailability, UnlockConnector, // TriggerMessage, SetChargingProfile, ClearChargingProfile, ChangeConfiguration. return map[string]any{"status": "Accepted"} } } // call sends a CALL from the charge point and waits for the reply. func (cp *simCP) call(t *testing.T, action string, payload any) Message { t.Helper() id := newMessageID() frame, err := EncodeCall(id, action, payload) if err != nil { t.Fatalf("encode %s: %v", action, err) } ch := make(chan Message, 1) cp.mu.Lock() cp.pending[id] = ch cp.mu.Unlock() if err := cp.conn.WriteMessage(frame); err != nil { t.Fatalf("write %s: %v", action, err) } select { case m := <-ch: return m case <-time.After(5 * time.Second): t.Fatalf("timeout awaiting %s reply", action) return Message{} } } // waitRecv returns the next inbound control CALL the sim received, or fails. func (cp *simCP) waitRecv(t *testing.T) Message { t.Helper() select { case m := <-cp.recv: return m case <-time.After(5 * time.Second): t.Fatal("timeout awaiting inbound control call") return Message{} } } func (cp *simCP) close() { _ = cp.conn.Close() }