From ca9d77bc3b9febac3e3ea7b4189d275e694c31b6 Mon Sep 17 00:00:00 2001 From: Max Lin Date: Wed, 19 Feb 2025 04:52:46 +1100 Subject: [PATCH 01/11] impmlement unit test --- AutoPile.API/Program.cs | 1 + AutoPile.DATA/Data/AutoPileMongoDbContext.cs | 4 +- AutoPile.DOMAIN/Interface/IStripeService.cs | 9 + AutoPile.SERVICE/Services/AuthService.cs | 24 +- AutoPile.SERVICE/Services/PaymentService.cs | 27 +-- AutoPile.SERVICE/Services/StripeService.cs | 27 +++ AutoPile.UnitTests/AuthServiceTests.cs | 236 +++++++++++++++++++ AutoPile.UnitTests/AutoPile.UnitTests.csproj | 28 +++ AutoPile.UnitTests/PaymentServiceTests.cs | 55 +++++ AutoPile.UnitTests/ReviewServiceTests.cs | 201 ++++++++++++++++ AutoPile.sln | 6 + 11 files changed, 576 insertions(+), 42 deletions(-) create mode 100644 AutoPile.DOMAIN/Interface/IStripeService.cs create mode 100644 AutoPile.SERVICE/Services/StripeService.cs create mode 100644 AutoPile.UnitTests/AuthServiceTests.cs create mode 100644 AutoPile.UnitTests/AutoPile.UnitTests.csproj create mode 100644 AutoPile.UnitTests/PaymentServiceTests.cs create mode 100644 AutoPile.UnitTests/ReviewServiceTests.cs diff --git a/AutoPile.API/Program.cs b/AutoPile.API/Program.cs index 329aeed..22e381e 100644 --- a/AutoPile.API/Program.cs +++ b/AutoPile.API/Program.cs @@ -110,6 +110,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Add this to the service configuration section in Program.cs // Register both QueueClients // Register QueueClient for Email Queue diff --git a/AutoPile.DATA/Data/AutoPileMongoDbContext.cs b/AutoPile.DATA/Data/AutoPileMongoDbContext.cs index fc4f5f9..0b84560 100644 --- a/AutoPile.DATA/Data/AutoPileMongoDbContext.cs +++ b/AutoPile.DATA/Data/AutoPileMongoDbContext.cs @@ -14,8 +14,8 @@ namespace AutoPile.DATA.Data { public class AutoPileMongoDbContext : DbContext { - public DbSet Products { get; set; } - public DbSet Reviews { get; set; } + public virtual DbSet Products { get; set; } + public virtual DbSet Reviews { get; set; } public static AutoPileMongoDbContext Create(IMongoDatabase database) => new(new DbContextOptionsBuilder() diff --git a/AutoPile.DOMAIN/Interface/IStripeService.cs b/AutoPile.DOMAIN/Interface/IStripeService.cs new file mode 100644 index 0000000..46e33e0 --- /dev/null +++ b/AutoPile.DOMAIN/Interface/IStripeService.cs @@ -0,0 +1,9 @@ +using Stripe; + +namespace AutoPile.SERVICE.Services +{ + public interface IStripeService + { + Task CreatePaymentIntentAsync(long amount, string currency); + } +} \ No newline at end of file diff --git a/AutoPile.SERVICE/Services/AuthService.cs b/AutoPile.SERVICE/Services/AuthService.cs index fb2a4cd..d924e5d 100644 --- a/AutoPile.SERVICE/Services/AuthService.cs +++ b/AutoPile.SERVICE/Services/AuthService.cs @@ -113,20 +113,13 @@ public AuthService(UserManager userManager, ILogger u.PhoneNumber == userSignupDTO.PhoneNumber); + var existUserWithPhone = _userManager.Users.Any(u => u.PhoneNumber == userSignupDTO.PhoneNumber); if (existUserWithPhone) { throw new BadRequestException("Phone number already registered"); } - var user = new ApplicationUser - { - UserName = userSignupDTO.UserName, - Email = userSignupDTO.Email, - FirstName = userSignupDTO.FirstName, - LastName = userSignupDTO.LastName, - PhoneNumber = userSignupDTO.PhoneNumber - }; + var user = _mapper.Map(userSignupDTO); var result = await _userManager.CreateAsync(user, userSignupDTO.Password); if (!result.Succeeded) @@ -144,13 +137,8 @@ public AuthService(UserManager userManager, ILogger(user); + responseDTO.Roles = await _userManager.GetRolesAsync(user); return (responseDTO, token); } @@ -165,8 +153,8 @@ public AuthService(UserManager userManager, ILogger(user); userResponseDTO.Roles = await _userManager.GetRolesAsync(user); - var userResponseInfoDTO = _mapper.Map(user); - userResponseInfoDTO.Roles = userResponseDTO.Roles; + //var userResponseInfoDTO = _mapper.Map(user); + //userResponseInfoDTO.Roles = userResponseDTO.Roles; //await _userInfoCache.SetUserAsync(user.Id, userResponseInfoDTO); _logger.LogInformation("Successfully cached user info for user {UserId}", user.Id); return (userResponseDTO, token); diff --git a/AutoPile.SERVICE/Services/PaymentService.cs b/AutoPile.SERVICE/Services/PaymentService.cs index 02f851e..e93069e 100644 --- a/AutoPile.SERVICE/Services/PaymentService.cs +++ b/AutoPile.SERVICE/Services/PaymentService.cs @@ -1,27 +1,22 @@ -using AutoMapper; -using AutoPile.DATA.Data; +using AutoPile.DATA.Data; using AutoPile.DATA.Exceptions; using AutoPile.DOMAIN.DTOs.Requests; using MongoDB.Bson; using Stripe; -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace AutoPile.SERVICE.Services { public class PaymentService : IPaymentService { - private readonly AutoPileManagementDbContext _autoPileManagementDbContext; private readonly AutoPileMongoDbContext _autoPileMongoDbContext; - private readonly IMapper _mapper; + private readonly IStripeService _stripeService; - public PaymentService(AutoPileManagementDbContext autoPileManagementDbContext, AutoPileMongoDbContext autoPileMongoDbContext, IMapper mapper) + public PaymentService(AutoPileMongoDbContext autoPileMongoDbContext, IStripeService stripeService) { - _autoPileManagementDbContext = autoPileManagementDbContext; _autoPileMongoDbContext = autoPileMongoDbContext; - _mapper = mapper; + _stripeService = stripeService; } public async Task PaymentIntentCreateAsync(PaymentIntentCreate paymentIntentCreate) @@ -32,19 +27,7 @@ public async Task PaymentIntentCreateAsync(PaymentIntentCreate pa } var totalAmount = await CalculateTotalAmountAsync(paymentIntentCreate.Items); - - var options = new PaymentIntentCreateOptions - { - Amount = (long)(totalAmount * 100), - Currency = "aud", - AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions - { - Enabled = true, - }, - }; - - var service = new PaymentIntentService(); - return await service.CreateAsync(options); + return await _stripeService.CreatePaymentIntentAsync((long)(totalAmount * 100), "aud"); } private async Task CalculateTotalAmountAsync(Item[] items) diff --git a/AutoPile.SERVICE/Services/StripeService.cs b/AutoPile.SERVICE/Services/StripeService.cs new file mode 100644 index 0000000..ec852d3 --- /dev/null +++ b/AutoPile.SERVICE/Services/StripeService.cs @@ -0,0 +1,27 @@ +using Stripe; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AutoPile.SERVICE.Services +{ + public class StripeService : IStripeService + { + public async Task CreatePaymentIntentAsync(long amount, string currency) + { + var options = new PaymentIntentCreateOptions + { + Amount = amount, + Currency = currency, + AutomaticPaymentMethods = new PaymentIntentAutomaticPaymentMethodsOptions + { + Enabled = true, + }, + }; + var service = new PaymentIntentService(); + return await service.CreateAsync(options); + } + } +} \ No newline at end of file diff --git a/AutoPile.UnitTests/AuthServiceTests.cs b/AutoPile.UnitTests/AuthServiceTests.cs new file mode 100644 index 0000000..c481c99 --- /dev/null +++ b/AutoPile.UnitTests/AuthServiceTests.cs @@ -0,0 +1,236 @@ +using AutoMapper; +using AutoPile.DATA.Cache; +using AutoPile.DATA.Exceptions; +using AutoPile.DOMAIN.DTOs.Requests; +using AutoPile.DOMAIN.DTOs.Responses; +using AutoPile.DOMAIN.Models.Entities; +using AutoPile.SERVICE.Services; +using AutoPile.SERVICE.Utilities; +using FluentAssertions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using Resend; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AutoPile.UnitTests +{ + public class AuthServiceTests + { + private readonly Mock> _userManagerMock; + private readonly Mock _mapperMock; + private readonly Mock _jwtTokenGeneratorMock; + private readonly Mock _resendMock; + private readonly Mock _configurationMock; + private readonly Mock _emailQueueServiceMock; + private readonly Mock _userInfoCacheMock; + private readonly Mock> _loggerMock; + private readonly AuthService _authServiceMock; + + public AuthServiceTests() + { + var storeMock = new Mock>(); + _userManagerMock = new Mock>( + storeMock.Object, + null, // IOptions + null, // IPasswordHasher + null, // IEnumerable> + null, // IEnumerable> + null, // ILookupNormalizer + null, // IdentityErrorDescriber + null, // IServiceProvider + null // ILogger> + ); + _mapperMock = new Mock(); + _userInfoCacheMock = new Mock(); + _jwtTokenGeneratorMock = new Mock(); + _configurationMock = new Mock(); + _resendMock = new Mock(); + _emailQueueServiceMock = new Mock(); + _loggerMock = new Mock>(); + _authServiceMock = new AuthService(_userManagerMock.Object, _loggerMock.Object, _emailQueueServiceMock.Object, + _configurationMock.Object, _mapperMock.Object, _userInfoCacheMock.Object, _jwtTokenGeneratorMock.Object, _resendMock.Object); + } + + [Fact] + public async Task SignUpUser_ValidInput_ReturnUserResponseDTO() + { + // Arrange + var password = "TestPassword"; + var userSignUpDTO = new UserSignupDTO() + { + FirstName = "firstName", + LastName = "lastName", + UserName = "TestUserName", + PhoneNumber = "TestPhoneNumber", + Email = "TestEmail", + Password = password + }; + + _mapperMock.Setup(m => m.Map(It.IsAny())) + .Returns(new ApplicationUser + { + UserName = userSignUpDTO.UserName, + Email = userSignUpDTO.Email, + FirstName = userSignUpDTO.FirstName, + LastName = userSignUpDTO.LastName, + PhoneNumber = userSignUpDTO.PhoneNumber, + Id = "TestId" + }); + + _mapperMock.Setup(m => m.Map(It.IsAny())) + .Returns(new UserResponseDTO + { + UserName = userSignUpDTO.UserName, + Email = userSignUpDTO.Email, + Id = "TestId" + }); + + _userManagerMock.Setup(u => u.FindByEmailAsync(userSignUpDTO.Email)) + .ReturnsAsync(null as ApplicationUser); + + _userManagerMock.Setup(u => u.Users) + .Returns(new List().AsQueryable()); + + _userManagerMock.Setup(u => u.CreateAsync(It.IsAny(), password)) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(u => u.AddToRoleAsync(It.IsAny(), "User")) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(u => u.GetRolesAsync(It.IsAny())) + .ReturnsAsync(["User"]); + + _jwtTokenGeneratorMock.Setup(t => t.GenerateJwtToken(It.IsAny())) + .Returns("test_access_token"); + + // Act + var result = await _authServiceMock.SignupUserAsync(userSignUpDTO); + + // Assert + result.Should().NotBeNull(); + var (userResponse, accessToken, refreshToken) = result; + + userResponse.Should().NotBeNull(); + userResponse.Should().BeOfType(); + userResponse.UserName.Should().Be(userSignUpDTO.UserName); + userResponse.Email.Should().Be(userSignUpDTO.Email); + userResponse.Roles.Should().Contain("User"); + + accessToken.Should().Be("test_access_token"); + refreshToken.Should().NotBeNull(); + + _userManagerMock.Verify(u => u.CreateAsync(It.IsAny(), password), Times.Once); + _userManagerMock.Verify(u => u.AddToRoleAsync(It.IsAny(), "User"), Times.Once); + _jwtTokenGeneratorMock.Verify(t => t.GenerateJwtToken(It.IsAny()), Times.Once); + } + + [Fact] + public async Task SignInUser_ValidInput_ReturnUserResponseDTO() + { + //Arrange + var password = "TestPassword"; + var userSignUpDTO = new UserSignupDTO() + { + FirstName = "firstName", + LastName = "lastName", + UserName = "TestUserName", + PhoneNumber = "TestPhoneNumber", + Email = "TestEmail", + Password = password + }; + + var userSigninDTO = new UserSigninDTO() + { + Email = "TestEmail", + Password = password + }; + + var user = new ApplicationUser + { + UserName = userSignUpDTO.UserName, + Email = userSignUpDTO.Email, + FirstName = userSignUpDTO.FirstName, + LastName = userSignUpDTO.LastName, + PhoneNumber = userSignUpDTO.PhoneNumber, + Id = "TestId" + }; + _userManagerMock.Setup(u => u.FindByEmailAsync(userSignUpDTO.Email)).ReturnsAsync(user); + _userManagerMock.Setup(u => u.CheckPasswordAsync(It.IsAny(), password)).ReturnsAsync(true); + _jwtTokenGeneratorMock.Setup(t => t.GenerateJwtToken(It.IsAny())).Returns("test_access_token"); + + _mapperMock.Setup(m => m.Map(user)).Returns(new UserResponseDTO + { + UserName = userSignUpDTO.UserName, + Email = userSignUpDTO.Email, + Id = "TestId", + }); + + _userManagerMock.Setup(u => u.GetRolesAsync(It.IsAny())) + .ReturnsAsync(["User"]); + + //Act + var result = await _authServiceMock.SigninAsync(userSigninDTO); + + //Assert + result.Should().NotBeNull(); + var (userResponse, accessToken, refreshToken) = result; + + userResponse.Should().NotBeNull(); + userResponse.Should().BeOfType(); + userResponse.UserName.Should().Be(userSignUpDTO.UserName); + userResponse.Email.Should().Be(userSignUpDTO.Email); + userResponse.Roles.Should().Contain("User"); + + accessToken.Should().Be("test_access_token"); + refreshToken.Should().NotBeNull(); + + _userManagerMock.Verify(u => u.CheckPasswordAsync(It.IsAny(), password), Times.Once); + _jwtTokenGeneratorMock.Verify(t => t.GenerateJwtToken(It.IsAny()), Times.Once); + _userManagerMock.Verify(u => u.GetRolesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task SigninUser_WrongCredentialInput_ReturnNoFoundException() + { + //Arrange + var password = "TestPassword"; + var userSignUpDTO = new UserSignupDTO() + { + FirstName = "firstName", + LastName = "lastName", + UserName = "TestUserName", + PhoneNumber = "TestPhoneNumber", + Email = "TestEmail", + Password = password + }; + var userSignInDTO = new UserSigninDTO() + { + Email = "TestEmail", + Password = password + }; + var user = new ApplicationUser + { + UserName = userSignUpDTO.UserName, + Email = userSignUpDTO.Email, + FirstName = userSignUpDTO.FirstName, + LastName = userSignUpDTO.LastName, + PhoneNumber = userSignUpDTO.PhoneNumber, + Id = "TestId" + }; + _userManagerMock.Setup(u => u.FindByEmailAsync(userSignUpDTO.Email)).ReturnsAsync(user); + _userManagerMock.Setup(u => u.CheckPasswordAsync(user, password)).ReturnsAsync(false); + + //Act & Assert + await _authServiceMock.Invoking(x => x.SigninAsync(userSignInDTO)) + .Should().ThrowAsync() + .WithMessage("Email does not exist or incorrect password"); + } + } +} \ No newline at end of file diff --git a/AutoPile.UnitTests/AutoPile.UnitTests.csproj b/AutoPile.UnitTests/AutoPile.UnitTests.csproj new file mode 100644 index 0000000..e7c5b83 --- /dev/null +++ b/AutoPile.UnitTests/AutoPile.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/AutoPile.UnitTests/PaymentServiceTests.cs b/AutoPile.UnitTests/PaymentServiceTests.cs new file mode 100644 index 0000000..7efa1e9 --- /dev/null +++ b/AutoPile.UnitTests/PaymentServiceTests.cs @@ -0,0 +1,55 @@ +using AutoPile.DATA.Data; +using AutoPile.DOMAIN.DTOs.Requests; +using AutoPile.DOMAIN.Models.Entities; +using AutoPile.SERVICE.Services; +using Microsoft.EntityFrameworkCore; +using MongoDB.Bson; +using Moq; +using Stripe; +using FluentAssertions; +using Xunit; + +namespace AutoPile.UnitTests +{ + public class PaymentServiceTests + { + private readonly Mock _mongoDbContextMock; + private readonly Mock _stripeServiceMock; + private readonly PaymentService _paymentService; + + public PaymentServiceTests() + { + _mongoDbContextMock = new Mock(new DbContextOptionsBuilder().Options); + _stripeServiceMock = new Mock(); + _paymentService = new PaymentService(_mongoDbContextMock.Object, _stripeServiceMock.Object); + } + + [Fact] + public async Task CreatePaymentIntent_ValidItemListInput_ReturnSecretObject() + { + // Arrange + var productId = ObjectId.GenerateNewId(); + var product = new DOMAIN.Models.Entities.Product { Id = productId, Price = 100 }; + var item = new Item { ProductId = productId.ToString(), Quantity = 2 }; + var itemList = new PaymentIntentCreate { Items = [item] }; + + _mongoDbContextMock.Setup(x => x.Products.FindAsync(productId)) + .ReturnsAsync(product); + + var mockPaymentIntent = new PaymentIntent + { + ClientSecret = "mock_client_secret_test" + }; + + _stripeServiceMock.Setup(x => x.CreatePaymentIntentAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockPaymentIntent); + + // Act + var result = await _paymentService.PaymentIntentCreateAsync(itemList); + + // Assert + Assert.NotNull(result); + result.ClientSecret.Should().Be("mock_client_secret_test"); + } + } +} \ No newline at end of file diff --git a/AutoPile.UnitTests/ReviewServiceTests.cs b/AutoPile.UnitTests/ReviewServiceTests.cs new file mode 100644 index 0000000..dee53ac --- /dev/null +++ b/AutoPile.UnitTests/ReviewServiceTests.cs @@ -0,0 +1,201 @@ +using AutoMapper; +using AutoPile.DATA.Cache; +using AutoPile.DATA.Data; +using AutoPile.DOMAIN.DTOs.Requests; +using AutoPile.DOMAIN.DTOs.Responses; +using AutoPile.DOMAIN.Interface; +using AutoPile.DOMAIN.Models.Entities; +using AutoPile.SERVICE.Services; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using MongoDB.Bson; +using MongoDB.Driver; +using Moq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoPile.UnitTests +{ + public class ReviewServiceTests + { + private readonly Mock _mapperMock; + private readonly Mock _blobServiceMock; + private readonly Mock _mongoDbContextMock; + private readonly Mock _managementDbContextMock; + private readonly Mock _reviewsCacheMock; + private readonly ReviewService _reviewService; + private readonly Mock> _reviewsDbSetMock; + private readonly Mock> _productsDbSetMock; + private readonly Mock> _usersDbSetMock; + + public ReviewServiceTests() + { + _mapperMock = new Mock(); + _blobServiceMock = new Mock(); + _mongoDbContextMock = new Mock(new DbContextOptionsBuilder().Options); + _managementDbContextMock = new Mock(new DbContextOptions()); + _reviewsCacheMock = new Mock(); + _reviewsDbSetMock = new Mock>(); + _productsDbSetMock = new Mock>(); + _usersDbSetMock = new Mock>(); + + _mongoDbContextMock.Setup(x => x.Reviews).Returns(_reviewsDbSetMock.Object); + _mongoDbContextMock.Setup(x => x.Products).Returns(_productsDbSetMock.Object); + _managementDbContextMock.Setup(x => x.Users).Returns(_usersDbSetMock.Object); + + _reviewService = new ReviewService( + _mapperMock.Object, + _blobServiceMock.Object, + _mongoDbContextMock.Object, + _managementDbContextMock.Object, + _reviewsCacheMock.Object + ); + var userId = "testUserId"; + var user = new ApplicationUser { Id = userId }; + _managementDbContextMock.Setup(x => x.Users.FindAsync(userId)) + .ReturnsAsync(user); + } + + [Fact] + public async Task CreateReviewAsync_ValidInput_ReturnsReviewResponseDTO() + { + // Arrange + + var productId = ObjectId.GenerateNewId().ToString(); + + var reviewCreateDto = new ReviewCreateDTO + { + ProductId = productId, + Title = "Test Review", + Content = "Test Content", + Subtitle = "Test Subtitle", + Rating = 5 + }; + + var product = new Product { Id = ObjectId.Parse(productId) }; + + var review = new Review + { + Id = ObjectId.GenerateNewId(), + Title = reviewCreateDto.Title, + Content = reviewCreateDto.Content, + Subtitle = reviewCreateDto.Subtitle, + Rating = reviewCreateDto.Rating, + UserId = "testUserId", + ProductId = ObjectId.Parse(productId) + }; + + var reviewResponseDto = new ReviewResponseDTO + { + Id = review.Id.ToString(), + Title = review.Title, + Content = review.Content, + Subtitle = review.Subtitle, + Rating = review.Rating, + UserId = review.UserId, + ProductId = review.ProductId.ToString(), + CreatedAt = review.CreatedAt, + UpdatedAt = review.CreatedAt + }; + + _mongoDbContextMock.Setup(x => x.Products.FindAsync(ObjectId.Parse(productId))) + .ReturnsAsync(product); + + _mapperMock.Setup(x => x.Map(reviewCreateDto)) + .Returns(review); + + _mapperMock.Setup(x => x.Map(review)) + .Returns(reviewResponseDto); + + // Act + var result = await _reviewService.CreateReviewAsync(reviewCreateDto, "testUserId"); + + // Assert + Assert.NotNull(result); + result.Should().BeEquivalentTo(reviewResponseDto); + + _mongoDbContextMock.Verify(x => x.Reviews.AddAsync(review, default), Times.Once); + _mongoDbContextMock.Verify(x => x.SaveChangesAsync(default), Times.Once); + } + + [Fact] + public async Task GetReview_ValidInput_ReturnResponseDTO() + { + //Arrange + var productId = ObjectId.GenerateNewId(); + var review = new Review + { + Id = ObjectId.GenerateNewId(), + ProductId = productId, + Title = "Test Review", + Content = "Test Content", + Subtitle = "Test Subtitle", + UserId = "testUserId", + Rating = 5 + }; + + var reviewResponseDto = new ReviewResponseDTO + { + Id = review.Id.ToString(), + Title = review.Title, + Content = review.Content, + Subtitle = review.Subtitle, + Rating = review.Rating, + UserId = review.UserId, + ProductId = review.ProductId.ToString(), + CreatedAt = review.CreatedAt, + UpdatedAt = review.CreatedAt + }; + _mongoDbContextMock.Setup(x => x.Reviews.FindAsync(review.Id)).ReturnsAsync(review); + _mapperMock.Setup(x => x.Map(review)) + .Returns(reviewResponseDto); + + //Act + var result = await _reviewService.GetReviewByIdAsync(review.Id.ToString()); + + //Assert + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(reviewResponseDto); + } + + [Fact] + public async Task DeleteReview_ValidInput_NoReturn() + { + //Arrange + var productId = ObjectId.GenerateNewId(); + var review = new Review + { + Id = ObjectId.GenerateNewId(), + ProductId = productId, + Title = "Test Review", + Content = "Test Content", + Subtitle = "Test Subtitle", + UserId = "testUserId", + Rating = 5, + ImageUrl = "test-image.jpg" + }; + + var reviewResponseDto = new ReviewResponseDTO + { + Id = review.Id.ToString(), + Title = review.Title, + Content = review.Content, + Subtitle = review.Subtitle, + Rating = review.Rating, + UserId = review.UserId, + ProductId = review.ProductId.ToString(), + CreatedAt = review.CreatedAt, + UpdatedAt = review.CreatedAt + }; + _mongoDbContextMock.Setup(x => x.Reviews.FindAsync(review.Id)).ReturnsAsync(review); + _mapperMock.Setup(x => x.Map(review)) + .Returns(reviewResponseDto); + //Act + await _reviewService.DeleteReviewAsync(review.Id.ToString(), "testUserId"); + + //Assert + _mongoDbContextMock.Verify(x => x.Remove(review), Times.Once); + _blobServiceMock.Verify(x => x.DeleteImageAsync(review.ImageUrl), Times.Once); + } + } +} \ No newline at end of file diff --git a/AutoPile.sln b/AutoPile.sln index a963626..c5c5660 100644 --- a/AutoPile.sln +++ b/AutoPile.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoPile.SERVICE", "AutoPil EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoPile.API", "AutoPile.API\AutoPile.API.csproj", "{3DDF33AA-990E-4BA9-AC47-29E2EB706D0C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoPile.UnitTests", "AutoPile.UnitTests\AutoPile.UnitTests.csproj", "{D8051ACC-12FD-4ED0-BE59-5D937D0A1A84}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {3DDF33AA-990E-4BA9-AC47-29E2EB706D0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DDF33AA-990E-4BA9-AC47-29E2EB706D0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DDF33AA-990E-4BA9-AC47-29E2EB706D0C}.Release|Any CPU.Build.0 = Release|Any CPU + {D8051ACC-12FD-4ED0-BE59-5D937D0A1A84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8051ACC-12FD-4ED0-BE59-5D937D0A1A84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8051ACC-12FD-4ED0-BE59-5D937D0A1A84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8051ACC-12FD-4ED0-BE59-5D937D0A1A84}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 78224c0b91bb18e1aa77c0f5ea02dee2135431aa Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Wed, 19 Feb 2025 05:09:44 +1100 Subject: [PATCH 02/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/develop_autopile.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 5aa434c..661de0c 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -12,6 +12,8 @@ on: jobs: build: runs-on: windows-latest + permissions: + contents: read #This is required for actions/checkout steps: - uses: actions/checkout@v4 @@ -36,11 +38,12 @@ jobs: deploy: runs-on: windows-latest needs: build - environment: - name: 'Production' + environment: + name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} permissions: id-token: write #This is required for requesting the JWT + contents: read #This is required for actions/checkout steps: - name: Download artifact from build job @@ -51,9 +54,9 @@ jobs: - name: Login to Azure uses: azure/login@v2 with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_8D7D3ADE3F6945FDB842D69367CCC0A2 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_D560FBBA60F5467B96310A66546941E1 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7B5512553F074DD889236ED335B0C6F2 }} + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_3C4C651FF6524979BB965B0354E38839 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_E7E02745BF6745CAB9A2556BC9589083 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7358A5EDABA14CFC8A46942662980678 }} - name: Deploy to Azure Web App id: deploy-to-webapp From ddb33d86241685ae6418c114aedd2784e658c29e Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Wed, 19 Feb 2025 05:59:25 +1100 Subject: [PATCH 03/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/develop_autopile.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 661de0c..56c8dc9 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -54,9 +54,9 @@ jobs: - name: Login to Azure uses: azure/login@v2 with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_3C4C651FF6524979BB965B0354E38839 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_E7E02745BF6745CAB9A2556BC9589083 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_7358A5EDABA14CFC8A46942662980678 }} + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_52885B5FECF1412698E318F6686ACB6C }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_C2643ABB89C846899D10C83F30427691 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_EFD2D2E585E841AEB33E44FABC88F67F }} - name: Deploy to Azure Web App id: deploy-to-webapp From d946ae99dc4aa1371af38a6a45aa076b180fa007 Mon Sep 17 00:00:00 2001 From: Max Lin Date: Fri, 21 Feb 2025 05:29:15 +1100 Subject: [PATCH 04/11] fix deploy issue --- AutoPile.UnitTests/AutoPile.UnitTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/AutoPile.UnitTests/AutoPile.UnitTests.csproj b/AutoPile.UnitTests/AutoPile.UnitTests.csproj index e7c5b83..d0b088d 100644 --- a/AutoPile.UnitTests/AutoPile.UnitTests.csproj +++ b/AutoPile.UnitTests/AutoPile.UnitTests.csproj @@ -10,7 +10,6 @@ - From b548e26ef42ff37786a4fe5bfa952778afcf1ea0 Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 05:38:56 +1100 Subject: [PATCH 05/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/develop_autopile.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 56c8dc9..2f97af8 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -54,9 +54,9 @@ jobs: - name: Login to Azure uses: azure/login@v2 with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_52885B5FECF1412698E318F6686ACB6C }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_C2643ABB89C846899D10C83F30427691 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_EFD2D2E585E841AEB33E44FABC88F67F }} + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_9C4F3161F29F4F07BFA664545B8ACF09 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_5757BEE8DD1C49A7B748ED2C508E1323 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_EB96897FE9CC497FA4A65155035E9525 }} - name: Deploy to Azure Web App id: deploy-to-webapp From 84f7aac22fda0b94314a0a85f10cf602672ce1f9 Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:20:02 +1100 Subject: [PATCH 06/11] Update develop_autopile.yml --- .github/workflows/develop_autopile.yml | 53 ++++---------------------- 1 file changed, 8 insertions(+), 45 deletions(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 2f97af8..5da190f 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -1,6 +1,3 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - name: Build and deploy ASP.Net Core app to Azure Web App - autopile on: @@ -11,7 +8,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest permissions: contents: read #This is required for actions/checkout @@ -23,46 +20,12 @@ jobs: with: dotnet-version: '9.x' - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: dotnet publish - run: dotnet publish -c Release -o "${{env.DOTNET_ROOT}}/myapp" - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v4 - with: - name: .net-app - path: ${{env.DOTNET_ROOT}}/myapp - - deploy: - runs-on: windows-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - permissions: - id-token: write #This is required for requesting the JWT - contents: read #This is required for actions/checkout + - name: Restore + run: dotnet restore ./Autopile.sln - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: .net-app - - - name: Login to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_9C4F3161F29F4F07BFA664545B8ACF09 }} - tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_5757BEE8DD1C49A7B748ED2C508E1323 }} - subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_EB96897FE9CC497FA4A65155035E9525 }} + - name: Build + run: dotnet build ./Autopile.sln --configuration Release --no-restore - - name: Deploy to Azure Web App - id: deploy-to-webapp - uses: azure/webapps-deploy@v3 - with: - app-name: 'autopile' - slot-name: 'Production' - package: . - \ No newline at end of file + + + From 9cfe3e8c179c012ad7bc330f8764cb644195f68c Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:21:15 +1100 Subject: [PATCH 07/11] Update develop_autopile.yml --- .github/workflows/develop_autopile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 5da190f..7e30f93 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -21,7 +21,7 @@ jobs: dotnet-version: '9.x' - name: Restore - run: dotnet restore ./Autopile.sln + run: dotnet restore ./AutoPile.sln - name: Build run: dotnet build ./Autopile.sln --configuration Release --no-restore From 52c8bd7b6a19edeafa64859632fe9dd902c1ef91 Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:23:13 +1100 Subject: [PATCH 08/11] Update CI pipeline --- .github/workflows/develop_autopile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index 7e30f93..fe6ddd3 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -24,7 +24,7 @@ jobs: run: dotnet restore ./AutoPile.sln - name: Build - run: dotnet build ./Autopile.sln --configuration Release --no-restore + run: dotnet build ./AutoPile.sln --configuration Release --no-restore From ef416e88809cec867f3d0bd961d95c93c74474bd Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:25:30 +1100 Subject: [PATCH 09/11] Update develop_autopile.yml with Unit Test --- .github/workflows/develop_autopile.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index fe6ddd3..b9f0ffb 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -26,6 +26,9 @@ jobs: - name: Build run: dotnet build ./AutoPile.sln --configuration Release --no-restore + - name: Test + run: dotnet test --no-build --configuration Release + From 751ae8a22892b3279a7320158257970f42b89de1 Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:51:12 +1100 Subject: [PATCH 10/11] Update develop_autopile.yml --- .github/workflows/develop_autopile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml index b9f0ffb..ece7576 100644 --- a/.github/workflows/develop_autopile.yml +++ b/.github/workflows/develop_autopile.yml @@ -1,4 +1,4 @@ -name: Build and deploy ASP.Net Core app to Azure Web App - autopile +name: CI pipeline with Unit Test - autopile on: push: From 31fc6b699885a95defd692644d0fe9d9667081c4 Mon Sep 17 00:00:00 2001 From: Max <56611662+Linwentao-tech@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:02:30 +1100 Subject: [PATCH 11/11] Update and rename develop_autopile.yml to CICD.yml --- .github/workflows/CICD.yml | 66 ++++++++++++++++++++++++++ .github/workflows/develop_autopile.yml | 34 ------------- 2 files changed, 66 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/CICD.yml delete mode 100644 .github/workflows/develop_autopile.yml diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml new file mode 100644 index 0000000..c07a434 --- /dev/null +++ b/.github/workflows/CICD.yml @@ -0,0 +1,66 @@ +name: AutoPile CI/CD Pipeline +on: + push: + branches: + - develop + - master + workflow_dispatch: + +env: + AZURE_WEBAPP_NAME: autopile + AZURE_WEBAPP_PACKAGE_PATH: "./publish" + +jobs: + ci: + name: Build and Test + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.x' + + - name: Restore + run: dotnet restore ./AutoPile.sln + + - name: Build + run: dotnet build ./AutoPile.sln --configuration Release --no-restore + + - name: Test + run: dotnet test --no-build --configuration Release + + - name: Publish + if: github.ref == 'refs/heads/master' + run: dotnet publish ./AutoPile.sln --configuration Release --no-build --output ${{env.AZURE_WEBAPP_PACKAGE_PATH}} + + - name: Upload Artifact + if: github.ref == 'refs/heads/master' + uses: actions/upload-artifact@v4 + with: + name: app-build + path: ${{env.AZURE_WEBAPP_PACKAGE_PATH}} + + cd: + name: Deploy to Azure + needs: ci + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: app-build + path: ${{env.AZURE_WEBAPP_PACKAGE_PATH}} + + - name: Deploy to Azure Web App + uses: azure/webapps-deploy@v3 + with: + app-name: ${{env.AZURE_WEBAPP_NAME}} + publish-profile: ${{secrets.AZURE_PUBLISH_PROFILE}} + package: "${{env.AZURE_WEBAPP_PACKAGE_PATH}}" diff --git a/.github/workflows/develop_autopile.yml b/.github/workflows/develop_autopile.yml deleted file mode 100644 index ece7576..0000000 --- a/.github/workflows/develop_autopile.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI pipeline with Unit Test - autopile - -on: - push: - branches: - - develop - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read #This is required for actions/checkout - - steps: - - uses: actions/checkout@v4 - - - name: Set up .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.x' - - - name: Restore - run: dotnet restore ./AutoPile.sln - - - name: Build - run: dotnet build ./AutoPile.sln --configuration Release --no-restore - - - name: Test - run: dotnet test --no-build --configuration Release - - - -