From 983d863ae8c99eb6bc5259714efc0d4a7a7da767 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Wed, 16 Nov 2022 21:42:26 +0100 Subject: [PATCH 01/47] Add IsConnected utility method for charge points Signed-off-by: Lorenzo --- ocpp1.6/charge_point.go | 4 ++++ ocpp1.6/v16.go | 3 +++ ocpp1.6_test/ocpp16_test.go | 15 +++++++++++++++ ocpp2.0.1/charging_station.go | 4 ++++ ocpp2.0.1/v2.go | 3 +++ ocpp2.0.1_test/ocpp2_test.go | 15 +++++++++++++++ ocppj/charge_point_test.go | 7 +++++++ ocppj/client.go | 4 ++++ ocppj/ocppj_test.go | 5 +++++ 9 files changed, 60 insertions(+) diff --git a/ocpp1.6/charge_point.go b/ocpp1.6/charge_point.go index 5e81b93d..e0e87a24 100644 --- a/ocpp1.6/charge_point.go +++ b/ocpp1.6/charge_point.go @@ -340,6 +340,10 @@ func (cp *chargePoint) Stop() { } } +func (cp *chargePoint) IsConnected() bool { + return cp.client.IsConnected() +} + func (cp *chargePoint) notImplementedError(requestId string, action string) { err := cp.client.SendError(requestId, ocppj.NotImplemented, fmt.Sprintf("no handler for action %v implemented", action), nil) if err != nil { diff --git a/ocpp1.6/v16.go b/ocpp1.6/v16.go index f3806a7b..85a5fd56 100644 --- a/ocpp1.6/v16.go +++ b/ocpp1.6/v16.go @@ -101,6 +101,9 @@ type ChargePoint interface { // Stops the charge point routine, disconnecting it from the central system. // Any pending requests are discarded. Stop() + // Returns true if the charge point is currently connected to the central system, false otherwise. + // While automatically reconnecting to the central system, the method returns false. + IsConnected() bool // Errors returns a channel for error messages. If it doesn't exist it es created. // The channel is closed by the charge point when stopped. Errors() <-chan error diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index be21ded4..d3358653 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -143,6 +143,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // Default queue capacity const queueCapacity = 10 @@ -684,6 +689,16 @@ func (suite *OcppV16TestSuite) SetupTest() { ocppj.SetMessageIdGenerator(suite.messageIdGenerator.generateId) } +func (suite *OcppV16TestSuite) TestIsConnected() { + t := suite.T() + // Simulate ws connected + mockCall := suite.mockWsClient.On("IsConnected").Return(true) + assert.True(t, suite.chargePoint.IsConnected()) + // Simulate ws disconnected + mockCall.Return(false) + assert.False(t, suite.chargePoint.IsConnected()) +} + //TODO: implement generic protocol tests func TestOcpp16Protocol(t *testing.T) { diff --git a/ocpp2.0.1/charging_station.go b/ocpp2.0.1/charging_station.go index 6fbea1f1..5f4bc827 100644 --- a/ocpp2.0.1/charging_station.go +++ b/ocpp2.0.1/charging_station.go @@ -591,6 +591,10 @@ func (cs *chargingStation) Stop() { cs.client.Stop() } +func (cs *chargingStation) IsConnected() bool { + return cs.client.IsConnected() +} + func (cs *chargingStation) notImplementedError(requestId string, action string) { err := cs.client.SendError(requestId, ocppj.NotImplemented, fmt.Sprintf("no handler for action %v implemented", action), nil) if err != nil { diff --git a/ocpp2.0.1/v2.go b/ocpp2.0.1/v2.go index 99460341..f8df9a29 100644 --- a/ocpp2.0.1/v2.go +++ b/ocpp2.0.1/v2.go @@ -165,6 +165,9 @@ type ChargingStation interface { // Stops the charging station routine, disconnecting it from the CSMS. // Any pending requests are discarded. Stop() + // Returns true if the charging station is currently connected to the CSMS, false otherwise. + // While automatically reconnecting to the CSMS, the method returns false. + IsConnected() bool // Errors returns a channel for error messages. If it doesn't exist it es created. // The channel is closed by the charging station when stopped. Errors() <-chan error diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index 9e144378..e60a4efd 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -159,6 +159,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // Default queue capacity const queueCapacity = 10 @@ -1118,6 +1123,16 @@ func (suite *OcppV2TestSuite) SetupTest() { ocppj.SetMessageIdGenerator(suite.messageIdGenerator.generateId) } +func (suite *OcppV2TestSuite) TestIsConnected() { + t := suite.T() + // Simulate ws connected + mockCall := suite.mockWsClient.On("IsConnected").Return(true) + assert.True(t, suite.chargingStation.IsConnected()) + // Simulate ws disconnected + mockCall.Return(false) + assert.False(t, suite.chargingStation.IsConnected()) +} + //TODO: implement generic protocol tests func TestOcpp2Protocol(t *testing.T) { diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 3595824a..e704dc9c 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -562,9 +562,11 @@ func (suite *OcppJTestSuite) TestClientReconnected() { require.NotNil(t, call) writeC <- call }).Return(nil) + isConnectedCall := suite.mockClient.On("IsConnected").Return(true) // Start normally err := suite.chargePoint.Start("someUrl") require.Nil(t, err) + assert.True(t, suite.chargePoint.IsConnected()) // Start mocked response routine go func() { counter := 0 @@ -593,16 +595,20 @@ func (suite *OcppJTestSuite) TestClientReconnected() { } // Wait for trigger disconnect after a few responses were returned <-triggerC + isConnectedCall.Return(false) suite.mockClient.DisconnectedHandler(disconnectError) // One message was sent, but all others are still in queue time.Sleep(200 * time.Millisecond) assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) // Wait for some more time and then reconnect time.Sleep(500 * time.Millisecond) + isConnectedCall.Return(true) suite.mockClient.ReconnectedHandler() assert.False(t, suite.clientDispatcher.IsPaused()) assert.True(t, suite.clientDispatcher.IsRunning()) assert.False(t, suite.clientRequestQueue.IsEmpty()) + assert.True(t, suite.chargePoint.IsConnected()) // Wait until remaining messages are sent <-triggerC assert.False(t, suite.clientDispatcher.IsPaused()) @@ -610,6 +616,7 @@ func (suite *OcppJTestSuite) TestClientReconnected() { assert.Equal(t, messagesToQueue, sentMessages) assert.True(t, suite.clientRequestQueue.IsEmpty()) assert.False(t, state.HasPendingRequest()) + assert.True(t, suite.chargePoint.IsConnected()) } // TestClientResponseTimeout ensures that upon a response timeout, the client dispatcher: diff --git a/ocppj/client.go b/ocppj/client.go index 78fd0e01..1a0e9482 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -115,6 +115,10 @@ func (c *Client) Stop() { <-cleanupC } +func (c *Client) IsConnected() bool { + return c.client.IsConnected() +} + // Sends an OCPP Request to the server. // The protocol is based on request-response and cannot send multiple messages concurrently. // To guarantee this, outgoing messages are added to a queue and processed sequentially. diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index 9b0253a6..bb4ef42c 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -162,6 +162,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // ---------------------- MOCK FEATURE ---------------------- const ( MockFeatureName = "Mock" From 564c732aba985e48660631040d3dbd1fa82dc136 Mon Sep 17 00:00:00 2001 From: Marco Trettner Date: Wed, 16 Nov 2022 08:47:22 +0100 Subject: [PATCH 02/47] fix: use go-routines in handle functions to prevent blocking the underlying websocket when the callback functions are blocking --- ocpp1.6/central_system.go | 9 ++++++--- ocpp2.0.1/csms.go | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index 95d5d85d..2b8bec43 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -519,7 +519,8 @@ func (cs *centralSystem) handleIncomingRequest(chargePoint ChargePointConnection func (cs *centralSystem) handleIncomingConfirmation(chargePoint ChargePointConnection, confirmation ocpp.Response, requestId string) { if callback, ok := cs.callbackQueue.Dequeue(chargePoint.ID()); ok { - callback(confirmation, nil) + // Execute in separate goroutine, so the caller goroutine is available + go callback(confirmation, nil) } else { err := fmt.Errorf("no handler available for call of type %v from client %s for request %s", confirmation.GetFeatureName(), chargePoint.ID(), requestId) cs.error(err) @@ -528,7 +529,8 @@ func (cs *centralSystem) handleIncomingConfirmation(chargePoint ChargePointConne func (cs *centralSystem) handleIncomingError(chargePoint ChargePointConnection, err *ocpp.Error, details interface{}) { if callback, ok := cs.callbackQueue.Dequeue(chargePoint.ID()); ok { - callback(nil, err) + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) } else { err := fmt.Errorf("no handler available for call error %w from client %s", err, chargePoint.ID()) cs.error(err) @@ -537,7 +539,8 @@ func (cs *centralSystem) handleIncomingError(chargePoint ChargePointConnection, func (cs *centralSystem) handleCanceledRequest(chargePointID string, request ocpp.Request, err *ocpp.Error) { if callback, ok := cs.callbackQueue.Dequeue(chargePointID); ok { - callback(nil, err) + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) } else { err := fmt.Errorf("no handler available for canceled request %s for client %s: %w", request.GetFeatureName(), chargePointID, err) diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index d574cc7f..a612ea86 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -990,7 +990,8 @@ func (cs *csms) handleIncomingRequest(chargingStation ChargingStationConnection, func (cs *csms) handleIncomingResponse(chargingStation ChargingStationConnection, response ocpp.Response, requestId string) { if callback, ok := cs.callbackQueue.Dequeue(chargingStation.ID()); ok { - callback(response, nil) + // Execute in separate goroutine, so the caller goroutine is available + go callback(response, nil) } else { err := fmt.Errorf("no handler available for call of type %v from client %s for request %s", response.GetFeatureName(), chargingStation.ID(), requestId) cs.error(err) @@ -999,7 +1000,8 @@ func (cs *csms) handleIncomingResponse(chargingStation ChargingStationConnection func (cs *csms) handleIncomingError(chargingStation ChargingStationConnection, err *ocpp.Error, details interface{}) { if callback, ok := cs.callbackQueue.Dequeue(chargingStation.ID()); ok { - callback(nil, err) + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) } else { cs.error(fmt.Errorf("no handler available for call error %w from client %s", err, chargingStation.ID())) } @@ -1007,7 +1009,8 @@ func (cs *csms) handleIncomingError(chargingStation ChargingStationConnection, e func (cs *csms) handleCanceledRequest(chargePointID string, request ocpp.Request, err *ocpp.Error) { if callback, ok := cs.callbackQueue.Dequeue(chargePointID); ok { - callback(nil, err) + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) } else { err := fmt.Errorf("no handler available for canceled request %s for client %s: %w", request.GetFeatureName(), chargePointID, err) From 956223d45e767a294840642385e48d298f925fd6 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 11 Dec 2022 14:08:15 +0100 Subject: [PATCH 03/47] Make SampledValue.Value field not required Signed-off-by: Lorenzo --- ocpp2.0.1/types/types.go | 2 +- ocpp2.0.1_test/common_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index 3655ab8f..daad62ab 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -681,7 +681,7 @@ type SignedMeterValue struct { } type SampledValue struct { - Value float64 `json:"value" validate:"required"` // Indicates the measured value. + Value float64 `json:"value"` // Indicates the measured value. This value is required. Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand"` // Type of measurement. Default = "Energy.Active.Import.Register" Phase Phase `json:"phase,omitempty" validate:"omitempty,phase"` // Indicates how the measured value is to be interpreted. For instance between L1 and neutral (L1-N) Please note that not all values of phase are applicable to all Measurands. When phase is absent, the measured value is interpreted as an overall value. diff --git a/ocpp2.0.1_test/common_test.go b/ocpp2.0.1_test/common_test.go index 63e6bbb0..728e25f1 100644 --- a/ocpp2.0.1_test/common_test.go +++ b/ocpp2.0.1_test/common_test.go @@ -237,6 +237,7 @@ func (suite *OcppV2TestSuite) TestSampledValueValidation() { {types.SampledValue{Value: 3.14, Context: types.ReadingContextTransactionEnd}, true}, {types.SampledValue{Value: 3.14}, true}, {types.SampledValue{Value: -3.14}, true}, + {types.SampledValue{}, true}, {types.SampledValue{Value: 3.14, Context: "invalidContext"}, false}, {types.SampledValue{Value: 3.14, Measurand: "invalidMeasurand"}, false}, {types.SampledValue{Value: 3.14, Phase: "invalidPhase"}, false}, From e66011a1ddfccc91d78770d82035f31563f2c28a Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 24 Dec 2022 11:44:43 +0100 Subject: [PATCH 04/47] v1.6: Change ChargingProfileStatus value from NotImplemented to NotSupported Signed-off-by: Lorenzo --- example/1.6/cp/handler.go | 2 +- ocpp1.6/smartcharging/set_charging_profile.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/1.6/cp/handler.go b/example/1.6/cp/handler.go index f3cabab8..a7e42de0 100644 --- a/example/1.6/cp/handler.go +++ b/example/1.6/cp/handler.go @@ -293,7 +293,7 @@ func (handler *ChargePointHandler) OnCancelReservation(request *reservation.Canc func (handler *ChargePointHandler) OnSetChargingProfile(request *smartcharging.SetChargingProfileRequest) (confirmation *smartcharging.SetChargingProfileConfirmation, err error) { //TODO: handle logic logDefault(request.GetFeatureName()).Warn("no set charging profile logic implemented yet") - return smartcharging.NewSetChargingProfileConfirmation(smartcharging.ChargingProfileStatusNotImplemented), nil + return smartcharging.NewSetChargingProfileConfirmation(smartcharging.ChargingProfileStatusNotSupported), nil } func (handler *ChargePointHandler) OnClearChargingProfile(request *smartcharging.ClearChargingProfileRequest) (confirmation *smartcharging.ClearChargingProfileConfirmation, err error) { diff --git a/ocpp1.6/smartcharging/set_charging_profile.go b/ocpp1.6/smartcharging/set_charging_profile.go index 347c798e..9392a82e 100644 --- a/ocpp1.6/smartcharging/set_charging_profile.go +++ b/ocpp1.6/smartcharging/set_charging_profile.go @@ -15,15 +15,15 @@ const SetChargingProfileFeatureName = "SetChargingProfile" type ChargingProfileStatus string const ( - ChargingProfileStatusAccepted ChargingProfileStatus = "Accepted" - ChargingProfileStatusRejected ChargingProfileStatus = "Rejected" - ChargingProfileStatusNotImplemented ChargingProfileStatus = "NotImplemented" + ChargingProfileStatusAccepted ChargingProfileStatus = "Accepted" + ChargingProfileStatusRejected ChargingProfileStatus = "Rejected" + ChargingProfileStatusNotSupported ChargingProfileStatus = "NotSupported" ) func isValidChargingProfileStatus(fl validator.FieldLevel) bool { status := ChargingProfileStatus(fl.Field().String()) switch status { - case ChargingProfileStatusAccepted, ChargingProfileStatusRejected, ChargingProfileStatusNotImplemented: + case ChargingProfileStatusAccepted, ChargingProfileStatusRejected, ChargingProfileStatusNotSupported: return true default: return false From b542323eb512b62b64e257d74f3b99ee03de33e8 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 24 Dec 2022 15:22:12 +0100 Subject: [PATCH 05/47] v2.0.1: Fix incorrect IdTokenType fixes occurrences in: - reservation.ReserveNow - remotecontrol.RequestStartTransaction Signed-off-by: Lorenzo --- ocpp2.0.1/csms.go | 4 +- .../request_start_transaction.go | 6 +-- ocpp2.0.1/reservation/reserve_now.go | 14 +++--- ocpp2.0.1/transactions/transaction_event.go | 4 +- ocpp2.0.1/v2.go | 4 +- .../request_start_transaction_test.go | 46 +++++++++-------- ocpp2.0.1_test/reserve_now_test.go | 49 ++++++++++--------- 7 files changed, 67 insertions(+), 60 deletions(-) diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index a612ea86..ba0d7aee 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -431,7 +431,7 @@ func (cs *csms) PublishFirmware(clientId string, callback func(*firmware.Publish return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdTokenType, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error { +func (cs *csms) RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdToken, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error { request := remotecontrol.NewRequestStartTransactionRequest(remoteStartID, IdToken) for _, fn := range props { fn(request) @@ -461,7 +461,7 @@ func (cs *csms) RequestStopTransaction(clientId string, callback func(*remotecon return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdTokenType, props ...func(request *reservation.ReserveNowRequest)) error { +func (cs *csms) ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdToken, props ...func(request *reservation.ReserveNowRequest)) error { request := reservation.NewReserveNowRequest(id, expiryDateTime, idToken) for _, fn := range props { fn(request) diff --git a/ocpp2.0.1/remotecontrol/request_start_transaction.go b/ocpp2.0.1/remotecontrol/request_start_transaction.go index e63d839a..13e3f124 100644 --- a/ocpp2.0.1/remotecontrol/request_start_transaction.go +++ b/ocpp2.0.1/remotecontrol/request_start_transaction.go @@ -33,9 +33,9 @@ func isValidRequestStartStopStatus(fl validator.FieldLevel) bool { type RequestStartTransactionRequest struct { EvseID *int `json:"evseId,omitempty" validate:"omitempty,gt=0"` RemoteStartID int `json:"remoteStartId" validate:"gte=0"` - IDToken types.IdTokenType `json:"idToken" validate:"idTokenType"` + IDToken types.IdToken `json:"idToken" validate:"idTokenType"` ChargingProfile *types.ChargingProfile `json:"chargingProfile,omitempty"` - GroupIdToken types.IdTokenType `json:"groupIdToken,omitempty" validate:"omitempty,idTokenType"` + GroupIdToken *types.IdToken `json:"groupIdToken,omitempty" validate:"omitempty,dive"` } // This field definition of the RequestStartTransaction response payload, sent by the Charging Station to the CSMS in response to a RequestStartTransactionRequest. @@ -77,7 +77,7 @@ func (c RequestStartTransactionResponse) GetFeatureName() string { } // Creates a new RequestStartTransactionRequest, containing all required fields. Optional fields may be set afterwards. -func NewRequestStartTransactionRequest(remoteStartID int, IdToken types.IdTokenType) *RequestStartTransactionRequest { +func NewRequestStartTransactionRequest(remoteStartID int, IdToken types.IdToken) *RequestStartTransactionRequest { return &RequestStartTransactionRequest{RemoteStartID: remoteStartID, IDToken: IdToken} } diff --git a/ocpp2.0.1/reservation/reserve_now.go b/ocpp2.0.1/reservation/reserve_now.go index a5f916f9..bdac41fc 100644 --- a/ocpp2.0.1/reservation/reserve_now.go +++ b/ocpp2.0.1/reservation/reserve_now.go @@ -78,12 +78,12 @@ func isValidConnectorType(fl validator.FieldLevel) bool { // The field definition of the ReserveNow request payload sent by the CSMS to the Charging Station. type ReserveNowRequest struct { - ID int `json:"id" validate:"gte=0"` // ID of reservation - ExpiryDateTime *types.DateTime `json:"expiryDateTime" validate:"required"` - ConnectorType ConnectorType `json:"connectorType,omitempty" validate:"omitempty,connectorType"` - EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` - IdToken types.IdTokenType `json:"idToken" validate:"required,idTokenType"` - GroupIdToken types.IdTokenType `json:"groupIdToken,omitempty" validate:"omitempty,idTokenType"` + ID int `json:"id" validate:"gte=0"` // ID of reservation + ExpiryDateTime *types.DateTime `json:"expiryDateTime" validate:"required"` + ConnectorType ConnectorType `json:"connectorType,omitempty" validate:"omitempty,connectorType"` + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` + IdToken types.IdToken `json:"idToken" validate:"required,dive"` + GroupIdToken *types.IdToken `json:"groupIdToken,omitempty" validate:"omitempty,dive"` } // This field definition of the ReserveNow response payload, sent by the Charging Station to the CSMS in response to a ReserveNowRequest. @@ -125,7 +125,7 @@ func (c ReserveNowResponse) GetFeatureName() string { } // Creates a new ReserveNowRequest, containing all required fields. Optional fields may be set afterwards. -func NewReserveNowRequest(id int, expiryDateTime *types.DateTime, idToken types.IdTokenType) *ReserveNowRequest { +func NewReserveNowRequest(id int, expiryDateTime *types.DateTime, idToken types.IdToken) *ReserveNowRequest { return &ReserveNowRequest{ID: id, ExpiryDateTime: expiryDateTime, IdToken: idToken} } diff --git a/ocpp2.0.1/transactions/transaction_event.go b/ocpp2.0.1/transactions/transaction_event.go index c7620531..e759d179 100644 --- a/ocpp2.0.1/transactions/transaction_event.go +++ b/ocpp2.0.1/transactions/transaction_event.go @@ -144,9 +144,9 @@ type TransactionEventRequest struct { Offline bool `json:"offline,omitempty"` NumberOfPhasesUsed *int `json:"numberOfPhasesUsed,omitempty" validate:"omitempty,gte=0"` CableMaxCurrent *int `json:"cableMaxCurrent,omitempty"` // The maximum current of the connected cable in Ampere (A). - ReservationID *int `json:"reservationId,omitempty"` // The Id of the reservation that terminates as a result of this transaction. + ReservationID *int `json:"reservationId,omitempty"` // The ID of the reservation that terminates as a result of this transaction. TransactionInfo Transaction `json:"transactionInfo" validate:"required"` // Contains transaction specific information. - IDToken *types.IdToken `json:"idToken,omitempty" validate:"omitempty"` + IDToken *types.IdToken `json:"idToken,omitempty" validate:"omitempty,dive"` Evse *types.EVSE `json:"evse,omitempty" validate:"omitempty"` // Identifies which evse (and connector) of the Charging Station is used. MeterValue []types.MeterValue `json:"meterValue,omitempty" validate:"omitempty,dive"` // Contains the relevant meter values. } diff --git a/ocpp2.0.1/v2.go b/ocpp2.0.1/v2.go index f8df9a29..a26bf73e 100644 --- a/ocpp2.0.1/v2.go +++ b/ocpp2.0.1/v2.go @@ -301,11 +301,11 @@ type CSMS interface { // Publishes a firmware to a local controller, allowing charging stations to download the same firmware from the local controller directly. PublishFirmware(clientId string, callback func(*firmware.PublishFirmwareResponse, error), location string, checksum string, requestID int, props ...func(request *firmware.PublishFirmwareRequest)) error // Remotely triggers a transaction to be started on a charging station. - RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdTokenType, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error + RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdToken, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error // Remotely triggers an ongoing transaction to be stopped on a charging station. RequestStopTransaction(clientId string, callback func(*remotecontrol.RequestStopTransactionResponse, error), transactionID string, props ...func(request *remotecontrol.RequestStopTransactionRequest)) error // Attempts to reserve a connector for an EV, on a specific charging station. - ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdTokenType, props ...func(request *reservation.ReserveNowRequest)) error + ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdToken, props ...func(request *reservation.ReserveNowRequest)) error // Instructs the Charging Station to reset itself. Reset(clientId string, callback func(*provisioning.ResetResponse, error), t provisioning.ResetType, props ...func(request *provisioning.ResetRequest)) error // Sends a local authorization list to a charging station, which can be used for the authorization of idTokens. diff --git a/ocpp2.0.1_test/request_start_transaction_test.go b/ocpp2.0.1_test/request_start_transaction_test.go index 13388a2f..c778a68c 100644 --- a/ocpp2.0.1_test/request_start_transaction_test.go +++ b/ocpp2.0.1_test/request_start_transaction_test.go @@ -32,17 +32,17 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionRequestValidation() { }, } var requestTable = []GenericTestEntry{ - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, true}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile}, true}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode}, true}, - {remotecontrol.RequestStartTransactionRequest{RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode}, true}, - {remotecontrol.RequestStartTransactionRequest{IDToken: types.IdTokenTypeKeyCode}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {remotecontrol.RequestStartTransactionRequest{RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {remotecontrol.RequestStartTransactionRequest{IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, {remotecontrol.RequestStartTransactionRequest{}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(0), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: -1, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: "invalidIdToken", ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &types.ChargingProfile{}, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: "invalidGroupIdToken"}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(0), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: -1, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: "invalidIdToken"}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &types.ChargingProfile{}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: "invalidGroupIdToken"}}, false}, } ExecuteGenericTestTable(t, requestTable) } @@ -69,7 +69,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { wsUrl := "someUrl" evseId := newInt(1) remoteStartID := 42 - idToken := types.IdTokenTypeKeyCode + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} schedule := []types.ChargingSchedule{ { ID: 1, @@ -89,12 +89,12 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { ChargingProfileKind: types.ChargingProfileKindAbsolute, ChargingSchedule: schedule, } - groupIdToken := types.IdTokenTypeISO15693 + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} status := remotecontrol.RequestStartStopStatusAccepted transactionId := "12345" statusInfo := types.StatusInfo{ReasonCode: "200"} - requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":"%v","chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":"%v"}]`, - messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":{"idToken":"%s","type":"%s"},"chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken.IdToken, idToken.Type, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken.IdToken, groupIdToken.Type) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","transactionId":"%v","statusInfo":{"reasonCode":"%v"}}]`, messageId, status, transactionId, statusInfo.ReasonCode) requestStartTransactionResponse := remotecontrol.NewRequestStartTransactionResponse(status) @@ -108,7 +108,8 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { require.True(t, ok) assert.Equal(t, *evseId, *request.EvseID) assert.Equal(t, remoteStartID, request.RemoteStartID) - assert.Equal(t, idToken, request.IDToken) + assert.Equal(t, idToken.IdToken, request.IDToken.IdToken) + assert.Equal(t, idToken.Type, request.IDToken.Type) assert.Equal(t, chargingProfile.ID, request.ChargingProfile.ID) assert.Equal(t, chargingProfile.ChargingProfilePurpose, request.ChargingProfile.ChargingProfilePurpose) assert.Equal(t, chargingProfile.ChargingProfileKind, request.ChargingProfile.ChargingProfileKind) @@ -119,6 +120,9 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { require.Len(t, s.ChargingSchedulePeriod, len(chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod)) assert.Equal(t, chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod[0].Limit, s.ChargingSchedulePeriod[0].Limit) assert.Equal(t, chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod[0].StartPeriod, s.ChargingSchedulePeriod[0].StartPeriod) + require.NotNil(t, request.GroupIdToken) + assert.Equal(t, groupIdToken.IdToken, request.GroupIdToken.IdToken) + assert.Equal(t, groupIdToken.Type, request.GroupIdToken.Type) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -137,7 +141,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { }, remoteStartID, idToken, func(request *remotecontrol.RequestStartTransactionRequest) { request.EvseID = evseId request.ChargingProfile = &chargingProfile - request.GroupIdToken = groupIdToken + request.GroupIdToken = &groupIdToken }) require.Nil(t, err) result := <-resultChannel @@ -148,7 +152,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionInvalidEndpoint() { messageId := defaultMessageId evseId := newInt(1) remoteStartID := 42 - idToken := types.IdTokenTypeKeyCode + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} schedule := []types.ChargingSchedule{ { ChargingRateUnit: types.ChargingRateUnitAmperes, @@ -167,15 +171,15 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionInvalidEndpoint() { ChargingProfileKind: types.ChargingProfileKindAbsolute, ChargingSchedule: schedule, } - groupIdToken := types.IdTokenTypeISO15693 + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} request := remotecontrol.RequestStartTransactionRequest{ EvseID: evseId, RemoteStartID: remoteStartID, IDToken: idToken, ChargingProfile: &chargingProfile, - GroupIdToken: groupIdToken, + GroupIdToken: &groupIdToken, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":"%v","chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":"%v"}]`, - messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":{"idToken":"%s","type":"%s"},"chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken.IdToken, idToken.Type, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken.IdToken, groupIdToken.Type) testUnsupportedRequestFromChargingStation(suite, request, requestJson, messageId) } diff --git a/ocpp2.0.1_test/reserve_now_test.go b/ocpp2.0.1_test/reserve_now_test.go index fd82e82f..2ab17b81 100644 --- a/ocpp2.0.1_test/reserve_now_test.go +++ b/ocpp2.0.1_test/reserve_now_test.go @@ -16,19 +16,19 @@ import ( func (suite *OcppV2TestSuite) TestReserveNowRequestValidation() { t := suite.T() var requestTable = []GenericTestEntry{ - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdTokenTypeKeyCode}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now())}, false}, - {reservation.ReserveNowRequest{ID: 42, IdToken: types.IdTokenTypeKeyCode}, false}, + {reservation.ReserveNowRequest{ID: 42, IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, false}, {reservation.ReserveNowRequest{}, false}, - {reservation.ReserveNowRequest{ID: -1, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: "invalidConnectorType", EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(-1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: "invalidIdToken", GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: "invalidIdToken"}, false}, + {reservation.ReserveNowRequest{ID: -1, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: "invalidConnectorType", EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(-1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: "invalidIdToken"}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: "invalidIdToken"}}, false}, } ExecuteGenericTestTable(t, requestTable) } @@ -54,12 +54,12 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { expiryDateTime := types.NewDateTime(time.Now()) connectorType := reservation.ConnectorTypeCCS1 evseID := newInt(1) - idToken := types.IdTokenTypeKeyCode - groupIdToken := types.IdTokenTypeISO15693 + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} status := reservation.ReserveNowStatusAccepted statusInfo := types.StatusInfo{ReasonCode: "200"} - requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":"%v","groupIdToken":"%v"}]`, - messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":{"idToken":"%s","type":"%s"},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken.IdToken, idToken.Type, groupIdToken.IdToken, groupIdToken.Type) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","statusInfo":{"reasonCode":"%v"}}]`, messageId, status, statusInfo.ReasonCode) reserveNowResponse := reservation.NewReserveNowResponse(status) @@ -74,8 +74,11 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { assert.Equal(t, expiryDateTime.FormatTimestamp(), request.ExpiryDateTime.FormatTimestamp()) assert.Equal(t, connectorType, request.ConnectorType) assert.Equal(t, *evseID, *request.EvseID) - assert.Equal(t, idToken, request.IdToken) - assert.Equal(t, groupIdToken, request.GroupIdToken) + assert.Equal(t, idToken.IdToken, request.IdToken.IdToken) + assert.Equal(t, idToken.Type, request.IdToken.Type) + require.NotNil(t, request.GroupIdToken) + assert.Equal(t, groupIdToken.IdToken, request.GroupIdToken.IdToken) + assert.Equal(t, groupIdToken.Type, request.GroupIdToken.Type) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -93,7 +96,7 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { }, id, expiryDateTime, idToken, func(request *reservation.ReserveNowRequest) { request.ConnectorType = connectorType request.EvseID = evseID - request.GroupIdToken = groupIdToken + request.GroupIdToken = &groupIdToken }) require.Nil(t, err) result := <-resultChannel @@ -106,17 +109,17 @@ func (suite *OcppV2TestSuite) TestReserveNowInvalidEndpoint() { expiryDateTime := types.NewDateTime(time.Now()) connectorType := reservation.ConnectorTypeCCS1 evseID := newInt(1) - idToken := types.IdTokenTypeKeyCode - groupIdToken := types.IdTokenTypeISO15693 + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} reserveNowRequest := reservation.ReserveNowRequest{ ID: id, ExpiryDateTime: expiryDateTime, ConnectorType: connectorType, EvseID: evseID, IdToken: idToken, - GroupIdToken: groupIdToken, + GroupIdToken: &groupIdToken, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":"%v","groupIdToken":"%v"}]`, - messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":{"idToken":"%s","type":"%s"},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken.IdToken, idToken.Type, groupIdToken.IdToken, groupIdToken.Type) testUnsupportedRequestFromChargingStation(suite, reserveNowRequest, requestJson, messageId) } From 2fa541266d4e7e74df8013a836462def36d02a39 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 24 Dec 2022 15:31:09 +0100 Subject: [PATCH 06/47] v2.0.1: Fix csms example after merging #163 Signed-off-by: Lorenzo --- example/2.0.1/csms/csms_sim.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/2.0.1/csms/csms_sim.go b/example/2.0.1/csms/csms_sim.go index ae2d12e9..c6ddd050 100644 --- a/example/2.0.1/csms/csms_sim.go +++ b/example/2.0.1/csms/csms_sim.go @@ -83,7 +83,7 @@ func exampleRoutine(chargingStationID string, handler *CSMSHandler) { time.Sleep(2 * time.Second) // Reserve a connector reservationID := 42 - clientIDTokenType := types.IdTokenTypeKeyCode + clientIDTokenType := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} clientIdTag := "l33t" connectorID := 1 expiryDate := types.NewDateTime(time.Now().Add(1 * time.Hour)) From 8c6098d59bf6dce86dc832c45dc8b18d9d5d22ad Mon Sep 17 00:00:00 2001 From: Stefan Bindzau Date: Thu, 23 Feb 2023 17:02:25 +0100 Subject: [PATCH 07/47] Use correct name to validate ClearChargingProfileConfirmation --- ocpp1.6/smartcharging/clear_charging_profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp1.6/smartcharging/clear_charging_profile.go b/ocpp1.6/smartcharging/clear_charging_profile.go index e10f0a9b..2b9225c4 100644 --- a/ocpp1.6/smartcharging/clear_charging_profile.go +++ b/ocpp1.6/smartcharging/clear_charging_profile.go @@ -39,7 +39,7 @@ type ClearChargingProfileRequest struct { // This field definition of the ClearChargingProfile confirmation payload, sent by the Charge Point to the Central System in response to a ClearChargingProfileRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ClearChargingProfileConfirmation struct { - Status ClearChargingProfileStatus `json:"status" validate:"required,chargingProfileStatus"` + Status ClearChargingProfileStatus `json:"status" validate:"required,clearChargingProfileStatus"` } // If the Central System wishes to clear some or all of the charging profiles that were previously sent the Charge Point, From 0195c3faf41aefe3789f5dcbf11ae482bb92b7dc Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 4 Feb 2023 17:03:04 +0100 Subject: [PATCH 08/47] Add debug logs for raw json messages Signed-off-by: Lorenzo --- ocppj/client.go | 2 ++ ocppj/dispatcher.go | 3 +++ ocppj/server.go | 2 ++ 3 files changed, 7 insertions(+) diff --git a/ocppj/client.go b/ocppj/client.go index 1a0e9482..47b6e911 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -177,6 +177,7 @@ func (c *Client) SendResponse(requestId string, response ocpp.Response) error { return err } log.Debugf("sent CALL RESULT [%s]", callResult.UniqueId) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) return nil } @@ -211,6 +212,7 @@ func (c *Client) ocppMessageHandler(data []byte) error { log.Error(err) return err } + log.Debugf("received JSON message from server: %s", string(data)) message, err := c.ParseMessage(parsedJson, c.RequestState) if err != nil { ocppErr := err.(*ocpp.Error) diff --git a/ocppj/dispatcher.go b/ocppj/dispatcher.go index 16891156..f7ef9ab4 100644 --- a/ocppj/dispatcher.go +++ b/ocppj/dispatcher.go @@ -232,6 +232,8 @@ func (d *DefaultClientDispatcher) dispatchNextRequest() { ocpp.NewError(InternalError, err.Error(), bundle.Call.UniqueId)) } } + log.Infof("dispatched request %s to server", bundle.Call.UniqueId) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) } func (d *DefaultClientDispatcher) Pause() { @@ -569,6 +571,7 @@ func (d *DefaultServerDispatcher) dispatchNextRequest(clientID string) (clientCt clientCtx = clientTimeoutContext{ctx: ctx, cancel: cancel} } log.Infof("dispatched request %s for %s", callID, clientID) + log.Debugf("sent JSON message to %s: %s", clientID, string(jsonMessage)) return } diff --git a/ocppj/server.go b/ocppj/server.go index 5de6fd02..f15cad91 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -171,6 +171,7 @@ func (s *Server) SendResponse(clientID string, requestId string, response ocpp.R return err } log.Debugf("sent CALL RESULT [%s] for %s", callResult.UniqueId, clientID) + log.Debugf("sent JSON message to %s: %s", clientID, string(jsonMessage)) return nil } @@ -205,6 +206,7 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { log.Error(err) return err } + log.Debugf("received JSON message from %s: %s", wsChannel.ID(), string(data)) // Get pending requests for client pending := s.RequestState.GetClientState(wsChannel.ID()) message, err := s.ParseMessage(parsedJson, pending) From 70f461427f7575d0c0389f4bd33b2e4776227408 Mon Sep 17 00:00:00 2001 From: Ashmeet Singh Date: Tue, 7 Feb 2023 13:24:41 +0100 Subject: [PATCH 09/47] EscapeHTML configuration for json.Marshal --- ocppj/ocppj.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index ba531b96..f9ea7602 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -2,6 +2,7 @@ package ocppj import ( + "bytes" "encoding/json" "fmt" "math/rand" @@ -23,6 +24,8 @@ var validationEnabled bool // The internal verbose logger var log logging.Logger +var EscapeHTML = true + func init() { _ = Validate.RegisterValidation("errorCode", IsErrorCodeValid) log = &logging.VoidLogger{} @@ -40,6 +43,12 @@ func SetLogger(logger logging.Logger) { log = logger } +// Allows an instance of ocppj to configure if the message is Marshaled by escaping special caracters like "<", ">", "&" etc +// For more info https://pkg.go.dev/encoding/json#HTMLEscape +func SetHTMLEscape(flag bool) { + EscapeHTML = flag +} + // Allows to enable/disable automatic validation for OCPP messages // (this includes the field constraints defined for every request/response). // The feature may be useful when working with OCPP implementations that don't fully comply to the specs. @@ -111,7 +120,7 @@ func (call *Call) MarshalJSON() ([]byte, error) { fields[1] = call.UniqueId fields[2] = call.Action fields[3] = call.Payload - return json.Marshal(fields) + return jsonMarshal(fields) } // -------------------- Call Result -------------------- @@ -137,7 +146,7 @@ func (callResult *CallResult) MarshalJSON() ([]byte, error) { fields[0] = int(callResult.MessageTypeId) fields[1] = callResult.UniqueId fields[2] = callResult.Payload - return json.Marshal(fields) + return jsonMarshal(fields) } // -------------------- Call Error -------------------- @@ -214,7 +223,7 @@ func ParseJsonMessage(dataJson string) ([]interface{}, error) { } func ocppMessageToJson(message interface{}) ([]byte, error) { - jsonData, err := json.Marshal(message) + jsonData, err := jsonMarshal(message) if err != nil { return nil, err } @@ -276,6 +285,15 @@ func errorFromValidation(validationErrors validator.ValidationErrors, messageId return ocpp.NewError(GenericError, fmt.Sprintf("%v", validationErrors.Error()), messageId) } +// Marshals data by manipulating EscapeHTML property of encoder +func jsonMarshal(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(EscapeHTML) + err := encoder.Encode(t) + return buffer.Bytes(), err +} + // -------------------- Endpoint -------------------- // An OCPP-J endpoint is one of the two entities taking part in the communication. From fc83394875b1fc6f96fe4ad46e5d6889c3daa0f4 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 12 Mar 2023 11:49:36 +0100 Subject: [PATCH 10/47] Remove newline from encoded json byte array Signed-off-by: Lorenzo --- ocppj/ocppj.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index f9ea7602..d6e1846f 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -291,7 +291,7 @@ func jsonMarshal(t interface{}) ([]byte, error) { encoder := json.NewEncoder(buffer) encoder.SetEscapeHTML(EscapeHTML) err := encoder.Encode(t) - return buffer.Bytes(), err + return bytes.TrimRight(buffer.Bytes(), "\n"), err } // -------------------- Endpoint -------------------- From 0374c843ff526335d4307696a125331c8bf4130b Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 14 Mar 2023 22:51:42 +0100 Subject: [PATCH 11/47] Fix property constraint violation on ConfigurationKey nil values Signed-off-by: Lorenzo --- ocpp1.6/core/get_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp1.6/core/get_configuration.go b/ocpp1.6/core/get_configuration.go index 3e15584a..75078c73 100644 --- a/ocpp1.6/core/get_configuration.go +++ b/ocpp1.6/core/get_configuration.go @@ -12,7 +12,7 @@ const GetConfigurationFeatureName = "GetConfiguration" type ConfigurationKey struct { Key string `json:"key" validate:"required,max=50"` Readonly bool `json:"readonly"` - Value *string `json:"value,omitempty" validate:"max=500"` + Value *string `json:"value,omitempty" validate:"omitempty,max=500"` } // The field definition of the GetConfiguration request payload sent by the Central System to the Charge Point. From 3c24b0d28486a4d37e5a893563e384a2b6c8e3fa Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 17 Mar 2023 15:45:48 +0100 Subject: [PATCH 12/47] Remove incorrect validation for IdToken in RequestStartTransactionRequest Signed-off-by: Lorenzo --- ocpp2.0.1/remotecontrol/request_start_transaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.0.1/remotecontrol/request_start_transaction.go b/ocpp2.0.1/remotecontrol/request_start_transaction.go index 13e3f124..aabc40f2 100644 --- a/ocpp2.0.1/remotecontrol/request_start_transaction.go +++ b/ocpp2.0.1/remotecontrol/request_start_transaction.go @@ -33,7 +33,7 @@ func isValidRequestStartStopStatus(fl validator.FieldLevel) bool { type RequestStartTransactionRequest struct { EvseID *int `json:"evseId,omitempty" validate:"omitempty,gt=0"` RemoteStartID int `json:"remoteStartId" validate:"gte=0"` - IDToken types.IdToken `json:"idToken" validate:"idTokenType"` + IDToken types.IdToken `json:"idToken"` ChargingProfile *types.ChargingProfile `json:"chargingProfile,omitempty"` GroupIdToken *types.IdToken `json:"groupIdToken,omitempty" validate:"omitempty,dive"` } From 5869d11fadb550ceffdddfe119570dd9851b5646 Mon Sep 17 00:00:00 2001 From: dwibudut Date: Mon, 20 Feb 2023 16:49:50 +0700 Subject: [PATCH 13/47] feature to add a custom client validation handler before connected --- ocpp2.0.1/csms.go | 4 ++++ ocpp2.0.1/v2.go | 3 +++ ocppj/server.go | 7 +++++++ ws/websocket.go | 20 ++++++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index ba0d7aee..41dbdcd9 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -735,6 +735,10 @@ func (cs *csms) SetDataHandler(handler data.CSMSHandler) { cs.dataHandler = handler } +func (cs *csms) SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) { + cs.server.SetNewClientValidationHandler(handler) +} + func (cs *csms) SetNewChargingStationHandler(handler ChargingStationConnectionHandler) { cs.server.SetNewClientHandler(func(chargingStation ws.Channel) { handler(chargingStation) diff --git a/ocpp2.0.1/v2.go b/ocpp2.0.1/v2.go index a26bf73e..abb3b6b3 100644 --- a/ocpp2.0.1/v2.go +++ b/ocpp2.0.1/v2.go @@ -34,6 +34,7 @@ type ChargingStationConnection interface { TLSConnectionState() *tls.ConnectionState } +type ChargingStationValidationHandler ws.CheckClientHandler type ChargingStationConnectionHandler func(chargePoint ChargingStationConnection) // -------------------- v2.0 Charging Station -------------------- @@ -366,6 +367,8 @@ type CSMS interface { // Registers a handler for incoming data transfer messages SetDataHandler(handler data.CSMSHandler) // Registers a handler for new incoming Charging station connections. + SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) + // Registers a handler for new incoming Charging station connections. SetNewChargingStationHandler(handler ChargingStationConnectionHandler) // Registers a handler for Charging station disconnections. SetChargingStationDisconnectedHandler(handler ChargingStationConnectionHandler) diff --git a/ocppj/server.go b/ocppj/server.go index f15cad91..613febe2 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -12,6 +12,7 @@ import ( type Server struct { Endpoint server ws.WsServer + checkClientHandler ws.CheckClientHandler newClientHandler ClientHandler disconnectedClientHandler ClientHandler requestHandler RequestHandler @@ -86,6 +87,11 @@ func (s *Server) SetNewClientHandler(handler ClientHandler) { s.newClientHandler = handler } +// Registers a handler for validate incoming client connections. +func (s *Server) SetNewClientValidationHandler(handler ws.CheckClientHandler) { + s.checkClientHandler = handler +} + // Registers a handler for client disconnections. func (s *Server) SetDisconnectedClientHandler(handler ClientHandler) { s.disconnectedClientHandler = handler @@ -99,6 +105,7 @@ func (s *Server) SetDisconnectedClientHandler(handler ClientHandler) { // An error may be returned, if the websocket server couldn't be started. func (s *Server) Start(listenPort int, listenPath string) { // Set internal message handler + s.server.SetCheckClientHandler(s.checkClientHandler) s.server.SetNewClientHandler(s.onClientConnected) s.server.SetDisconnectedClientHandler(s.onClientDisconnected) s.server.SetMessageHandler(s.ocppMessageHandler) diff --git a/ws/websocket.go b/ws/websocket.go index 002dc535..5fb57b57 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -149,6 +149,8 @@ func (e HttpConnectionError) Error() string { // ---------------------- SERVER ---------------------- +type CheckClientHandler func(id string, r *http.Request) bool + // WsServer defines a websocket server, which passively listens for incoming connections on ws or wss protocol. // The offered API are of asynchronous nature, and each incoming connection/message is handled using callbacks. // @@ -234,6 +236,9 @@ type WsServer interface { // By default, if the Origin header is present in the request, and the Origin host is not equal // to the Host request header, the websocket handshake fails. SetCheckOriginHandler(handler func(r *http.Request) bool) + // SetCheckClientHandler sets a handler for validate incoming websocket connections, allowing to perform + // custom client connection checks. + SetCheckClientHandler(handler func(id string, r *http.Request) bool) // Addr gives the address on which the server is listening, useful if, for // example, the port is system-defined (set to 0). Addr() *net.TCPAddr @@ -246,6 +251,7 @@ type Server struct { connections map[string]*WebSocket httpServer *http.Server messageHandler func(ws Channel, data []byte) error + checkClientHandler func(id string, r *http.Request) bool newClientHandler func(ws Channel) disconnectedHandler func(ws Channel) basicAuthHandler func(username string, password string) bool @@ -297,6 +303,10 @@ func (server *Server) SetMessageHandler(handler func(ws Channel, data []byte) er server.messageHandler = handler } +func (server *Server) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { + server.checkClientHandler = handler +} + func (server *Server) SetNewClientHandler(handler func(ws Channel)) { server.newClientHandler = handler } @@ -467,6 +477,16 @@ out: return } } + + if server.checkClientHandler != nil { + ok := server.checkClientHandler(id, r) + if !ok { + server.error(fmt.Errorf("client validation: invalid client")) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + } + // Upgrade websocket conn, err := server.upgrader.Upgrade(w, r, responseHeader) if err != nil { From 404fb1f9d1110ddb437b646b20d431c58b07b9ad Mon Sep 17 00:00:00 2001 From: dwibudut Date: Tue, 21 Mar 2023 14:32:21 +0700 Subject: [PATCH 14/47] add/implement SetCheckClientHandler method on MockWebsocketServer --- ocpp1.6_test/ocpp16_test.go | 5 ++++ ocpp2.0.1_test/ocpp2_test.go | 5 ++++ ocppj/ocppj_test.go | 5 ++++ ws/websocket_test.go | 44 ++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index d3358653..663721e7 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -53,6 +53,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) } @@ -88,6 +89,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { mock.Mock diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index e60a4efd..dcb54525 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -68,6 +68,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) } @@ -103,6 +104,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index bb4ef42c..4810153c 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -51,6 +51,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) errC chan error } @@ -100,6 +101,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { diff --git a/ws/websocket_test.go b/ws/websocket_test.go index a80371c6..98fd3a67 100644 --- a/ws/websocket_test.go +++ b/ws/websocket_test.go @@ -665,6 +665,50 @@ func TestCustomOriginHeaderHandler(t *testing.T) { wsServer.Stop() } +func TestCustomCheckClientHandler(t *testing.T) { + invalidTestPath := "/ws/invalid-testws" + id := path.Base(testPath) + connected := make(chan bool) + wsServer := newWebsocketServer(t, func(data []byte) ([]byte, error) { + assert.Fail(t, "no message should be received from client!") + return nil, nil + }) + wsServer.SetNewClientHandler(func(ws Channel) { + connected <- true + }) + wsServer.SetCheckClientHandler(func(clientId string, r *http.Request) bool { + return id == clientId + }) + go wsServer.Start(serverPort, serverPath) + time.Sleep(500 * time.Millisecond) + + // Test message + wsClient := newWebsocketClient(t, func(data []byte) ([]byte, error) { + assert.Fail(t, "no message should be received from server!") + return nil, nil + }) + + host := fmt.Sprintf("localhost:%v", serverPort) + // Set invalid client (not /ws/testws) + u := url.URL{Scheme: "ws", Host: host, Path: invalidTestPath} + // Attempt to connect and expect invalid client id error + err := wsClient.Start(u.String()) + require.Error(t, err) + httpErr, ok := err.(HttpConnectionError) + require.True(t, ok) + assert.Equal(t, http.StatusUnauthorized, httpErr.HttpCode) + assert.Equal(t, "websocket: bad handshake", httpErr.Message) + + // Re-attempt with correct client id + u = url.URL{Scheme: "ws", Host: host, Path: testPath} + err = wsClient.Start(u.String()) + require.NoError(t, err) + result := <-connected + assert.True(t, result) + // Cleanup + wsServer.Stop() +} + func TestValidClientTLSCertificate(t *testing.T) { // Create self-signed TLS certificate clientCertFilename := "/tmp/client.pem" From 7652fcf0b6dda9a83d847c506db467b8814cfa4f Mon Sep 17 00:00:00 2001 From: dwibudut Date: Tue, 21 Mar 2023 14:52:44 +0700 Subject: [PATCH 15/47] fix/replace with direct func type arg on implement method SetCheckClientHandler on MockWebsocketServer --- ocpp1.6_test/ocpp16_test.go | 3 ++- ocpp2.0.1_test/ocpp2_test.go | 3 ++- ocppj/ocppj_test.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index 663721e7..c966bffb 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" @@ -89,7 +90,7 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } -func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { websocketServer.CheckClientHandler = handler } diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index dcb54525..b2515dee 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" @@ -104,7 +105,7 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } -func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { websocketServer.CheckClientHandler = handler } diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index 4810153c..d3beba67 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" @@ -101,7 +102,7 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } -func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler ws.CheckClientHandler) { +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { websocketServer.CheckClientHandler = handler } From fc3ab08fb238f45d35f9928cacfbe3bd92d2e1db Mon Sep 17 00:00:00 2001 From: dwibudut Date: Thu, 23 Mar 2023 13:50:55 +0700 Subject: [PATCH 16/47] implement method `SetNewChargingStationValidationHandler` on ocpp1.6 (central system) --- ocpp1.6/central_system.go | 4 ++++ ocpp1.6/v16.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index 2b8bec43..c8765736 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -360,6 +360,10 @@ func (cs *centralSystem) SetSmartChargingHandler(handler smartcharging.CentralSy cs.smartChargingHandler = handler } +func (cs *centralSystem) SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) { + cs.server.SetNewClientValidationHandler(handler) +} + func (cs *centralSystem) SetNewChargePointHandler(handler ChargePointConnectionHandler) { cs.server.SetNewClientHandler(func(chargePoint ws.Channel) { handler(chargePoint) diff --git a/ocpp1.6/v16.go b/ocpp1.6/v16.go index 85a5fd56..be035d71 100644 --- a/ocpp1.6/v16.go +++ b/ocpp1.6/v16.go @@ -236,6 +236,8 @@ type CentralSystem interface { SetRemoteTriggerHandler(handler remotetrigger.CentralSystemHandler) // Registers a handler for incoming smart charging profile messages. SetSmartChargingHandler(handler smartcharging.CentralSystemHandler) + // Registers a handler for new incoming Charging station connections. + SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) // Registers a handler for new incoming charge point connections. SetNewChargePointHandler(handler ChargePointConnectionHandler) // Registers a handler for charge point disconnections. From 7d6d270bd79ce6db7210098eb84434c6dd03f752 Mon Sep 17 00:00:00 2001 From: shiv3 Date: Mon, 27 Feb 2023 14:39:44 +0900 Subject: [PATCH 17/47] Add handler --- ws/websocket.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ws/websocket.go b/ws/websocket.go index 5fb57b57..4bec7f98 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -9,6 +9,7 @@ import ( "crypto/tls" "encoding/base64" "fmt" + "github.com/gorilla/mux" "io" "net" "net/http" @@ -19,7 +20,6 @@ import ( "github.com/lorenzodonini/ocpp-go/logging" - "github.com/gorilla/mux" "github.com/gorilla/websocket" ) @@ -262,6 +262,7 @@ type Server struct { errC chan error connMutex sync.RWMutex addr *net.TCPAddr + httpHandler *mux.Router } // Creates a new simple websocket server (the websockets are not secured). @@ -288,6 +289,7 @@ func NewServer() *Server { // If no tlsConfig parameter is passed, the server will by default // not perform any client certificate verification. func NewTLSServer(certificatePath string, certificateKey string, tlsConfig *tls.Config) *Server { + router := mux.NewRouter() return &Server{ tlsCertificatePath: certificatePath, tlsCertificateKey: certificateKey, @@ -296,6 +298,7 @@ func NewTLSServer(certificatePath string, certificateKey string, tlsConfig *tls. }, timeoutConfig: NewServerTimeoutConfig(), upgrader: websocket.Upgrader{Subprotocols: []string{}}, + httpHandler: router, } } @@ -355,11 +358,12 @@ func (server *Server) Addr() *net.TCPAddr { return server.addr } +func (server *Server) AddHttpHandler(listenPath string, handler func(w http.ResponseWriter, r *http.Request)) { + server.httpHandler.HandleFunc(listenPath, handler) +} + func (server *Server) Start(port int, listenPath string) { - router := mux.NewRouter() - router.HandleFunc(listenPath, func(w http.ResponseWriter, r *http.Request) { - server.wsHandler(w, r) - }) + server.connections = make(map[string]*WebSocket) if server.httpServer == nil { server.httpServer = &http.Server{} @@ -367,7 +371,11 @@ func (server *Server) Start(port int, listenPath string) { addr := fmt.Sprintf(":%v", port) server.httpServer.Addr = addr - server.httpServer.Handler = router + + server.AddHttpHandler(listenPath, func(w http.ResponseWriter, r *http.Request) { + server.wsHandler(w, r) + }) + server.httpServer.Handler = server.httpHandler ln, err := net.Listen("tcp", addr) if err != nil { From 22b4f86c1d1c3fb381e826de9840e2955a26f55b Mon Sep 17 00:00:00 2001 From: shiv3 Date: Wed, 22 Mar 2023 11:58:50 +0900 Subject: [PATCH 18/47] Add router to non tls server --- ws/websocket.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ws/websocket.go b/ws/websocket.go index 4bec7f98..2b147537 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -267,10 +267,12 @@ type Server struct { // Creates a new simple websocket server (the websockets are not secured). func NewServer() *Server { + router := mux.NewRouter() return &Server{ httpServer: &http.Server{}, timeoutConfig: NewServerTimeoutConfig(), upgrader: websocket.Upgrader{Subprotocols: []string{}}, + httpHandler: router, } } From b979d02c9a4ede24b3cf2fdaa3621e06e888becc Mon Sep 17 00:00:00 2001 From: shiv3 Date: Wed, 22 Mar 2023 12:08:40 +0900 Subject: [PATCH 19/47] Fix goimport --- ws/websocket.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ws/websocket.go b/ws/websocket.go index 2b147537..d9862b85 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -9,7 +9,6 @@ import ( "crypto/tls" "encoding/base64" "fmt" - "github.com/gorilla/mux" "io" "net" "net/http" @@ -18,9 +17,9 @@ import ( "sync" "time" - "github.com/lorenzodonini/ocpp-go/logging" - + "github.com/gorilla/mux" "github.com/gorilla/websocket" + "github.com/lorenzodonini/ocpp-go/logging" ) const ( From 3facaad2fd9dcb39d584c28cf3e0ee80332dade1 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 7 Apr 2023 16:28:57 +0200 Subject: [PATCH 20/47] Fix CallError marshaling of empty fields Signed-off-by: Lorenzo --- ocppj/ocppj.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index d6e1846f..ae3a4e37 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -157,7 +157,7 @@ type CallError struct { MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=4"` UniqueId string `json:"uniqueId" validate:"required,max=36"` ErrorCode ocpp.ErrorCode `json:"errorCode" validate:"errorCode"` - ErrorDescription string `json:"errorDescription" validate:"required"` + ErrorDescription string `json:"errorDescription" validate:"omitempty"` ErrorDetails interface{} `json:"errorDetails" validate:"omitempty"` } @@ -175,7 +175,11 @@ func (callError *CallError) MarshalJSON() ([]byte, error) { fields[1] = callError.UniqueId fields[2] = callError.ErrorCode fields[3] = callError.ErrorDescription - fields[4] = callError.ErrorDetails + if callError.ErrorDetails == nil { + fields[4] = struct{}{} + } else { + fields[4] = callError.ErrorDetails + } return ocppMessageToJson(fields) } From c3dee61bb74c27bc20a6ce30d5a12a1c6669e5ca Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 7 Apr 2023 16:30:27 +0200 Subject: [PATCH 21/47] Add send response error handling function Signed-off-by: Lorenzo --- ocppj/charge_point_test.go | 59 +++++++++++++++++++++++++++++++++++++- ocppj/client.go | 35 ++++++++++++++++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index e704dc9c..4926fefb 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -2,11 +2,15 @@ package ocppj_test import ( "encoding/json" + "errors" "fmt" + "reflect" "strconv" "sync" "time" + "gopkg.in/go-playground/validator.v9" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -84,7 +88,7 @@ func (suite *OcppJTestSuite) TestChargePointSendInvalidRequest() { _ = suite.chargePoint.Start("someUrl") mockRequest := newMockRequest("") err := suite.chargePoint.SendRequest(mockRequest) - assert.NotNil(suite.T(), err) + require.NotNil(suite.T(), err) } func (suite *OcppJTestSuite) TestChargePointSendRequestNoValidation() { @@ -226,6 +230,59 @@ func (suite *OcppJTestSuite) TestChargePointSendErrorFailed() { assert.Equal(t, "networkError", err.Error()) } +func (suite *OcppJTestSuite) TestChargePointHandleFailedResponse() { + t := suite.T() + msgC := make(chan []byte, 1) + mockUniqueID := "1234" + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + suite.mockClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data, ok := args.Get(0).([]byte) + require.True(t, ok) + msgC <- data + }) + // 1. occurrence validation error + mockField := "MyStruct.Field1" + mockError := MockValidationError{ + tag: "required", + namespace: mockField, + typ: reflect.TypeOf(""), + } + var err error + err = validator.ValidationErrors{mockError} + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, MockFeatureName) + rawResponse := <-msgC + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, MockFeatureName) + assert.Equal(t, expectedErr, string(rawResponse)) + // 2. property constraint validation error + val := "len4" + mockError = MockValidationError{ + tag: "min", + namespace: mockField, + param: "5", + value: val, + typ: reflect.TypeOf(val), + } + err = validator.ValidationErrors{mockError} + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, MockFeatureName) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, mockError.param, len(val), MockFeatureName) + assert.Equal(t, expectedErr, string(rawResponse)) + // 3. ocpp error validation failed + err = validator.ValidationErrors{} + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, err.Error()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. profile error + err = errors.New("this is some uncharted error") + feature := "SomeFeature" + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, feature) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Unsupported feature %s",{}]`, mockUniqueID, ocppj.NotSupported, feature) + assert.Equal(t, expectedErr, string(rawResponse)) +} + // ----------------- Call Handlers tests ----------------- func (suite *OcppJTestSuite) TestChargePointCallHandler() { diff --git a/ocppj/client.go b/ocppj/client.go index 47b6e911..89152bda 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -3,6 +3,8 @@ package ocppj import ( "fmt" + "gopkg.in/go-playground/validator.v9" + "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ws" ) @@ -173,10 +175,10 @@ func (c *Client) SendResponse(requestId string, response ocpp.Response) error { return err } if err = c.client.Write(jsonMessage); err != nil { - log.Errorf("error sending response [%s]: %v", callResult.UniqueId, err) + log.Errorf("error sending response [%s]: %v", callResult.GetUniqueId(), err) return err } - log.Debugf("sent CALL RESULT [%s]", callResult.UniqueId) + log.Debugf("sent CALL RESULT [%s]", callResult.GetUniqueId()) log.Debugf("sent JSON message to server: %s", string(jsonMessage)) return nil } @@ -203,6 +205,7 @@ func (c *Client) SendError(requestId string, errorCode ocpp.ErrorCode, descripti return err } log.Debugf("sent CALL ERROR [%s]", callError.UniqueId) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) return nil } @@ -250,6 +253,34 @@ func (c *Client) ocppMessageHandler(data []byte) error { return nil } +// HandleFailedResponseError allows to handle failures while sending responses (either CALL_RESULT or CALL_ERROR). +// It internally analyzes and creates an ocpp.Error based on the given error. +// It will the attempt to send it to the server. +// +// The function helps to prevent starvation on the other endpoint, which is caused by a response never reaching it. +// The method will, however, only attempt to send a default error once. +// If this operation fails, the other endpoint may still starve. +func (c *Client) HandleFailedResponseError(requestID string, err error, featureName string) { + log.Debugf("handling error for failed response [%s]", requestID) + // There's two possible errors: invalid profile or invalid payload + validationErr, ok := err.(validator.ValidationErrors) + var responseErr *ocpp.Error + if ok { + if featureName == "" { + // Validation for OcppError failed, generating a default error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) + } else { + // Validation failed for a feature, sending an error instead + responseErr = errorFromValidation(validationErr, requestID, featureName) + } + } else { + // unsupported profile error + responseErr = ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", featureName), requestID) + } + // Send an OCPP error to the target, since no regular response could be sent + _ = c.SendError(requestID, responseErr.Code, responseErr.Description, nil) +} + func (c *Client) onDisconnected(err error) { log.Error("disconnected from server", err) c.dispatcher.Pause() From 3fbe3387ea14867ab49daf5154e4ea06ad309189 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 7 Apr 2023 16:36:02 +0200 Subject: [PATCH 22/47] Add error handling function for server SendResponse Signed-off-by: Lorenzo --- ocppj/central_system_test.go | 62 +++++++++++++++++++++++++++++++++++- ocppj/ocppj_test.go | 23 +++++++++++++ ocppj/server.go | 34 ++++++++++++++++++-- 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/ocppj/central_system_test.go b/ocppj/central_system_test.go index 482d3c38..cb21197c 100644 --- a/ocppj/central_system_test.go +++ b/ocppj/central_system_test.go @@ -2,11 +2,15 @@ package ocppj_test import ( "encoding/json" + "errors" "fmt" + "reflect" "strconv" "sync" "time" + "gopkg.in/go-playground/validator.v9" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -246,7 +250,63 @@ func (suite *OcppJTestSuite) TestCentralSystemSendErrorFailed() { assert.NotNil(t, err) } -// Handlers +func (suite *OcppJTestSuite) TestCentralSystemHandleFailedResponse() { + t := suite.T() + msgC := make(chan []byte, 1) + mockChargePointID := "0101" + mockUniqueID := "1234" + suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data, ok := args.Get(1).([]byte) + require.True(t, ok) + msgC <- data + }) + suite.centralSystem.Start(8887, "/{ws}") + suite.serverDispatcher.CreateClient(mockChargePointID) + // 1. occurrence validation error + mockField := "MyStruct.Field1" + mockError := MockValidationError{ + tag: "required", + namespace: mockField, + typ: reflect.TypeOf(""), + } + var err error + err = validator.ValidationErrors{mockError} + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, MockFeatureName) + rawResponse := <-msgC + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, MockFeatureName) + assert.Equal(t, expectedErr, string(rawResponse)) + // 2. property constraint validation error + val := "len4" + mockError = MockValidationError{ + tag: "min", + namespace: mockField, + param: "5", + value: val, + typ: reflect.TypeOf(val), + } + err = validator.ValidationErrors{mockError} + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, MockFeatureName) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, mockError.param, len(val), MockFeatureName) + assert.Equal(t, expectedErr, string(rawResponse)) + // 3. ocpp error validation failed + err = validator.ValidationErrors{} + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, err.Error()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. profile error + err = errors.New("this is some uncharted error") + feature := "SomeFeature" + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, feature) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Unsupported feature %s",{}]`, mockUniqueID, ocppj.NotSupported, feature) + assert.Equal(t, expectedErr, string(rawResponse)) +} + +// ----------------- Handlers tests ----------------- func (suite *OcppJTestSuite) TestCentralSystemNewClientHandler() { t := suite.T() diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index d3beba67..d53ad0e0 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -8,6 +8,8 @@ import ( "reflect" "testing" + ut "github.com/go-playground/universal-translator" + "github.com/lorenzodonini/ocpp-go/logging" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -781,6 +783,27 @@ func (suite *OcppJTestSuite) TestLogger() { }) } +type MockValidationError struct { + tag string + namespace string + param string + value string + typ reflect.Type +} + +func (m MockValidationError) ActualTag() string { return m.tag } +func (m MockValidationError) Tag() string { return m.tag } +func (m MockValidationError) Namespace() string { return m.namespace } +func (m MockValidationError) StructNamespace() string { return m.namespace } +func (m MockValidationError) Field() string { return m.namespace } +func (m MockValidationError) StructField() string { return m.namespace } +func (m MockValidationError) Value() interface{} { return m.value } +func (m MockValidationError) Param() string { return m.param } +func (m MockValidationError) Kind() reflect.Kind { return m.typ.Kind() } +func (m MockValidationError) Type() reflect.Type { return m.typ } +func (m MockValidationError) Translate(ut ut.Translator) string { return "" } +func (m MockValidationError) Error() string { return fmt.Sprintf("some error for value %s", m.value) } + func TestMockOcppJ(t *testing.T) { suite.Run(t, new(ClientQueueTestSuite)) suite.Run(t, new(ServerQueueMapTestSuite)) diff --git a/ocppj/server.go b/ocppj/server.go index 613febe2..5668ca5f 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -3,6 +3,8 @@ package ocppj import ( "fmt" + "gopkg.in/go-playground/validator.v9" + "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ws" ) @@ -174,10 +176,10 @@ func (s *Server) SendResponse(clientID string, requestId string, response ocpp.R return err } if err = s.server.Write(clientID, jsonMessage); err != nil { - log.Errorf("error sending response [%s] to %s: %v", callResult.UniqueId, clientID, err) + log.Errorf("error sending response [%s] to %s: %v", callResult.GetUniqueId(), clientID, err) return err } - log.Debugf("sent CALL RESULT [%s] for %s", callResult.UniqueId, clientID) + log.Debugf("sent CALL RESULT [%s] for %s", callResult.GetUniqueId(), clientID) log.Debugf("sent JSON message to %s: %s", clientID, string(jsonMessage)) return nil } @@ -253,6 +255,34 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { return nil } +// HandleFailedResponseError allows to handle failures while sending responses (either CALL_RESULT or CALL_ERROR). +// It internally analyzes and creates an ocpp.Error based on the given error. +// It will the attempt to send it to the client. +// +// The function helps to prevent starvation on the other endpoint, which is caused by a response never reaching it. +// The method will, however, only attempt to send a default error once. +// If this operation fails, the other endpoint may still starve. +func (s *Server) HandleFailedResponseError(clientID string, requestID string, err error, featureName string) { + log.Debugf("handling error for failed response [%s]", requestID) + // There's two possible errors: invalid profile or invalid payload + validationErr, ok := err.(validator.ValidationErrors) + var responseErr *ocpp.Error + if ok { + if featureName == "" { + // Validation for OcppError failed, generating a default error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) + } else { + // Validation failed for a feature, sending an error instead + responseErr = errorFromValidation(validationErr, requestID, featureName) + } + } else { + // unsupported profile error + responseErr = ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", featureName), requestID) + } + // Send an OCPP error to the target, since no regular response could be sent + _ = s.SendError(clientID, requestID, responseErr.Code, responseErr.Description, nil) +} + func (s *Server) onClientConnected(ws ws.Channel) { // Create state for connected client s.dispatcher.CreateClient(ws.ID()) From 0ae68c72db4ac3f883da7e91f2e94380f90de338 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 11:19:17 +0200 Subject: [PATCH 23/47] Improve error handling and rewrite tests Signed-off-by: Lorenzo --- ocppj/central_system_test.go | 78 +++++++++++++++++++++--------------- ocppj/charge_point_test.go | 78 +++++++++++++++++++++--------------- ocppj/client.go | 33 ++++++++------- ocppj/dispatcher_test.go | 4 +- ocppj/ocppj.go | 2 +- ocppj/ocppj_test.go | 20 ++++++--- ocppj/server.go | 33 ++++++++------- 7 files changed, 141 insertions(+), 107 deletions(-) diff --git a/ocppj/central_system_test.go b/ocppj/central_system_test.go index cb21197c..89d6bcc3 100644 --- a/ocppj/central_system_test.go +++ b/ocppj/central_system_test.go @@ -2,15 +2,11 @@ package ocppj_test import ( "encoding/json" - "errors" "fmt" - "reflect" "strconv" "sync" "time" - "gopkg.in/go-playground/validator.v9" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -263,46 +259,64 @@ func (suite *OcppJTestSuite) TestCentralSystemHandleFailedResponse() { }) suite.centralSystem.Start(8887, "/{ws}") suite.serverDispatcher.CreateClient(mockChargePointID) - // 1. occurrence validation error - mockField := "MyStruct.Field1" - mockError := MockValidationError{ - tag: "required", - namespace: mockField, - typ: reflect.TypeOf(""), - } + var callResult *ocppj.CallResult + var callError *ocppj.CallError var err error - err = validator.ValidationErrors{mockError} - suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, MockFeatureName) + // 1. occurrence validation error + mockField := "CallResult.Payload.MockValue" + mockResponse := newMockConfirmation("") + callResult, err = suite.centralSystem.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockResponse.GetFeatureName()) rawResponse := <-msgC - expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, MockFeatureName) + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, mockResponse.GetFeatureName()) assert.Equal(t, expectedErr, string(rawResponse)) // 2. property constraint validation error val := "len4" - mockError = MockValidationError{ - tag: "min", - namespace: mockField, - param: "5", - value: val, - typ: reflect.TypeOf(val), - } - err = validator.ValidationErrors{mockError} - suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, MockFeatureName) + minParamLength := "5" + mockResponse = newMockConfirmation(val) + callResult, err = suite.centralSystem.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockResponse.GetFeatureName()) rawResponse = <-msgC expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, - mockUniqueID, ocppj.PropertyConstraintViolation, mockField, mockError.param, len(val), MockFeatureName) + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, minParamLength, len(val), mockResponse.GetFeatureName()) assert.Equal(t, expectedErr, string(rawResponse)) - // 3. ocpp error validation failed - err = validator.ValidationErrors{} + // 3. profile not supported + mockUnsupportedResponse := &MockUnsupportedResponse{MockValue: "someValue"} + callResult, err = suite.centralSystem.CreateCallResult(mockUnsupportedResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockUnsupportedResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","couldn't create Call Result for unsupported action %s",{}]`, + mockUniqueID, ocppj.NotSupported, mockUnsupportedResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. ocpp error validation failed + invalidErrorCode := "InvalidErrorCode" + callError, err = suite.centralSystem.CreateCallError(mockUniqueID, ocpp.ErrorCode(invalidErrorCode), "", nil) + require.Error(t, err) + require.Nil(t, callError) suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") rawResponse = <-msgC - expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, err.Error()) + expectedErr = fmt.Sprintf(`[4,"%v","%v","Key: 'CallError.ErrorCode' Error:Field validation for 'ErrorCode' failed on the 'errorCode' tag",{}]`, + mockUniqueID, ocppj.GenericError) assert.Equal(t, expectedErr, string(rawResponse)) - // 4. profile error - err = errors.New("this is some uncharted error") - feature := "SomeFeature" - suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, feature) + // 5. marshaling err + err = suite.centralSystem.SendError(mockChargePointID, mockUniqueID, ocppj.SecurityError, "", make(chan struct{})) + require.Error(t, err) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","json: unsupported type: chan struct {}",{}]`, mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 6. network error + rawErr := fmt.Sprintf("couldn't write to websocket. No socket with id %s is open", mockChargePointID) + err = ocpp.NewError(ocppj.GenericError, rawErr, mockUniqueID) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") rawResponse = <-msgC - expectedErr = fmt.Sprintf(`[4,"%v","%v","Unsupported feature %s",{}]`, mockUniqueID, ocppj.NotSupported, feature) + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, rawErr) assert.Equal(t, expectedErr, string(rawResponse)) } diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 4926fefb..969a3c42 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -2,15 +2,11 @@ package ocppj_test import ( "encoding/json" - "errors" "fmt" - "reflect" "strconv" "sync" "time" - "gopkg.in/go-playground/validator.v9" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -240,46 +236,64 @@ func (suite *OcppJTestSuite) TestChargePointHandleFailedResponse() { require.True(t, ok) msgC <- data }) - // 1. occurrence validation error - mockField := "MyStruct.Field1" - mockError := MockValidationError{ - tag: "required", - namespace: mockField, - typ: reflect.TypeOf(""), - } + var callResult *ocppj.CallResult + var callError *ocppj.CallError var err error - err = validator.ValidationErrors{mockError} - suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, MockFeatureName) + // 1. occurrence validation error + mockField := "CallResult.Payload.MockValue" + mockResponse := newMockConfirmation("") + callResult, err = suite.chargePoint.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockResponse.GetFeatureName()) rawResponse := <-msgC - expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, MockFeatureName) + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, mockResponse.GetFeatureName()) assert.Equal(t, expectedErr, string(rawResponse)) // 2. property constraint validation error val := "len4" - mockError = MockValidationError{ - tag: "min", - namespace: mockField, - param: "5", - value: val, - typ: reflect.TypeOf(val), - } - err = validator.ValidationErrors{mockError} - suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, MockFeatureName) + minParamLength := "5" + mockResponse = newMockConfirmation(val) + callResult, err = suite.chargePoint.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockResponse.GetFeatureName()) rawResponse = <-msgC expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, - mockUniqueID, ocppj.PropertyConstraintViolation, mockField, mockError.param, len(val), MockFeatureName) + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, minParamLength, len(val), mockResponse.GetFeatureName()) assert.Equal(t, expectedErr, string(rawResponse)) - // 3. ocpp error validation failed - err = validator.ValidationErrors{} + // 3. profile not supported + mockUnsupportedResponse := &MockUnsupportedResponse{MockValue: "someValue"} + callResult, err = suite.chargePoint.CreateCallResult(mockUnsupportedResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockUnsupportedResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","couldn't create Call Result for unsupported action %s",{}]`, + mockUniqueID, ocppj.NotSupported, mockUnsupportedResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. ocpp error validation failed + invalidErrorCode := "InvalidErrorCode" + callError, err = suite.chargePoint.CreateCallError(mockUniqueID, ocpp.ErrorCode(invalidErrorCode), "", nil) + require.Error(t, err) + require.Nil(t, callError) suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") rawResponse = <-msgC - expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, err.Error()) + expectedErr = fmt.Sprintf(`[4,"%v","%v","Key: 'CallError.ErrorCode' Error:Field validation for 'ErrorCode' failed on the 'errorCode' tag",{}]`, + mockUniqueID, ocppj.GenericError) assert.Equal(t, expectedErr, string(rawResponse)) - // 4. profile error - err = errors.New("this is some uncharted error") - feature := "SomeFeature" - suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, feature) + // 5. marshaling err + err = suite.chargePoint.SendError(mockUniqueID, ocppj.SecurityError, "", make(chan struct{})) + require.Error(t, err) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","json: unsupported type: chan struct {}",{}]`, mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 6. network error + rawErr := "client is currently not connected, cannot send data" + err = ocpp.NewError(ocppj.GenericError, rawErr, mockUniqueID) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") rawResponse = <-msgC - expectedErr = fmt.Sprintf(`[4,"%v","%v","Unsupported feature %s",{}]`, mockUniqueID, ocppj.NotSupported, feature) + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, rawErr) assert.Equal(t, expectedErr, string(rawResponse)) } diff --git a/ocppj/client.go b/ocppj/client.go index 89152bda..ea5c200b 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -172,11 +172,11 @@ func (c *Client) SendResponse(requestId string, response ocpp.Response) error { } jsonMessage, err := callResult.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = c.client.Write(jsonMessage); err != nil { log.Errorf("error sending response [%s]: %v", callResult.GetUniqueId(), err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL RESULT [%s]", callResult.GetUniqueId()) log.Debugf("sent JSON message to server: %s", string(jsonMessage)) @@ -198,11 +198,11 @@ func (c *Client) SendError(requestId string, errorCode ocpp.ErrorCode, descripti } jsonMessage, err := callError.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = c.client.Write(jsonMessage); err != nil { log.Errorf("error sending response error [%s]: %v", callError.UniqueId, err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL ERROR [%s]", callError.UniqueId) log.Debugf("sent JSON message to server: %s", string(jsonMessage)) @@ -262,20 +262,19 @@ func (c *Client) ocppMessageHandler(data []byte) error { // If this operation fails, the other endpoint may still starve. func (c *Client) HandleFailedResponseError(requestID string, err error, featureName string) { log.Debugf("handling error for failed response [%s]", requestID) - // There's two possible errors: invalid profile or invalid payload - validationErr, ok := err.(validator.ValidationErrors) var responseErr *ocpp.Error - if ok { - if featureName == "" { - // Validation for OcppError failed, generating a default error - responseErr = ocpp.NewError(GenericError, err.Error(), requestID) - } else { - // Validation failed for a feature, sending an error instead - responseErr = errorFromValidation(validationErr, requestID, featureName) - } - } else { - // unsupported profile error - responseErr = ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", featureName), requestID) + // There's several possible errors: invalid profile, invalid payload or send error + switch err.(type) { + case validator.ValidationErrors: + // Validation error + validationErr := err.(validator.ValidationErrors) + responseErr = errorFromValidation(validationErr, requestID, featureName) + case *ocpp.Error: + // Internal OCPP error + responseErr = err.(*ocpp.Error) + case error: + // Unknown error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) } // Send an OCPP error to the target, since no regular response could be sent _ = c.SendError(requestID, responseErr.Code, responseErr.Description, nil) diff --git a/ocppj/dispatcher_test.go b/ocppj/dispatcher_test.go index 5cceac55..a33d5162 100644 --- a/ocppj/dispatcher_test.go +++ b/ocppj/dispatcher_test.go @@ -26,7 +26,7 @@ type ServerDispatcherTestSuite struct { func (s *ServerDispatcherTestSuite) SetupTest() { s.endpoint = ocppj.Server{} - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) s.endpoint.AddProfile(mockProfile) s.queueMap = ocppj.NewFIFOQueueMap(10) s.dispatcher = ocppj.NewDefaultServerDispatcher(s.queueMap) @@ -242,7 +242,7 @@ type ClientDispatcherTestSuite struct { func (c *ClientDispatcherTestSuite) SetupTest() { c.endpoint = ocppj.Client{Id: "client1"} - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) c.endpoint.AddProfile(mockProfile) c.queue = ocppj.NewFIFOClientQueue(10) c.dispatcher = ocppj.NewDefaultClientDispatcher(c.queue) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index ae3a4e37..86e300e1 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -500,7 +500,7 @@ func (endpoint *Endpoint) CreateCallResult(confirmation ocpp.Response, uniqueId action := confirmation.GetFeatureName() profile, _ := endpoint.GetProfileForFeature(action) if profile == nil { - return nil, fmt.Errorf("Couldn't create Call Result for unsupported action %v", action) + return nil, ocpp.NewError(NotSupported, fmt.Sprintf("couldn't create Call Result for unsupported action %v", action), uniqueId) } callResult := CallResult{ MessageTypeId: CALL_RESULT, diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index d53ad0e0..d0d13d83 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -195,23 +195,23 @@ type MockFeature struct { mock.Mock } -func (f MockFeature) GetFeatureName() string { +func (f *MockFeature) GetFeatureName() string { return MockFeatureName } -func (f MockFeature) GetRequestType() reflect.Type { +func (f *MockFeature) GetRequestType() reflect.Type { return reflect.TypeOf(MockRequest{}) } -func (f MockFeature) GetResponseType() reflect.Type { +func (f *MockFeature) GetResponseType() reflect.Type { return reflect.TypeOf(MockConfirmation{}) } -func (r MockRequest) GetFeatureName() string { +func (r *MockRequest) GetFeatureName() string { return MockFeatureName } -func (c MockConfirmation) GetFeatureName() string { +func (c *MockConfirmation) GetFeatureName() string { return MockFeatureName } @@ -223,6 +223,14 @@ func newMockConfirmation(value string) *MockConfirmation { return &MockConfirmation{MockValue: value} } +type MockUnsupportedResponse struct { + MockValue string `json:"mockValue" validate:"required,min=5"` +} + +func (m *MockUnsupportedResponse) GetFeatureName() string { + return "SomeRandomFeature" +} + // ---------------------- COMMON UTILITY METHODS ---------------------- func NewWebsocketServer(t *testing.T, onMessage func(data []byte) ([]byte, error)) *ws.Server { @@ -356,7 +364,7 @@ type OcppJTestSuite struct { } func (suite *OcppJTestSuite) SetupTest() { - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) mockClient := MockWebsocketClient{} mockServer := MockWebsocketServer{} suite.mockClient = &mockClient diff --git a/ocppj/server.go b/ocppj/server.go index 5668ca5f..e4feaafe 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -173,11 +173,11 @@ func (s *Server) SendResponse(clientID string, requestId string, response ocpp.R } jsonMessage, err := callResult.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = s.server.Write(clientID, jsonMessage); err != nil { log.Errorf("error sending response [%s] to %s: %v", callResult.GetUniqueId(), clientID, err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL RESULT [%s] for %s", callResult.GetUniqueId(), clientID) log.Debugf("sent JSON message to %s: %s", clientID, string(jsonMessage)) @@ -199,11 +199,11 @@ func (s *Server) SendError(clientID string, requestId string, errorCode ocpp.Err } jsonMessage, err := callError.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = s.server.Write(clientID, jsonMessage); err != nil { log.Errorf("error sending response error [%s] to %s: %v", callError.UniqueId, clientID, err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL ERROR [%s] for %s", callError.UniqueId, clientID) return nil @@ -264,20 +264,19 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { // If this operation fails, the other endpoint may still starve. func (s *Server) HandleFailedResponseError(clientID string, requestID string, err error, featureName string) { log.Debugf("handling error for failed response [%s]", requestID) - // There's two possible errors: invalid profile or invalid payload - validationErr, ok := err.(validator.ValidationErrors) var responseErr *ocpp.Error - if ok { - if featureName == "" { - // Validation for OcppError failed, generating a default error - responseErr = ocpp.NewError(GenericError, err.Error(), requestID) - } else { - // Validation failed for a feature, sending an error instead - responseErr = errorFromValidation(validationErr, requestID, featureName) - } - } else { - // unsupported profile error - responseErr = ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", featureName), requestID) + // There's several possible errors: invalid profile, invalid payload or send error + switch err.(type) { + case validator.ValidationErrors: + // Validation error + validationErr := err.(validator.ValidationErrors) + responseErr = errorFromValidation(validationErr, requestID, featureName) + case *ocpp.Error: + // Internal OCPP error + responseErr = err.(*ocpp.Error) + case error: + // Unknown error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) } // Send an OCPP error to the target, since no regular response could be sent _ = s.SendError(clientID, requestID, responseErr.Code, responseErr.Description, nil) From dfa368e0b7a253711ff019844779c32af0288c0d Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 12:03:25 +0200 Subject: [PATCH 24/47] Convert v1.6 core listeners to pointer receiver Signed-off-by: Lorenzo --- ocpp1.6_test/authorize_test.go | 5 ++- ocpp1.6_test/boot_notification_test.go | 5 ++- ocpp1.6_test/change_availability_test.go | 3 +- ocpp1.6_test/change_configuration_test.go | 3 +- ocpp1.6_test/clear_cache_test.go | 3 +- ocpp1.6_test/data_transfer_test.go | 5 ++- ocpp1.6_test/get_configuration_test.go | 2 +- ocpp1.6_test/heartbeat_test.go | 5 ++- ocpp1.6_test/meter_values_test.go | 5 ++- ocpp1.6_test/ocpp16_test.go | 38 +++++++++---------- ocpp1.6_test/remote_start_transaction_test.go | 3 +- ocpp1.6_test/remote_stop_transaction_test.go | 3 +- ocpp1.6_test/reset_test.go | 3 +- ocpp1.6_test/start_transaction_test.go | 5 ++- ocpp1.6_test/status_notification_test.go | 2 +- ocpp1.6_test/stop_transaction_test.go | 2 +- ocpp1.6_test/unlock_connector_test.go | 3 +- 17 files changed, 54 insertions(+), 41 deletions(-) diff --git a/ocpp1.6_test/authorize_test.go b/ocpp1.6_test/authorize_test.go index db7f4698..665b36f1 100644 --- a/ocpp1.6_test/authorize_test.go +++ b/ocpp1.6_test/authorize_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -48,7 +49,7 @@ func (suite *OcppV16TestSuite) TestAuthorizeE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnAuthorize", mock.AnythingOfType("string"), mock.Anything).Return(authorizeConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.AuthorizeRequest) require.True(t, ok) diff --git a/ocpp1.6_test/boot_notification_test.go b/ocpp1.6_test/boot_notification_test.go index 0a3d5962..90b33f33 100644 --- a/ocpp1.6_test/boot_notification_test.go +++ b/ocpp1.6_test/boot_notification_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Tests @@ -61,7 +62,7 @@ func (suite *OcppV16TestSuite) TestBootNotificationE2EMocked() { bootNotificationConfirmation := core.NewBootNotificationConfirmation(currentTime, interval, registrationStatus) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnBootNotification", mock.AnythingOfType("string"), mock.Anything).Return(bootNotificationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.BootNotificationRequest) require.True(t, ok) diff --git a/ocpp1.6_test/change_availability_test.go b/ocpp1.6_test/change_availability_test.go index 0726cf7b..a0192f78 100644 --- a/ocpp1.6_test/change_availability_test.go +++ b/ocpp1.6_test/change_availability_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -47,7 +48,7 @@ func (suite *OcppV16TestSuite) TestChangeAvailabilityE2EMocked() { changeAvailabilityConfirmation := core.NewChangeAvailabilityConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnChangeAvailability", mock.Anything).Return(changeAvailabilityConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ChangeAvailabilityRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/change_configuration_test.go b/ocpp1.6_test/change_configuration_test.go index 1735c295..3002bff9 100644 --- a/ocpp1.6_test/change_configuration_test.go +++ b/ocpp1.6_test/change_configuration_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -47,7 +48,7 @@ func (suite *OcppV16TestSuite) TestChangeConfigurationE2EMocked() { changeConfigurationConfirmation := core.NewChangeConfigurationConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnChangeConfiguration", mock.Anything).Return(changeConfigurationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ChangeConfigurationRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/clear_cache_test.go b/ocpp1.6_test/clear_cache_test.go index cdec7e8c..eb9b644a 100644 --- a/ocpp1.6_test/clear_cache_test.go +++ b/ocpp1.6_test/clear_cache_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -39,7 +40,7 @@ func (suite *OcppV16TestSuite) TestClearCacheE2EMocked() { clearCacheConfirmation := core.NewClearCacheConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnClearCache", mock.Anything).Return(clearCacheConfirmation, nil) setupDefaultCentralSystemHandlers(suite, nil, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargePointHandlers(suite, coreListener, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}) diff --git a/ocpp1.6_test/data_transfer_test.go b/ocpp1.6_test/data_transfer_test.go index 60dc22c4..319b530d 100644 --- a/ocpp1.6_test/data_transfer_test.go +++ b/ocpp1.6_test/data_transfer_test.go @@ -3,6 +3,7 @@ package ocpp16_test import ( "encoding/json" "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -61,7 +62,7 @@ func (suite *OcppV16TestSuite) TestDataTransferFromChargePointE2EMocked() { dataTransferConfirmation := core.NewDataTransferConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.DataTransferRequest) require.NotNil(t, request) @@ -101,7 +102,7 @@ func (suite *OcppV16TestSuite) TestDataTransferFromCentralSystemE2EMocked() { dataTransferConfirmation := core.NewDataTransferConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.DataTransferRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/get_configuration_test.go b/ocpp1.6_test/get_configuration_test.go index 8ee537db..5f8aac99 100644 --- a/ocpp1.6_test/get_configuration_test.go +++ b/ocpp1.6_test/get_configuration_test.go @@ -63,7 +63,7 @@ func (suite *OcppV16TestSuite) TestGetConfigurationE2EMocked() { getConfigurationConfirmation.UnknownKey = unknownKeys channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnGetConfiguration", mock.Anything).Return(getConfigurationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.GetConfigurationRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/heartbeat_test.go b/ocpp1.6_test/heartbeat_test.go index 9a647163..648f2480 100644 --- a/ocpp1.6_test/heartbeat_test.go +++ b/ocpp1.6_test/heartbeat_test.go @@ -2,11 +2,12 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -38,7 +39,7 @@ func (suite *OcppV16TestSuite) TestHeartbeatE2EMocked() { heartbeatConfirmation := core.NewHeartbeatConfirmation(currentTime) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnHeartbeat", mock.AnythingOfType("string"), mock.Anything).Return(heartbeatConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.HeartbeatRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/meter_values_test.go b/ocpp1.6_test/meter_values_test.go index 0a514f00..e5792974 100644 --- a/ocpp1.6_test/meter_values_test.go +++ b/ocpp1.6_test/meter_values_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -46,7 +47,7 @@ func (suite *OcppV16TestSuite) TestMeterValuesE2EMocked() { meterValuesConfirmation := core.NewMeterValuesConfirmation() channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnMeterValues", mock.AnythingOfType("string"), mock.Anything).Return(meterValuesConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.MeterValuesRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index c966bffb..10075269 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -209,49 +209,49 @@ type MockCentralSystemCoreListener struct { mock.Mock } -func (coreListener MockCentralSystemCoreListener) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { args := coreListener.MethodCalled("OnAuthorize", chargePointId, request) conf := args.Get(0).(*core.AuthorizeConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) { args := coreListener.MethodCalled("OnBootNotification", chargePointId, request) conf := args.Get(0).(*core.BootNotificationConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", chargePointId, request) conf := args.Get(0).(*core.DataTransferConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { args := coreListener.MethodCalled("OnHeartbeat", chargePointId, request) conf := args.Get(0).(*core.HeartbeatConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (confirmation *core.MeterValuesConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (confirmation *core.MeterValuesConfirmation, err error) { args := coreListener.MethodCalled("OnMeterValues", chargePointId, request) conf := args.Get(0).(*core.MeterValuesConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStartTransaction(chargePointId string, request *core.StartTransactionRequest) (confirmation *core.StartTransactionConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStartTransaction(chargePointId string, request *core.StartTransactionRequest) (confirmation *core.StartTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnStartTransaction", chargePointId, request) conf := args.Get(0).(*core.StartTransactionConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStatusNotification(chargePointId string, request *core.StatusNotificationRequest) (confirmation *core.StatusNotificationConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStatusNotification(chargePointId string, request *core.StatusNotificationRequest) (confirmation *core.StatusNotificationConfirmation, err error) { args := coreListener.MethodCalled("OnStatusNotification", chargePointId, request) conf := args.Get(0).(*core.StatusNotificationConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStopTransaction(chargePointId string, request *core.StopTransactionRequest) (confirmation *core.StopTransactionConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStopTransaction(chargePointId string, request *core.StopTransactionRequest) (confirmation *core.StopTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnStopTransaction", chargePointId, request) conf := args.Get(0).(*core.StopTransactionConfirmation) return conf, args.Error(1) @@ -262,55 +262,55 @@ type MockChargePointCoreListener struct { mock.Mock } -func (coreListener MockChargePointCoreListener) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { args := coreListener.MethodCalled("OnChangeAvailability", request) conf := args.Get(0).(*core.ChangeAvailabilityConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", request) conf := args.Get(0).(*core.DataTransferConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { args := coreListener.MethodCalled("OnChangeConfiguration", request) conf := args.Get(0).(*core.ChangeConfigurationConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) { args := coreListener.MethodCalled("OnClearCache", request) conf := args.Get(0).(*core.ClearCacheConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) { args := coreListener.MethodCalled("OnGetConfiguration", request) conf := args.Get(0).(*core.GetConfigurationConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) { args := coreListener.MethodCalled("OnReset", request) conf := args.Get(0).(*core.ResetConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) { args := coreListener.MethodCalled("OnUnlockConnector", request) conf := args.Get(0).(*core.UnlockConnectorConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnRemoteStartTransaction", request) conf := args.Get(0).(*core.RemoteStartTransactionConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnRemoteStopTransaction", request) conf := args.Get(0).(*core.RemoteStopTransactionConfirmation) return conf, args.Error(1) @@ -557,7 +557,7 @@ func testUnsupportedRequestFromChargePoint(suite *OcppV16TestSuite, request ocpp channel := NewMockWebSocket(wsId) setupDefaultChargePointHandlers(suite, nil, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: false}) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} setupDefaultCentralSystemHandlers(suite, coreListener, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) resultChannel := make(chan bool, 1) suite.ocppjChargePoint.SetErrorHandler(func(err *ocpp.Error, details interface{}) { @@ -595,7 +595,7 @@ func testUnsupportedRequestFromCentralSystem(suite *OcppV16TestSuite, request oc channel := NewMockWebSocket(wsId) setupDefaultCentralSystemHandlers(suite, nil, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: false}) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} setupDefaultChargePointHandlers(suite, coreListener, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) suite.ocppjCentralSystem.SetErrorHandler(func(chargePoint ws.Channel, err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) diff --git a/ocpp1.6_test/remote_start_transaction_test.go b/ocpp1.6_test/remote_start_transaction_test.go index d19964fa..32e31425 100644 --- a/ocpp1.6_test/remote_start_transaction_test.go +++ b/ocpp1.6_test/remote_start_transaction_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" @@ -69,7 +70,7 @@ func (suite *OcppV16TestSuite) TestRemoteStartTransactionE2EMocked() { RemoteStartTransactionConfirmation := core.NewRemoteStartTransactionConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnRemoteStartTransaction", mock.Anything).Return(RemoteStartTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.RemoteStartTransactionRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/remote_stop_transaction_test.go b/ocpp1.6_test/remote_stop_transaction_test.go index d85c2ac6..7f9ec40c 100644 --- a/ocpp1.6_test/remote_stop_transaction_test.go +++ b/ocpp1.6_test/remote_stop_transaction_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" @@ -43,7 +44,7 @@ func (suite *OcppV16TestSuite) TestRemoteStopTransactionE2EMocked() { RemoteStopTransactionConfirmation := core.NewRemoteStopTransactionConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnRemoteStopTransaction", mock.Anything).Return(RemoteStopTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.RemoteStopTransactionRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/reset_test.go b/ocpp1.6_test/reset_test.go index d2bf78bb..eaea7ed4 100644 --- a/ocpp1.6_test/reset_test.go +++ b/ocpp1.6_test/reset_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -43,7 +44,7 @@ func (suite *OcppV16TestSuite) TestResetE2EMocked() { resetConfirmation := core.NewResetConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnReset", mock.Anything).Return(resetConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ResetRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/start_transaction_test.go b/ocpp1.6_test/start_transaction_test.go index 23494d03..5991acb5 100644 --- a/ocpp1.6_test/start_transaction_test.go +++ b/ocpp1.6_test/start_transaction_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -60,7 +61,7 @@ func (suite *OcppV16TestSuite) TestStartTransactionE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStartTransaction", mock.AnythingOfType("string"), mock.Anything).Return(startTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StartTransactionRequest) require.True(t, ok) diff --git a/ocpp1.6_test/status_notification_test.go b/ocpp1.6_test/status_notification_test.go index 3b9fc6da..376aa611 100644 --- a/ocpp1.6_test/status_notification_test.go +++ b/ocpp1.6_test/status_notification_test.go @@ -57,7 +57,7 @@ func (suite *OcppV16TestSuite) TestStatusNotificationE2EMocked() { statusNotificationConfirmation := core.NewStatusNotificationConfirmation() channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStatusNotification", mock.AnythingOfType("string"), mock.Anything).Return(statusNotificationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StatusNotificationRequest) require.True(t, ok) diff --git a/ocpp1.6_test/stop_transaction_test.go b/ocpp1.6_test/stop_transaction_test.go index 3cc093e4..c176875b 100644 --- a/ocpp1.6_test/stop_transaction_test.go +++ b/ocpp1.6_test/stop_transaction_test.go @@ -64,7 +64,7 @@ func (suite *OcppV16TestSuite) TestStopTransactionE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStopTransaction", mock.AnythingOfType("string"), mock.Anything).Return(stopTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StopTransactionRequest) require.True(t, ok) diff --git a/ocpp1.6_test/unlock_connector_test.go b/ocpp1.6_test/unlock_connector_test.go index 6033e5ea..1a6173cb 100644 --- a/ocpp1.6_test/unlock_connector_test.go +++ b/ocpp1.6_test/unlock_connector_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -41,7 +42,7 @@ func (suite *OcppV16TestSuite) TestUnlockConnectorE2EMocked() { unlockConnectorConfirmation := core.NewUnlockConnectorConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnUnlockConnector", mock.Anything).Return(unlockConnectorConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.UnlockConnectorRequest) require.NotNil(t, request) From 830bc32fd21ad6a3f78c5e239d6fa0f8f8311223 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 14:15:20 +0200 Subject: [PATCH 25/47] Fix v1.6 error handling in sendResponse functions Signed-off-by: Lorenzo --- ocpp1.6/central_system.go | 16 ++++++++++++---- ocpp1.6/charge_point.go | 20 ++++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index c8765736..ad4d949b 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -2,6 +2,7 @@ package ocpp16 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -407,19 +408,23 @@ func (cs *centralSystem) Start(listenPort int, listenPath string) { } func (cs *centralSystem) sendResponse(chargePointId string, confirmation ocpp.Response, err error, requestId string) { - // send error response if err != nil { - cs.error(fmt.Errorf("error handling request: %w", err)) - err := cs.server.SendError(chargePointId, requestId, ocppj.InternalError, "Error handling request", nil) + // Send error response + err = cs.server.SendError(chargePointId, requestId, ocppj.InternalError, err.Error(), nil) if err != nil { + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargePointId, requestId, err, "") + // Notify client implementation err = fmt.Errorf("error replying cp %s to request %s with 'internal error': %w", chargePointId, requestId, err) cs.error(err) } return } - if confirmation == nil { + if confirmation == nil || reflect.ValueOf(confirmation).IsNil() { err = fmt.Errorf("empty confirmation to %s for request %s", chargePointId, requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.server.SendError(chargePointId, requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } @@ -427,6 +432,9 @@ func (cs *centralSystem) sendResponse(chargePointId string, confirmation ocpp.Re // send confirmation response err = cs.server.SendResponse(chargePointId, requestId, confirmation) if err != nil { + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargePointId, requestId, err, confirmation.GetFeatureName()) + // Notify client implementation err = fmt.Errorf("error replying cp %s to request %s: %w", chargePointId, requestId, err) cs.error(err) } diff --git a/ocpp1.6/charge_point.go b/ocpp1.6/charge_point.go index e0e87a24..0d853cbf 100644 --- a/ocpp1.6/charge_point.go +++ b/ocpp1.6/charge_point.go @@ -2,6 +2,7 @@ package ocpp16 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -295,19 +296,23 @@ func (cp *chargePoint) clearCallbacks(invokeCallback bool) { } func (cp *chargePoint) sendResponse(confirmation ocpp.Response, err error, requestId string) { - // send error response if err != nil { - err = cp.client.SendError(requestId, ocppj.ProtocolError, err.Error(), nil) + // Send error response + err = cp.client.SendError(requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - err = fmt.Errorf("replying cs to request %s with 'protocol error': %w", requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cp.client.HandleFailedResponseError(requestId, err, "") + // Notify client implementation + err = fmt.Errorf("replying to request %s with 'internal error' failed: %w", requestId, err) cp.error(err) } - return } - if confirmation == nil { + if confirmation == nil || reflect.ValueOf(confirmation).IsNil() { err = fmt.Errorf("empty confirmation to request %s", requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cp.client.SendError(requestId, ocppj.GenericError, err.Error(), nil) cp.error(err) return } @@ -315,7 +320,10 @@ func (cp *chargePoint) sendResponse(confirmation ocpp.Response, err error, reque // send confirmation response err = cp.client.SendResponse(requestId, confirmation) if err != nil { - err = fmt.Errorf("replying cs to request %s: %w", requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cp.client.HandleFailedResponseError(requestId, err, confirmation.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("failed responding to request %s: %w", requestId, err) cp.error(err) } } From 9490eba7451dfa93187f0f1b10d64a004bbde723 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 14:15:44 +0200 Subject: [PATCH 26/47] Add v1.6 tests for new error handling Signed-off-by: Lorenzo --- ocpp1.6_test/ocpp16_test.go | 16 +++- ocpp1.6_test/proto_test.go | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 ocpp1.6_test/proto_test.go diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index 10075269..5c3562f2 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -223,8 +223,12 @@ func (coreListener *MockCentralSystemCoreListener) OnBootNotification(chargePoin func (coreListener *MockCentralSystemCoreListener) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", chargePointId, request) - conf := args.Get(0).(*core.DataTransferConfirmation) - return conf, args.Error(1) + rawConf := args.Get(0) + err = args.Error(1) + if rawConf != nil { + confirmation = rawConf.(*core.DataTransferConfirmation) + } + return } func (coreListener *MockCentralSystemCoreListener) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { @@ -270,8 +274,12 @@ func (coreListener *MockChargePointCoreListener) OnChangeAvailability(request *c func (coreListener *MockChargePointCoreListener) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", request) - conf := args.Get(0).(*core.DataTransferConfirmation) - return conf, args.Error(1) + rawConf := args.Get(0) + err = args.Error(1) + if rawConf != nil { + confirmation = rawConf.(*core.DataTransferConfirmation) + } + return } func (coreListener *MockChargePointCoreListener) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { diff --git a/ocpp1.6_test/proto_test.go b/ocpp1.6_test/proto_test.go new file mode 100644 index 00000000..517dc7fc --- /dev/null +++ b/ocpp1.6_test/proto_test.go @@ -0,0 +1,154 @@ +package ocpp16_test + +import ( + "fmt" + + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func (suite *OcppV16TestSuite) TestChargePointSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + // Setup internal communication and listeners + coreListener := &MockChargePointCoreListener{} + suite.chargePoint.SetCoreHandler(coreListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.centralSystem.Start(8887, "somePath") + err := suite.chargePoint.Start("someUrl") + require.Nil(t, err) + resultChannel := make(chan error, 1) + // Test 1: occurrence validation error + dataTransferConfirmation := core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = CustomData{Field1: "", Field2: 42} + coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result := <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferConfirmation = core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = make(chan struct{}) + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.Anything).Return(nil, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "empty confirmation to request 1234", ocppErr.Description) +} + +func (suite *OcppV16TestSuite) TestCentralSystemSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + var response *core.DataTransferConfirmation + // Setup internal communication and listeners + coreListener := &MockCentralSystemCoreListener{} + suite.centralSystem.SetCoreHandler(coreListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.centralSystem.Start(8887, "somePath") + err := suite.chargePoint.Start("someUrl") + require.Nil(t, err) + // Test 1: occurrence validation error + dataTransferConfirmation := core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = CustomData{Field1: "", Field2: 42} + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferConfirmation = core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = make(chan struct{}) + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(nil, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, fmt.Sprintf("empty confirmation to %s for request 1234", wsId), ocppErr.Description) +} From 3430e25a8fb36f1cefd0bfb4a26c12c5337e36da Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 14:29:53 +0200 Subject: [PATCH 27/47] Fix v2.0.1 error handling in sendResponse functions Signed-off-by: Lorenzo --- ocpp2.0.1/charging_station.go | 23 +++++++++++++++++------ ocpp2.0.1/csms.go | 22 +++++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/ocpp2.0.1/charging_station.go b/ocpp2.0.1/charging_station.go index 5f4bc827..ae0bd7b9 100644 --- a/ocpp2.0.1/charging_station.go +++ b/ocpp2.0.1/charging_station.go @@ -2,6 +2,7 @@ package ocpp2 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -555,25 +556,35 @@ func (cs *chargingStation) asyncCallbackHandler() { } func (cs *chargingStation) sendResponse(response ocpp.Response, err error, requestId string) { - // send error response if err != nil { - err = cs.client.SendError(requestId, ocppj.ProtocolError, err.Error(), nil) + // Send error response + err = cs.client.SendError(requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - cs.error(fmt.Errorf("replying cs to request %s with 'protocol error': %w", requestId, err)) + // Error while sending an error. Will attempt to send a default error instead + cs.client.HandleFailedResponseError(requestId, err, "") + // Notify client implementation + err = fmt.Errorf("replying to request %s with 'internal error' failed: %w", requestId, err) + cs.error(err) } return } - if response == nil { + if response == nil || reflect.ValueOf(response).IsNil() { err = fmt.Errorf("empty response to request %s", requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.client.SendError(requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } - // send response + // send confirmation response err = cs.client.SendResponse(requestId, response) if err != nil { - cs.error(fmt.Errorf("replying to request %s with 'protocol error': %w", requestId, err)) + // Error while sending an error. Will attempt to send a default error instead + cs.client.HandleFailedResponseError(requestId, err, response.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("failed responding to request %s: %w", requestId, err) + cs.error(err) } } diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index 41dbdcd9..8e8dea67 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -2,6 +2,7 @@ package ocpp2 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -814,22 +815,33 @@ func (cs *csms) Start(listenPort int, listenPath string) { func (cs *csms) sendResponse(chargingStationID string, response ocpp.Response, err error, requestId string) { if err != nil { - err := cs.server.SendError(chargingStationID, requestId, ocppj.ProtocolError, "Couldn't generate valid confirmation", nil) + // Send error response + err = cs.server.SendError(chargingStationID, requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - err = fmt.Errorf("replying cs %s to request %s with 'protocol error': %w", chargingStationID, requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargingStationID, requestId, err, "") + // Notify client implementation + err = fmt.Errorf("error replying cp %s to request %s with 'internal error': %w", chargingStationID, requestId, err) cs.error(err) } return } - if response == nil { + + if response == nil || reflect.ValueOf(response).IsNil() { err = fmt.Errorf("empty response to %s for request %s", chargingStationID, requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.server.SendError(chargingStationID, requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } - // send response + + // send confirmation response err = cs.server.SendResponse(chargingStationID, requestId, response) if err != nil { - err = fmt.Errorf("replying cs %s to request %s: %w", chargingStationID, requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargingStationID, requestId, err, response.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("error replying cp %s to request %s: %w", chargingStationID, requestId, err) cs.error(err) } } From 3ec1db975b5350740fa211e42507c5c649836cd3 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 14:30:33 +0200 Subject: [PATCH 28/47] Add v2.0.1 tests for new error handling Signed-off-by: Lorenzo --- ocpp2.0.1_test/ocpp2_test.go | 20 +++-- ocpp2.0.1_test/proto_test.go | 159 +++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 ocpp2.0.1_test/proto_test.go diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index b2515dee..fe4d01ae 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -400,10 +400,14 @@ type MockChargingStationDataHandler struct { mock.Mock } -func (handler *MockChargingStationDataHandler) OnDataTransfer(request *data.DataTransferRequest) (confirmation *data.DataTransferResponse, err error) { +func (handler *MockChargingStationDataHandler) OnDataTransfer(request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { args := handler.MethodCalled("OnDataTransfer", request) - conf := args.Get(0).(*data.DataTransferResponse) - return conf, args.Error(1) + rawResp := args.Get(0) + err = args.Error(1) + if rawResp != nil { + response = rawResp.(*data.DataTransferResponse) + } + return } // ---------------------- MOCK CSMS DATA HANDLER ---------------------- @@ -412,10 +416,14 @@ type MockCSMSDataHandler struct { mock.Mock } -func (handler *MockCSMSDataHandler) OnDataTransfer(chargingStationID string, request *data.DataTransferRequest) (confirmation *data.DataTransferResponse, err error) { +func (handler *MockCSMSDataHandler) OnDataTransfer(chargingStationID string, request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { args := handler.MethodCalled("OnDataTransfer", chargingStationID, request) - conf := args.Get(0).(*data.DataTransferResponse) - return conf, args.Error(1) + rawResp := args.Get(0) + err = args.Error(1) + if rawResp != nil { + response = rawResp.(*data.DataTransferResponse) + } + return } // ---------------------- MOCK CS DIAGNOSTICS HANDLER ---------------------- diff --git a/ocpp2.0.1_test/proto_test.go b/ocpp2.0.1_test/proto_test.go new file mode 100644 index 00000000..846aa838 --- /dev/null +++ b/ocpp2.0.1_test/proto_test.go @@ -0,0 +1,159 @@ +package ocpp2_test + +import ( + "fmt" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/data" + + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func (suite *OcppV2TestSuite) TestChargePointSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + // Setup internal communication and listeners + dataListener := &MockChargingStationDataHandler{} + suite.chargingStation.SetDataHandler(dataListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.csms.Start(8887, "somePath") + err := suite.chargingStation.Start("someUrl") + require.Nil(t, err) + resultChannel := make(chan error, 1) + // Test 1: occurrence validation error + dataTransferResponse := data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = struct { + Field1 string `validate:"required"` + }{Field1: ""} + dataListener.On("OnDataTransfer", mock.Anything).Return(dataTransferResponse, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result := <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferResponse = data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = make(chan struct{}) + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.Anything).Return(dataTransferResponse, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.Anything).Return(nil, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "empty response to request 1234", ocppErr.Description) +} + +func (suite *OcppV2TestSuite) TestCentralSystemSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + var response *data.DataTransferResponse + // Setup internal communication and listeners + dataListener := &MockCSMSDataHandler{} + suite.csms.SetDataHandler(dataListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.csms.Start(8887, "somePath") + err := suite.chargingStation.Start("someUrl") + require.Nil(t, err) + // Test 1: occurrence validation error + dataTransferResponse := data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = struct { + Field1 string `validate:"required"` + }{Field1: ""} + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferResponse, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferResponse = data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = make(chan struct{}) + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferResponse, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(nil, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, fmt.Sprintf("empty response to %s for request 1234", wsId), ocppErr.Description) +} From 782920b6653316ab7d5d258cfd66883e6de61f54 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Apr 2023 14:35:13 +0200 Subject: [PATCH 29/47] Fix broken tests Signed-off-by: Lorenzo --- ocpp1.6_test/ocpp16_test.go | 39 +++++++++++++++++++++--------------- ocpp2.0.1_test/ocpp2_test.go | 20 +++++++++++------- ocppj/central_system_test.go | 5 ++++- ocppj/charge_point_test.go | 6 ++++-- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index 5c3562f2..3e206edd 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -8,6 +8,8 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/require" + "github.com/lorenzodonini/ocpp-go/ocpp" ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" @@ -561,36 +563,37 @@ func testUnsupportedRequestFromChargePoint(suite *OcppV16TestSuite, request ocpp wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on charge point, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on central system", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultChargePointHandlers(suite, nil, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: false}) coreListener := &MockCentralSystemCoreListener{} setupDefaultCentralSystemHandlers(suite, coreListener, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) - resultChannel := make(chan bool, 1) + resultChannel := make(chan struct{}, 1) suite.ocppjChargePoint.SetErrorHandler(func(err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) - resultChannel <- true + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.centralSystem.Start(8887, "somePath") err := suite.chargePoint.Start(wsUrl) - assert.Nil(t, err) - // Run request test, expecting an error + require.Nil(t, err) + // 1. Test sending an unsupported request, expecting an error err = suite.chargePoint.SendRequestAsync(request, func(confirmation ocpp.Response, err error) { t.Fail() }) - assert.Error(t, err) + require.Error(t, err) assert.Equal(t, expectedError, err.Error()) - // Run response test + // 2. Test receiving an unsupported request on the other endpoint and receiving an error + // Mark mocked request as pending, otherwise response will be ignored suite.ocppjChargePoint.RequestState.AddPendingRequest(messageId, request) err = suite.mockWsServer.MessageHandler(channel, []byte(requestJson)) assert.Nil(t, err) - result := <-resultChannel - assert.True(t, result) + _, ok := <-resultChannel + assert.True(t, ok) } func testUnsupportedRequestFromCentralSystem(suite *OcppV16TestSuite, request ocpp.Request, requestJson string, messageId string) { @@ -599,34 +602,38 @@ func testUnsupportedRequestFromCentralSystem(suite *OcppV16TestSuite, request oc wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on central system, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on charge point", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultCentralSystemHandlers(suite, nil, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: false}) coreListener := &MockChargePointCoreListener{} setupDefaultChargePointHandlers(suite, coreListener, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) + resultChannel := make(chan struct{}, 1) suite.ocppjCentralSystem.SetErrorHandler(func(chargePoint ws.Channel, err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, wsId, chargePoint.ID()) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.centralSystem.Start(8887, "somePath") err := suite.chargePoint.Start(wsUrl) - assert.Nil(t, err) - // Run request test, expecting an error + require.Nil(t, err) + // 1. Test sending an unsupported request, expecting an error err = suite.centralSystem.SendRequestAsync(wsId, request, func(confirmation ocpp.Response, err error) { t.Fail() }) - assert.Error(t, err) + require.Error(t, err) assert.Equal(t, expectedError, err.Error()) + // 2. Test receiving an unsupported request on the other endpoint and receiving an error // Mark mocked request as pending, otherwise response will be ignored suite.ocppjCentralSystem.RequestState.AddPendingRequest(wsId, messageId, request) - // Run response test err = suite.mockWsClient.MessageHandler([]byte(requestJson)) assert.Nil(t, err) + _, ok := <-resultChannel + assert.True(t, ok) } type GenericTestEntry struct { diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index fe4d01ae..dc1cc53d 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -996,7 +996,7 @@ func testUnsupportedRequestFromChargingStation(suite *OcppV2TestSuite, request o wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on charging station, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on CSMS", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: false}) @@ -1006,20 +1006,21 @@ func testUnsupportedRequestFromChargingStation(suite *OcppV2TestSuite, request o assert.Equal(t, messageId, err.MessageId) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) resultChannel <- true }) // Start suite.csms.Start(8887, "somePath") err := suite.chargingStation.Start(wsUrl) require.Nil(t, err) - // Run request test + // 1. Test sending an unsupported request, expecting an error err = suite.chargingStation.SendRequestAsync(request, func(confirmation ocpp.Response, err error) { t.Fail() }) require.Error(t, err) assert.Equal(t, expectedError, err.Error()) - // Run response test + // 2. Test receiving an unsupported request on the other endpoint and receiving an error + // Mark mocked request as pending, otherwise response will be ignored suite.ocppjClient.RequestState.AddPendingRequest(messageId, request) err = suite.mockWsServer.MessageHandler(channel, []byte(requestJson)) require.Nil(t, err) @@ -1033,33 +1034,38 @@ func testUnsupportedRequestFromCentralSystem(suite *OcppV2TestSuite, request ocp wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on CSMS, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on charging station", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: false}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}, handlers...) + resultChannel := make(chan struct{}, 1) suite.ocppjServer.SetErrorHandler(func(channel ws.Channel, err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, wsId, channel.ID()) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.csms.Start(8887, "somePath") err := suite.chargingStation.Start(wsUrl) require.Nil(t, err) - // Run request test, expecting an error + // 1. Test sending an unsupported request, expecting an error err = suite.csms.SendRequestAsync(wsId, request, func(response ocpp.Response, err error) { t.Fail() }) require.Error(t, err) assert.Equal(t, expectedError, err.Error()) + // 2. Test receiving an unsupported request on the other endpoint and receiving an error // Mark mocked request as pending, otherwise response will be ignored suite.ocppjServer.RequestState.AddPendingRequest(wsId, messageId, request) // Run response test err = suite.mockWsClient.MessageHandler([]byte(requestJson)) assert.Nil(t, err) + _, ok := <-resultChannel + assert.True(t, ok) } type GenericTestEntry struct { diff --git a/ocppj/central_system_test.go b/ocppj/central_system_test.go index 89d6bcc3..e925e960 100644 --- a/ocppj/central_system_test.go +++ b/ocppj/central_system_test.go @@ -203,7 +203,8 @@ func (suite *OcppJTestSuite) TestCentralSystemSendConfirmationFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.centralSystem.SendResponse(mockChargePointId, mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } // SendError @@ -244,6 +245,8 @@ func (suite *OcppJTestSuite) TestCentralSystemSendErrorFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.centralSystem.SendResponse(mockChargePointId, mockUniqueId, mockConfirmation) assert.NotNil(t, err) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } func (suite *OcppJTestSuite) TestCentralSystemHandleFailedResponse() { diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 969a3c42..603e9d95 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -193,7 +193,8 @@ func (suite *OcppJTestSuite) TestChargePointSendConfirmationFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.chargePoint.SendResponse(mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } // ----------------- SendError tests ----------------- @@ -223,7 +224,8 @@ func (suite *OcppJTestSuite) TestChargePointSendErrorFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.chargePoint.SendResponse(mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } func (suite *OcppJTestSuite) TestChargePointHandleFailedResponse() { From c38d419ea5d1b2cbd072ea65318be85a572e5283 Mon Sep 17 00:00:00 2001 From: Scott Clark Date: Mon, 17 Apr 2023 14:38:17 -0400 Subject: [PATCH 30/47] resolves issues/173 checking and handling invalid message property for CALL_ERROR. --- go.mod | 2 +- ocppj/ocppj.go | 5 ++++- ocppj/ocppj_test.go | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 38b2fd2a..3e0d0789 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/Shopify/toxiproxy v2.1.4+incompatible github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 // indirect + github.com/go-playground/universal-translator v0.16.0 github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 github.com/kr/pretty v0.1.0 // indirect diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 86e300e1..33655766 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -443,7 +443,10 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl if len(arr) > 4 { details = arr[4] } - rawErrorCode := arr[2].(string) + rawErrorCode, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormationViolation, fmt.Sprintf("Invalid element %v at 2, expected rawErrorCode (string)", arr[2]), rawErrorCode) + } errorCode := ocpp.ErrorCode(rawErrorCode) errorDescription := "" if v, ok := arr[3].(string); ok { diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index d0d13d83..62b8e2ba 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -664,6 +664,27 @@ func (suite *OcppJTestSuite) TestParseMessageInvalidCallError() { assert.Equal(t, "Invalid Call Error message. Expected array length >= 4", protoErr.Description) } +func (suite *OcppJTestSuite) TestParseMessageInvalidRawErrorCode() { + t := suite.T() + mockMessage := make([]interface{}, 5) + messageId := "12345" + pendingRequest := newMockRequest("request") + mockMessage[0] = float64(ocppj.CALL_ERROR) // Message Type ID + mockMessage[1] = messageId // Unique ID + mockMessage[2] = float64(42) // test invalid typecast + mockMessage[3] = "error description" + mockMessage[4] = "error details" + suite.chargePoint.RequestState.AddPendingRequest(messageId, pendingRequest) // Manually add a pending request, so that response is not rejected + message, err := suite.chargePoint.ParseMessage(mockMessage, suite.chargePoint.RequestState) + require.Nil(t, message) + require.Error(t, err) + protoErr := err.(*ocpp.Error) + require.NotNil(t, protoErr) + assert.Equal(t, protoErr.MessageId, "") // unique id is never set after invalid type cast return + assert.Equal(t, ocppj.FormationViolation, protoErr.Code) + assert.Equal(t, "Invalid element 42 at 2, expected rawErrorCode (string)", protoErr.Description) +} + func (suite *OcppJTestSuite) TestParseMessageInvalidRequest() { t := suite.T() mockMessage := make([]interface{}, 4) From 3ecaf00b33b6b5f94c9e198cfc793b238297147c Mon Sep 17 00:00:00 2001 From: Scott Clark Date: Mon, 17 Apr 2023 14:50:48 -0400 Subject: [PATCH 31/47] reverted go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3e0d0789..38b2fd2a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/Shopify/toxiproxy v2.1.4+incompatible github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 + github.com/go-playground/universal-translator v0.16.0 // indirect github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 github.com/kr/pretty v0.1.0 // indirect From 0c739679e2e0b57ab15d04871686a401825b7368 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Fri, 19 May 2023 15:28:17 +0200 Subject: [PATCH 32/47] Fix SetVariableStatus validation tag Signed-off-by: Lorenzo --- ocpp2.0.1/provisioning/set_variables.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.0.1/provisioning/set_variables.go b/ocpp2.0.1/provisioning/set_variables.go index 4f75be23..dfc100de 100644 --- a/ocpp2.0.1/provisioning/set_variables.go +++ b/ocpp2.0.1/provisioning/set_variables.go @@ -42,7 +42,7 @@ type SetVariableData struct { type SetVariableResult struct { AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute"` - AttributeStatus SetVariableStatus `json:"attributeStatus" validate:"required,getVariableStatus"` + AttributeStatus SetVariableStatus `json:"attributeStatus" validate:"required,setVariableStatus"` Component types.Component `json:"component" validate:"required"` Variable types.Variable `json:"variable" validate:"required"` StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` From 34d3b6098157dbede93b982e7dbebd83be1942f1 Mon Sep 17 00:00:00 2001 From: Scott Zhou Date: Wed, 10 May 2023 09:55:20 -0400 Subject: [PATCH 33/47] fix messagesinqueue json --- ocpp2.0.1/transactions/get_transaction_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.0.1/transactions/get_transaction_status.go b/ocpp2.0.1/transactions/get_transaction_status.go index 1582d6ad..098c7199 100644 --- a/ocpp2.0.1/transactions/get_transaction_status.go +++ b/ocpp2.0.1/transactions/get_transaction_status.go @@ -17,7 +17,7 @@ type GetTransactionStatusRequest struct { // In case the request was invalid, or couldn't be processed, an error will be sent instead. type GetTransactionStatusResponse struct { OngoingIndicator *bool `json:"ongoingIndicator,omitempty" validate:"omitempty"` - MessageInQueue bool `json:"messageInQueue"` + MessageInQueue bool `json:"messagesInQueue"` } // In some scenarios a CSMS needs to know whether there are still messages for a transaction that need to be delivered. From e034d3913431284a4241948c6ecb91aa6982758c Mon Sep 17 00:00:00 2001 From: Scott Zhou Date: Fri, 19 May 2023 09:42:05 -0400 Subject: [PATCH 34/47] Change messageinqueue to messagesinqueue --- ocpp2.0.1/transactions/get_transaction_status.go | 6 +++--- ocpp2.0.1_test/get_transaction_status_test.go | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ocpp2.0.1/transactions/get_transaction_status.go b/ocpp2.0.1/transactions/get_transaction_status.go index 098c7199..d1a3f0ba 100644 --- a/ocpp2.0.1/transactions/get_transaction_status.go +++ b/ocpp2.0.1/transactions/get_transaction_status.go @@ -17,7 +17,7 @@ type GetTransactionStatusRequest struct { // In case the request was invalid, or couldn't be processed, an error will be sent instead. type GetTransactionStatusResponse struct { OngoingIndicator *bool `json:"ongoingIndicator,omitempty" validate:"omitempty"` - MessageInQueue bool `json:"messagesInQueue"` + MessagesInQueue bool `json:"messagesInQueue"` } // In some scenarios a CSMS needs to know whether there are still messages for a transaction that need to be delivered. @@ -52,6 +52,6 @@ func NewGetTransactionStatusRequest() *GetTransactionStatusRequest { } // Creates a new GetTransactionStatusResponse, containing all required fields. There are no optional fields for this message. -func NewGetTransactionStatusResponse(messageInQueue bool) *GetTransactionStatusResponse { - return &GetTransactionStatusResponse{MessageInQueue: messageInQueue} +func NewGetTransactionStatusResponse(messagesInQueue bool) *GetTransactionStatusResponse { + return &GetTransactionStatusResponse{MessagesInQueue: messagesInQueue} } diff --git a/ocpp2.0.1_test/get_transaction_status_test.go b/ocpp2.0.1_test/get_transaction_status_test.go index 222bbfee..872c35a5 100644 --- a/ocpp2.0.1_test/get_transaction_status_test.go +++ b/ocpp2.0.1_test/get_transaction_status_test.go @@ -24,8 +24,8 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusRequestValidation() { func (suite *OcppV2TestSuite) TestGetTransactionStatusResponseValidation() { t := suite.T() var confirmationTable = []GenericTestEntry{ - {transactions.GetTransactionStatusResponse{OngoingIndicator: newBool(true), MessageInQueue: true}, true}, - {transactions.GetTransactionStatusResponse{MessageInQueue: true}, true}, + {transactions.GetTransactionStatusResponse{OngoingIndicator: newBool(true), MessagesInQueue: true}, true}, + {transactions.GetTransactionStatusResponse{MessagesInQueue: true}, true}, {transactions.GetTransactionStatusResponse{}, true}, } ExecuteGenericTestTable(t, confirmationTable) @@ -37,11 +37,11 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusE2EMocked() { messageId := defaultMessageId wsUrl := "someUrl" transactionID := "12345" - messageInQueue := false + messagesInQueue := false ongoingIndicator := newBool(true) requestJson := fmt.Sprintf(`[2,"%v","%v",{"transactionId":"%v"}]`, messageId, transactions.GetTransactionStatusFeatureName, transactionID) - responseJson := fmt.Sprintf(`[3,"%v",{"ongoingIndicator":%v,"messageInQueue":%v}]`, messageId, *ongoingIndicator, messageInQueue) - getTransactionStatusResponse := transactions.NewGetTransactionStatusResponse(messageInQueue) + responseJson := fmt.Sprintf(`[3,"%v",{"ongoingIndicator":%v,"messagesInQueue":%v}]`, messageId, *ongoingIndicator, messagesInQueue) + getTransactionStatusResponse := transactions.NewGetTransactionStatusResponse(messagesInQueue) getTransactionStatusResponse.OngoingIndicator = ongoingIndicator channel := NewMockWebSocket(wsId) @@ -62,7 +62,7 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusE2EMocked() { err = suite.csms.GetTransactionStatus(wsId, func(response *transactions.GetTransactionStatusResponse, err error) { require.Nil(t, err) require.NotNil(t, response) - assert.Equal(t, messageInQueue, response.MessageInQueue) + assert.Equal(t, messagesInQueue, response.MessagesInQueue) require.NotNil(t, response.OngoingIndicator) require.Equal(t, *ongoingIndicator, *response.OngoingIndicator) resultChannel <- true From c34f6ad20983d34d7575d9fa05d85368ed0109e2 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 11 Jun 2023 15:37:02 +0200 Subject: [PATCH 35/47] Fix validation override between v1.6 and v2.0.1 Signed-off-by: Lorenzo --- ocpp1.6/core/remote_start_transaction.go | 5 ++- ocpp1.6/core/remote_stop_transaction.go | 5 ++- .../smartcharging/clear_charging_profile.go | 5 ++- .../smartcharging/get_composite_schedule.go | 5 ++- ocpp1.6/types/types.go | 38 ++++++++-------- .../smartcharging/clear_charging_profile.go | 2 +- .../smartcharging/get_charging_profiles.go | 2 +- .../smartcharging/get_composite_schedule.go | 2 +- ocpp2.0.1/types/types.go | 45 +++++++++---------- ocpp2.0.1_test/boot_notification_test.go | 1 - 10 files changed, 56 insertions(+), 54 deletions(-) diff --git a/ocpp1.6/core/remote_start_transaction.go b/ocpp1.6/core/remote_start_transaction.go index 1fd96e51..2b008077 100644 --- a/ocpp1.6/core/remote_start_transaction.go +++ b/ocpp1.6/core/remote_start_transaction.go @@ -1,8 +1,9 @@ package core import ( - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // -------------------- Remote Start Transaction (CS -> CP) -------------------- @@ -19,7 +20,7 @@ type RemoteStartTransactionRequest struct { // This field definition of the RemoteStartTransaction confirmation payload, sent by the Charge Point to the Central System in response to a RemoteStartTransactionRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type RemoteStartTransactionConfirmation struct { - Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus"` + Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus16"` } // Central System can request a Charge Point to start a transaction by sending a RemoteStartTransactionRequest. diff --git a/ocpp1.6/core/remote_stop_transaction.go b/ocpp1.6/core/remote_stop_transaction.go index 97809a24..ddf94c79 100644 --- a/ocpp1.6/core/remote_stop_transaction.go +++ b/ocpp1.6/core/remote_stop_transaction.go @@ -1,8 +1,9 @@ package core import ( - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // -------------------- Remote Stop Transaction (CS -> CP) -------------------- @@ -17,7 +18,7 @@ type RemoteStopTransactionRequest struct { // This field definition of the RemoteStopTransaction confirmation payload, sent by the Charge Point to the Central System in response to a RemoteStopTransactionRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type RemoteStopTransactionConfirmation struct { - Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus"` + Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus16"` } // Central System can request a Charge Point to stop a transaction by sending a RemoteStopTransactionRequest to Charge Point with the identifier of the transaction. diff --git a/ocpp1.6/smartcharging/clear_charging_profile.go b/ocpp1.6/smartcharging/clear_charging_profile.go index 2b9225c4..b280d7fd 100644 --- a/ocpp1.6/smartcharging/clear_charging_profile.go +++ b/ocpp1.6/smartcharging/clear_charging_profile.go @@ -1,9 +1,10 @@ package smartcharging import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Clear Charging Profile (CS -> CP) -------------------- @@ -32,7 +33,7 @@ func isValidClearChargingProfileStatus(fl validator.FieldLevel) bool { type ClearChargingProfileRequest struct { Id *int `json:"id,omitempty" validate:"omitempty"` ConnectorId *int `json:"connectorId,omitempty" validate:"omitempty,gte=0"` - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose16"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gte=0"` } diff --git a/ocpp1.6/smartcharging/get_composite_schedule.go b/ocpp1.6/smartcharging/get_composite_schedule.go index 0cb174f5..dab6a128 100644 --- a/ocpp1.6/smartcharging/get_composite_schedule.go +++ b/ocpp1.6/smartcharging/get_composite_schedule.go @@ -1,9 +1,10 @@ package smartcharging import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Get Composite Schedule (CS -> CP) -------------------- @@ -32,7 +33,7 @@ func isValidGetCompositeScheduleStatus(fl validator.FieldLevel) bool { type GetCompositeScheduleRequest struct { ConnectorId int `json:"connectorId" validate:"gte=0"` Duration int `json:"duration" validate:"gte=0"` - ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit"` + ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit16"` } // This field definition of the GetCompositeSchedule confirmation payload, sent by the Charge Point to the Central System in response to a GetCompositeScheduleRequest. diff --git a/ocpp1.6/types/types.go b/ocpp1.6/types/types.go index f0b1e44d..feac2e19 100644 --- a/ocpp1.6/types/types.go +++ b/ocpp1.6/types/types.go @@ -42,7 +42,7 @@ func isValidAuthorizationStatus(fl validator.FieldLevel) bool { type IdTagInfo struct { ExpiryDate *DateTime `json:"expiryDate,omitempty" validate:"omitempty"` ParentIdTag string `json:"parentIdTag,omitempty" validate:"omitempty,max=20"` - Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus"` + Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus16"` } func NewIdTagInfo(status AuthorizationStatus) *IdTagInfo { @@ -121,7 +121,7 @@ func NewChargingSchedulePeriod(startPeriod int, limit float64) ChargingScheduleP type ChargingSchedule struct { Duration *int `json:"duration,omitempty" validate:"omitempty,gte=0"` StartSchedule *DateTime `json:"startSchedule,omitempty"` - ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit"` + ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit16"` ChargingSchedulePeriod []ChargingSchedulePeriod `json:"chargingSchedulePeriod" validate:"required,min=1"` MinChargingRate *float64 `json:"minChargingRate,omitempty" validate:"omitempty,gte=0"` } @@ -134,9 +134,9 @@ type ChargingProfile struct { ChargingProfileId int `json:"chargingProfileId"` TransactionId int `json:"transactionId,omitempty"` StackLevel int `json:"stackLevel" validate:"gte=0"` - ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose"` - ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind"` - RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind"` + ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose16"` + ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind16"` + RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind16"` ValidFrom *DateTime `json:"validFrom,omitempty"` ValidTo *DateTime `json:"validTo,omitempty"` ChargingSchedule *ChargingSchedule `json:"chargingSchedule" validate:"required"` @@ -300,11 +300,11 @@ func isValidUnitOfMeasure(fl validator.FieldLevel) bool { type SampledValue struct { Value string `json:"value" validate:"required"` - Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext"` + Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext16"` Format ValueFormat `json:"format,omitempty" validate:"omitempty,valueFormat"` - Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand"` - Phase Phase `json:"phase,omitempty" validate:"omitempty,phase"` - Location Location `json:"location,omitempty" validate:"omitempty,location"` + Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand16"` + Phase Phase `json:"phase,omitempty" validate:"omitempty,phase16"` + Location Location `json:"location,omitempty" validate:"omitempty,location16"` Unit UnitOfMeasure `json:"unit,omitempty" validate:"omitempty,unitOfMeasure"` } @@ -317,16 +317,16 @@ type MeterValue struct { var Validate = ocppj.Validate func init() { - _ = Validate.RegisterValidation("authorizationStatus", isValidAuthorizationStatus) - _ = Validate.RegisterValidation("chargingProfilePurpose", isValidChargingProfilePurpose) - _ = Validate.RegisterValidation("chargingProfileKind", isValidChargingProfileKind) - _ = Validate.RegisterValidation("recurrencyKind", isValidRecurrencyKind) - _ = Validate.RegisterValidation("chargingRateUnit", isValidChargingRateUnit) - _ = Validate.RegisterValidation("remoteStartStopStatus", isValidRemoteStartStopStatus) - _ = Validate.RegisterValidation("readingContext", isValidReadingContext) + _ = Validate.RegisterValidation("authorizationStatus16", isValidAuthorizationStatus) + _ = Validate.RegisterValidation("chargingProfilePurpose16", isValidChargingProfilePurpose) + _ = Validate.RegisterValidation("chargingProfileKind16", isValidChargingProfileKind) + _ = Validate.RegisterValidation("recurrencyKind16", isValidRecurrencyKind) + _ = Validate.RegisterValidation("chargingRateUnit16", isValidChargingRateUnit) + _ = Validate.RegisterValidation("remoteStartStopStatus16", isValidRemoteStartStopStatus) + _ = Validate.RegisterValidation("readingContext16", isValidReadingContext) _ = Validate.RegisterValidation("valueFormat", isValidValueFormat) - _ = Validate.RegisterValidation("measurand", isValidMeasurand) - _ = Validate.RegisterValidation("phase", isValidPhase) - _ = Validate.RegisterValidation("location", isValidLocation) + _ = Validate.RegisterValidation("measurand16", isValidMeasurand) + _ = Validate.RegisterValidation("phase16", isValidPhase) + _ = Validate.RegisterValidation("location16", isValidLocation) _ = Validate.RegisterValidation("unitOfMeasure", isValidUnitOfMeasure) } diff --git a/ocpp2.0.1/smartcharging/clear_charging_profile.go b/ocpp2.0.1/smartcharging/clear_charging_profile.go index d12c4876..e3ce5941 100644 --- a/ocpp2.0.1/smartcharging/clear_charging_profile.go +++ b/ocpp2.0.1/smartcharging/clear_charging_profile.go @@ -22,7 +22,7 @@ const ( type ClearChargingProfileType struct { EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose201"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gt=0"` } diff --git a/ocpp2.0.1/smartcharging/get_charging_profiles.go b/ocpp2.0.1/smartcharging/get_charging_profiles.go index 8fa4c053..5f78e526 100644 --- a/ocpp2.0.1/smartcharging/get_charging_profiles.go +++ b/ocpp2.0.1/smartcharging/get_charging_profiles.go @@ -33,7 +33,7 @@ func isValidGetChargingProfileStatus(fl validator.FieldLevel) bool { // ChargingProfileCriterion specifies the charging profile within a GetChargingProfilesRequest. // A ChargingProfile consists of ChargingSchedule, describing the amount of power or current that can be delivered per time interval. type ChargingProfileCriterion struct { - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose201"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gte=0"` ChargingProfileID []int `json:"chargingProfileId,omitempty" validate:"omitempty"` // This field SHALL NOT contain more ids than set in ChargingProfileEntries.maxLimit ChargingLimitSource []types.ChargingLimitSourceType `json:"chargingLimitSource,omitempty" validate:"omitempty,max=4,dive,chargingLimitSource"` diff --git a/ocpp2.0.1/smartcharging/get_composite_schedule.go b/ocpp2.0.1/smartcharging/get_composite_schedule.go index 08574b67..cb6eb5a7 100644 --- a/ocpp2.0.1/smartcharging/get_composite_schedule.go +++ b/ocpp2.0.1/smartcharging/get_composite_schedule.go @@ -37,7 +37,7 @@ type CompositeSchedule struct { // The field definition of the GetCompositeSchedule request payload sent by the CSMS to the Charging System. type GetCompositeScheduleRequest struct { Duration int `json:"duration" validate:"gte=0"` - ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit"` + ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit201"` EvseID int `json:"evseId" validate:"gte=0"` } diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index daad62ab..77c37da6 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -247,7 +247,7 @@ type GroupIdToken struct { } type IdTokenInfo struct { - Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus"` + Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus201"` CacheExpiryDateTime *DateTime `json:"cacheExpiryDateTime,omitempty" validate:"omitempty"` ChargingPriority int `json:"chargingPriority,omitempty" validate:"min=-9,max=9"` Language1 string `json:"language1,omitempty" validate:"max=8"` @@ -479,7 +479,7 @@ type ChargingSchedule struct { ID int `json:"id" validate:"gte=0"` // Identifies the ChargingSchedule. StartSchedule *DateTime `json:"startSchedule,omitempty" validate:"omitempty"` Duration *int `json:"duration,omitempty" validate:"omitempty,gte=0"` - ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit"` + ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit201"` MinChargingRate *float64 `json:"minChargingRate,omitempty" validate:"omitempty,gte=0"` ChargingSchedulePeriod []ChargingSchedulePeriod `json:"chargingSchedulePeriod" validate:"required,min=1,max=1024"` SalesTariff *SalesTariff `json:"salesTariff,omitempty" validate:"omitempty"` // Sales tariff associated with this charging schedule. @@ -492,9 +492,9 @@ func NewChargingSchedule(id int, chargingRateUnit ChargingRateUnitType, schedule type ChargingProfile struct { ID int `json:"id" validate:"gte=0"` StackLevel int `json:"stackLevel" validate:"gte=0"` - ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose"` - ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind"` - RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind"` + ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose201"` + ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind201"` + RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind201"` ValidFrom *DateTime `json:"validFrom,omitempty"` ValidTo *DateTime `json:"validTo,omitempty"` TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` @@ -526,7 +526,6 @@ func isValidRemoteStartStopStatus(fl validator.FieldLevel) bool { // Meter Value type ReadingContext string -type ValueFormat string type Measurand string type Phase string type Location string @@ -681,13 +680,13 @@ type SignedMeterValue struct { } type SampledValue struct { - Value float64 `json:"value"` // Indicates the measured value. This value is required. - Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" - Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand"` // Type of measurement. Default = "Energy.Active.Import.Register" - Phase Phase `json:"phase,omitempty" validate:"omitempty,phase"` // Indicates how the measured value is to be interpreted. For instance between L1 and neutral (L1-N) Please note that not all values of phase are applicable to all Measurands. When phase is absent, the measured value is interpreted as an overall value. - Location Location `json:"location,omitempty" validate:"omitempty,location"` // Indicates where the measured value has been sampled. - SignedMeterValue *SignedMeterValue `json:"signedMeterValue,omitempty" validate:"omitempty"` // Contains the MeterValueSignature with sign/encoding method information. - UnitOfMeasure *UnitOfMeasure `json:"unitOfMeasure,omitempty" validate:"omitempty"` // Represents a UnitOfMeasure including a multiplier. + Value float64 `json:"value"` // Indicates the measured value. This value is required. + Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext201"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" + Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand201"` // Type of measurement. Default = "Energy.Active.Import.Register" + Phase Phase `json:"phase,omitempty" validate:"omitempty,phase201"` // Indicates how the measured value is to be interpreted. For instance between L1 and neutral (L1-N) Please note that not all values of phase are applicable to all Measurands. When phase is absent, the measured value is interpreted as an overall value. + Location Location `json:"location,omitempty" validate:"omitempty,location201"` // Indicates where the measured value has been sampled. + SignedMeterValue *SignedMeterValue `json:"signedMeterValue,omitempty" validate:"omitempty"` // Contains the MeterValueSignature with sign/encoding method information. + UnitOfMeasure *UnitOfMeasure `json:"unitOfMeasure,omitempty" validate:"omitempty"` // Represents a UnitOfMeasure including a multiplier. } type MeterValue struct { @@ -705,18 +704,18 @@ func init() { _ = Validate.RegisterValidation("genericStatus", isValidGenericStatus) _ = Validate.RegisterValidation("hashAlgorithm", isValidHashAlgorithmType) _ = Validate.RegisterValidation("messageFormat", isValidMessageFormatType) - _ = Validate.RegisterValidation("authorizationStatus", isValidAuthorizationStatus) + _ = Validate.RegisterValidation("authorizationStatus201", isValidAuthorizationStatus) _ = Validate.RegisterValidation("attribute", isValidAttribute) - _ = Validate.RegisterValidation("chargingProfilePurpose", isValidChargingProfilePurpose) - _ = Validate.RegisterValidation("chargingProfileKind", isValidChargingProfileKind) - _ = Validate.RegisterValidation("recurrencyKind", isValidRecurrencyKind) - _ = Validate.RegisterValidation("chargingRateUnit", isValidChargingRateUnit) + _ = Validate.RegisterValidation("chargingProfilePurpose201", isValidChargingProfilePurpose) + _ = Validate.RegisterValidation("chargingProfileKind201", isValidChargingProfileKind) + _ = Validate.RegisterValidation("recurrencyKind201", isValidRecurrencyKind) + _ = Validate.RegisterValidation("chargingRateUnit201", isValidChargingRateUnit) _ = Validate.RegisterValidation("chargingLimitSource", isValidChargingLimitSource) - _ = Validate.RegisterValidation("remoteStartStopStatus", isValidRemoteStartStopStatus) - _ = Validate.RegisterValidation("readingContext", isValidReadingContext) - _ = Validate.RegisterValidation("measurand", isValidMeasurand) - _ = Validate.RegisterValidation("phase", isValidPhase) - _ = Validate.RegisterValidation("location", isValidLocation) + _ = Validate.RegisterValidation("remoteStartStopStatus201", isValidRemoteStartStopStatus) + _ = Validate.RegisterValidation("readingContext201", isValidReadingContext) + _ = Validate.RegisterValidation("measurand201", isValidMeasurand) + _ = Validate.RegisterValidation("phase201", isValidPhase) + _ = Validate.RegisterValidation("location201", isValidLocation) _ = Validate.RegisterValidation("signatureMethod", isValidSignatureMethod) _ = Validate.RegisterValidation("encodingMethod", isValidEncodingMethod) _ = Validate.RegisterValidation("certificateSigningUse", isValidCertificateSigningUse) diff --git a/ocpp2.0.1_test/boot_notification_test.go b/ocpp2.0.1_test/boot_notification_test.go index f710c4d1..d342a625 100644 --- a/ocpp2.0.1_test/boot_notification_test.go +++ b/ocpp2.0.1_test/boot_notification_test.go @@ -63,7 +63,6 @@ func (suite *OcppV2TestSuite) TestBootNotificationE2EMocked() { currentTime := types.NewDateTime(time.Now()) requestJson := fmt.Sprintf(`[2,"%v","%v",{"reason":"%v","chargingStation":{"model":"%v","vendorName":"%v"}}]`, messageId, provisioning.BootNotificationFeatureName, reason, chargePointModel, chargePointVendor) responseJson := fmt.Sprintf(`[3,"%v",{"currentTime":"%v","interval":%v,"status":"%v"}]`, messageId, currentTime.FormatTimestamp(), interval, registrationStatus) - fmt.Println(responseJson) bootNotificationConfirmation := provisioning.NewBootNotificationResponse(currentTime, interval, registrationStatus) channel := NewMockWebSocket(wsId) From 694d9853eb633f657e8766e17f4dc5256e082f89 Mon Sep 17 00:00:00 2001 From: Dwi BudUt Date: Thu, 29 Jun 2023 12:37:44 +0700 Subject: [PATCH 36/47] fix certificateType on CertificateSignedRequest & GetInstalledCertificateIds request-response (base on json schemas) --- ocpp2.0.1/csms.go | 4 +- .../iso15118/get_installed_certificate_ids.go | 11 ++-- ocpp2.0.1/security/certificate_signed.go | 2 +- ocpp2.0.1/types/types.go | 10 ++- ocpp2.0.1/v2.go | 2 +- ocpp2.0.1_test/certificate_signed_test.go | 4 +- .../get_installed_certificate_ids_test.go | 61 ++++++++++--------- 7 files changed, 53 insertions(+), 41 deletions(-) diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index 8e8dea67..9d5fc043 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -297,8 +297,8 @@ func (cs *csms) GetDisplayMessages(clientId string, callback func(*display.GetDi return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), typeOfCertificate types.CertificateUse, props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error { - request := iso15118.NewGetInstalledCertificateIdsRequest(typeOfCertificate) +func (cs *csms) GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error { + request := iso15118.NewGetInstalledCertificateIdsRequest() for _, fn := range props { fn(request) } diff --git a/ocpp2.0.1/iso15118/get_installed_certificate_ids.go b/ocpp2.0.1/iso15118/get_installed_certificate_ids.go index 7f8a549a..58271d02 100644 --- a/ocpp2.0.1/iso15118/get_installed_certificate_ids.go +++ b/ocpp2.0.1/iso15118/get_installed_certificate_ids.go @@ -31,13 +31,14 @@ func isValidGetInstalledCertificateStatus(fl validator.FieldLevel) bool { // The field definition of the GetInstalledCertificateIdsRequest PDU sent by the CSMS to the Charging Station. type GetInstalledCertificateIdsRequest struct { - TypeOfCertificate types.CertificateUse `json:"typeOfCertificate" validate:"required,certificateUse"` + CertificateTypes []types.CertificateUse `json:"certificateType" validate:"omitempty,dive,certificateUse"` } // The field definition of the GetInstalledCertificateIds response payload sent by the Charging Station to the CSMS in response to a GetInstalledCertificateIdsRequest. type GetInstalledCertificateIdsResponse struct { - Status GetInstalledCertificateStatus `json:"status" validate:"required,getInstalledCertificateStatus"` - CertificateHashData []types.CertificateHashData `json:"certificateHashData,omitempty" validate:"omitempty,dive"` + Status GetInstalledCertificateStatus `json:"status" validate:"required,getInstalledCertificateStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` + CertificateHashDataChain []types.CertificateHashDataChain `json:"certificateHashDataChain,omitempty" validate:"omitempty,dive"` } // To facilitate the management of the Charging Station’s installed certificates, a method of retrieving the installed certificates is provided. @@ -66,8 +67,8 @@ func (c GetInstalledCertificateIdsResponse) GetFeatureName() string { } // Creates a new GetInstalledCertificateIdsRequest, containing all required fields. There are no optional fields for this message. -func NewGetInstalledCertificateIdsRequest(typeOfCertificate types.CertificateUse) *GetInstalledCertificateIdsRequest { - return &GetInstalledCertificateIdsRequest{TypeOfCertificate: typeOfCertificate} +func NewGetInstalledCertificateIdsRequest() *GetInstalledCertificateIdsRequest { + return &GetInstalledCertificateIdsRequest{} } // Creates a new NewGetInstalledCertificateIdsResponse, containing all required fields. Additional optional fields may be set afterwards. diff --git a/ocpp2.0.1/security/certificate_signed.go b/ocpp2.0.1/security/certificate_signed.go index 9c84d82b..7bf62a79 100644 --- a/ocpp2.0.1/security/certificate_signed.go +++ b/ocpp2.0.1/security/certificate_signed.go @@ -33,7 +33,7 @@ func isValidCertificateSignedStatus(fl validator.FieldLevel) bool { // The field definition of the CertificateSignedRequest PDU sent by the CSMS to the Charging Station. type CertificateSignedRequest struct { CertificateChain string `json:"certificateChain" validate:"required,max=10000"` - TypeOfCertificate types.CertificateSigningUse `json:"typeOfCertificate,omitempty" validate:"omitempty,certificateSigningUse"` + TypeOfCertificate types.CertificateSigningUse `json:"certificateType,omitempty" validate:"omitempty,certificateSigningUse"` } // The field definition of the CertificateSignedResponse payload sent by the Charging Station to the CSMS in response to a CertificateSignedRequest. diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index 77c37da6..936ee55e 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -154,6 +154,13 @@ type CertificateHashData struct { SerialNumber string `json:"serialNumber" validate:"required,max=20"` } +// CertificateHashDataChain +type CertificateHashDataChain struct { + CertificateType CertificateUse `json:"certificateType" validate:"required,certificateUse"` + CertificateHashData CertificateHashData `json:"certificateHashData" validate:"required"` + ChildCertificateHashData []CertificateHashData `json:"childCertificateHashData,omitempty" validate:"omitempty,dive"` +} + // Certificate15118EVStatus type Certificate15118EVStatus string @@ -202,13 +209,14 @@ const ( CSOSubCA1 CertificateUse = "CSOSubCA1" CSOSubCA2 CertificateUse = "CSOSubCA2" CSMSRootCertificate CertificateUse = "CSMSRootCertificate" + V2GCertificateChain CertificateUse = "V2GCertificateChain" ManufacturerRootCertificate CertificateUse = "ManufacturerRootCertificate" ) func isValidCertificateUse(fl validator.FieldLevel) bool { use := CertificateUse(fl.Field().String()) switch use { - case V2GRootCertificate, MORootCertificate, CSOSubCA1, CSOSubCA2, CSMSRootCertificate, ManufacturerRootCertificate: + case V2GRootCertificate, MORootCertificate, CSOSubCA1, CSOSubCA2, CSMSRootCertificate, V2GCertificateChain, ManufacturerRootCertificate: return true default: return false diff --git a/ocpp2.0.1/v2.go b/ocpp2.0.1/v2.go index abb3b6b3..8627d8ee 100644 --- a/ocpp2.0.1/v2.go +++ b/ocpp2.0.1/v2.go @@ -284,7 +284,7 @@ type CSMS interface { // Retrieves all messages currently configured on a charging station. GetDisplayMessages(clientId string, callback func(*display.GetDisplayMessagesResponse, error), requestId int, props ...func(*display.GetDisplayMessagesRequest)) error // Retrieves all installed certificates on a charging station. - GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), typeOfCertificate types.CertificateUse, props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error + GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error // Queries a charging station for version number of the Local Authorization List. GetLocalListVersion(clientId string, callback func(*localauth.GetLocalListVersionResponse, error), props ...func(*localauth.GetLocalListVersionRequest)) error // Instructs a charging station to upload a diagnostics or security logfile to the CSMS. diff --git a/ocpp2.0.1_test/certificate_signed_test.go b/ocpp2.0.1_test/certificate_signed_test.go index 573aaa15..adea66b7 100644 --- a/ocpp2.0.1_test/certificate_signed_test.go +++ b/ocpp2.0.1_test/certificate_signed_test.go @@ -46,7 +46,7 @@ func (suite *OcppV2TestSuite) TestCertificateSignedE2EMocked() { certificateChain := "someX509CertificateChain" certificateType := types.ChargingStationCert status := security.CertificateSignedStatusAccepted - requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","typeOfCertificate":"%v"}]`, + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","certificateType":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificateChain, certificateType) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v"}]`, messageId, status) certificateSignedConfirmation := security.NewCertificateSignedResponse(status) @@ -85,6 +85,6 @@ func (suite *OcppV2TestSuite) TestCertificateSignedInvalidEndpoint() { certificateType := types.ChargingStationCert certificateSignedRequest := security.NewCertificateSignedRequest(certificate) certificateSignedRequest.TypeOfCertificate = certificateType - requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","typeOfCertificate":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificate, certificateType) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","certificateType":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificate, certificateType) testUnsupportedRequestFromChargingStation(suite, certificateSignedRequest, requestJson, messageId) } diff --git a/ocpp2.0.1_test/get_installed_certificate_ids_test.go b/ocpp2.0.1_test/get_installed_certificate_ids_test.go index c419f728..206cf0b1 100644 --- a/ocpp2.0.1_test/get_installed_certificate_ids_test.go +++ b/ocpp2.0.1_test/get_installed_certificate_ids_test.go @@ -14,14 +14,14 @@ import ( func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsRequestValidation() { t := suite.T() var testTable = []GenericTestEntry{ - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.V2GRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.MORootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSOSubCA1}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSOSubCA2}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSMSRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.ManufacturerRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{}, false}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: "invalidCertificateUse"}, false}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.V2GRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.MORootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSOSubCA1}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSOSubCA2}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSMSRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.ManufacturerRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{"invalidCertificateUse"}}, false}, } ExecuteGenericTestTable(t, testTable) } @@ -29,13 +29,13 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsRequestValidation() func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsConfirmationValidation() { t := suite.T() var testTable = []GenericTestEntry{ - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, true}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusNotFound, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, true}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusNotFound, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{}}, true}, {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted}, true}, {iso15118.GetInstalledCertificateIdsResponse{}, false}, {iso15118.GetInstalledCertificateIdsResponse{Status: "invalidGetInstalledCertificateStatus"}, false}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: "invalidHashAlgorithm", IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, false}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: "invalidHashAlgorithm", IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, false}, } ExecuteGenericTestTable(t, testTable) } @@ -46,16 +46,16 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { wsId := "test_id" messageId := defaultMessageId wsUrl := "someUrl" - certificateType := types.CSMSRootCertificate + certificateTypes := []types.CertificateUse{types.CSMSRootCertificate} status := iso15118.GetInstalledCertificateStatusAccepted - certificateHashData := []types.CertificateHashData{ - {HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}, + certificateHashDataChain := []types.CertificateHashDataChain{ + {CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"typeOfCertificate":"%v"}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateType) - responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","certificateHashData":[{"hashAlgorithm":"%v","issuerNameHash":"%v","issuerKeyHash":"%v","serialNumber":"%v"}]}]`, - messageId, status, certificateHashData[0].HashAlgorithm, certificateHashData[0].IssuerNameHash, certificateHashData[0].IssuerKeyHash, certificateHashData[0].SerialNumber) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateType":["%v"]}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateTypes[0]) + responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","certificateHashDataChain":[{"certificateType":"%v","certificateHashData":{"hashAlgorithm":"%v","issuerNameHash":"%v","issuerKeyHash":"%v","serialNumber":"%v"}}]}]`, + messageId, status, certificateHashDataChain[0].CertificateType, certificateHashDataChain[0].CertificateHashData.HashAlgorithm, certificateHashDataChain[0].CertificateHashData.IssuerNameHash, certificateHashDataChain[0].CertificateHashData.IssuerKeyHash, certificateHashDataChain[0].CertificateHashData.SerialNumber) getInstalledCertificateIdsConfirmation := iso15118.NewGetInstalledCertificateIdsResponse(status) - getInstalledCertificateIdsConfirmation.CertificateHashData = certificateHashData + getInstalledCertificateIdsConfirmation.CertificateHashDataChain = certificateHashDataChain channel := NewMockWebSocket(wsId) // Setting handlers handler := &MockChargingStationIso15118Handler{} @@ -63,7 +63,7 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { request, ok := args.Get(0).(*iso15118.GetInstalledCertificateIdsRequest) require.True(t, ok) require.NotNil(t, request) - assert.Equal(t, certificateType, request.TypeOfCertificate) + assert.Equal(t, certificateTypes, request.CertificateTypes) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -76,13 +76,15 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { require.Nil(t, err) require.NotNil(t, confirmation) assert.Equal(t, status, confirmation.Status) - require.Len(t, confirmation.CertificateHashData, len(certificateHashData)) - assert.Equal(t, certificateHashData[0].HashAlgorithm, confirmation.CertificateHashData[0].HashAlgorithm) - assert.Equal(t, certificateHashData[0].IssuerNameHash, confirmation.CertificateHashData[0].IssuerNameHash) - assert.Equal(t, certificateHashData[0].IssuerKeyHash, confirmation.CertificateHashData[0].IssuerKeyHash) - assert.Equal(t, certificateHashData[0].SerialNumber, confirmation.CertificateHashData[0].SerialNumber) + require.Len(t, confirmation.CertificateHashDataChain, len(certificateHashDataChain)) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.HashAlgorithm, confirmation.CertificateHashDataChain[0].CertificateHashData.HashAlgorithm) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.IssuerNameHash, confirmation.CertificateHashDataChain[0].CertificateHashData.IssuerNameHash) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.IssuerKeyHash, confirmation.CertificateHashDataChain[0].CertificateHashData.IssuerKeyHash) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.SerialNumber, confirmation.CertificateHashDataChain[0].CertificateHashData.SerialNumber) resultChannel <- true - }, certificateType) + }, func(request *iso15118.GetInstalledCertificateIdsRequest) { + request.CertificateTypes = certificateTypes + }) require.Nil(t, err) result := <-resultChannel assert.True(t, result) @@ -90,8 +92,9 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsInvalidEndpoint() { messageId := defaultMessageId - certificateType := types.CSMSRootCertificate - GetInstalledCertificateIdsRequest := iso15118.NewGetInstalledCertificateIdsRequest(certificateType) - requestJson := fmt.Sprintf(`[2,"%v","%v",{"typeOfCertificate":"%v"}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateType) + certificateTypes := []types.CertificateUse{types.CSMSRootCertificate} + GetInstalledCertificateIdsRequest := iso15118.NewGetInstalledCertificateIdsRequest() + GetInstalledCertificateIdsRequest.CertificateTypes = certificateTypes + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateType":["%v"]}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateTypes[0]) testUnsupportedRequestFromChargingStation(suite, GetInstalledCertificateIdsRequest, requestJson, messageId) } From 69eec62ce7afd131fccdd9a09120a83cdb0bd8e0 Mon Sep 17 00:00:00 2001 From: Dwi BudUt Date: Thu, 29 Jun 2023 12:59:57 +0700 Subject: [PATCH 37/47] add another FirmwareStatus type on FirmwareStatusNotification (support in ocpp2.0.1) --- .../firmware/firmware_status_notification.go | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ocpp2.0.1/firmware/firmware_status_notification.go b/ocpp2.0.1/firmware/firmware_status_notification.go index 5fbbca72..319b7d3b 100644 --- a/ocpp2.0.1/firmware/firmware_status_notification.go +++ b/ocpp2.0.1/firmware/firmware_status_notification.go @@ -16,19 +16,26 @@ const FirmwareStatusNotificationFeatureName = "FirmwareStatusNotification" type FirmwareStatus string const ( - FirmwareStatusDownloaded FirmwareStatus = "Downloaded" - FirmwareStatusDownloadFailed FirmwareStatus = "DownloadFailed" - FirmwareStatusDownloading FirmwareStatus = "Downloading" - FirmwareStatusIdle FirmwareStatus = "Idle" - FirmwareStatusInstallationFailed FirmwareStatus = "InstallationFailed" - FirmwareStatusInstalling FirmwareStatus = "Installing" - FirmwareStatusInstalled FirmwareStatus = "Installed" + FirmwareStatusDownloaded FirmwareStatus = "Downloaded" + FirmwareStatusDownloadFailed FirmwareStatus = "DownloadFailed" + FirmwareStatusDownloading FirmwareStatus = "Downloading" + FirmwareStatusDownloadScheduled FirmwareStatus = "DownloadScheduled" + FirmwareStatusDownloadPaused FirmwareStatus = "DownloadPaused" + FirmwareStatusIdle FirmwareStatus = "Idle" + FirmwareStatusInstallationFailed FirmwareStatus = "InstallationFailed" + FirmwareStatusInstalling FirmwareStatus = "Installing" + FirmwareStatusInstalled FirmwareStatus = "Installed" + FirmwareStatusInstallRebooting FirmwareStatus = "InstallRebooting" + FirmwareStatusInstallScheduled FirmwareStatus = "InstallScheduled" + FirmwareStatusInstallVerificationFailed FirmwareStatus = "InstallVerificationFailed" + FirmwareStatusInvalidSignature FirmwareStatus = "InvalidSignature" + FirmwareStatusSignatureVerified FirmwareStatus = "SignatureVerified" ) func isValidFirmwareStatus(fl validator.FieldLevel) bool { status := FirmwareStatus(fl.Field().String()) switch status { - case FirmwareStatusDownloaded, FirmwareStatusDownloadFailed, FirmwareStatusDownloading, FirmwareStatusIdle, FirmwareStatusInstallationFailed, FirmwareStatusInstalling, FirmwareStatusInstalled: + case FirmwareStatusDownloaded, FirmwareStatusDownloadFailed, FirmwareStatusDownloading, FirmwareStatusDownloadScheduled, FirmwareStatusDownloadPaused, FirmwareStatusIdle, FirmwareStatusInstallationFailed, FirmwareStatusInstalling, FirmwareStatusInstalled, FirmwareStatusInstallRebooting, FirmwareStatusInstallScheduled, FirmwareStatusInstallVerificationFailed, FirmwareStatusInvalidSignature, FirmwareStatusSignatureVerified: return true default: return false From 118355fe21d3f58ac888e07f579bc2f4fe7053dd Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Mon, 3 Jul 2023 21:38:16 +0200 Subject: [PATCH 38/47] Fix deadlock on double client stop Signed-off-by: Lorenzo --- ocppj/charge_point_test.go | 34 ++++++++++++++++++++++++++++++++++ ocppj/client.go | 15 +++++++++++---- ws/websocket.go | 5 +++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 603e9d95..8b6a1b0b 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -55,12 +55,14 @@ func (suite *OcppJTestSuite) TestClientStoppedError() { // Simulate websocket internal working suite.mockClient.DisconnectedHandler(nil) }) + call := suite.mockClient.On("IsConnected").Return(true) err := suite.chargePoint.Start("someUrl") require.NoError(t, err) // Stop client suite.chargePoint.Stop() // Send message. Expected error time.Sleep(20 * time.Millisecond) + call.Return(false) assert.False(t, suite.clientDispatcher.IsRunning()) req := newMockRequest("somevalue") err = suite.chargePoint.SendRequest(req) @@ -735,3 +737,35 @@ func (suite *OcppJTestSuite) TestClientResponseTimeout() { assert.True(t, suite.clientDispatcher.IsRunning()) assert.False(t, state.HasPendingRequest()) } + +func (suite *OcppJTestSuite) TestStopDisconnectedClient() { + t := suite.T() + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + suite.mockClient.On("Write", mock.Anything).Return(nil) + suite.mockClient.On("Stop").Return(nil) + call := suite.mockClient.On("IsConnected").Return(true) + // Start normally + err := suite.chargePoint.Start("someUrl") + require.NoError(t, err) + // Trigger network disconnect + disconnectError := fmt.Errorf("some error") + suite.chargePoint.SetOnDisconnectedHandler(func(err error) { + require.Errorf(t, err, disconnectError.Error()) + }) + call.Return(false) + suite.mockClient.DisconnectedHandler(disconnectError) + time.Sleep(100 * time.Millisecond) + // Dispatcher should be paused + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) + // Stop client while reconnecting + suite.chargePoint.Stop() + time.Sleep(50 * time.Millisecond) + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) + // Attempt stopping client again + suite.chargePoint.Stop() + time.Sleep(50 * time.Millisecond) + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) +} diff --git a/ocppj/client.go b/ocppj/client.go index ea5c200b..1f5acc08 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -29,6 +29,7 @@ type Client struct { // a state handler and a list of supported profiles (optional). // // You may create a simple new server by using these default values: +// // s := ocppj.NewClient(ws.NewClient(), nil, nil) // // The wsClient parameter cannot be nil. Refer to the ws package for information on how to create and @@ -108,11 +109,17 @@ func (c *Client) Start(serverURL string) error { func (c *Client) Stop() { // Overwrite handler to intercept disconnected signal cleanupC := make(chan struct{}, 1) - c.client.SetDisconnectedHandler(func(err error) { - cleanupC <- struct{}{} - }) + if c.IsConnected() { + c.client.SetDisconnectedHandler(func(err error) { + cleanupC <- struct{}{} + }) + } else { + close(cleanupC) + } c.client.Stop() - c.dispatcher.Stop() + if c.dispatcher.IsRunning() { + c.dispatcher.Stop() + } // Wait for websocket to be cleaned up <-cleanupC } diff --git a/ws/websocket.go b/ws/websocket.go index d9862b85..3ef66e0c 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -1086,13 +1086,14 @@ func (client *Client) Stop() { } client.mutex.Unlock() // Notify reconnection goroutine to stop (if any) - close(client.reconnectC) + if client.reconnectC != nil { + close(client.reconnectC) + } if client.errC != nil { close(client.errC) client.errC = nil } // Wait for connection to actually close - } func (client *Client) error(err error) { From 290c89e216888735d79e7ba9b6b1ab07ffcc0f89 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Tue, 4 Jul 2023 22:32:47 +0200 Subject: [PATCH 39/47] Override FormatViolation error code for v2.0.1 Signed-off-by: Lorenzo --- ocpp1.6/central_system.go | 3 +++ ocpp1.6/charge_point.go | 5 ++++- ocpp2.0.1/charging_station.go | 5 ++++- ocpp2.0.1/csms.go | 3 +++ ocppj/ocppj.go | 7 ++++++- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index ad4d949b..8a22c427 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -404,6 +404,9 @@ func (cs *centralSystem) SendRequestAsync(clientId string, request ocpp.Request, } func (cs *centralSystem) Start(listenPort int, listenPath string) { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV16 + // Start server cs.server.Start(listenPort, listenPath) } diff --git a/ocpp1.6/charge_point.go b/ocpp1.6/charge_point.go index 0d853cbf..16d87628 100644 --- a/ocpp1.6/charge_point.go +++ b/ocpp1.6/charge_point.go @@ -329,9 +329,12 @@ func (cp *chargePoint) sendResponse(confirmation ocpp.Response, err error, reque } func (cp *chargePoint) Start(centralSystemUrl string) error { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV16 + // Start client cp.stopC = make(chan struct{}, 1) - // Async response handler receives incoming responses/errors and triggers callbacks err := cp.client.Start(centralSystemUrl) + // Async response handler receives incoming responses/errors and triggers callbacks if err == nil { go cp.asyncCallbackHandler() } diff --git a/ocpp2.0.1/charging_station.go b/ocpp2.0.1/charging_station.go index ae0bd7b9..7ad905bc 100644 --- a/ocpp2.0.1/charging_station.go +++ b/ocpp2.0.1/charging_station.go @@ -589,9 +589,12 @@ func (cs *chargingStation) sendResponse(response ocpp.Response, err error, reque } func (cs *chargingStation) Start(csmsUrl string) error { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV2 + // Start client cs.stopC = make(chan struct{}, 1) - // Async response handler receives incoming responses/errors and triggers callbacks err := cs.client.Start(csmsUrl) + // Async response handler receives incoming responses/errors and triggers callbacks if err == nil { go cs.asyncCallbackHandler() } diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index 9d5fc043..32a14af0 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -810,6 +810,9 @@ func (cs *csms) SendRequestAsync(clientId string, request ocpp.Request, callback } func (cs *csms) Start(listenPort int, listenPath string) { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV2 + // Start server cs.server.Start(listenPort, listenPath) } diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 33655766..9b0d43a9 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -190,11 +190,16 @@ const ( MessageTypeNotSupported ocpp.ErrorCode = "MessageTypeNotSupported" // A message with an Message Type Number received that is not supported by this implementation. ProtocolError ocpp.ErrorCode = "ProtocolError" // Payload for Action is incomplete. SecurityError ocpp.ErrorCode = "SecurityError" // During the processing of Action a security issue occurred preventing receiver from completing the Action successfully. - FormationViolation ocpp.ErrorCode = "FormationViolation" // Payload for Action is syntactically incorrect or not conform the PDU structure for Action. PropertyConstraintViolation ocpp.ErrorCode = "PropertyConstraintViolation" // Payload is syntactically correct but at least one field contains an invalid value. OccurrenceConstraintViolation ocpp.ErrorCode = "OccurrenceConstraintViolation" // Payload for Action is syntactically correct but at least one of the fields violates occurrence constraints. TypeConstraintViolation ocpp.ErrorCode = "TypeConstraintViolation" // Payload for Action is syntactically correct but at least one of the fields violates data type constraints (e.g. “somestring”: 12). GenericError ocpp.ErrorCode = "GenericError" // Any other error not covered by the previous ones. + FormatViolationV2 ocpp.ErrorCode = "FormatViolation" // Payload for Action is syntactically incorrect. This is only valid for OCPP 2.0.1 + FormatViolationV16 ocpp.ErrorCode = "FormationViolation" // Payload for Action is syntactically incorrect or not conform the PDU structure for Action. This is only valid for OCPP 1.6 +) + +var ( + FormationViolation = FormatViolationV16 // Used as constant, but can be overwritten depending on protocol version. Sett FormatViolationV16 and FormatViolationV2. ) func IsErrorCodeValid(fl validator.FieldLevel) bool { From 2fef29625bb4b307387138b3b8944dadb12589a4 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sat, 8 Jul 2023 16:06:12 +0200 Subject: [PATCH 40/47] Add tests for FormatViolation value Signed-off-by: Lorenzo --- ocpp1.6_test/proto_test.go | 7 +++++++ ocpp2.0.1_test/proto_test.go | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/ocpp1.6_test/proto_test.go b/ocpp1.6_test/proto_test.go index 517dc7fc..39815663 100644 --- a/ocpp1.6_test/proto_test.go +++ b/ocpp1.6_test/proto_test.go @@ -152,3 +152,10 @@ func (suite *OcppV16TestSuite) TestCentralSystemSendResponseError() { assert.Equal(t, ocppj.GenericError, ocppErr.Code) assert.Equal(t, fmt.Sprintf("empty confirmation to %s for request 1234", wsId), ocppErr.Description) } + +func (suite *OcppV16TestSuite) TestErrorCodes() { + t := suite.T() + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.centralSystem.Start(8887, "somePath") + assert.Equal(t, ocppj.FormatViolationV16, ocppj.FormationViolation) +} diff --git a/ocpp2.0.1_test/proto_test.go b/ocpp2.0.1_test/proto_test.go index 846aa838..95b3af2d 100644 --- a/ocpp2.0.1_test/proto_test.go +++ b/ocpp2.0.1_test/proto_test.go @@ -157,3 +157,10 @@ func (suite *OcppV2TestSuite) TestCentralSystemSendResponseError() { assert.Equal(t, ocppj.GenericError, ocppErr.Code) assert.Equal(t, fmt.Sprintf("empty response to %s for request 1234", wsId), ocppErr.Description) } + +func (suite *OcppV2TestSuite) TestErrorCodes() { + t := suite.T() + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.csms.Start(8887, "somePath") + assert.Equal(t, ocppj.FormatViolationV2, ocppj.FormationViolation) +} From 3760b411599e32abb63b5a838f4bb3bf3a209705 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 30 Jul 2023 17:49:40 +0200 Subject: [PATCH 41/47] Add hook for custom handling of invalid messages Signed-off-by: Lorenzo --- ocppj/central_system_test.go | 61 +++++++++++++++++++++++++++++++++++- ocppj/charge_point_test.go | 55 ++++++++++++++++++++++++++++++++ ocppj/client.go | 31 ++++++++++++++++++ ocppj/server.go | 36 ++++++++++++++++++++- 4 files changed, 181 insertions(+), 2 deletions(-) diff --git a/ocppj/central_system_test.go b/ocppj/central_system_test.go index e925e960..5d6ec7ed 100644 --- a/ocppj/central_system_test.go +++ b/ocppj/central_system_test.go @@ -92,7 +92,7 @@ func (suite *OcppJTestSuite) TestCentralSystemSendRequestNoValidation() { assert.Nil(suite.T(), err) } -func (suite *OcppJTestSuite) TestServerSendInvalidJsonRequest() { +func (suite *OcppJTestSuite) TestCentralSystemSendInvalidJsonRequest() { mockChargePointId := "1234" suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) suite.mockServer.On("Write", mockChargePointId, mock.Anything).Return(nil) @@ -105,6 +105,65 @@ func (suite *OcppJTestSuite) TestServerSendInvalidJsonRequest() { assert.IsType(suite.T(), &json.UnsupportedTypeError{}, err) } +func (suite *OcppJTestSuite) TestCentralSystemInvalidMessageHook() { + t := suite.T() + mockChargePointId := "1234" + mockChargePoint := NewMockWebSocket(mockChargePointId) + // Prepare invalid payload + mockID := "1234" + mockPayload := map[string]interface{}{ + "mockValue": float64(1234), + } + serializedPayload, err := json.Marshal(mockPayload) + require.NoError(t, err) + invalidMessage := fmt.Sprintf("[2,\"%v\",\"%s\",%v]", mockID, MockFeatureName, string(serializedPayload)) + expectedError := fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockID, ocppj.FormationViolation, "json: cannot unmarshal number into Go struct field MockRequest.mockValue of type string") + writeHook := suite.mockServer.On("Write", mockChargePointId, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data := args.Get(1).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + // Setup hook 1 + suite.centralSystem.SetInvalidMessageHook(func(client ws.Channel, err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + assert.Equal(t, mockChargePoint.ID(), client.ID()) + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return nil + }) + suite.centralSystem.Start(8887, "/{ws}") + // Trigger incoming invalid CALL + err = suite.mockServer.MessageHandler(mockChargePoint, []byte(invalidMessage)) + ocppErr, ok := err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, ocppj.FormationViolation, ocppErr.Code) + // Setup hook 2 + mockError := ocpp.NewError(ocppj.InternalError, "custom error", mockID) + expectedError = fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockError.MessageId, mockError.Code, mockError.Description) + writeHook.Run(func(args mock.Arguments) { + data := args.Get(1).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.centralSystem.SetInvalidMessageHook(func(client ws.Channel, err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + assert.Equal(t, mockChargePoint.ID(), client.ID()) + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return mockError + }) + // Trigger incoming invalid CALL that returns custom error + err = suite.mockServer.MessageHandler(mockChargePoint, []byte(invalidMessage)) + ocppErr, ok = err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, mockError.Code, ocppErr.Code) + assert.Equal(t, mockError.Description, ocppErr.Description) + assert.Equal(t, mockError.MessageId, ocppErr.MessageId) +} + func (suite *OcppJTestSuite) TestServerSendInvalidCall() { mockChargePointId := "1234" suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 8b6a1b0b..e0da3a16 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -112,6 +112,61 @@ func (suite *OcppJTestSuite) TestChargePointSendInvalidJsonRequest() { assert.IsType(suite.T(), &json.UnsupportedTypeError{}, err) } +func (suite *OcppJTestSuite) TestChargePointInvalidMessageHook() { + t := suite.T() + // Prepare invalid payload + mockID := "1234" + mockPayload := map[string]interface{}{ + "mockValue": float64(1234), + } + serializedPayload, err := json.Marshal(mockPayload) + require.NoError(t, err) + invalidMessage := fmt.Sprintf("[2,\"%v\",\"%s\",%v]", mockID, MockFeatureName, string(serializedPayload)) + expectedError := fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockID, ocppj.FormationViolation, "json: cannot unmarshal number into Go struct field MockRequest.mockValue of type string") + writeHook := suite.mockClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data := args.Get(0).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + // Setup hook 1 + suite.chargePoint.SetInvalidMessageHook(func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return nil + }) + _ = suite.chargePoint.Start("someUrl") + // Trigger incoming invalid CALL + err = suite.mockClient.MessageHandler([]byte(invalidMessage)) + ocppErr, ok := err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, ocppj.FormationViolation, ocppErr.Code) + // Setup hook 2 + mockError := ocpp.NewError(ocppj.InternalError, "custom error", mockID) + expectedError = fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockError.MessageId, mockError.Code, mockError.Description) + writeHook.Run(func(args mock.Arguments) { + data := args.Get(0).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.chargePoint.SetInvalidMessageHook(func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return mockError + }) + // Trigger incoming invalid CALL that returns custom error + err = suite.mockClient.MessageHandler([]byte(invalidMessage)) + ocppErr, ok = err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, mockError.Code, ocppErr.Code) + assert.Equal(t, mockError.Description, ocppErr.Description) + assert.Equal(t, mockError.MessageId, ocppErr.MessageId) +} + func (suite *OcppJTestSuite) TestChargePointSendInvalidCall() { suite.mockClient.On("Write", mock.Anything).Return(nil) suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) diff --git a/ocppj/client.go b/ocppj/client.go index 1f5acc08..083f544f 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -20,6 +20,7 @@ type Client struct { errorHandler func(err *ocpp.Error, details interface{}) onDisconnectedHandler func(err error) onReconnectedHandler func() + invalidMessageHook func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error dispatcher ClientDispatcher RequestState ClientState } @@ -68,6 +69,24 @@ func (c *Client) SetErrorHandler(handler func(err *ocpp.Error, details interface c.errorHandler = handler } +// SetInvalidMessageHook registers an optional hook for incoming messages that couldn't be parsed. +// This hook is called when a message is received but cannot be parsed to the target OCPP message struct. +// +// The application is notified synchronously of the error. +// The callback provides the raw JSON string, along with the parsed fields. +// The application MUST return as soon as possible, since the hook is called synchronously and awaits a return value. +// +// While the hook does not allow responding to the message directly, +// the return value will be used to send an OCPP error to the other endpoint. +// +// If no handler is registered (or no error is returned by the hook), +// the internal error message is sent to the client without further processing. +// +// Note: Failing to return from the hook will cause the client to block indefinitely. +func (c *Client) SetInvalidMessageHook(hook func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error) { + c.invalidMessageHook = hook +} + func (c *Client) SetOnDisconnectedHandler(handler func(err error)) { c.onDisconnectedHandler = handler } @@ -226,6 +245,18 @@ func (c *Client) ocppMessageHandler(data []byte) error { message, err := c.ParseMessage(parsedJson, c.RequestState) if err != nil { ocppErr := err.(*ocpp.Error) + messageID := ocppErr.MessageId + // Support ad-hoc callback for invalid message handling + if c.invalidMessageHook != nil { + err2 := c.invalidMessageHook(ocppErr, string(data), parsedJson) + // If the hook returns an error, use it as output error. If not, use the original error. + if err2 != nil { + ocppErr = err2 + ocppErr.MessageId = messageID + } + } + err = ocppErr + // Send error to other endpoint if a message ID is available if ocppErr.MessageId != "" { err2 := c.SendError(ocppErr.MessageId, ocppErr.Code, ocppErr.Description, nil) if err2 != nil { diff --git a/ocppj/server.go b/ocppj/server.go index e4feaafe..09abf63a 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -20,6 +20,7 @@ type Server struct { requestHandler RequestHandler responseHandler ResponseHandler errorHandler ErrorHandler + invalidMessageHook InvalidMessageHook dispatcher ServerDispatcher RequestState ServerState } @@ -28,6 +29,7 @@ type ClientHandler func(client ws.Channel) type RequestHandler func(client ws.Channel, request ocpp.Request, requestId string, action string) type ResponseHandler func(client ws.Channel, response ocpp.Response, requestId string) type ErrorHandler func(client ws.Channel, err *ocpp.Error, details interface{}) +type InvalidMessageHook func(client ws.Channel, err *ocpp.Error, rawJson string, parsedFields []interface{}) *ocpp.Error // Creates a new Server endpoint. // Requires a a websocket server. Optionally a structure for queueing/dispatching requests, @@ -79,6 +81,24 @@ func (s *Server) SetErrorHandler(handler ErrorHandler) { s.errorHandler = handler } +// SetInvalidMessageHook registers an optional hook for incoming messages that couldn't be parsed. +// This hook is called when a message is received but cannot be parsed to the target OCPP message struct. +// +// The application is notified synchronously of the error. +// The callback provides the raw JSON string, along with the parsed fields. +// The application MUST return as soon as possible, since the hook is called synchronously and awaits a return value. +// +// The hook does not allow responding to the message directly, +// but the return value will be used to send an OCPP error to the other endpoint. +// +// If no handler is registered (or no error is returned by the hook), +// the internal error message is sent to the client without further processing. +// +// Note: Failing to return from the hook will cause the handler for this client to block indefinitely. +func (s *Server) SetInvalidMessageHook(hook InvalidMessageHook) { + s.invalidMessageHook = hook +} + // Registers a handler for canceled request messages. func (s *Server) SetCanceledRequestHandler(handler CanceledRequestHandler) { s.dispatcher.SetOnRequestCanceled(handler) @@ -221,6 +241,18 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { message, err := s.ParseMessage(parsedJson, pending) if err != nil { ocppErr := err.(*ocpp.Error) + messageID := ocppErr.MessageId + // Support ad-hoc callback for invalid message handling + if s.invalidMessageHook != nil { + err2 := s.invalidMessageHook(wsChannel, ocppErr, string(data), parsedJson) + // If the hook returns an error, use it as output error. If not, use the original error. + if err2 != nil { + ocppErr = err2 + ocppErr.MessageId = messageID + } + } + err = ocppErr + // Send error to other endpoint if a message ID is available if ocppErr.MessageId != "" { err2 := s.SendError(wsChannel.ID(), ocppErr.MessageId, ocppErr.Code, ocppErr.Description, nil) if err2 != nil { @@ -235,7 +267,9 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { case CALL: call := message.(*Call) log.Debugf("handling incoming CALL [%s, %s] from %s", call.UniqueId, call.Action, wsChannel.ID()) - s.requestHandler(wsChannel, call.Payload, call.UniqueId, call.Action) + if s.requestHandler != nil { + s.requestHandler(wsChannel, call.Payload, call.UniqueId, call.Action) + } case CALL_RESULT: callResult := message.(*CallResult) log.Debugf("handling incoming CALL RESULT [%s] from %s", callResult.UniqueId, wsChannel.ID()) From d0d7f6d970f2fdc1b5028d93ca02258b8252ddc7 Mon Sep 17 00:00:00 2001 From: Scott Zhou Date: Tue, 25 Jul 2023 16:53:33 -0400 Subject: [PATCH 42/47] change serial number validate max to 40 --- ocpp2.0.1/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index 936ee55e..0f346f94 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -151,7 +151,7 @@ type CertificateHashData struct { HashAlgorithm HashAlgorithmType `json:"hashAlgorithm" validate:"required,hashAlgorithm"` IssuerNameHash string `json:"issuerNameHash" validate:"required,max=128"` IssuerKeyHash string `json:"issuerKeyHash" validate:"required,max=128"` - SerialNumber string `json:"serialNumber" validate:"required,max=20"` + SerialNumber string `json:"serialNumber" validate:"required,max=40"` } // CertificateHashDataChain From ff45def820e97be481316b316508d02e5ba902c0 Mon Sep 17 00:00:00 2001 From: Scott Zhou Date: Wed, 26 Jul 2023 08:35:12 -0400 Subject: [PATCH 43/47] Serial number in OCS Request need to be change as well --- ocpp2.0.1/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index 0f346f94..f4170259 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -142,7 +142,7 @@ type OCSPRequestDataType struct { HashAlgorithm HashAlgorithmType `json:"hashAlgorithm" validate:"required,hashAlgorithm"` IssuerNameHash string `json:"issuerNameHash" validate:"required,max=128"` IssuerKeyHash string `json:"issuerKeyHash" validate:"required,max=128"` - SerialNumber string `json:"serialNumber" validate:"required,max=20"` + SerialNumber string `json:"serialNumber" validate:"required,max=40"` ResponderURL string `json:"responderURL,omitempty" validate:"max=512"` } From 139992cfbe1164739c6478f1378b72e0da60186b Mon Sep 17 00:00:00 2001 From: dwibudut Date: Thu, 27 Jul 2023 12:16:16 +0700 Subject: [PATCH 44/47] add IdTokenType MacAddress --- ocpp2.0.1/types/types.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index f4170259..fd99f2a4 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -53,16 +53,17 @@ const ( IdTokenTypeCentral IdTokenType = "Central" IdTokenTypeEMAID IdTokenType = "eMAID" IdTokenTypeISO14443 IdTokenType = "ISO14443" + IdTokenTypeISO15693 IdTokenType = "ISO15693" IdTokenTypeKeyCode IdTokenType = "KeyCode" IdTokenTypeLocal IdTokenType = "Local" + IdTokenTypeMacAddress IdTokenType = "MacAddress" IdTokenTypeNoAuthorization IdTokenType = "NoAuthorization" - IdTokenTypeISO15693 IdTokenType = "ISO15693" ) func isValidIdTokenType(fl validator.FieldLevel) bool { tokenType := IdTokenType(fl.Field().String()) switch tokenType { - case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeNoAuthorization, IdTokenTypeISO15693: + case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeISO15693, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeMacAddress, IdTokenTypeNoAuthorization: return true default: return false From fa5b59a8d82e39981a28cc07b1ad69668758410d Mon Sep 17 00:00:00 2001 From: Scott Zhou Date: Thu, 17 Aug 2023 14:24:42 -0400 Subject: [PATCH 45/47] add key16 and key201 to resetType and resetStatus --- ocpp1.6/core/reset.go | 15 ++++++++------- ocpp2.0.1/provisioning/reset.go | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ocpp1.6/core/reset.go b/ocpp1.6/core/reset.go index ae035f36..7ef0770d 100644 --- a/ocpp1.6/core/reset.go +++ b/ocpp1.6/core/reset.go @@ -1,9 +1,10 @@ package core import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Reset (CS -> CP) -------------------- @@ -45,13 +46,13 @@ func isValidResetStatus(fl validator.FieldLevel) bool { // The field definition of the Reset request payload sent by the Central System to the Charge Point. type ResetRequest struct { - Type ResetType `json:"type" validate:"required,resetType"` + Type ResetType `json:"type" validate:"required,resetType16"` } // This field definition of the Reset confirmation payload, sent by the Charge Point to the Central System in response to a ResetRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ResetConfirmation struct { - Status ResetStatus `json:"status" validate:"required,resetStatus"` + Status ResetStatus `json:"status" validate:"required,resetStatus16"` } // The Central System SHALL send a ResetRequest for requesting a Charge Point to reset itself. @@ -62,8 +63,8 @@ type ResetConfirmation struct { // At receipt of a soft reset, the Charge Point SHALL stop ongoing transactions gracefully and send StopTransactionRequest for every ongoing transaction. // It should then restart the application software (if possible, otherwise restart the processor/controller). // At receipt of a hard reset the Charge Point SHALL restart (all) the hardware, it is not required to gracefully stop ongoing transaction. -//If possible the Charge Point sends a StopTransactionRequest for previously ongoing transactions after having restarted and having been accepted by the Central System via a BootNotificationConfirmation. -//This is a last resort solution for a not correctly functioning Charge Points, by sending a "hard" reset, (queued) information might get lost. +// If possible the Charge Point sends a StopTransactionRequest for previously ongoing transactions after having restarted and having been accepted by the Central System via a BootNotificationConfirmation. +// This is a last resort solution for a not correctly functioning Charge Points, by sending a "hard" reset, (queued) information might get lost. type ResetFeature struct{} func (f ResetFeature) GetFeatureName() string { @@ -97,6 +98,6 @@ func NewResetConfirmation(status ResetStatus) *ResetConfirmation { } func init() { - _ = types.Validate.RegisterValidation("resetType", isValidResetType) - _ = types.Validate.RegisterValidation("resetStatus", isValidResetStatus) + _ = types.Validate.RegisterValidation("resetType16", isValidResetType) + _ = types.Validate.RegisterValidation("resetStatus16", isValidResetStatus) } diff --git a/ocpp2.0.1/provisioning/reset.go b/ocpp2.0.1/provisioning/reset.go index 0846241e..86d3bc66 100644 --- a/ocpp2.0.1/provisioning/reset.go +++ b/ocpp2.0.1/provisioning/reset.go @@ -52,14 +52,14 @@ func isValidResetStatus(fl validator.FieldLevel) bool { // The field definition of the Reset request payload sent by the CSMS to the Charging Station. type ResetRequest struct { - Type ResetType `json:"type" validate:"resetType"` + Type ResetType `json:"type" validate:"resetType201"` EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` } // This field definition of the Reset response payload, sent by the Charging Station to the CSMS in response to a ResetRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ResetResponse struct { - Status ResetStatus `json:"status" validate:"required,resetStatus"` + Status ResetStatus `json:"status" validate:"required,resetStatus201"` StatusInfo *types.StatusInfo `json:"statusInfo" validate:"omitempty"` } @@ -103,6 +103,6 @@ func NewResetResponse(status ResetStatus) *ResetResponse { } func init() { - _ = types.Validate.RegisterValidation("resetType", isValidResetType) - _ = types.Validate.RegisterValidation("resetStatus", isValidResetStatus) + _ = types.Validate.RegisterValidation("resetType201", isValidResetType) + _ = types.Validate.RegisterValidation("resetStatus201", isValidResetStatus) } From f0af2ad3b04f53e37166496e1cc466f5751719bc Mon Sep 17 00:00:00 2001 From: Stefan Bindzau Date: Thu, 24 Aug 2023 11:45:26 +0200 Subject: [PATCH 46/47] check type on action in call and add test --- ocppj/ocppj.go | 6 +++++- ocppj/ocppj_test.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 9b0d43a9..12b3b96d 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -394,7 +394,11 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl if len(arr) != 4 { return nil, ocpp.NewError(FormationViolation, "Invalid Call message. Expected array length 4", uniqueId) } - action := arr[2].(string) + action, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormationViolation, fmt.Sprintf("Invalid element %v at 2, expected action (string)", arr[2]), "") + } + profile, ok := endpoint.GetProfileForFeature(action) if !ok { return nil, ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", action), uniqueId) diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index 62b8e2ba..c9d73385 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -629,6 +629,26 @@ func (suite *OcppJTestSuite) TestParseMessageInvalidCall() { assert.Equal(t, "Invalid Call message. Expected array length 4", protoErr.Description) } +func (suite *OcppJTestSuite) TestParseMessageInvalidActionCall() { + t := suite.T() + mockMessage := make([]interface{}, 4) + messageId := "12345" + mockRequest := newMockRequest("") + // Test invalid message length + mockMessage[0] = float64(ocppj.CALL) // Message Type ID + mockMessage[1] = messageId // Unique ID + mockMessage[2] = float64(42) // Wrong type on action parameter + mockMessage[3] = mockRequest + message, err := suite.chargePoint.ParseMessage(mockMessage, suite.chargePoint.RequestState) + require.Nil(t, message) + require.Error(t, err) + protoErr := err.(*ocpp.Error) + require.NotNil(t, protoErr) + assert.Equal(t, protoErr.MessageId, "") // unique id is never set after invalid type cast return + assert.Equal(t, ocppj.FormationViolation, protoErr.Code) + assert.Equal(t, "Invalid element 42 at 2, expected action (string)", protoErr.Description) +} + func (suite *OcppJTestSuite) TestParseMessageInvalidCallResult() { t := suite.T() mockMessage := make([]interface{}, 3) From c686786441c2a65af4a74334d693e7a3b99af0c4 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Sun, 3 Sep 2023 23:04:29 +0200 Subject: [PATCH 47/47] Fix assertion for invalid TLS certificate test Signed-off-by: Lorenzo --- ws/websocket_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws/websocket_test.go b/ws/websocket_test.go index 98fd3a67..8267d8db 100644 --- a/ws/websocket_test.go +++ b/ws/websocket_test.go @@ -821,7 +821,7 @@ func TestInvalidClientTLSCertificate(t *testing.T) { assert.NotNil(t, err) netError, ok := err.(net.Error) require.True(t, ok) - assert.Equal(t, "remote error: tls: bad certificate", netError.Error()) // tls.alertBadCertificate = 42 + assert.Equal(t, "remote error: tls: unknown certificate authority", netError.Error()) // tls.alertUnknownCA = 48 // Cleanup wsServer.Stop() }