| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- using Application.Abstractions.Authentication;
- using Application.Abstractions.Cache;
- using Application.Abstractions.Crypto;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Logging.Abstractions;
- using Microsoft.Extensions.Options;
- using SharedKernel;
- using SocialLoginCommand = Application.Features.Api.Auth.SocialLogin.Command;
- using SocialLoginHandler = Application.Features.Api.Auth.SocialLogin.Handler;
- namespace Application.Tests;
- [TestClass]
- public sealed class SocialLoginHandlerTests
- {
- private sealed class FakeSocialAuthProvider(string provider, SocialUserInfo? userInfo) : ISocialAuthProvider
- {
- public string Provider => provider;
- public string GetAuthorizationUrl(string clientId, string redirectUri, string state) => $"https://fake/{provider}?state={state}";
- public Task<SocialTokens?> ExchangeCodeAsync(string code, string redirectUri, string? state, string clientId, string? clientSecret, CancellationToken ct)
- {
- return Task.FromResult<SocialTokens?>(new SocialTokens("fake-access-token", "fake-refresh-token", DateTime.UtcNow.AddHours(1), ["account_email"]));
- }
- public Task<SocialUserInfo?> GetUserInfoAsync(string accessToken, CancellationToken ct) => Task.FromResult(userInfo);
- }
- private sealed class FakeJwtTokenProvider : IJwtTokenProvider
- {
- public string CreateAccessToken(int memberID, string email, string? name) => $"jwt-{memberID}";
- public string CreateRefreshToken() => $"rt-{Guid.NewGuid():N}";
- public string CreateOAuth2AccessToken(int applicationID, string clientID, int ownerMemberID, IEnumerable<string> scopes, TimeSpan ttl) => "oauth2";
- }
- 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 sealed class FakeCacheService : ICacheService
- {
- public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) => Task.FromResult<T?>(default);
- public Task SetAsync<T>(string key, T value, CancellationToken ct = default) => Task.CompletedTask;
- public Task SetAsync<T>(string key, T value, TimeSpan expiry, CancellationToken ct = default) => Task.CompletedTask;
- public Task RemoveAsync(string key, CancellationToken ct = default) => Task.CompletedTask;
- public Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default) => Task.CompletedTask;
- public Task<bool> TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default) => Task.FromResult(true);
- }
- private static SocialLoginHandler CreateHandler(Infrastructure.Persistence.AppDbContext db, SocialUserInfo? userInfo, string provider = "Kakao")
- {
- var options = Options.Create(new AppSettings
- {
- JWT = new AppSettings.JwtSection
- {
- AccessTokenExpiration = 30,
- RefreshTokenExpiration = 7
- },
- OAuth = new AppSettings.OAuthSection
- {
- Naver = new AppSettings.OAuthSection.OAuthProviderSection
- {
- ClientId = "naver-client",
- ClientSecret = "naver-secret",
- RedirectUri = "https://localhost:3000/login/naver/callback"
- },
- Kakao = new AppSettings.OAuthSection.OAuthProviderSection
- {
- ClientId = "kakao-rest-key",
- ClientSecret = "",
- RedirectUri = "https://localhost:3000/login/kakao/callback"
- }
- }
- });
- return new SocialLoginHandler(
- db,
- new FakeJwtTokenProvider(),
- [new FakeSocialAuthProvider(provider, userInfo)],
- new FakeFieldEncryptor(),
- options,
- NullLogger<SocialLoginHandler>.Instance,
- new FakeCacheService()
- );
- }
- [TestMethod]
- public async Task KakaoEmailNotProvided_Fails_EmailRequired()
- {
- // 기대 행위: 카카오 이메일 선택동의 미동의(Email null) 시 Auth.EmailRequired 로 실패해
- // 프론트가 이메일 보완 화면으로 유도할 수 있어야 한다 (d5 §③ 정책 4).
- using var db = TestDb.Create();
- var handler = CreateHandler(db, new SocialUserInfo("987654321", null, false, "미동의개미", null));
- var result = await handler.Handle(new SocialLoginCommand("kakao", "auth-code"), default);
- Assert.IsFalse(result.IsSuccess);
- Assert.AreEqual("Auth.EmailRequired", result.Error.Code);
- }
- [TestMethod]
- public async Task ExistingMemberWithoutLink_Fails_AccountExists_NoAutoLink()
- {
- // 기대 행위: 같은 이메일의 기존 계정이 있어도 자동 연결하지 않고 Auth.AccountExists 로 실패한다 (d0 §5-⑦ 자동 계정연결 금지).
- using var db = TestDb.Create();
- var memberID = await TestDb.CreateMemberAsync(db);
- var email = (await db.Member.SingleAsync(c => c.ID == memberID)).Email;
- var handler = CreateHandler(db, new SocialUserInfo("11122233", email, true, "개미", null));
- var result = await handler.Handle(new SocialLoginCommand("kakao", "auth-code"), default);
- Assert.IsFalse(result.IsSuccess);
- Assert.AreEqual("Auth.AccountExists", result.Error.Code);
- using var verify = TestDb.Create();
- Assert.IsFalse(await verify.MemberOAuthToken.AnyAsync(c => c.MemberID == memberID), "자동 계정연결이 일어나면 안 된다");
- }
- [TestMethod]
- public async Task NewEmail_AutoSignup_CreatesMemberAndOAuthLink()
- {
- // 기대 행위: 미가입 이메일은 자동가입 + MemberOAuthToken 생성(암호화 저장) 후 JWT 를 발급한다.
- // 검증된 이메일(카카오 is_email_verified)만 이메일 인증 완료 처리한다.
- using var db = TestDb.Create();
- var email = $"social-{Guid.NewGuid():N}@antooza.test";
- var handler = CreateHandler(db, new SocialUserInfo("55566677", email, true, "신규개미", null));
- var result = await handler.Handle(new SocialLoginCommand("kakao", "auth-code", null, null, "127.0.0.1", "test-agent"), default);
- Assert.IsTrue(result.IsSuccess, $"자동가입 로그인이 성공해야 한다: {result.Error?.Code}");
- Assert.IsFalse(string.IsNullOrEmpty(result.Value.AccessToken));
- using var verify = TestDb.Create();
- var member = await verify.Member.SingleAsync(c => c.Email == email);
- Assert.IsTrue(member.IsEmailVerified, "검증된 이메일은 인증 완료 처리된다");
- var link = await verify.MemberOAuthToken.SingleAsync(c => c.MemberID == member.ID);
- Assert.AreEqual("Kakao", link.Provider);
- Assert.AreEqual("enc:fake-access-token", link.AccessTokenEnc, "신규 저장분은 IFieldEncryptor 로 암호화되어야 한다");
- Assert.IsTrue(await verify.RefreshToken.AnyAsync(c => c.MemberID == member.ID), "refresh token 이 저장되어야 한다");
- }
- }
|