diff --git a/builder.go b/builder.go index b77594a..108e974 100644 --- a/builder.go +++ b/builder.go @@ -2,8 +2,6 @@ package ocmf_go import ( "crypto" - "encoding/base64" - "encoding/hex" "encoding/json" "fmt" @@ -14,32 +12,34 @@ type BuilderOption func(*Builder) func WithSignatureAlgorithm(algorithm SignatureAlgorithm) BuilderOption { return func(b *Builder) { - b.signature.Algorithm = algorithm + if isValidSignatureAlgorithm(algorithm) { + b.signature.Algorithm = algorithm + } } } func WithSignatureEncoding(encoding SignatureEncoding) BuilderOption { return func(b *Builder) { - b.signature.Encoding = encoding + if isValidSignatureEncoding(encoding) { + b.signature.Encoding = encoding + } } } type Builder struct { - payload PayloadSection - signature Signature + payload PayloadSection + signature Signature + privateKey crypto.PrivateKey } -func NewBuilder(opts ...BuilderOption) *Builder { +func NewBuilder(privateKey crypto.PrivateKey, opts ...BuilderOption) *Builder { builder := &Builder{ payload: PayloadSection{ - FormatVersion: "0.4", + FormatVersion: OcmfVersion, }, // Set default signature parameters - signature: Signature{ - Algorithm: SignatureAlgorithmECDSAsecp256r1SHA256, - Encoding: SignatureEncodingHex, - MimeType: SignatureMimeTypeDer, - }, + signature: *NewDefaultSignature(), + privateKey: privateKey, } // Apply builder options @@ -75,74 +75,86 @@ func (b *Builder) AddReading(reading Reading) *Builder { return b } -func (b *Builder) AddFlag(flag string) *Builder { +func (b *Builder) AddIdentificationFlag(flag string) *Builder { b.payload.IdentificationFlags = append(b.payload.IdentificationFlags, flag) return b } -func (b *Builder) AddLossCompensation(lossCompensation LossCompensation) *Builder { - b.payload.LossCompensation = lossCompensation +func (b *Builder) WithMeterSerial(serial string) *Builder { + b.payload.MeterSerial = serial return b } -func (b *Builder) ClearPayloadSection() *Builder { - b.payload = PayloadSection{ - FormatVersion: "0.4", - } +func (b *Builder) WithIdentificationStatus(status bool) *Builder { + b.payload.IdentificationStatus = status return b } -// Sign payload -func (b *Builder) signPayload(privateKey crypto.PrivateKey) { - var signedData string +func (b *Builder) WithIdentificationLevel(level string) *Builder { + b.payload.IdentificationLevel = level + return b +} - switch b.signature.Algorithm { - case SignatureAlgorithmECDSAsecp192k1SHA256: - // TODO - case SignatureAlgorithmECDSAsecp256k1SHA256: - // TODO - case SignatureAlgorithmECDSAsecp384r1SHA256: - // TODO - case SignatureAlgorithmECDSAbrainpool256r11SHA256: - // TODO - case SignatureAlgorithmECDSAsecp256r1SHA256: - // TODO - default: +func (b *Builder) WithIdentificationType(idType string) *Builder { + b.payload.IdentificationType = idType + return b +} - } +func (b *Builder) WithIdentificationData(data string) *Builder { + b.payload.IdentificationData = data + return b +} - // Encode signed data - switch b.signature.Encoding { - case SignatureEncodingBase64: - signedData = base64.StdEncoding.EncodeToString([]byte(signedData)) - default: - signedData = hex.EncodeToString([]byte(signedData)) - } +func (b *Builder) WithTariffText(text string) *Builder { + b.payload.TariffText = text + return b +} - b.signature.Data = signedData +func (b *Builder) WithChargeControllerVersion(version string) *Builder { + b.payload.ChargeControllerVersion = version + return b } -// Sign payload -func (b *Builder) validatePayload() error { - return b.payload.Validate() +func (b *Builder) WithChargePointIdentificationType(serial string) *Builder { + b.payload.ChargePointIdentificationType = serial + return b } -func (b *Builder) Build(privateKey crypto.PrivateKey) (*string, error) { - err := b.validatePayload() +func (b *Builder) WithChargePointIdentification(serial string) *Builder { + b.payload.ChargePointIdentification = serial + return b +} + +func (b *Builder) AddLossCompensation(lossCompensation LossCompensation) *Builder { + b.payload.LossCompensation = lossCompensation + return b +} + +func (b *Builder) ClearPayloadSection() *Builder { + b.payload = PayloadSection{ + FormatVersion: OcmfVersion, + } + return b +} + +func (b *Builder) Build() (*string, error) { + // Validate payload + err := b.payload.Validate() if err != nil { return nil, err } - // Build payload - payload, err := json.Marshal(b.payload) + // Sign payload with private key + err = b.signature.Sign(b.privateKey) if err != nil { return nil, err } - // Sign payload - b.signPayload(privateKey) + payload, err := json.Marshal(b.payload) + if err != nil { + return nil, err + } - // Build signature signature, err := json.Marshal(b.signature) if err != nil { return nil, err diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 0000000..51bfc70 --- /dev/null +++ b/builder_test.go @@ -0,0 +1,200 @@ +package ocmf_go + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type builderTestSuite struct { + suite.Suite +} + +func (s *builderTestSuite) SetupTest() { +} + +func (s *builderTestSuite) TearDownSuite() { +} + +func (s *builderTestSuite) TestNewBuilder() { + tests := []struct { + name string + opts []BuilderOption + }{ + {}, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + + }) + } +} + +func (s *builderTestSuite) TestBuilder_Valid() { + privateKey := "" // todo + builder := NewBuilder(privateKey). + WithPagination("1"). + WithMeterSerial("exampleSerial123"). + WithIdentificationStatus(true). + WithIdentificationType(string(RfidNone)). + AddReading(Reading{ + Time: "2018-07-24T13:22:04,000+0200", + ReadingValue: 123, + ReadingUnit: string(UnitskWh), + Status: string(MeterOk), + }) + + s.Equal("1", builder.payload.Pagination) + s.Equal("exampleSerial123", builder.payload.MeterSerial) + s.Equal(true, builder.payload.IdentificationStatus) + s.Equal(string(RfidNone), builder.payload.IdentificationType) + s.Len(builder.payload.Readings, 1) + s.Equal("2018-07-24T13:22:04,000+0200", builder.payload.Readings[0].Time) + s.Equal(123, builder.payload.Readings[0].ReadingValue) + s.Equal(string(UnitskWh), builder.payload.Readings[0].ReadingUnit) + s.Equal(string(MeterOk), builder.payload.Readings[0].Status) + + payload, err := builder.Build() + s.NoError(err) + s.NotNil(payload) +} + +func (s *builderTestSuite) TestBuilder_MissingAttributes() { + builder := NewBuilder("privateKey"). + // WithPagination("1"). + WithMeterSerial("exampleSerial123"). + WithIdentificationStatus(true). + WithIdentificationType(string(RfidNone)). + AddReading(Reading{ + Time: "2021-01-01T00:00:00Z", + ReadingValue: 123, + ReadingUnit: string(UnitskWh), + Status: string(MeterOk), + }) + + payload, err := builder.Build() + s.Error(err) + s.Nil(payload) +} + +func (s *builderTestSuite) TestBuilder_CantSign() { + builder := NewBuilder("privateKey"). + WithPagination("1"). + WithMeterSerial("exampleSerial123"). + WithIdentificationStatus(true). + WithIdentificationType(string(RfidNone)). + AddReading(Reading{ + Time: "2021-01-01T00:00:00Z", + ReadingValue: 123, + ReadingUnit: string(UnitskWh), + Status: string(MeterOk), + }) + + builder.privateKey = "" + + payload, err := builder.Build() + s.Error(err) + s.Nil(payload) +} + +func (s *builderTestSuite) TestWithSignatureAlgorithm() { + tests := []struct { + name string + algorithm SignatureAlgorithm + want bool + }{ + { + name: "ECDSA-secp192k1-SHA256", + algorithm: SignatureAlgorithmECDSAsecp192k1SHA256, + want: true, + }, + { + name: "ECDSA-secp256k1-SHA256", + algorithm: SignatureAlgorithmECDSAsecp256k1SHA256, + want: true, + }, + { + name: "ECDSA-secp384r1-SHA256", + algorithm: SignatureAlgorithmECDSAsecp384r1SHA256, + want: true, + }, + { + name: "ECDSA-brainpool256r1-SHA256", + algorithm: SignatureAlgorithmECDSAbrainpool256r11SHA256, + want: true, + }, + { + name: "ECDSA-secp256r1-SHA256", + algorithm: SignatureAlgorithmECDSAsecp256r1SHA256, + want: true, + }, + { + name: "ECDSA-secp192r1-SHA256", + algorithm: SignatureAlgorithmECDSAsecp192r1SHA256, + want: true, + }, + { + name: "Unknown algorithm", + algorithm: SignatureAlgorithm("ABCD"), + want: false, + }, + { + name: "Empty algorithm", + algorithm: SignatureAlgorithm(""), + want: false, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + builder := NewBuilder("privateKey", WithSignatureAlgorithm(tt.algorithm)) + + if tt.name == "Unknown algorithm" || tt.name == "Empty algorithm" { + s.NotEqual(tt.algorithm, builder.signature.Algorithm) + } else { + s.Equal(tt.algorithm, builder.signature.Algorithm) + } + }) + } +} + +func (s *builderTestSuite) TestWithWithSignatureEncoding() { + tests := []struct { + name string + encoding SignatureEncoding + }{ + { + name: "Base64 encoding", + encoding: SignatureEncodingBase64, + }, + { + name: "Hex encoding", + encoding: SignatureEncodingHex, + }, + { + name: "Empty encoding", + encoding: SignatureEncoding(""), + }, + { + name: "Unknown encoding", + encoding: SignatureEncoding("ABDD"), + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + builder := NewBuilder("privateKey", WithSignatureEncoding(tt.encoding)) + + if tt.encoding == SignatureEncodingBase64 || tt.encoding == SignatureEncodingHex { + s.Equal(tt.encoding, builder.signature.Encoding) + } else { + s.NotEqual(tt.encoding, builder.signature.Encoding) + } + }) + } +} + +func TestBuilder(t *testing.T) { + suite.Run(t, new(builderTestSuite)) +} diff --git a/payload.go b/payload.go index 2646a84..c597647 100644 --- a/payload.go +++ b/payload.go @@ -200,11 +200,11 @@ type PayloadSection struct { MeterSerial string `json:"MS" validate:"required"` MeterFirmware string `json:"MF,omitempty"` // User assignment - IdentificationStatus bool `json:"IS" validate:"required,"` + IdentificationStatus bool `json:"IS" validate:"required"` IdentificationLevel string `json:"IL,omitempty" validate:"omitempty,userAssignmentState"` IdentificationFlags []string `json:"IF" validate:"omitempty,max=4"` IdentificationType string `json:"IT" validate:"required,rfidState"` - IdentificationData string `json:"ID,omitempty" validate:"omitempty,hex"` + IdentificationData string `json:"ID,omitempty" validate:"omitempty,hexadecimal"` TariffText string `json:"TT,omitempty" validate:"omitempty,max=250"` // EVSE metrologic parameters LossCompensation LossCompensation `json:"LC,omitempty"` diff --git a/validation.go b/validation.go index d30eca1..bb9bd02 100644 --- a/validation.go +++ b/validation.go @@ -71,7 +71,7 @@ func currentTypeValidator(fl validator.FieldLevel) bool { return isValidCurrentType(CurrentType(fl.Field().String())) } -var iso8601WithMillisRegex = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$`) +var iso8601WithMillisRegex = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2},\d{3}[+-]\d{4}$`) func iso8601WithMillisValidator(fl validator.FieldLevel) bool { return iso8601WithMillisRegex.MatchString(fl.Field().String())