Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/RESTful API/Controllers/ClientesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,19 @@ public async Task<ActionResult<ClienteResponseDto>> GetCliente(int id)
.Include(c => c.CodigoPostalCpNavigation)
.Include(c => c.LoginIdloginNavigation)
Comment on lines 100 to 101

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include(...) calls are unnecessary when you immediately project into a DTO (Select(new ClienteResponseDto { ... })). They add noise and can confuse future readers; EF will generate the required joins from the projection alone. Consider removing the Include calls for this query.

Suggested change
.Include(c => c.CodigoPostalCpNavigation)
.Include(c => c.LoginIdloginNavigation)

Copilot uses AI. Check for mistakes.
.Where(c => c.Idcliente == id)
.Select(c => new ClienteResponseDto { /* Mapping */ })
.Select(c => new ClienteResponseDto {
Idcliente = c.Idcliente,
NomeCliente = c.NomeCliente,
DataNascCliente = c.DataNascCliente,
NifCliente = c.Nifcliente,
RuaCliente = c.RuaCliente,
CodigoPostal = c.CodigoPostalCpNavigation != null ? c.CodigoPostalCpNavigation.Cp.ToString("0000000") : null,
Localidade = c.CodigoPostalCpNavigation != null ? c.CodigoPostalCpNavigation.Localidade : null,
Email = c.LoginIdloginNavigation != null ? c.LoginIdloginNavigation.Email : null,
Comment on lines +107 to +111

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodigoPostalCpNavigation.Cp.ToString("0000000") is inside an EF Core LINQ-to-Entities projection. The SQL Server provider generally cannot translate the ToString(string format) overload, which can cause this endpoint to throw at runtime when executed against the real database. Consider projecting the numeric CP value first and formatting it after materialization (or formatting in a separate step after FirstOrDefaultAsync).

Copilot uses AI. Check for mistakes.
ContactoC1 = c.ContactoC1,
ContactoC2 = c.ContactoC2,
EstadoValCc = c.EstadoValCc
})
.FirstOrDefaultAsync();

if (cliente == null) return NotFound();
Expand Down
204 changes: 204 additions & 0 deletions backend/Tests/CarXPress Unit Tests/ClientesControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RESTful_API.Controllers;
using RESTful_API.Models;
using Xunit;

namespace Unit_Tests
{
public class ClientesControllerTests
{
private PdsContext GetDbContextWithData()
{
var options = new DbContextOptionsBuilder<PdsContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var context = new PdsContext(options);
Comment on lines +19 to +22

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests use EF Core's InMemory provider. InMemory doesn't behave like SQL Server for query translation, so tests may pass even when production queries fail to translate (e.g., string formatting inside projections). Consider using SQLite in-memory for controller/unit tests that exercise LINQ queries, or add a targeted test that runs against a relational provider to catch translation issues.

Copilot uses AI. Check for mistakes.

var login1 = new Login { Idlogin = 1, Email = "test@test.com", HashPassword = "hashedpassword" };
var login2 = new Login { Idlogin = 2, Email = "test2@test.com", HashPassword = "hashedpassword2" };

var cp = new CodigoPostal { Cp = 1234567, Localidade = "Lisboa" };

var cliente1 = new Cliente
{
Idcliente = 1,
NomeCliente = "Joao",
Nifcliente = 123456789,
DataNascCliente = new DateTime(1990, 1, 1),
RuaCliente = "Rua A",
CodigoPostalCp = 1234567,
CodigoPostalCpNavigation = cp,
LoginIdlogin = 1,
LoginIdloginNavigation = login1,
ContactoC1 = 910000000,
EstadoValCc = true
};

var cliente2 = new Cliente
{
Idcliente = 2,
NomeCliente = "Maria",
Nifcliente = 987654321,
DataNascCliente = new DateTime(1995, 1, 1),
RuaCliente = "Rua B",
CodigoPostalCp = 1234567,
CodigoPostalCpNavigation = cp,
LoginIdlogin = 2,
LoginIdloginNavigation = login2,
ContactoC1 = 920000000,
EstadoValCc = false
};

context.Logins.AddRange(login1, login2);
context.CodigoPostals.Add(cp);
context.Clientes.AddRange(cliente1, cliente2);
context.SaveChanges();

return context;
}

[Fact]
public async Task GetCliente_ValidId_ReturnsCliente()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var result = await controller.GetCliente(1);

var actionResult = Assert.IsType<ActionResult<ClienteResponseDto>>(result);
var clienteResponse = Assert.IsType<ClienteResponseDto>(actionResult.Value);
Assert.Equal("Joao", clienteResponse.NomeCliente);
Assert.Equal(123456789, clienteResponse.NifCliente);
}

[Fact]
public async Task GetCliente_InvalidId_ReturnsNotFound()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var result = await controller.GetCliente(999);

Assert.IsType<NotFoundResult>(result.Result);
}

