diff --git a/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs b/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs
new file mode 100644
index 0000000..5c3641b
--- /dev/null
+++ b/tests/SIGCM2.Api.Tests/Usuarios/ChangeMyPasswordEndpointTests.cs
@@ -0,0 +1,130 @@
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text.Json;
+using Dapper;
+using Microsoft.Data.SqlClient;
+using SIGCM2.TestSupport;
+
+namespace SIGCM2.Api.Tests.Usuarios;
+
+///
+/// Integration tests for PUT /api/v1/users/me/password (UDT-008 B6).
+///
+[Collection("ApiIntegration")]
+public sealed class ChangeMyPasswordEndpointTests : IAsyncLifetime
+{
+ private const string TestConnectionString =
+ "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
+
+ // This hash corresponds to "@Diego550@"
+ private const string DefaultHash = "$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW";
+
+ private readonly HttpClient _client;
+ private readonly SqlTestFixture _db;
+
+ public ChangeMyPasswordEndpointTests(TestWebAppFactory factory)
+ {
+ _client = factory.CreateClient();
+ _db = new SqlTestFixture(TestConnectionString);
+ }
+
+ public async Task InitializeAsync() => await _db.InitializeAsync();
+ public async Task DisposeAsync() => await _db.DisposeAsync();
+
+ private async Task SeedUserAsync(string username)
+ {
+ await using var conn = new SqlConnection(TestConnectionString);
+ await conn.OpenAsync();
+ return await conn.ExecuteScalarAsync($"""
+ IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = '{username}')
+ INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
+ VALUES ('{username}', '{DefaultHash}', 'Test', 'User', 'cajero', '[]', 1, 0);
+ SELECT Id FROM dbo.Usuario WHERE Username = '{username}'
+ """);
+ }
+
+ private async Task GetTokenAsync(string username)
+ {
+ var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
+ new { username, password = "@Diego550@" });
+ response.EnsureSuccessStatusCode();
+ var json = await response.Content.ReadFromJsonAsync();
+ return json.GetProperty("accessToken").GetString()!;
+ }
+
+ [Fact]
+ public async Task PUT_Me_Password_204_No_Content()
+ {
+ await SeedUserAsync("user_chpwd_happy");
+ var token = await GetTokenAsync("user_chpwd_happy");
+
+ var request = new HttpRequestMessage(HttpMethod.Put, "/api/v1/users/me/password");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ request.Content = JsonContent.Create(new { oldPassword = "@Diego550@", newPassword = "Nuevo1234!" });
+
+ var response = await _client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PUT_Me_Password_400_Wrong_Old_With_Error_Key()
+ {
+ await SeedUserAsync("user_chpwd_wrongold");
+ var token = await GetTokenAsync("user_chpwd_wrongold");
+
+ var request = new HttpRequestMessage(HttpMethod.Put, "/api/v1/users/me/password");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ request.Content = JsonContent.Create(new { oldPassword = "WrongPassword!", newPassword = "Nuevo1234!" });
+
+ var response = await _client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Contains("invalid-old-password", body, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task PUT_Me_Password_400_Weak_New_Password()
+ {
+ await SeedUserAsync("user_chpwd_weak");
+ var token = await GetTokenAsync("user_chpwd_weak");
+
+ var request = new HttpRequestMessage(HttpMethod.Put, "/api/v1/users/me/password");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ request.Content = JsonContent.Create(new { oldPassword = "@Diego550@", newPassword = "abc" }); // too short
+
+ var response = await _client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PUT_Me_Password_401_No_Auth()
+ {
+ var request = new HttpRequestMessage(HttpMethod.Put, "/api/v1/users/me/password");
+ request.Content = JsonContent.Create(new { oldPassword = "@Diego550@", newPassword = "Nuevo1234!" });
+
+ var response = await _client.SendAsync(request);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PUT_Me_Password_Does_NOT_Require_Users_Manage_Permission()
+ {
+ // Cajero user (no users:gestionar permission) should be able to change own password
+ await SeedUserAsync("cajero_chpwd");
+ var token = await GetTokenAsync("cajero_chpwd");
+
+ var request = new HttpRequestMessage(HttpMethod.Put, "/api/v1/users/me/password");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ request.Content = JsonContent.Create(new { oldPassword = "@Diego550@", newPassword = "Nuevo1234!" });
+
+ var response = await _client.SendAsync(request);
+
+ // Should succeed with 204, NOT 403
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ }
+}
diff --git a/tests/SIGCM2.Application.Tests/Usuarios/ChangeMyPasswordCommandHandlerTests.cs b/tests/SIGCM2.Application.Tests/Usuarios/ChangeMyPasswordCommandHandlerTests.cs
new file mode 100644
index 0000000..4c686cf
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Usuarios/ChangeMyPasswordCommandHandlerTests.cs
@@ -0,0 +1,72 @@
+using NSubstitute;
+using SIGCM2.Application.Abstractions.Persistence;
+using SIGCM2.Application.Abstractions.Security;
+using SIGCM2.Application.Usuarios.ChangeMyPassword;
+using SIGCM2.Domain.Entities;
+using SIGCM2.Domain.Exceptions;
+
+namespace SIGCM2.Application.Tests.Usuarios;
+
+public class ChangeMyPasswordCommandHandlerTests
+{
+ private readonly IUsuarioRepository _repo = Substitute.For();
+ private readonly IPasswordHasher _hasher = Substitute.For();
+ private readonly IRefreshTokenRepository _refreshRepo = Substitute.For();
+ private readonly ChangeMyPasswordCommandHandler _handler;
+
+ public ChangeMyPasswordCommandHandlerTests()
+ {
+ _handler = new ChangeMyPasswordCommandHandler(_repo, _hasher);
+ }
+
+ private static Usuario MakeUser(int id = 1, bool mustChangePassword = false)
+ => new(id, "user" + id, "$2a$12$oldhash", "Test", "User", null, "cajero", "[]", true,
+ mustChangePassword: mustChangePassword);
+
+ [Fact]
+ public async Task Handle_Happy_Path_Hashes_New_Password_Clears_MustChange()
+ {
+ var user = MakeUser(1, mustChangePassword: true);
+ _repo.GetByIdAsync(1, Arg.Any()).Returns(user);
+ _hasher.Verify("oldPass1!", "$2a$12$oldhash").Returns(true);
+ _hasher.Hash("newPass2!").Returns("$2a$12$newhash");
+
+ await _handler.Handle(new ChangeMyPasswordCommand(1, "oldPass1!", "newPass2!"));
+
+ await _repo.Received(1).UpdatePasswordAsync(1, "$2a$12$newhash", false, Arg.Any());
+ }
+
+ [Fact]
+ public async Task Handle_Throws_InvalidOldPasswordException_When_Wrong_Old()
+ {
+ var user = MakeUser(1);
+ _repo.GetByIdAsync(1, Arg.Any()).Returns(user);
+ _hasher.Verify(Arg.Any(), Arg.Any()).Returns(false);
+
+ await Assert.ThrowsAsync(
+ () => _handler.Handle(new ChangeMyPasswordCommand(1, "wrongPass!", "newPass2!")));
+ }
+
+ [Fact]
+ public async Task Handle_Throws_UsuarioNotFoundException_When_Not_Found()
+ {
+ _repo.GetByIdAsync(9999, Arg.Any()).Returns((Usuario?)null);
+
+ await Assert.ThrowsAsync(
+ () => _handler.Handle(new ChangeMyPasswordCommand(9999, "old", "new1234")));
+ }
+
+ [Fact]
+ public async Task Handle_Does_NOT_Revoke_Own_Refresh_Tokens()
+ {
+ var user = MakeUser(1);
+ _repo.GetByIdAsync(1, Arg.Any()).Returns(user);
+ _hasher.Verify(Arg.Any(), Arg.Any()).Returns(true);
+ _hasher.Hash(Arg.Any()).Returns("$2a$12$newhash");
+
+ await _handler.Handle(new ChangeMyPasswordCommand(1, "oldPass1!", "newPass2!"));
+
+ // spec REQ-BCP-05: change password does NOT revoke own tokens
+ await _refreshRepo.DidNotReceive().RevokeAllActiveForUserAsync(Arg.Any(), Arg.Any(), Arg.Any());
+ }
+}