SocialDisconnectHandlerTests.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. using Application.Abstractions.Crypto;
  2. using Domain.Entities.Members;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.Extensions.Logging.Abstractions;
  5. using Microsoft.Extensions.Options;
  6. using SharedKernel;
  7. using DisconnectCommand = Application.Features.Api.Auth.SocialDisconnect.Command;
  8. using DisconnectHandler = Application.Features.Api.Auth.SocialDisconnect.Handler;
  9. namespace Application.Tests;
  10. [TestClass]
  11. public sealed class SocialDisconnectHandlerTests
  12. {
  13. private const string KakaoAdminKey = "test-admin-key-123";
  14. private static DisconnectHandler CreateHandler(Infrastructure.Persistence.AppDbContext db)
  15. {
  16. var options = Options.Create(new AppSettings
  17. {
  18. OAuth = new AppSettings.OAuthSection
  19. {
  20. Kakao = new AppSettings.OAuthSection.OAuthProviderSection
  21. {
  22. ClientId = "kakao-rest-key",
  23. AdminKey = KakaoAdminKey
  24. },
  25. Naver = new AppSettings.OAuthSection.OAuthProviderSection
  26. {
  27. ClientId = "naver-client",
  28. ClientSecret = "naver-secret"
  29. }
  30. }
  31. });
  32. return new DisconnectHandler(db, new FakeFieldEncryptor(), options, NullLogger<DisconnectHandler>.Instance);
  33. }
  34. private sealed class FakeFieldEncryptor : IFieldEncryptor
  35. {
  36. public int CurrentKeyVersion => 1;
  37. public string Encrypt(string plaintext) => $"enc:{plaintext}";
  38. public string Decrypt(string ciphertextBase64, int keyVersion) => ciphertextBase64.StartsWith("enc:") ? ciphertextBase64[4..] : ciphertextBase64;
  39. public string Hash(string plaintext) => plaintext;
  40. }
  41. private static async Task<(int MemberID, string ProviderUserKey)> SeedKakaoLinkAsync(Infrastructure.Persistence.AppDbContext db)
  42. {
  43. var memberID = await TestDb.CreateMemberAsync(db);
  44. var providerUserKey = Guid.NewGuid().ToString("N");
  45. var token = MemberOAuthToken.Create(memberID, "Kakao", "enc:access", "enc:refresh", DateTime.UtcNow.AddHours(1), "account_email", providerUserKey);
  46. await db.MemberOAuthToken.AddAsync(token);
  47. await db.SaveChangesAsync(default);
  48. return (memberID, providerUserKey);
  49. }
  50. [TestMethod]
  51. public async Task Kakao_ValidAdminKey_MatchedKey_DeletesLink()
  52. {
  53. // 기대 행위: Admin 키가 일치하고 user_id 가 매칭되면 해당 연동행을 삭제하고 성공(Removed=1)한다.
  54. using var db = TestDb.Create();
  55. var (memberID, providerUserKey) = await SeedKakaoLinkAsync(db);
  56. var handler = CreateHandler(db);
  57. var result = await handler.Handle(new DisconnectCommand("kakao", providerUserKey, $"KakaoAK {KakaoAdminKey}"), default);
  58. Assert.IsTrue(result.IsSuccess, $"검증 통과 시 성공해야 한다: {result.Error?.Code}");
  59. Assert.AreEqual(1, result.Value.Removed);
  60. using var verify = TestDb.Create();
  61. Assert.IsFalse(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Kakao"), "연동행이 삭제되어야 한다");
  62. }
  63. [TestMethod]
  64. public async Task Kakao_ValidAdminKey_NoMatch_ReturnsSuccessIdempotent()
  65. {
  66. // 기대 행위: Admin 키는 맞지만 매칭되는 연동행이 없으면 멱등 성공(Removed=0)으로 처리한다 (이미 해제됐을 수 있음).
  67. using var db = TestDb.Create();
  68. var handler = CreateHandler(db);
  69. var result = await handler.Handle(new DisconnectCommand("kakao", "nonexistent-user-id", $"KakaoAK {KakaoAdminKey}"), default);
  70. Assert.IsTrue(result.IsSuccess, "미매칭이어도 멱등 성공이어야 한다");
  71. Assert.AreEqual(0, result.Value.Removed);
  72. }
  73. [TestMethod]
  74. public async Task Kakao_WrongAdminKey_Rejected_Unauthorized()
  75. {
  76. // 기대 행위: Admin 키가 불일치하면 Unauthorized 로 거부하고 연동행은 삭제하지 않는다.
  77. using var db = TestDb.Create();
  78. var (memberID, providerUserKey) = await SeedKakaoLinkAsync(db);
  79. var handler = CreateHandler(db);
  80. var result = await handler.Handle(new DisconnectCommand("kakao", providerUserKey, "KakaoAK wrong-key"), default);
  81. Assert.IsFalse(result.IsSuccess, "잘못된 Admin 키는 거부되어야 한다");
  82. Assert.AreEqual("Auth.DisconnectUnauthorized", result.Error.Code);
  83. using var verify = TestDb.Create();
  84. Assert.IsTrue(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Kakao"), "거부 시 연동행은 유지되어야 한다");
  85. }
  86. [TestMethod]
  87. public async Task Naver_NoAdminKeyRequired_MatchedKey_DeletesLink()
  88. {
  89. // 기대 행위: 네이버는 Admin 키 검증 없이 관대하게 수신 — id 매칭 시 삭제한다.
  90. using var db = TestDb.Create();
  91. var memberID = await TestDb.CreateMemberAsync(db);
  92. var providerUserKey = Guid.NewGuid().ToString("N");
  93. var token = MemberOAuthToken.Create(memberID, "Naver", "enc:access", null, DateTime.UtcNow.AddHours(1), "", providerUserKey);
  94. await db.MemberOAuthToken.AddAsync(token);
  95. await db.SaveChangesAsync(default);
  96. var handler = CreateHandler(db);
  97. var result = await handler.Handle(new DisconnectCommand("naver", providerUserKey, null), default);
  98. Assert.IsTrue(result.IsSuccess, $"네이버는 Admin 키 없이도 성공해야 한다: {result.Error?.Code}");
  99. Assert.AreEqual(1, result.Value.Removed);
  100. using var verify = TestDb.Create();
  101. Assert.IsFalse(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID && c.Provider == "Naver"));
  102. }
  103. }