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/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..02b80f2 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"": { @@ -103,10 +108,6 @@ public async Task GetAll() ] }, ""longRun"": { - ""input"": { - ""type"": ""object"", - ""properties"": {} - }, ""links"": [ { ""href"": ""/things/lamp/actions/longRun"" @@ -125,15 +126,6 @@ public async Task GetAll() ""href"": ""/things/lamp/events/overheated"" } ] - }, - ""otherEvent"": { - ""title"": ""OtherEvent"", - ""type"": ""string"", - ""links"": [ - { - ""href"": ""/things/lamp/events/otherEvent"" - } - ] } }, ""links"": [ @@ -154,200 +146,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 +159,7 @@ public async Task GetAllWhenUseThingAdapter() ""@type"": ""OnOffProperty"", ""links"": [ { - ""href"": ""/things/lamp/properties/on"" + ""href"": ""/things/property/properties/on"" } ] }, @@ -371,7 +173,7 @@ public async Task GetAllWhenUseThingAdapter() ""maximum"": 100, ""links"": [ { - ""href"": ""/things/lamp/properties/brightness"" + ""href"": ""/things/property/properties/brightness"" } ] }, @@ -379,49 +181,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 +216,7 @@ public async Task GetAllWhenUseThingAdapter() ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/lamp/events/overheated"" + ""href"": ""/things/event/events/overheated"" } ] }, @@ -439,7 +225,7 @@ public async Task GetAllWhenUseThingAdapter() ""type"": ""string"", ""links"": [ { - ""href"": ""/things/lamp/events/otherEvent"" + ""href"": ""/things/event/events/otherEvent"" } ] } @@ -447,33 +233,197 @@ 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"": { + ""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 +432,802 @@ 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"": { + ""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"": { + ""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"": { + ""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"": { + ""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"": { + ""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..6c67edc --- /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() + { + return Task.Delay(3_000); + } + + } +} 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..b7619e7 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/EventThing.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Mozilla.IoT.WebThing.Attributes; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.Things +{ + public class EventThing : Thing + { + public EventThing() + { + Task.Factory.StartNew(() => + { + while (true) + { + Task.Delay(3_000).GetAwaiter().GetResult(); + var @event = Overheated; + @event?.Invoke(this, 0); + } + }); + + Task.Factory.StartNew(() => + { + while (true) + { + Task.Delay(4_000).GetAwaiter().GetResult(); + var @event = OtherEvent; + @event?.Invoke(this, 1.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..549821a 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( @@ -81,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); } } } 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);