Skip to content
Merged

Test #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
444 changes: 444 additions & 0 deletions ProjectVG.Tests/Api/Configuration/ConfigurationExtensionsTests.cs

Large diffs are not rendered by default.

283 changes: 283 additions & 0 deletions ProjectVG.Tests/Api/Filters/JwtAuthenticationFilterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using ProjectVG.Api.Filters;
using ProjectVG.Common.Constants;
using ProjectVG.Common.Exceptions;
using ProjectVG.Infrastructure.Auth;
using System.Security.Claims;
using Xunit;

namespace ProjectVG.Tests.Api.Filters
{
public class JwtAuthenticationFilterTests
{
private readonly Mock<ITokenService> _mockTokenService;
private readonly Mock<ILogger<JwtAuthenticationAttribute>> _mockLogger;
private readonly JwtAuthenticationAttribute _filter;
private readonly ServiceProvider _serviceProvider;

public JwtAuthenticationFilterTests()
{
_mockTokenService = new Mock<ITokenService>();
_mockLogger = new Mock<ILogger<JwtAuthenticationAttribute>>();

var services = new ServiceCollection();
services.AddSingleton(_mockTokenService.Object);
services.AddSingleton(_mockLogger.Object);
_serviceProvider = services.BuildServiceProvider();

_filter = new JwtAuthenticationAttribute();
}

private AuthorizationFilterContext CreateFilterContext(string? headerName = null, string? headerValue = null)
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};

if (!string.IsNullOrEmpty(headerName) && !string.IsNullOrEmpty(headerValue))
{
httpContext.Request.Headers[headerName] = headerValue;
}
Comment on lines +46 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

빈 Authorization 헤더가 설정되지 않아 테스트가 의도대로 동작하지 않습니다

headerValue가 빈 문자열일 때 헤더가 아예 추가되지 않아 “빈 헤더” 시나리오가 “헤더 없음”과 동일하게 동작합니다. 빈 값도 설정되도록 조건을 완화해 주세요.

-            if (!string.IsNullOrEmpty(headerName) && !string.IsNullOrEmpty(headerValue))
-            {
-                httpContext.Request.Headers[headerName] = headerValue;
-            }
+            if (!string.IsNullOrEmpty(headerName))
+            {
+                httpContext.Request.Headers[headerName] = headerValue ?? string.Empty;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!string.IsNullOrEmpty(headerName) && !string.IsNullOrEmpty(headerValue))
{
httpContext.Request.Headers[headerName] = headerValue;
}
if (!string.IsNullOrEmpty(headerName))
{
httpContext.Request.Headers[headerName] = headerValue ?? string.Empty;
}
🤖 Prompt for AI Agents
In ProjectVG.Tests/Api/Filters/JwtAuthenticationFilterTests.cs around lines 46
to 49, the test currently skips adding the header when headerValue is empty,
causing the "empty Authorization header" scenario to behave like "no header";
change the condition to only require headerName (i.e., remove the IsNullOrEmpty
check for headerValue) and ensure you set the header even when headerValue is an
empty string (protect against null by using headerValue ?? string.Empty before
assigning).


var actionContext = new ActionContext(
httpContext,
new RouteData(),
new ActionDescriptor()
);

return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
}

#region Token Extraction Tests

[Theory]
[InlineData("Authorization", "Bearer valid-token-123")]
[InlineData("X-Forwarded-Authorization", "Bearer forwarded-token-456")]
[InlineData("X-Original-Authorization", "Bearer original-token-789")]
[InlineData("HTTP_AUTHORIZATION", "Bearer http-token-abc")]
public async Task OnAuthorizationAsync_ValidTokenInDifferentHeaders_ShouldAuthenticate(string headerName, string headerValue)
{
// Arrange
var userId = Guid.NewGuid();
var expectedToken = headerValue.Substring("Bearer ".Length);
var filterContext = CreateFilterContext(headerName, headerValue);

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(expectedToken))
.ReturnsAsync(true);
_mockTokenService.Setup(x => x.GetUserIdFromTokenAsync(expectedToken))
.ReturnsAsync(userId);

