diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs
new file mode 100644
index 0000000..f8bc793
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Infrastructure/RefreshTokenRepositoryTests.cs
@@ -0,0 +1,176 @@
+using Dapper;
+using Microsoft.Data.SqlClient;
+using SIGCM2.Domain.Entities;
+using SIGCM2.Infrastructure.Persistence;
+
+namespace SIGCM2.Application.Tests.Infrastructure;
+
+///
+/// Integration tests for RefreshTokenRepository against SIGCM2_Test.
+/// Each test resets to a clean state using a transaction rollback pattern.
+///
+[Collection("SqlIntegration")]
+public class RefreshTokenRepositoryTests : IAsyncLifetime
+{
+ private const string ConnectionString =
+ "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
+
+ private SqlConnection _connection = null!;
+ private SqlTransaction _transaction = null!;
+ private RefreshTokenRepository _repository = null!;
+
+ public async Task InitializeAsync()
+ {
+ _connection = new SqlConnection(ConnectionString);
+ await _connection.OpenAsync();
+ _transaction = (SqlTransaction)await _connection.BeginTransactionAsync();
+
+ // Seed a test user for FK requirements
+ await _connection.ExecuteAsync("""
+ IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'test_rt_user')
+ INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo)
+ VALUES ('test_rt_user', '$2a$12$testhash', 'Test', 'User', 'admin', '["*"]', 1);
+ """, transaction: _transaction);
+
+ var factory = new SqlConnectionFactory(ConnectionString);
+ _repository = new RefreshTokenRepository(factory);
+ }
+
+ public async Task DisposeAsync()
+ {
+ // Rollback transaction to clean up all test data
+ await _transaction.RollbackAsync();
+ await _connection.DisposeAsync();
+ }
+
+ private static int GetTestUserId(SqlConnection conn, SqlTransaction tx)
+ {
+ return conn.QuerySingle(
+ "SELECT Id FROM dbo.Usuario WHERE Username = 'test_rt_user'",
+ transaction: tx);
+ }
+
+ private static RefreshToken BuildToken(int usuarioId, string hash = "test_hash_abc123xyz", bool expired = false)
+ {
+ var now = DateTime.UtcNow;
+ var ttl = expired ? TimeSpan.FromSeconds(1) : TimeSpan.FromDays(7);
+ return RefreshToken.IssueForNewFamily(usuarioId, hash, now.AddHours(-1), ttl, "10.0.0.1", "TestAgent");
+ }
+
+ [Fact]
+ public async Task AddAsync_PersistsAndReturnsId()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var token = BuildToken(userId, "unique_hash_persist_" + Guid.NewGuid().ToString("N")[..8]);
+
+ var id = await _repository.AddAsync(token);
+
+ Assert.True(id > 0);
+ }
+
+ [Fact]
+ public async Task AddAsync_DuplicateHash_Throws()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var hash = "duplicate_hash_" + Guid.NewGuid().ToString("N")[..8];
+ var token1 = BuildToken(userId, hash);
+ var token2 = BuildToken(userId, hash);
+
+ await _repository.AddAsync(token1);
+
+ // Duplicate hash must violate UQ_RefreshToken_TokenHash
+ await Assert.ThrowsAnyAsync(() => _repository.AddAsync(token2));
+ }
+
+ [Fact]
+ public async Task GetByHashAsync_RoundTripsAllFields()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var hash = "roundtrip_hash_" + Guid.NewGuid().ToString("N")[..8];
+ var token = BuildToken(userId, hash);
+
+ await _repository.AddAsync(token);
+ var retrieved = await _repository.GetByHashAsync(hash);
+
+ Assert.NotNull(retrieved);
+ Assert.Equal(userId, retrieved.UsuarioId);
+ Assert.Equal(hash, retrieved.TokenHash);
+ Assert.Equal(token.FamilyId, retrieved.FamilyId);
+ Assert.Null(retrieved.RevokedAt);
+ Assert.Null(retrieved.ReplacedById);
+ }
+
+ [Fact]
+ public async Task GetByHashAsync_NonExistentHash_ReturnsNull()
+ {
+ var result = await _repository.GetByHashAsync("does_not_exist_hash_abc");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task RevokeAsync_SetsRevokedAtAndReplacedById()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var hash = "revoke_test_" + Guid.NewGuid().ToString("N")[..8];
+ var token = BuildToken(userId, hash);
+
+ var id = await _repository.AddAsync(token);
+ var revokedAt = DateTime.UtcNow;
+ await _repository.RevokeAsync(id, replacedById: null, revokedAt: revokedAt);
+
+ var retrieved = await _repository.GetByHashAsync(hash);
+
+ Assert.NotNull(retrieved?.RevokedAt);
+ Assert.Null(retrieved.ReplacedById);
+ }
+
+ [Fact]
+ public async Task RevokeFamilyAsync_OnlyAffectsMatchingFamily()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var hash1 = "family_a_" + Guid.NewGuid().ToString("N")[..8];
+ var hash2 = "family_b_" + Guid.NewGuid().ToString("N")[..8];
+
+ var tokenA = BuildToken(userId, hash1);
+ var tokenB = BuildToken(userId, hash2);
+
+ await _repository.AddAsync(tokenA);
+ await _repository.AddAsync(tokenB);
+
+ // Revoke only family A
+ var count = await _repository.RevokeFamilyAsync(tokenA.FamilyId, DateTime.UtcNow);
+
+ Assert.Equal(1, count);
+
+ var retrievedA = await _repository.GetByHashAsync(hash1);
+ var retrievedB = await _repository.GetByHashAsync(hash2);
+
+ Assert.NotNull(retrievedA?.RevokedAt); // A is revoked
+ Assert.Null(retrievedB?.RevokedAt); // B is untouched
+ }
+
+ [Fact]
+ public async Task RevokeAllActiveForUserAsync_DoesNotTouchAlreadyRevoked()
+ {
+ var userId = GetTestUserId(_connection, _transaction);
+ var hash1 = "user_active_" + Guid.NewGuid().ToString("N")[..8];
+ var hash2 = "user_revoked_" + Guid.NewGuid().ToString("N")[..8];
+
+ var tokenActive = BuildToken(userId, hash1);
+ var tokenAlreadyRevoked = BuildToken(userId, hash2);
+
+ var idActive = await _repository.AddAsync(tokenActive);
+ var idRevoked = await _repository.AddAsync(tokenAlreadyRevoked);
+ await _repository.RevokeAsync(idRevoked, null, DateTime.UtcNow.AddMinutes(-5));
+
+ var count = await _repository.RevokeAllActiveForUserAsync(userId, DateTime.UtcNow);
+
+ Assert.Equal(1, count); // only the active one was revoked
+
+ var retrievedActive = await _repository.GetByHashAsync(hash1);
+ Assert.NotNull(retrievedActive?.RevokedAt);
+ }
+}
+
+[CollectionDefinition("SqlIntegration")]
+public class SqlIntegrationCollection : ICollectionFixture