diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index fe1152b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,30 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md -!**/.gitignore -!.git/HEAD -!.git/config -!.git/packed-refs -!.git/refs/heads/** \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d30f5b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore + run: dotnet restore src/Sample.GraphQL.API.sln + + - name: Build + run: dotnet build src/Sample.GraphQL.API.sln --no-restore --configuration Release + + - name: Test + run: dotnet test src/Sample.GraphQL.API.sln --no-build --configuration Release --verbosity normal diff --git a/.kiro/specs/dotnet10-upgrade-and-docs/.config.kiro b/.kiro/specs/dotnet10-upgrade-and-docs/.config.kiro new file mode 100644 index 0000000..b5e1f3b --- /dev/null +++ b/.kiro/specs/dotnet10-upgrade-and-docs/.config.kiro @@ -0,0 +1 @@ +{"specId": "255e8395-edde-441d-8a1b-2e86273392d4", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/dotnet10-upgrade-and-docs/design.md b/.kiro/specs/dotnet10-upgrade-and-docs/design.md new file mode 100644 index 0000000..8d5e110 --- /dev/null +++ b/.kiro/specs/dotnet10-upgrade-and-docs/design.md @@ -0,0 +1,163 @@ +# Design Document + +## Overview + +This design covers two coordinated changes to the Sample GraphQL API project: + +1. **Framework upgrade**: Migrate from .NET 9.0 to .NET 10 across all four projects, update all NuGet dependencies to their latest stable versions, and update the Dockerfile base images. +2. **README rewrite**: Replace the current README.md with a comprehensive, well-structured document that explains GraphQL concepts, the Hot Chocolate integration, the DDD architecture, query examples (select, filtering, pagination), and getting started instructions. + +Both changes are low-risk: the upgrade targets an in-memory demo project with no production traffic, and the README is a documentation-only change. The upgrade should be validated with `dotnet build`; the README is validated by visual review. + +## Architecture + +No architectural changes are introduced. The existing four-layer DDD structure remains intact: + +``` +API → Application → Domain +API → DataModel → Domain +``` + +The upgrade is a horizontal change across all layers (target framework + package versions). The README rewrite is external to the codebase. + +### Upgrade Strategy + +The upgrade follows a bottom-up dependency order to avoid transient build failures: + +```mermaid +graph TD + A[1. Domain - no dependencies] --> B[2. DataModel - depends on Domain] + A --> C[3. Application - depends on Domain, DataModel] + B --> D[4. API - depends on Application, DataModel] + C --> D + D --> E[5. Dockerfile - base images] +``` + +Each project's `.csproj` is updated in sequence: target framework first, then NuGet packages. A single `dotnet build` at the solution level validates the entire upgrade. + +### README Strategy + +The README is rewritten as a single Markdown file at the repository root. It reuses existing screenshot assets in the `assets/` folder. The structure follows a logical reading order: what is this → what technologies → how it's built → how to run it → how to query it. + +## Components and Interfaces + +### Component 1: Project Files (`.csproj`) + +Four project files require updates: + +| Project | File | Changes | +|---------|------|---------| +| Domain | `Sample.GraphQL.Domain.csproj` | TFM `net9.0` → `net10.0`, update `Bogus`, `Microsoft.Extensions.DependencyInjection.Abstractions` | +| DataModel | `Sample.GraphQL.Persistence.csproj` | TFM `net9.0` → `net10.0`, update `HotChocolate.Data.EntityFramework`, all `Microsoft.EntityFrameworkCore.*` packages, remove or update `Microsoft.AspNetCore.Http.Abstractions` | +| Application | `Sample.GraphQL.Application.csproj` | TFM `net9.0` → `net10.0`, update `HotChocolate.AspNetCore`, remove `Microsoft.AspNetCore.OpenApi` | +| API | `Sample.GraphQL.API.csproj` | TFM `net9.0` → `net10.0`, remove `Swashbuckle.AspNetCore`, remove `Microsoft.AspNetCore.OpenApi`, update container tools | + +### Component 2: Dockerfile + +The Dockerfile at `src/Sample.GraphQL.API/Dockerfile` currently references .NET 8.0 base images (already behind the codebase). Both the SDK and runtime images are updated to 10.0. + +### Component 3: README.md + +The README at the repository root is fully rewritten with the following sections: + +1. **Title and badges** — project name +2. **Project overview** — what the API does, key technologies +3. **GraphQL concepts** — what GraphQL is, why Hot Chocolate +4. **Architecture** — DDD layers, dependency direction, conventions +5. **Getting started** — prerequisites, build, run, access playground +6. **Query examples** — basic select, filtering, pagination (with screenshots) +7. **Project structure** — folder layout reference + +## Data Models + +No data model changes. The domain entities (`MovieEntity`, `ShowtimeEntity`, `ShowtimeSeatEntity`, `Seat`) and the EF Core `CinemaDbContext` remain unchanged. + +### NuGet Package Version Mapping + +Current → target versions based on research: + +| Package | Current | Target | Project(s) | +|---------|---------|--------|------------| +| `Bogus` | 35.6.2 | 35.6.5 | Domain | +| `Microsoft.Extensions.DependencyInjection.Abstractions` | 9.0.2 | 10.0.0 | Domain | +| `HotChocolate.Data.EntityFramework` | 15.0.3 | 15.1.12 | DataModel | +| `Microsoft.EntityFrameworkCore` | 9.0.2 | 10.0.0 | DataModel | +| `Microsoft.EntityFrameworkCore.InMemory` | 9.0.2 | 10.0.0 | DataModel | +| `Microsoft.EntityFrameworkCore.SqlServer` | 9.0.2 | 10.0.0 | DataModel | +| `Microsoft.AspNetCore.Http.Abstractions` | 2.3.0 | Remove (included in shared framework) | DataModel | +| `HotChocolate.AspNetCore` | 15.0.3 | 15.1.12 | Application | +| `Microsoft.AspNetCore.OpenApi` | 9.0.2 | Remove (not needed — only web UI is GraphQL playground) | Application, API | +| `Swashbuckle.AspNetCore` | 7.2.0 | Remove (not needed — only web UI is GraphQL playground) | API | +| `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` | 1.21.2 | latest stable | API | + +**Key decisions:** + +- **Remove Swashbuckle and OpenAPI entirely**: The only web interface needed is the Hot Chocolate GraphQL playground at `/graphql/`. Swashbuckle, `Microsoft.AspNetCore.OpenApi`, and all Swagger middleware are removed from the project. No replacement (Scalar or otherwise) is added. +- **Hot Chocolate stays on v15**: The latest stable release is 15.1.x. Version 16 is in release candidate and not yet stable. Staying on 15.1.x avoids breaking changes while picking up bug fixes. +- **Remove `Microsoft.AspNetCore.Http.Abstractions`**: This package is included in the ASP.NET Core shared framework since .NET 6. The explicit reference is unnecessary and can be removed. + +### Dockerfile Image Mapping + +| Stage | Current | Target | +|-------|---------|--------| +| Runtime base | `mcr.microsoft.com/dotnet/aspnet:8.0` | `mcr.microsoft.com/dotnet/aspnet:10.0` | +| Build SDK | `mcr.microsoft.com/dotnet/sdk:8.0` | `mcr.microsoft.com/dotnet/sdk:10.0` | + +### Program.cs Changes — Remove Swagger + +All Swagger/OpenAPI middleware is removed from `Program.cs`. The only web UI is the Hot Chocolate GraphQL playground, already mapped via `app.MapGraphQL()`. + +**Before:** +```csharp +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +// ... +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +``` + +**After:** +```csharp +// Swagger/OpenAPI lines removed entirely. +// The GraphQL playground at /graphql/ is the only web UI. +``` + +## Error Handling + +No new error handling is introduced. The upgrade is a version bump; error handling patterns in the existing code remain unchanged. + +**Build validation**: If the upgrade introduces compilation errors (e.g., breaking API changes in EF Core 10 or Hot Chocolate 15.1), they will surface during `dotnet build` and must be resolved before the upgrade is considered complete. + +**Dockerfile validation**: The Dockerfile should be validated with `docker build` to confirm the .NET 10 base images resolve correctly and the application compiles inside the container. + +## Testing Strategy + +**PBT applicability assessment**: Property-based testing is **not applicable** to this feature. The changes consist of: +- Configuration file edits (`.csproj` target frameworks and package versions) — these are declarative, not functional code +- Dockerfile base image updates — infrastructure configuration +- README documentation rewrite — prose content +- A small `Program.cs` cleanup removing Swagger middleware + +None of these involve pure functions with varying inputs or universal properties. There is no meaningful "for all inputs X, property P(X) holds" statement to write. + +**Recommended testing approach:** + +### Build Verification +- Run `dotnet build src/Sample.GraphQL.API.sln` after all `.csproj` changes — must produce zero errors +- Run `dotnet build src/Sample.GraphQL.API.sln --warnaserror` to catch any new warnings introduced by the upgrade + +### Runtime Smoke Test +- Start the application with `dotnet run --project src/Sample.GraphQL.API` +- Verify the GraphQL playground loads at `http://localhost:5055/graphql/` +- Execute a basic `all` query and a `showTimes` query with filtering to confirm Hot Chocolate still works + +### Dockerfile Validation +- Run `docker build -t sample-graphql-api .` from the `src/` directory +- Verify the image builds without errors + +### README Review +- Visual review of the rendered Markdown for formatting, link validity, and image references +- Verify all `assets/*.png` references resolve correctly diff --git a/.kiro/specs/dotnet10-upgrade-and-docs/requirements.md b/.kiro/specs/dotnet10-upgrade-and-docs/requirements.md new file mode 100644 index 0000000..3083746 --- /dev/null +++ b/.kiro/specs/dotnet10-upgrade-and-docs/requirements.md @@ -0,0 +1,129 @@ +# Requirements Document + +## Introduction + +This feature covers two goals for the Sample GraphQL API project: + +1. **Upgrade to .NET 10**: Migrate the entire solution from .NET 9.0 to .NET 10 and update all NuGet package dependencies to their latest compatible versions, including Hot Chocolate, Entity Framework Core, Bogus, and supporting packages. Swagger/Swashbuckle and OpenAPI packages will be removed entirely since the only web interface needed is the Hot Chocolate GraphQL playground. The Dockerfile should also be updated to use .NET 10 base images. + +2. **Improve README documentation**: Rewrite the README.md to make the project more understandable, incorporating content from the author's Medium article about ASP.NET Core GraphQL with Hot Chocolate. The documentation should explain GraphQL concepts, the Hot Chocolate integration, the DDD architecture, and provide clear query examples for selecting, filtering, and pagination. + +## Glossary + +- **Solution**: The .NET solution file (`Sample.GraphQL.API.sln`) and all four projects it contains. +- **Project_File**: A `.csproj` file that defines a project's target framework and NuGet dependencies. +- **Target_Framework_Moniker**: The `` value in a Project_File (e.g., `net9.0`, `net10.0`). +- **NuGet_Package**: A third-party or Microsoft library referenced via `` in a Project_File. +- **Dockerfile**: The container build definition at `src/Sample.GraphQL.API/Dockerfile`. +- **README**: The `README.md` file at the repository root. +- **GraphQL_Playground**: The Hot Chocolate built-in UI available at `http://localhost:5055/graphql/` for testing queries. +- **Build_System**: The `dotnet build` toolchain used to compile the Solution. +- **Query_Example**: A GraphQL query snippet demonstrating how to use the API. + +## Requirements + +### Requirement 1: Update Target Framework to .NET 10 + +**User Story:** As a developer, I want the solution to target .NET 10, so that the project benefits from the latest runtime features, performance improvements, and long-term support. + +#### Acceptance Criteria + +1. THE Build_System SHALL compile the Solution successfully after all Project_File Target_Framework_Moniker values are changed from `net9.0` to `net10.0`. +2. WHEN the Solution is built with `dotnet build`, THE Build_System SHALL produce zero errors and zero warnings related to the framework upgrade. +3. THE Solution SHALL contain no Project_File with a Target_Framework_Moniker value of `net9.0` after the upgrade is complete. + +### Requirement 2: Update All NuGet Dependencies to Latest Versions + +**User Story:** As a developer, I want all NuGet package dependencies updated to their latest stable versions compatible with .NET 10, so that the project uses the most current and secure libraries. + +#### Acceptance Criteria + +1. WHEN the upgrade is performed, THE Project_File for each project SHALL reference the latest stable version of every NuGet_Package currently listed. +2. THE Build_System SHALL compile the Solution successfully after all NuGet_Package versions are updated. +3. WHEN a NuGet_Package has a major version update available (e.g., Hot Chocolate, Entity Framework Core), THE Project_File SHALL reference the latest stable major version compatible with .NET 10. +4. IF a NuGet_Package is deprecated or replaced in .NET 10, THEN THE Project_File SHALL replace the deprecated package with its recommended successor. + +### Requirement 3: Update Dockerfile for .NET 10 + +**User Story:** As a developer, I want the Dockerfile updated to use .NET 10 base images, so that the containerized application runs on the correct runtime. + +#### Acceptance Criteria + +1. THE Dockerfile SHALL use the `mcr.microsoft.com/dotnet/aspnet:10.0` base image for the runtime stage. +2. THE Dockerfile SHALL use the `mcr.microsoft.com/dotnet/sdk:10.0` base image for the build stage. +3. WHEN the Dockerfile is built with `docker build`, THE Build_System SHALL produce a valid container image without errors. + +### Requirement 4: Add Project Overview Section to README + +**User Story:** As a new developer exploring the repository, I want the README to contain a clear project overview explaining what the API does and what technologies it uses, so that I can quickly understand the project's purpose. + +#### Acceptance Criteria + +1. THE README SHALL contain an introduction section that describes the project as a cinema showtime management API built with ASP.NET Core and Hot Chocolate. +2. THE README SHALL list the key technologies used: .NET 10, Hot Chocolate, Entity Framework Core with InMemory provider, and Bogus for data generation. +3. THE README SHALL explain that the project follows a Domain-Driven Design (DDD) layered architecture with four projects: API, Application, Domain, and DataModel. + +### Requirement 5: Add GraphQL Concepts Section to README + +**User Story:** As a developer unfamiliar with GraphQL, I want the README to explain what GraphQL is and why Hot Chocolate is used, so that I can understand the technology choices. + +#### Acceptance Criteria + +1. THE README SHALL contain a section explaining that GraphQL is a query language that allows clients to declaratively specify the exact data they need. +2. THE README SHALL explain that Hot Chocolate is an open-source GraphQL server for .NET that adheres to the latest GraphQL specifications. +3. THE README SHALL explain that Hot Chocolate integrates with Entity Framework Core through IQueryable, enabling filtering and pagination at the database level. + +### Requirement 6: Add Getting Started Section to README + +**User Story:** As a developer cloning the repository, I want clear instructions on how to build and run the project, so that I can start exploring the API immediately. + +#### Acceptance Criteria + +1. THE README SHALL list the prerequisites needed to run the project, including the .NET 10 SDK. +2. THE README SHALL provide the command to build the solution: `dotnet build src/Sample.GraphQL.API.sln`. +3. THE README SHALL provide the command to run the API: `dotnet run --project src/Sample.GraphQL.API`. +4. THE README SHALL state that the GraphQL_Playground is available at `http://localhost:5055/graphql/` after starting the application. +5. THE README SHALL state that the only web interface is the GraphQL_Playground at `http://localhost:5055/graphql/`; no Swagger or OpenAPI UI is provided. + +### Requirement 7: Document GraphQL Query Examples for Basic Selection + +**User Story:** As a developer using the API, I want the README to show how to write a basic GraphQL query to select showtime data, so that I can learn how to retrieve data from the API. + +#### Acceptance Criteria + +1. THE README SHALL contain a Query_Example demonstrating a basic `all` query that retrieves showtime properties including `id`, `movieId`, and nested `movie.title`. +2. THE README SHALL explain that the `all` endpoint corresponds to the `GetAll` method in `ShowtimesQuery` and returns all showtimes without filtering. +3. THE README SHALL include a screenshot or reference to the existing `assets/SampleResult1.png` image showing a sample result. + +### Requirement 8: Document GraphQL Filtering Examples + +**User Story:** As a developer using the API, I want the README to show how to use GraphQL filtering with the `where` clause, so that I can query specific subsets of data. + +#### Acceptance Criteria + +1. THE README SHALL contain a Query_Example demonstrating filtering on the `showTimes` endpoint using a `where` clause with nested entity filtering (e.g., filtering by `movie.title`). +2. THE README SHALL explain that filtering is enabled by the `[UseFiltering]` attribute on the `GetShowTimes` method in `ShowtimesQuery`. +3. THE README SHALL include a reference to the existing `assets/Filters.png` image showing a filtering result. +4. THE README SHALL provide a link to the official Hot Chocolate filtering documentation. + +### Requirement 9: Document GraphQL Pagination Examples + +**User Story:** As a developer using the API, I want the README to show how cursor-based pagination works, so that I can navigate through large result sets. + +#### Acceptance Criteria + +1. THE README SHALL contain a Query_Example demonstrating cursor-based pagination on the `showTimes` endpoint using `first`, `after`, `totalCount`, `pageInfo`, `edges`, `nodes`, and `cursor` fields. +2. THE README SHALL show two Query_Example instances: one for the first page and one for navigating to the next page using the `after` cursor parameter. +3. THE README SHALL explain that pagination is enabled by the `[UsePaging]` attribute on the `GetShowTimes` method in `ShowtimesQuery`. +4. THE README SHALL include references to the existing `assets/Pagination_Page1.png` and `assets/Pagination_Page2.png` images. +5. THE README SHALL provide a link to the official GraphQL pagination documentation. + +### Requirement 10: Document Project Architecture in README + +**User Story:** As a developer contributing to the project, I want the README to describe the layered architecture and project structure, so that I know where to find and place code. + +#### Acceptance Criteria + +1. THE README SHALL contain a section describing the four-project layered architecture: API, Application, Domain, and DataModel. +2. THE README SHALL explain the dependency direction: API references Application and DataModel; Application and DataModel reference Domain; Domain has no project references. +3. THE README SHALL describe the key conventions: domain entities use private constructors with static `Create()` factory methods, repository interfaces live in Domain, and implementations live in DataModel. diff --git a/.kiro/specs/dotnet10-upgrade-and-docs/tasks.md b/.kiro/specs/dotnet10-upgrade-and-docs/tasks.md new file mode 100644 index 0000000..b7e8456 --- /dev/null +++ b/.kiro/specs/dotnet10-upgrade-and-docs/tasks.md @@ -0,0 +1,116 @@ +# Implementation Plan: .NET 10 Upgrade and README Documentation + +## Overview + +This plan upgrades the Sample GraphQL API solution from .NET 9.0 to .NET 10, updates all NuGet dependencies, removes Swashbuckle/Swagger entirely (the only web UI is the GraphQL playground), updates the Dockerfile, and rewrites the README with comprehensive documentation. The upgrade follows a bottom-up dependency order (Domain → DataModel → Application → API → Dockerfile) to avoid transient build failures. + +## Tasks + +- [x] 1. Upgrade Domain project to .NET 10 + - [x] 1.1 Update `src/Sample.GraphQL.Domain/Sample.GraphQL.Domain.csproj` + - Change `` from `net9.0` to `net10.0` + - Update `Bogus` from `35.6.2` to `35.6.5` + - Update `Microsoft.Extensions.DependencyInjection.Abstractions` from `9.0.2` to `10.0.0` + - Remove the stale `` item if present + - _Requirements: 1.1, 1.3, 2.1, 2.2_ + +- [x] 2. Upgrade DataModel project to .NET 10 + - [x] 2.1 Update `src/Sample.GraphQL.DataModel/Sample.GraphQL.Persistence.csproj` + - Change `` from `net9.0` to `net10.0` + - Update `HotChocolate.Data.EntityFramework` from `15.0.3` to `15.1.12` + - Update `Microsoft.EntityFrameworkCore` from `9.0.2` to `10.0.0` + - Update `Microsoft.EntityFrameworkCore.InMemory` from `9.0.2` to `10.0.0` + - Update `Microsoft.EntityFrameworkCore.SqlServer` from `9.0.2` to `10.0.0` + - Remove `Microsoft.AspNetCore.Http.Abstractions` package reference entirely (included in ASP.NET Core shared framework since .NET 6) + - _Requirements: 1.1, 1.3, 2.1, 2.2, 2.3, 2.4_ + +- [x] 3. Upgrade Application project to .NET 10 + - [x] 3.1 Update `src/Sample.GraphQL.Application/Sample.GraphQL.Application.csproj` + - Change `` from `net9.0` to `net10.0` + - Update `HotChocolate.AspNetCore` from `15.0.3` to `15.1.12` + - Remove `Microsoft.AspNetCore.OpenApi` package reference entirely (not needed — only web UI is GraphQL playground) + - _Requirements: 1.1, 1.3, 2.1, 2.2, 2.3, 2.4_ + +- [x] 4. Upgrade API project to .NET 10 and remove Swagger/OpenAPI + - [x] 4.1 Update `src/Sample.GraphQL.API/Sample.GraphQL.API.csproj` + - Change `` from `net9.0` to `net10.0` + - Remove `Microsoft.AspNetCore.OpenApi` package reference entirely + - Remove `Swashbuckle.AspNetCore` package reference entirely + - Update `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` to latest stable version + - _Requirements: 1.1, 1.3, 2.1, 2.2, 2.4_ + - [x] 4.2 Update `src/Sample.GraphQL.API/Program.cs` to remove Swagger middleware + - Remove `builder.Services.AddEndpointsApiExplorer();` + - Remove `builder.Services.AddSwaggerGen();` + - Remove the entire `if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }` block + - The GraphQL playground at `/graphql/` (mapped via `app.MapGraphQL()`) is the only web UI + - _Requirements: 2.4_ + +- [x] 5. Checkpoint — Build verification + - Run `dotnet build src/Sample.GraphQL.API.sln` and confirm zero errors + - Ensure all tests pass, ask the user if questions arise. + - _Requirements: 1.1, 1.2, 2.2_ + +- [x] 6. Update Dockerfile to .NET 10 base images + - [x] 6.1 Update `src/Sample.GraphQL.API/Dockerfile` + - Change runtime base image from `mcr.microsoft.com/dotnet/aspnet:8.0` to `mcr.microsoft.com/dotnet/aspnet:10.0` + - Change build SDK image from `mcr.microsoft.com/dotnet/sdk:8.0` to `mcr.microsoft.com/dotnet/sdk:10.0` + - _Requirements: 3.1, 3.2, 3.3_ + +- [x] 7. Rewrite README — Project overview, technologies, and architecture + - [x] 7.1 Rewrite `README.md` with title, project overview, and technology list + - Add a title section with the project name + - Describe the project as a cinema showtime management API built with ASP.NET Core and Hot Chocolate + - List key technologies: .NET 10, Hot Chocolate, Entity Framework Core with InMemory provider, Bogus for data generation + - _Requirements: 4.1, 4.2_ + - [x] 7.2 Add GraphQL concepts section + - Explain that GraphQL is a query language allowing clients to declaratively specify exact data needed + - Explain that Hot Chocolate is an open-source GraphQL server for .NET adhering to latest specs + - Explain Hot Chocolate's integration with EF Core through IQueryable for filtering and pagination + - _Requirements: 5.1, 5.2, 5.3_ + - [x] 7.3 Add architecture section + - Describe the four-project layered architecture: API, Application, Domain, DataModel + - Explain dependency direction: API → Application/DataModel → Domain + - Describe key conventions: private constructors with static `Create()` factory methods, repository interfaces in Domain, implementations in DataModel + - _Requirements: 4.3, 10.1, 10.2, 10.3_ + +- [x] 8. Rewrite README — Getting started and query examples + - [x] 8.1 Add getting started section + - List prerequisites including .NET 10 SDK + - Provide build command: `dotnet build src/Sample.GraphQL.API.sln` + - Provide run command: `dotnet run --project src/Sample.GraphQL.API` + - State GraphQL Playground is available at `http://localhost:5055/graphql/` (the only web UI) + - Include reference to `assets/SchemaReference.png` + - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_ + - [x] 8.2 Add basic query examples section + - Show a basic `all` query retrieving `id`, `movieId`, and nested `movie.title` + - Explain that the `all` endpoint corresponds to the `GetAll` method in `ShowtimesQuery` + - Include reference to `assets/SampleResult1.png` + - _Requirements: 7.1, 7.2, 7.3_ + - [x] 8.3 Add filtering examples section + - Show a `showTimes` query with `where` clause filtering by `movie.title` + - Explain that filtering is enabled by the `[UseFiltering]` attribute on `GetShowTimes` + - Include reference to `assets/Filters.png` + - Provide link to official Hot Chocolate filtering documentation + - _Requirements: 8.1, 8.2, 8.3, 8.4_ + - [x] 8.4 Add pagination examples section + - Show cursor-based pagination on `showTimes` using `first`, `after`, `totalCount`, `pageInfo`, `edges`, and `cursor` + - Show two query examples: first page and navigating to next page with `after` cursor + - Explain that pagination is enabled by the `[UsePaging]` attribute on `GetShowTimes` + - Include references to `assets/Pagination_Page1.png` and `assets/Pagination_Page2.png` + - Provide link to official GraphQL pagination documentation + - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_ + +- [x] 9. Final checkpoint — Verify build and review + - Run `dotnet build src/Sample.GraphQL.API.sln` and confirm zero errors after all changes + - Verify all `assets/*.png` references in README resolve to existing files + - Ensure all tests pass, ask the user if questions arise. + - _Requirements: 1.1, 1.2, 2.2_ + +## Notes + +- No property-based tests or unit tests are included because the changes are configuration edits, Swagger removal, and documentation — none involve pure functions with varying inputs +- The upgrade follows bottom-up dependency order (Domain → DataModel → Application → API) to prevent transient build failures +- Hot Chocolate stays on v15.1.x (v16 is RC, not yet stable) +- `Microsoft.AspNetCore.Http.Abstractions` is removed rather than updated — it's included in the shared framework since .NET 6 +- Swashbuckle and `Microsoft.AspNetCore.OpenApi` are removed entirely — the only web UI is the Hot Chocolate GraphQL playground +- The README reuses all existing screenshot assets in the `assets/` folder diff --git a/.kiro/specs/project-improvements/.config.kiro b/.kiro/specs/project-improvements/.config.kiro new file mode 100644 index 0000000..d0646d1 --- /dev/null +++ b/.kiro/specs/project-improvements/.config.kiro @@ -0,0 +1 @@ +{"specId": "bcdde554-1367-49fe-a0f4-b1c735b4e2bb", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/project-improvements/design.md b/.kiro/specs/project-improvements/design.md new file mode 100644 index 0000000..ebe6daa --- /dev/null +++ b/.kiro/specs/project-improvements/design.md @@ -0,0 +1,290 @@ +# Design Document + +## Overview + +This design covers six improvement areas for the Sample GraphQL API project, a .NET 10 cinema showtime management demo built with Hot Chocolate and EF Core InMemory. The improvements are: + +1. **HTTP file rewrite** — Replace the stale weatherforecast `.http` file with proper GraphQL queries covering basic selection, filtering, and cursor-based pagination. +2. **Dockerfile removal** — Remove the Dockerfile, `.dockerignore`, and Docker-related csproj properties since the project is a demo with an in-memory database. +3. **Launch settings cleanup** — Remove stale Swagger references, Docker profile, and IIS Express profile; point launch URLs to the GraphQL playground. +4. **NuGet package updates** — Update all packages to latest stable versions (already done: .NET 10, Hot Chocolate 15.1.15, EF Core 10.0.7, DI Abstractions 10.0.7, Bogus 35.6.5). +5. **License audit** — Verify all NuGet dependencies use permissive open-source licenses. +6. **Unit test project** — Create an xUnit test project with tests for domain entity factory methods and business rules. + +All changes are scoped to the existing solution structure. No new application features are introduced. + +## Architecture + +The existing layered architecture remains unchanged: + +``` +API → Application → Domain +API → DataModel → Domain +``` + +The only structural addition is a new test project: + +``` +src/ +├── Sample.GraphQL.API.sln +├── Sample.GraphQL.API/ (host — modified: .http, launchSettings, csproj) +├── Sample.GraphQL.Application/ (unchanged) +├── Sample.GraphQL.Domain/ (unchanged) +├── Sample.GraphQL.DataModel/ (unchanged) +└── Sample.GraphQL.Tests/ (NEW — xUnit test project) +``` + +```mermaid +graph TD + A[Sample.GraphQL.API] --> B[Sample.GraphQL.Application] + A --> C[Sample.GraphQL.DataModel] + B --> D[Sample.GraphQL.Domain] + C --> D + E[Sample.GraphQL.Tests] --> D +``` + +The test project references only the Domain project. It tests domain entities in isolation without requiring the API, Application, or Persistence layers. + +## Components and Interfaces + +### 1. HTTP File (`Sample.GraphQL.API.http`) + +The existing `.http` file contains a stale `GET /weatherforecast` request. It will be rewritten with GraphQL POST requests. + +**Structure:** +- A `@host` variable set to `http://localhost:5055` +- `###` separators between requests +- All requests use `POST {{host}}/graphql` with `Content-Type: application/json` + +**Queries included:** +1. **Basic selection** — `all` query returning `id`, `sessionDate`, `movieId`, and nested `movie { title, stars, releaseDate, imdbId }` +2. **Filtering** — `showTimes` query with `where: { movie: { title: { eq: "Dune Part 1" } } }` demonstrating Hot Chocolate filtering syntax +3. **Pagination (page 1)** — `showTimes(first: 2)` with `totalCount`, `pageInfo { hasNextPage, endCursor }`, `edges { cursor, node { ... } }` +4. **Pagination (page 2)** — `showTimes(first: 2, after: "")` demonstrating next-page navigation + +### 2. Dockerfile and Docker Configuration Removal + +Files to delete: +- `src/Sample.GraphQL.API/Dockerfile` +- `.dockerignore` + +Changes to `Sample.GraphQL.API.csproj`: +- Remove `Linux` from `PropertyGroup` +- Remove `` + +**Rationale:** The project is a demo/sample with an in-memory database. Docker adds complexity without value — there is no persistent state, no multi-service orchestration, and no deployment target. + +### 3. Launch Settings Cleanup + +The `launchSettings.json` will be reduced to two profiles: + +```json +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "graphql", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5055" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "graphql", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7196;http://localhost:5055" + } + } +} +``` + +**Removed:** +- `IIS Express` profile and `iisSettings` section +- `Docker` profile +- All `swagger` references in `launchUrl` + +### 4. NuGet Package Updates + +Current state (updated to .NET 10, all packages at latest stable versions): + +| Project | Package | Current Version | License | +|---------|---------|----------------|---------| +| Domain | Bogus | 35.6.5 | MIT | +| Domain | Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.7 | MIT | +| Application | HotChocolate.AspNetCore | 15.1.15 | MIT | +| Persistence | HotChocolate.Data.EntityFramework | 15.1.15 | MIT | +| Persistence | Microsoft.EntityFrameworkCore | 10.0.7 | MIT | +| Persistence | Microsoft.EntityFrameworkCore.InMemory | 10.0.7 | MIT | +| Persistence | Microsoft.EntityFrameworkCore.SqlServer | 10.0.7 | MIT | + +**Note:** The Container Tools package (`Microsoft.VisualStudio.Azure.Containers.Tools.Targets`) was removed as part of Docker cleanup in Task 2. All remaining packages are at their latest stable versions as verified by `dotnet list package`. + +### 5. License Audit + +All current NuGet dependencies use permissive open-source licenses: + +| Package | License | Permissive | +|---------|---------|-----------| +| Bogus | MIT | ✅ | +| Microsoft.Extensions.DependencyInjection.Abstractions | MIT | ✅ | +| HotChocolate.AspNetCore | MIT | ✅ | +| HotChocolate.Data.EntityFramework | MIT | ✅ | +| Microsoft.EntityFrameworkCore | MIT | ✅ | +| Microsoft.EntityFrameworkCore.InMemory | MIT | ✅ | +| Microsoft.EntityFrameworkCore.SqlServer | MIT | ✅ | +| xunit (to be added) | Apache 2.0 | ✅ | +| xunit.runner.visualstudio (to be added) | Apache 2.0 | ✅ | +| Microsoft.NET.Test.Sdk (to be added) | MIT | ✅ | +| FsCheck.Xunit (to be added) | BSD-3-Clause | ✅ | + +No packages with restrictive or commercial licenses are present. + +**Audit verified:** All licenses confirmed via NuGet package metadata and source repository license files (GitHub). Sources checked: [nuget.org](https://www.nuget.org) package license pages, [dotnet/efcore](https://github.com/dotnet/efcore) (MIT), [ChilliCream/graphql-platform](https://github.com/ChilliCream/graphql-platform) (MIT), [bchavez/Bogus](https://github.com/bchavez/Bogus) (MIT), [dotnet/runtime](https://github.com/dotnet/runtime) (MIT), [xunit/xunit](https://github.com/xunit/xunit) (Apache 2.0), [fscheck/FsCheck](https://github.com/fscheck/FsCheck) (BSD-3-Clause), [microsoft/vstest](https://github.com/microsoft/vstest) (MIT). + +### 6. Unit Test Project (`Sample.GraphQL.Tests`) + +**Project setup:** +- xUnit test project targeting `net10.0` +- Added to `Sample.GraphQL.API.sln` +- References `Sample.GraphQL.Domain` +- Uses FsCheck.Xunit for property-based testing + +**Test classes:** + +| Class | Tests | Domain Entity | +|-------|-------|--------------| +| `MovieEntityTests` | Factory method property assignment, Id generation, Id uniqueness | `MovieEntity` | +| `ShowtimeEntityTests` | Factory method assignment, null movie rejection, multi-row seat rejection, non-contiguous seat rejection | `ShowtimeEntity` | +| `ShowtimeSeatEntityTests` | Initial state, reservation activation, double-purchase rejection, reserve-after-purchase rejection, cooldown enforcement | `ShowtimeSeatEntity` | + +## Data Models + +No data model changes. The existing domain entities are: + +- **`MovieEntity`** — `Id` (Guid), `Title`, `Stars`, `ImdbId`, `ReleaseDate`. Created via `MovieEntity.Create(title, stars, imdbId, releaseDate)`. +- **`ShowtimeEntity`** — `Id` (Guid), `Movie`, `SessionDate`, `Seats`, `AuditoriumId`. Created via `ShowtimeEntity.Create(movie, sessionDate)`. Has `ReserveSeats(seats)` and `HasBeenPurchased(seats)` methods. +- **`ShowtimeSeatEntity`** — `Id` (Guid), `ShowtimeId`, `Seat`, `ReservationCooldown`, `ReservationTime`, `Purchased`. Created via `ShowtimeSeatEntity.Create(seat, showtimeId)`. Has `SetReserved()` and `SetPurchased()` methods. +- **`Seat`** — Value object record: `Seat(short RowNumber, short SeatNumber)`. + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: MovieEntity.Create() round-trip + +*For any* valid title (non-null string), stars (non-null string), imdbId (non-null string), and releaseDate (DateTime), calling `MovieEntity.Create(title, stars, imdbId, releaseDate)` SHALL produce an entity where `Title == title`, `Stars == stars`, `ImdbId == imdbId`, `ReleaseDate == releaseDate`, and `Id != Guid.Empty`. + +**Validates: Requirements 10.1, 10.2** + +### Property 2: MovieEntity.Create() produces unique Ids + +*For any* two calls to `MovieEntity.Create()` with arbitrary valid inputs, the resulting entities SHALL have distinct `Id` values. + +**Validates: Requirements 10.3** + +### Property 3: ShowtimeEntity.Create() assigns movie and sessionDate + +*For any* valid `MovieEntity` and `DateTime` sessionDate, calling `ShowtimeEntity.Create(movie, sessionDate)` SHALL produce an entity where `Movie` is the provided movie and `SessionDate` is the provided date. + +**Validates: Requirements 11.1** + +### Property 4: ReserveSeats() rejects multi-row seats + +*For any* `ShowtimeEntity` and any collection of `Seat` objects spanning two or more distinct `RowNumber` values, calling `ReserveSeats(seats)` SHALL throw `InvalidOperationException`. + +**Validates: Requirements 11.3** + +### Property 5: ReserveSeats() rejects non-contiguous seats + +*For any* `ShowtimeEntity` and any collection of `Seat` objects in the same row where the sorted seat numbers have at least one gap greater than 1, calling `ReserveSeats(seats)` SHALL throw `InvalidOperationException`. + +**Validates: Requirements 11.4** + +### Property 6: ShowtimeSeatEntity.Create() initializes default state + +*For any* valid `Seat` and `Guid` showtimeId, calling `ShowtimeSeatEntity.Create(seat, showtimeId)` SHALL produce an entity where `Purchased == false` and `ReservationTime == null`. + +**Validates: Requirements 12.1** + +### Property 7: SetReserved() activates reservation time + +*For any* newly created `ShowtimeSeatEntity` (not purchased, not previously reserved), calling `SetReserved()` SHALL set `ReservationTime` to a non-null value. + +**Validates: Requirements 12.2** + +### Property 8: Purchased seat rejects further state changes + +*For any* `ShowtimeSeatEntity` that has been purchased, calling `SetPurchased()` SHALL throw `InvalidOperationException`, and calling `SetReserved()` SHALL throw `InvalidOperationException`. + +**Validates: Requirements 12.3, 12.4** + +## Error Handling + +### Domain Entity Validation Errors + +| Method | Condition | Exception | +|--------|-----------|-----------| +| `ShowtimeEntity.Create()` | `movie` is null | `ArgumentNullException` | +| `ShowtimeEntity.ReserveSeats()` | Seats span multiple rows | `InvalidOperationException` | +| `ShowtimeEntity.ReserveSeats()` | Seat numbers not contiguous | `InvalidOperationException` | +| `ShowtimeSeatEntity.SetReserved()` | Seat already purchased | `InvalidOperationException` | +| `ShowtimeSeatEntity.SetReserved()` | Within 10-minute cooldown | `InvalidOperationException` | +| `ShowtimeSeatEntity.SetPurchased()` | Seat already purchased | `InvalidOperationException` | + +### Build and Configuration Errors + +- After Dockerfile removal: `dotnet build` must succeed with zero errors. +- After NuGet updates: `dotnet build` must succeed with zero errors. +- After test project creation: `dotnet test` must discover and run all tests. + +## Testing Strategy + +### Dual Testing Approach + +**Property-based tests** (using FsCheck.Xunit): +- Verify universal properties across randomly generated inputs +- Minimum 100 iterations per property test +- Each test tagged with: `Feature: project-improvements, Property {N}: {description}` +- Cover Properties 1–8 from the Correctness Properties section + +**Example-based unit tests** (using xUnit): +- `ShowtimeEntity.Create()` throws `ArgumentNullException` when movie is null (Requirement 11.2) +- `ShowtimeSeatEntity.SetReserved()` throws within 10-minute cooldown (Requirement 12.5) +- These are specific scenarios not suited for property-based testing (null input is a single case; cooldown requires time-dependent setup) + +### Test Project Configuration + +- **Framework:** xUnit with `Microsoft.NET.Test.Sdk` +- **Property-based testing:** FsCheck.Xunit +- **Target:** `net10.0` +- **Project reference:** `Sample.GraphQL.Domain` + +### Test Coverage Matrix + +| Requirement | Test Type | Property # | +|-------------|-----------|-----------| +| 10.1, 10.2 | Property | 1 | +| 10.3 | Property | 2 | +| 11.1 | Property | 3 | +| 11.2 | Example | — | +| 11.3 | Property | 4 | +| 11.4 | Property | 5 | +| 12.1 | Property | 6 | +| 12.2 | Property | 7 | +| 12.3, 12.4 | Property | 8 | +| 12.5 | Example | — | + +### Non-Code Verification + +Requirements 1–9 involve file changes, configuration, and documentation. These are verified by: +- **Build verification:** `dotnet build` succeeds after all changes +- **Test discovery:** `dotnet test` discovers and runs all tests +- **Manual review:** HTTP file content, launchSettings structure, license audit results diff --git a/.kiro/specs/project-improvements/requirements.md b/.kiro/specs/project-improvements/requirements.md new file mode 100644 index 0000000..cda28e0 --- /dev/null +++ b/.kiro/specs/project-improvements/requirements.md @@ -0,0 +1,162 @@ +# Requirements Document + +## Introduction + +This feature covers a set of improvements to the Sample GraphQL API project, a .NET 10 cinema showtime management API built with Hot Chocolate and EF Core InMemory. The improvements span six areas: + +1. **HTTP file for GraphQL testing**: Add a comprehensive `.http` file containing all GraphQL queries (basic selection, filtering, pagination) for quick testing in Visual Studio / VS Code. +2. **Dockerfile evaluation and removal**: Assess whether the Dockerfile adds value for a demo/sample project with an in-memory database, and remove it along with Docker-related configuration if it does not. +3. **Launch settings cleanup**: Clean up `launchSettings.json` to remove stale Swagger references, the Docker profile, and the IIS Express profile, and point the launch URL to the GraphQL playground. +4. **NuGet package updates**: Update all NuGet packages across all four projects to their latest stable versions. +5. **License audit**: Verify all NuGet dependencies are free and open-source with permissive licenses (MIT, Apache 2.0, etc.). +6. **Unit test project**: Create a new xUnit test project with meaningful tests for existing domain logic (entity factory methods, seat reservation rules, repository behavior). + +## Glossary + +- **Solution**: The .NET solution file (`Sample.GraphQL.API.sln`) and all projects it contains. +- **Project_File**: A `.csproj` file that defines a project's target framework and NuGet dependencies. +- **NuGet_Package**: A third-party or Microsoft library referenced via `` in a Project_File. +- **HTTP_File**: A `.http` file that contains HTTP requests executable by Visual Studio, VS Code REST Client, or the built-in .NET HTTP file support. +- **GraphQL_Endpoint**: The Hot Chocolate GraphQL server endpoint at `http://localhost:5055/graphql/`. +- **Dockerfile**: The container build definition at `src/Sample.GraphQL.API/Dockerfile`. +- **Docker_Configuration**: All Docker-related files and settings including the Dockerfile, `.dockerignore`, and `DockerDefaultTargetOS` property in the API Project_File. +- **Build_System**: The `dotnet build` toolchain used to compile the Solution. +- **Test_Project**: An xUnit test project added to the Solution for unit testing domain logic. +- **Domain_Entity**: A class in the Domain layer that uses a private constructor with a static `Create()` factory method (e.g., `MovieEntity`, `ShowtimeEntity`, `ShowtimeSeatEntity`). +- **Seat**: A value object (C# record) representing a row number and seat number in the cinema. +- **License_Audit**: A review of all NuGet_Package dependencies to confirm their license types. +- **Launch_Settings**: The `launchSettings.json` file at `src/Sample.GraphQL.API/Properties/launchSettings.json` that defines launch profiles for the API project. + +## Requirements + +### Requirement 1: HTTP File with Basic Selection Query + +**User Story:** As a developer testing the API, I want an HTTP file containing a basic GraphQL selection query, so that I can quickly test the `all` endpoint without opening the GraphQL playground. + +#### Acceptance Criteria + +1. THE HTTP_File SHALL exist at `src/Sample.GraphQL.API/Sample.GraphQL.API.http` and contain a GraphQL POST request targeting the GraphQL_Endpoint. +2. THE HTTP_File SHALL contain a basic selection query that retrieves showtime properties including `id`, `sessionDate`, `movieId`, and nested `movie` fields (`title`, `stars`, `releaseDate`, `imdbId`). +3. WHEN the basic selection query is sent to the GraphQL_Endpoint, THE GraphQL_Endpoint SHALL return a JSON response containing the requested showtime data. + +### Requirement 2: HTTP File with Filtering Query + +**User Story:** As a developer testing the API, I want an HTTP file containing a GraphQL filtering query, so that I can quickly test the `showTimes` endpoint with `where` clause filtering. + +#### Acceptance Criteria + +1. THE HTTP_File SHALL contain a GraphQL query that uses the `showTimes` endpoint with a `where` clause to filter results by a nested entity field (e.g., filtering by `movie.title`). +2. THE HTTP_File SHALL demonstrate the Hot Chocolate filtering syntax with the `where` input parameter containing a nested object filter expression. +3. WHEN the filtering query is sent to the GraphQL_Endpoint, THE GraphQL_Endpoint SHALL return only showtimes matching the filter criteria. + +### Requirement 3: HTTP File with Pagination Query + +**User Story:** As a developer testing the API, I want an HTTP file containing GraphQL pagination queries, so that I can quickly test cursor-based pagination on the `showTimes` endpoint. + +#### Acceptance Criteria + +1. THE HTTP_File SHALL contain a GraphQL query that uses the `showTimes` endpoint with cursor-based pagination parameters including `first` and `after`. +2. THE HTTP_File SHALL include pagination metadata fields in the query: `totalCount`, `pageInfo` (with `hasNextPage`, `endCursor`), and `edges` with `cursor` and `node` fields. +3. THE HTTP_File SHALL contain at least two pagination queries: one for the first page (using `first` only) and one demonstrating navigation to the next page (using `first` and `after` with a cursor value). + +### Requirement 4: HTTP File Format and Compatibility + +**User Story:** As a developer, I want the HTTP file to follow standard `.http` file conventions, so that it works with Visual Studio, VS Code REST Client, and the built-in .NET HTTP file support. + +#### Acceptance Criteria + +1. THE HTTP_File SHALL use `###` separators between individual requests. +2. THE HTTP_File SHALL define a variable for the base host address and use the variable in all request URLs. +3. THE HTTP_File SHALL set the `Content-Type` header to `application/json` for all GraphQL requests. +4. THE HTTP_File SHALL use `POST` method for all GraphQL requests with the query in the JSON request body. + +### Requirement 5: Evaluate and Remove Dockerfile + +**User Story:** As a project maintainer, I want the Dockerfile removed if it does not add value for a sample/demo project with an in-memory database, so that the project contains only relevant files. + +#### Acceptance Criteria + +1. WHEN the Dockerfile is determined to not add value for a sample project with an in-memory database, THE Solution SHALL have the Dockerfile at `src/Sample.GraphQL.API/Dockerfile` removed. +2. WHEN the Dockerfile is removed, THE Solution SHALL have the `.dockerignore` file at the repository root removed. +3. WHEN the Dockerfile is removed, THE Project_File for the API project SHALL have the `DockerDefaultTargetOS` property removed from its `PropertyGroup`. +4. WHEN the Dockerfile is removed, THE Project_File for the API project SHALL have the `Microsoft.VisualStudio.Azure.Containers.Tools.Targets` NuGet_Package reference removed. +5. WHEN the Dockerfile is removed, THE Build_System SHALL compile the Solution successfully with zero errors. + +### Requirement 6: Clean Up Launch Settings + +**User Story:** As a developer, I want the launch settings cleaned up to remove stale references and unnecessary profiles, so that the development experience is consistent and correct. + +#### Acceptance Criteria + +1. THE Launch_Settings SHALL NOT contain any `launchUrl` value referencing `swagger` (Swagger has been removed from the project). +2. THE Launch_Settings SHALL set the `launchUrl` to `graphql` for all remaining profiles, so the browser opens the GraphQL playground on launch. +3. THE Launch_Settings SHALL NOT contain a `Docker` profile (the Dockerfile is being removed). +4. THE Launch_Settings SHALL NOT contain an `IIS Express` profile or `iisSettings` section (not needed for a sample project using Kestrel). +5. THE Launch_Settings SHALL retain the `http` profile with `applicationUrl` set to `http://localhost:5055`. +6. THE Launch_Settings SHALL retain the `https` profile with both HTTPS and HTTP URLs. + +### Requirement 7: Update All NuGet Packages to Latest Stable Versions + +**User Story:** As a developer, I want all NuGet packages updated to their latest stable versions, so that the project uses the most current and secure libraries. + +#### Acceptance Criteria + +1. WHEN the update is performed, THE Project_File for each project SHALL reference the latest stable version of every NuGet_Package currently listed. +2. THE Build_System SHALL compile the Solution successfully after all NuGet_Package versions are updated. +3. IF a NuGet_Package has a newer stable major version available, THEN THE Project_File SHALL reference the latest stable major version. +4. IF a NuGet_Package is deprecated or no longer needed, THEN THE Project_File SHALL remove the deprecated package reference. + +### Requirement 8: Verify All Packages Are Free and Open-Source + +**User Story:** As a project maintainer, I want all NuGet dependencies audited for licensing, so that I can confirm the project uses only free and open-source packages. + +#### Acceptance Criteria + +1. THE License_Audit SHALL examine every NuGet_Package referenced across all Project_File instances in the Solution. +2. THE License_Audit SHALL verify that each NuGet_Package uses a permissive open-source license (MIT, Apache 2.0, BSD, or equivalent). +3. IF a NuGet_Package uses a restrictive or commercial license, THEN THE License_Audit SHALL flag the package with its license type and a recommendation for replacement or removal. +4. THE License_Audit results SHALL be documented in a section of the requirements or design document listing each package and its license. + +### Requirement 9: Create Unit Test Project + +**User Story:** As a developer, I want a unit test project added to the solution, so that the domain logic has automated test coverage. + +#### Acceptance Criteria + +1. THE Test_Project SHALL be an xUnit test project targeting the same framework as the Solution (net10.0). +2. THE Test_Project SHALL be added to the Solution file (`Sample.GraphQL.API.sln`). +3. THE Test_Project SHALL reference the Domain project (`Sample.GraphQL.Domain`). +4. WHEN `dotnet test` is run against the Solution, THE Build_System SHALL discover and execute all tests in the Test_Project. + +### Requirement 10: Unit Tests for MovieEntity Factory Method + +**User Story:** As a developer, I want unit tests for the `MovieEntity.Create()` factory method, so that I can verify movie entities are created correctly. + +#### Acceptance Criteria + +1. THE Test_Project SHALL contain tests that verify `MovieEntity.Create()` assigns the provided `title`, `stars`, `imdbId`, and `releaseDate` to the corresponding properties of the created entity. +2. THE Test_Project SHALL contain a test that verifies `MovieEntity.Create()` assigns a non-empty `Guid` to the `Id` property. +3. THE Test_Project SHALL contain a test that verifies two calls to `MovieEntity.Create()` produce entities with distinct `Id` values. + +### Requirement 11: Unit Tests for ShowtimeEntity Factory Method and Seat Reservation + +**User Story:** As a developer, I want unit tests for `ShowtimeEntity.Create()` and the seat reservation logic, so that I can verify showtime creation and business rules are enforced. + +#### Acceptance Criteria + +1. THE Test_Project SHALL contain tests that verify `ShowtimeEntity.Create()` assigns the provided `movie` and `sessionDate` to the created entity. +2. THE Test_Project SHALL contain a test that verifies `ShowtimeEntity.Create()` throws `ArgumentNullException` when the `movie` parameter is null. +3. THE Test_Project SHALL contain a test that verifies `ShowtimeEntity.ReserveSeats()` throws `InvalidOperationException` when seats span multiple rows. +4. THE Test_Project SHALL contain a test that verifies `ShowtimeEntity.ReserveSeats()` throws `InvalidOperationException` when seat numbers are not contiguous within the same row. + +### Requirement 12: Unit Tests for ShowtimeSeatEntity Business Rules + +**User Story:** As a developer, I want unit tests for `ShowtimeSeatEntity` reservation and purchase logic, so that I can verify the seat lifecycle business rules are enforced. + +#### Acceptance Criteria + +1. THE Test_Project SHALL contain a test that verifies `ShowtimeSeatEntity.Create()` initializes `Purchased` to `false` and `ReservationTime` to `null`. +2. THE Test_Project SHALL contain a test that verifies `ShowtimeSeatEntity.SetReserved()` sets the `ReservationTime` to a non-null value. +3. THE Test_Project SHALL contain a test that verifies `ShowtimeSeatEntity.SetPurchased()` throws `InvalidOperationException` when the seat is already purchased. +4. THE Test_Project SHALL contain a test that verifies `ShowtimeSeatEntity.SetReserved()` throws `InvalidOperationException` when the seat is already purchased. +5. THE Test_Project SHALL contain a test that verifies `ShowtimeSeatEntity.SetReserved()` throws `InvalidOperationException` when called within the 10-minute reservation cooldown period. diff --git a/.kiro/specs/project-improvements/tasks.md b/.kiro/specs/project-improvements/tasks.md new file mode 100644 index 0000000..b5a9ddb --- /dev/null +++ b/.kiro/specs/project-improvements/tasks.md @@ -0,0 +1,53 @@ +# Tasks + +## Task 1: Rewrite HTTP File with GraphQL Queries +- [x] 1.1 Replace the contents of `src/Sample.GraphQL.API/Sample.GraphQL.API.http` with a `@host` variable set to `http://localhost:5055` +- [x] 1.2 Add a basic selection query using `POST {{host}}/graphql` that calls the `all` query and retrieves `id`, `sessionDate`, `movieId`, and nested `movie { title, stars, releaseDate, imdbId }` +- [x] 1.3 Add a filtering query using the `showTimes` endpoint with `where: { movie: { title: { eq: "Dune Part 1" } } }` +- [x] 1.4 Add a first-page pagination query using `showTimes(first: 2)` with `totalCount`, `pageInfo { hasNextPage, endCursor }`, and `edges { cursor, node { ... } }` +- [x] 1.5 Add a next-page pagination query using `showTimes(first: 2, after: "")` demonstrating cursor-based navigation +- [x] 1.6 Ensure all requests use `###` separators, `POST` method, and `Content-Type: application/json` header + +## Task 2: Remove Dockerfile and Docker Configuration +- [x] 2.1 Delete `src/Sample.GraphQL.API/Dockerfile` +- [x] 2.2 Delete `.dockerignore` from the repository root +- [x] 2.3 Remove `Linux` from `src/Sample.GraphQL.API/Sample.GraphQL.API.csproj` +- [x] 2.4 Remove the `` from `src/Sample.GraphQL.API/Sample.GraphQL.API.csproj` +- [x] 2.5 Run `dotnet build src/Sample.GraphQL.API.sln` and verify zero errors + +## Task 3: Clean Up Launch Settings +- [x] 3.1 Remove the `Docker` profile from `src/Sample.GraphQL.API/Properties/launchSettings.json` +- [x] 3.2 Remove the `IIS Express` profile and the `iisSettings` section +- [x] 3.3 Change `launchUrl` from `swagger` to `graphql` in the `http` and `https` profiles +- [x] 3.4 Verify the `http` profile retains `applicationUrl` of `http://localhost:5055` and the `https` profile retains `https://localhost:7196;http://localhost:5055` + +## Task 4: Verify NuGet Packages Are Up to Date +- [x] 4.1 Check all four `.csproj` files for latest stable NuGet package versions and update if newer versions are available +- [x] 4.2 Run `dotnet build src/Sample.GraphQL.API.sln` and verify zero errors after any updates + +## Task 5: Document License Audit +- [x] 5.1 Verify all NuGet packages across the solution use permissive open-source licenses (MIT, Apache 2.0, BSD) and confirm the audit table in the design document is accurate + +## Task 6: Create Unit Test Project +- [x] 6.1 Create `src/Sample.GraphQL.Tests/Sample.GraphQL.Tests.csproj` as an xUnit test project targeting `net10.0` with references to `xunit`, `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`, and `FsCheck.Xunit` +- [x] 6.2 Add a `` to `Sample.GraphQL.Domain` in the test project +- [x] 6.3 Add the test project to `src/Sample.GraphQL.API.sln` using `dotnet sln add` + +## Task 7: Implement MovieEntity Tests +- [x] 7.1 Create `src/Sample.GraphQL.Tests/MovieEntityTests.cs` with a property-based test for Property 1 (Create round-trip: all properties assigned correctly and Id is non-empty) +- [x] 7.2 Add a property-based test for Property 2 (two Create calls produce distinct Ids) +- [x] 7.3 Run `dotnet test src/Sample.GraphQL.API.sln` and verify all MovieEntity tests pass + +## Task 8: Implement ShowtimeEntity Tests +- [x] 8.1 Create `src/Sample.GraphQL.Tests/ShowtimeEntityTests.cs` with a property-based test for Property 3 (Create assigns movie and sessionDate) +- [x] 8.2 Add an example-based test that verifies `ShowtimeEntity.Create()` throws `ArgumentNullException` when movie is null +- [x] 8.3 Add a property-based test for Property 4 (ReserveSeats rejects multi-row seats) +- [x] 8.4 Add a property-based test for Property 5 (ReserveSeats rejects non-contiguous seats) +- [x] 8.5 Run `dotnet test src/Sample.GraphQL.API.sln` and verify all ShowtimeEntity tests pass + +## Task 9: Implement ShowtimeSeatEntity Tests +- [x] 9.1 Create `src/Sample.GraphQL.Tests/ShowtimeSeatEntityTests.cs` with a property-based test for Property 6 (Create initializes Purchased=false and ReservationTime=null) +- [x] 9.2 Add a property-based test for Property 7 (SetReserved sets ReservationTime to non-null) +- [x] 9.3 Add a property-based test for Property 8 (purchased seat rejects both SetPurchased and SetReserved) +- [x] 9.4 Add an example-based test that verifies `SetReserved()` throws `InvalidOperationException` within the 10-minute cooldown period +- [x] 9.5 Run `dotnet test src/Sample.GraphQL.API.sln` and verify all ShowtimeSeatEntity tests pass diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 0000000..2b572e6 --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,7 @@ +# Product Overview + +Sample GraphQL API is a cinema showtime management API built with ASP.NET Core and Hot Chocolate. It exposes a GraphQL endpoint for querying movie showtimes with support for filtering, sorting, and cursor-based pagination. A REST endpoint is also available under `/v1/cinema`. + +The domain models a cinema system with movies, showtimes, and seat reservations. Seats can be reserved (with a cooldown period) and purchased. The database is in-memory (EF Core InMemory provider) and seeded on startup with sample Dune movie data. + +The GraphQL playground is available at `http://localhost:5055/graphql/` during development. diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 0000000..2d799d7 --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,58 @@ +# Project Structure + +The solution follows a layered architecture inspired by Domain-Driven Design (DDD), with four projects under `src/`. + +``` +src/ +├── Sample.GraphQL.API.sln # Solution file +├── Sample.GraphQL.API/ # Host / entry point (Minimal API) +│ ├── Program.cs # App bootstrap, middleware, seed +│ ├── Dockerfile +│ └── Properties/launchSettings.json +├── Sample.GraphQL.Application/ # Application layer (GraphQL + REST) +│ ├── ShowtimesQuery.cs # GraphQL query type (Hot Chocolate) +│ ├── Endpoints/ # REST Minimal API endpoint groups +│ │ └── CinemaEndpoint.cs +│ └── ServicesExtensions.cs # DI registration for GraphQL +├── Sample.GraphQL.Domain/ # Domain layer (entities, interfaces) +│ ├── MovieEntity.cs +│ ├── ShowtimeEntity.cs +│ ├── ShowtimeSeatEntity.cs +│ ├── Seat.cs # Value object (record) +│ └── Repository/ # Repository interfaces +│ ├── IRepository.cs +│ └── IShowtimesRepository.cs +└── Sample.GraphQL.DataModel/ # Persistence layer (EF Core) + ├── Context/CinemaDbContext.cs # DbContext + ├── Configuration/ # EF entity type configurations + │ ├── MovieConfiguration.cs + │ ├── ShowtimeConfiguration.cs + │ └── ShowtimeSeatConfiguration.cs + ├── Repository/ # Repository implementations + │ └── ShowtimesRepository.cs + ├── SeedDb.cs # Database seeding + ├── GlobalUsing.cs # Shared global usings + └── ServicesExtensions.cs # DI registration for persistence +``` + +## Layer Dependencies + +``` +API → Application → Domain +API → DataModel → Domain +``` + +- **API** references Application and DataModel. It wires everything together in `Program.cs`. +- **Application** references Domain and DataModel. It defines GraphQL queries and REST endpoints. +- **Domain** has no project references. It defines entities with private constructors and static factory methods (`Create()`), repository interfaces, and value objects. +- **DataModel** (assembly name: `Sample.GraphQL.Persistence`) references Domain. It implements repositories, EF configurations, and the DbContext. + +## Conventions + +- Domain entities use private constructors with static `Create()` factory methods for instantiation. +- Value objects are modeled as C# records (e.g., `Seat`). +- Repository interfaces live in `Domain/Repository/`; implementations live in `DataModel/Repository/`. +- EF entity configurations are in `DataModel/Configuration/` using `IEntityTypeConfiguration`. +- Each layer has a `ServicesExtensions.cs` that exposes a single `IServiceCollection` extension method for DI registration. +- REST endpoints are organized as static extension methods on `IEndpointRouteBuilder` in `Application/Endpoints/`. +- GraphQL query types are plain classes with constructor-injected dependencies, using Hot Chocolate attributes (`[UsePaging]`, `[UseFiltering]`). diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 0000000..3c9b27f --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,41 @@ +# Tech Stack + +## Runtime & Language +- .NET 9.0 (net9.0) +- C# with nullable reference types and implicit usings enabled + +## Frameworks & Libraries +- ASP.NET Core Minimal API +- Hot Chocolate v15.0.3 (GraphQL server) with filtering, sorting, and cursor-based pagination +- Entity Framework Core 9.0.2 with InMemory provider (SQL Server provider also referenced) +- Bogus v35.6.2 for fake data generation in domain entities +- Swashbuckle / OpenAPI for REST endpoint documentation + +## Build System +- .NET SDK / MSBuild +- Solution file: `src/Sample.GraphQL.API.sln` +- Docker support via `Dockerfile` in the API project + +## Common Commands + +Build the solution: +``` +dotnet build src/Sample.GraphQL.API.sln +``` + +Run the API (launches on http://localhost:5055): +``` +dotnet run --project src/Sample.GraphQL.API +``` + +Restore NuGet packages: +``` +dotnet restore src/Sample.GraphQL.API.sln +``` + +## Key Conventions +- Dependency injection is configured via `ServicesExtensions.cs` files using `IServiceCollection` extension methods (e.g., `AddPersistence()`, `AddPresentationLayer()`) +- GraphQL server is registered through Hot Chocolate's `AddGraphQLServer()` fluent API +- EF Core DbContext is registered as a singleton with InMemory provider +- Database is seeded in `SeedDb.Initialize()` called from `Program.cs` after app build +- No test project exists in the solution currently diff --git a/README.md b/README.md index 375960d..45104ab 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,192 @@ # Sample GraphQL API -This app is a sample API that exposes GraphQL. -I utilized the Hot Chocolate NuGet package for this purpose. With this package, we can also filter the endpoints. +A cinema showtime management API built with **ASP.NET Core** and **Hot Chocolate**. The API exposes a GraphQL endpoint for querying movie showtimes, with support for filtering, sorting, and cursor-based pagination. An in-memory database is seeded on startup with sample data, so you can start exploring queries immediately. -The implementation uses Minimal API. +## Technologies -The server architecture follows a kind of Domain-Driven Design (DDD), and the database is in-memory for testing purposes. +| Technology | Version | Purpose | +|---|---|---| +| **.NET 10** | net10.0 | Runtime and SDK | +| **Hot Chocolate** | 15.1.15 | Open-source GraphQL server for .NET | +| **Entity Framework Core** | 10.0.7 | ORM with the **InMemory** provider for zero-setup persistence | +| **Bogus** | 35.6.5 | Fake data generation for seeding domain entities | +| **xUnit + FsCheck** | — | Unit and property-based testing | -When you start the app, you can use the built-in GraphQL playground by accessing http://localhost:5055/graphql/. +## GraphQL Concepts -Here's an image of what you'll find: +**GraphQL** is a query language for APIs that lets clients declaratively specify exactly the data they need. Instead of multiple REST endpoints returning fixed response shapes, a single GraphQL endpoint allows consumers to request only the fields they care about, reducing over-fetching and under-fetching. -![GraphQL Playground](assets/SchemaReference.png) +**Hot Chocolate** is an open-source GraphQL server for .NET that adheres to the latest GraphQL specifications. It provides a fluent configuration API, a built-in query playground, and first-class support for features like filtering, sorting, and cursor-based pagination through simple C# attributes (`[UseFiltering]`, `[UsePaging]`). -## Selecting and Expanding +Hot Chocolate integrates with Entity Framework Core through `IQueryable`, which means filtering and pagination expressions are translated into queries executed at the database level rather than in memory. This keeps the API efficient even as the dataset grows. -To create your first GraphQL query, you can use the following example: +## Architecture + +The solution follows a layered architecture inspired by Domain-Driven Design (DDD), split into five projects: + +``` +src/ +├── Sample.GraphQL.API/ # Host / entry point (Minimal API) +├── Sample.GraphQL.Application/ # Application layer (GraphQL queries + REST endpoints) +├── Sample.GraphQL.Domain/ # Domain layer (entities, repository interfaces) +├── Sample.GraphQL.DataModel/ # Persistence layer (EF Core, repository implementations) +└── Sample.GraphQL.Tests/ # Unit tests (xUnit + FsCheck property-based testing) +``` + +### Dependency Direction -C# code: -```C# - public async Task> GetAll() - { - var result = await showtimesRepository.GetAsync(default); - return result.AsQueryable(); - } ``` +API → Application → Domain +API → DataModel → Domain +Tests → Domain +``` + +- **API** is the host. It references Application and DataModel, wires up dependency injection, and starts the server. +- **Application** defines GraphQL query types and REST endpoints. It depends on Domain for entity types and repository interfaces. +- **Domain** has no project references. It contains entities, value objects, and repository interfaces — the core of the business logic. +- **DataModel** implements the repository interfaces defined in Domain. It owns the EF Core `DbContext`, entity configurations, and database seeding. +- **Tests** references only Domain. It verifies domain entity factory methods and business rules using property-based and example-based tests. -Where All is the name of the entity in GraphQL. +### Key Conventions + +- **Private constructors with static factory methods** — Domain entities like `MovieEntity` and `ShowtimeEntity` use private constructors and expose a static `Create()` method for instantiation. This enforces invariants at creation time. +- **Repository interfaces in Domain, implementations in DataModel** — `IShowtimesRepository` is defined in `Domain/Repository/`; its implementation `ShowtimesRepository` lives in `DataModel/Repository/`. This keeps the domain layer free of persistence concerns. +- **Value objects as records** — Simple value types like `Seat` are modeled as C# records. +- **Extension methods for DI registration** — Each layer has a `ServicesExtensions.cs` file that exposes a single `IServiceCollection` extension method (e.g., `AddPersistence()`, `AddPresentationLayer()`), keeping `Program.cs` clean. + +## Query Examples + +The project includes an `.http` file at `src/Sample.GraphQL.API/Sample.GraphQL.API.http` with ready-to-use queries. You can execute them directly from Visual Studio or VS Code with the REST Client extension. + +### Basic Selection + +The simplest way to retrieve data is through the `all` query. This returns every showtime in the database without any filtering or pagination. ```graphql { all { id + sessionDate movieId movie { title + stars + releaseDate + imdbId } - } + } } ``` -Here's a sample of the result: - -![GraphQL Playground](assets/SampleResult1.png) - -## Filtering - -C# code: -```C# - [UsePaging(IncludeTotalCount =true, DefaultPageSize =50)] - [UseFiltering] - public async Task> GetShowTimes() - { - var result = await showtimesRepository.GetAsync(default); - return result.AsQueryable(); - } -``` -Where ShowTimes is the name of the entity in GraphQL. +### Filtering +The `showTimes` endpoint supports filtering through the `where` clause. You can filter on any field, including nested entities like `movie.title`. ```graphql - -query { +{ showTimes( - first: 2 - where: { movie:{ title: { contains: "Dune"}}} + where: { movie: { title: { eq: "Dune Part 1" } } } ) { - edges { - node { - id - movieId - movie { - title - } - } + id + sessionDate + movie { + title + stars } } } - ``` -Here's a sample of the result: - - -![GetAll](assets/SchemaReference.png) +For the full list of filter operators, see the [Hot Chocolate filtering documentation](https://chillicream.com/docs/hotchocolate/v15/fetching-data/filtering). -You can refer to the Hot Chocolate documentation for guidance on implementing filtering: [Hot Chocolate - Fetching Data: Filtering](https://chillicream.com/docs/hotchocolate/v13/fetching-data/filtering) +### Pagination -## Pagination +The `showTimes` endpoint supports cursor-based pagination. Cursor-based pagination uses opaque cursors instead of page numbers, which provides stable navigation even when the underlying data changes. -To provide pagination, you should add totalCount and pageInfo arguments to the query. -Then, you should add a cursor to enable requesting the next page. - - -Page 1: +#### First Page ```graphql -query { - showTimes( - first: 1 - where: { movie:{ title: { contains: "Dune"}}} - ) { +{ + showTimes(first: 2) { totalCount pageInfo { - hasNextPage - hasPreviousPage - + hasNextPage + endCursor } edges { + cursor node { id - movieId + sessionDate movie { title + stars + releaseDate + imdbId } } - cursor } } } ``` -![Page1](assets/Pagination_Page1.png) -Page 2: +The response includes `totalCount`, `pageInfo.hasNextPage`, and `pageInfo.endCursor` (the cursor to use for the next page). + +#### Next Page + +Pass the `endCursor` value from the previous response as the `after` parameter: + ```graphql -query { - showTimes( - first: 1 - after: "MA==" - where: { movie:{ title: { contains: "Dune"}}} - ) { +{ + showTimes(first: 2, after: "") { totalCount pageInfo { - hasNextPage - hasPreviousPage - + hasNextPage + endCursor } edges { + cursor node { id - movieId + sessionDate movie { title + stars + releaseDate + imdbId } } - cursor } } } ``` -![Page1](assets/Pagination_Page2.png) +Replace `""` with the actual `endCursor` value from your first page response. + +For more on cursor-based pagination in GraphQL, see the [official GraphQL pagination documentation](https://graphql.org/learn/pagination/). + +## Getting Started + +### Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +### Build + +```bash +dotnet build src/Sample.GraphQL.API.sln +``` + +### Run + +```bash +dotnet run --project src/Sample.GraphQL.API +``` + +After the application starts, the **GraphQL Playground** is available at [`http://localhost:5055/graphql/`](http://localhost:5055/graphql/). + +Use the playground to explore the schema, write queries, and inspect results interactively. + +## License -For more information on pagination in GraphQL, you can visit [GraphQL Pagination](https://graphql.org/learn/pagination/) +All NuGet dependencies use permissive open-source licenses (MIT, Apache 2.0, BSD-3-Clause). diff --git a/assets/Filters.png b/assets/Filters.png deleted file mode 100644 index fac00c5..0000000 Binary files a/assets/Filters.png and /dev/null differ diff --git a/assets/Pagination_Page1.png b/assets/Pagination_Page1.png deleted file mode 100644 index 1ca0011..0000000 Binary files a/assets/Pagination_Page1.png and /dev/null differ diff --git a/assets/Pagination_Page2.png b/assets/Pagination_Page2.png deleted file mode 100644 index ddc712b..0000000 Binary files a/assets/Pagination_Page2.png and /dev/null differ diff --git a/assets/SampleResult1.png b/assets/SampleResult1.png deleted file mode 100644 index 15a0248..0000000 Binary files a/assets/SampleResult1.png and /dev/null differ diff --git a/assets/SchemaReference.png b/assets/SchemaReference.png deleted file mode 100644 index 0adfdbd..0000000 Binary files a/assets/SchemaReference.png and /dev/null differ diff --git a/src/Sample.GraphQL.API.sln b/src/Sample.GraphQL.API.sln index bf18655..40b9998 100644 --- a/src/Sample.GraphQL.API.sln +++ b/src/Sample.GraphQL.API.sln @@ -11,28 +11,78 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.GraphQL.Persistence" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.GraphQL.Domain", "Sample.GraphQL.Domain\Sample.GraphQL.Domain.csproj", "{893A6379-AA7C-467D-9879-C8931A470CF0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.GraphQL.Tests", "Sample.GraphQL.Tests\Sample.GraphQL.Tests.csproj", "{96E19D34-D502-4F11-82B2-5565895EF980}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|x64.Build.0 = Debug|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Debug|x86.Build.0 = Debug|Any CPU {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|Any CPU.Build.0 = Release|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|x64.ActiveCfg = Release|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|x64.Build.0 = Release|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|x86.ActiveCfg = Release|Any CPU + {C2302118-D968-4075-88C8-4C5B01930A4F}.Release|x86.Build.0 = Release|Any CPU {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|x64.Build.0 = Debug|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Debug|x86.Build.0 = Debug|Any CPU {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|Any CPU.ActiveCfg = Release|Any CPU {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|Any CPU.Build.0 = Release|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|x64.ActiveCfg = Release|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|x64.Build.0 = Release|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|x86.ActiveCfg = Release|Any CPU + {0B1C07D9-A9FC-43BF-BE39-4412EFECD3E8}.Release|x86.Build.0 = Release|Any CPU {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|Any CPU.Build.0 = Debug|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|x64.ActiveCfg = Debug|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|x64.Build.0 = Debug|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|x86.ActiveCfg = Debug|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Debug|x86.Build.0 = Debug|Any CPU {590E712D-CE34-44F8-86E8-200311CB2961}.Release|Any CPU.ActiveCfg = Release|Any CPU {590E712D-CE34-44F8-86E8-200311CB2961}.Release|Any CPU.Build.0 = Release|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Release|x64.ActiveCfg = Release|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Release|x64.Build.0 = Release|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Release|x86.ActiveCfg = Release|Any CPU + {590E712D-CE34-44F8-86E8-200311CB2961}.Release|x86.Build.0 = Release|Any CPU {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|x64.Build.0 = Debug|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Debug|x86.Build.0 = Debug|Any CPU {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|Any CPU.Build.0 = Release|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|x64.ActiveCfg = Release|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|x64.Build.0 = Release|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|x86.ActiveCfg = Release|Any CPU + {893A6379-AA7C-467D-9879-C8931A470CF0}.Release|x86.Build.0 = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|x64.ActiveCfg = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|x64.Build.0 = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|x86.ActiveCfg = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Debug|x86.Build.0 = Debug|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|Any CPU.Build.0 = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|x64.ActiveCfg = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|x64.Build.0 = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|x86.ActiveCfg = Release|Any CPU + {96E19D34-D502-4F11-82B2-5565895EF980}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Sample.GraphQL.API/Dockerfile b/src/Sample.GraphQL.API/Dockerfile deleted file mode 100644 index 75800b6..0000000 --- a/src/Sample.GraphQL.API/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER app -WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["Sample.GraphQL.API/Sample.GraphQL.API.csproj", "Sample.GraphQL.API/"] -RUN dotnet restore "./Sample.GraphQL.API/./Sample.GraphQL.API.csproj" -COPY . . -WORKDIR "/src/Sample.GraphQL.API" -RUN dotnet build "./Sample.GraphQL.API.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./Sample.GraphQL.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Sample.GraphQL.API.dll"] \ No newline at end of file diff --git a/src/Sample.GraphQL.API/Program.cs b/src/Sample.GraphQL.API/Program.cs index 2adab44..f89ca3c 100644 --- a/src/Sample.GraphQL.API/Program.cs +++ b/src/Sample.GraphQL.API/Program.cs @@ -4,8 +4,6 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); builder.Services.AddPersistence(); builder.Services.AddPresentationLayer(); @@ -18,13 +16,6 @@ var rider = app.MapGroup("/v1/cinema"); rider.AddEndpoints(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - - SeedDb.Initialize(app); app.Run(); diff --git a/src/Sample.GraphQL.API/Properties/launchSettings.json b/src/Sample.GraphQL.API/Properties/launchSettings.json index c15a053..32de3d3 100644 --- a/src/Sample.GraphQL.API/Properties/launchSettings.json +++ b/src/Sample.GraphQL.API/Properties/launchSettings.json @@ -1,9 +1,10 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -13,40 +14,12 @@ "https": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7196;http://localhost:5055" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", - "environmentVariables": { - "ASPNETCORE_HTTPS_PORTS": "8081", - "ASPNETCORE_HTTP_PORTS": "8080" - }, - "publishAllPorts": true, - "useSSL": true - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:18813", - "sslPort": 44389 } } } \ No newline at end of file diff --git a/src/Sample.GraphQL.API/Sample.GraphQL.API.csproj b/src/Sample.GraphQL.API/Sample.GraphQL.API.csproj index 9daa0f5..114a2c3 100644 --- a/src/Sample.GraphQL.API/Sample.GraphQL.API.csproj +++ b/src/Sample.GraphQL.API/Sample.GraphQL.API.csproj @@ -1,19 +1,12 @@  - net9.0 + net10.0 enable enable 0bb365aa-cbaa-4c82-b8b2-344320f3b809 - Linux - - - - - - diff --git a/src/Sample.GraphQL.API/Sample.GraphQL.API.http b/src/Sample.GraphQL.API/Sample.GraphQL.API.http index b5c6e8e..a2e3687 100644 --- a/src/Sample.GraphQL.API/Sample.GraphQL.API.http +++ b/src/Sample.GraphQL.API/Sample.GraphQL.API.http @@ -1,6 +1,33 @@ -@Sample.GraphQL.API_HostAddress = http://localhost:5055 +@host = http://localhost:5055 -GET {{Sample.GraphQL.API_HostAddress}}/weatherforecast/ -Accept: application/json +### Basic Selection Query - Get all showtimes with movie details +POST {{host}}/graphql +Content-Type: application/json -### +{ + "query": "{ all { id sessionDate movieId movie { title stars releaseDate imdbId } } }" +} + +### Filtering Query - Filter showtimes by movie title +POST {{host}}/graphql +Content-Type: application/json + +{ + "query": "{ showTimes(where: { movie: { title: { eq: \"Dune Part 1\" } } }) { id sessionDate movie { title stars } } }" +} + +### Pagination Query - First page (2 items) +POST {{host}}/graphql +Content-Type: application/json + +{ + "query": "{ showTimes(first: 2) { totalCount pageInfo { hasNextPage endCursor } edges { cursor node { id sessionDate movie { title stars releaseDate imdbId } } } } }" +} + +### Pagination Query - Next page using cursor +POST {{host}}/graphql +Content-Type: application/json + +{ + "query": "{ showTimes(first: 2, after: \"\") { totalCount pageInfo { hasNextPage endCursor } edges { cursor node { id sessionDate movie { title stars releaseDate imdbId } } } } }" +} diff --git a/src/Sample.GraphQL.Application/Sample.GraphQL.Application.csproj b/src/Sample.GraphQL.Application/Sample.GraphQL.Application.csproj index 10b7e22..3475780 100644 --- a/src/Sample.GraphQL.Application/Sample.GraphQL.Application.csproj +++ b/src/Sample.GraphQL.Application/Sample.GraphQL.Application.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable @@ -12,8 +12,7 @@ - - + diff --git a/src/Sample.GraphQL.DataModel/Sample.GraphQL.Persistence.csproj b/src/Sample.GraphQL.DataModel/Sample.GraphQL.Persistence.csproj index 67e5078..27d5a4c 100644 --- a/src/Sample.GraphQL.DataModel/Sample.GraphQL.Persistence.csproj +++ b/src/Sample.GraphQL.DataModel/Sample.GraphQL.Persistence.csproj @@ -1,17 +1,20 @@  - net9.0 + net10.0 enable enable - - - - - + + + + + + + + diff --git a/src/Sample.GraphQL.Domain/Sample.GraphQL.Domain.csproj b/src/Sample.GraphQL.Domain/Sample.GraphQL.Domain.csproj index 219c3b9..65ba650 100644 --- a/src/Sample.GraphQL.Domain/Sample.GraphQL.Domain.csproj +++ b/src/Sample.GraphQL.Domain/Sample.GraphQL.Domain.csproj @@ -1,18 +1,14 @@  - net9.0 + net10.0 enable enable - - - - - - + + diff --git a/src/Sample.GraphQL.Tests/MovieEntityTests.cs b/src/Sample.GraphQL.Tests/MovieEntityTests.cs new file mode 100644 index 0000000..6d0384e --- /dev/null +++ b/src/Sample.GraphQL.Tests/MovieEntityTests.cs @@ -0,0 +1,55 @@ +using FsCheck; +using FsCheck.Xunit; +using Sample.GraphQL.Domain; + +namespace Sample.GraphQL.Tests; + +/// +/// Property-based tests for MovieEntity.Create() factory method. +/// +public class MovieEntityTests +{ + /// + /// Property 1: MovieEntity.Create() round-trip — all properties assigned correctly and Id is non-empty. + /// For any valid title, stars, imdbId, and releaseDate, calling Create() produces an entity + /// where all properties match the inputs and Id != Guid.Empty. + /// **Validates: Requirements 10.1, 10.2** + /// + [Property(MaxTest = 100)] + public bool Create_RoundTrip_AllPropertiesAssignedAndIdNonEmpty( + NonNull title, + NonNull stars, + NonNull imdbId, + DateTime releaseDate) + { + var movie = MovieEntity.Create(title.Get, stars.Get, imdbId.Get, releaseDate); + + return movie.Title == title.Get + && movie.Stars == stars.Get + && movie.ImdbId == imdbId.Get + && movie.ReleaseDate == releaseDate + && movie.Id != Guid.Empty; + } + + /// + /// Property 2: MovieEntity.Create() produces unique Ids. + /// For any two calls to Create() with arbitrary valid inputs, the resulting entities have distinct Id values. + /// **Validates: Requirements 10.3** + /// + [Property(MaxTest = 100)] + public bool Create_TwoCalls_ProduceDistinctIds( + NonNull title1, + NonNull stars1, + NonNull imdbId1, + DateTime releaseDate1, + NonNull title2, + NonNull stars2, + NonNull imdbId2, + DateTime releaseDate2) + { + var movie1 = MovieEntity.Create(title1.Get, stars1.Get, imdbId1.Get, releaseDate1); + var movie2 = MovieEntity.Create(title2.Get, stars2.Get, imdbId2.Get, releaseDate2); + + return movie1.Id != movie2.Id; + } +} diff --git a/src/Sample.GraphQL.Tests/Sample.GraphQL.Tests.csproj b/src/Sample.GraphQL.Tests/Sample.GraphQL.Tests.csproj new file mode 100644 index 0000000..754768f --- /dev/null +++ b/src/Sample.GraphQL.Tests/Sample.GraphQL.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Sample.GraphQL.Tests/ShowtimeEntityTests.cs b/src/Sample.GraphQL.Tests/ShowtimeEntityTests.cs new file mode 100644 index 0000000..608cebb --- /dev/null +++ b/src/Sample.GraphQL.Tests/ShowtimeEntityTests.cs @@ -0,0 +1,126 @@ +using FsCheck; +using FsCheck.Xunit; +using Sample.GraphQL.Domain; +using Xunit; + +namespace Sample.GraphQL.Tests; + +/// +/// Property-based and example-based tests for ShowtimeEntity. +/// +public class ShowtimeEntityTests +{ + /// + /// Property 3: ShowtimeEntity.Create() assigns movie and sessionDate. + /// For any valid MovieEntity and DateTime sessionDate, calling ShowtimeEntity.Create(movie, sessionDate) + /// SHALL produce an entity where Movie is the provided movie and SessionDate is the provided date. + /// **Validates: Requirements 11.1** + /// + [Property(MaxTest = 100)] + public bool Create_AssignsMovieAndSessionDate( + NonNull title, + NonNull stars, + NonNull imdbId, + DateTime releaseDate, + DateTime sessionDate) + { + var movie = MovieEntity.Create(title.Get, stars.Get, imdbId.Get, releaseDate); + var showtime = ShowtimeEntity.Create(movie, sessionDate); + + return showtime.Movie == movie + && showtime.SessionDate == sessionDate; + } + + /// + /// Example-based test: ShowtimeEntity.Create() throws ArgumentNullException when movie is null. + /// **Validates: Requirements 11.2** + /// + [Fact] + public void Create_NullMovie_ThrowsArgumentNullException() + { + var sessionDate = DateTime.UtcNow; + + var ex = Assert.Throws(() => ShowtimeEntity.Create(null!, sessionDate)); + Assert.Equal("movie", ex.ParamName); + } + + /// + /// Property 4: ReserveSeats() rejects multi-row seats. + /// For any collection of Seat objects spanning two or more distinct RowNumber values, + /// calling ReserveSeats(seats) SHALL throw InvalidOperationException. + /// **Validates: Requirements 11.3** + /// + [Property(MaxTest = 100)] + public bool ReserveSeats_MultipleRows_ThrowsInvalidOperationException( + PositiveInt row1, + PositiveInt row2, + PositiveInt seatNum1, + PositiveInt seatNum2) + { + var r1 = (short)(row1.Get % 100 + 1); + var r2 = (short)(row2.Get % 100 + 1); + + // Ensure distinct rows + if (r1 == r2) r2 = (short)(r1 % 100 + 1); + if (r1 == r2) return true; // Skip degenerate case + + var s1 = (short)(seatNum1.Get % 50 + 1); + var s2 = (short)(seatNum2.Get % 50 + 1); + + var seats = new List + { + new Seat(r1, s1), + new Seat(r2, s2) + }; + + var movie = MovieEntity.Create("Test", "Stars", "tt0000001", DateTime.UtcNow); + var showtime = ShowtimeEntity.Create(movie, DateTime.UtcNow); + + try + { + showtime.ReserveSeats(seats); + return false; // Should have thrown + } + catch (InvalidOperationException) + { + return true; + } + } + + /// + /// Property 5: ReserveSeats() rejects non-contiguous seats. + /// For any collection of Seat objects in the same row where the sorted seat numbers have + /// at least one gap greater than 1, calling ReserveSeats(seats) SHALL throw InvalidOperationException. + /// **Validates: Requirements 11.4** + /// + [Property(MaxTest = 100)] + public bool ReserveSeats_NonContiguousSeats_ThrowsInvalidOperationException( + PositiveInt row, + PositiveInt baseSeat, + PositiveInt gap) + { + var r = (short)(row.Get % 100 + 1); + var s1 = (short)(baseSeat.Get % 40 + 1); + var gapVal = gap.Get % 9 + 2; // gap between 2 and 10 + var s2 = (short)(s1 + gapVal); + + var seats = new List + { + new Seat(r, s1), + new Seat(r, s2) + }; + + var movie = MovieEntity.Create("Test", "Stars", "tt0000001", DateTime.UtcNow); + var showtime = ShowtimeEntity.Create(movie, DateTime.UtcNow); + + try + { + showtime.ReserveSeats(seats); + return false; // Should have thrown + } + catch (InvalidOperationException) + { + return true; + } + } +} diff --git a/src/Sample.GraphQL.Tests/ShowtimeSeatEntityTests.cs b/src/Sample.GraphQL.Tests/ShowtimeSeatEntityTests.cs new file mode 100644 index 0000000..a12b6c2 --- /dev/null +++ b/src/Sample.GraphQL.Tests/ShowtimeSeatEntityTests.cs @@ -0,0 +1,112 @@ +using FsCheck; +using FsCheck.Xunit; +using Sample.GraphQL.Domain; +using Xunit; + +namespace Sample.GraphQL.Tests; + +/// +/// Property-based and example-based tests for ShowtimeSeatEntity. +/// +public class ShowtimeSeatEntityTests +{ + /// + /// Property 6: ShowtimeSeatEntity.Create() initializes default state. + /// For any valid Seat and Guid showtimeId, calling Create(seat, showtimeId) + /// SHALL produce an entity where Purchased == false and ReservationTime == null. + /// **Validates: Requirements 12.1** + /// + [Property(MaxTest = 100)] + public bool Create_InitializesPurchasedFalseAndReservationTimeNull( + short rowNumber, + short seatNumber, + Guid showtimeId) + { + var seat = new Seat(rowNumber, seatNumber); + var entity = ShowtimeSeatEntity.Create(seat, showtimeId); + + return entity.Purchased == false + && entity.ReservationTime == null; + } + + /// + /// Property 7: SetReserved() activates reservation time. + /// For any newly created ShowtimeSeatEntity (not purchased, not previously reserved), + /// calling SetReserved() SHALL set ReservationTime to a non-null value. + /// **Validates: Requirements 12.2** + /// + [Property(MaxTest = 100)] + public bool SetReserved_SetsReservationTimeToNonNull( + short rowNumber, + short seatNumber, + Guid showtimeId) + { + var seat = new Seat(rowNumber, seatNumber); + var entity = ShowtimeSeatEntity.Create(seat, showtimeId); + + entity.SetReserved(); + + return entity.ReservationTime != null; + } + + /// + /// Property 8: Purchased seat rejects further state changes. + /// For any ShowtimeSeatEntity that has been purchased, calling SetPurchased() + /// SHALL throw InvalidOperationException, and calling SetReserved() + /// SHALL throw InvalidOperationException. + /// **Validates: Requirements 12.3, 12.4** + /// + [Property(MaxTest = 100)] + public bool PurchasedSeat_RejectsBothSetPurchasedAndSetReserved( + short rowNumber, + short seatNumber, + Guid showtimeId) + { + var seat = new Seat(rowNumber, seatNumber); + var entity = ShowtimeSeatEntity.Create(seat, showtimeId); + entity.SetPurchased(); + + bool setPurchasedThrew; + bool setReservedThrew; + + try + { + entity.SetPurchased(); + setPurchasedThrew = false; + } + catch (InvalidOperationException) + { + setPurchasedThrew = true; + } + + try + { + entity.SetReserved(); + setReservedThrew = false; + } + catch (InvalidOperationException) + { + setReservedThrew = true; + } + + return setPurchasedThrew && setReservedThrew; + } + + /// + /// Example-based test: SetReserved() throws InvalidOperationException within the 10-minute cooldown period. + /// **Validates: Requirements 12.5** + /// + [Fact] + public void SetReserved_WithinCooldownPeriod_ThrowsInvalidOperationException() + { + var seat = new Seat(1, 1); + var entity = ShowtimeSeatEntity.Create(seat, Guid.NewGuid()); + + // First reservation sets ReservationTime to DateTime.UtcNow + entity.SetReserved(); + + // Second call is immediately after, well within the 10-minute cooldown + var ex = Assert.Throws(() => entity.SetReserved()); + Assert.Contains("10", ex.Message); + } +}