// Act
await _filter.OnAuthorizationAsync(filterContext);

// Assert
filterContext.HttpContext.User.Should().NotBeNull();
filterContext.HttpContext.User.Identity!.IsAuthenticated.Should().BeTrue();
filterContext.HttpContext.User.Identity.AuthenticationType.Should().Be("Bearer");

var userIdClaim = filterContext.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
userIdClaim.Should().NotBeNull();
userIdClaim!.Value.Should().Be(userId.ToString());

var customUserIdClaim = filterContext.HttpContext.User.FindFirst("user_id");
customUserIdClaim.Should().NotBeNull();
customUserIdClaim!.Value.Should().Be(userId.ToString());
}

[Fact]
public async Task OnAuthorizationAsync_TokenWithExtraSpaces_ShouldTrimAndUse()
{
// Arrange
var userId = Guid.NewGuid();
var token = "token-with-spaces";
var filterContext = CreateFilterContext("Authorization", $"Bearer {token} ");

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(token))
.ReturnsAsync(true);
_mockTokenService.Setup(x => x.GetUserIdFromTokenAsync(token))
.ReturnsAsync(userId);

// Act
await _filter.OnAuthorizationAsync(filterContext);

// Assert
_mockTokenService.Verify(x => x.ValidateAccessTokenAsync(token), Times.Once);
filterContext.HttpContext.User.Identity!.IsAuthenticated.Should().BeTrue();
}

#endregion

#region Token Missing Tests

[Fact]
public async Task OnAuthorizationAsync_NoAuthorizationHeader_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext();

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}

[Fact]
public async Task OnAuthorizationAsync_EmptyAuthorizationHeader_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext("Authorization", "");

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}
Comment on lines +135 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

EmptyAuthorizationHeader 테스트가 현재 ‘헤더 없음’과 동일하게 동작합니다

위 CreateFilterContext 수정 전까지는 이 테스트가 실제로 헤더를 추가하지 않습니다. 헬퍼를 수정하지 않는다면 아래처럼 테스트에서 직접 헤더를 설정해 주세요. 수정 후 두 테스트(헤더 없음/빈 헤더)가 확실히 분리되는지 확인 바랍니다.

-            var filterContext = CreateFilterContext("Authorization", "");
+            var filterContext = CreateFilterContext();
+            filterContext.HttpContext.Request.Headers["Authorization"] = string.Empty;

🏁 Script executed:

#!/bin/bash
# 검색: CreateFilterContext 정의 및 사용 위치 확인
rg -nP '\bCreateFilterContext\b' -C3 --type=cs

Length of output: 8500


🏁 Script executed:

sed -n '30,80p' ProjectVG.Tests/Api/Filters/JwtAuthenticationFilterTests.cs

Length of output: 2158


EmptyAuthorizationHeader 테스트에 빈 헤더가 실제로 설정되도록 수정 필요
CreateFilterContext는 빈 문자열("")일 경우 헤더를 추가하지 않으므로, 테스트에서 헬퍼 호출 후 직접 빈 헤더를 설정하세요.

- var filterContext = CreateFilterContext("Authorization", "");
+ var filterContext = CreateFilterContext();
+ filterContext.HttpContext.Request.Headers["Authorization"] = string.Empty;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[Fact]
public async Task OnAuthorizationAsync_EmptyAuthorizationHeader_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext("Authorization", "");
// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);
exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}
[Fact]
public async Task OnAuthorizationAsync_EmptyAuthorizationHeader_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext();
filterContext.HttpContext.Request.Headers["Authorization"] = string.Empty;
// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);
exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}
🤖 Prompt for AI Agents
In ProjectVG.Tests/Api/Filters/JwtAuthenticationFilterTests.cs around lines 135
to 147, the test passes an empty string to CreateFilterContext but that helper
skips adding the header when value is empty; update the test so after calling
CreateFilterContext you explicitly add an empty Authorization header to the
request (e.g. set HttpContext.Request.Headers["Authorization"] = "" on the
returned filterContext) before invoking _filter.OnAuthorizationAsync, so the
header exists but is empty and the test exercises the TOKEN_MISSING path.


