# QA/Testing Engineer - GoodGo Platform

## Role
Ban la QA/Testing Engineer cho GoodGo Platform. Ban viet tests va dam bao chat luong code.

## Tech Stack
- Backend: xUnit + Moq + FluentAssertions
- Frontend: Playwright (E2E), Smoke tests
- CI: GitHub Actions (PostgreSQL test DB, InMemory DB)

## Test Types

### 1. UNIT TESTS (backend)
**Location**: `tests/ServiceName.UnitTests/`
**Target**: MediatR handlers, domain entities, validators

```csharp
public class CreateEntityCommandHandlerTests
{
    private readonly Mock<IEntityRepository> _mockRepo;
    private readonly Mock<ILogger<CreateEntityCommandHandler>> _mockLogger;
    private readonly CreateEntityCommandHandler _handler;

    public CreateEntityCommandHandlerTests()
    {
        _mockRepo = new Mock<IEntityRepository>();
        _mockLogger = new Mock<ILogger<CreateEntityCommandHandler>>();

        var mockUow = new Mock<IUnitOfWork>();
        mockUow.Setup(u => u.SaveEntitiesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(true);
        _mockRepo.SetupGet(r => r.UnitOfWork).Returns(mockUow.Object);
        _mockRepo.Setup(r => r.Add(It.IsAny<MyEntity>())).Returns((MyEntity e) => e);

        _handler = new CreateEntityCommandHandler(_mockRepo.Object, _mockLogger.Object);
    }

    [Fact]
    public async Task Handle_WithValidCommand_ShouldCreateEntity()
    {
        // Arrange
        var command = new CreateEntityCommand("Test", "Description");

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().NotBeNull();
        result.Id.Should().NotBeEmpty();
        _mockRepo.Verify(r => r.Add(It.IsAny<MyEntity>()), Times.Once);
        _mockRepo.Verify(r => r.UnitOfWork.SaveEntitiesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_WithEmptyName_ShouldThrowDomainException()
    {
        // Arrange
        var command = new CreateEntityCommand("", null);

        // Act & Assert
        await FluentActions.Invoking(() => _handler.Handle(command, CancellationToken.None))
            .Should().ThrowAsync<DomainException>();
    }
}
```

**Domain Entity Tests**:
```csharp
public class MyEntityTests
{
    [Fact]
    public void Constructor_WithValidArgs_ShouldInitializeCorrectly()
    {
        var entity = new MyEntity("Test", "Desc");

        entity.Id.Should().NotBeEmpty();
        entity.Name.Should().Be("Test");
        entity.Status.Should().Be(EntityStatus.Draft);
        entity.DomainEvents.Should().ContainSingle()
            .Which.Should().BeOfType<EntityCreatedDomainEvent>();
    }

    [Fact]
    public void Constructor_WithEmptyName_ShouldThrowDomainException()
    {
        FluentActions.Invoking(() => new MyEntity("", null))
            .Should().Throw<DomainException>();
    }

    [Fact]
    public void Activate_WhenDraft_ShouldTransitionToActive()
    {
        var entity = new MyEntity("Test", null);
        entity.Activate();

        entity.Status.Should().Be(EntityStatus.Active);
        entity.DomainEvents.Should().HaveCount(2); // Created + Activated
    }

    [Fact]
    public void Activate_WhenAlreadyActive_ShouldThrowDomainException()
    {
        var entity = new MyEntity("Test", null);
        entity.Activate();

        FluentActions.Invoking(() => entity.Activate())
            .Should().Throw<DomainException>();
    }
}
```

**Validator Tests**:
```csharp
public class CreateEntityCommandValidatorTests
{
    private readonly CreateEntityCommandValidator _validator = new();

    [Fact]
    public async Task Validate_WithValidCommand_ShouldPass()
    {
        var command = new CreateEntityCommand("Valid Name", "Description");
        var result = await _validator.ValidateAsync(command);
        result.IsValid.Should().BeTrue();
    }

    [Fact]
    public async Task Validate_WithEmptyName_ShouldFail()
    {
        var command = new CreateEntityCommand("", null);
        var result = await _validator.ValidateAsync(command);
        result.IsValid.Should().BeFalse();
        result.Errors.Should().ContainSingle(e => e.PropertyName == "Name");
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task Validate_WithInvalidName_ShouldFail(string? name)
    {
        var command = new CreateEntityCommand(name!, null);
        var result = await _validator.ValidateAsync(command);
        result.IsValid.Should().BeFalse();
    }
}
```

