diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 831761455..a6eb53c6b 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -10,12 +10,10 @@ on: - development - production - # Actions jobs: deploy: - - runs-on: ubuntu-latest + runs-on: ubuntu-latest steps: - name: Set build profile @@ -36,15 +34,15 @@ jobs: uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Docker-compose build shell: bash run: docker-compose -f .deploy/docker-compose.build.yml --profile ${PROFILE} build - name: Docker-compose push - shell: bash - run: docker-compose -f .deploy/docker-compose.build.yml --profile ${PROFILE} push + shell: bash + run: docker-compose -f .deploy/docker-compose.build.yml --profile ${PROFILE} push - name: Execute docker-compose up through SSH uses: appleboy/ssh-action@master diff --git a/apps/admin-page-e2e/src/integration/pages/orders.spec.ts b/apps/admin-page-e2e/src/integration/pages/orders.spec.ts index 602ca7371..41c3bd134 100644 --- a/apps/admin-page-e2e/src/integration/pages/orders.spec.ts +++ b/apps/admin-page-e2e/src/integration/pages/orders.spec.ts @@ -176,10 +176,12 @@ const mockOrders = [ describe('ordersPage', () => { beforeEach(() => { localStorage.setItem(LocalStorageVars.authUser, JSON.stringify(authMockService.getMockUser(AuthUserEnum.authUser))); + cy.intercept('GET', 'localhost:5000/orders', { body: mockOrders, statusCode: 200, - }); + }).as('fetchOrders'); + cy.visit('/orders'); }); @@ -247,4 +249,36 @@ describe('ordersPage', () => { }); }); }); + + it('should display a list of options', () => { + cy.get('.mat-select-panel').should('not.exist'); + cy.get('[data-cy=order-status]').first().click(); + cy.get('.mat-select-panel').should('be.visible'); + cy.get('[data-cy=order-status-option]').should('have.length', 8); + }); + + it('should correctly change the status', () => { + cy.intercept('PATCH', 'localhost:5000/orders/status/MakeMeWantIt', { + body: mockOrder(OrderStatusEnum.delivered, 14), + statusCode: 200, + }).as('updateOrderStatus'); + + cy.get('[data-cy=order-status]').first().contains('INITIAL'); + cy.get('[data-cy=order-status]').first().click(); + cy.get('[data-cy=order-status-option]').first().click(); + cy.get('[data-cy=order-status]').first().contains('DELIVERED'); + cy.wait('@updateOrderStatus').its('request.body').should('include', { status: 'DELIVERED' }); + }); + + it('should reload orders on status change failure', () => { + cy.intercept('PATCH', 'localhost:5000/orders/status/MakeMeWantIt', { + statusCode: 500, + }).as('updateOrderStatus'); + + cy.get('[data-cy=order-status]').first().click(); + cy.get('[data-cy=order-status-option]').first().click(); + + cy.wait('@fetchOrders'); + cy.get('[data-cy=order-status]').first().contains('INITIAL'); + }); }); diff --git a/apps/admin-page/src/app/material.constant.ts b/apps/admin-page/src/app/material.constant.ts index 9a4f88a1f..be2be9603 100644 --- a/apps/admin-page/src/app/material.constant.ts +++ b/apps/admin-page/src/app/material.constant.ts @@ -11,6 +11,7 @@ import { MatListModule } from '@angular/material/list'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatCardModule } from '@angular/material/card'; import { MatTableModule } from '@angular/material/table'; +import { MatSelectModule } from '@angular/material/select'; export const materialModules = [ MatCommonModule, @@ -27,4 +28,5 @@ export const materialModules = [ MatProgressSpinnerModule, MatFormFieldModule, MatSnackBarModule, + MatSelectModule, ]; diff --git a/apps/admin-page/src/app/pages/orders/orders.component.css b/apps/admin-page/src/app/pages/orders/orders.component.css index 592c0caba..9ec4b8e58 100644 --- a/apps/admin-page/src/app/pages/orders/orders.component.css +++ b/apps/admin-page/src/app/pages/orders/orders.component.css @@ -41,3 +41,34 @@ button { padding: 0 6px; border-radius: 2px; } + +.order-status-select { + width: 10vw; + height: 2.3vh; + min-width: 8.1vw; + min-height: 15px; + padding: 0 6px; + border-radius: 3px; +} + +::ng-deep table .mat-select-value { + color: white; +} + +::ng-deep .mat-option-text { + color: white; +} + +mat-option { + width: 10vw; + min-width: 8.1vw; + padding: 0 6px; +} + +::ng-deep .mat-select-panel { + min-width: 10vw !important; +} + +::ng-deep table .mat-select-arrow { + color: white !important; +} diff --git a/apps/admin-page/src/app/pages/orders/orders.component.html b/apps/admin-page/src/app/pages/orders/orders.component.html index 02eb0874d..51dbaa47f 100644 --- a/apps/admin-page/src/app/pages/orders/orders.component.html +++ b/apps/admin-page/src/app/pages/orders/orders.component.html @@ -49,13 +49,21 @@ Status -
- {{ order.status }} -
+ {{ status }} +
diff --git a/apps/admin-page/src/app/pages/orders/orders.component.ts b/apps/admin-page/src/app/pages/orders/orders.component.ts index c5a3a6154..ceda96ae4 100644 --- a/apps/admin-page/src/app/pages/orders/orders.component.ts +++ b/apps/admin-page/src/app/pages/orders/orders.component.ts @@ -19,6 +19,16 @@ export class OrdersComponent implements OnInit { 'status', 'actions', ]; + orderStatusOptions: OrderStatusEnum[] = [ + OrderStatusEnum.delivered, + OrderStatusEnum.assembling, + OrderStatusEnum.shipped, + OrderStatusEnum.pending, + OrderStatusEnum.initial, + OrderStatusEnum.processed, + OrderStatusEnum.new, + OrderStatusEnum.rejected, + ]; orders!: IOrder[]; constructor(private ordersService: OrdersService) {} @@ -27,6 +37,22 @@ export class OrdersComponent implements OnInit { this.fetchOrders(); } + /** + * Gets triggered when the status of an order has been changed.\ + * It will call the API to update the order with its new status.\ + * In case an error is encountered, the orders will be reloaded from the database. + * + * @param order - the order containing the new status. + */ + onStatusChange(order: IOrder): void { + this.ordersService.updateOrder(order).subscribe({ + error: (error: HttpErrorResponse) => { + this.fetchOrders(); + console.error(error); + }, + }); + } + /** * Fetches the orders from the API.\ * \ diff --git a/apps/admin-page/src/app/services/orders/orders.service.ts b/apps/admin-page/src/app/services/orders/orders.service.ts index 2b64c5b79..0a350d47d 100644 --- a/apps/admin-page/src/app/services/orders/orders.service.ts +++ b/apps/admin-page/src/app/services/orders/orders.service.ts @@ -10,7 +10,22 @@ import { environment as env } from '../../../environments/environment'; export class OrdersService { constructor(private http: HttpClient) {} + /** + * Calls the API to fetch all available orders. + * + * @returns an observable with the list of orders. + */ public getOrders(): Observable { return this.http.get(`${env.apiUrl}/orders`); } + + /** + * Calls the API to update the status of an order. + * + * @param order - the order that needs to be updated. + * @returns an observable with the updated order. + */ + public updateOrder(order: IOrder): Observable { + return this.http.patch(`${env.apiUrl}/orders/status/${order.orderId}`, { status: order.status }); + } } diff --git a/apps/api/src/main/java/dk/treecreate/api/order/OrderController.java b/apps/api/src/main/java/dk/treecreate/api/order/OrderController.java index 9978e5b9e..43015ac3a 100644 --- a/apps/api/src/main/java/dk/treecreate/api/order/OrderController.java +++ b/apps/api/src/main/java/dk/treecreate/api/order/OrderController.java @@ -8,6 +8,7 @@ import dk.treecreate.api.order.dto.CreateOrderRequest; import dk.treecreate.api.order.dto.GetAllOrdersResponse; import dk.treecreate.api.order.dto.GetOrdersResponse; +import dk.treecreate.api.order.dto.UpdateOrderStatusRequest; import dk.treecreate.api.transactionitem.TransactionItemRepository; import dk.treecreate.api.user.User; import dk.treecreate.api.user.UserRepository; @@ -16,6 +17,7 @@ import dk.treecreate.api.utils.model.quickpay.dto.CreatePaymentLinkResponse; import io.sentry.Sentry; import io.swagger.annotations.Api; +import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.v3.oas.annotations.Operation; @@ -33,6 +35,7 @@ import java.net.URISyntaxException; import java.util.List; import java.util.Locale; +import java.util.UUID; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -159,4 +162,36 @@ public CreatePaymentLinkResponse createPayment( Sentry.captureMessage("New order has been created"); return createPaymentLinkResponse; } + + /** + * Attempts to update the order with the ID received as a path parameter + * with the new status received in the body. + * Will return a response with the full order if it is successful or 404 - Not Found + * if there is no order with specified id. + * + * @param updateOrderStatusRequest DTO for the request. + * @param orderId the ID of the order. + * @return the updated order. + */ + @PatchMapping("/status/{orderId}") + @Operation(summary = "Update an order's status") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Updated the orders's status", + response = Order.class) + }) + @PreAuthorize("hasRole('DEVELOPER') or hasRole('ADMIN')") + public Order updateOrderStatus( + @RequestBody() @Valid UpdateOrderStatusRequest updateOrderStatusRequest, + @ApiParam(name = "orderId", example = "c0a80121-7ac0-190b-817a-c08ab0a12345") + @PathVariable UUID orderId) + { + try + { + return orderService.updateOrderStatus(orderId, updateOrderStatusRequest.getStatus()); + } catch (ResourceNotFoundException e) + { + throw new ResourceNotFoundException("Order not found"); + } + } + } diff --git a/apps/api/src/main/java/dk/treecreate/api/order/OrderService.java b/apps/api/src/main/java/dk/treecreate/api/order/OrderService.java index d169acfe6..6306bb1e5 100644 --- a/apps/api/src/main/java/dk/treecreate/api/order/OrderService.java +++ b/apps/api/src/main/java/dk/treecreate/api/order/OrderService.java @@ -110,6 +110,23 @@ public boolean verifyPrice(Order order) return true; } + /** + * Updates the order with the provided ID to contain the new status. + * + * @param orderId the ID of the order. + * @param status the new status of the order. + * @return the updated order. + */ + public Order updateOrderStatus(UUID orderId, OrderStatus status) + throws ResourceNotFoundException + { + Order order = orderRepository.findByOrderId(orderId) + .orElseThrow(() -> new ResourceNotFoundException("Order not found")); + + order.setStatus(status); + return orderRepository.save(order); + } + public BigDecimal calculateTotal(BigDecimal subTotal, Discount discount, boolean hasMoreThan3) { BigDecimal total = subTotal; @@ -252,8 +269,7 @@ public Order setupOrderFromCreateRequest(CreateOrderRequest createOrderRequest) User user = userRepository.findByEmail(userDetails.getUsername()) .orElseThrow(() -> new ResourceNotFoundException("User not found")); order.setUserId(user.getUserId()); - for ( - UUID itemId : createOrderRequest.getTransactionItemIds()) + for (UUID itemId : createOrderRequest.getTransactionItemIds()) { TransactionItem transactionItem = transactionItemRepository.findByTransactionItemId(itemId) diff --git a/apps/api/src/main/java/dk/treecreate/api/order/dto/UpdateOrderStatusRequest.java b/apps/api/src/main/java/dk/treecreate/api/order/dto/UpdateOrderStatusRequest.java new file mode 100644 index 000000000..c944456f4 --- /dev/null +++ b/apps/api/src/main/java/dk/treecreate/api/order/dto/UpdateOrderStatusRequest.java @@ -0,0 +1,24 @@ +package dk.treecreate.api.order.dto; + +import dk.treecreate.api.utils.OrderStatus; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotNull; + +public class UpdateOrderStatusRequest +{ + @NotNull + @ApiModelProperty(notes = "Status of the order", example = "PENDING", + required = true) + private OrderStatus status; + + public OrderStatus getStatus() + { + return status; + } + + public void setStatus(OrderStatus status) + { + this.status = status; + } +} diff --git a/apps/api/src/main/java/dk/treecreate/api/utils/model/quickpay/dto/CreatePaymentLinkRequest.java b/apps/api/src/main/java/dk/treecreate/api/utils/model/quickpay/dto/CreatePaymentLinkRequest.java index 4ecb43323..6bf75932f 100644 --- a/apps/api/src/main/java/dk/treecreate/api/utils/model/quickpay/dto/CreatePaymentLinkRequest.java +++ b/apps/api/src/main/java/dk/treecreate/api/utils/model/quickpay/dto/CreatePaymentLinkRequest.java @@ -6,7 +6,7 @@ public class CreatePaymentLinkRequest { public int amount; - // no floating points, multiply the values by 100 to get two-points of precision! + // no floating points, multiply the values by 100 to get two-points of precision! public String language; public String continue_url; public String cancel_url; diff --git a/apps/api/src/main/resources/templates/emails/order-confirmation.html b/apps/api/src/main/resources/templates/emails/order-confirmation.html index f21a5bc6c..fc6c0bca3 100644 --- a/apps/api/src/main/resources/templates/emails/order-confirmation.html +++ b/apps/api/src/main/resources/templates/emails/order-confirmation.html @@ -1,9 +1,5 @@ - + - - + + - + @@ -42,7 +38,7 @@ text-decoration: none !important; } - - - +
- +
- - + +
- +
+
- +
- +
- +
info@treecreate.dk @@ -602,7 +647,11 @@
- +
+
- + @@ -140,12 +136,14 @@ width: auto !important; } } + .mailto { text-decoration: none; width: 200px; font-size: 0.8rem; color: #555555; } + .mail { width: 100%; text-align: center; @@ -165,7 +163,8 @@ - +
- +
- - + +
- +
+
- +
- +
- +
info@treecreate.dk @@ -615,7 +660,11 @@
- +
+
- + @@ -140,12 +136,14 @@ width: auto !important; } } + .mailto { text-decoration: none; width: 200px; font-size: 0.8rem; color: #555555; } + .mail { width: 100%; text-align: center; @@ -165,7 +163,8 @@ - +
- +
- - + +
- +
+
- +
- +
- +
info@treecreate.dk @@ -617,7 +662,11 @@
- +
+
assertTrue(result.getResolvedException() instanceof ResponseStatusException)) + result -> assertTrue(result.getResolvedException() instanceof ResponseStatusException)) // The exception message in ResponseStatusException includes the status code and error type so it has to be added to expected message .andExpect(result -> assertEquals("418 I_AM_A_TEAPOT \"" + customMessage + "\"", result.getResolvedException().getMessage())); @@ -82,10 +82,10 @@ void handleResourceNotFoundCustom() throws Exception String exceptionName = "ResourceNotFoundException"; String customMessage = "Failed to find a resource"; mvc.perform(post("/exception").contentType(MediaType.APPLICATION_JSON) - .param("exceptionName", exceptionName).param("customMessage", customMessage)) + .param("exceptionName", exceptionName).param("customMessage", customMessage)) .andExpect(status().isNotFound()).andExpect(result -> assertTrue( - result.getResolvedException() instanceof ResourceNotFoundException)).andExpect( - result -> assertEquals(customMessage, result.getResolvedException().getMessage())); + result.getResolvedException() instanceof ResourceNotFoundException)).andExpect( + result -> assertEquals(customMessage, result.getResolvedException().getMessage())); } } diff --git a/apps/api/src/test/java/dk/treecreate/api/healthcheck/HealthcheckTests.java b/apps/api/src/test/java/dk/treecreate/api/healthcheck/HealthcheckTests.java index 2a5841531..68d7dc881 100644 --- a/apps/api/src/test/java/dk/treecreate/api/healthcheck/HealthcheckTests.java +++ b/apps/api/src/test/java/dk/treecreate/api/healthcheck/HealthcheckTests.java @@ -25,21 +25,17 @@ class HealthcheckTests { + @MockBean + CustomPropertiesConfig customProperties; @Autowired private MockMvc mvc; - @MockBean private UserDetailsServiceImpl userDetailsService; - @MockBean private AuthEntryPointJwt authEntryPointJwt; - @MockBean private JwtUtils jwtUtils; - @MockBean - CustomPropertiesConfig customProperties; - @Test // MockMvc throws Exception, so i must catch it void healthcheckReturnedStatusTest() throws Exception { @@ -53,8 +49,8 @@ void healthcheckBodyTest() throws Exception Mockito.when(customProperties.getEnvironment()).thenReturn(Environment.STAGING); mvc.perform(get("/healthcheck") - // Ensure it is a Json - .contentType(MediaType.APPLICATION_JSON)) + // Ensure it is a Json + .contentType(MediaType.APPLICATION_JSON)) // Check the contents of the body. .andExpect(jsonPath("status", is("OK"))) .andExpect(jsonPath("message", is("Server is live"))) diff --git a/apps/api/src/test/java/dk/treecreate/api/mail/MailControllerTest.java b/apps/api/src/test/java/dk/treecreate/api/mail/MailControllerTest.java index 55a265c5a..6d3444875 100644 --- a/apps/api/src/test/java/dk/treecreate/api/mail/MailControllerTest.java +++ b/apps/api/src/test/java/dk/treecreate/api/mail/MailControllerTest.java @@ -127,7 +127,7 @@ void signupMessageBadRequest() throws Exception Mockito.when(mailService.isValidEmail(email)).thenReturn(false); mvc.perform(post("/mail/signup").content(TestUtilsService.asJsonString(params)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON)) // the message comes from a ResponseStatusException so has to be handled differently from normal content .andExpect(result -> assertEquals("Provided email is not a valid email", result.getResponse().getErrorMessage())); @@ -147,7 +147,7 @@ void signupMessageException() throws Exception .sendSignupEmail(anyString(), any(UUID.class), any(Locale.class)); mvc.perform(post("/mail/signup").content(TestUtilsService.asJsonString(params)) - .contentType(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON)) // the message comes from a ResponseStatusException so has to be handled differently from normal content .andExpect(result -> assertEquals("Failed to send an email", result.getResponse().getErrorMessage())); diff --git a/apps/api/src/test/java/dk/treecreate/api/order/OrderServiceTest.java b/apps/api/src/test/java/dk/treecreate/api/order/OrderServiceTest.java index 2df2b08d5..f42519332 100644 --- a/apps/api/src/test/java/dk/treecreate/api/order/OrderServiceTest.java +++ b/apps/api/src/test/java/dk/treecreate/api/order/OrderServiceTest.java @@ -5,25 +5,39 @@ import dk.treecreate.api.designs.DesignType; import dk.treecreate.api.discount.Discount; import dk.treecreate.api.discount.DiscountType; +import dk.treecreate.api.exceptionhandling.ResourceNotFoundException; import dk.treecreate.api.transactionitem.TransactionItem; +import dk.treecreate.api.utils.OrderStatus; import dk.treecreate.api.utils.model.quickpay.ShippingMethod; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.web.server.ResponseStatusException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +@SpringBootTest class OrderServiceTest { - OrderService orderService = new OrderService(); + @Autowired + OrderService orderService; + + @MockBean + OrderRepository orderRepository; private static Stream verifyPriceArguments() { @@ -85,7 +99,8 @@ private static Stream verifyPriceArguments() DiscountType.PERCENT, 69, DesignDimension.LARGE, 69, DesignType.FAMILY_TREE, ShippingMethod.PICK_UP_POINT, null), - // Discount - percent. Shipping - own delivery. Off number resulting in a complex floating point + // Discount - percent. Shipping - own delivery. Off number resulting in a + // complex floating point Arguments.of(69, new BigDecimal(4737195), new BigDecimal("1469310.45"), 69, DiscountType.PERCENT, 69, DesignDimension.LARGE, 69, DesignType.FAMILY_TREE, ShippingMethod.OWN_DELIVERY, @@ -154,6 +169,29 @@ private static Stream verifyPriceArguments() "dimension data is not valid")); } + private static Stream calculateTotalArguments() + { + return Stream.of( + Arguments.of(new BigDecimal(495), 100, DiscountType.AMOUNT, false, + new BigDecimal("395.00"), null), + Arguments.of(new BigDecimal(495), 100, DiscountType.PERCENT, false, + new BigDecimal("0.00"), null), + Arguments.of(new BigDecimal(495), 50, DiscountType.PERCENT, false, + new BigDecimal("247.50"), null), + Arguments.of(new BigDecimal(495), 0, null, false, new BigDecimal("495.00"), null), + Arguments.of(new BigDecimal(495), 0, null, true, new BigDecimal("371.25"), null)); + } + + private static Stream pricePerItemArguments() + { + return Stream.of( + Arguments.of(DesignType.FAMILY_TREE, DesignDimension.SMALL, new BigDecimal(495), null), + Arguments.of(DesignType.FAMILY_TREE, DesignDimension.MEDIUM, new BigDecimal(695), null), + Arguments.of(DesignType.FAMILY_TREE, DesignDimension.LARGE, new BigDecimal(995), null), + Arguments.of(DesignType.FAMILY_TREE, DesignDimension.ONE_SIZE, null, + "dimension data is not valid")); + } + @ParameterizedTest @MethodSource("verifyPriceArguments") @DisplayName("verifyPrice() correctly validates pricing information") @@ -206,20 +244,6 @@ void verifyPrice(int plantedTrees, BigDecimal total, BigDecimal subTotal, int di } } - private static Stream calculateTotalArguments() - { - return Stream.of( - Arguments.of(new BigDecimal(495), 100, DiscountType.AMOUNT, false, - new BigDecimal("395.00"), null), - Arguments.of(new BigDecimal(495), 100, DiscountType.PERCENT, false, - new BigDecimal("0.00"), null), - Arguments.of(new BigDecimal(495), 50, DiscountType.PERCENT, false, - new BigDecimal("247.50"), null), - Arguments.of(new BigDecimal(495), 0, null, false, new BigDecimal("495.00"), null), - Arguments.of(new BigDecimal(495), 0, null, true, new BigDecimal("371.25"), null) - ); - } - @ParameterizedTest @MethodSource("calculateTotalArguments") @DisplayName("calculateTotal() correctly applies discounts to the subtotal") @@ -254,24 +278,14 @@ void calculateTotal(BigDecimal subTotal, int discountAmount, DiscountType discou } } - private static Stream pricePerItemArguments() - { - return Stream.of( - Arguments.of(DesignType.FAMILY_TREE, DesignDimension.SMALL, new BigDecimal(495), null), - Arguments.of(DesignType.FAMILY_TREE, DesignDimension.MEDIUM, new BigDecimal(695), null), - Arguments.of(DesignType.FAMILY_TREE, DesignDimension.LARGE, new BigDecimal(995), null), - Arguments.of(DesignType.FAMILY_TREE, DesignDimension.ONE_SIZE, null, - "dimension data is not valid") - ); - } - @ParameterizedTest @MethodSource("pricePerItemArguments") @DisplayName("pricePerItem() correctly returns price based on design type and dimensions") void pricePerItem(DesignType designType, DesignDimension designDimension, BigDecimal expectedPrice, String exceptionMessageSnippet) { - // there is no point testing the default options sine they are impossible to each as of now (there are not enum values not covered by the switches) + // there is no point testing the default options sine they are impossible to + // each as of now (there are not enum values not covered by the switches) if (expectedPrice != null) { assertEquals(orderService.pricePerItem(designType, designDimension), expectedPrice); @@ -283,4 +297,42 @@ void pricePerItem(DesignType designType, DesignDimension designDimension, assertThat(exception.getMessage()).contains(exceptionMessageSnippet); } } + + @Test + @DisplayName("updateOrderStatus() correctly updates the status of the order") + void updateOrderStatus() + { + // prepare the order + UUID orderId = UUID.fromString("c0a80121-7adb-10c0-817a-dbc2f0ec1234"); + OrderStatus status = OrderStatus.ASSEMBLING; + OrderStatus newStatus = OrderStatus.NEW; + + Order order = new Order(); + Order newOrder = new Order(); + + order.setOrderId(orderId); + order.setStatus(status); + + newOrder.setOrderId(orderId); + newOrder.setStatus(status); + + + Mockito.when(orderRepository.findByOrderId(orderId)).thenReturn(Optional.of(order)); + Mockito.when(orderRepository.save(order)).thenReturn(newOrder); + + assertEquals(newOrder, orderService.updateOrderStatus(orderId, newStatus)); + } + + @Test + @DisplayName("updateOrderStatus() throws ResourceNotFoundException if no order is found") + void updateOrderStatusNotFound() + { + UUID orderId = UUID.fromString("c0a80121-7adb-10c0-817a-dbc2f0ec1234"); + OrderStatus status = OrderStatus.ASSEMBLING; + + Mockito.when(orderRepository.findByOrderId(orderId)).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, + () -> orderService.updateOrderStatus(orderId, status)); + } } diff --git a/apps/api/src/test/java/dk/treecreate/api/users/UserControllerTests.java b/apps/api/src/test/java/dk/treecreate/api/users/UserControllerTests.java index 67f5b5e29..6064f7fda 100644 --- a/apps/api/src/test/java/dk/treecreate/api/users/UserControllerTests.java +++ b/apps/api/src/test/java/dk/treecreate/api/users/UserControllerTests.java @@ -219,8 +219,8 @@ void updateCurrentUserReturnsUpdatedUser() throws Exception Mockito.when(userRepository.save(user)).thenReturn(user); mvc.perform(put("/users") - .contentType(MediaType.APPLICATION_JSON) - .content(TestUtilsService.asJsonString(updateUserRequest))) + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtilsService.asJsonString(updateUserRequest))) .andExpect(status().isOk()) .andExpect(content().json(TestUtilsService.asJsonString(user))); } @@ -246,8 +246,8 @@ void updateUserReturnsUpdatedUser() throws Exception Mockito.when(userRepository.save(user)).thenReturn(user); mvc.perform(put("/users/" + userId) - .contentType(MediaType.APPLICATION_JSON) - .content(TestUtilsService.asJsonString(updateUserRequest))) + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtilsService.asJsonString(updateUserRequest))) .andExpect(status().isOk()) .andExpect(content().json(TestUtilsService.asJsonString(user))); } @@ -265,8 +265,8 @@ void updateUserWithInvalidEmailReturnsBadRequest() throws Exception updateUserRequest.setEmail("invalid format"); mvc.perform(put("/users") - .contentType(MediaType.APPLICATION_JSON) - .content(TestUtilsService.asJsonString(updateUserRequest))) + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtilsService.asJsonString(updateUserRequest))) .andExpect(status().isBadRequest()); } @@ -291,8 +291,8 @@ void updateUserWithDuplicateEmailReturnsBadRequest() throws Exception .thenReturn(true); mvc.perform(put("/users/" + userId) - .contentType(MediaType.APPLICATION_JSON) - .content(TestUtilsService.asJsonString(updateUserRequest))) + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtilsService.asJsonString(updateUserRequest))) .andExpect(status().isBadRequest()); } @@ -309,8 +309,8 @@ void updateUserWithTooLongEntryReturnsBadRequest() updateUserRequest.setPhoneNumber("12345678901234567890"); // the limit is 15 mvc.perform(put("/users/" + userId) - .contentType(MediaType.APPLICATION_JSON) - .content(TestUtilsService.asJsonString(updateUserRequest))) + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtilsService.asJsonString(updateUserRequest))) .andExpect(status().isBadRequest()); } } diff --git a/apps/api/src/test/java/dk/treecreate/api/utils/LinkServiceTest.java b/apps/api/src/test/java/dk/treecreate/api/utils/LinkServiceTest.java index 49bdbe80d..c82fe747b 100644 --- a/apps/api/src/test/java/dk/treecreate/api/utils/LinkServiceTest.java +++ b/apps/api/src/test/java/dk/treecreate/api/utils/LinkServiceTest.java @@ -53,18 +53,6 @@ private static Stream generatePaymentRedirectUrlArguments() "https://treecreate.dk/dk/payment/cancelled")); } - @ParameterizedTest - @MethodSource("generatePaymentRedirectUrlArguments") - @DisplayName("generatePaymentRedirectUrl() returns a correctly structured redirect url") - void generatePaymentRedirectUrl(Environment environment, Locale locale, - boolean successLink, String expectedUrl) - { - Mockito.when(customProperties.getEnvironment()).thenReturn(environment); - - assertEquals(linkService.generatePaymentRedirectUrl(locale, successLink), - expectedUrl); - } - private static Stream generateCallbackUrlArguments() { return Stream.of( @@ -76,16 +64,6 @@ private static Stream generateCallbackUrlArguments() "https://api.treecreate.dk/paymentCallback")); } - @ParameterizedTest - @MethodSource("generateCallbackUrlArguments") - @DisplayName("generateCallbackUrl() returns a correctly structured callback url") - void generateCallbackUrl(Environment environment, String expectedUrl) - { - Mockito.when(customProperties.getEnvironment()).thenReturn(environment); - - assertEquals(linkService.generateCallbackUrl(), expectedUrl); - } - private static Stream generateVerificationLinkArguments() { return Stream.of( @@ -103,18 +81,6 @@ private static Stream generateVerificationLinkArguments() "https://treecreate.dk/dk/verification/00000000-0000-0000-0000-000000000000")); } - @ParameterizedTest - @MethodSource("generateVerificationLinkArguments") - @DisplayName("generateVerificationLink() returns a correctly structured verification link") - void generateVerificationLink(UUID token, Locale locale, Environment environment, - String expectedUrl) - { - Mockito.when(customProperties.getEnvironment()).thenReturn(environment); - - assertEquals(linkService.generateVerificationLink(token, locale), - expectedUrl); - } - private static Stream generateResetPasswordLinkArguments() { return Stream.of( @@ -132,18 +98,6 @@ private static Stream generateResetPasswordLinkArguments() "https://treecreate.dk/dk/resetPassword/00000000-0000-0000-0000-000000000000")); } - @ParameterizedTest - @MethodSource("generateResetPasswordLinkArguments") - @DisplayName("generateResetPasswordLink() returns a correctly structured reset password link") - void generateResetPasswordLink(UUID token, Locale locale, Environment environment, - String expectedUrl) - { - Mockito.when(customProperties.getEnvironment()).thenReturn(environment); - - assertEquals(linkService.generateResetPasswordLink(token, locale), - expectedUrl); - } - private static Stream generateNewsletterUnsubscribeLinkArguments() { return Stream.of( @@ -161,6 +115,52 @@ private static Stream generateNewsletterUnsubscribeLinkArguments() "https://treecreate.dk/dk/newsletter/unsubscribe/00000000-0000-0000-0000-000000000000")); } + @ParameterizedTest + @MethodSource("generatePaymentRedirectUrlArguments") + @DisplayName("generatePaymentRedirectUrl() returns a correctly structured redirect url") + void generatePaymentRedirectUrl(Environment environment, Locale locale, + boolean successLink, String expectedUrl) + { + Mockito.when(customProperties.getEnvironment()).thenReturn(environment); + + assertEquals(linkService.generatePaymentRedirectUrl(locale, successLink), + expectedUrl); + } + + @ParameterizedTest + @MethodSource("generateCallbackUrlArguments") + @DisplayName("generateCallbackUrl() returns a correctly structured callback url") + void generateCallbackUrl(Environment environment, String expectedUrl) + { + Mockito.when(customProperties.getEnvironment()).thenReturn(environment); + + assertEquals(linkService.generateCallbackUrl(), expectedUrl); + } + + @ParameterizedTest + @MethodSource("generateVerificationLinkArguments") + @DisplayName("generateVerificationLink() returns a correctly structured verification link") + void generateVerificationLink(UUID token, Locale locale, Environment environment, + String expectedUrl) + { + Mockito.when(customProperties.getEnvironment()).thenReturn(environment); + + assertEquals(linkService.generateVerificationLink(token, locale), + expectedUrl); + } + + @ParameterizedTest + @MethodSource("generateResetPasswordLinkArguments") + @DisplayName("generateResetPasswordLink() returns a correctly structured reset password link") + void generateResetPasswordLink(UUID token, Locale locale, Environment environment, + String expectedUrl) + { + Mockito.when(customProperties.getEnvironment()).thenReturn(environment); + + assertEquals(linkService.generateResetPasswordLink(token, locale), + expectedUrl); + } + @ParameterizedTest @MethodSource("generateNewsletterUnsubscribeLinkArguments") @DisplayName(