From 680ce10fa7c04ad0c325533591db1cd8b4cd0364 Mon Sep 17 00:00:00 2001 From: hello Date: Thu, 11 Jul 2024 20:57:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=20gRPC=20=E8=B4=AD=E7=89=A9?= =?UTF-8?q?=E8=BD=A6=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HelloShop.sln | 18 ++- .../Baskets/BasketListItemValidator.cs | 1 - .../AuthenticatedInterceptor.cs | 25 ++++ .../BasketServiceIntegrationTest.cs | 69 +++++++++ .../GreeterServiceIntegrationTest.cs | 25 ++++ .../GrpcConstants.cs | 10 ++ ...ackup.BasketService.FunctionalTests.csproj | 39 +++++ ...oShop.BasketService.FunctionalTests.csproj | 46 ++++++ .../HelloShop.BasketService.UnitTests.csproj | 34 +++++ .../Helpers/TestServerCallContext.cs | 71 ++++++++++ .../Services/BasketServiceTest.cs | 133 ++++++++++++++++++ .../Services/GreeterServiceTest.cs | 26 ++++ 12 files changed, 494 insertions(+), 3 deletions(-) create mode 100644 tests/HelloShop.BasketService.FunctionalTests/AuthenticatedInterceptor.cs create mode 100644 tests/HelloShop.BasketService.FunctionalTests/BasketServiceIntegrationTest.cs create mode 100644 tests/HelloShop.BasketService.FunctionalTests/GreeterServiceIntegrationTest.cs create mode 100644 tests/HelloShop.BasketService.FunctionalTests/GrpcConstants.cs create mode 100644 tests/HelloShop.BasketService.FunctionalTests/HelloShop - Backup.BasketService.FunctionalTests.csproj create mode 100644 tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj create mode 100644 tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj create mode 100644 tests/HelloShop.BasketService.UnitTests/Helpers/TestServerCallContext.cs create mode 100644 tests/HelloShop.BasketService.UnitTests/Services/BasketServiceTest.cs create mode 100644 tests/HelloShop.BasketService.UnitTests/Services/GreeterServiceTest.cs diff --git a/HelloShop.sln b/HelloShop.sln index a4eec76..928b9b7 100644 --- a/HelloShop.sln +++ b/HelloShop.sln @@ -27,9 +27,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.ProductService.Un EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.ProductService.FunctionalTests", "tests\HelloShop.ProductService.FunctionalTests\HelloShop.ProductService.FunctionalTests.csproj", "{45932B7F-6ED0-40F3-AA2C-F14A844FEE18}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloShop.HybridApp", "src\HelloShop.HybridApp\HelloShop.HybridApp.csproj", "{E58F82E2-2E48-459B-A40E-497F24FC6DC1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.HybridApp", "src\HelloShop.HybridApp\HelloShop.HybridApp.csproj", "{E58F82E2-2E48-459B-A40E-497F24FC6DC1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloShop.FunctionalTests", "tests\HelloShop.FunctionalTests\HelloShop.FunctionalTests.csproj", "{6BAA9747-E0D0-41B9-8A1B-88B777498C43}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.FunctionalTests", "tests\HelloShop.FunctionalTests\HelloShop.FunctionalTests.csproj", "{6BAA9747-E0D0-41B9-8A1B-88B777498C43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloShop.BasketService.UnitTests", "tests\HelloShop.BasketService.UnitTests\HelloShop.BasketService.UnitTests.csproj", "{BE88233A-D6EB-462B-B53C-B588A0BEFAFC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloShop.BasketService.FunctionalTests", "tests\HelloShop.BasketService.FunctionalTests\HelloShop.BasketService.FunctionalTests.csproj", "{A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +91,14 @@ Global {6BAA9747-E0D0-41B9-8A1B-88B777498C43}.Debug|Any CPU.Build.0 = Debug|Any CPU {6BAA9747-E0D0-41B9-8A1B-88B777498C43}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BAA9747-E0D0-41B9-8A1B-88B777498C43}.Release|Any CPU.Build.0 = Release|Any CPU + {BE88233A-D6EB-462B-B53C-B588A0BEFAFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE88233A-D6EB-462B-B53C-B588A0BEFAFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE88233A-D6EB-462B-B53C-B588A0BEFAFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE88233A-D6EB-462B-B53C-B588A0BEFAFC}.Release|Any CPU.Build.0 = Release|Any CPU + {A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +116,8 @@ Global {45932B7F-6ED0-40F3-AA2C-F14A844FEE18} = {29BE158E-825E-48AB-A02D-4E537A5DC502} {E58F82E2-2E48-459B-A40E-497F24FC6DC1} = {1AD03316-A743-4E9D-B3BC-FB9499D15141} {6BAA9747-E0D0-41B9-8A1B-88B777498C43} = {29BE158E-825E-48AB-A02D-4E537A5DC502} + {BE88233A-D6EB-462B-B53C-B588A0BEFAFC} = {29BE158E-825E-48AB-A02D-4E537A5DC502} + {A0903D4D-EA4E-433A-AC5B-BE6ED4A5C958} = {29BE158E-825E-48AB-A02D-4E537A5DC502} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {845545A8-2006-46C3-ABD7-5BDF63F3858C} diff --git a/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs b/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs index ca5e69a..ba42044 100644 --- a/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs +++ b/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs @@ -2,7 +2,6 @@ // See the license file in the project root for more information. using FluentValidation; -using FluentValidation.Validators; using HelloShop.BasketService.Protos; namespace HelloShop.BasketService.Validations.Baskets diff --git a/tests/HelloShop.BasketService.FunctionalTests/AuthenticatedInterceptor.cs b/tests/HelloShop.BasketService.FunctionalTests/AuthenticatedInterceptor.cs new file mode 100644 index 0000000..a0975b6 --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/AuthenticatedInterceptor.cs @@ -0,0 +1,25 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Grpc.Core; +using Grpc.Core.Interceptors; +using Microsoft.Net.Http.Headers; + +namespace HelloShop.BasketService.FunctionalTests +{ + internal class AuthenticatedInterceptor : Interceptor + { + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) + { + const string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJhZG1pbiIsInJvbGVpZCI6IjEiLCJuYmYiOjE3MjA1NzY3NDYsImV4cCI6MTc0MjYwODc0NiwiaWF0IjoxNzIwNTc2NzQ2fQ.ju_D3zeGLKqJYVckbb8Y3yNkp40nOqRAJrdOsISs4d4"; + + Metadata headers = [new Metadata.Entry(HeaderNames.Authorization, $"Bearer {token}")]; + + var newOptions = context.Options.WithHeaders(headers); + + var newContext = new ClientInterceptorContext(context.Method, context.Host, newOptions); + + return base.AsyncUnaryCall(request, newContext, continuation); + } + } +} diff --git a/tests/HelloShop.BasketService.FunctionalTests/BasketServiceIntegrationTest.cs b/tests/HelloShop.BasketService.FunctionalTests/BasketServiceIntegrationTest.cs new file mode 100644 index 0000000..86b7900 --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/BasketServiceIntegrationTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Grpc.Net.Client; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.FunctionalTests +{ + public class BasketServiceIntegrationTest + { + private readonly Basket.BasketClient _client; + + public BasketServiceIntegrationTest() + { + GrpcChannel channel = GrpcChannel.ForAddress(GrpcConstants.GrpcAddress); + CallInvoker invoker = channel.Intercept(new AuthenticatedInterceptor()); + _client = new Basket.BasketClient(invoker); + } + + [Fact] + public async Task GetBasketReturnsBasket() + { + // Arrange + var request = new Empty(); + + // Act + var reply = await _client.GetBasketAsync(request); + + // Assert + Assert.NotNull(reply); + } + + [Fact] + public async Task UpdateBasketReturnsBasketResponse() + { + // Arrange + var request = new UpdateBasketRequest + { + Items = + { + new BasketListItem { ProductId = 1, Quantity = 2 }, + new BasketListItem { ProductId = 2, Quantity = 3 } + } + }; + + // Act + var reply = await _client.UpdateBasketAsync(request); + + // Assert + Assert.Equal(2, reply.Items.Count); + } + + [Fact] + public async Task DeleteBasketReturnsEmpty() + { + // Arrange + var request = new Empty(); + + // Act + var reply = await _client.DeleteBasketAsync(request); + + // Assert + Assert.NotNull(reply); + } + } +} \ No newline at end of file diff --git a/tests/HelloShop.BasketService.FunctionalTests/GreeterServiceIntegrationTest.cs b/tests/HelloShop.BasketService.FunctionalTests/GreeterServiceIntegrationTest.cs new file mode 100644 index 0000000..d7cc13a --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/GreeterServiceIntegrationTest.cs @@ -0,0 +1,25 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Grpc.Net.Client; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.FunctionalTests +{ + public class GreeterServiceIntegrationTest + { + [Fact] + public async Task SayHelloReturnsHelloMessage() + { + // Arrange + using var channel = GrpcChannel.ForAddress(GrpcConstants.GrpcAddress); + var client = new Greeter.GreeterClient(channel); + + // Act + var reply = await client.SayHelloAsync(new HelloRequest { Name = "Greeter" }); + + // Assert + Assert.Equal("Hello Greeter", reply.Message); + } + } +} diff --git a/tests/HelloShop.BasketService.FunctionalTests/GrpcConstants.cs b/tests/HelloShop.BasketService.FunctionalTests/GrpcConstants.cs new file mode 100644 index 0000000..8e2abfa --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/GrpcConstants.cs @@ -0,0 +1,10 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +namespace HelloShop.BasketService.FunctionalTests +{ + internal class GrpcConstants + { + public const string GrpcAddress = "http://localhost:8004"; + } +} diff --git a/tests/HelloShop.BasketService.FunctionalTests/HelloShop - Backup.BasketService.FunctionalTests.csproj b/tests/HelloShop.BasketService.FunctionalTests/HelloShop - Backup.BasketService.FunctionalTests.csproj new file mode 100644 index 0000000..510c478 --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/HelloShop - Backup.BasketService.FunctionalTests.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Protos\basket.proto + + + Protos\greet.proto + + + + + + + + diff --git a/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj b/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj new file mode 100644 index 0000000..df16895 --- /dev/null +++ b/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj @@ -0,0 +1,46 @@ + + + + net8.0 + enable + enable + + false + true + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Protos\basket.proto + + + Protos\greet.proto + + + + + + + + diff --git a/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj b/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj new file mode 100644 index 0000000..0c01a46 --- /dev/null +++ b/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/HelloShop.BasketService.UnitTests/Helpers/TestServerCallContext.cs b/tests/HelloShop.BasketService.UnitTests/Helpers/TestServerCallContext.cs new file mode 100644 index 0000000..9ef32b7 --- /dev/null +++ b/tests/HelloShop.BasketService.UnitTests/Helpers/TestServerCallContext.cs @@ -0,0 +1,71 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Grpc.Core; + +namespace HelloShop.BasketService.UnitTests.Helpers +{ + public class TestServerCallContext : ServerCallContext + { + private readonly Metadata _requestHeaders; + + private readonly CancellationToken _cancellationToken; + + private readonly Metadata _responseTrailers; + + private readonly AuthContext _authContext; + + private readonly Dictionary _userState; + + private WriteOptions? _writeOptions; + + public Metadata? ResponseHeaders { get; private set; } + + private TestServerCallContext(Metadata requestHeaders, CancellationToken cancellationToken) + { + _requestHeaders = requestHeaders; + _cancellationToken = cancellationToken; + _responseTrailers = []; + _authContext = new AuthContext(string.Empty, []); + _userState = []; + } + + protected override string MethodCore => "MethodName"; + + protected override string HostCore => "HostName"; + + protected override string PeerCore => "PeerName"; + + protected override DateTime DeadlineCore { get; } + + protected override Metadata RequestHeadersCore => _requestHeaders; + + protected override CancellationToken CancellationTokenCore => _cancellationToken; + + protected override Metadata ResponseTrailersCore => _responseTrailers; + + protected override Status StatusCore { get; set; } + + protected override WriteOptions? WriteOptionsCore { get => _writeOptions; set { _writeOptions = value; } } + + protected override AuthContext AuthContextCore => _authContext; + + protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) => throw new NotImplementedException(); + + protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) + { + if (ResponseHeaders != null) + { + throw new InvalidOperationException("Response headers have already been written."); + } + + ResponseHeaders = responseHeaders; + + return Task.CompletedTask; + } + + protected override IDictionary UserStateCore => _userState; + + public static TestServerCallContext Create(Metadata? requestHeaders = null, CancellationToken cancellationToken = default) => new(requestHeaders ?? [], cancellationToken); + } +} diff --git a/tests/HelloShop.BasketService.UnitTests/Services/BasketServiceTest.cs b/tests/HelloShop.BasketService.UnitTests/Services/BasketServiceTest.cs new file mode 100644 index 0000000..626cc39 --- /dev/null +++ b/tests/HelloShop.BasketService.UnitTests/Services/BasketServiceTest.cs @@ -0,0 +1,133 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using AutoMapper; +using Google.Protobuf.WellKnownTypes; +using HelloShop.BasketService.AutoMapper; +using HelloShop.BasketService.Entities; +using HelloShop.BasketService.Protos; +using HelloShop.BasketService.Repositories; +using HelloShop.BasketService.Services; +using HelloShop.BasketService.UnitTests.Helpers; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System.Security.Claims; + +namespace HelloShop.BasketService.UnitTests.Services +{ + public class BasketServiceTest + { + [Fact] + public async Task GetBasketReturnsEmptyForNoUser() + { + // Arrange + var basketRepositoryMock = new Mock(); + var loggerMock = NullLogger.Instance; + var mapperMock = new Mock(); + var service = new CustomerBasketService(basketRepositoryMock.Object, loggerMock, mapperMock.Object); + + TestServerCallContext serverCallContext = TestServerCallContext.Create(); + + var httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, "1")])) + }; + + serverCallContext.UserState["__HttpContext"] = httpContext; + + // Act + CustomerBasketResponse result = await service.GetBasket(new Empty(), serverCallContext); + + // Assert + Assert.Empty(result.Items); + } + + [Fact] + public async Task GetBasketReturnsItemsForValidUserId() + { + // Arrange + var basketRepositoryMock = new Mock(); + basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new CustomerBasket() { BuyerId = 1, Items = [new BasketItem { ProductId = 1, Quantity = 1 }] }); + + var logger = NullLogger.Instance; + + var mapper = new Mapper(new MapperConfiguration(cfg => cfg.AddProfile())); + + var service = new CustomerBasketService(basketRepositoryMock.Object, logger, mapper); + + TestServerCallContext serverCallContext = TestServerCallContext.Create(); + + HttpContext httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, "1")])) + }; + + serverCallContext.UserState["__HttpContext"] = httpContext; + + // Act + CustomerBasketResponse result = await service.GetBasket(new Empty(), serverCallContext); + + // Assert + Assert.NotEmpty(result.Items); + } + + [Fact] + public async Task UpdateBasketReturnsCustomerBasketResponse() + { + // Arrange + var basketRepositoryMock = new Mock(); + basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny(), It.IsAny())).ReturnsAsync((CustomerBasket basket, CancellationToken token) => basket); + + var logger = NullLogger.Instance; + var mapper = new Mapper(new MapperConfiguration(cfg => cfg.AddProfile())); + + var service = new CustomerBasketService(basketRepositoryMock.Object, logger, mapper); + + TestServerCallContext serverCallContext = TestServerCallContext.Create(); + + HttpContext httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, "1")])) + }; + + serverCallContext.UserState["__HttpContext"] = httpContext; + + // Act + UpdateBasketRequest updateBasketRequest = new() + { + Items = { new BasketListItem { ProductId = 1, Quantity = 1 }, new BasketListItem { ProductId = 2, Quantity = 2 } } + }; + + CustomerBasketResponse result = await service.UpdateBasket(updateBasketRequest, serverCallContext); + + // Assert + Assert.Collection(result.Items, item => Assert.Equal(1, item.ProductId), item => Assert.Equal(2, item.ProductId)); + } + + [Fact] + public async Task DeleteBasketReturnsEmpty() + { + // Arrange + var basketRepositoryMock = new Mock(); + var logger = NullLogger.Instance; + var mapper = new Mapper(new MapperConfiguration(cfg => cfg.AddProfile())); + var service = new CustomerBasketService(basketRepositoryMock.Object, logger, mapper); + + TestServerCallContext serverCallContext = TestServerCallContext.Create(); + + HttpContext httpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, "1")])) + }; + + serverCallContext.UserState["__HttpContext"] = httpContext; + + // Act + Empty result = await service.DeleteBasket(new Empty(), serverCallContext); + + // Assert + Assert.NotNull(result); + } + } +} diff --git a/tests/HelloShop.BasketService.UnitTests/Services/GreeterServiceTest.cs b/tests/HelloShop.BasketService.UnitTests/Services/GreeterServiceTest.cs new file mode 100644 index 0000000..2699a93 --- /dev/null +++ b/tests/HelloShop.BasketService.UnitTests/Services/GreeterServiceTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.BasketService.Protos; +using HelloShop.BasketService.Services; +using HelloShop.BasketService.UnitTests.Helpers; + +namespace HelloShop.BasketService.UnitTests.Services +{ + public class GreeterServiceTest + { + [Fact] + public async Task SayHelloReturnsCorrectMessage() + { + // Arrange + var service = new GreeterService(); + var request = new HelloRequest { Name = "World" }; + + // Act + var response = await service.SayHello(request, TestServerCallContext.Create()); + + // Assert + Assert.Equal("Hello World", response.Message); + } + } +}