-
Notifications
You must be signed in to change notification settings - Fork 2
Add certificate PDF download to Certificate page #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Copilot
wants to merge
2
commits into
main
Choose a base branch
from
copilot/add-certificate-download-functionality
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
api/CourseRegistration.API/Controllers/CertificatesController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using CourseRegistration.Application.DTOs; | ||
| using CourseRegistration.Application.Services; | ||
|
|
||
| namespace CourseRegistration.API.Controllers; | ||
|
|
||
| /// <summary> | ||
| /// Controller for certificate operations | ||
| /// </summary> | ||
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Produces("application/json")] | ||
| public class CertificatesController : ControllerBase | ||
| { | ||
| private readonly ICertificateService _certificateService; | ||
| private readonly ILogger<CertificatesController> _logger; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the CertificatesController | ||
| /// </summary> | ||
| public CertificatesController(ICertificateService certificateService, ILogger<CertificatesController> logger) | ||
| { | ||
| _certificateService = certificateService ?? throw new ArgumentNullException(nameof(certificateService)); | ||
| _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets a certificate by ID | ||
| /// </summary> | ||
| /// <param name="id">Certificate ID</param> | ||
| /// <returns>Certificate details</returns> | ||
| [HttpGet("{id:guid}")] | ||
| [ProducesResponseType(typeof(CertificateDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| public async Task<ActionResult<CertificateDto>> GetCertificate(Guid id) | ||
| { | ||
| _logger.LogInformation("Getting certificate with ID: {CertificateId}", id); | ||
|
|
||
| var certificate = await _certificateService.GetCertificateByIdAsync(id); | ||
| if (certificate == null) | ||
| { | ||
| return NotFound(new { message = "Certificate not found" }); | ||
| } | ||
|
|
||
| return Ok(certificate); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Searches certificates by student name | ||
| /// </summary> | ||
| /// <param name="studentName">Student name to search for</param> | ||
| /// <returns>List of matching certificates</returns> | ||
| [HttpGet("search")] | ||
| [ProducesResponseType(typeof(IEnumerable<CertificateDto>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<ActionResult<IEnumerable<CertificateDto>>> SearchCertificates( | ||
| [FromQuery] string studentName = "") | ||
| { | ||
| _logger.LogInformation("Searching certificates for student name: {StudentName}", studentName); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(studentName)) | ||
| { | ||
| return BadRequest(new { message = "Student name is required" }); | ||
| } | ||
|
|
||
| var certificates = await _certificateService.GetCertificatesByStudentNameAsync(studentName); | ||
| return Ok(certificates); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Downloads a certificate as a PDF file | ||
| /// </summary> | ||
| /// <param name="certificateId">Certificate ID</param> | ||
| /// <returns>PDF file</returns> | ||
| [HttpGet("{certificateId:guid}/download")] | ||
| [Produces("application/pdf")] | ||
| [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
| public async Task<IActionResult> DownloadCertificate(Guid certificateId) | ||
| { | ||
| _logger.LogInformation("Download requested for certificate ID: {CertificateId}", certificateId); | ||
|
|
||
| if (certificateId == Guid.Empty) | ||
| { | ||
| return BadRequest(new { message = "Invalid certificate ID" }); | ||
| } | ||
|
|
||
| var pdfBytes = await _certificateService.DownloadCertificatePdfAsync(certificateId); | ||
| if (pdfBytes == null) | ||
| { | ||
| return NotFound(new { message = "Certificate not found" }); | ||
| } | ||
|
|
||
| var fileName = $"certificate-{certificateId}.pdf"; | ||
| return File(pdfBytes, "application/pdf", fileName); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
api/CourseRegistration.Tests/Services/CertificateServiceDownloadTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| using Xunit; | ||
| using CourseRegistration.Application.Services; | ||
| using CourseRegistration.Application.DTOs; | ||
| using CourseRegistration.Domain.Enums; | ||
|
|
||
| namespace CourseRegistration.Tests.Services; | ||
|
|
||
| /// <summary> | ||
| /// Unit tests for CertificateService PDF download functionality | ||
| /// </summary> | ||
| public class CertificateServiceDownloadTests | ||
| { | ||
| private readonly CertificateService _service; | ||
|
|
||
| public CertificateServiceDownloadTests() | ||
| { | ||
| _service = new CertificateService(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_WithValidId_ReturnsPdfBytes() | ||
| { | ||
| // Arrange | ||
| var newCert = await _service.CreateCertificateAsync(new CreateCertificateDto | ||
| { | ||
| StudentId = Guid.Parse("11111111-1111-1111-1111-111111111111"), | ||
| CourseId = Guid.Parse("33333333-3333-3333-3333-333333333333"), | ||
| FinalGrade = Grade.A, | ||
| Remarks = "Test certificate" | ||
| }); | ||
|
|
||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(newCert.CertificateId); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(pdfBytes); | ||
| Assert.True(pdfBytes.Length > 0); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_WithValidId_ReturnsPdfHeader() | ||
| { | ||
| // Arrange | ||
| var newCert = await _service.CreateCertificateAsync(new CreateCertificateDto | ||
| { | ||
| StudentId = Guid.Parse("11111111-1111-1111-1111-111111111111"), | ||
| CourseId = Guid.Parse("33333333-3333-3333-3333-333333333333"), | ||
| FinalGrade = Grade.B, | ||
| Remarks = "PDF header test" | ||
| }); | ||
|
|
||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(newCert.CertificateId); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(pdfBytes); | ||
| var header = System.Text.Encoding.Latin1.GetString(pdfBytes, 0, Math.Min(8, pdfBytes.Length)); | ||
| Assert.StartsWith("%PDF-", header); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_WithInvalidId_ReturnsNull() | ||
| { | ||
| // Arrange | ||
| var nonExistentId = Guid.NewGuid(); | ||
|
|
||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(nonExistentId); | ||
|
|
||
| // Assert | ||
| Assert.Null(pdfBytes); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_WithEmptyGuid_ReturnsNull() | ||
| { | ||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(Guid.Empty); | ||
|
|
||
| // Assert | ||
| Assert.Null(pdfBytes); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_PdfContainsCertificateDetails() | ||
| { | ||
| // Arrange | ||
| // StudentId 11111111... maps to "John Doe" and CourseId 33333333... maps to | ||
| // "Introduction to Programming" per CertificateService in-memory seed data. | ||
| var newCert = await _service.CreateCertificateAsync(new CreateCertificateDto | ||
| { | ||
| StudentId = Guid.Parse("11111111-1111-1111-1111-111111111111"), | ||
| CourseId = Guid.Parse("33333333-3333-3333-3333-333333333333"), | ||
| FinalGrade = Grade.A, | ||
| Remarks = "Content verification test" | ||
| }); | ||
|
|
||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(newCert.CertificateId); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(pdfBytes); | ||
| var pdfContent = System.Text.Encoding.Latin1.GetString(pdfBytes); | ||
| Assert.Contains("Certificate of Completion", pdfContent); | ||
| Assert.Contains("John Doe", pdfContent); | ||
| Assert.Contains("Introduction to Programming", pdfContent); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DownloadCertificatePdfAsync_PdfEndsWithEof() | ||
| { | ||
| // Arrange | ||
| var newCert = await _service.CreateCertificateAsync(new CreateCertificateDto | ||
| { | ||
| StudentId = Guid.Parse("22222222-2222-2222-2222-222222222222"), | ||
| CourseId = Guid.Parse("44444444-4444-4444-4444-444444444444"), | ||
| FinalGrade = Grade.B, | ||
| Remarks = "EOF test" | ||
| }); | ||
|
|
||
| // Act | ||
| var pdfBytes = await _service.DownloadCertificatePdfAsync(newCert.CertificateId); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(pdfBytes); | ||
| var pdfContent = System.Text.Encoding.Latin1.GetString(pdfBytes); | ||
| Assert.Contains("%%EOF", pdfContent); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Log entries created from user input High
Copilot Autofix
AI 11 days ago
In general, to fix this kind of issue, all user-provided data that is written into log messages should be sanitized to remove or encode characters that could alter the log structure (notably
\rand\n). For plain-text logs, removing newline characters is usually sufficient; for HTML logs, HTML-encoding should be used. Since this controller likely writes to plain-text/structured logs, the minimal non-breaking fix is to create a sanitized version ofstudentNamewith line breaks removed and use that sanitized variable in the log call, leaving the underlying business logic (GetCertificatesByStudentNameAsync) unchanged.Concretely, in
api/CourseRegistration.API/Controllers/CertificatesController.cs, within theSearchCertificatesaction, we’ll introduce a local variable (e.g.,sanitizedStudentName) that replaces any\rand\ncharacters with empty strings (or otherwise strips them) before passing it to_logger.LogInformation. The rest of the method will continue to use the originalstudentNamefor validation and searching to avoid altering functional behavior. No new using directives or external dependencies are needed; we can usestring.Replacefrom the BCL. The change is localized around line 59: add the sanitization line immediately before the log call and change the log call to use the sanitized variable.