Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
T
Joseph Doherty 627c17fae1 fix(gateway): reject oversized sparse array total_length with InvalidArgument
Guard against proto uint32 total_length values that exceed Array.MaxLength
before casting; the previous checked cast threw OverflowException (gRPC
Internal) instead of the intended InvalidArgument. Adds tests for the new
guard, for the null-value ArgumentNullException path, and removes the
checked keyword (redundant after the guard).
2026-06-18 02:58:03 -04:00

211 lines
6.6 KiB
C#

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Sessions;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
public sealed class SparseArrayExpanderTests
{
private static MxValue SparseValue(
MxDataType elementType,
uint totalLength,
params (uint Index, MxValue Value)[] elements)
{
MxSparseArray sparse = new()
{
ElementDataType = elementType,
TotalLength = totalLength,
};
foreach ((uint index, MxValue value) in elements)
{
sparse.Elements.Add(new MxSparseElement { Index = index, Value = value });
}
return new MxValue { SparseArrayValue = sparse };
}
[Fact]
public void Expand_Int32_FillsDefaultsAndSetsElement()
{
MxValue value = SparseValue(
MxDataType.Integer,
4,
(1, new MxValue { Int32Value = 7 }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxValue.KindOneofCase.ArrayValue, value.KindCase);
Assert.Equal(MxDataType.Integer, value.ArrayValue.ElementDataType);
Assert.Equal(new uint[] { 4 }, value.ArrayValue.Dimensions);
Assert.Equal(MxArray.ValuesOneofCase.Int32Values, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { 0, 7, 0, 0 }, value.ArrayValue.Int32Values.Values);
}
[Fact]
public void Expand_Boolean_EmptyElements_AllDefaultFalse()
{
MxValue value = SparseValue(MxDataType.Boolean, 3);
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.BoolValues, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { false, false, false }, value.ArrayValue.BoolValues.Values);
}
[Fact]
public void Expand_ZeroTotalLength_Throws()
{
MxValue value = SparseValue(MxDataType.Integer, 0);
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
[Fact]
public void Expand_IndexOutOfRange_Throws()
{
MxValue value = SparseValue(
MxDataType.Integer,
2,
(5, new MxValue { Int32Value = 1 }));
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
[Fact]
public void Expand_DuplicateIndex_Throws()
{
MxValue value = SparseValue(
MxDataType.Integer,
4,
(1, new MxValue { Int32Value = 1 }),
(1, new MxValue { Int32Value = 2 }));
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
[Fact]
public void Expand_UnsupportedElementType_Throws()
{
MxValue value = SparseValue(MxDataType.Unspecified, 2);
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
[Fact]
public void Expand_ElementValueKindMismatch_Throws()
{
MxValue value = SparseValue(
MxDataType.Integer,
2,
(0, new MxValue { StringValue = "nope" }));
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
[Fact]
public void Expand_String_FillsEmptyStringDefault()
{
MxValue value = SparseValue(
MxDataType.String,
2,
(0, new MxValue { StringValue = "a" }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.StringValues, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { "a", string.Empty }, value.ArrayValue.StringValues.Values);
}
[Fact]
public void Expand_Time_FillsEpochDefault()
{
MxValue value = SparseValue(
MxDataType.Time,
2,
(1, new MxValue { TimestampValue = new Timestamp { Seconds = 5 } }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.TimestampValues, value.ArrayValue.ValuesCase);
Assert.Equal(2, value.ArrayValue.TimestampValues.Values.Count);
Assert.Equal(0, value.ArrayValue.TimestampValues.Values[0].Seconds);
Assert.Equal(0, value.ArrayValue.TimestampValues.Values[0].Nanos);
Assert.Equal(5, value.ArrayValue.TimestampValues.Values[1].Seconds);
}
[Fact]
public void Expand_Double_HappyPath()
{
MxValue value = SparseValue(
MxDataType.Double,
3,
(2, new MxValue { DoubleValue = 1.5 }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.DoubleValues, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { 0d, 0d, 1.5 }, value.ArrayValue.DoubleValues.Values);
}
[Fact]
public void Expand_Float_HappyPath()
{
MxValue value = SparseValue(
MxDataType.Float,
2,
(0, new MxValue { FloatValue = 2.5f }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.FloatValues, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { 2.5f, 0f }, value.ArrayValue.FloatValues.Values);
}
[Fact]
public void Expand_Int64_WhenElementIsInt64()
{
MxValue value = SparseValue(
MxDataType.Integer,
3,
(2, new MxValue { Int64Value = 9_000_000_000L }));
SparseArrayExpander.Expand(value);
Assert.Equal(MxArray.ValuesOneofCase.Int64Values, value.ArrayValue.ValuesCase);
Assert.Equal(new[] { 0L, 0L, 9_000_000_000L }, value.ArrayValue.Int64Values.Values);
}
[Fact]
public void Expand_NonSparseValue_NoOps()
{
MxValue value = new() { Int32Value = 42 };
SparseArrayExpander.Expand(value);
Assert.Equal(MxValue.KindOneofCase.Int32Value, value.KindCase);
Assert.Equal(42, value.Int32Value);
}
[Fact]
public void Expand_NullValue_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() => SparseArrayExpander.Expand(null!));
}
[Fact]
public void Expand_TotalLengthExceedsMaxArrayLength_Throws()
{
MxValue value = SparseValue(MxDataType.Integer, 2_147_483_648u);
RpcException ex = Assert.Throws<RpcException>(() => SparseArrayExpander.Expand(value));
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
}