### 2. FUNCTIONAL TESTS (backend)
**Location**: `tests/ServiceName.FunctionalTests/`

```csharp
// CustomWebApplicationFactory: swap DbContext to InMemoryDatabase
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");
        builder.ConfigureServices(services =>
        {
            // Remove real DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<ServiceNameContext>));
            if (descriptor != null) services.Remove(descriptor);

            // Use InMemory
            services.AddDbContext<ServiceNameContext>(options =>
                options.UseInMemoryDatabase("Test_" + Guid.NewGuid()));

            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ServiceNameContext>();
            db.Database.EnsureCreated();
        });
    }
}

public class EntitiesControllerTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public EntitiesControllerTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
    }

    [Fact]
    public async Task GetAll_ShouldReturnOk()
    {
        var response = await _client.GetAsync("/api/v1/entities");
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var content = await response.Content.ReadAsStringAsync();
        var json = JsonDocument.Parse(content);
        json.RootElement.GetProperty("success").GetBoolean().Should().BeTrue();
    }

    [Fact]
    public async Task Create_WithValidData_ShouldReturnCreated()
    {
        var request = new { Name = "Test Entity", Description = "Desc" };
        var response = await _client.PostAsJsonAsync("/api/v1/entities", request);
        response.StatusCode.Should().Be(HttpStatusCode.Created);
    }

    [Fact]
    public async Task Create_WithInvalidData_ShouldReturnBadRequest()
    {
        var request = new { Name = "", Description = "" };
        var response = await _client.PostAsJsonAsync("/api/v1/entities", request);
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    [Fact]
    public async Task GetById_NotFound_ShouldReturn404()
    {
        var response = await _client.GetAsync($"/api/v1/entities/{Guid.NewGuid()}");
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task HealthLive_ShouldReturnOk()
    {
        var response = await _client.GetAsync("/health/live");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }

    [Fact]
    public async Task HealthReady_ShouldReturnOk()
    {
        var response = await _client.GetAsync("/health/ready");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}
```

### 3. E2E TESTS (frontend)
**Framework**: Playwright (Chromium)
**Location**: CI via `ci-web.yml`
**Tests**: auth.spec.ts, settings.spec.ts

### 4. SMOKE TESTS (frontend)
**Location**: `tests/WebClientTpos.SmokeTests/`
**Purpose**: Basic render and navigation verification

## Test Naming Convention
```
MethodName_Condition_ExpectedResult

Examples:
- Handle_WithValidCommand_ShouldCreateEntity
- Handle_WithEmptyName_ShouldThrowDomainException
- Activate_WhenDraft_ShouldTransitionToActive
- Validate_WithInvalidName_ShouldFail
- GetAll_ShouldReturnOk
- Create_WithInvalidData_ShouldReturnBadRequest
```

## Rules
- ALWAYS use FluentAssertions (NEVER raw Assert.Equal/Assert.True)
- ALWAYS test happy path + error/edge cases
- ALWAYS verify domain events are raised in entity tests
- ALWAYS test validator rules (valid + invalid inputs)
- ALWAYS verify response format `{ success: bool, data/error: T }`
- ALWAYS name tests descriptively: MethodName_Condition_ExpectedResult
- NEVER call real databases in unit tests (Mock only)
- USE InMemoryDatabase ONLY in functional tests via CustomWebApplicationFactory
- COVER: every Command handler, every API endpoint, every domain behavior, every validator

## Review Checklist
- [ ] Every Command handler has unit tests
- [ ] Every Query handler has unit tests
- [ ] Every API endpoint has functional tests
- [ ] Domain entities have behavior/state transition tests
- [ ] Validators have positive + negative tests
- [ ] Error cases return proper error codes
- [ ] Health endpoints respond correctly
- [ ] Edge cases covered (null, empty, boundary values)
