From 2313e727f0b9a171bb54fdb9d01666f0ea14bfb9 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 22:23:40 +0000 Subject: [PATCH 01/13] Fixes error to add thing using thing-adapter --- sample/MultiThing/Startup.cs | 3 +- .../MultiThing/Things/ExampleDimmableLight.cs | 15 ++++- .../Things/FakeGpioHumiditySensor.cs | 24 ++++++-- sample/MultiThing/appsettings.json | 2 +- .../Converts/ThingConverter.cs | 4 +- .../Endpoints/GetAction.cs | 8 ++- .../Endpoints/GetActionById.cs | 9 +-- .../Endpoints/GetActions.cs | 5 +- .../Endpoints/GetAllThings.cs | 2 +- .../Endpoints/GetEvent.cs | 8 +-- .../Endpoints/GetEvents.cs | 6 +- .../Endpoints/GetProperties.cs | 6 +- .../Endpoints/GetProperty.cs | 8 +-- .../Endpoints/GetThing.cs | 8 +-- .../IEndpointRouteBuilderExtensions.cs | 1 + .../Extensions/ThingCollectionBuilder.cs | 8 +-- .../Converter/ConverterPropertyIntercept.cs | 15 +++-- .../Converter/Utf8JsonWriterILGenerator.cs | 2 +- .../Properties/PropertiesIntercept.cs | 2 +- .../WebSockets/WebSocket.cs | 57 +++++++++++-------- 20 files changed, 120 insertions(+), 73 deletions(-) diff --git a/sample/MultiThing/Startup.cs b/sample/MultiThing/Startup.cs index 2617e59..a529835 100644 --- a/sample/MultiThing/Startup.cs +++ b/sample/MultiThing/Startup.cs @@ -30,7 +30,8 @@ public void ConfigureServices(IServiceCollection services) { services.AddThings() .AddThing() - .AddThing(); + .AddThing() + ; services.AddWebSockets(opt => { }); } diff --git a/sample/MultiThing/Things/ExampleDimmableLight.cs b/sample/MultiThing/Things/ExampleDimmableLight.cs index 9b84ee4..f4f7540 100644 --- a/sample/MultiThing/Things/ExampleDimmableLight.cs +++ b/sample/MultiThing/Things/ExampleDimmableLight.cs @@ -9,6 +9,13 @@ namespace MultiThing.Things { public class ExampleDimmableLight : Thing { + private readonly ILogger _logger; + + public ExampleDimmableLight(ILogger logger) + { + _logger = logger; + } + public override string Name => "my-lamp-1234"; public override string Title => "My Lamp"; @@ -27,12 +34,14 @@ public bool On { _on = value; - Console.WriteLine($"On-State is now {_on}"); + _logger.LogInformation("On-State is now {on}", _on); + OnPropertyChanged(); } } private int _brightness = 50; + [ThingProperty(Type = new []{ "BrightnessProperty" }, Title = "Brightness", Description = "The level of light from 0-100", Minimum = 0, Maximum = 100, Unit = "percent")] public int Brightness @@ -41,8 +50,8 @@ public int Brightness set { _brightness = value; - - Console.WriteLine($"Brightness is now {_brightness}"); + _logger.LogInformation("Brightness is now {brightness}", _brightness); + OnPropertyChanged(); } } diff --git a/sample/MultiThing/Things/FakeGpioHumiditySensor.cs b/sample/MultiThing/Things/FakeGpioHumiditySensor.cs index 52043c4..ba0e7d2 100644 --- a/sample/MultiThing/Things/FakeGpioHumiditySensor.cs +++ b/sample/MultiThing/Things/FakeGpioHumiditySensor.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing; using Mozilla.IoT.WebThing.Attributes; @@ -8,8 +9,10 @@ namespace MultiThing.Things public class FakeGpioHumiditySensor : Thing { private readonly Random _random; - public FakeGpioHumiditySensor() + private readonly ILogger _logger; + public FakeGpioHumiditySensor(ILogger logger) { + _logger = logger; _random = new Random(); Task.Factory.StartNew(() => { @@ -17,7 +20,6 @@ public FakeGpioHumiditySensor() { Task.Delay(3_000).GetAwaiter().GetResult(); var newLevel = ReadFromGPIO(); - Console.WriteLine("setting new humidity level: {0}", newLevel); Level = newLevel; } }, TaskCreationOptions.LongRunning); @@ -29,11 +31,21 @@ public FakeGpioHumiditySensor() public override string[] Type { get; } = new[] {"MultiLevelSensor"}; public override string Description => "A web connected humidity sensor"; - - - [ThingProperty(Type = new []{"LevelProperty"}, Title = "Humidity", Description = "The current humidity in %", + + private double _level; + + [ThingProperty(Type = new[] {"LevelProperty"}, Title = "Humidity", Description = "The current humidity in %", Minimum = 0, Maximum = 100, Unit = "percent")] - public double Level { get; private set; } + public double Level + { + get => _level; + private set + { + _level = value; + _logger.LogInformation("setting new humidity level: {level}", value); + OnPropertyChanged(); + } + } /// /// Mimic an actual sensor updating its reading every couple seconds. diff --git a/sample/MultiThing/appsettings.json b/sample/MultiThing/appsettings.json index 81ff877..3f32442 100644 --- a/sample/MultiThing/appsettings.json +++ b/sample/MultiThing/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } diff --git a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs index 0b89e1a..315375f 100644 --- a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs +++ b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs @@ -45,8 +45,10 @@ public override void Write(Utf8JsonWriter writer, Thing value, JsonSerializerOpt writer.WriteStartObject(); writer.WriteString("@context", value.Context); var builder = new UriBuilder(value.Prefix) {Path = $"/things/{options.GetPropertyName(value.Name)}"}; - WriteProperty(writer, "Id", builder.Uri.ToString(), options); + WriteProperty(writer, "Id", value.Name, options); value.ThingContext.Converter.Write(writer, value, options); + WriteProperty(writer, "href", builder.Path, options); + WriteProperty(writer, "base", builder.Uri.ToString(), options); StartArray(writer, "Links", options); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs index b7259cb..9ee9015 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs @@ -18,12 +18,13 @@ public static async Task InvokeAsync(HttpContext context) var logger = service.GetRequiredService>(); var things = service.GetRequiredService>(); var thingName = context.GetRouteData("name"); - logger.LogInformation("Requesting Action for Thing. [Name: {name}]", thingName); + + logger.LogInformation("Requesting Action for Thing. [Thing: {name}]", thingName); var thing = things.FirstOrDefault(x => x.Name.Equals(thingName, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", thingName); + logger.LogInformation("Thing not found. [Thing: {name}]", thingName); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } @@ -34,11 +35,12 @@ public static async Task InvokeAsync(HttpContext context) if (!thing.ThingContext.Actions.TryGetValue(actionName, out var actionContext)) { - logger.LogInformation("{actionName} Action not found in {thingName}", actionName, thingName); + logger.LogInformation("Action not found. [Thing: {name}][Action: {action}]", thingName, actionName); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } + logger.LogInformation("Action found. [Thing: {name}][Action: {action}]", thingName, actionName); await context.WriteBodyAsync(HttpStatusCode.OK, actionContext.Actions, option) .ConfigureAwait(false); } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs index 7fe8b9e..de735a6 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs @@ -18,12 +18,12 @@ public static async Task InvokeAsync(HttpContext context) var logger = service.GetRequiredService>(); var things = service.GetRequiredService>(); var thingName = context.GetRouteData("name"); - logger.LogInformation("Requesting Action for Thing. [Name: {name}]", thingName); + logger.LogInformation("Requesting Action for Thing. [Thing: {name}]", thingName); var thing = things.FirstOrDefault(x => x.Name.Equals(thingName, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", thingName); + logger.LogInformation("Thing not found. [Thing: {name}]", thingName); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } @@ -35,18 +35,19 @@ public static async Task InvokeAsync(HttpContext context) if (!thing.ThingContext.Actions.TryGetValue(actionName, out var actionContext)) { - logger.LogInformation("{actionName} Action not found in {thingName}", actionName, thingName); + logger.LogInformation("Action not found. [Thing: {name}][Action: {action}]", thingName, actionName); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } if (!actionContext.Actions.TryGetValue(id, out var actionInfo)) { - logger.LogInformation("{actionName} Action with {id} id not found in {thingName}", actionName, id, thingName); + logger.LogInformation("Action id not found. [Thing: {name}][Action: {action}][Id: {id}]", thingName, actionName, id); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } + logger.LogInformation("Action Id found. [Thing: {name}][Action: {action}][Id: {id}]", thingName, actionName, id); await context.WriteBodyAsync(HttpStatusCode.OK, actionInfo, option) .ConfigureAwait(false); } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs index 59153ca..03c0f0e 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs @@ -19,12 +19,12 @@ public static async Task InvokeAsync(HttpContext context) var things = service.GetRequiredService>(); var thingName = context.GetRouteData("name"); - logger.LogInformation("Requesting Action for Thing. [Name: {name}]", thingName); + logger.LogInformation("Requesting Action for Thing. [Thing: {name}]", thingName); var thing = things.FirstOrDefault(x => x.Name.Equals(thingName, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", thingName); + logger.LogInformation("Thing not found. [Thing: {name}]", thingName); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } @@ -41,6 +41,7 @@ public static async Task InvokeAsync(HttpContext context) } } + logger.LogInformation("Found {counter} Actions. [Thing: {name}]", result.Count, thingName); await context.WriteBodyAsync(HttpStatusCode.OK, result, option) .ConfigureAwait(false); } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs index 072900e..3a445a7 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs @@ -31,7 +31,7 @@ internal static Task InvokeAsync(HttpContext context) } } - logger.LogTrace("Found {counter} things", things.Count()); + logger.LogInformation("Found {counter} things", things.Count()); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs index 9ea4624..8d9e7b7 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs @@ -20,13 +20,13 @@ public static Task InvokeAsync(HttpContext context) var things = service.GetRequiredService>(); var name = context.GetRouteData("name"); - logger.LogInformation("Requesting Thing. [Name: {name}]", name); + logger.LogInformation("Requesting Thing. [Thing: {name}]", name); var thing = things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", name); + logger.LogInformation("Thing not found. [Thing: {name}]", name); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } @@ -35,7 +35,7 @@ public static Task InvokeAsync(HttpContext context) if (!thing.ThingContext.Events.TryGetValue(@event, out var events)) { - logger.LogInformation("Event not found.[Name: {thingName}][Event: {eventName}]", thing.Name, @event); + logger.LogInformation("Event not found.[Thing: {thingName}][Event: {eventName}]", thing.Name, @event); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } @@ -47,7 +47,7 @@ public static Task InvokeAsync(HttpContext context) result.AddLast(new Dictionary {[@event] = e}); } - logger.LogInformation("Found {counter} {eventName} events. [Name: {name}]", result.Count, @event, thing.Name); + logger.LogInformation("Found {counter} events. [Thing: {name}][Event: {eventName}]", result.Count, thing.Name, @event); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs index bc2c289..143508b 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs @@ -20,13 +20,13 @@ public static Task InvokeAsync(HttpContext context) var things = service.GetRequiredService>(); var name = context.GetRouteData("name"); - logger.LogInformation("Requesting Thing. [Name: {name}]", name); + logger.LogInformation("Requesting Events. [Thing: {name}]", name); var thing = things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", name); + logger.LogInformation("Thing not found. [Thing: {name}]", name); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } @@ -43,7 +43,7 @@ public static Task InvokeAsync(HttpContext context) } - logger.LogInformation("Found Thing with {counter} events. [Name: {name}]", result.Count, thing.Name); + logger.LogInformation("Found Thing with {counter} events. [Thing: {name}]", result.Count, thing.Name); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs index 4e0386f..41b7aa5 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs @@ -21,18 +21,18 @@ public static Task InvokeAsync(HttpContext context) var name = context.GetRouteData("name"); - logger.LogInformation("Requesting Thing. [Name: {name}]", name); + logger.LogInformation("Requesting Thing. [Thing: {name}]", name); var thing = things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", name); + logger.LogInformation("Thing not found. [Thing: {name}]", name); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } var properties = thing.ThingContext.Properties.GetProperties()!; - logger.LogInformation("Found Thing with {counter} properties. [Name: {name}]", properties.Count, thing.Name); + logger.LogInformation("Found Thing with {counter} properties. [Thing: {name}]", properties.Count, thing.Name); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs index 50ba07f..2093e41 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs @@ -21,12 +21,12 @@ public static Task InvokeAsync(HttpContext context) var name = context.GetRouteData("name"); - logger.LogInformation("Requesting Thing. [Name: {name}]", name); + logger.LogInformation("Requesting Thing. [Thing: {name}]", name); var thing = things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", name); + logger.LogInformation("Thing not found. [Thing: {name}]", name); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } @@ -36,12 +36,12 @@ public static Task InvokeAsync(HttpContext context) if (properties == null) { - logger.LogInformation("Property not found. [Thing Name: {thingName}][Property Name: {propertyName}]", thing.Name, property); + logger.LogInformation("Property not found. [Thing: {thingName}][Property: {propertyName}]", thing.Name, property); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } - logger.LogInformation("Found Thing with {property} Property. [Name: {name}]", property, thing.Name); + logger.LogInformation("Found Property. [Thing: {thingName}][Property: {propertyName}]", thing.Name, property); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs index 141effe..8369e5d 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs @@ -21,25 +21,25 @@ internal static Task InvokeAsync(HttpContext context) var things = service.GetRequiredService>(); var name = context.GetRouteData("name"); - logger.LogInformation("Requesting Thing. [Name: {name}]", name); + logger.LogInformation("Requesting Thing. [Thing: {name}]", name); var thing = things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (thing == null) { - logger.LogInformation("Thing not found. [Name: {name}]", name); + logger.LogInformation("Thing not found. [Thing: {name}]", name); context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } if (thing.Prefix == null) { - logger.LogDebug("Thing without prefix. [Name: {name}]", thing.Name); + logger.LogDebug("Thing without prefix. [Thing: {name}]", thing.Name); thing.Prefix = new Uri(UriHelper.BuildAbsolute(context.Request.Scheme, context.Request.Host)); } - logger.LogInformation("Found 1 Thing. [Name: {name}]", thing.Name); + logger.LogInformation("Found 1 Thing. [Thing: {name}]", thing.Name); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; diff --git a/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs index 8c6b2ec..3f2e598 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs @@ -17,6 +17,7 @@ public static void MapThings(this IEndpointRouteBuilder endpoint) throw new ArgumentNullException(nameof(endpoint)); } + endpoint.MapGet("/", GetAllThings.InvokeAsync); endpoint.MapGet("/things", GetAllThings.InvokeAsync); endpoint.MapGet("/things/{name}", context => context.WebSockets.IsWebSocketRequest ? WebSocket.InvokeAsync(context) : GetThing.InvokeAsync(context)); diff --git a/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs b/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs index 1ee9d67..3d692d0 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs @@ -24,8 +24,8 @@ public ThingCollectionBuilder(IServiceCollection service) public IThingCollectionBuilder AddThing() where T : Thing { - _service.TryAddSingleton(); - _service.TryAddSingleton(ConfigureThing); + _service.AddSingleton(); + _service.AddSingleton(ConfigureThing); return this; } @@ -37,8 +37,8 @@ public IThingCollectionBuilder AddThing(T thing) throw new ArgumentNullException(nameof(thing)); } - _service.TryAddSingleton(thing); - _service.TryAddSingleton(ConfigureThing); + _service.AddSingleton(thing); + _service.AddSingleton(ConfigureThing); return this; } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs index 10b313e..bc3f98c 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs @@ -43,7 +43,7 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri _isObjectStart = true; } - var propertyName = thingPropertyAttribute?.Name ?? propertyInfo.Name; + var propertyName = _options.GetPropertyName(thingPropertyAttribute?.Name ?? propertyInfo.Name); var propertyType = propertyInfo.PropertyType; var jsonType = GetJsonType(propertyType); if (jsonType == null) @@ -59,7 +59,8 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri thingPropertyAttribute.Title); _jsonWriter.PropertyWithNullableValue(nameof(ThingPropertyAttribute.Description), thingPropertyAttribute.Description); - _jsonWriter.PropertyWithNullableValue("ReadOnly", thingPropertyAttribute.IsReadOnly); + var readOnly = thingPropertyAttribute.IsReadOnly || !propertyInfo.CanWrite || !propertyInfo.SetMethod.IsPublic; + _jsonWriter.PropertyWithNullableValue("ReadOnly", readOnly); _jsonWriter.PropertyWithNullableValue("Type", jsonType); _jsonWriter.PropertyEnum("@enum", propertyType, thingPropertyAttribute.Enum); _jsonWriter.PropertyWithNullableValue(nameof(ThingPropertyAttribute.Unit), thingPropertyAttribute.Unit); @@ -75,7 +76,13 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri thingPropertyAttribute.MultipleOfValue); } } - + else + { + if (!propertyInfo.CanWrite || !propertyInfo.SetMethod.IsPublic) + { + _jsonWriter.PropertyWithNullableValue("ReadOnly", true); + } + } _jsonWriter.StartArray("Links"); @@ -83,7 +90,7 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri _jsonWriter.StartObject(); _jsonWriter.PropertyWithValue("href", - $"/things/{_options.GetPropertyName(thing.Name)}/properties/{_options.GetPropertyName(propertyName)}"); + $"/things/{_options.GetPropertyName(thing.Name)}/properties/{propertyName}"); _jsonWriter.EndObject(); _jsonWriter.EndArray(); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs index 00521c7..d7e3de1 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/Utf8JsonWriterILGenerator.cs @@ -152,7 +152,7 @@ public void PropertyWithValue(string propertyName, double value) { _ilGenerator.Emit(OpCodes.Ldarg_1); _ilGenerator.Emit(OpCodes.Ldstr, _options.GetPropertyName(propertyName)); - _ilGenerator.Emit(OpCodes.Ldc_I4_S, value); + _ilGenerator.Emit(OpCodes.Ldc_R8, value); _ilGenerator.EmitCall(OpCodes.Callvirt, s_writeNumberDouble, new[] { typeof(string), typeof(double) }); } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index 4c4f2cf..4cf71c1 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs @@ -50,7 +50,7 @@ private static Func GetGetMethod(PropertyInfo property) private static Action GetSetMethod(PropertyInfo property, ThingPropertyAttribute? thingPropertyAttribute) { if ((thingPropertyAttribute != null && thingPropertyAttribute.IsReadOnly) - || !property.CanWrite) + || !property.CanWrite || !property.SetMethod.IsPublic) { return null; } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs index 691f6ea..039020f 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -85,42 +85,53 @@ public static async Task InvokeAsync(HttpContext context) .ReceiveAsync(segment, cancellation) .ConfigureAwait(false); - var json = JsonSerializer.Deserialize(segment.Slice(0, received.Count), jsonOptions); - - if (!json.TryGetProperty("messageType", out var messageType)) - { - logger.LogInformation("Web Socket request without messageType"); - await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) - .ConfigureAwait(false); - continue; - } - - if (!json.TryGetProperty("data", out var data)) + if (received.CloseStatus.HasValue) { - logger.LogInformation("Web Socket request without data. [Message Type: {messageType}]", messageType.GetString()); - await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) - .ConfigureAwait(false); - continue; - } - - if (!actions.TryGetValue(messageType.GetString(), out var action)) - { - logger.LogInformation("Invalid Message Type: {messageType}", messageType.GetString()); - await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) - .ConfigureAwait(false); + logger.LogInformation("Going to close socket. [Thing: {name}]", thing.Name); continue; } + var messageTypeString = string.Empty; try { + var json = JsonSerializer.Deserialize(segment.Slice(0, received.Count), jsonOptions); + + if (!json.TryGetProperty("messageType", out var messageType)) + { + logger.LogInformation("Web Socket request without messageType"); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + if (!json.TryGetProperty("data", out var data)) + { + logger.LogInformation("Web Socket request without data. [Message Type: {messageType}]", messageType.GetString()); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + messageTypeString = messageType.GetString(); + if (!actions.TryGetValue(messageType.GetString(), out var action)) + { + logger.LogInformation("Invalid Message Type: {messageType}", messageType.GetString()); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + using var scope = service.CreateScope(); scope.ServiceProvider.GetRequiredService().Observer = observer; await action.ExecuteAsync(socket, thing, data, jsonOptions, scope.ServiceProvider, cancellation) .ConfigureAwait(false); + + messageTypeString = string.Empty; + } catch (Exception e) { - logger.LogError(e, "Error to execute Web Socket Action: {action}", messageType.GetString()); + logger.LogError(e, "Error to execute Web Socket Action: {action}", messageTypeString); } } } From 83cebbe2acfc81859accd8be9bbc8a4a217e6141 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 22:25:10 +0000 Subject: [PATCH 02/13] Remove unused namespace --- sample/MultiThing/Program.cs | 46 ++++---- sample/MultiThing/Startup.cs | 106 ++++++++---------- .../Endpoints/GetEvent.cs | 1 - .../Endpoints/GetEvents.cs | 1 - .../Endpoints/GetProperties.cs | 1 - .../Endpoints/GetProperty.cs | 1 - .../Endpoints/PostAction.cs | 1 - .../Endpoints/PostActions.cs | 1 - .../Endpoints/PutProperty.cs | 1 - .../Extensions/ThingCollectionBuilder.cs | 1 - .../WebSockets/SetThingProperty.cs | 1 - 11 files changed, 69 insertions(+), 92 deletions(-) diff --git a/sample/MultiThing/Program.cs b/sample/MultiThing/Program.cs index 2c918f7..b7c16ed 100644 --- a/sample/MultiThing/Program.cs +++ b/sample/MultiThing/Program.cs @@ -1,26 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace MultiThing -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace MultiThing +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/sample/MultiThing/Startup.cs b/sample/MultiThing/Startup.cs index a529835..5e2467b 100644 --- a/sample/MultiThing/Startup.cs +++ b/sample/MultiThing/Startup.cs @@ -1,57 +1,49 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.WebSockets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using MultiThing.Things; - -namespace MultiThing -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddThings() - .AddThing() - .AddThing() - ; - - services.AddWebSockets(opt => { }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseWebSockets(); - - app.UseEndpoints(endpoints => - { - endpoints.MapThings(); - }); - } - } -} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebSockets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MultiThing.Things; + +namespace MultiThing +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddThings() + .AddThing() + .AddThing(); + + services.AddWebSockets(opt => { }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseWebSockets(); + + app.UseEndpoints(endpoints => + { + endpoints.MapThings(); + }); + } + } +} diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs index 8d9e7b7..df32c81 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs index 143508b..4fead78 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs index 41b7aa5..8667c08 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs index 2093e41..b43ab6d 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs index 7774ec2..966b79e 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Actions; -using Mozilla.IoT.WebThing.Converts; using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs index eabd938..5afbd4c 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Actions; -using Mozilla.IoT.WebThing.Converts; using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs index 24d2f97..d576b8d 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs b/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs index 3d692d0..322dd30 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/ThingCollectionBuilder.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Mozilla.IoT.WebThing.Factories; using Mozilla.IoT.WebThing.Factories.Generator.Actions; using Mozilla.IoT.WebThing.Factories.Generator.Converter; diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs index b47fa45..da5a30e 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Net.WebSockets; using System.Text.Json; using System.Threading; From 7651163e8ae6cbc3a44543574fa9b299ed8d1ec7 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 22:35:02 +0000 Subject: [PATCH 03/13] Add UseThingAdapter --- sample/MultiThing/Startup.cs | 2 +- .../Converts/ThingConverter.cs | 26 +++++++++++------ .../Endpoints/DeleteAction.cs | 6 ++-- .../Endpoints/GetAction.cs | 3 +- .../Endpoints/GetActionById.cs | 3 +- .../Endpoints/GetActions.cs | 3 +- .../Endpoints/GetAllThings.cs | 5 +++- .../Endpoints/GetThing.cs | 5 +++- .../Extensions/ThingOption.cs | 29 +++++++++++++++++++ .../Generator/ConverterInterceptorTest.cs | 3 +- 10 files changed, 67 insertions(+), 18 deletions(-) diff --git a/sample/MultiThing/Startup.cs b/sample/MultiThing/Startup.cs index 5e2467b..182a6ce 100644 --- a/sample/MultiThing/Startup.cs +++ b/sample/MultiThing/Startup.cs @@ -21,7 +21,7 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddThings() + services.AddThings(opt => opt.UseThingAdapterUrl = true) .AddThing() .AddThing(); diff --git a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs index 315375f..c11bacc 100644 --- a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs +++ b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs @@ -1,18 +1,18 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Converts { public class ThingConverter : JsonConverter { - public static JsonSerializerOptions Options { get; } = new JsonSerializerOptions + + private readonly ThingOption _option; + public ThingConverter(ThingOption option) { - WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - Converters = { new ThingConverter()} - }; + _option = option; + } public override bool CanConvert(Type typeToConvert) { @@ -45,10 +45,18 @@ public override void Write(Utf8JsonWriter writer, Thing value, JsonSerializerOpt writer.WriteStartObject(); writer.WriteString("@context", value.Context); var builder = new UriBuilder(value.Prefix) {Path = $"/things/{options.GetPropertyName(value.Name)}"}; - WriteProperty(writer, "Id", value.Name, options); + if (_option.UseThingAdapterUrl) + { + WriteProperty(writer, "Id", value.Name, options); + WriteProperty(writer, "href", builder.Path, options); + WriteProperty(writer, "base", builder.Uri.ToString(), options); + } + else + { + WriteProperty(writer, "Id", builder.Uri.ToString(), options); + } + value.ThingContext.Converter.Write(writer, value, options); - WriteProperty(writer, "href", builder.Path, options); - WriteProperty(writer, "base", builder.Uri.ToString(), options); StartArray(writer, "Links", options); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/DeleteAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/DeleteAction.cs index eebc997..f51fee2 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/DeleteAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/DeleteAction.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { @@ -28,8 +30,8 @@ public static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.NotFound; return Task.CompletedTask; } - - var option = ThingConverter.Options;; + + var option = service.GetRequiredService(); var actionName = context.GetRouteData("action"); var id = Guid.Parse(context.GetRouteData("id")); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs index 9ee9015..ccc884b 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -29,7 +30,7 @@ public static async Task InvokeAsync(HttpContext context) return; } - var option = ThingConverter.Options; + var option = service.GetRequiredService(); var actionName = context.GetRouteData("action"); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs index de735a6..1bc6969 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -28,7 +29,7 @@ public static async Task InvokeAsync(HttpContext context) return; } - var option = ThingConverter.Options;; + var option = service.GetRequiredService(); var actionName = context.GetRouteData("action"); var id = Guid.Parse(context.GetRouteData("id")); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs index 03c0f0e..2cbf948 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -29,7 +30,7 @@ public static async Task InvokeAsync(HttpContext context) return; } - var option = ThingConverter.Options; + var option = service.GetRequiredService(); var result = new LinkedList(); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs index 3a445a7..7391bd3 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetAllThings.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { @@ -35,7 +36,9 @@ internal static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, things, ThingConverter.Options, context.RequestAborted); + return JsonSerializer.SerializeAsync(context.Response.Body, things, + service.GetRequiredService().ToJsonSerializerOptions(), + context.RequestAborted); } } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs index 8369e5d..d070665 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetThing.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { @@ -43,7 +44,9 @@ internal static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, thing, ThingConverter.Options, context.RequestAborted); + return JsonSerializer.SerializeAsync(context.Response.Body, thing, + service.GetRequiredService().ToJsonSerializerOptions(), + context.RequestAborted); } } } diff --git a/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs b/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs index 9e5364b..9a05b8d 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing.Extensions { @@ -6,6 +7,34 @@ public class ThingOption { public int MaxEventSize { get; set; } = 10; public bool IgnoreCase { get; set; } = true; + public bool UseThingAdapterUrl { get; set; } public JsonNamingPolicy PropertyNamingPolicy { get; set; } = JsonNamingPolicy.CamelCase; + + private JsonSerializerOptions _options; + private readonly object _locker = null; + public JsonSerializerOptions ToJsonSerializerOptions() + { + if (_options == null) + { + lock (_locker) + { + if (_options == null) + { + _options = new JsonSerializerOptions + { + PropertyNamingPolicy = PropertyNamingPolicy, + DictionaryKeyPolicy = PropertyNamingPolicy, + IgnoreNullValues = true, + Converters = + { + new ThingConverter(this) + } + }; + } + } + } + + return _options; + } } } diff --git a/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs b/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs index 8a6e762..ab81316 100644 --- a/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs +++ b/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs @@ -5,6 +5,7 @@ using FluentAssertions.Json; using Mozilla.IoT.WebThing.Attributes; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; using Mozilla.IoT.WebThing.Factories; using Mozilla.IoT.WebThing.Factories.Generator.Converter; using Newtonsoft.Json.Linq; @@ -44,7 +45,7 @@ public void Serialize() new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, IgnoreNullValues = true, - Converters = { new ThingConverter() } + Converters = { new ThingConverter(new ThingOption()) } }); JToken.Parse(value) From a47d4160459b1b2032b0d26b2ecfa6eab5d3e013 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 22:40:26 +0000 Subject: [PATCH 04/13] fixes test --- src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs | 2 +- test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs b/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs index 9a05b8d..179d055 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/ThingOption.cs @@ -11,7 +11,7 @@ public class ThingOption public JsonNamingPolicy PropertyNamingPolicy { get; set; } = JsonNamingPolicy.CamelCase; private JsonSerializerOptions _options; - private readonly object _locker = null; + private readonly object _locker = new object(); public JsonSerializerOptions ToJsonSerializerOptions() { if (_options == null) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 3d613f7..5134f0b 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -67,6 +67,7 @@ public async Task GetAll() ] }, ""reader"": { + ""readOnly"": true, ""links"": [ { ""href"": ""/things/lamp/properties/reader"" @@ -212,6 +213,7 @@ public async Task Get() ] }, ""reader"": { + ""readOnly"": true, ""links"": [ { ""href"": ""/things/lamp/properties/reader"" From 5313de0e3a8197f925688685052981b684e5e8ad Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 22:40:40 +0000 Subject: [PATCH 05/13] fixes test --- test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 5134f0b..6c83942 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -213,7 +213,7 @@ public async Task Get() ] }, ""reader"": { - ""readOnly"": true, + ""readOnly"": truek, ""links"": [ { ""href"": ""/things/lamp/properties/reader"" From 085e2d77fedcb31379a8e90bb91e13c854b1a1d9 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 23:26:20 +0000 Subject: [PATCH 06/13] Remove logging and fixes test --- test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs | 2 +- test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 6c83942..5134f0b 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -213,7 +213,7 @@ public async Task Get() ] }, ""reader"": { - ""readOnly"": truek, + ""readOnly"": true, ""links"": [ { ""href"": ""/things/lamp/properties/reader"" diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs index 7172673..fcb395b 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Mozilla.IoT.WebThing.AcceptanceTest { @@ -9,6 +10,12 @@ public class Program { public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureLogging(logger => + { + logger.ClearProviders() + .AddConsole() + .AddFilter("*", LogLevel.Error); + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder From 353c3af1165de448fb6a1a697ea77c9401fdf50c Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sat, 15 Feb 2020 23:37:38 +0000 Subject: [PATCH 07/13] Add test --- .../Converts/ThingConverter.cs | 2 +- .../Http/Thing.cs | 300 ++++++++++++++++++ .../Program.cs | 5 +- .../Startup.cs | 5 +- 4 files changed, 309 insertions(+), 3 deletions(-) diff --git a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs index c11bacc..4833d7c 100644 --- a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs +++ b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs @@ -47,7 +47,7 @@ public override void Write(Utf8JsonWriter writer, Thing value, JsonSerializerOpt var builder = new UriBuilder(value.Prefix) {Path = $"/things/{options.GetPropertyName(value.Name)}"}; if (_option.UseThingAdapterUrl) { - WriteProperty(writer, "Id", value.Name, options); + WriteProperty(writer, "Id", options.GetPropertyName(value.Name), options); WriteProperty(writer, "href", builder.Path, options); WriteProperty(writer, "base", builder.Uri.ToString(), options); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 5134f0b..c965559 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -3,6 +3,7 @@ using AutoFixture; using FluentAssertions; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; using Xunit; @@ -314,5 +315,304 @@ public async Task GetInvalid() response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + + [Fact] + public async Task GetAllWhenUseThingAdapter() + { + var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) + .StartAsync(); + var client = host.GetTestServer().CreateClient(); + var response = await client.GetAsync("/things"); + + 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(); + var json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Array); + ((JArray)json).Should().HaveCount(1); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse(@" +[ + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""base"": ""http://localhost/things/lamp"", + ""href"": ""/things/lamp"", + ""id"": ""lamp"", + ""title"": ""My Lamp"", + ""description"": ""A web connected lamp"", + ""@type"": [ + ""Light"", + ""OnOffSwitch"" + ], + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/lamp/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/lamp/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/lamp/properties/reader"" + } + ] + } + }, + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/longRun"" + } + ] + } + }, + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/lamp/events/overheated"" + } + ] + }, + ""otherEvent"": { + ""title"": ""OtherEvent"", + ""type"": ""string"", + ""links"": [ + { + ""href"": ""/things/lamp/events/otherEvent"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/lamp/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/lamp/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/lamp/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/lamp"" + } + ] + } +] +")); + } + + [Fact] + public async Task GetWhenUseThingAdapter() + { + var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) + .StartAsync(); + var client = host.GetTestServer().CreateClient(); + var response = await client.GetAsync("/things/Lamp"); + + 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(); + var json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse(@" +{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""base"": ""http://localhost/things/lamp"", + ""href"": ""/things/lamp"", + ""id"": ""lamp"", + ""title"": ""My Lamp"", + ""description"": ""A web connected lamp"", + ""@type"": [ + ""Light"", + ""OnOffSwitch"" + ], + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/lamp/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/lamp/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/lamp/properties/reader"" + } + ] + } + }, + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/longRun"" + } + ] + } + }, + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/lamp/events/overheated"" + } + ] + }, + ""otherEvent"": { + ""title"": ""OtherEvent"", + ""type"": ""string"", + ""links"": [ + { + ""href"": ""/things/lamp/events/otherEvent"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/lamp/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/lamp/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/lamp/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/lamp"" + } + ] +} +")); + } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs index fcb395b..bd5cea6 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs @@ -1,14 +1,16 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.AcceptanceTest { public class Program { - public static IHostBuilder CreateHostBuilder(string[] args) => + public static IHostBuilder CreateHostBuilder(string[] args, Action? option = null) => Host.CreateDefaultBuilder(args) .ConfigureLogging(logger => { @@ -18,6 +20,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => }) .ConfigureWebHostDefaults(webBuilder => { + Startup.Option = option; webBuilder .UseTestServer() .UseStartup(); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs index d115183..0b78bde 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; @@ -5,16 +6,18 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Mozilla.IoT.WebThing.AcceptanceTest.Things; +using Mozilla.IoT.WebThing.Extensions; 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) { - services.AddThings() + services.AddThings(Option) .AddThing(); services.AddWebSockets(o => { }); From abc0edd3e06435e50845172b95a3d7d5fb800900 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 10:59:32 +0000 Subject: [PATCH 08/13] Improve test --- .../Http/Action.cs | 116 +- .../Http/Events.cs | 109 +- .../Http/Properties.cs | 94 +- .../Http/Thing.cs | 1413 ++++++++++++----- .../Startup.cs | 6 +- .../Things/ActionThing.cs | 27 + .../Things/EventThing.cs | 43 + .../Things/LampThing.cs | 31 +- .../Things/PropertyThing.cs | 38 + .../Things/WebSocketPropertyThing.cs | 38 + .../WebScokets/Action.cs | 49 +- .../WebScokets/Event.cs | 53 +- .../WebScokets/Property.cs | 82 +- 13 files changed, 1499 insertions(+), 600 deletions(-) create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyThing.cs create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyThing.cs diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs index a1a85dd..dbad5c3 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.TestHost; @@ -14,15 +15,22 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Http { public class Action { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly HttpClient _client; + public Action() + { + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + } + [Theory] [InlineData(50, 2_000)] public async Task Create(int level, int duration) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); - - var response = await client.PostAsync("/things/Lamp/actions", + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PostAsync("/things/action/actions", new StringContent($@" {{ ""fade"": {{ @@ -31,12 +39,13 @@ public async Task Create(int level, int duration) ""duration"": {duration} }} }} -}}")); +}}"), 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() @@ -45,7 +54,7 @@ public async Task Create(int level, int duration) json.Input.Should().NotBeNull(); json.Input.Level.Should().Be(level); json.Input.Duration.Should().Be(duration); - json.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Href.Should().StartWith("/things/action/actions/fade/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); } @@ -54,11 +63,10 @@ public async Task Create(int level, int duration) [InlineData(50, 2_000)] public async Task CreateInSpecificUrl(int level, int duration) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/lamp/actions/fade", + var response = await _client.PostAsync("/things/action/actions/fade", new StringContent($@" {{ ""fade"": {{ @@ -67,12 +75,14 @@ public async Task CreateInSpecificUrl(int level, int duration) ""duration"": {duration} }} }} -}}")); +}}"), 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() @@ -81,7 +91,7 @@ public async Task CreateInSpecificUrl(int level, int duration) json.Input.Should().NotBeNull(); json.Input.Level.Should().Be(level); json.Input.Duration.Should().Be(duration); - json.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Href.Should().StartWith("/things/action/actions/fade/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); } @@ -89,11 +99,10 @@ public async Task CreateInSpecificUrl(int level, int duration) [Fact] public async Task InvalidAction() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/lamp/actions/aaaa", + var response = await _client.PostAsync("/things/action/actions/aaaa", new StringContent(@" { ""aaaa"": { @@ -102,7 +111,8 @@ public async Task InvalidAction() ""duration"": 100 } } -}")); +}"), source.Token).ConfigureAwait(false); + response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } @@ -110,11 +120,10 @@ public async Task InvalidAction() [Fact] public async Task TryCreateActionWithOtherName() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/lamp/actions/fade", + var response = await _client.PostAsync("/things/action/actions/fade", new StringContent(@" { ""aaaa"": { @@ -123,7 +132,8 @@ public async Task TryCreateActionWithOtherName() ""duration"": 100 } } -}")); +}"), source.Token).ConfigureAwait(false); + response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } @@ -133,11 +143,10 @@ public async Task TryCreateActionWithOtherName() [InlineData(101, 2_000)] public async Task TryCreateWithInvalidParameter(int level, int duration) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/Lamp/actions", + var response = await _client.PostAsync("/things/action/actions", new StringContent($@" {{ ""fade"": {{ @@ -146,17 +155,18 @@ public async Task TryCreateWithInvalidParameter(int level, int duration) ""duration"": {duration} }} }} -}}")); +}}"), source.Token).ConfigureAwait(false); + response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - response = await client.GetAsync("/things/Lamp/actions"); + response = await _client.GetAsync("/things/action/actions", 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Array); ((JArray)json).Should().HaveCount(0); @@ -165,40 +175,42 @@ public async Task TryCreateWithInvalidParameter(int level, int duration) [Fact] public async Task LongRunner() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/Lamp/actions", + var response = await _client.PostAsync("/things/action/actions", new StringContent($@" {{ ""longRun"": {{ }} -}}")); +}}"), 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/lamp/actions/longRun/"); + json.Href.Should().StartWith("/things/action/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); - await Task.Delay(3_000); + await Task.Delay(3_000).ConfigureAwait(false); - response = await client.GetAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await _client.GetAsync($"/things/action/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}", source.Token) + .ConfigureAwait(false); + message = await response.Content.ReadAsStringAsync(); json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/lamp/actions/longRun/"); + json.Href.Should().StartWith("/things/action/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.Status.Should().Be("completed"); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); @@ -209,34 +221,36 @@ public async Task LongRunner() [Fact] public async Task CancelAction() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - var response = await client.PostAsync("/things/Lamp/actions", + var response = await _client.PostAsync("/things/action/actions", new StringContent($@" {{ ""LongRun"": {{ }} -}}")); +}}"), 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/lamp/actions/longRun/"); + json.Href.Should().StartWith("/things/action/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); - response = await client.DeleteAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await _client.DeleteAsync($"/things/action/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}", source.Token) + .ConfigureAwait(false); response.StatusCode.Should().Be(HttpStatusCode.NoContent); - response = await client.GetAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await _client.GetAsync($"/things/action/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}", source.Token) + .ConfigureAwait(false); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs index ee9ca98..48cc34f 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs @@ -1,5 +1,7 @@ using System; using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.TestHost; @@ -11,39 +13,56 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Http { public class Events { - + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly HttpClient _client; + + public Events() + { + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + } + #region GET + [Fact] public async Task GetAll() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp/events"); - + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync("/things/event/events", 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(); + 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.Array); - ((JArray)json).Should().BeEmpty(); - await Task.Delay(3_000); + json.Type.Should().Be(JTokenType.Array); - response = await client.GetAsync("/things/Lamp/events"); + if (((JArray)json).Count == 0) + { + await Task.Delay(3_000).ConfigureAwait(false); - response.IsSuccessStatusCode.Should().BeTrue(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - message = await response.Content.ReadAsStringAsync(); - json = JToken.Parse(message); + response = await _client.GetAsync("/things/event/events", source.Token) + .ConfigureAwait(false); - json.Type.Should().Be(JTokenType.Array); - ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); + 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.Array); + ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); + } + var obj = ((JArray)json)[0] as JObject; obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Object); @@ -62,34 +81,41 @@ public async Task GetAll() [Fact] public async Task GetEvent() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp/events/overheated"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync("/things/event/events/overheated", 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Array); - ((JArray)json).Should().BeEmpty(); - - await Task.Delay(3_000); - response = await client.GetAsync("/things/Lamp/events/overheated"); + if (((JArray)json).Count == 0) + { + await Task.Delay(3_000).ConfigureAwait(false); - response.IsSuccessStatusCode.Should().BeTrue(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); - message = await response.Content.ReadAsStringAsync(); - json = JToken.Parse(message); + response = await _client.GetAsync("/things/event/events", source.Token) + .ConfigureAwait(false); - json.Type.Should().Be(JTokenType.Array); - ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); + 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.Array); + ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); + } var obj = ((JArray)json)[0] as JObject; obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Object); @@ -102,15 +128,16 @@ public async Task GetEvent() ((JObject)obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase)) .GetValue("timestamp", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Date); - } [Fact] public async Task GetInvalidEvent() { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp/events/aaaaa"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync("/things/event/events/aaaaa", source.Token) + .ConfigureAwait(false); response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs index a1795b0..25a0243 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs @@ -1,10 +1,11 @@ -using System.Net; +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 Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; using Xunit; @@ -13,25 +14,30 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Http public class Properties { private readonly Fixture _fixture; - + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly HttpClient _client; public Properties() { _fixture = new Fixture(); + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); } #region GET [Fact] public async Task GetAll() { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp/properties"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync("/things/property/properties", 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); @@ -51,15 +57,17 @@ public async Task GetAll() [InlineData("brightness", 0)] public async Task Get(string property, object value) { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync($"/things/Lamp/properties/{property}"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/property/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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); @@ -71,9 +79,11 @@ public async Task Get(string property, object value) [Fact] public async Task GetInvalid() { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync($"/things/Lamp/properties/{_fixture.Create()}"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/property/properties/{_fixture.Create()}", source.Token) + .ConfigureAwait(false); response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); @@ -87,17 +97,18 @@ public async Task GetInvalid() [InlineData("brightness", 10)] public async Task Put(string property, object value) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); - var response = await client.PutAsync($"/things/Lamp/properties/{property}", - new StringContent($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PutAsync($"/things/property/properties/{property}", + new StringContent($@"{{ ""{property}"": {value.ToString().ToLower()} }}"), 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); @@ -105,13 +116,19 @@ public async Task Put(string property, object value) .Should(json) .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); - response = await client.GetAsync($"/things/Lamp/properties/{property}"); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + response = await _client.GetAsync($"/things/property/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(); + message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); @@ -124,39 +141,48 @@ public async Task Put(string property, object value) [InlineData("brightness", -1, 0)] [InlineData("brightness", 101, 0)] [InlineData("reader", 101, 0)] - public async Task PutInvalidValue(string property, object value, object defaulValue) + public async Task PutInvalidValue(string property, object value, object defaultValue) { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.PutAsync($"/things/Lamp/properties/{property}", - new StringContent($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.PutAsync($"/things/property/properties/{property}", + new StringContent($@"{{ ""{property}"": {value.ToString().ToLower()} }}"), source.Token) + .ConfigureAwait(false); response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - response = await client.GetAsync($"/things/Lamp/properties/{property}"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + response = await _client.GetAsync($"/things/property/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(); + 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}"": {defaulValue.ToString().ToLower()} }}")); + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {defaultValue.ToString().ToLower()} }}")); } [Fact] public async Task PutInvalidProperty() { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); var property = _fixture.Create(); - var response = await client.PutAsync($"/things/Lamp/properties/{property}", - new StringContent($@"{{ ""{property}"": {_fixture.Create()} }}")); + + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + + var response = await _client.PutAsync($"/things/property/properties/{property}", + new StringContent($@"{{ ""{property}"": {_fixture.Create()} }}"), source.Token); response.IsSuccessStatusCode.Should().BeFalse(); response.StatusCode.Should().Be(HttpStatusCode.NotFound); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index c965559..4f56f06 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -1,4 +1,7 @@ -using System.Net; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; @@ -11,22 +14,32 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Http { public class Thing { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly HttpClient _client; + public Thing() + { + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + } + [Fact] public async Task GetAll() { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things"); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync("/things", 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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Array); - ((JArray)json).Should().HaveCount(1); + ((JArray)json).Should().HaveCount(5); FluentAssertions.Json.JsonAssertionExtensions .Should(json) .BeEquivalentTo(JToken.Parse(@" @@ -66,14 +79,6 @@ public async Task GetAll() ""href"": ""/things/lamp/properties/brightness"" } ] - }, - ""reader"": { - ""readOnly"": true, - ""links"": [ - { - ""href"": ""/things/lamp/properties/reader"" - } - ] } }, ""actions"": { @@ -125,15 +130,6 @@ public async Task GetAll() ""href"": ""/things/lamp/events/overheated"" } ] - }, - ""otherEvent"": { - ""title"": ""OtherEvent"", - ""type"": ""string"", - ""links"": [ - { - ""href"": ""/things/lamp/events/otherEvent"" - } - ] } }, ""links"": [ @@ -154,200 +150,10 @@ public async Task GetAll() ""href"": ""ws://localhost/things/lamp"" } ] - } -] -")); - } - - [Fact] - public async Task Get() - { - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp"); - - 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(); - var json = JToken.Parse(message); - - json.Type.Should().Be(JTokenType.Object); - FluentAssertions.Json.JsonAssertionExtensions - .Should(json) - .BeEquivalentTo(JToken.Parse(@" -{ - ""@context"": ""https://iot.mozilla.org/schemas"", - ""id"": ""http://localhost/things/lamp"", - ""title"": ""My Lamp"", - ""description"": ""A web connected lamp"", - ""@type"": [ - ""Light"", - ""OnOffSwitch"" - ], - ""properties"": { - ""on"": { - ""title"": ""On/Off"", - ""description"": ""Whether the lamp is turned on"", - ""readOnly"": false, - ""type"": ""boolean"", - ""@type"": ""OnOffProperty"", - ""links"": [ - { - ""href"": ""/things/lamp/properties/on"" - } - ] - }, - ""brightness"": { - ""title"": ""Brightness"", - ""description"": ""The level of light from 0-100"", - ""readOnly"": false, - ""type"": ""integer"", - ""@type"": ""BrightnessProperty"", - ""minimum"": 0, - ""maximum"": 100, - ""links"": [ - { - ""href"": ""/things/lamp/properties/brightness"" - } - ] - }, - ""reader"": { - ""readOnly"": true, - ""links"": [ - { - ""href"": ""/things/lamp/properties/reader"" - } - ] - } - }, - ""actions"": { - ""fade"": { - ""title"": ""Fade"", - ""description"": ""Fade the lamp to a given level"", - ""@type"": ""FadeAction"", - ""input"": { - ""type"": ""object"", - ""properties"": { - ""level"": { - ""type"": ""integer"", - ""minimum"": 0, - ""maximum"": 100 - }, - ""duration"": { - ""type"": ""integer"", - ""unit"": ""milliseconds"", - ""minimum"": 0 - } - } - }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/fade"" - } - ] - }, - ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/longRun"" - } - ] - } - }, - ""events"": { - ""overheated"": { - ""title"": ""Overheated"", - ""description"": ""The lamp has exceeded its safe operating temperature"", - ""@type"": ""OverheatedEvent"", - ""type"": ""integer"", - ""links"": [ - { - ""href"": ""/things/lamp/events/overheated"" - } - ] - }, - ""otherEvent"": { - ""title"": ""OtherEvent"", - ""type"": ""string"", - ""links"": [ - { - ""href"": ""/things/lamp/events/otherEvent"" - } - ] - } }, - ""links"": [ - { - ""rel"": ""properties"", - ""href"": ""/things/lamp/properties"" - }, - { - ""rel"": ""actions"", - ""href"": ""/things/lamp/actions"" - }, - { - ""rel"": ""events"", - ""href"": ""/things/lamp/events"" - }, - { - ""rel"": ""alternate"", - ""href"": ""ws://localhost/things/lamp"" - } - ] -} -")); - } - - [Fact] - public async Task GetInvalid() - { - var fixture = new Fixture(); - var host = await Program.GetHost(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync($"/things/{fixture.Create()}"); - - response.IsSuccessStatusCode.Should().BeFalse(); - response.StatusCode.Should().Be(HttpStatusCode.NotFound); - } - - [Fact] - public async Task GetAllWhenUseThingAdapter() - { - var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) - .StartAsync(); - var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things"); - - 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(); - var json = JToken.Parse(message); - - json.Type.Should().Be(JTokenType.Array); - ((JArray)json).Should().HaveCount(1); - FluentAssertions.Json.JsonAssertionExtensions - .Should(json) - .BeEquivalentTo(JToken.Parse(@" -[ { ""@context"": ""https://iot.mozilla.org/schemas"", - ""base"": ""http://localhost/things/lamp"", - ""href"": ""/things/lamp"", - ""id"": ""lamp"", - ""title"": ""My Lamp"", - ""description"": ""A web connected lamp"", - ""@type"": [ - ""Light"", - ""OnOffSwitch"" - ], + ""id"": ""http://localhost/things/property"", ""properties"": { ""on"": { ""title"": ""On/Off"", @@ -357,7 +163,7 @@ public async Task GetAllWhenUseThingAdapter() ""@type"": ""OnOffProperty"", ""links"": [ { - ""href"": ""/things/lamp/properties/on"" + ""href"": ""/things/property/properties/on"" } ] }, @@ -371,7 +177,7 @@ public async Task GetAllWhenUseThingAdapter() ""maximum"": 100, ""links"": [ { - ""href"": ""/things/lamp/properties/brightness"" + ""href"": ""/things/property/properties/brightness"" } ] }, @@ -379,49 +185,33 @@ public async Task GetAllWhenUseThingAdapter() ""readOnly"": true, ""links"": [ { - ""href"": ""/things/lamp/properties/reader"" + ""href"": ""/things/property/properties/reader"" } ] } }, - ""actions"": { - ""fade"": { - ""title"": ""Fade"", - ""description"": ""Fade the lamp to a given level"", - ""@type"": ""FadeAction"", - ""input"": { - ""type"": ""object"", - ""properties"": { - ""level"": { - ""type"": ""integer"", - ""minimum"": 0, - ""maximum"": 100 - }, - ""duration"": { - ""type"": ""integer"", - ""unit"": ""milliseconds"", - ""minimum"": 0 - } - } - }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/fade"" - } - ] + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/property/properties"" }, - ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/longRun"" - } - ] + { + ""rel"": ""actions"", + ""href"": ""/things/property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/property"" } - }, + ] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/event"", ""events"": { ""overheated"": { ""title"": ""Overheated"", @@ -430,7 +220,7 @@ public async Task GetAllWhenUseThingAdapter() ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/lamp/events/overheated"" + ""href"": ""/things/event/events/overheated"" } ] }, @@ -439,7 +229,7 @@ public async Task GetAllWhenUseThingAdapter() ""type"": ""string"", ""links"": [ { - ""href"": ""/things/lamp/events/otherEvent"" + ""href"": ""/things/event/events/otherEvent"" } ] } @@ -447,33 +237,201 @@ public async Task GetAllWhenUseThingAdapter() ""links"": [ { ""rel"": ""properties"", - ""href"": ""/things/lamp/properties"" + ""href"": ""/things/event/properties"" }, { ""rel"": ""actions"", - ""href"": ""/things/lamp/actions"" + ""href"": ""/things/event/actions"" }, { ""rel"": ""events"", - ""href"": ""/things/lamp/events"" + ""href"": ""/things/event/events"" }, { ""rel"": ""alternate"", - ""href"": ""ws://localhost/things/lamp"" + ""href"": ""ws://localhost/things/event"" } ] - } -] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/action"", + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/action/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/action/actions/longRun"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/action/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/action/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/action/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/action"" + } + ] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/web-socket-property"", + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/reader"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/web-socket-property/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/web-socket-property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/web-socket-property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/web-socket-property"" + } + ] + } +] ")); } + [Theory] + [InlineData("lamp", LAMP)] + [InlineData("property", PROPERTY)] + [InlineData("event", EVENT)] + [InlineData("action", ACTION)] + [InlineData("web-socket-property", WEB_SOCKET_PROPERTY)] + public async Task Get(string thing, string expected) + { + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/{thing}", 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(expected)); + } + [Fact] - public async Task GetWhenUseThingAdapter() + public async Task GetInvalid() + { + var fixture = new Fixture(); + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/{fixture.Create()}", source.Token) + .ConfigureAwait(false); + + response.IsSuccessStatusCode.Should().BeFalse(); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task GetAllWhenUseThingAdapter() { + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) - .StartAsync(); + .StartAsync(source.Token) + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); - var response = await client.GetAsync("/things/Lamp"); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + var response = await client.GetAsync("/things", source.Token) + .ConfigureAwait(false); response.IsSuccessStatusCode.Should().BeTrue(); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -482,137 +440,822 @@ public async Task GetWhenUseThingAdapter() var message = await response.Content.ReadAsStringAsync(); var json = JToken.Parse(message); - json.Type.Should().Be(JTokenType.Object); + json.Type.Should().Be(JTokenType.Array); + ((JArray)json).Should().HaveCount(5); FluentAssertions.Json.JsonAssertionExtensions .Should(json) .BeEquivalentTo(JToken.Parse(@" -{ - ""@context"": ""https://iot.mozilla.org/schemas"", - ""base"": ""http://localhost/things/lamp"", - ""href"": ""/things/lamp"", - ""id"": ""lamp"", - ""title"": ""My Lamp"", - ""description"": ""A web connected lamp"", - ""@type"": [ - ""Light"", - ""OnOffSwitch"" - ], - ""properties"": { - ""on"": { - ""title"": ""On/Off"", - ""description"": ""Whether the lamp is turned on"", - ""readOnly"": false, - ""type"": ""boolean"", - ""@type"": ""OnOffProperty"", - ""links"": [ - { - ""href"": ""/things/lamp/properties/on"" - } - ] - }, - ""brightness"": { - ""title"": ""Brightness"", - ""description"": ""The level of light from 0-100"", - ""readOnly"": false, - ""type"": ""integer"", - ""@type"": ""BrightnessProperty"", - ""minimum"": 0, - ""maximum"": 100, - ""links"": [ - { - ""href"": ""/things/lamp/properties/brightness"" - } - ] - }, - ""reader"": { - ""readOnly"": true, - ""links"": [ - { - ""href"": ""/things/lamp/properties/reader"" - } - ] - } - }, - ""actions"": { - ""fade"": { - ""title"": ""Fade"", - ""description"": ""Fade the lamp to a given level"", - ""@type"": ""FadeAction"", - ""input"": { - ""type"": ""object"", - ""properties"": { - ""level"": { - ""type"": ""integer"", - ""minimum"": 0, - ""maximum"": 100 - }, - ""duration"": { - ""type"": ""integer"", - ""unit"": ""milliseconds"", - ""minimum"": 0 +[ + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""lamp"", + ""href"": ""/things/lamp"", + ""base"": ""http://localhost/things/lamp"", + ""title"": ""My Lamp"", + ""description"": ""A web connected lamp"", + ""@type"": [ + ""Light"", + ""OnOffSwitch"" + ], + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/lamp/properties/on"" } - } + ] }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/fade"" - } - ] + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/lamp/properties/brightness"" + } + ] + } }, - ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, - ""links"": [ - { - ""href"": ""/things/lamp/actions/longRun"" - } - ] - } - }, - ""events"": { - ""overheated"": { - ""title"": ""Overheated"", - ""description"": ""The lamp has exceeded its safe operating temperature"", - ""@type"": ""OverheatedEvent"", - ""type"": ""integer"", - ""links"": [ - { - ""href"": ""/things/lamp/events/overheated"" - } - ] + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/longRun"" + } + ] + } }, - ""otherEvent"": { - ""title"": ""OtherEvent"", - ""type"": ""string"", - ""links"": [ - { - ""href"": ""/things/lamp/events/otherEvent"" - } - ] - } + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/lamp/events/overheated"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/lamp/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/lamp/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/lamp/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/lamp"" + } + ] }, - ""links"": [ - { - ""rel"": ""properties"", - ""href"": ""/things/lamp/properties"" + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""property"", + ""href"": ""/things/property"", + ""base"": ""http://localhost/things/property"", + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/property/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/property/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/property/properties/reader"" + } + ] + } }, - { - ""rel"": ""actions"", - ""href"": ""/things/lamp/actions"" + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/property/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/property"" + } + ] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""event"", + ""href"": ""/things/event"", + ""base"": ""http://localhost/things/event"", + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/event/events/overheated"" + } + ] + }, + ""otherEvent"": { + ""title"": ""OtherEvent"", + ""type"": ""string"", + ""links"": [ + { + ""href"": ""/things/event/events/otherEvent"" + } + ] + } }, - { - ""rel"": ""events"", - ""href"": ""/things/lamp/events"" + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/event/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/event/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/event/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/event"" + } + ] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""action"", + ""href"": ""/things/action"", + ""base"": ""http://localhost/things/action"", + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/action/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/action/actions/longRun"" + } + ] + } }, - { - ""rel"": ""alternate"", - ""href"": ""ws://localhost/things/lamp"" - } - ] -} -")); - } + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/action/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/action/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/action/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/action"" + } + ] + }, + { + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""web-socket-property"", + ""href"": ""/things/web-socket-property"", + ""base"": ""http://localhost/things/web-socket-property"", + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/reader"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/web-socket-property/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/web-socket-property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/web-socket-property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/web-socket-property"" + } + ] + } +] +")); + } + + [Fact] + public async Task GetWhenUseThingAdapter() + { + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) + .StartAsync(source.Token) + .ConfigureAwait(false); + + var client = host.GetTestServer().CreateClient(); + + var response = await client.GetAsync("/things/Lamp", 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(@" +{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""base"": ""http://localhost/things/lamp"", + ""href"": ""/things/lamp"", + ""id"": ""lamp"", + ""title"": ""My Lamp"", + ""description"": ""A web connected lamp"", + ""@type"": [ + ""Light"", + ""OnOffSwitch"" + ], + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/lamp/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/lamp/properties/brightness"" + } + ] + } + }, + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/longRun"" + } + ] + } + }, + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/lamp/events/overheated"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/lamp/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/lamp/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/lamp/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/lamp"" + } + ] +} +")); + } + + + private const string WEB_SOCKET_PROPERTY = @"{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/web-socket-property"", + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/web-socket-property/properties/reader"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/web-socket-property/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/web-socket-property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/web-socket-property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/web-socket-property"" + } + ] + }"; + + private const string ACTION = @"{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/action"", + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/action/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/action/actions/longRun"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/action/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/action/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/action/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/action"" + } + ] + }"; + private const string EVENT = @"{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/event"", + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/event/events/overheated"" + } + ] + }, + ""otherEvent"": { + ""title"": ""OtherEvent"", + ""type"": ""string"", + ""links"": [ + { + ""href"": ""/things/event/events/otherEvent"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/event/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/event/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/event/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/event"" + } + ] + }"; + + private const string PROPERTY = @"{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/property"", + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/property/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/property/properties/brightness"" + } + ] + }, + ""reader"": { + ""readOnly"": true, + ""links"": [ + { + ""href"": ""/things/property/properties/reader"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/property/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/property/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/property/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/property"" + } + ] + }"; + + private const string LAMP = @" +{ + ""@context"": ""https://iot.mozilla.org/schemas"", + ""id"": ""http://localhost/things/lamp"", + ""title"": ""My Lamp"", + ""description"": ""A web connected lamp"", + ""@type"": [ + ""Light"", + ""OnOffSwitch"" + ], + ""properties"": { + ""on"": { + ""title"": ""On/Off"", + ""description"": ""Whether the lamp is turned on"", + ""readOnly"": false, + ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", + ""links"": [ + { + ""href"": ""/things/lamp/properties/on"" + } + ] + }, + ""brightness"": { + ""title"": ""Brightness"", + ""description"": ""The level of light from 0-100"", + ""readOnly"": false, + ""type"": ""integer"", + ""@type"": ""BrightnessProperty"", + ""minimum"": 0, + ""maximum"": 100, + ""links"": [ + { + ""href"": ""/things/lamp/properties/brightness"" + } + ] + } + }, + ""actions"": { + ""fade"": { + ""title"": ""Fade"", + ""description"": ""Fade the lamp to a given level"", + ""@type"": ""FadeAction"", + ""input"": { + ""type"": ""object"", + ""properties"": { + ""level"": { + ""type"": ""integer"", + ""minimum"": 0, + ""maximum"": 100 + }, + ""duration"": { + ""type"": ""integer"", + ""unit"": ""milliseconds"", + ""minimum"": 0 + } + } + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/fade"" + } + ] + }, + ""longRun"": { + ""input"": { + ""type"": ""object"", + ""properties"": {} + }, + ""links"": [ + { + ""href"": ""/things/lamp/actions/longRun"" + } + ] + } + }, + ""events"": { + ""overheated"": { + ""title"": ""Overheated"", + ""description"": ""The lamp has exceeded its safe operating temperature"", + ""@type"": ""OverheatedEvent"", + ""type"": ""integer"", + ""links"": [ + { + ""href"": ""/things/lamp/events/overheated"" + } + ] + } + }, + ""links"": [ + { + ""rel"": ""properties"", + ""href"": ""/things/lamp/properties"" + }, + { + ""rel"": ""actions"", + ""href"": ""/things/lamp/actions"" + }, + { + ""rel"": ""events"", + ""href"": ""/things/lamp/events"" + }, + { + ""rel"": ""alternate"", + ""href"": ""ws://localhost/things/lamp"" + } + ] + }"; } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs index 0b78bde..6e879af 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs @@ -18,7 +18,11 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddThings(Option) - .AddThing(); + .AddThing() + .AddThing() + .AddThing() + .AddThing() + .AddThing(); services.AddWebSockets(o => { }); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs new file mode 100644 index 0000000..2b3e560 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class ActionThing : Thing + { + + public override string Name => "action"; + + [ThingAction(Name = "fade", Title = "Fade", Type = new []{"FadeAction"}, + Description = "Fade the lamp to a given level")] + public void Fade( + [ThingParameter(Minimum = 0, Maximum = 100)]int level, + [ThingParameter(Minimum = 0, Unit = "milliseconds")]int duration) + { + + } + + public Task LongRun(CancellationToken cancellationToken) + { + return Task.Delay(3_000, cancellationToken); + } + + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs new file mode 100644 index 0000000..ae3738b --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class EventThing : Thing + { + private int _counter = 0; + public EventThing() + { + Task.Factory.StartNew(() => + { + while (true) + { + Task.Delay(3_000).GetAwaiter().GetResult(); + var @event = Overheated; + @event?.Invoke(this, _counter++); + } + }); + + Task.Factory.StartNew(() => + { + while (true) + { + Task.Delay(4_000).GetAwaiter().GetResult(); + var @event = OtherEvent; + @event?.Invoke(this, _counter.ToString()); + } + }); + } + + public override string Name => "event"; + + [ThingEvent(Title = "Overheated", + Type = new [] {"OverheatedEvent"}, + Description = "The lamp has exceeded its safe operating temperature")] + public event EventHandler Overheated; + + [ThingEvent(Title = "OtherEvent")] + public event EventHandler OtherEvent; + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs index 8279051..fc067d1 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -7,29 +7,6 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Things { public class LampThing : Thing { - private int _counter = 0; - public LampThing() - { - Task.Factory.StartNew(() => - { - while (true) - { - Task.Delay(3_000).GetAwaiter().GetResult(); - var @event = Overheated; - @event?.Invoke(this, _counter++); - } - }); - - Task.Factory.StartNew(() => - { - while (true) - { - Task.Delay(4_000).GetAwaiter().GetResult(); - var @event = OtherEvent; - @event?.Invoke(this, _counter.ToString()); - } - }); - } public override string Name => "Lamp"; public override string Title => "My Lamp"; public override string Description => "A web connected lamp"; @@ -60,18 +37,12 @@ public int Brightness OnPropertyChanged(); } } - - public int Reader => _brightness; - [ThingEvent(Title = "Overheated", Type = new [] {"OverheatedEvent"}, Description = "The lamp has exceeded its safe operating temperature")] public event EventHandler Overheated; - - [ThingEvent(Title = "OtherEvent")] - public event EventHandler OtherEvent; - + [ThingAction(Name = "fade", Title = "Fade", Type = new []{"FadeAction"}, Description = "Fade the lamp to a given level")] public void Fade( diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyThing.cs new file mode 100644 index 0000000..d84528c --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/PropertyThing.cs @@ -0,0 +1,38 @@ +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class PropertyThing : Thing + { + public override string Name => "Property"; + + private bool _on; + + [ThingProperty(Type = new[] {"OnOffProperty"}, Title = "On/Off", Description = "Whether the lamp is turned on")] + public bool On + { + get => _on; + set + { + _on = value; + OnPropertyChanged(); + } + } + + private int _brightness; + + [ThingProperty(Type = new[] {"BrightnessProperty"}, Title = "Brightness", + Description = "The level of light from 0-100", Minimum = 0, Maximum = 100)] + public int Brightness + { + get => _brightness; + set + { + _brightness = value; + OnPropertyChanged(); + } + } + + public int Reader => _brightness; + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyThing.cs new file mode 100644 index 0000000..60050ec --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/WebSocketPropertyThing.cs @@ -0,0 +1,38 @@ +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class WebSocketPropertyThing : Thing + { + public override string Name => "web-socket-property"; + + private bool _on; + + [ThingProperty(Type = new[] {"OnOffProperty"}, Title = "On/Off", Description = "Whether the lamp is turned on")] + public bool On + { + get => _on; + set + { + _on = value; + OnPropertyChanged(); + } + } + + private int _brightness; + + [ThingProperty(Type = new[] {"BrightnessProperty"}, Title = "Brightness", + Description = "The level of light from 0-100", Minimum = 0, Maximum = 100)] + public int Brightness + { + get => _brightness; + set + { + _brightness = value; + OnPropertyChanged(); + } + } + + public int Reader => _brightness; + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs index 56a9089..bb5e61a 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs @@ -15,6 +15,8 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets { public class Action { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + [Theory] [InlineData(50, 2_000)] public async Task Create(int level, int duration) @@ -28,9 +30,19 @@ public async Task Create(int level, int duration) var uri = new UriBuilder(client.BaseAddress) { Scheme = "ws", - Path = "/things/lamp" + Path = "/things/action" }.Uri; - var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + + 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($@" {{ @@ -44,11 +56,15 @@ await socket }} }} }}"), WebSocketMessageType.Text, true, - CancellationToken.None) + source.Token) .ConfigureAwait(false); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + var segment = new ArraySegment(new byte[4096]); - var result = await socket.ReceiveAsync(segment, CancellationToken.None) + var result = await socket.ReceiveAsync(segment, source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -62,13 +78,16 @@ await socket json.Data.Fade.Input.Should().NotBeNull(); json.Data.Fade.Input.Level.Should().Be(level); json.Data.Fade.Input.Duration.Should().Be(duration); - json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Href.Should().StartWith("/things/action/actions/fade/"); json.Data.Fade.Status.Should().Be("pending"); json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); json.Data.Fade.TimeCompleted.Should().BeNull(); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + segment = new ArraySegment(new byte[4096]); - result = await socket.ReceiveAsync(segment, CancellationToken.None) + result = await socket.ReceiveAsync(segment, source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -82,13 +101,17 @@ await socket json.Data.Fade.Input.Should().NotBeNull(); json.Data.Fade.Input.Level.Should().Be(level); json.Data.Fade.Input.Duration.Should().Be(duration); - json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Href.Should().StartWith("/things/action/actions/fade/"); json.Data.Fade.Status.Should().Be("executing"); json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); json.Data.Fade.TimeCompleted.Should().BeNull(); + + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + segment = new ArraySegment(new byte[4096]); - result = await socket.ReceiveAsync(segment, CancellationToken.None) + result = await socket.ReceiveAsync(segment, source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -102,19 +125,23 @@ await socket json.Data.Fade.Input.Should().NotBeNull(); json.Data.Fade.Input.Level.Should().Be(level); json.Data.Fade.Input.Duration.Should().Be(duration); - json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Href.Should().StartWith("/things/action/actions/fade/"); json.Data.Fade.Status.Should().Be("completed"); json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); json.Data.Fade.TimeCompleted.Should().NotBeNull(); - var response = await client.GetAsync($"/things/lamp/actions/fade"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await client.GetAsync($"/things/action/actions/fade", source.Token) + .ConfigureAwait(false); var message = await response.Content.ReadAsStringAsync(); var json2 = JsonConvert.DeserializeObject>(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json2[0].Href.Should().StartWith("/things/lamp/actions/fade/"); + json2[0].Href.Should().StartWith("/things/action/actions/fade/"); json2[0].Status.Should().NotBeNullOrEmpty(); json2[0].Status.Should().Be("completed"); json2[0].TimeRequested.Should().BeBefore(DateTime.UtcNow); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs index 0c7f750..4caa617 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Net; +using System.Net.Http; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -14,23 +16,33 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets { public class Event { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly WebSocketClient _webSocketClient; + private readonly HttpClient _client; + private readonly Uri _uri; + + public Event() + { + var host = Program.GetHost().GetAwaiter().GetResult(); + _client = host.GetTestServer().CreateClient(); + _webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + _uri = new UriBuilder(_client.BaseAddress) {Scheme = "ws", Path = "/things/event"}.Uri; + } + [Theory] [InlineData("overheated")] public async Task EventSubscription(string @event) { - var host = await Program.CreateHostBuilder(null) - .StartAsync() + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await _webSocketClient.ConnectAsync(_uri, source.Token) .ConfigureAwait(false); - var client = host.GetTestServer().CreateClient(); - var webSocketClient = host.GetTestServer().CreateWebSocketClient(); - - var uri = new UriBuilder(client.BaseAddress) - { - Scheme = "ws", - Path = "/things/lamp" - }.Uri; - var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + await socket .SendAsync(Encoding.UTF8.GetBytes($@" {{ @@ -39,11 +51,14 @@ await socket ""{@event}"": {{}} }} }}"), WebSocketMessageType.Text, true, - CancellationToken.None) + source.Token) .ConfigureAwait(false); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + var segment = new ArraySegment(new byte[4096]); - var result = await socket.ReceiveAsync(segment, CancellationToken.None) + var result = await socket.ReceiveAsync(segment,source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -73,19 +88,25 @@ await socket overheated .GetValue("timestamp", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Date); - var response = await client.GetAsync("/things/Lamp/events/overheated"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/event/events/{@event}", 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(); + var message = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Array); ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); - obj = ((JArray)json)[0] as JObject; + obj = ((JArray)json).Last() as JObject; obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Object); ((JObject)obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase)) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs index b4fb148..100ce3a 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs @@ -1,12 +1,12 @@ 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 Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; using Xunit; @@ -14,24 +14,36 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets { public class Property { + private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30); + private readonly WebSocketClient _webSocketClient; + private readonly HttpClient _client; + private readonly Uri _uri; + public Property() + { + 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" + }.Uri; + } + [Theory] [InlineData("on", true)] [InlineData("brightness", 10)] public async Task SetProperties(string property, object value) { - var host = await Program.CreateHostBuilder(null) - .StartAsync() + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await _webSocketClient.ConnectAsync(_uri, source.Token) .ConfigureAwait(false); - var client = host.GetTestServer().CreateClient(); - var webSocketClient = host.GetTestServer().CreateWebSocketClient(); - - var uri = new UriBuilder(client.BaseAddress) - { - Scheme = "ws", - Path = "/things/lamp" - }.Uri; - var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); await socket .SendAsync(Encoding.UTF8.GetBytes($@" {{ @@ -40,11 +52,14 @@ await socket ""{property}"": {value.ToString().ToLower()} }} }}"), WebSocketMessageType.Text, true, - CancellationToken.None) + source.Token) .ConfigureAwait(false); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + var segment = new ArraySegment(new byte[4096]); - var result = await socket.ReceiveAsync(segment, CancellationToken.None) + var result = await socket.ReceiveAsync(segment, source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -62,12 +77,17 @@ await socket ""{property}"": {value.ToString().ToLower()} }} }}")); - var response = await client.GetAsync($"/things/Lamp/properties/{property}"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/web-socket-property/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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); @@ -82,19 +102,15 @@ await socket [InlineData("reader", 50, 0, "Read-only property")] public async Task SetPropertiesInvalidValue(string property, object value, object defaultValue, string errorMessage) { - var host = await Program.CreateHostBuilder(null) - .StartAsync() + var source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var socket = await _webSocketClient.ConnectAsync(_uri, source.Token) .ConfigureAwait(false); - var client = host.GetTestServer().CreateClient(); - var webSocketClient = host.GetTestServer().CreateWebSocketClient(); - - var uri = new UriBuilder(client.BaseAddress) - { - Scheme = "ws", - Path = "/things/lamp" - }.Uri; - var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + await socket .SendAsync(Encoding.UTF8.GetBytes($@" {{ @@ -103,12 +119,12 @@ await socket ""{property}"": {value.ToString().ToLower()} }} }}"), WebSocketMessageType.Text, true, - CancellationToken.None) + source.Token) .ConfigureAwait(false); var segment = new ArraySegment(new byte[4096]); - var result = await socket.ReceiveAsync(segment, CancellationToken.None) + var result = await socket.ReceiveAsync(segment, source.Token) .ConfigureAwait(false); result.MessageType.Should().Be(WebSocketMessageType.Text); @@ -128,12 +144,16 @@ await socket }} }}")); - var response = await client.GetAsync($"/things/Lamp/properties/{property}"); + source = new CancellationTokenSource(); + source.CancelAfter(s_timeout); + + var response = await _client.GetAsync($"/things/web-socket-property/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(); + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); json = JToken.Parse(message); json.Type.Should().Be(JTokenType.Object); From 433a6d4817cea22ddecfcf6ef0f0a8d8d80a9f9e Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 12:26:17 +0000 Subject: [PATCH 09/13] test on ARM64 --- .github/workflows/pull-request.yml | 2 +- Mozzila.IoT.WebThing.sln | 8 ++++++++ test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3a420b3..96facd3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,7 +4,7 @@ on: pull_request jobs: build: name: build - runs-on: ubuntu-latest + runs-on: [self-hosted, linux, ARM64] steps: - uses: actions/checkout@v1 - name: Setup dotnet version diff --git a/Mozzila.IoT.WebThing.sln b/Mozzila.IoT.WebThing.sln index 6b7992a..c968945 100644 --- a/Mozzila.IoT.WebThing.sln +++ b/Mozzila.IoT.WebThing.sln @@ -26,6 +26,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mozilla.IoT.WebThing.Accept EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiThing", "sample\MultiThing\MultiThing.csproj", "{3CDFC9FB-F240-419A-800D-79C506CBDAE2}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github", "Github", "{425538CC-334D-4DB3-B529-48EA7CD778BF}" +ProjectSection(SolutionItems) = preProject + .github\workflows\pull-request.yml = .github\workflows\pull-request.yml + .github\workflows\build-master.yml = .github\workflows\build-master.yml + .github\workflows\release.yml = .github\workflows\release.yml +EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,5 +113,6 @@ Global {6FB673AA-FD52-4509-97C8-28572549F609} = {370B1F76-EFE0-44D4-A395-59F5EF266112} {0D709627-98FA-4A39-8631-90C982ADED44} = {65C51E32-2901-4983-A238-0F931D9EB651} {3CDFC9FB-F240-419A-800D-79C506CBDAE2} = {370B1F76-EFE0-44D4-A395-59F5EF266112} + {425538CC-334D-4DB3-B529-48EA7CD778BF} = {E90FFA85-A210-450A-AA08-528D7F8962C2} EndGlobalSection EndGlobal diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 4f56f06..657e780 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -792,7 +792,7 @@ public async Task GetWhenUseThingAdapter() { var source = new CancellationTokenSource(); source.CancelAfter(s_timeout); - + var host = await Program.CreateHostBuilder(null, opt => opt.UseThingAdapterUrl = true) .StartAsync(source.Token) .ConfigureAwait(false); From 46d26e4ed047b1068e7cbc8b478e5b9f18aa3ac7 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 12:34:43 +0000 Subject: [PATCH 10/13] Rollback change on PR --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 96facd3..3a420b3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,7 +4,7 @@ on: pull_request jobs: build: name: build - runs-on: [self-hosted, linux, ARM64] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup dotnet version From be5fb1e9ef65718156829174d186450113126595 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 12:37:54 +0000 Subject: [PATCH 11/13] Fixes error to execute LongRun --- .../Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs | 4 ++-- test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs index 2b3e560..6c67edc 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/ActionThing.cs @@ -18,9 +18,9 @@ public void Fade( } - public Task LongRun(CancellationToken cancellationToken) + public Task LongRun() { - return Task.Delay(3_000, cancellationToken); + return Task.Delay(3_000); } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs index fc067d1..549821a 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -52,9 +52,9 @@ public void Fade( } - public Task LongRun(CancellationToken cancellationToken) + public Task LongRun() { - return Task.Delay(3_000, cancellationToken); + return Task.Delay(3_000); } } } From 675c4997865bd7e976efe9f38019344378a945d0 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 12:42:54 +0000 Subject: [PATCH 12/13] Fixes error to comparer things --- .../Http/Thing.cs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 657e780..02b80f2 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -108,10 +108,6 @@ public async Task GetAll() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/lamp/actions/longRun"" @@ -283,10 +279,6 @@ public async Task GetAll() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/action/actions/longRun"" @@ -512,10 +504,6 @@ public async Task GetAllWhenUseThingAdapter() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/lamp/actions/longRun"" @@ -693,10 +681,6 @@ public async Task GetAllWhenUseThingAdapter() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/action/actions/longRun"" @@ -879,10 +863,6 @@ public async Task GetWhenUseThingAdapter() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/lamp/actions/longRun"" @@ -1015,10 +995,6 @@ public async Task GetWhenUseThingAdapter() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/action/actions/longRun"" @@ -1214,10 +1190,6 @@ public async Task GetWhenUseThingAdapter() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/lamp/actions/longRun"" From 7af180ccfec12d51ef378881522967b20a07abfd Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Sun, 16 Feb 2020 12:48:16 +0000 Subject: [PATCH 13/13] Fixes error to get counter in WebSocket --- .../Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs index ae3738b..b7619e7 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs @@ -6,7 +6,6 @@ namespace Mozilla.IoT.WebThing.AcceptanceTest.Things { public class EventThing : Thing { - private int _counter = 0; public EventThing() { Task.Factory.StartNew(() => @@ -15,7 +14,7 @@ public EventThing() { Task.Delay(3_000).GetAwaiter().GetResult(); var @event = Overheated; - @event?.Invoke(this, _counter++); + @event?.Invoke(this, 0); } }); @@ -25,7 +24,7 @@ public EventThing() { Task.Delay(4_000).GetAwaiter().GetResult(); var @event = OtherEvent; - @event?.Invoke(this, _counter.ToString()); + @event?.Invoke(this, 1.ToString()); } }); }