[Fact]
public async Task OnAuthorizationAsync_BearerWithoutToken_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext("Authorization", "Bearer");

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}

[Fact]
public async Task OnAuthorizationAsync_BearerWithEmptyToken_ShouldThrowTokenMissingException()
{
// Arrange
var filterContext = CreateFilterContext("Authorization", "Bearer ");

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.TOKEN_MISSING);
}

#endregion

#region Token Validation Tests

[Fact]
public async Task OnAuthorizationAsync_InvalidToken_ShouldThrowTokenInvalidException()
{
// Arrange
var invalidToken = "invalid-token";
var filterContext = CreateFilterContext("Authorization", $"Bearer {invalidToken}");

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(invalidToken))
.ReturnsAsync(false);

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.TOKEN_INVALID);
}

#endregion

#region User ID Extraction Tests

[Fact]
public async Task OnAuthorizationAsync_ValidTokenButNoUserId_ShouldThrowAuthenticationFailedException()
{
// Arrange
var validToken = "valid-token-no-user";
var filterContext = CreateFilterContext("Authorization", $"Bearer {validToken}");

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(validToken))
.ReturnsAsync(true);
_mockTokenService.Setup(x => x.GetUserIdFromTokenAsync(validToken))
.ReturnsAsync((Guid?)null);

// Act & Assert
var exception = await Assert.ThrowsAsync<AuthenticationException>(
() => _filter.OnAuthorizationAsync(filterContext)
);

exception.ErrorCode.Should().Be(ErrorCode.AUTHENTICATION_FAILED);
}

#endregion

#region Successful Authentication Tests

[Fact]
public async Task OnAuthorizationAsync_ValidTokenAndUserId_ShouldSetUserWithCorrectClaims()
{
// Arrange
var userId = Guid.NewGuid();
var validToken = "valid-token-123";
var filterContext = CreateFilterContext("Authorization", $"Bearer {validToken}");

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(validToken))
.ReturnsAsync(true);
_mockTokenService.Setup(x => x.GetUserIdFromTokenAsync(validToken))
.ReturnsAsync(userId);

// Act
await _filter.OnAuthorizationAsync(filterContext);

// Assert
var user = filterContext.HttpContext.User;
user.Should().NotBeNull();
user.Identity!.IsAuthenticated.Should().BeTrue();
user.Identity.AuthenticationType.Should().Be("Bearer");

var nameIdentifierClaim = user.FindFirst(ClaimTypes.NameIdentifier);
nameIdentifierClaim.Should().NotBeNull();
nameIdentifierClaim!.Value.Should().Be(userId.ToString());

var userIdClaim = user.FindFirst("user_id");
userIdClaim.Should().NotBeNull();
userIdClaim!.Value.Should().Be(userId.ToString());

user.Claims.Should().HaveCount(2);
}

[Fact]
public async Task OnAuthorizationAsync_CompleteValidFlow_ShouldNotSetResult()
{
// Arrange
var userId = Guid.NewGuid();
var validToken = "complete-valid-token";
var filterContext = CreateFilterContext("Authorization", $"Bearer {validToken}");

_mockTokenService.Setup(x => x.ValidateAccessTokenAsync(validToken))
.ReturnsAsync(true);
_mockTokenService.Setup(x => x.GetUserIdFromTokenAsync(validToken))
.ReturnsAsync(userId);

// Act
await _filter.OnAuthorizationAsync(filterContext);

// Assert
filterContext.Result.Should().BeNull(); // No result means continue with request
filterContext.HttpContext.User.Identity!.IsAuthenticated.Should().BeTrue();
}

#endregion
}
}
Loading