diff --git a/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceRequest.cs b/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceRequest.cs new file mode 100644 index 00000000..b0aacbf4 --- /dev/null +++ b/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceRequest.cs @@ -0,0 +1,32 @@ +namespace Meadow.Cloud.Client.Devices; + +public class AddDeviceRequest(string id, string orgId, string publicKey) +{ + public AddDeviceRequest(string id, string name, string orgId, string publicKey) + : this(id, orgId, publicKey) + { + Name = name; + } + + public AddDeviceRequest(string id, string name, string orgId, string collectionId, string publicKey) + : this(id, orgId, publicKey) + { + Name = name; + CollectionId = collectionId; + } + + [JsonPropertyName("id")] + public string Id { get; set; } = id; + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("orgId")] + public string OrgId { get; set; } = orgId; + + [JsonPropertyName("collectionId")] + public string? CollectionId { get; set; } + + [JsonPropertyName("publicKey")] + public string PublicKey { get; set; } = publicKey; +} diff --git a/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceResponse.cs b/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceResponse.cs new file mode 100644 index 00000000..529576da --- /dev/null +++ b/Source/v2/Meadow.Cloud.Client/Devices/AddDeviceResponse.cs @@ -0,0 +1,16 @@ +namespace Meadow.Cloud.Client.Devices; + +public class AddDeviceResponse(string id, string name, string orgId, string collectionId) +{ + [JsonPropertyName("id")] + public string Id { get; set; } = id; + + [JsonPropertyName("name")] + public string? Name { get; set; } = name; + + [JsonPropertyName("orgId")] + public string OrgId { get; set; } = orgId; + + [JsonPropertyName("collectionId")] + public string? CollectionId { get; set; } = collectionId; +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Devices/DeviceClient.cs b/Source/v2/Meadow.Cloud.Client/Devices/DeviceClient.cs index 96ca611e..50eef5a1 100644 --- a/Source/v2/Meadow.Cloud.Client/Devices/DeviceClient.cs +++ b/Source/v2/Meadow.Cloud.Client/Devices/DeviceClient.cs @@ -1,9 +1,22 @@ namespace Meadow.Cloud.Client.Devices; -public interface IDeviceClient +public class DeviceClient : MeadowCloudClientBase, IDeviceClient { -} + public DeviceClient(MeadowCloudContext meadowCloudContext, ILogger logger) + : base(meadowCloudContext, logger) + { + } -public class DeviceClient : IDeviceClient -{ -} + public async Task AddDevice(AddDeviceRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + using var httpRequest = CreateHttpRequestMessage(HttpMethod.Post, "api/v1/devices", request); + using var httpResponse = await HttpClient.SendAsync(httpRequest, cancellationToken); + + return await ProcessResponse(httpResponse, cancellationToken); + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Devices/IDeviceClient.cs b/Source/v2/Meadow.Cloud.Client/Devices/IDeviceClient.cs new file mode 100644 index 00000000..254ec593 --- /dev/null +++ b/Source/v2/Meadow.Cloud.Client/Devices/IDeviceClient.cs @@ -0,0 +1,6 @@ +namespace Meadow.Cloud.Client.Devices; + +public interface IDeviceClient +{ + Task AddDevice(AddDeviceRequest request, CancellationToken cancellationToken = default); +} diff --git a/Source/v2/Meadow.Cloud.Client/Firmware/FirmwareClient.cs b/Source/v2/Meadow.Cloud.Client/Firmware/FirmwareClient.cs index 814cdf9c..052656ed 100644 --- a/Source/v2/Meadow.Cloud.Client/Firmware/FirmwareClient.cs +++ b/Source/v2/Meadow.Cloud.Client/Firmware/FirmwareClient.cs @@ -19,7 +19,7 @@ public async Task> GetVersions(string t if (response.StatusCode == HttpStatusCode.NotFound) { - return Enumerable.Empty(); + return []; } return await ProcessResponse>(response, cancellationToken); diff --git a/Source/v2/Meadow.Cloud.Client/MeadowCloudClient.cs b/Source/v2/Meadow.Cloud.Client/MeadowCloudClient.cs index be8777a1..a5fc38a3 100644 --- a/Source/v2/Meadow.Cloud.Client/MeadowCloudClient.cs +++ b/Source/v2/Meadow.Cloud.Client/MeadowCloudClient.cs @@ -13,6 +13,7 @@ public class MeadowCloudClient : IMeadowCloudClient public const string DefaultHost = "https://www.meadowcloud.co"; public static readonly Uri DefaultHostUri = new(DefaultHost); + private readonly Lazy _deviceClient; private readonly Lazy _firmwareClient; private readonly MeadowCloudContext _meadowCloudContext; private readonly IdentityManager _identityManager; @@ -22,7 +23,8 @@ public MeadowCloudClient(HttpClient httpClient, IdentityManager identityManager, loggerFactory ??= NullLoggerFactory.Instance; _meadowCloudContext = new(httpClient, userAgent); - + + _deviceClient = new Lazy(() => new DeviceClient(_meadowCloudContext, loggerFactory.CreateLogger())); _firmwareClient = new Lazy(() => new FirmwareClient(_meadowCloudContext, loggerFactory.CreateLogger())); _identityManager = identityManager; } @@ -30,7 +32,7 @@ public MeadowCloudClient(HttpClient httpClient, IdentityManager identityManager, public IApiTokenClient ApiToken => throw new NotImplementedException("This client is not implemented yet. Please use the 'ApiTokenService' instead."); public ICollectionClient Collection => throw new NotImplementedException("This client is not implemented yet. Please use the 'CollectionService' instead."); public ICommandClient Command => throw new NotImplementedException("This client is not implemented yet. Please use the 'CommandService' instead."); - public IDeviceClient Device => throw new NotImplementedException("This client is not implemented yet. Please use the 'DeviceService' instead."); + public IDeviceClient Device => _deviceClient.Value; public IFirmwareClient Firmware => _firmwareClient.Value; public IPackageClient Package => throw new NotImplementedException("This client is not implemented yet. Please use the 'PackageService' instead."); public IUserClient User => throw new NotImplementedException("This client is not implemented yet. Please use the 'UserService' instead."); diff --git a/Source/v2/Meadow.Cloud.Client/MeadowCloudClientBase.cs b/Source/v2/Meadow.Cloud.Client/MeadowCloudClientBase.cs index 08c3d70c..7132399e 100644 --- a/Source/v2/Meadow.Cloud.Client/MeadowCloudClientBase.cs +++ b/Source/v2/Meadow.Cloud.Client/MeadowCloudClientBase.cs @@ -1,4 +1,6 @@ -namespace Meadow.Cloud.Client; +using System.Net.Http; + +namespace Meadow.Cloud.Client; public abstract class MeadowCloudClientBase { @@ -54,6 +56,21 @@ protected HttpRequestMessage CreateHttpRequestMessage(HttpMethod method, string return CreateHttpRequestMessage(method, new Uri(urlBuilder.ToString(), UriKind.Relative)); } + protected HttpRequestMessage CreateHttpRequestMessage(HttpMethod method, string requestUri, T request) + { + var json = JsonSerializer.SerializeToUtf8Bytes(request); + var content = new ByteArrayContent(json); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + var httpRequest = new HttpRequestMessage(method, new Uri(MeadowCloudContext.BaseAddress, requestUri)) + { + Content = content + }; + SetHeaders(httpRequest); + + return httpRequest; + } + private static IReadOnlyDictionary> GetHeaders(HttpResponseMessage response) { var headers = new Dictionary>(); diff --git a/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/DeviceClientTests/AddDeviceTests.cs b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/DeviceClientTests/AddDeviceTests.cs new file mode 100644 index 00000000..60099b64 --- /dev/null +++ b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/DeviceClientTests/AddDeviceTests.cs @@ -0,0 +1,72 @@ +namespace Meadow.Cloud.Client.Unit.Tests.DeviceClientTests; + +public class AddDeviceTests +{ + private readonly FakeableHttpMessageHandler _handler; + private readonly DeviceClient _deviceClient; + + public AddDeviceTests() + { + _handler = A.Fake(); + var httpClient = new HttpClient(_handler) { BaseAddress = new Uri("https://example.org") }; + + A.CallTo(() => _handler + .FakeSendAsync(A.Ignored, A.Ignored)) + .Returns(new HttpResponseMessage(HttpStatusCode.NotFound)); + + var context = new MeadowCloudContext(httpClient, new Uri("https://example.org"), new MeadowCloudUserAgent("Meadow.Cloud.Client.Unit.Tests")); + _deviceClient = new DeviceClient(context, NullLogger.Instance); + } + + [Fact] + public async Task AddDevice_WithNullRequest_ShouldThrowException() + { + // Act/Assert + await Assert.ThrowsAsync(() => _deviceClient.AddDevice(null!)); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest)] + [InlineData(HttpStatusCode.Conflict)] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.Unauthorized)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task AddDevice_WithUnsuccessfulResponse_ShouldThrowException(HttpStatusCode httpStatusCode) + { + // Arrange + var addDeviceRequest = new AddDeviceRequest("id", "orgId", "publicKey"); + + A.CallTo(() => _handler + .FakeSendAsync( + A.That.Matches(r => r.RequestUri!.AbsolutePath == $"/api/v1/devices"), + A.Ignored)) + .Returns(new HttpResponseMessage(httpStatusCode)); + + // Act/Assert + var ex = await Assert.ThrowsAsync(() => _deviceClient.AddDevice(addDeviceRequest)); + Assert.Equal(httpStatusCode, ex.StatusCode); + } + + [Fact] + public async Task AddDevice_WithResponse_ShouldReturnResult() + { + // Arrange + var addDeviceRequest = new AddDeviceRequest("device-id", "device-org-id", "device-public-key"); + + A.CallTo(() => _handler + .FakeSendAsync( + A.That.Matches(r => r.RequestUri!.AbsolutePath == $"/api/v1/devices"), + A.Ignored)) + .Returns(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = JsonContent.Create(new AddDeviceResponse("device-id", "name", "device-org-id", "device-collection-id")) + }); + + // Act + var response = await _deviceClient.AddDevice(addDeviceRequest); + + // Assert + Assert.NotNull(response); + Assert.Equal("device-id", response.Id); + } +} diff --git a/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Usings.cs b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Usings.cs index 6b621aaf..eedefe38 100644 --- a/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Usings.cs +++ b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Usings.cs @@ -1,6 +1,8 @@ global using FakeItEasy; +global using Meadow.Cloud.Client.Devices; global using Meadow.Cloud.Client.Firmware; global using Meadow.Cloud.Client.Unit.Tests.Builders; +global using Microsoft.Extensions.Logging.Abstractions; global using System.Net; global using System.Net.Http.Headers; global using System.Net.Http.Json;