using Domain.Entities.Members; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using SharedKernel; using DisconnectCommand = Application.Features.Api.Auth.SocialDisconnect.Command; using DisconnectHandler = Application.Features.Api.Auth.SocialDisconnect.Handler; namespace Application.Tests; [TestClass] public sealed class SocialDisconnectHandlerTests { private const string KakaoAdminKey = "test-admin-key-123"; private static DisconnectHandler CreateHandler(Infrastructure.Persistence.AppDbContext db) { var options = Options.Create(new AppSettings { OAuth = new AppSettings.OAuthSection { Kakao = new AppSettings.OAuthSection.OAuthProviderSection { ClientId = "kakao-rest-key", AdminKey = KakaoAdminKey }, Naver = new AppSettings.OAuthSection.OAuthProviderSection { ClientId = "naver-client", ClientSecret = "naver-secret" } } }); return new DisconnectHandler(db, options, NullLogger.Instance); } private static async Task<(int MemberID, string ProviderUserKey)> SeedKakaoLinkAsync(Infrastructure.Persistence.AppDbContext db) { var memberID = await TestDb.CreateMemberAsync(db); var providerUserKey = Guid.NewGuid().ToString("N"); var token = MemberOAuthToken.Create(memberID, "Kakao", "enc:access", "enc:refresh", DateTime.UtcNow.AddHours(1), "account_email", providerUserKey); await db.MemberOAuthToken.AddAsync(token); await db.SaveChangesAsync(default); return (memberID, providerUserKey); } [TestMethod] public async Task Kakao_ValidAdminKey_MatchedKey_DeletesLink() { // 기대 행위: Admin 키가 일치하고 user_id 가 매칭되면 해당 연동행을 삭제하고 성공(Removed=1)한다. using var db = TestDb.Create(); var (memberID, providerUserKey) = await SeedKakaoLinkAsync(db); var handler = CreateHandler(db); var result = await handler.Handle(new DisconnectCommand("kakao", providerUserKey, $"KakaoAK {KakaoAdminKey}"), default); Assert.IsTrue(result.IsSuccess, $"검증 통과 시 성공해야 한다: {result.Error?.Code}"); Assert.AreEqual(1, result.Value.Removed); using var verify = TestDb.Create(); Assert.IsFalse(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Kakao"), "연동행이 삭제되어야 한다"); } [TestMethod] public async Task Kakao_ValidAdminKey_NoMatch_ReturnsSuccessIdempotent() { // 기대 행위: Admin 키는 맞지만 매칭되는 연동행이 없으면 멱등 성공(Removed=0)으로 처리한다 (이미 해제됐을 수 있음). using var db = TestDb.Create(); var handler = CreateHandler(db); var result = await handler.Handle(new DisconnectCommand("kakao", "nonexistent-user-id", $"KakaoAK {KakaoAdminKey}"), default); Assert.IsTrue(result.IsSuccess, "미매칭이어도 멱등 성공이어야 한다"); Assert.AreEqual(0, result.Value.Removed); } [TestMethod] public async Task Kakao_WrongAdminKey_Rejected_Unauthorized() { // 기대 행위: Admin 키가 불일치하면 Unauthorized 로 거부하고 연동행은 삭제하지 않는다. using var db = TestDb.Create(); var (memberID, providerUserKey) = await SeedKakaoLinkAsync(db); var handler = CreateHandler(db); var result = await handler.Handle(new DisconnectCommand("kakao", providerUserKey, "KakaoAK wrong-key"), default); Assert.IsFalse(result.IsSuccess, "잘못된 Admin 키는 거부되어야 한다"); Assert.AreEqual("Auth.DisconnectUnauthorized", result.Error.Code); using var verify = TestDb.Create(); Assert.IsTrue(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Kakao"), "거부 시 연동행은 유지되어야 한다"); } [TestMethod] public async Task Naver_NoAdminKeyRequired_MatchedKey_DeletesLink() { // 기대 행위: 네이버는 Admin 키 검증 없이 관대하게 수신 — id 매칭 시 삭제한다. using var db = TestDb.Create(); var memberID = await TestDb.CreateMemberAsync(db); var providerUserKey = Guid.NewGuid().ToString("N"); var token = MemberOAuthToken.Create(memberID, "Naver", "enc:access", null, DateTime.UtcNow.AddHours(1), "", providerUserKey); await db.MemberOAuthToken.AddAsync(token); await db.SaveChangesAsync(default); var handler = CreateHandler(db); var result = await handler.Handle(new DisconnectCommand("naver", providerUserKey, null), default); Assert.IsTrue(result.IsSuccess, $"네이버는 Admin 키 없이도 성공해야 한다: {result.Error?.Code}"); Assert.AreEqual(1, result.Value.Removed); using var verify = TestDb.Create(); Assert.IsFalse(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Naver")); } }