From 10956254d985918b17af0608725d1328beecb99f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:32:13 +0000 Subject: [PATCH 1/4] Initial plan From 39ca7b9ad6863889231919c0bda9cc7a5a238319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:39:33 +0000 Subject: [PATCH 2/4] Implement instructor rating feature - entities, repositories, services, and API Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068 --- .../InstructorRatingsController.cs | 207 ++++++++++++++++++ .../CourseRegistration.API.csproj | 4 + api/CourseRegistration.API/Program.cs | 2 + .../DTOs/InstructorRatingDtos.cs | 160 ++++++++++++++ .../Interfaces/IInstructorRatingService.cs | 54 +++++ .../Mappings/MappingProfile.cs | 22 ++ .../Services/InstructorRatingService.cs | 180 +++++++++++++++ .../Entities/Course.cs | 19 ++ .../Entities/InstructorRating.cs | 63 ++++++ .../Entities/Student.cs | 5 + .../Interfaces/IInstructorRatingRepository.cs | 44 ++++ .../Interfaces/IUnitOfWork.cs | 5 + .../Data/CourseRegistrationDbContext.cs | 40 ++++ .../CourseRegistrationDbContextFactory.cs | 20 ++ .../InstructorRatingRepository.cs | 103 +++++++++ .../Repositories/UnitOfWork.cs | 13 ++ 16 files changed, 941 insertions(+) create mode 100644 api/CourseRegistration.API/Controllers/InstructorRatingsController.cs create mode 100644 api/CourseRegistration.Application/DTOs/InstructorRatingDtos.cs create mode 100644 api/CourseRegistration.Application/Interfaces/IInstructorRatingService.cs create mode 100644 api/CourseRegistration.Application/Services/InstructorRatingService.cs create mode 100644 api/CourseRegistration.Domain/Entities/InstructorRating.cs create mode 100644 api/CourseRegistration.Domain/Interfaces/IInstructorRatingRepository.cs create mode 100644 api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContextFactory.cs create mode 100644 api/CourseRegistration.Infrastructure/Repositories/InstructorRatingRepository.cs diff --git a/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs new file mode 100644 index 0000000..efb8734 --- /dev/null +++ b/api/CourseRegistration.API/Controllers/InstructorRatingsController.cs @@ -0,0 +1,207 @@ +using Microsoft.AspNetCore.Mvc; +using CourseRegistration.Application.DTOs; +using CourseRegistration.Application.Interfaces; + +namespace CourseRegistration.API.Controllers; + +/// +/// Controller for instructor rating operations +/// +[ApiController] +[Route("api/[controller]")] +[Produces("application/json")] +public class InstructorRatingsController : ControllerBase +{ + private readonly IInstructorRatingService _ratingService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the InstructorRatingsController + /// + public InstructorRatingsController(IInstructorRatingService ratingService, ILogger logger) + { + _ratingService = ratingService ?? throw new ArgumentNullException(nameof(ratingService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Creates a new instructor rating + /// + /// Rating creation details + /// Created rating + [HttpPost] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status400BadRequest)] + public async Task>> CreateRating([FromBody] CreateInstructorRatingDto createRatingDto) + { + _logger.LogInformation("Creating new rating for course {CourseId} by student {StudentId}", + createRatingDto.CourseId, createRatingDto.StudentId); + + try + { + var rating = await _ratingService.CreateRatingAsync(createRatingDto); + + return CreatedAtAction( + nameof(GetRating), + new { id = rating.RatingId }, + ApiResponseDto.SuccessResponse(rating, "Rating created successfully")); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning("Failed to create rating: {Message}", ex.Message); + return BadRequest(ApiResponseDto.ErrorResponse(ex.Message)); + } + } + + /// + /// Gets a rating by ID + /// + /// Rating ID + /// Rating details + [HttpGet("{id:guid}")] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status404NotFound)] + public async Task>> GetRating(Guid id) + { + _logger.LogInformation("Getting rating with ID: {RatingId}", id); + + var rating = await _ratingService.GetRatingByIdAsync(id); + if (rating == null) + { + return NotFound(ApiResponseDto.ErrorResponse("Rating not found")); + } + + return Ok(ApiResponseDto.SuccessResponse(rating, "Rating retrieved successfully")); + } + + /// + /// Gets all ratings for a specific course + /// + /// Course ID + /// List of ratings + [HttpGet("course/{courseId:guid}")] + [ProducesResponseType(typeof(ApiResponseDto>), StatusCodes.Status200OK)] + public async Task>>> GetRatingsByCourse(Guid courseId) + { + _logger.LogInformation("Getting ratings for course: {CourseId}", courseId); + + var ratings = await _ratingService.GetRatingsByCourseIdAsync(courseId); + return Ok(ApiResponseDto>.SuccessResponse(ratings, "Ratings retrieved successfully")); + } + + /// + /// Gets all ratings submitted by a specific student + /// + /// Student ID + /// List of ratings + [HttpGet("student/{studentId:guid}")] + [ProducesResponseType(typeof(ApiResponseDto>), StatusCodes.Status200OK)] + public async Task>>> GetRatingsByStudent(Guid studentId) + { + _logger.LogInformation("Getting ratings by student: {StudentId}", studentId); + + var ratings = await _ratingService.GetRatingsByStudentIdAsync(studentId); + return Ok(ApiResponseDto>.SuccessResponse(ratings, "Ratings retrieved successfully")); + } + + /// + /// Gets all ratings for a specific instructor + /// + /// Instructor name + /// List of ratings + [HttpGet("instructor/{instructorName}")] + [ProducesResponseType(typeof(ApiResponseDto>), StatusCodes.Status200OK)] + public async Task>>> GetRatingsByInstructor(string instructorName) + { + _logger.LogInformation("Getting ratings for instructor: {InstructorName}", instructorName); + + var ratings = await _ratingService.GetRatingsByInstructorNameAsync(instructorName); + return Ok(ApiResponseDto>.SuccessResponse(ratings, "Ratings retrieved successfully")); + } + + /// + /// Gets rating statistics for an instructor + /// + /// Instructor name + /// Rating statistics + [HttpGet("instructor/{instructorName}/stats")] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status404NotFound)] + public async Task>> GetInstructorStats(string instructorName) + { + _logger.LogInformation("Getting stats for instructor: {InstructorName}", instructorName); + + var stats = await _ratingService.GetInstructorStatsAsync(instructorName); + if (stats == null) + { + return NotFound(ApiResponseDto.ErrorResponse("No ratings found for this instructor")); + } + + return Ok(ApiResponseDto.SuccessResponse(stats, "Statistics retrieved successfully")); + } + + /// + /// Gets a rating by student and course + /// + /// Student ID + /// Course ID + /// Rating details + [HttpGet("student/{studentId:guid}/course/{courseId:guid}")] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status404NotFound)] + public async Task>> GetRatingByStudentAndCourse(Guid studentId, Guid courseId) + { + _logger.LogInformation("Getting rating for student {StudentId} and course {CourseId}", studentId, courseId); + + var rating = await _ratingService.GetRatingByStudentAndCourseAsync(studentId, courseId); + if (rating == null) + { + return NotFound(ApiResponseDto.ErrorResponse("Rating not found")); + } + + return Ok(ApiResponseDto.SuccessResponse(rating, "Rating retrieved successfully")); + } + + /// + /// Updates an existing rating + /// + /// Rating ID + /// Rating update details + /// Updated rating + [HttpPut("{id:guid}")] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status404NotFound)] + public async Task>> UpdateRating(Guid id, [FromBody] UpdateInstructorRatingDto updateRatingDto) + { + _logger.LogInformation("Updating rating with ID: {RatingId}", id); + + var rating = await _ratingService.UpdateRatingAsync(id, updateRatingDto); + if (rating == null) + { + return NotFound(ApiResponseDto.ErrorResponse("Rating not found")); + } + + return Ok(ApiResponseDto.SuccessResponse(rating, "Rating updated successfully")); + } + + /// + /// Deletes a rating + /// + /// Rating ID + /// Success status + [HttpDelete("{id:guid}")] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponseDto), StatusCodes.Status404NotFound)] + public async Task>> DeleteRating(Guid id) + { + _logger.LogInformation("Deleting rating with ID: {RatingId}", id); + + var success = await _ratingService.DeleteRatingAsync(id); + if (!success) + { + return NotFound(ApiResponseDto.ErrorResponse("Rating not found")); + } + + return Ok(ApiResponseDto.SuccessResponse(null, "Rating deleted successfully")); + } +} diff --git a/api/CourseRegistration.API/CourseRegistration.API.csproj b/api/CourseRegistration.API/CourseRegistration.API.csproj index 1653847..4ead178 100644 --- a/api/CourseRegistration.API/CourseRegistration.API.csproj +++ b/api/CourseRegistration.API/CourseRegistration.API.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/api/CourseRegistration.API/Program.cs b/api/CourseRegistration.API/Program.cs index e7e52e4..c0be55d 100644 --- a/api/CourseRegistration.API/Program.cs +++ b/api/CourseRegistration.API/Program.cs @@ -60,12 +60,14 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); // Register services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Register authorization services builder.Services.AddScoped(); diff --git a/api/CourseRegistration.Application/DTOs/InstructorRatingDtos.cs b/api/CourseRegistration.Application/DTOs/InstructorRatingDtos.cs new file mode 100644 index 0000000..749010e --- /dev/null +++ b/api/CourseRegistration.Application/DTOs/InstructorRatingDtos.cs @@ -0,0 +1,160 @@ +using System.ComponentModel.DataAnnotations; + +namespace CourseRegistration.Application.DTOs; + +/// +/// DTO for creating a new instructor rating +/// +public class CreateInstructorRatingDto +{ + /// + /// Course ID for which the instructor is being rated + /// + [Required] + public Guid CourseId { get; set; } + + /// + /// Student ID who is submitting the rating + /// + [Required] + public Guid StudentId { get; set; } + + /// + /// Rating value (1-5 scale) + /// + [Required] + [Range(1, 5, ErrorMessage = "Rating must be between 1 and 5")] + public int Rating { get; set; } + + /// + /// Optional comment/review text + /// + [MaxLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters")] + public string? Comment { get; set; } +} + +/// +/// DTO for updating an existing instructor rating +/// +public class UpdateInstructorRatingDto +{ + /// + /// Rating value (1-5 scale) + /// + [Required] + [Range(1, 5, ErrorMessage = "Rating must be between 1 and 5")] + public int Rating { get; set; } + + /// + /// Optional comment/review text + /// + [MaxLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters")] + public string? Comment { get; set; } +} + +/// +/// DTO for returning instructor rating details +/// +public class InstructorRatingDto +{ + /// + /// Unique identifier for the rating + /// + public Guid RatingId { get; set; } + + /// + /// Course ID + /// + public Guid CourseId { get; set; } + + /// + /// Course name + /// + public string CourseName { get; set; } = string.Empty; + + /// + /// Instructor name + /// + public string InstructorName { get; set; } = string.Empty; + + /// + /// Student ID who submitted the rating + /// + public Guid StudentId { get; set; } + + /// + /// Student name + /// + public string StudentName { get; set; } = string.Empty; + + /// + /// Rating value (1-5 scale) + /// + public int Rating { get; set; } + + /// + /// Optional comment/review text + /// + public string? Comment { get; set; } + + /// + /// Date when the rating was created + /// + public DateTime CreatedAt { get; set; } + + /// + /// Date when the rating was last updated + /// + public DateTime UpdatedAt { get; set; } +} + +/// +/// DTO for instructor rating statistics +/// +public class InstructorRatingStatsDto +{ + /// + /// Instructor name + /// + public string InstructorName { get; set; } = string.Empty; + + /// + /// Average rating + /// + public double AverageRating { get; set; } + + /// + /// Total number of ratings + /// + public int TotalRatings { get; set; } + + /// + /// Number of 5-star ratings + /// + public int FiveStarCount { get; set; } + + /// + /// Number of 4-star ratings + /// + public int FourStarCount { get; set; } + + /// + /// Number of 3-star ratings + /// + public int ThreeStarCount { get; set; } + + /// + /// Number of 2-star ratings + /// + public int TwoStarCount { get; set; } + + /// + /// Number of 1-star ratings + /// + public int OneStarCount { get; set; } + + /// + /// List of courses taught by this instructor + /// + public List CourseIds { get; set; } = new(); +} diff --git a/api/CourseRegistration.Application/Interfaces/IInstructorRatingService.cs b/api/CourseRegistration.Application/Interfaces/IInstructorRatingService.cs new file mode 100644 index 0000000..089f84c --- /dev/null +++ b/api/CourseRegistration.Application/Interfaces/IInstructorRatingService.cs @@ -0,0 +1,54 @@ +using CourseRegistration.Application.DTOs; + +namespace CourseRegistration.Application.Interfaces; + +/// +/// Service interface for instructor rating operations +/// +public interface IInstructorRatingService +{ + /// + /// Creates a new instructor rating + /// + Task CreateRatingAsync(CreateInstructorRatingDto createRatingDto); + + /// + /// Gets a rating by ID + /// + Task GetRatingByIdAsync(Guid ratingId); + + /// + /// Gets all ratings for a specific course + /// + Task> GetRatingsByCourseIdAsync(Guid courseId); + + /// + /// Gets all ratings submitted by a specific student + /// + Task> GetRatingsByStudentIdAsync(Guid studentId); + + /// + /// Gets all ratings for a specific instructor + /// + Task> GetRatingsByInstructorNameAsync(string instructorName); + + /// + /// Gets rating statistics for an instructor + /// + Task GetInstructorStatsAsync(string instructorName); + + /// + /// Updates an existing rating + /// + Task UpdateRatingAsync(Guid ratingId, UpdateInstructorRatingDto updateRatingDto); + + /// + /// Deletes a rating + /// + Task DeleteRatingAsync(Guid ratingId); + + /// + /// Gets a rating by student and course + /// + Task GetRatingByStudentAndCourseAsync(Guid studentId, Guid courseId); +} diff --git a/api/CourseRegistration.Application/Mappings/MappingProfile.cs b/api/CourseRegistration.Application/Mappings/MappingProfile.cs index 10be3ee..bdf158a 100644 --- a/api/CourseRegistration.Application/Mappings/MappingProfile.cs +++ b/api/CourseRegistration.Application/Mappings/MappingProfile.cs @@ -69,5 +69,27 @@ public MappingProfile() .ForMember(dest => dest.RegistrationDate, opt => opt.Ignore()) .ForMember(dest => dest.Student, opt => opt.Ignore()) .ForMember(dest => dest.Course, opt => opt.Ignore()); + + // InstructorRating mappings + CreateMap() + .ForMember(dest => dest.CourseName, opt => opt.MapFrom(src => src.Course.CourseName)) + .ForMember(dest => dest.InstructorName, opt => opt.MapFrom(src => src.Course.InstructorName)) + .ForMember(dest => dest.StudentName, opt => opt.MapFrom(src => src.Student.FullName)); + + CreateMap() + .ForMember(dest => dest.RatingId, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()) + .ForMember(dest => dest.UpdatedAt, opt => opt.Ignore()) + .ForMember(dest => dest.Course, opt => opt.Ignore()) + .ForMember(dest => dest.Student, opt => opt.Ignore()); + + CreateMap() + .ForMember(dest => dest.RatingId, opt => opt.Ignore()) + .ForMember(dest => dest.CourseId, opt => opt.Ignore()) + .ForMember(dest => dest.StudentId, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedAt, opt => opt.Ignore()) + .ForMember(dest => dest.UpdatedAt, opt => opt.Ignore()) + .ForMember(dest => dest.Course, opt => opt.Ignore()) + .ForMember(dest => dest.Student, opt => opt.Ignore()); } } \ No newline at end of file diff --git a/api/CourseRegistration.Application/Services/InstructorRatingService.cs b/api/CourseRegistration.Application/Services/InstructorRatingService.cs new file mode 100644 index 0000000..7b983ab --- /dev/null +++ b/api/CourseRegistration.Application/Services/InstructorRatingService.cs @@ -0,0 +1,180 @@ +using AutoMapper; +using CourseRegistration.Application.DTOs; +using CourseRegistration.Application.Interfaces; +using CourseRegistration.Domain.Entities; +using CourseRegistration.Domain.Interfaces; + +namespace CourseRegistration.Application.Services; + +/// +/// Service implementation for instructor rating operations +/// +public class InstructorRatingService : IInstructorRatingService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + /// + /// Initializes a new instance of the InstructorRatingService + /// + public InstructorRatingService(IUnitOfWork unitOfWork, IMapper mapper) + { + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + /// + /// Creates a new instructor rating + /// + public async Task CreateRatingAsync(CreateInstructorRatingDto createRatingDto) + { + // Validate that the course exists + var course = await _unitOfWork.Courses.GetByIdAsync(createRatingDto.CourseId); + if (course == null) + { + throw new InvalidOperationException("Course not found."); + } + + // Validate that the student exists + var student = await _unitOfWork.Students.GetByIdAsync(createRatingDto.StudentId); + if (student == null) + { + throw new InvalidOperationException("Student not found."); + } + + // Check if student has already rated this course + var existingRating = await _unitOfWork.InstructorRatings + .GetByStudentAndCourseAsync(createRatingDto.StudentId, createRatingDto.CourseId); + + if (existingRating != null) + { + throw new InvalidOperationException("Student has already rated this course. Use update instead."); + } + + // Validate that student is enrolled in the course + var isEnrolled = await _unitOfWork.Registrations + .IsStudentRegisteredForCourseAsync(createRatingDto.StudentId, createRatingDto.CourseId); + + if (!isEnrolled) + { + throw new InvalidOperationException("Student must be enrolled in the course to submit a rating."); + } + + var rating = _mapper.Map(createRatingDto); + await _unitOfWork.InstructorRatings.AddAsync(rating); + await _unitOfWork.SaveChangesAsync(); + + // Retrieve with details for response + var ratingWithDetails = await _unitOfWork.InstructorRatings.GetWithDetailsAsync(rating.RatingId); + return _mapper.Map(ratingWithDetails); + } + + /// + /// Gets a rating by ID + /// + public async Task GetRatingByIdAsync(Guid ratingId) + { + var rating = await _unitOfWork.InstructorRatings.GetWithDetailsAsync(ratingId); + return rating != null ? _mapper.Map(rating) : null; + } + + /// + /// Gets all ratings for a specific course + /// + public async Task> GetRatingsByCourseIdAsync(Guid courseId) + { + var ratings = await _unitOfWork.InstructorRatings.GetByCourseIdAsync(courseId); + return _mapper.Map>(ratings); + } + + /// + /// Gets all ratings submitted by a specific student + /// + public async Task> GetRatingsByStudentIdAsync(Guid studentId) + { + var ratings = await _unitOfWork.InstructorRatings.GetByStudentIdAsync(studentId); + return _mapper.Map>(ratings); + } + + /// + /// Gets all ratings for a specific instructor + /// + public async Task> GetRatingsByInstructorNameAsync(string instructorName) + { + var ratings = await _unitOfWork.InstructorRatings.GetByInstructorNameAsync(instructorName); + return _mapper.Map>(ratings); + } + + /// + /// Gets rating statistics for an instructor + /// + public async Task GetInstructorStatsAsync(string instructorName) + { + var ratings = await _unitOfWork.InstructorRatings.GetByInstructorNameAsync(instructorName); + var ratingsList = ratings.ToList(); + + if (!ratingsList.Any()) + { + return null; + } + + var courses = await _unitOfWork.Courses.GetCoursesByInstructorAsync(instructorName); + + return new InstructorRatingStatsDto + { + InstructorName = instructorName, + AverageRating = ratingsList.Average(r => r.Rating), + TotalRatings = ratingsList.Count, + FiveStarCount = ratingsList.Count(r => r.Rating == 5), + FourStarCount = ratingsList.Count(r => r.Rating == 4), + ThreeStarCount = ratingsList.Count(r => r.Rating == 3), + TwoStarCount = ratingsList.Count(r => r.Rating == 2), + OneStarCount = ratingsList.Count(r => r.Rating == 1), + CourseIds = courses.Select(c => c.CourseId).ToList() + }; + } + + /// + /// Updates an existing rating + /// + public async Task UpdateRatingAsync(Guid ratingId, UpdateInstructorRatingDto updateRatingDto) + { + var existingRating = await _unitOfWork.InstructorRatings.GetByIdAsync(ratingId); + if (existingRating == null) + { + return null; + } + + _mapper.Map(updateRatingDto, existingRating); + _unitOfWork.InstructorRatings.Update(existingRating); + await _unitOfWork.SaveChangesAsync(); + + var ratingWithDetails = await _unitOfWork.InstructorRatings.GetWithDetailsAsync(ratingId); + return _mapper.Map(ratingWithDetails); + } + + /// + /// Deletes a rating + /// + public async Task DeleteRatingAsync(Guid ratingId) + { + var rating = await _unitOfWork.InstructorRatings.GetByIdAsync(ratingId); + if (rating == null) + { + return false; + } + + _unitOfWork.InstructorRatings.Remove(rating); + await _unitOfWork.SaveChangesAsync(); + return true; + } + + /// + /// Gets a rating by student and course + /// + public async Task GetRatingByStudentAndCourseAsync(Guid studentId, Guid courseId) + { + var rating = await _unitOfWork.InstructorRatings.GetByStudentAndCourseAsync(studentId, courseId); + return rating != null ? _mapper.Map(rating) : null; + } +} diff --git a/api/CourseRegistration.Domain/Entities/Course.cs b/api/CourseRegistration.Domain/Entities/Course.cs index dc3f941..0c9cafb 100644 --- a/api/CourseRegistration.Domain/Entities/Course.cs +++ b/api/CourseRegistration.Domain/Entities/Course.cs @@ -73,9 +73,28 @@ public class Course /// public virtual ICollection Registrations { get; set; } = new List(); + /// + /// Navigation property for instructor ratings + /// + public virtual ICollection InstructorRatings { get; set; } = new List(); + /// /// Computed property for current enrollment count /// [NotMapped] public int CurrentEnrollment => Registrations?.Count(r => r.Status == Enums.RegistrationStatus.Confirmed) ?? 0; + + /// + /// Computed property for average instructor rating + /// + [NotMapped] + public double? AverageRating => InstructorRatings?.Any() == true + ? InstructorRatings.Average(r => r.Rating) + : null; + + /// + /// Computed property for total number of ratings + /// + [NotMapped] + public int RatingCount => InstructorRatings?.Count ?? 0; } \ No newline at end of file diff --git a/api/CourseRegistration.Domain/Entities/InstructorRating.cs b/api/CourseRegistration.Domain/Entities/InstructorRating.cs new file mode 100644 index 0000000..e1ed6cb --- /dev/null +++ b/api/CourseRegistration.Domain/Entities/InstructorRating.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CourseRegistration.Domain.Entities; + +/// +/// Represents a student's rating and review of an instructor for a specific course +/// +public class InstructorRating +{ + /// + /// Unique identifier for the rating + /// + [Key] + public Guid RatingId { get; set; } = Guid.NewGuid(); + + /// + /// Foreign key to the course + /// + [Required] + public Guid CourseId { get; set; } + + /// + /// Foreign key to the student who submitted the rating + /// + [Required] + public Guid StudentId { get; set; } + + /// + /// Rating value (1-5 scale) + /// + [Required] + [Range(1, 5)] + public int Rating { get; set; } + + /// + /// Optional comment/review text + /// + [MaxLength(1000)] + public string? Comment { get; set; } + + /// + /// Date when the rating was created + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Date when the rating was last updated + /// + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Navigation property to the course + /// + [ForeignKey(nameof(CourseId))] + public virtual Course Course { get; set; } = null!; + + /// + /// Navigation property to the student + /// + [ForeignKey(nameof(StudentId))] + public virtual Student Student { get; set; } = null!; +} diff --git a/api/CourseRegistration.Domain/Entities/Student.cs b/api/CourseRegistration.Domain/Entities/Student.cs index ee4a5b1..ffa5cee 100644 --- a/api/CourseRegistration.Domain/Entities/Student.cs +++ b/api/CourseRegistration.Domain/Entities/Student.cs @@ -69,6 +69,11 @@ public class Student /// public virtual ICollection Registrations { get; set; } = new List(); + /// + /// Navigation property for student's instructor ratings + /// + public virtual ICollection InstructorRatings { get; set; } = new List(); + /// /// Computed property for student's full name /// diff --git a/api/CourseRegistration.Domain/Interfaces/IInstructorRatingRepository.cs b/api/CourseRegistration.Domain/Interfaces/IInstructorRatingRepository.cs new file mode 100644 index 0000000..d3ca9b4 --- /dev/null +++ b/api/CourseRegistration.Domain/Interfaces/IInstructorRatingRepository.cs @@ -0,0 +1,44 @@ +using CourseRegistration.Domain.Entities; + +namespace CourseRegistration.Domain.Interfaces; + +/// +/// InstructorRating repository interface with specific operations +/// +public interface IInstructorRatingRepository : IRepository +{ + /// + /// Gets all ratings for a specific course + /// + Task> GetByCourseIdAsync(Guid courseId); + + /// + /// Gets all ratings submitted by a specific student + /// + Task> GetByStudentIdAsync(Guid studentId); + + /// + /// Gets all ratings for a specific instructor across all courses + /// + Task> GetByInstructorNameAsync(string instructorName); + + /// + /// Gets a specific rating by student and course + /// + Task GetByStudentAndCourseAsync(Guid studentId, Guid courseId); + + /// + /// Checks if a student has already rated a course + /// + Task HasStudentRatedCourseAsync(Guid studentId, Guid courseId); + + /// + /// Gets average rating for an instructor + /// + Task GetAverageRatingForInstructorAsync(string instructorName); + + /// + /// Gets rating with student and course details + /// + Task GetWithDetailsAsync(Guid ratingId); +} diff --git a/api/CourseRegistration.Domain/Interfaces/IUnitOfWork.cs b/api/CourseRegistration.Domain/Interfaces/IUnitOfWork.cs index c56d8f4..72b409a 100644 --- a/api/CourseRegistration.Domain/Interfaces/IUnitOfWork.cs +++ b/api/CourseRegistration.Domain/Interfaces/IUnitOfWork.cs @@ -20,6 +20,11 @@ public interface IUnitOfWork : IDisposable /// IRegistrationRepository Registrations { get; } + /// + /// InstructorRating repository + /// + IInstructorRatingRepository InstructorRatings { get; } + /// /// Saves all changes made in this unit of work asynchronously /// diff --git a/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs index 7491da8..afa0e65 100644 --- a/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs +++ b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContext.cs @@ -32,6 +32,11 @@ public CourseRegistrationDbContext(DbContextOptions /// public DbSet Registrations { get; set; } = null!; + /// + /// InstructorRatings DbSet + /// + public DbSet InstructorRatings { get; set; } = null!; + /// /// Configures the model relationships and constraints /// @@ -108,6 +113,33 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(r => r.CourseId) .OnDelete(DeleteBehavior.Cascade); }); + + // Configure InstructorRating entity + modelBuilder.Entity(entity => + { + entity.HasKey(ir => ir.RatingId); + entity.Property(ir => ir.CourseId).IsRequired(); + entity.Property(ir => ir.StudentId).IsRequired(); + entity.Property(ir => ir.Rating).IsRequired(); + entity.Property(ir => ir.Comment).HasMaxLength(1000); + entity.Property(ir => ir.CreatedAt).IsRequired(); + entity.Property(ir => ir.UpdatedAt).IsRequired(); + + // Create unique constraint to prevent duplicate ratings per student per course + entity.HasIndex(ir => new { ir.StudentId, ir.CourseId }) + .IsUnique(); + + // Configure relationships + entity.HasOne(ir => ir.Student) + .WithMany(s => s.InstructorRatings) + .HasForeignKey(ir => ir.StudentId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(ir => ir.Course) + .WithMany(c => c.InstructorRatings) + .HasForeignKey(ir => ir.CourseId) + .OnDelete(DeleteBehavior.Cascade); + }); } /// @@ -156,6 +188,14 @@ private void UpdateTimestamps() } course.UpdatedAt = currentTime; } + else if (entry.Entity is InstructorRating rating) + { + if (entry.State == EntityState.Added) + { + rating.CreatedAt = currentTime; + } + rating.UpdatedAt = currentTime; + } } } } \ No newline at end of file diff --git a/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContextFactory.cs b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContextFactory.cs new file mode 100644 index 0000000..0269dca --- /dev/null +++ b/api/CourseRegistration.Infrastructure/Data/CourseRegistrationDbContextFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace CourseRegistration.Infrastructure.Data; + +/// +/// Design-time factory for CourseRegistrationDbContext to enable migrations +/// +public class CourseRegistrationDbContextFactory : IDesignTimeDbContextFactory +{ + public CourseRegistrationDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + // Use in-memory database for design-time operations + optionsBuilder.UseInMemoryDatabase("CourseRegistrationDb"); + + return new CourseRegistrationDbContext(optionsBuilder.Options); + } +} diff --git a/api/CourseRegistration.Infrastructure/Repositories/InstructorRatingRepository.cs b/api/CourseRegistration.Infrastructure/Repositories/InstructorRatingRepository.cs new file mode 100644 index 0000000..ca96be5 --- /dev/null +++ b/api/CourseRegistration.Infrastructure/Repositories/InstructorRatingRepository.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore; +using CourseRegistration.Domain.Entities; +using CourseRegistration.Domain.Interfaces; +using CourseRegistration.Infrastructure.Data; + +namespace CourseRegistration.Infrastructure.Repositories; + +/// +/// InstructorRating repository implementation with specific operations +/// +public class InstructorRatingRepository : Repository, IInstructorRatingRepository +{ + /// + /// Initializes a new instance of the InstructorRatingRepository + /// + /// Database context + public InstructorRatingRepository(CourseRegistrationDbContext context) : base(context) + { + } + + /// + /// Gets all ratings for a specific course + /// + public async Task> GetByCourseIdAsync(Guid courseId) + { + return await _dbSet + .Include(ir => ir.Student) + .Include(ir => ir.Course) + .Where(ir => ir.CourseId == courseId) + .OrderByDescending(ir => ir.CreatedAt) + .ToListAsync(); + } + + /// + /// Gets all ratings submitted by a specific student + /// + public async Task> GetByStudentIdAsync(Guid studentId) + { + return await _dbSet + .Include(ir => ir.Course) + .Include(ir => ir.Student) + .Where(ir => ir.StudentId == studentId) + .OrderByDescending(ir => ir.CreatedAt) + .ToListAsync(); + } + + /// + /// Gets all ratings for a specific instructor across all courses + /// + public async Task> GetByInstructorNameAsync(string instructorName) + { + return await _dbSet + .Include(ir => ir.Course) + .Include(ir => ir.Student) + .Where(ir => ir.Course.InstructorName == instructorName) + .OrderByDescending(ir => ir.CreatedAt) + .ToListAsync(); + } + + /// + /// Gets a specific rating by student and course + /// + public async Task GetByStudentAndCourseAsync(Guid studentId, Guid courseId) + { + return await _dbSet + .Include(ir => ir.Course) + .Include(ir => ir.Student) + .FirstOrDefaultAsync(ir => ir.StudentId == studentId && ir.CourseId == courseId); + } + + /// + /// Checks if a student has already rated a course + /// + public async Task HasStudentRatedCourseAsync(Guid studentId, Guid courseId) + { + return await _dbSet + .AnyAsync(ir => ir.StudentId == studentId && ir.CourseId == courseId); + } + + /// + /// Gets average rating for an instructor + /// + public async Task GetAverageRatingForInstructorAsync(string instructorName) + { + var ratings = await _dbSet + .Include(ir => ir.Course) + .Where(ir => ir.Course.InstructorName == instructorName) + .ToListAsync(); + + return ratings.Any() ? ratings.Average(ir => ir.Rating) : null; + } + + /// + /// Gets rating with student and course details + /// + public async Task GetWithDetailsAsync(Guid ratingId) + { + return await _dbSet + .Include(ir => ir.Course) + .Include(ir => ir.Student) + .FirstOrDefaultAsync(ir => ir.RatingId == ratingId); + } +} diff --git a/api/CourseRegistration.Infrastructure/Repositories/UnitOfWork.cs b/api/CourseRegistration.Infrastructure/Repositories/UnitOfWork.cs index 63a4e8d..afea56e 100644 --- a/api/CourseRegistration.Infrastructure/Repositories/UnitOfWork.cs +++ b/api/CourseRegistration.Infrastructure/Repositories/UnitOfWork.cs @@ -18,6 +18,7 @@ public class UnitOfWork : IUnitOfWork private IStudentRepository? _students; private ICourseRepository? _courses; private IRegistrationRepository? _registrations; + private IInstructorRatingRepository? _instructorRatings; /// /// Initializes a new instance of the UnitOfWork @@ -64,6 +65,18 @@ public IRegistrationRepository Registrations } } + /// + /// InstructorRating repository + /// + public IInstructorRatingRepository InstructorRatings + { + get + { + _instructorRatings ??= new InstructorRatingRepository(_context); + return _instructorRatings; + } + } + /// /// Saves all changes made in this unit of work asynchronously /// From 6cd826ccc03dfea4184e8e51b548f355f752a2c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:43:29 +0000 Subject: [PATCH 3/4] Add unit tests for InstructorRatingService and verify API endpoints Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068 --- .../CourseRegistration.Tests.csproj | 1 + .../Services/InstructorRatingServiceTests.cs | 540 ++++++++++++++++++ 2 files changed, 541 insertions(+) create mode 100644 api/CourseRegistration.Tests/Services/InstructorRatingServiceTests.cs diff --git a/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj b/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj index 17b82fa..f7a658d 100644 --- a/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj +++ b/api/CourseRegistration.Tests/CourseRegistration.Tests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/api/CourseRegistration.Tests/Services/InstructorRatingServiceTests.cs b/api/CourseRegistration.Tests/Services/InstructorRatingServiceTests.cs new file mode 100644 index 0000000..2ddd739 --- /dev/null +++ b/api/CourseRegistration.Tests/Services/InstructorRatingServiceTests.cs @@ -0,0 +1,540 @@ +using Xunit; +using Moq; +using AutoMapper; +using CourseRegistration.Application.Services; +using CourseRegistration.Application.DTOs; +using CourseRegistration.Application.Mappings; +using CourseRegistration.Domain.Entities; +using CourseRegistration.Domain.Interfaces; +using CourseRegistration.Domain.Enums; + +namespace CourseRegistration.Tests.Services; + +/// +/// Unit tests for InstructorRatingService +/// +public class InstructorRatingServiceTests +{ + private readonly Mock _mockUnitOfWork; + private readonly IMapper _mapper; + private readonly InstructorRatingService _service; + private readonly Mock _mockRatingRepository; + private readonly Mock _mockCourseRepository; + private readonly Mock _mockStudentRepository; + private readonly Mock _mockRegistrationRepository; + + public InstructorRatingServiceTests() + { + // Setup AutoMapper + var config = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + }); + _mapper = config.CreateMapper(); + + // Setup mocks + _mockUnitOfWork = new Mock(); + _mockRatingRepository = new Mock(); + _mockCourseRepository = new Mock(); + _mockStudentRepository = new Mock(); + _mockRegistrationRepository = new Mock(); + + _mockUnitOfWork.Setup(u => u.InstructorRatings).Returns(_mockRatingRepository.Object); + _mockUnitOfWork.Setup(u => u.Courses).Returns(_mockCourseRepository.Object); + _mockUnitOfWork.Setup(u => u.Students).Returns(_mockStudentRepository.Object); + _mockUnitOfWork.Setup(u => u.Registrations).Returns(_mockRegistrationRepository.Object); + + _service = new InstructorRatingService(_mockUnitOfWork.Object, _mapper); + } + + [Fact] + public async Task CreateRatingAsync_ValidData_ReturnsRatingDto() + { + // Arrange + var courseId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11", + IsActive = true + }; + var student = new Student + { + StudentId = studentId, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@test.com", + DateOfBirth = DateTime.UtcNow.AddYears(-20), + IsActive = true + }; + + var createDto = new CreateInstructorRatingDto + { + CourseId = courseId, + StudentId = studentId, + Rating = 5, + Comment = "Excellent instructor!" + }; + + _mockCourseRepository.Setup(r => r.GetByIdAsync(courseId)).ReturnsAsync(course); + _mockStudentRepository.Setup(r => r.GetByIdAsync(studentId)).ReturnsAsync(student); + _mockRatingRepository.Setup(r => r.GetByStudentAndCourseAsync(studentId, courseId)) + .ReturnsAsync((InstructorRating?)null); + _mockRegistrationRepository.Setup(r => r.IsStudentRegisteredForCourseAsync(studentId, courseId)) + .ReturnsAsync(true); + _mockRatingRepository.Setup(r => r.AddAsync(It.IsAny())) + .ReturnsAsync((InstructorRating rating) => rating); + _mockUnitOfWork.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1); + _mockRatingRepository.Setup(r => r.GetWithDetailsAsync(It.IsAny())) + .ReturnsAsync((Guid id) => new InstructorRating + { + RatingId = id, + CourseId = courseId, + StudentId = studentId, + Rating = 5, + Comment = "Excellent instructor!", + Course = course, + Student = student + }); + + // Act + var result = await _service.CreateRatingAsync(createDto); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Rating); + Assert.Equal("Excellent instructor!", result.Comment); + Assert.Equal("Test Course", result.CourseName); + Assert.Equal("Dr. Smith", result.InstructorName); + Assert.Equal("John Doe", result.StudentName); + _mockRatingRepository.Verify(r => r.AddAsync(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once); + } + + [Fact] + public async Task CreateRatingAsync_CourseNotFound_ThrowsInvalidOperationException() + { + // Arrange + var createDto = new CreateInstructorRatingDto + { + CourseId = Guid.NewGuid(), + StudentId = Guid.NewGuid(), + Rating = 5, + Comment = "Great!" + }; + + _mockCourseRepository.Setup(r => r.GetByIdAsync(createDto.CourseId)) + .ReturnsAsync((Course?)null); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _service.CreateRatingAsync(createDto)); + } + + [Fact] + public async Task CreateRatingAsync_StudentNotFound_ThrowsInvalidOperationException() + { + // Arrange + var courseId = Guid.NewGuid(); + var createDto = new CreateInstructorRatingDto + { + CourseId = courseId, + StudentId = Guid.NewGuid(), + Rating = 5, + Comment = "Great!" + }; + + var course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11", + IsActive = true + }; + + _mockCourseRepository.Setup(r => r.GetByIdAsync(courseId)).ReturnsAsync(course); + _mockStudentRepository.Setup(r => r.GetByIdAsync(createDto.StudentId)) + .ReturnsAsync((Student?)null); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _service.CreateRatingAsync(createDto)); + } + + [Fact] + public async Task CreateRatingAsync_StudentAlreadyRated_ThrowsInvalidOperationException() + { + // Arrange + var courseId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11", + IsActive = true + }; + var student = new Student + { + StudentId = studentId, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@test.com", + DateOfBirth = DateTime.UtcNow.AddYears(-20), + IsActive = true + }; + + var createDto = new CreateInstructorRatingDto + { + CourseId = courseId, + StudentId = studentId, + Rating = 5, + Comment = "Great!" + }; + + var existingRating = new InstructorRating + { + RatingId = Guid.NewGuid(), + CourseId = courseId, + StudentId = studentId, + Rating = 4, + Comment = "Good" + }; + + _mockCourseRepository.Setup(r => r.GetByIdAsync(courseId)).ReturnsAsync(course); + _mockStudentRepository.Setup(r => r.GetByIdAsync(studentId)).ReturnsAsync(student); + _mockRatingRepository.Setup(r => r.GetByStudentAndCourseAsync(studentId, courseId)) + .ReturnsAsync(existingRating); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _service.CreateRatingAsync(createDto)); + } + + [Fact] + public async Task CreateRatingAsync_StudentNotEnrolled_ThrowsInvalidOperationException() + { + // Arrange + var courseId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + var course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11", + IsActive = true + }; + var student = new Student + { + StudentId = studentId, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@test.com", + DateOfBirth = DateTime.UtcNow.AddYears(-20), + IsActive = true + }; + + var createDto = new CreateInstructorRatingDto + { + CourseId = courseId, + StudentId = studentId, + Rating = 5, + Comment = "Great!" + }; + + _mockCourseRepository.Setup(r => r.GetByIdAsync(courseId)).ReturnsAsync(course); + _mockStudentRepository.Setup(r => r.GetByIdAsync(studentId)).ReturnsAsync(student); + _mockRatingRepository.Setup(r => r.GetByStudentAndCourseAsync(studentId, courseId)) + .ReturnsAsync((InstructorRating?)null); + _mockRegistrationRepository.Setup(r => r.IsStudentRegisteredForCourseAsync(studentId, courseId)) + .ReturnsAsync(false); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _service.CreateRatingAsync(createDto)); + } + + [Fact] + public async Task GetRatingByIdAsync_ValidId_ReturnsRatingDto() + { + // Arrange + var ratingId = Guid.NewGuid(); + var courseId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + + var rating = new InstructorRating + { + RatingId = ratingId, + CourseId = courseId, + StudentId = studentId, + Rating = 4, + Comment = "Good course", + Course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11" + }, + Student = new Student + { + StudentId = studentId, + FirstName = "Jane", + LastName = "Smith", + Email = "jane.smith@test.com", + DateOfBirth = DateTime.UtcNow.AddYears(-22) + } + }; + + _mockRatingRepository.Setup(r => r.GetWithDetailsAsync(ratingId)).ReturnsAsync(rating); + + // Act + var result = await _service.GetRatingByIdAsync(ratingId); + + // Assert + Assert.NotNull(result); + Assert.Equal(ratingId, result.RatingId); + Assert.Equal(4, result.Rating); + Assert.Equal("Good course", result.Comment); + Assert.Equal("Test Course", result.CourseName); + Assert.Equal("Dr. Smith", result.InstructorName); + Assert.Equal("Jane Smith", result.StudentName); + } + + [Fact] + public async Task GetRatingByIdAsync_InvalidId_ReturnsNull() + { + // Arrange + var ratingId = Guid.NewGuid(); + _mockRatingRepository.Setup(r => r.GetWithDetailsAsync(ratingId)) + .ReturnsAsync((InstructorRating?)null); + + // Act + var result = await _service.GetRatingByIdAsync(ratingId); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task UpdateRatingAsync_ValidData_ReturnsUpdatedRatingDto() + { + // Arrange + var ratingId = Guid.NewGuid(); + var courseId = Guid.NewGuid(); + var studentId = Guid.NewGuid(); + + var existingRating = new InstructorRating + { + RatingId = ratingId, + CourseId = courseId, + StudentId = studentId, + Rating = 4, + Comment = "Good" + }; + + var updateDto = new UpdateInstructorRatingDto + { + Rating = 5, + Comment = "Excellent!" + }; + + var updatedRating = new InstructorRating + { + RatingId = ratingId, + CourseId = courseId, + StudentId = studentId, + Rating = 5, + Comment = "Excellent!", + Course = new Course + { + CourseId = courseId, + CourseName = "Test Course", + InstructorName = "Dr. Smith", + StartDate = DateTime.UtcNow.AddDays(1), + EndDate = DateTime.UtcNow.AddDays(30), + Schedule = "MWF 10-11" + }, + Student = new Student + { + StudentId = studentId, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@test.com", + DateOfBirth = DateTime.UtcNow.AddYears(-20) + } + }; + + _mockRatingRepository.Setup(r => r.GetByIdAsync(ratingId)).ReturnsAsync(existingRating); + _mockRatingRepository.Setup(r => r.Update(It.IsAny())); + _mockUnitOfWork.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1); + _mockRatingRepository.Setup(r => r.GetWithDetailsAsync(ratingId)).ReturnsAsync(updatedRating); + + // Act + var result = await _service.UpdateRatingAsync(ratingId, updateDto); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Rating); + Assert.Equal("Excellent!", result.Comment); + _mockRatingRepository.Verify(r => r.Update(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once); + } + + [Fact] + public async Task UpdateRatingAsync_InvalidId_ReturnsNull() + { + // Arrange + var ratingId = Guid.NewGuid(); + var updateDto = new UpdateInstructorRatingDto + { + Rating = 5, + Comment = "Excellent!" + }; + + _mockRatingRepository.Setup(r => r.GetByIdAsync(ratingId)) + .ReturnsAsync((InstructorRating?)null); + + // Act + var result = await _service.UpdateRatingAsync(ratingId, updateDto); + + // Assert + Assert.Null(result); + _mockRatingRepository.Verify(r => r.Update(It.IsAny()), Times.Never); + } + + [Fact] + public async Task DeleteRatingAsync_ValidId_ReturnsTrue() + { + // Arrange + var ratingId = Guid.NewGuid(); + var rating = new InstructorRating + { + RatingId = ratingId, + CourseId = Guid.NewGuid(), + StudentId = Guid.NewGuid(), + Rating = 4, + Comment = "Good" + }; + + _mockRatingRepository.Setup(r => r.GetByIdAsync(ratingId)).ReturnsAsync(rating); + _mockRatingRepository.Setup(r => r.Remove(rating)); + _mockUnitOfWork.Setup(u => u.SaveChangesAsync()).ReturnsAsync(1); + + // Act + var result = await _service.DeleteRatingAsync(ratingId); + + // Assert + Assert.True(result); + _mockRatingRepository.Verify(r => r.Remove(rating), Times.Once); + _mockUnitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once); + } + + [Fact] + public async Task DeleteRatingAsync_InvalidId_ReturnsFalse() + { + // Arrange + var ratingId = Guid.NewGuid(); + _mockRatingRepository.Setup(r => r.GetByIdAsync(ratingId)) + .ReturnsAsync((InstructorRating?)null); + + // Act + var result = await _service.DeleteRatingAsync(ratingId); + + // Assert + Assert.False(result); + _mockRatingRepository.Verify(r => r.Remove(It.IsAny()), Times.Never); + } + + [Fact] + public async Task GetInstructorStatsAsync_ValidInstructor_ReturnsStats() + { + // Arrange + var instructorName = "Dr. Smith"; + var courseId1 = Guid.NewGuid(); + var courseId2 = Guid.NewGuid(); + + var ratings = new List + { + new InstructorRating + { + RatingId = Guid.NewGuid(), + CourseId = courseId1, + StudentId = Guid.NewGuid(), + Rating = 5, + Course = new Course { CourseId = courseId1, InstructorName = instructorName, CourseName = "Course1", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(30), Schedule = "MWF" } + }, + new InstructorRating + { + RatingId = Guid.NewGuid(), + CourseId = courseId2, + StudentId = Guid.NewGuid(), + Rating = 4, + Course = new Course { CourseId = courseId2, InstructorName = instructorName, CourseName = "Course2", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(30), Schedule = "TTh" } + }, + new InstructorRating + { + RatingId = Guid.NewGuid(), + CourseId = courseId1, + StudentId = Guid.NewGuid(), + Rating = 5, + Course = new Course { CourseId = courseId1, InstructorName = instructorName, CourseName = "Course1", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(30), Schedule = "MWF" } + } + }; + + var courses = new List + { + new Course { CourseId = courseId1, InstructorName = instructorName, CourseName = "Course1", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(30), Schedule = "MWF" }, + new Course { CourseId = courseId2, InstructorName = instructorName, CourseName = "Course2", StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddDays(30), Schedule = "TTh" } + }; + + _mockRatingRepository.Setup(r => r.GetByInstructorNameAsync(instructorName)) + .ReturnsAsync(ratings); + _mockCourseRepository.Setup(r => r.GetCoursesByInstructorAsync(instructorName)) + .ReturnsAsync(courses); + + // Act + var result = await _service.GetInstructorStatsAsync(instructorName); + + // Assert + Assert.NotNull(result); + Assert.Equal(instructorName, result.InstructorName); + Assert.Equal(4.67, Math.Round(result.AverageRating, 2)); // (5 + 4 + 5) / 3 = 4.67 + Assert.Equal(3, result.TotalRatings); + Assert.Equal(2, result.FiveStarCount); + Assert.Equal(1, result.FourStarCount); + Assert.Equal(0, result.ThreeStarCount); + Assert.Equal(2, result.CourseIds.Count); + } + + [Fact] + public async Task GetInstructorStatsAsync_NoRatings_ReturnsNull() + { + // Arrange + var instructorName = "Dr. Nobody"; + _mockRatingRepository.Setup(r => r.GetByInstructorNameAsync(instructorName)) + .ReturnsAsync(new List()); + + // Act + var result = await _service.GetInstructorStatsAsync(instructorName); + + // Assert + Assert.Null(result); + } +} From 903b3ee0f6bb99104059210ce92e7cd8fb99539b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 05:46:57 +0000 Subject: [PATCH 4/4] Address code review feedback - remove computed properties to prevent N+1 queries Co-authored-by: kavyashri-as <213833080+kavyashri-as@users.noreply.github.com> Agent-Logs-Url: https://github.com/CanarysPlayground/CourseApplication/sessions/e5aba3b0-3bdb-4857-9b2a-57cfffb98068 --- api/CourseRegistration.Domain/Entities/Course.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/api/CourseRegistration.Domain/Entities/Course.cs b/api/CourseRegistration.Domain/Entities/Course.cs index 0c9cafb..d7571bd 100644 --- a/api/CourseRegistration.Domain/Entities/Course.cs +++ b/api/CourseRegistration.Domain/Entities/Course.cs @@ -83,18 +83,4 @@ public class Course /// [NotMapped] public int CurrentEnrollment => Registrations?.Count(r => r.Status == Enums.RegistrationStatus.Confirmed) ?? 0; - - /// - /// Computed property for average instructor rating - /// - [NotMapped] - public double? AverageRating => InstructorRatings?.Any() == true - ? InstructorRatings.Average(r => r.Rating) - : null; - - /// - /// Computed property for total number of ratings - /// - [NotMapped] - public int RatingCount => InstructorRatings?.Count ?? 0; } \ No newline at end of file