blob: 59e73d887811a63637398f8b44b78acc947f3b40 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package abeth
import (
"context"
"encoding/hex"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
readWriteModel "github.com/apache/plc4x/plc4go/protocols/abeth/readwrite/model"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/testutils"
"github.com/apache/plc4x/plc4go/spi/transports/test"
)
// newTestTransport builds the in-process test transport the codec and driver tests run on.
func newTestTransport(t *testing.T, _options ...options.WithOption) *test.Transport {
t.Helper()
return test.NewTransport(_options...)
}
// newTestCodec spins a MessageCodec up on a connected (but not running) test transport. The
// transport instance is what the tests feed packets into and read the codec's output back out of.
func newTestCodec(t *testing.T) (*MessageCodec, *test.TransportInstance) {
t.Helper()
_options := testutils.EnrichOptionsWithOptionsForTesting(t)
transport := newTestTransport(t, _options...)
transportInstance, err := transport.CreateTransportInstance(url.URL{Scheme: "test"}, nil, _options...)
require.NoError(t, err)
testTransportInstance, ok := transportInstance.(*test.TransportInstance)
require.True(t, ok)
require.NoError(t, testTransportInstance.Connect(t.Context()))
t.Cleanup(func() {
assert.NoError(t, testTransportInstance.Close())
})
return NewMessageCodec(testTransportInstance, _options...), testTransportInstance
}
func mustDecodeHex(t *testing.T, hexString string) []byte {
t.Helper()
decoded, err := hex.DecodeString(hexString)
require.NoError(t, err)
return decoded
}
func TestPacketSizeFromHeader(t *testing.T) {
tests := []struct {
name string
header []byte
want uint32
}{
{name: "a header-only packet is 28 bytes", header: []byte{0x01, 0x01, 0x00, 0x00}, want: 28},
{name: "the length field counts what follows the header", header: []byte{0x01, 0x07, 0x00, 0x0E}, want: 42},
{name: "the length field is big-endian", header: []byte{0x02, 0x07, 0x01, 0x00}, want: 284},
// A uint16 total would wrap 0xFFFF + 28 back to 27, making Read consume less than a frame
// and leaving the receive worker unable to ever resynchronize.
{name: "the biggest length doesn't wrap the total", header: []byte{0x02, 0x07, 0xFF, 0xFF}, want: 65563},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, packetSizeFromHeader(testCase.header))
})
}
}
func TestMessageCodec_SendSerializesBigEndian(t *testing.T) {
codec, transportInstance := newTestCodec(t)
request := readWriteModel.NewCIPEncapsulationConnectionRequest(0, 0, connectionRequestSenderContext, 0)
require.NoError(t, codec.Send(testutils.TestContext(t), "connect", request))
// Byte for byte the "Connection Request" case of the parser-serializer testsuite.
assert.Equal(t,
mustDecodeHex(t, "01010000000000000000000000040005000000000000000000000000"),
transportInstance.DrainWriteBuffer(28))
}
func TestMessageCodec_SendRejectsForeignMessages(t *testing.T) {
codec, _ := newTestCodec(t)
// A DF1 message on its own is not a CIP encapsulation packet, so it can't be framed.
err := codec.Send(testutils.TestContext(t), "nonsense",
readWriteModel.NewDF1CommandRequestMessage(0, 5, 0, 1,
readWriteModel.NewDF1RequestProtectedTypedLogicalRead(2, 7, 0x89, 3, 0)))
assert.Error(t, err)
}
func TestMessageCodec_ReceiveFramesTheStream(t *testing.T) {
codec, transportInstance := newTestCodec(t)
// A connection response (header only) directly followed by a read response with a 24 byte
// payload - both cases of the parser-serializer testsuite, back to back in one chunk.
transportInstance.FillReadBuffer(mustDecodeHex(t,
"02010000000003320000000000040005000000000000000000000000"+
"02070020000003320000000040000000000000000000000000000000000508004f000401910101000900040405001f02010003000404050000024000"))
connectionResponse, err := codec.Receive(testutils.TestContext(t))
require.NoError(t, err)
typedConnectionResponse, ok := connectionResponse.(readWriteModel.CIPEncapsulationConnectionResponse)
require.True(t, ok, "%T is not a connection response", connectionResponse)
assert.Equal(t, uint32(818), typedConnectionResponse.GetSessionHandle())
readResponse, err := codec.Receive(testutils.TestContext(t))
require.NoError(t, err)
typedReadResponse, ok := readResponse.(readWriteModel.CIPEncapsulationReadResponse)
require.True(t, ok, "%T is not a read response", readResponse)
assert.Equal(t, uint32(818), typedReadResponse.GetSessionHandle())
assert.Equal(t, uint16(1025), typedReadResponse.GetResponse().GetTransactionCounter())
logicalRead, ok := typedReadResponse.GetResponse().(readWriteModel.DF1CommandResponseMessageProtectedTypedLogicalRead)
require.True(t, ok)
assert.Len(t, logicalRead.GetData(), 24, "the payload length follows from packetLen - 8")
}
func TestMessageCodec_ReceiveWaitsForTheWholeFrame(t *testing.T) {
codec, transportInstance := newTestCodec(t)
// A partial frame leaves the codec waiting for the rest of it, so the two incomplete steps
// below get a context of their own that runs out - which is what a real transport going quiet
// looks like from in here.
receivePartial := func() any {
t.Helper()
ctx, cancel := context.WithTimeout(testutils.TestContext(t), 250*time.Millisecond)
defer cancel()
message, err := codec.Receive(ctx)
require.NoError(t, err)
return message
}
// Only the first two bytes of a 28 byte connection response: not even the length field is
// complete, so there is nothing to decide a frame size from.
transportInstance.FillReadBuffer(mustDecodeHex(t, "0201"))
assert.Nil(t, receivePartial(), "a packet whose length isn't known yet must not be framed")
// The length is decidable now, but the body is still missing.
transportInstance.FillReadBuffer(mustDecodeHex(t, "00000000033200000000"))
assert.Nil(t, receivePartial(), "an incomplete packet must not be framed")
// And now the rest of it.
transportInstance.FillReadBuffer(mustDecodeHex(t, "00040005000000000000000000000000"))
message, err := codec.Receive(testutils.TestContext(t))
require.NoError(t, err)
require.NotNil(t, message)
_, ok := message.(readWriteModel.CIPEncapsulationConnectionResponse)
assert.True(t, ok, "%T is not a connection response", message)
}
func TestMessageCodec_ReceiveDropsAMalformedFrame(t *testing.T) {
codec, transportInstance := newTestCodec(t)
// 0x9999 is not a command type the type switch knows, so the packet can't be parsed. The frame
// is consumed all the same, which is what keeps the stream in sync for the packet after it.
transportInstance.FillReadBuffer(mustDecodeHex(t, "99990000000003320000000000040005000000000000000000000000"))
message, err := codec.Receive(testutils.TestContext(t))
require.NoError(t, err)
assert.Nil(t, message)
transportInstance.FillReadBuffer(mustDecodeHex(t, "02010000000003320000000000040005000000000000000000000000"))
message, err = codec.Receive(testutils.TestContext(t))
require.NoError(t, err)
require.NotNil(t, message, "the codec has to resynchronize on the packet after a bad one")
_, ok := message.(readWriteModel.CIPEncapsulationConnectionResponse)
assert.True(t, ok, "%T is not a connection response", message)
}