[Fact]
public async Task GetMe_ValidUser_ReturnsCliente()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, "1")
}, "mock"));
Comment on lines +92 to +101

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetMe is only tested for the happy path. This endpoint has explicit branches for invalid/missing NameIdentifier (Unauthorized) and missing cliente (NotFound) that aren't covered here; adding those cases would better lock in the controller behavior.

Copilot uses AI. Check for mistakes.

controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = user }
};

var result = await controller.GetMe();

var okResult = Assert.IsType<OkObjectResult>(result.Result);
var clienteResponse = Assert.IsType<ClienteResponseDto>(okResult.Value);
Assert.Equal("Joao", clienteResponse.NomeCliente);
}

[Fact]
public async Task GetClientesAdmin_ReturnsAllClientes()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, "1"),
new Claim("roleId", "3") // Admin role
}, "mock"));

Comment on lines +115 to +126

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetClientesAdmin is only tested for the admin happy path. Since the controller contains several authorization/validation branches (invalid token claims, claim mismatch vs DB, non-admin role), adding tests for the expected Unauthorized/Forbid outcomes would meaningfully improve coverage of the core logic in this action.

Copilot uses AI. Check for mistakes.
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = user }
};

var login1 = context.Logins.Find(1);
login1.TipoLoginIdtlogin = 3;
context.SaveChanges();

var result = await controller.GetClientesAdmin();

var actionResult = Assert.IsType<ActionResult<List<ClienteResponseDtoAdmin>>>(result);
var list = Assert.IsType<List<ClienteResponseDtoAdmin>>(actionResult.Value);
Assert.Equal(2, list.Count);
}

[Fact]
public async Task PostCliente_ValidCliente_ReturnsCreated()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var newLogin = new Login { Idlogin = 3, Email = "new@test.com", HashPassword = "pwd" };
context.Logins.Add(newLogin);
context.SaveChanges();

var clienteDto = new ClienteCreateDto
{
NomeCliente = "Carlos",
NifCliente = 111222333,
DataNascCliente = new DateTime(2000, 1, 1),
RuaCliente = "Rua C",
CodigoPostal = "1234567",
Localidade = "Lisboa",
LoginIdlogin = 3,
ContactoC1 = 930000000
};

var result = await controller.PostCliente(clienteDto);

var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(result.Result);
var createdCliente = Assert.IsType<ClienteResponseDto>(createdAtActionResult.Value);

Assert.Equal("Carlos", createdCliente.NomeCliente);
Assert.Equal("1234567", createdCliente.CodigoPostal);

var dbCliente = context.Clientes.FirstOrDefault(c => c.NomeCliente == "Carlos");
Assert.NotNull(dbCliente);
Assert.Equal(111222333, dbCliente.Nifcliente);
}

[Fact]
public async Task DeleteClienteAndAnonymizeLogin_ValidId_AnonymizesLogin()
{
var context = GetDbContextWithData();
var controller = new ClientesController(context);

var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, "1"),
new Claim(ClaimTypes.Role, "admin")
}, "mock"));

Comment on lines +178 to +189

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeleteClienteAndAnonymizeLogin is only tested for the admin success path. The action contains important authorization behavior (self-delete allowed, non-admin deleting others forbidden) and a NotFound branch; adding tests for those outcomes would better validate the soft-delete/anonymization contract.

Copilot uses AI. Check for mistakes.
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = user }
};

var result = await controller.DeleteClienteAndAnonymizeLogin(2);

Assert.IsType<NoContentResult>(result);

var anonymizedLogin = context.Logins.Find(2);
Assert.Null(anonymizedLogin.Email);
Assert.Null(anonymizedLogin.HashPassword);
}
}
}