diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cb0d074..7e760eb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,10 +2,13 @@ # Build and test ASP.NET Core projects targeting .NET 8 on Linux. # https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core -# Trigger pipeline on commits to master and pull requests (equivalent to GitHub Actions 'on: [push, pull_request]') +# Pipeline triggers: +# - Runs on pushes to master (production deployments) +# - Runs on all pull requests to master (validation before merge) +# - Does NOT run on pushes to development branches (cost optimization) +# Development branches (refactor/*, fix/*, feat/*, etc.) are validated only when a PR is opened trigger: - master - - feature/* pr: - master diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs index 31c710d..4d8895d 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs @@ -10,6 +10,11 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Configurations /// public class AuthorizeCheckOperationFilter : IOperationFilter { + /// + /// Applies the Bearer security requirement to Swagger operations with [Authorize] attributes. + /// + /// The OpenAPI operation to modify. + /// The operation filter context containing method metadata. public void Apply(OpenApiOperation operation, OperationFilterContext context) { // Check if [Authorize] is applied at the method or class level diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs index 1b986e6..6facad1 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs @@ -4,8 +4,17 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Utilities; +/// +/// Provides static player data for database seeding and testing. +/// Single source of truth for all player definitions. +/// public static class PlayerData { + /// + /// Returns the starting 11 players without IDs (for EF Core auto-increment). + /// Used for database migrations and seeding. + /// + /// List of 11 Player entities representing the starting lineup. public static List MakeStarting11() { return diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs index 470b27e..5c1af3f 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs @@ -14,6 +14,11 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities /// public static class DatabaseFakes { + /// + /// Creates an in-memory SQLite connection and DbContext options for testing. + /// The connection remains open for the lifetime of the test. + /// + /// A tuple containing the SQLite connection and DbContext options. public static (DbConnection, DbContextOptions) CreateSqliteConnection() { var dbConnection = new SqliteConnection("Filename=:memory:"); @@ -26,6 +31,11 @@ public static (DbConnection, DbContextOptions) CreateSqliteConn return (dbConnection, dbContextOptions); } + /// + /// Creates a PlayerDbContext instance with the specified options. + /// + /// The DbContext options to use. + /// A new PlayerDbContext instance. public static PlayerDbContext CreateDbContext( DbContextOptions dbContextOptions ) @@ -33,6 +43,11 @@ DbContextOptions dbContextOptions return new PlayerDbContext(dbContextOptions); } + /// + /// Creates the database schema for the test database. + /// Extension method for PlayerDbContext. + /// + /// The PlayerDbContext instance. public static void CreateTable(this PlayerDbContext context) { using var cmd = context.Database.GetDbConnection().CreateCommand(); @@ -46,6 +61,11 @@ CREATE TABLE players ( cmd.ExecuteNonQuery(); } + /// + /// Seeds the test database with the starting 11 players. + /// Extension method for PlayerDbContext. + /// + /// The PlayerDbContext instance. public static void Seed(this PlayerDbContext context) { context.Players.AddRange(PlayerFakes.MakeStarting11()); diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs index 3f98710..7c118b3 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs @@ -1,207 +1,77 @@ -using Dotnet.Samples.AspNetCore.WebApi.Enums; using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Utilities; namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; /// -/// A Fake is a working implementation that’s simpler than the real one. -/// It usually has some “real” logic but is not suitable for production +/// Test data factory for Player entities. +/// Wraps production data from PlayerData with test-specific modifications (e.g., GUID assignment). +/// A Fake is a working implementation that's simpler than the real one. +/// It usually has some "real" logic but is not suitable for production /// (e.g., an in‑memory database instead of a full SQL Server). Fakes are -/// useful when you need behavior that’s closer to reality but still want +/// useful when you need behavior that's closer to reality but still want /// to avoid external dependencies. /// public static class PlayerFakes { + /// + /// Returns the starting 11 players with generated GUIDs for in-memory testing. + /// Reuses production player data from PlayerData.MakeStarting11(). + /// public static List MakeStarting11() { - return - [ - new() - { - Id = Guid.NewGuid(), - FirstName = "Damián", - MiddleName = "Emiliano", - LastName = "Martínez", - DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 23, - Position = Position.Goalkeeper.Text, - AbbrPosition = Position.Goalkeeper.Abbr, - Team = "Aston Villa FC", - League = "Premier League", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Nahuel", - LastName = "Molina", - DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 26, - Position = Position.RightBack.Text, - AbbrPosition = Position.RightBack.Abbr, - Team = "Altético Madrid", - League = "La Liga", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Cristian", - MiddleName = "Gabriel", - LastName = "Romero", - DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 13, - Position = Position.CentreBack.Text, - AbbrPosition = Position.CentreBack.Abbr, - Team = "Tottenham Hotspur", - League = "Premier League", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Nicolás", - MiddleName = "Hernán Gonzalo", - LastName = "Otamendi", - DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 19, - Position = Position.CentreBack.Text, - AbbrPosition = Position.CentreBack.Abbr, - Team = "SL Benfica", - League = "Liga Portugal", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Nicolás", - MiddleName = "Alejandro", - LastName = "Tagliafico", - DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 3, - Position = Position.LeftBack.Text, - AbbrPosition = Position.LeftBack.Abbr, - Team = "Olympique Lyon", - League = "Ligue 1", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Ángel", - MiddleName = "Fabián", - LastName = "Di María", - DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 11, - Position = Position.RightWinger.Text, - AbbrPosition = Position.RightWinger.Abbr, - Team = "SL Benfica", - League = "Liga Portugal", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Rodrigo", - MiddleName = "Javier", - LastName = "de Paul", - DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 7, - Position = Position.CentralMidfield.Text, - AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Altético Madrid", - League = "La Liga", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Enzo", - MiddleName = "Jeremías", - LastName = "Fernández", - DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 24, - Position = Position.CentralMidfield.Text, - AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Chelsea FC", - League = "Premier League", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Alexis", - LastName = "Mac Allister", - DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 20, - Position = Position.CentralMidfield.Text, - AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Liverpool FC", - League = "Premier League", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Lionel", - MiddleName = "Andrés", - LastName = "Messi", - DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 10, - Position = Position.RightWinger.Text, - AbbrPosition = Position.RightWinger.Abbr, - Team = "Inter Miami CF", - League = "Major League Soccer", - Starting11 = true, - }, - new() - { - Id = Guid.NewGuid(), - FirstName = "Julián", - LastName = "Álvarez", - DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 9, - Position = Position.CentreForward.Text, - AbbrPosition = Position.CentreForward.Abbr, - Team = "Manchester City", - League = "Premier League", - Starting11 = true, - } - ]; + var players = PlayerData.MakeStarting11(); + + // Assign GUIDs for in-memory database testing + foreach (var player in players) + { + player.Id = Guid.NewGuid(); + } + + return players; } + /// + /// Returns a specific player from the starting 11 by squad number. + /// Reuses production player data from PlayerData.MakeStarting11(). + /// public static Player MakeFromStarting11(int squadNumber) { var player = - MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber) + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) ?? throw new ArgumentNullException( $"Player with Squad Number {squadNumber} not found." ); + player.Id = Guid.NewGuid(); return player; } + /// + /// Returns a new player (substitute) for testing create operations. + /// Reuses production player data from PlayerData.GetSubstitutes(). + /// public static Player MakeNew() { - return new() - { - FirstName = "Leandro", - MiddleName = "Daniel", - LastName = "Paredes", - DateOfBirth = new DateTime(1994, 06, 29, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 5, - Position = Position.DefensiveMidfield.Text, - AbbrPosition = Position.DefensiveMidfield.Abbr, - Team = "AS Roma", - League = "Serie A", - Starting11 = false - }; + // Get Leandro Paredes (squad number 5) from substitutes + var player = + PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 5) + ?? throw new InvalidOperationException( + "Substitute player with squad number 5 not found." + ); + + player.Id = Guid.NewGuid(); + return player; } /* ------------------------------------------------------------------------- * Create * ---------------------------------------------------------------------- */ + /// + /// Creates a PlayerRequestModel for testing create operations. + /// Uses data from a substitute player (Leandro Paredes). + /// public static PlayerRequestModel MakeRequestModelForCreate() { var player = MakeNew(); @@ -219,6 +89,10 @@ public static PlayerRequestModel MakeRequestModelForCreate() }; } + /// + /// Creates a PlayerResponseModel for testing create operation responses. + /// Uses data from a substitute player (Leandro Paredes). + /// public static PlayerResponseModel MakeResponseModelForCreate() { var player = MakeNew(); @@ -240,10 +114,15 @@ public static PlayerResponseModel MakeResponseModelForCreate() * Retrieve * ---------------------------------------------------------------------- */ + /// + /// Creates a PlayerRequestModel for testing retrieve operations. + /// Uses data from the starting 11 based on the specified squad number. + /// + /// The squad number of the player to retrieve. public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber) { var player = - MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber) + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) ?? throw new ArgumentNullException( $"Player with Squad Number {squadNumber} not found." ); @@ -261,10 +140,15 @@ public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber) }; } + /// + /// Creates a PlayerResponseModel for testing retrieve operation responses. + /// Uses data from the starting 11 based on the specified squad number. + /// + /// The squad number of the player to retrieve. public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber) { var player = - MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber) + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) ?? throw new ArgumentNullException( $"Player with Squad Number {squadNumber} not found." ); @@ -282,19 +166,28 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber) }; } + /// + /// Creates a list of PlayerResponseModel for testing retrieve all operations. + /// Uses all players from the starting 11. + /// public static List MakeResponseModelsForRetrieve() => [ - .. MakeStarting11() - .Select(player => new PlayerResponseModel + .. PlayerData + .MakeStarting11() + .Select(player => { - FullName = - $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), - Birth = $"{player.DateOfBirth:MMMM d, yyyy}", - Dorsal = player.SquadNumber, - Position = player.Position, - Club = player.Team, - League = player.League, - Starting11 = player.Starting11 ? "Yes" : "No" + player.Id = Guid.NewGuid(); + return new PlayerResponseModel + { + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = $"{player.DateOfBirth:MMMM d, yyyy}", + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No" + }; }) ]; @@ -302,11 +195,21 @@ .. MakeStarting11() * Update * ---------------------------------------------------------------------- */ + /// + /// Creates a PlayerRequestModel for testing update operations. + /// Delegates to MakeRequestModelForRetrieve. + /// + /// The squad number of the player to update. public static PlayerRequestModel MakeRequestModelForUpdate(int squadNumber) { return MakeRequestModelForRetrieve(squadNumber); } + /// + /// Creates a PlayerResponseModel for testing update operation responses. + /// Delegates to MakeResponseModelForRetrieve. + /// + /// The squad number of the player to update. public static PlayerResponseModel MakeResponseModelForUpdate(int squadNumber) { return MakeResponseModelForRetrieve(squadNumber); diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs index 1c07405..4b52fde 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs @@ -22,6 +22,10 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities /// public static class PlayerMocks { + /// + /// Initializes mocks for PlayerController dependencies. + /// + /// A tuple containing mocked service, logger, and validator. public static ( Mock service, Mock> logger, @@ -35,6 +39,10 @@ Mock> validator return (service, logger, validator); } + /// + /// Creates a mock IUrlHelper configured to return any string for Action calls. + /// + /// A configured Mock of IUrlHelper. public static Mock SetupUrlHelperMock() { var mock = new Mock(); @@ -43,6 +51,12 @@ public static Mock SetupUrlHelperMock() return mock; } + /// + /// Initializes mocks for PlayerService dependencies. + /// + /// Optional cache value to return from memory cache. + /// The environment name (defaults to "Development"). + /// A tuple containing mocked repository, logger, cache, mapper, and environment. public static ( Mock repository, Mock> logger, @@ -60,6 +74,12 @@ Mock environment return (repository, logger, memoryCache, mapper, environment); } + /// + /// Creates a mock IMemoryCache with configurable behavior. + /// First TryGetValue call returns false, subsequent calls return true with the specified value. + /// + /// The value to return from cache after the first call. + /// A configured Mock of IMemoryCache. public static Mock SetupMemoryCacheMock(object? value) { var cachedValue = false; diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs index 282eb17..18bdb4d 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs @@ -14,6 +14,13 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities /// public static class PlayerStubs { + /// + /// Creates a ModelStateDictionary with a single validation error. + /// Used for testing validation failure scenarios. + /// + /// The property name or key for the error. + /// The error message. + /// A ModelStateDictionary containing the specified error. public static ModelStateDictionary CreateModelError(string key, string errorMessage) { var modelStateDictionary = new ModelStateDictionary();