diff --git a/src/Mozilla.IoT.WebThing/Attributes/ThingParameterAttribute.cs b/src/Mozilla.IoT.WebThing/Attributes/ThingParameterAttribute.cs index a4f3168..5d87c1c 100644 --- a/src/Mozilla.IoT.WebThing/Attributes/ThingParameterAttribute.cs +++ b/src/Mozilla.IoT.WebThing/Attributes/ThingParameterAttribute.cs @@ -62,5 +62,22 @@ public double ExclusiveMaximum get => ExclusiveMaximumValue ?? 0; set => ExclusiveMaximumValue = value; } + + + internal uint? MinimumLengthValue { get; set; } + public uint MinimumLength + { + get => MinimumLengthValue.GetValueOrDefault(); + set => MinimumLengthValue = value; + } + + internal uint? MaximumLengthValue { get; set; } + public uint MaximumLength + { + get => MaximumLengthValue.GetValueOrDefault(); + set => MaximumLengthValue = value; + } + + public string? Pattern { get; set; } } } diff --git a/src/Mozilla.IoT.WebThing/Attributes/ThingPropertyAttribute.cs b/src/Mozilla.IoT.WebThing/Attributes/ThingPropertyAttribute.cs index c482b1f..5723531 100644 --- a/src/Mozilla.IoT.WebThing/Attributes/ThingPropertyAttribute.cs +++ b/src/Mozilla.IoT.WebThing/Attributes/ThingPropertyAttribute.cs @@ -55,5 +55,21 @@ public double ExclusiveMaximum get => ExclusiveMaximumValue ?? 0; set => ExclusiveMaximumValue = value; } + + internal uint? MinimumLengthValue { get; set; } + public uint MinimumLength + { + get => MinimumLengthValue.GetValueOrDefault(); + set => MinimumLengthValue = value; + } + + internal uint? MaximumLengthValue { get; set; } + public uint MaximumLength + { + get => MaximumLengthValue.GetValueOrDefault(); + set => MaximumLengthValue = value; + } + + public string? Pattern { get; set; } } } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs index e2aeb8d..2b244a4 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -15,6 +16,12 @@ namespace Mozilla.IoT.WebThing.Factories.Generator.Actions { public class ActionIntercept : IActionIntercept { + private static readonly MethodInfo s_getLength = typeof(string).GetProperty(nameof(string.Length)).GetMethod; + private static readonly MethodInfo s_match = typeof(Regex).GetMethod(nameof(Regex.Match) , new [] { typeof(string) }); + private static readonly MethodInfo s_success = typeof(Match).GetProperty(nameof(Match.Success)).GetMethod; + private static readonly ConstructorInfo s_regexConstructor = typeof(Regex).GetConstructors()[1]; + + private readonly ICollection<(string pattern, FieldBuilder field)> _regex = new LinkedList<(string pattern, FieldBuilder field)>(); private const MethodAttributes s_getSetAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; @@ -70,14 +77,34 @@ public void Intercept(Thing thing, MethodInfo action, ThingActionAttribute? acti && x.ParameterType != typeof(CancellationToken)) .Select(x => x.ParameterType) .ToArray()); - var isValidIl = isValid.GetILGenerator(); - CreateParameterValidation(isValidIl, parameters); + var isValidIl = isValid.GetILGenerator(); + CreateParameterValidation(isValidIl, parameters, actionBuilder); CreateInputValidation(actionBuilder, inputBuilder, isValid, input); CreateExecuteAsync(actionBuilder, inputBuilder,input, action, thingType); - + CreateStaticConstructor(actionBuilder); + Actions.Add(_option.PropertyNamingPolicy.ConvertName(name), new ActionContext(actionBuilder.CreateType()!)); } + + private void CreateStaticConstructor(TypeBuilder typeBuilder) + { + if (_regex.Count > 0) + { + var constructor = typeBuilder.DefineTypeInitializer(); + var il = constructor.GetILGenerator(); + + foreach (var (pattern, field) in _regex) + { + il.Emit(OpCodes.Ldstr, pattern); + il.Emit(OpCodes.Ldc_I4_8); + il.Emit(OpCodes.Newobj, s_regexConstructor); + il.Emit(OpCodes.Stsfld, field); + } + + il.Emit(OpCodes.Ret); + } + } private static PropertyBuilder CreateProperty(TypeBuilder builder, string fieldName, Type type) { @@ -147,7 +174,7 @@ private static void CreateInputValidation(TypeBuilder builder, TypeBuilder input isInputValid.Emit(OpCodes.Ret); } - private static void CreateParameterValidation(ILGenerator il, ParameterInfo[] parameters) + private void CreateParameterValidation(ILGenerator il, ParameterInfo[] parameters, TypeBuilder typeBuilder) { Label? next = null; for (var i = 0; i < parameters.Length; i++) @@ -228,6 +255,44 @@ private static void CreateParameterValidation(ILGenerator il, ParameterInfo[] pa il.Emit(OpCodes.Ret); } } + else if (IsString(parameter.ParameterType)) + { + if (validationParameter.MinimumLengthValue.HasValue) + { + GenerateStringLengthValidation(il, i, validationParameter.MinimumLengthValue.Value, OpCodes.Bge_S, ref next); + } + + if (validationParameter.MaximumLengthValue.HasValue) + { + GenerateStringLengthValidation(il, i, validationParameter.MaximumLengthValue.Value, OpCodes.Ble_S, ref next); + } + + if (validationParameter.Pattern != null) + { + var regex = typeBuilder.DefineField($"_regex{parameter.Name}", typeof(Regex), + FieldAttributes.Private | FieldAttributes.Static | FieldAttributes.InitOnly); + _regex.Add((validationParameter.Pattern ,regex)); + if (next != null) + { + il.MarkLabel(next.Value); + } + + next = il.DefineLabel(); + var isNull = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_S, i); + il.Emit(OpCodes.Brfalse_S, isNull); + + il.Emit(OpCodes.Ldsfld, regex); + il.Emit(OpCodes.Ldarg_S, i); + il.EmitCall(OpCodes.Callvirt, s_match, null); + il.EmitCall(OpCodes.Callvirt, s_success, null); + il.Emit(OpCodes.Brtrue_S, next.Value); + + il.MarkLabel(isNull); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ret); + } + } } if (next.HasValue) @@ -255,6 +320,30 @@ static void GenerateNumberValidation(ILGenerator generator, int fieldIndex, Type generator.Emit(OpCodes.Ldc_I4_0); generator.Emit(OpCodes.Ret); } + + static void GenerateStringLengthValidation(ILGenerator generator, int fieldIndex, uint value, OpCode code, ref Label? next) + { + if (next != null) + { + generator.MarkLabel(next.Value); + } + + next = generator.DefineLabel(); + + var nextCheckNull = generator.DefineLabel(); + + generator.Emit(OpCodes.Ldarg_S, fieldIndex); + generator.Emit(OpCodes.Brfalse_S, nextCheckNull); + + generator.Emit(OpCodes.Ldarg_S, fieldIndex); + generator.EmitCall(OpCodes.Callvirt, s_getLength, null); + generator.Emit(OpCodes.Ldc_I4, value); + generator.Emit(code, next.Value); + + generator.MarkLabel(nextCheckNull); + generator.Emit(OpCodes.Ldc_I4_0); + generator.Emit(OpCodes.Ret); + } static void SetValue(ILGenerator generator, double value, Type fieldType) { @@ -323,6 +412,22 @@ static bool IsComplexNumber(Type parameterType) || parameterType == typeof(float) || parameterType == typeof(double) || parameterType == typeof(decimal); + + static bool IsString(Type type) + => type == typeof(string); + + static bool IsNumber(Type type) + => type == typeof(int) + || type == typeof(uint) + || type == typeof(long) + || type == typeof(ulong) + || type == typeof(short) + || type == typeof(ushort) + || type == typeof(double) + || type == typeof(float) + || type == typeof(decimal) + || type == typeof(byte) + || type == typeof(sbyte); } private static void CreateExecuteAsync(TypeBuilder builder, TypeBuilder inputBuilder, PropertyBuilder input, MethodInfo action, Type thingType) @@ -387,17 +492,6 @@ private static void CreateExecuteAsync(TypeBuilder builder, TypeBuilder inputBui il.Emit(OpCodes.Ret); } - private static bool IsNumber(Type type) - => type == typeof(int) - || type == typeof(uint) - || type == typeof(long) - || type == typeof(ulong) - || type == typeof(short) - || type == typeof(ushort) - || type == typeof(double) - || type == typeof(float) - || type == typeof(decimal) - || type == typeof(byte) - || type == typeof(sbyte); + } } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs index 0b3594c..0f846d5 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs @@ -99,9 +99,22 @@ public void Intercept(Thing thing, MethodInfo action, ThingActionAttribute? acti parameterActionInfo.MinimumValue); _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.Maximum), parameterType, parameterActionInfo.MaximumValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.ExclusiveMinimum), parameterType, + parameterActionInfo.ExclusiveMinimumValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.ExclusiveMaximum), parameterType, + parameterActionInfo.ExclusiveMaximumValue); _jsonWriter.PropertyWithNullableValue(nameof(ThingPropertyAttribute.MultipleOf), parameterActionInfo.MultipleOfValue); } + else if (jsonType == "string") + { + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.MinimumLength), parameterType, + parameterActionInfo.MinimumLengthValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.MaximumLength), parameterType, + parameterActionInfo.MaximumLengthValue); + _jsonWriter.PropertyString(nameof(ThingPropertyAttribute.Pattern), parameterType, + parameterActionInfo.Pattern); + } } _jsonWriter.EndObject(); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs index f63d93b..ec89cc0 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs @@ -85,9 +85,22 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri thingPropertyAttribute.MinimumValue); _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.Maximum), propertyType, thingPropertyAttribute.MaximumValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.ExclusiveMinimum), propertyType, + thingPropertyAttribute.ExclusiveMinimumValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.ExclusiveMaximum), propertyType, + thingPropertyAttribute.ExclusiveMaximumValue); _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.MultipleOf), propertyType, thingPropertyAttribute.MultipleOfValue); } + else if (jsonType == "string") + { + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.MinimumLength), propertyType, + thingPropertyAttribute.MinimumLengthValue); + _jsonWriter.PropertyNumber(nameof(ThingPropertyAttribute.MaximumLength), propertyType, + thingPropertyAttribute.MaximumLengthValue); + _jsonWriter.PropertyString(nameof(ThingPropertyAttribute.Pattern), propertyType, + thingPropertyAttribute.Pattern); + } } else { diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs index 80a9869..be29559 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs @@ -647,5 +647,16 @@ public void PropertyEnum(string propertyName, Type propertyType, object[]? @enum EndArray(); } + + public void PropertyString(string propertyName, Type propertyType, string? value) + { + if (value == null) + { + PropertyWithNullValue(propertyName); + return; + } + + PropertyWithValue(propertyName, value); + } } } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index a2f3797..b5d553e 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs @@ -73,12 +73,7 @@ private static IPropertyValidator CreateValidator(PropertyInfo propertyInfo, Thi { return new PropertyValidator( thingPropertyAttribute?.IsReadOnly ?? !propertyInfo.CanWrite, - thingPropertyAttribute?.MinimumValue, - thingPropertyAttribute?.MaximumValue, - thingPropertyAttribute?.MultipleOfValue, - Cast(thingPropertyAttribute?.Enum, propertyInfo.PropertyType), - propertyInfo.PropertyType == typeof(string) || Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null, - thingPropertyAttribute?.ExclusiveMinimumValue, thingPropertyAttribute?.ExclusiveMaximumValue); + propertyInfo.PropertyType.GetUnderlyingType(), thingPropertyAttribute); } private static IJsonMapper CreateMapper(Type type) @@ -163,215 +158,6 @@ private static IJsonMapper CreateMapper(Type type) throw new Exception(); } - private static object[] Cast(object?[] enums, Type type) - { - if (enums == null) - { - return null; - } - - type = Nullable.GetUnderlyingType(type) ?? type; - - var result = new object?[enums.Length]; - if (type == typeof(string)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToString(enums[i]); - } - } - - if(type == typeof(bool)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToBoolean(enums[i]); - } - } - - if (type == typeof(int)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToInt32(enums[i]); - } - } - - if (type == typeof(uint)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToUInt32(enums[i]); - } - } - - if (type == typeof(long)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToInt64(enums[i]); - } - } - - if (type == typeof(ulong)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToUInt64(enums[i]); - } - } - - if (type == typeof(short)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToInt16(enums[i]); - } - } - - if (type == typeof(ushort)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToUInt16(enums[i]); - } - } - - if (type == typeof(double)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToDouble(enums[i]); - } - } - - if (type == typeof(float)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToSingle(enums[i]); - } - } - - if (type == typeof(byte)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToByte(enums[i]); - } - } - - if (type == typeof(sbyte)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToSByte(enums[i]); - } - } - - if (type == typeof(decimal)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToDecimal(enums[i]); - } - } - - if (type == typeof(DateTime)) - { - for (var i = 0; i < enums.Length; i++) - { - if (enums[i] == null) - { - result[i] = null; - continue; - } - - result[i] = Convert.ToDateTime(enums[i]); - } - } - - return result; - } - public void After(Thing thing) { } diff --git a/src/Mozilla.IoT.WebThing/PropertyValidator.cs b/src/Mozilla.IoT.WebThing/PropertyValidator.cs index 0048804..953e852 100644 --- a/src/Mozilla.IoT.WebThing/PropertyValidator.cs +++ b/src/Mozilla.IoT.WebThing/PropertyValidator.cs @@ -1,97 +1,208 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; +using Mozilla.IoT.WebThing.Attributes; namespace Mozilla.IoT.WebThing { public class PropertyValidator : IPropertyValidator { - private readonly bool _isReadOnly; + private enum JsonType + { + String, + Number, + Array, + Bool + } + private readonly object[]? _enums; private readonly double? _minimum; private readonly double? _maximum; private readonly double? _exclusiveMinimum; private readonly double? _exclusiveMaximum; private readonly int? _multipleOf; - private readonly bool _acceptedNullableValue; + private readonly uint? _minimumLength; + private readonly uint? _maximumLength; + private readonly Regex? _patter; + private readonly JsonType _type; - public PropertyValidator(bool isReadOnly, - double? minimum, double? maximum, int? multipleOf, - object[]? enums, bool acceptedNullableValue, - double? exclusiveMinimum, double? exclusiveMaximum) + public PropertyValidator(bool isReadOnly, + Type propertyType, + ThingPropertyAttribute? propertyAttribute) { - _isReadOnly = isReadOnly; - _minimum = minimum; - _maximum = maximum; - _multipleOf = multipleOf; - _enums = enums; - _acceptedNullableValue = acceptedNullableValue; - _exclusiveMinimum = exclusiveMinimum; - _exclusiveMaximum = exclusiveMaximum; + IsReadOnly = isReadOnly; + + if (propertyAttribute != null) + { + _enums = propertyAttribute.Enum; + + _minimum = propertyAttribute.MinimumValue; + _maximum = propertyAttribute.MaximumValue; + _multipleOf = propertyAttribute.MultipleOfValue; + + _exclusiveMinimum = propertyAttribute.ExclusiveMinimumValue; + _exclusiveMaximum = propertyAttribute.ExclusiveMaximumValue; + + _minimumLength = propertyAttribute.MinimumLengthValue; + _maximumLength = propertyAttribute.MaximumLengthValue; + _patter = propertyAttribute.Pattern != null + ? new Regex(propertyAttribute.Pattern!, RegexOptions.Compiled) + : null; + } + + if (propertyType == typeof(string)) + { + _type = JsonType.String; + } + else if(propertyType == typeof(bool)) + { + _type = JsonType.Bool; + } + else if(propertyType == typeof(byte) + || propertyType == typeof(sbyte) + || propertyType == typeof(short) + || propertyType == typeof(ushort) + || propertyType == typeof(int) + || propertyType == typeof(uint) + || propertyType == typeof(long) + || propertyType == typeof(ulong) + || propertyType == typeof(float) + || propertyType == typeof(double) + || propertyType == typeof(decimal)) + { + _type = JsonType.Number; + + _enums = _enums?.Select(x => + { + if (x == null) + { + return (object)null; + } + return Convert.ToDouble(x); + }).Distinct().ToArray(); + } } - public bool IsReadOnly => _isReadOnly; + public bool IsReadOnly { get; } public bool IsValid(object? value) { - if (_isReadOnly) + if (IsReadOnly) { return false; } - if (_minimum.HasValue - || _maximum.HasValue - || _multipleOf.HasValue - || _exclusiveMinimum.HasValue - || _exclusiveMaximum.HasValue) + if (_type == JsonType.Number) { - - if (_acceptedNullableValue && value == null) - { - return true; - } - - var comparer = Convert.ToDouble(value); - if (_minimum.HasValue && comparer < _minimum.Value) + if (!IsValidNumber(value)) { return false; } + } - if (_maximum.HasValue && comparer > _maximum.Value) - { - return false; - } - - if (_exclusiveMinimum.HasValue && comparer <= _exclusiveMinimum.Value) + if (_type == JsonType.String) + { + if (!IsValidString(value)) { return false; } + } - if (_exclusiveMaximum.HasValue && comparer >= _exclusiveMaximum.Value) - { - return false; - } + return true; + } - if (_multipleOf.HasValue && comparer % _multipleOf.Value != 0) - { - return false; - } + + private bool IsValidNumber(object value) + { + if (!_minimum.HasValue + && !_maximum.HasValue && !_multipleOf.HasValue + && !_exclusiveMinimum.HasValue + && !_exclusiveMaximum.HasValue + && _enums == null) + { + return true; + } + + var isNull = value == null; + var comparer = Convert.ToDouble(value ?? 0); + if (_minimum.HasValue && (isNull || comparer < _minimum.Value)) + { + return false; + } + + if (_maximum.HasValue && comparer > _maximum.Value) + { + return false; + } + + if (_exclusiveMinimum.HasValue && (isNull || comparer <= _exclusiveMinimum.Value)) + { + return false; + } + + if (_exclusiveMaximum.HasValue && comparer >= _exclusiveMaximum.Value) + { + return false; + } + + if (_multipleOf.HasValue && (isNull || comparer % _multipleOf.Value != 0)) + { + return false; } if (_enums != null && !_enums.Any(x => { - if (value == null && x == null) + if (isNull && x == null) { return true; } - return value.Equals(x); + return comparer.Equals(x); })) { return false; } + + return true; + } - if (!_acceptedNullableValue && value == null) + private bool IsValidString(object value) + { + if (!_minimumLength.HasValue + && !_maximumLength.HasValue + && _patter == null + && _enums == null) + { + return true; + } + + var isNull = value == null; + var comparer = Convert.ToString(value ?? string.Empty); + if (_minimumLength.HasValue && (isNull || comparer.Length < _minimumLength.Value)) + { + return false; + } + + if (_maximumLength.HasValue && comparer.Length > _maximumLength.Value) + { + return false; + } + + if (_patter != null && !_patter.Match(comparer).Success) + { + return false; + } + + if (_enums != null && !_enums.Any(x => + { + if (isNull && x == null) + { + return true; + } + + return comparer.Equals(x); + })) { return false; } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/ActionType.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/ActionType.cs index bce8279..455af49 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/ActionType.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/ActionType.cs @@ -548,7 +548,70 @@ public class Run public DateTime TimeRequested { get; set; } public DateTime? TimeCompleted { get; set; } } - + + [Theory] + [InlineData("a")] + [InlineData("abc")] + [InlineData("0123456789")] + public async Task RunStringValidation(string min) + { + var email = "test@gmail.com"; + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PostAsync("/things/action-type/actions/runWithStringValidation", + new StringContent($@" +{{ + ""runWithStringValidation"": {{ + ""input"": {{ + ""minAnMax"": ""{min}"", + ""mail"": ""{email}"" + }} + }} +}}", Encoding.UTF8, "application/json"), source.Token).ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.Created); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + json.Input.Should().NotBeNull(); + json.Input.Mail.Should().Be(email); + json.Input.MinAnMax.Should().Be(min); + json.Status.Should().NotBeNullOrEmpty(); + } + + [Theory] + [InlineData(null, "test@tese.com")] + [InlineData("", "test@tese.com")] + [InlineData("a0123456789", "test@tese.com")] + [InlineData("abc", null)] + [InlineData("abc", "test")] + public async Task RunStringInvalidation(string min, string email) + { + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PostAsync("/things/action-type/actions/runWithStringValidation", + new StringContent($@" +{{ + ""runWithStringValidation"": {{ + ""input"": {{ + ""minAnMax"": ""{min}"", + ""mail"": ""{email}"" + }} + }} +}}", Encoding.UTF8, "application/json"), source.Token).ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeFalse(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + public class Input { public bool Bool { get; set; } @@ -568,7 +631,7 @@ public class Input public DateTimeOffset DateTimeOffset { get; set; } } - + public class RunNull { public InputNull Input { get; set; } @@ -596,5 +659,18 @@ public class InputNull public DateTime? DateTime { get; set; } public DateTimeOffset? DateTimeOffset { get; set; } } + public class RunString + { + public InputString Input { get; set; } + public string Href { get; set; } + public string Status { get; set; } + public DateTime TimeRequested { get; set; } + public DateTime? TimeCompleted { get; set; } + } + public class InputString + { + public string? MinAnMax { get; set; } + public string? Mail { get; set; } + } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/PropertiesValidation.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/PropertiesValidation.cs new file mode 100644 index 0000000..5139566 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/PropertiesValidation.cs @@ -0,0 +1,266 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Http +{ + public class PropertiesValidation + { + private readonly Fixture _fixture; + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30_000); + private readonly HttpClient _client; + public PropertiesValidation() + { + _fixture = new Fixture(); + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + } + + #region PUT + + [Theory] + [InlineData("numberByte", 1)] + [InlineData("numberByte", 10)] + [InlineData("numberByte", 100)] + [InlineData("numberSByte", 1)] + [InlineData("numberSByte", 10)] + [InlineData("numberSByte", 100)] + [InlineData("numberShort", 1)] + [InlineData("numberShort", 10)] + [InlineData("numberShort", 100)] + [InlineData("numberUShort", 1)] + [InlineData("numberUShort", 10)] + [InlineData("numberUShort", 100)] + [InlineData("numberInt", 1)] + [InlineData("numberInt", 10)] + [InlineData("numberInt", 100)] + [InlineData("numberUInt", 1)] + [InlineData("numberUInt", 10)] + [InlineData("numberUInt", 100)] + [InlineData("numberLong", 1)] + [InlineData("numberLong", 10)] + [InlineData("numberLong", 100)] + [InlineData("numberULong", 1)] + [InlineData("numberULong", 10)] + [InlineData("numberULong", 100)] + [InlineData("numberDouble", 1)] + [InlineData("numberDouble", 10)] + [InlineData("numberDouble", 100)] + [InlineData("numberFloat", 1)] + [InlineData("numberFloat", 10)] + [InlineData("numberFloat", 100)] + [InlineData("numberDecimal", 1)] + [InlineData("numberDecimal", 10)] + [InlineData("numberDecimal", 100)] + [InlineData("nullableByte", 1)] + [InlineData("nullableByte", 10)] + [InlineData("nullableByte", 100)] + [InlineData("nullableSByte", 1)] + [InlineData("nullableSByte", 10)] + [InlineData("nullableSByte", 100)] + [InlineData("nullableShort", 1)] + [InlineData("nullableShort", 10)] + [InlineData("nullableShort", 100)] + [InlineData("nullableUShort", 1)] + [InlineData("nullableUShort", 10)] + [InlineData("nullableUShort", 100)] + [InlineData("nullableInt", 1)] + [InlineData("nullableInt", 10)] + [InlineData("nullableInt", 100)] + [InlineData("nullableUInt", 1)] + [InlineData("nullableUInt", 10)] + [InlineData("nullableUInt", 100)] + [InlineData("nullableLong", 1)] + [InlineData("nullableLong", 10)] + [InlineData("nullableLong", 100)] + [InlineData("nullableULong", 1)] + [InlineData("nullableULong", 10)] + [InlineData("nullableULong", 100)] + [InlineData("nullableDouble", 1)] + [InlineData("nullableDouble", 10)] + [InlineData("nullableDouble", 100)] + [InlineData("nullableFloat", 1)] + [InlineData("nullableFloat", 10)] + [InlineData("nullableFloat", 100)] + [InlineData("nullableDecimal", 1)] + [InlineData("nullableDecimal", 10)] + [InlineData("nullableDecimal", 100)] + public async Task PutNumber(string property, object value) + { + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PutAsync($"/things/property-validation-type/properties/{property}", + new StringContent($@"{{ ""{property}"": {value} }}"), source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value} }}")); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + response = await _client.GetAsync($"/things/property-validation-type/properties/{property}", source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value} }}")); + } + + [Theory] + [InlineData("numberByte", 0)] + [InlineData("numberByte", 101)] + [InlineData("numberSByte", 0)] + [InlineData("numberSByte", 101)] + [InlineData("numberShort", 0)] + [InlineData("numberShort", 101)] + [InlineData("numberUShort", 0)] + [InlineData("numberUShort", 101)] + [InlineData("numberInt", 0)] + [InlineData("numberInt", 101)] + [InlineData("numberUInt", 0)] + [InlineData("numberUInt", 101)] + [InlineData("numberLong", 0)] + [InlineData("numberLong", 101)] + [InlineData("numberULong", 0)] + [InlineData("numberULong", 101)] + [InlineData("numberDouble", 0)] + [InlineData("numberDouble", 101)] + [InlineData("numberFloat", 0)] + [InlineData("numberFloat", 101)] + [InlineData("numberDecimal", 0)] + [InlineData("numberDecimal", 101)] + [InlineData("nullableByte", 0)] + [InlineData("nullableByte", 101)] + [InlineData("nullableSByte", 0)] + [InlineData("nullableSByte", 101)] + [InlineData("nullableShort", 0)] + [InlineData("nullableShort", 101)] + [InlineData("nullableUShort", 0)] + [InlineData("nullableUShort", 101)] + [InlineData("nullableInt", 0)] + [InlineData("nullableInt", 101)] + [InlineData("nullableUInt", 0)] + [InlineData("nullableUInt", 101)] + [InlineData("nullableLong", 0)] + [InlineData("nullableLong", 101)] + [InlineData("nullableULong", 0)] + [InlineData("nullableULong", 101)] + [InlineData("nullableDouble", 0)] + [InlineData("nullableDouble", 101)] + [InlineData("nullableFloat", 0)] + [InlineData("nullableFloat", 101)] + [InlineData("nullableDecimal", 0)] + [InlineData("nullableDecimal", 101)] + public async Task PutInvalidNumber(string property, object value) + { + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PutAsync($"/things/property-validation-type/properties/{property}", + new StringContent($@"{{ ""{property}"": {value} }}"), source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeFalse(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Theory] + [InlineData("text", "abc")] + [InlineData("email", "text@test.com")] + public async Task PutStringValue(string property, string value) + { + value = value != null ? $"\"{value}\"" : "null"; + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + var response = await _client.PutAsync($"/things/property-validation-type/properties/{property}", + new StringContent($@"{{ ""{property}"": {value} }}"), source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value} }}")); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + response = await _client.GetAsync($"/things/property-validation-type/properties/{property}", source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value} }}")); + } + + [Theory] + [InlineData("text", "")] + [InlineData("text", null)] + [InlineData("email", "text")] + [InlineData("email", null)] + public async Task PutInvalidString(string property, string value) + { + value = value != null ? $"\"{value}\"" : "null"; + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + var response = await _client.PutAsync($"/things/property-validation-type/properties/{property}", + new StringContent($@"{{ ""{property}"": {value} }}"), source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeFalse(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + #endregion + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs index 59810de..eac17e0 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs @@ -13,6 +13,7 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest public class Startup { public static Action? Option { get; set; } + // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) @@ -29,9 +30,11 @@ public void ConfigureServices(IServiceCollection services) .AddThing() .AddThing() .AddThing() + .AddThing() + .AddThing() ; - services.AddWebSockets(o => { }); + services.AddWebSockets(o => { }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionTypeThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionTypeThing.cs index 76460ce..b6cec49 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionTypeThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionTypeThing.cs @@ -88,5 +88,14 @@ [FromServices]ILogger logger { logger.LogInformation("Execution action...."); } + + public void RunWithStringValidation( + [ThingParameter(MinimumLength = 1, MaximumLength = 10)]string @minAnMax, + [ThingParameter(Pattern = @"^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$")]string mail, + [FromServices]ILogger logger + ) + { + logger.LogInformation("Execution action...."); + } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThingType.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThingType.cs index 4fb1fb8..02702bd 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThingType.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThingType.cs @@ -1,6 +1,4 @@ using System; -using System.Threading.Tasks; -using Mozilla.IoT.WebThing.Attributes; namespace Mozilla.IoT.WebThing.AcceptanceTest.Things { diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyValidationThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyValidationThing.cs new file mode 100644 index 0000000..09e0d29 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyValidationThing.cs @@ -0,0 +1,298 @@ +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class PropertyValidationThing : Thing + { + public override string Name => "property-validation-type"; + + private byte _numberByte; + [ThingProperty(Minimum = 1, Maximum = 100)] + public byte NumberByte + { + get => _numberByte; + set + { + _numberByte = value; + OnPropertyChanged(); + } + } + + private byte? _nullableByte; + [ThingProperty(Minimum = 1, Maximum = 100)] + public byte? NullableByte + { + get => _nullableByte; + set + { + _nullableByte = value; + OnPropertyChanged(); + } + } + + private sbyte _numberSByte; + [ThingProperty(Minimum = 1, Maximum = 100)] + public sbyte NumberSByte + { + get => _numberSByte; + set + { + _numberSByte = value; + OnPropertyChanged(); + } + } + + private sbyte? _nullableSByte; + [ThingProperty(Minimum = 1, Maximum = 100)] + public sbyte? NullableSByte + { + get => _nullableSByte; + set + { + _nullableSByte = value; + OnPropertyChanged(); + } + } + + private short _numberShort; + [ThingProperty(Minimum = 1, Maximum = 100)] + public short NumberShort + { + get => _numberShort; + set + { + _numberShort = value; + OnPropertyChanged(); + } + } + + private short? _nullableShort; + [ThingProperty(Minimum = 1, Maximum = 100)] + public short? NullableShort + { + get => _nullableShort; + set + { + _nullableShort = value; + OnPropertyChanged(); + } + } + + private ushort _numberUShort; + [ThingProperty(Minimum = 1, Maximum = 100)] + public ushort NumberUShort + { + get => _numberUShort; + set + { + _numberUShort = value; + OnPropertyChanged(); + } + } + + private ushort? _nullableUShort; + [ThingProperty(Minimum = 1, Maximum = 100)] + public ushort? NullableUShort + { + get => _nullableUShort; + set + { + _nullableUShort = value; + OnPropertyChanged(); + } + } + + private int _numberInt; + [ThingProperty(Minimum = 1, Maximum = 100)] + public int NumberInt + { + get => _numberInt; + set + { + _numberInt = value; + OnPropertyChanged(); + } + } + + private int? _nullableInt; + [ThingProperty(Minimum = 1, Maximum = 100)] + public int? NullableInt + { + get => _nullableInt; + set + { + _nullableInt = value; + OnPropertyChanged(); + } + } + + private uint _numberUInt; + [ThingProperty(Minimum = 1, Maximum = 100)] + public uint NumberUInt + { + get => _numberUInt; + set + { + _numberUInt = value; + OnPropertyChanged(); + } + } + + private uint? _nullableUInt; + [ThingProperty(Minimum = 1, Maximum = 100)] + public uint? NullableUInt + { + get => _nullableUInt; + set + { + _nullableUInt = value; + OnPropertyChanged(); + } + } + + private long _numberLong; + [ThingProperty(Minimum = 1, Maximum = 100)] + public long NumberLong + { + get => _numberLong; + set + { + _numberLong = value; + OnPropertyChanged(); + } + } + + private long? _nullableLong; + [ThingProperty(Minimum = 1, Maximum = 100)] + public long? NullableLong + { + get => _nullableLong; + set + { + _nullableLong = value; + OnPropertyChanged(); + } + } + + private ulong _numberULong; + [ThingProperty(Minimum = 1, Maximum = 100)] + public ulong NumberULong + { + get => _numberULong; + set + { + _numberULong = value; + OnPropertyChanged(); + } + } + + private ulong? _nullableULong; + [ThingProperty(Minimum = 1, Maximum = 100)] + public ulong? NullableULong + { + get => _nullableULong; + set + { + _nullableULong = value; + OnPropertyChanged(); + } + } + + private double _numberDouble; + [ThingProperty(Minimum = 1, Maximum = 100)] + public double NumberDouble + { + get => _numberDouble; + set + { + _numberDouble = value; + OnPropertyChanged(); + } + } + + private double? _nullableDouble; + [ThingProperty(Minimum = 1, Maximum = 100)] + public double? NullableDouble + { + get => _nullableDouble; + set + { + _nullableDouble = value; + OnPropertyChanged(); + } + } + + private float _numberFloat; + [ThingProperty(Minimum = 1, Maximum = 100)] + public float NumberFloat + { + get => _numberFloat; + set + { + _numberFloat = value; + OnPropertyChanged(); + } + } + + private float? _nullableFloat; + [ThingProperty(Minimum = 1, Maximum = 100)] + public float? NullableFloat + { + get => _nullableFloat; + set + { + _nullableFloat = value; + OnPropertyChanged(); + } + } + + private decimal _numberDecimal; + [ThingProperty(Minimum = 1, Maximum = 100)] + public decimal NumberDecimal + { + get => _numberDecimal; + set + { + _numberDecimal = value; + OnPropertyChanged(); + } + } + + private decimal? _nullableDecimal; + [ThingProperty(Minimum = 1, Maximum = 100)] + public decimal? NullableDecimal + { + get => _nullableDecimal; + set + { + _nullableDecimal = value; + OnPropertyChanged(); + } + } + + + private string _text; + [ThingProperty(MinimumLength = 1, MaximumLength = 100)] + public string Text + { + get => _text; + set + { + _text = value; + OnPropertyChanged(); + } + } + + private string _email; + [ThingProperty(Pattern = @"^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$")] + public string Email + { + get => _email; + set + { + _email = value; + OnPropertyChanged(); + } + } + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyEnumThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyEnumThing.cs index 0efd65f..da77867 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyEnumThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyEnumThing.cs @@ -1,5 +1,3 @@ -using Mozilla.IoT.WebThing.Attributes; - namespace Mozilla.IoT.WebThing.AcceptanceTest.Things { public class WebSocketPropertyEnumThing : PropertyEnumThing diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyValidationThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyValidationThing.cs new file mode 100644 index 0000000..b507a4a --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyValidationThing.cs @@ -0,0 +1,7 @@ +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class WebSocketPropertyValidationThing : PropertyValidationThing + { + public override string Name => "web-socket-property-validation-type"; + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/ActionType.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/ActionType.cs index a9f3c5d..ebade6b 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/ActionType.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/ActionType.cs @@ -791,6 +791,142 @@ await socket json.Data.Message.Should().Be("Invalid action request"); } + [Theory] + [InlineData("a")] + [InlineData("abc")] + [InlineData("0123456789")] + public async Task RunWithStringValidationValid(string min) + { + var email = "test@gmail.com"; + + + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/action-type" + }.Uri; + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await webSocketClient.ConnectAsync(uri, source.Token) + .ConfigureAwait(false); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""requestAction"", + ""data"": {{ + ""runWithStringValidation"": {{ + ""input"": {{ + ""minAnMax"": ""{min}"", + ""mail"": ""{email}"" + }} + }} + }} +}}"), WebSocketMessageType.Text, true, + source.Token) + .ConfigureAwait(false); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, source.Token) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var message = Encoding.UTF8.GetString(segment.Slice(0, result.Count)); + var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}); + + json.MessageType.Should().Be("actionStatus"); + json.Data.RunWithStringValidation.Input.MinAnMax.Should().Be(min); + json.Data.RunWithStringValidation.Input.Mail.Should().Be(email); + } + + + [Theory] + [InlineData(null, "test@tese.com")] + [InlineData("", "test@tese.com")] + [InlineData("a0123456789", "test@tese.com")] + [InlineData("abc", null)] + [InlineData("abc", "test")] + public async Task RunWithStringValidationInvalid(string min, string email) + { + + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/action-type" + }.Uri; + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await webSocketClient.ConnectAsync(uri, source.Token) + .ConfigureAwait(false); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""requestAction"", + ""data"": {{ + ""runWithStringValidation"": {{ + ""input"": {{ + ""minAnMax"": ""{min}"", + ""mail"": ""{email}"" + }} + }} + }} +}}"), WebSocketMessageType.Text, true, + source.Token) + .ConfigureAwait(false); + + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, source.Token) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var message = Encoding.UTF8.GetString(segment.Slice(0, result.Count)); + var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}); + + json.MessageType.Should().Be("error"); + json.Data.Status.Should().Be("400 Bad Request"); + json.Data.Message.Should().Be("Invalid action request"); + } + + public class Message { public string MessageType { get; set; } @@ -806,6 +942,9 @@ public class ActionSocket public Http.ActionType.Run RunWithValidation { get; set; } public Http.ActionType.Run RunWithValidationExclusive { get; set; } + + + public Http.ActionType.RunString RunWithStringValidation { get; set; } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/PropertyValidationType.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/PropertyValidationType.cs new file mode 100644 index 0000000..24b43c3 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/PropertyValidationType.cs @@ -0,0 +1,270 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets +{ + public class PropertyValidationType + { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly WebSocketClient _webSocketClient; + private readonly HttpClient _client; + private readonly Uri _uri; + + public PropertyValidationType() + { + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + _webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + _uri = new UriBuilder(_client.BaseAddress) + { + Scheme = "ws", + Path = "/things/web-socket-property-validation-type" + }.Uri; + } + + [Theory] + [InlineData("numberByte", 1)] + [InlineData("numberByte", 10)] + [InlineData("numberByte", 100)] + [InlineData("numberSByte", 1)] + [InlineData("numberSByte", 10)] + [InlineData("numberSByte", 100)] + [InlineData("numberShort", 1)] + [InlineData("numberShort", 10)] + [InlineData("numberShort", 100)] + [InlineData("numberUShort", 1)] + [InlineData("numberUShort", 10)] + [InlineData("numberUShort", 100)] + [InlineData("numberInt", 1)] + [InlineData("numberInt", 10)] + [InlineData("numberInt", 100)] + [InlineData("numberUInt", 1)] + [InlineData("numberUInt", 10)] + [InlineData("numberUInt", 100)] + [InlineData("numberLong", 1)] + [InlineData("numberLong", 10)] + [InlineData("numberLong", 100)] + [InlineData("numberULong", 1)] + [InlineData("numberULong", 10)] + [InlineData("numberULong", 100)] + [InlineData("numberDouble", 1)] + [InlineData("numberDouble", 10)] + [InlineData("numberDouble", 100)] + [InlineData("numberFloat", 1)] + [InlineData("numberFloat", 10)] + [InlineData("numberFloat", 100)] + [InlineData("numberDecimal", 1)] + [InlineData("numberDecimal", 10)] + [InlineData("numberDecimal", 100)] + [InlineData("nullableByte", 1)] + [InlineData("nullableByte", 10)] + [InlineData("nullableByte", 100)] + [InlineData("nullableSByte", 1)] + [InlineData("nullableSByte", 10)] + [InlineData("nullableSByte", 100)] + [InlineData("nullableShort", 1)] + [InlineData("nullableShort", 10)] + [InlineData("nullableShort", 100)] + [InlineData("nullableUShort", 1)] + [InlineData("nullableUShort", 10)] + [InlineData("nullableUShort", 100)] + [InlineData("nullableInt", 1)] + [InlineData("nullableInt", 10)] + [InlineData("nullableInt", 100)] + [InlineData("nullableUInt", 1)] + [InlineData("nullableUInt", 10)] + [InlineData("nullableUInt", 100)] + [InlineData("nullableLong", 1)] + [InlineData("nullableLong", 10)] + [InlineData("nullableLong", 100)] + [InlineData("nullableULong", 1)] + [InlineData("nullableULong", 10)] + [InlineData("nullableULong", 100)] + [InlineData("nullableDouble", 1)] + [InlineData("nullableDouble", 10)] + [InlineData("nullableDouble", 100)] + [InlineData("nullableFloat", 1)] + [InlineData("nullableFloat", 10)] + [InlineData("nullableFloat", 100)] + [InlineData("nullableDecimal", 1)] + [InlineData("nullableDecimal", 10)] + [InlineData("nullableDecimal", 100)] + [InlineData("text", "abc")] + [InlineData("email", "text@test.com")] + public async Task SetProperties(string property, object value) + { + if (value is string || value == null) + { + value = value != null ? $"\"{value}\"" : "null"; + } + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await _webSocketClient.ConnectAsync(_uri, source.Token) + .ConfigureAwait(false); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""setProperty"", + ""data"": {{ + ""{property}"": {value} + }} +}}"), WebSocketMessageType.Text, true, + source.Token) + .ConfigureAwait(false); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, source.Token) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JToken.Parse(Encoding.UTF8.GetString(segment.Slice(0, result.Count))); + + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@" +{{ + ""messageType"": ""propertyStatus"", + ""data"": {{ + ""{property}"": {value} + }} +}}")); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/web-socket-property-validation-type/properties/{property}", source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value} }}")); + } + + [Theory] + [InlineData("numberByte", 0)] + [InlineData("numberByte", 101)] + [InlineData("numberSByte", 0)] + [InlineData("numberSByte", 101)] + [InlineData("numberShort", 0)] + [InlineData("numberShort", 101)] + [InlineData("numberUShort", 0)] + [InlineData("numberUShort", 101)] + [InlineData("numberInt", 0)] + [InlineData("numberInt", 101)] + [InlineData("numberUInt", 0)] + [InlineData("numberUInt", 101)] + [InlineData("numberLong", 0)] + [InlineData("numberLong", 101)] + [InlineData("numberULong", 0)] + [InlineData("numberULong", 101)] + [InlineData("numberDouble", 0)] + [InlineData("numberDouble", 101)] + [InlineData("numberFloat", 0)] + [InlineData("numberFloat", 101)] + [InlineData("numberDecimal", 0)] + [InlineData("numberDecimal", 101)] + [InlineData("nullableByte", 0)] + [InlineData("nullableByte", 101)] + [InlineData("nullableSByte", 0)] + [InlineData("nullableSByte", 101)] + [InlineData("nullableShort", 0)] + [InlineData("nullableShort", 101)] + [InlineData("nullableUShort", 0)] + [InlineData("nullableUShort", 101)] + [InlineData("nullableInt", 0)] + [InlineData("nullableInt", 101)] + [InlineData("nullableUInt", 0)] + [InlineData("nullableUInt", 101)] + [InlineData("nullableLong", 0)] + [InlineData("nullableLong", 101)] + [InlineData("nullableULong", 0)] + [InlineData("nullableULong", 101)] + [InlineData("nullableDouble", 0)] + [InlineData("nullableDouble", 101)] + [InlineData("nullableFloat", 0)] + [InlineData("nullableFloat", 101)] + [InlineData("nullableDecimal", 0)] + [InlineData("nullableDecimal", 101)] + [InlineData("text", "")] + [InlineData("text", null)] + [InlineData("email", "text")] + [InlineData("email", null)] + public async Task SetPropertiesInvalidNumber(string property, object value) + { + if (value is string || value == null) + { + value = value != null ? $"\"{value}\"" : "null"; + } + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await _webSocketClient.ConnectAsync(_uri, source.Token) + .ConfigureAwait(false); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""setProperty"", + ""data"": {{ + ""{property}"": {value} + }} +}}"), WebSocketMessageType.Text, true, + source.Token) + .ConfigureAwait(false); + + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, source.Token) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JToken.Parse(Encoding.UTF8.GetString(segment.Slice(0, result.Count))); + + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@" +{{ + ""messageType"": ""error"", + ""data"": {{ + ""message"": ""Invalid property value"", + ""status"": ""400 Bad Request"" + }} +}}")); + } + } +}