Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17,518 changes: 17,518 additions & 0 deletions Sandbox.AngularWorkspace/package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

.confirmation-dialog {
border: 1px solid #ccc;
padding: 15px;
margin: 10px 0;
background-color: #f9f9f9;
border-radius: 5px;
}

.confirmation-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}

.error {
color: red;
margin-top: 10px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,29 @@
@if (customer.hasValue() && customer.value(); as customer) {
<div>
<h2>Customer Details</h2>
<a routerLink="..">Back to Overview</a>
<div class="actions">
<a routerLink="..">Back to Overview</a>
<button type="button" (click)="toggleDeleteConfirmation()" [disabled]="isDeleting()">Delete Customer</button>
</div>

@if (showDeleteConfirmation()) {
<div class="confirmation-dialog">
<p>Are you sure you want to delete this customer?</p>
<div class="confirmation-actions">
<button type="button" (click)="deleteCustomer()" [disabled]="isDeleting()">
@if (isDeleting()) {
Deleting...
} @else {
Confirm Delete
}
</button>
<button type="button" (click)="toggleDeleteConfirmation()" [disabled]="isDeleting()">Cancel</button>
</div>
@if (deleteError()) {
<div class="error">{{ deleteError() }}</div>
}
</div>
}

<fieldset>
<legend>Personal Information</legend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { expect, it } from 'vitest';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { render, screen } from '@testing-library/angular';
import { render, screen, waitFor } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { CustomerDetailsResponse } from '@sandbox-app/customer-management/models';
import CustomerDetailsComponent from './customer-details.component';
import { generateUuid } from '@sandbox-app/shared/functions';
import { ActivatedRoute, Router } from '@angular/router';

it('renders customer details when data is loaded', async () => {
const { mockRequest } = await setup();
Expand Down Expand Up @@ -174,14 +175,82 @@ it('displays error message when API request fails and can retry', async () => {
expect(screen.queryByText(/Customer Details/i)).toBeInTheDocument();
});

it('displays delete confirmation when delete button is clicked', async () => {
const { mockRequest, user } = await setup();

await mockRequest(customerDetails);

expect(screen.queryByText('Are you sure you want to delete this customer?')).not.toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /delete customer/i }));

expect(screen.queryByText('Are you sure you want to delete this customer?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /confirm delete/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
});

it('hides confirmation when cancel is clicked', async () => {
const { mockRequest, user } = await setup();

await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
expect(screen.queryByText('Are you sure you want to delete this customer?')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(screen.queryByText('Are you sure you want to delete this customer?')).not.toBeInTheDocument();
});

it('deletes customer and navigates to overview when confirmed', async () => {
const { mockRequest, user, httpMock, router } = await setup();

const customerId = customerDetails.id;
await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
await user.click(screen.getByRole('button', { name: /confirm delete/i }));

const deleteRequest = httpMock.expectOne(`/api/customers/${customerId}`);
expect(deleteRequest.request.method).toBe('DELETE');
deleteRequest.flush({});

await waitFor(() => {
expect(router.navigate).toHaveBeenCalledWith(['../'], { relativeTo: expect.anything() });
});
});

it('shows error message when deletion fails', async () => {
const { mockRequest, user, httpMock } = await setup();

await mockRequest(customerDetails);

await user.click(screen.getByRole('button', { name: /delete customer/i }));
await user.click(screen.getByRole('button', { name: /confirm delete/i }));

const deleteRequest = httpMock.expectOne(`/api/customers/${customerDetails.id}`);
deleteRequest.flush({ title: 'Deletion failed' }, { status: 500, statusText: 'Server Error' });

await waitFor(() => {
expect(screen.queryByText('Deletion failed')).toBeInTheDocument();
});
});

async function setup() {
const user = userEvent.setup();
const customerId = generateUuid();
const routerMock = { navigate: vi.fn() };
const activatedRouteMock = {};

const { fixture } = await render(CustomerDetailsComponent, {
inputs: {
customerId,
},
providers: [provideHttpClient(), provideHttpClientTesting()],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{ provide: Router, useValue: routerMock },
{ provide: ActivatedRoute, useValue: activatedRouteMock }
],
});
const httpMock = TestBed.inject(HttpTestingController);
return {
Expand All @@ -195,6 +264,8 @@ async function setup() {
return request;
},
user,
httpMock,
router: routerMock
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ChangeDetectionStrategy, Component, inject, input, signal } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { CustomersService } from '@sandbox-app/customer-management/customer-management.service';
import { stronglyTypedIdAttribute } from '@sandbox-app/shared/functions';
import { CustomerId } from '@sandbox-app/customer-management/models';
Expand All @@ -13,7 +13,33 @@ import { CustomerId } from '@sandbox-app/customer-management/models';
})
export default class CustomerDetailsComponent {
private readonly customersService = inject(CustomersService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);

protected readonly customerId = input.required({ transform: stronglyTypedIdAttribute(CustomerId) });
protected readonly customer = this.customersService.getCustomerDetails(this.customerId);
protected readonly showDeleteConfirmation = signal(false);
protected readonly isDeleting = signal(false);
protected readonly deleteError = signal<string | null>(null);

protected toggleDeleteConfirmation(): void {
this.showDeleteConfirmation.set(!this.showDeleteConfirmation());
this.deleteError.set(null);
}

protected deleteCustomer(): void {
this.isDeleting.set(true);
this.deleteError.set(null);

this.customersService.deleteCustomer(this.customerId()).subscribe({
next: () => {
this.isDeleting.set(false);
this.router.navigate(['../'], { relativeTo: this.route });
},
error: (error) => {
this.isDeleting.set(false);
this.deleteError.set(error?.error?.title || 'An unexpected error occurred while deleting the customer.');
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ export class CustomersService {
public createCustomer(customer: CreateCustomerCommand): Observable<CustomerId> {
return this.http.post<CustomerId>('/api/customers', customer);
}

public deleteCustomer(id: CustomerId): Observable<void> {
return this.http.delete<void>(`/api/customers/${id.toString()}`);
}
}
77 changes: 77 additions & 0 deletions Sandbox.Modules.CustomerManagement.Tests/DeleteCustomerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Sandbox.Modules.CustomerManagement.Application;
using Sandbox.Modules.CustomerManagement.Data;
using Sandbox.Modules.CustomerManagement.Domain;
using Sandbox.SharedKernel.StronglyTypedIds;
using Wolverine.EntityFrameworkCore;

namespace Sandbox.Modules.CustomerManagement.Tests;

internal sealed class DeleteCustomerTests
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot delete these tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've deleted the DeleteCustomerTests.cs file as requested. Commit: d96f675

{
[Test]
public async Task Handle_deletes_customer_when_exists()
{
// Arrange
var customerId = CustomerId.New();
var customer = Customer.Create(customerId, FullName.From("John", "Doe"));

var dbContextOptions = new DbContextOptionsBuilder<CustomerDbContext>()
.UseInMemoryDatabase("DeleteCustomerTest")
.Options;

await using var dbContext = new CustomerDbContext(dbContextOptions);
await dbContext.AddAsync(customer);
await dbContext.SaveChangesAsync();

var serviceProvider = new ServiceCollection()
.AddDbContext<CustomerDbContext>(options => options.UseInMemoryDatabase("DeleteCustomerTest"))
.AddWolverine(options => options.UseEntityFrameworkCoreTransactions())
.BuildServiceProvider();

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider,
};

// Act
var result = await DeleteCustomer.Handle(customerId, httpContext, CancellationToken.None);

// Assert
await Assert.That(result).IsInstanceOf<NoContent>();

await using var verifyContext = new CustomerDbContext(dbContextOptions);
var customerExists = await verifyContext.Set<Customer>().AnyAsync(c => c.Id == customerId);
await Assert.That(customerExists).IsFalse();
}

[Test]
public async Task Handle_returns_not_found_when_customer_does_not_exist()
{
// Arrange
var customerId = CustomerId.New();

var dbContextOptions = new DbContextOptionsBuilder<CustomerDbContext>()
.UseInMemoryDatabase("DeleteCustomerNotFoundTest")
.Options;

var serviceProvider = new ServiceCollection()
.AddDbContext<CustomerDbContext>(options => options.UseInMemoryDatabase("DeleteCustomerNotFoundTest"))
.AddWolverine(options => options.UseEntityFrameworkCoreTransactions())
.BuildServiceProvider();

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider,
};

// Act
var result = await DeleteCustomer.Handle(customerId, httpContext, CancellationToken.None);

// Assert
await Assert.That(result).IsInstanceOf<NotFound>();
}
}
42 changes: 42 additions & 0 deletions Sandbox.Modules.CustomerManagement/Application/DeleteCustomer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Sandbox.Modules.CustomerManagement.Data;
using Sandbox.SharedKernel.StronglyTypedIds;
using Wolverine.EntityFrameworkCore;

namespace Sandbox.Modules.CustomerManagement.Application;

public static class DeleteCustomer
{
/// <summary>
/// Delete an existing customer.
/// </summary>
/// <returns>No content if successful.</returns>
public static async Task<Results<NoContent, NotFound>> Handle(
CustomerId customerId,
HttpContext httpContext,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(customerId);
ArgumentNullException.ThrowIfNull(httpContext);

// TODO: Should be injected, but this is a workaround for https://github.com/dotnet/aspnetcore/issues/61388
var outbox = httpContext.RequestServices.GetRequiredService<IDbContextOutbox<CustomerDbContext>>();
ArgumentNullException.ThrowIfNull(outbox);

var customer = await outbox.DbContext.Set<Domain.Customer>()
.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);

if (customer == null)
{
return TypedResults.NotFound();
}

outbox.DbContext.Remove(customer);
await outbox.SaveChangesAndFlushMessagesAsync(cancellationToken);

return TypedResults.NoContent();
}
}
1 change: 1 addition & 0 deletions Sandbox.Modules.CustomerManagement/CustomerModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public WebApplication UseModule(WebApplication app)
group.MapGet("", GetCustomers.Query).DisableValidation();
group.MapGet("{customerId}", GetCustomer.Query).DisableValidation();
group.MapPost("", CreateCustomer.Handle);
group.MapDelete("{customerId}", DeleteCustomer.Handle).DisableValidation();
return app;
}
}