| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- using Application.Abstractions.Crypto;
- 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, new FakeFieldEncryptor(), options, NullLogger<DisconnectHandler>.Instance);
- }
- private sealed class FakeFieldEncryptor : IFieldEncryptor
- {
- public int CurrentKeyVersion => 1;
- public string Encrypt(string plaintext) => $"enc:{plaintext}";
- public string Decrypt(string ciphertextBase64, int keyVersion) => ciphertextBase64.StartsWith("enc:") ? ciphertextBase64[4..] : ciphertextBase64;
- public string Hash(string plaintext) => plaintext;
- }
- 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"));
- }
- }
|