3

I am having issues with testing Login Controller using IdentityServer4. It throws the following error:

{System.Net.Http.WinHttpException (0x80072EFD): A connection with the server could not be established

I am trying to generate the access Token using ResourceOwnerPassword, for which I have implemented IResourceOwnerPasswordValidator. I get the error in UserAccessToken.cs class when I call the RequestResourcePasswordAsync. I am pretty sure it is because of the handler. Because if I use a handler in my test class and call the TokenClient with that handler I do get access Token but then I cannot test my Login Controller.

LoginController.cs

[HttpPost]
public async Task<IActionResult> Login([FromBody]LoginViewModel user)
{       
    var accessToken = await UserAccessToken.GenerateTokenAsync(user.Username, user.Password);
    var loginToken = JsonConvert.DeserializeObject(accessToken);
    return Ok(loginToken); 
}

UserAccessToken.cs

public async Task<string> GenerateTokenAsync(string username, string password)
        {
            var tokenUrl = "http://localhost:5000/connect/token";
            var tokenClient = new TokenClient(tokenUrl,"ClientId","ClientPassword");
            var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(username, password, SecurityConfig.PublicApiResourceId);
            if (tokenResponse.IsError)
            {
                throw new AuthenticationFailedException(tokenResponse.Error);
            }
            return tokenResponse.Json.ToString();

        }

TestClass.cs

 [Fact]
 public async Task Login()
 {          
     var client = _identityServer.CreateClient();
     var data = new StringContent(JsonConvert.SerializeObject(new LoginViewModel { Username = "1206", Password = "5m{F?Hk92/Qj}n7Lp6" }), Encoding.UTF8, "application/json");
     var dd = await client.PostAsync("http://localhost:5000/login", data);
     var ss = dd;
 }

IdentityServerSetup.cs //Integration Test Setup

public class IdentityServerSetup
{
    private TestServer _identityServer;
    private const string TokenEndpoint = "http://localhost:5000/connect/token";
    public HttpMessageHandler _handler;

    //IF I use this code I do get a AccessToken
    public async Task<string> GetAccessTokenForUser(string userName, string password, string clientId, string clientSecret, string apiName = "integrapay.api.public")
    {
        var client = new TokenClient(TokenEndpoint, clientId, clientSecret, innerHttpMessageHandler: _handler);
        var response = await client.RequestResourceOwnerPasswordAsync(userName, password, apiName);
        return response.AccessToken;    
    }
}

1 Answer 1

4

Well, you have already answered the question yourself: The problem is with the HttpHandler the TokenClient uses. It should use the one provided by the TestServer to successfully communicate with it instead of doing actual requests to localhost.

Right now, UserAccessToken requires a TokenClient. This is a dependency of your class, so you should refactor the code to pass in a TokenClient instead of generating it yourself. This pattern is called Dependency Injection and is ideal for cases like yours, where you might have different requirements in your tests than in your production setup.

You could make the code look like this:

UserAccessToken.cs

public class UserAccessToken
{
    private readonly TokenClient _tokenClient;

    public UserAccessToken(TokenClient tokenClient)
    {
        _tokenClient = tokenClient;
    }

    public async Task<string> GenerateTokenAsync(string username, string password)
    {
        var tokenUrl = "http://localhost:5000/connect/token";
        var tokenResponse = await _tokenClient.RequestResourceOwnerPasswordAsync(username, password, SecurityConfig.PublicApiResourceId);
        if (tokenResponse.IsError)
        {
            throw new AuthenticationFailedException(tokenResponse.Error);
        }
        return tokenResponse.Json.ToString();
    }
}

TestHelpers.cs

public static class TestHelpers
{
    private static TestServer _testServer;
    private static readonly object _initializationLock = new object();

    public static TestServer GetTestServer()
    {
        if (_testServer == null)
        {
            InitializeTestServer();
        }
        return _testServer;
    }

    private static void InitializeTestServer()
    {
        lock (_initializationLock)
        {
            if (_testServer != null)
            {
                return;
            }
            var webHostBuilder = new WebHostBuilder()
                .UseStartup<IntegrationTestsStartup>();
            var testServer = new TestServer(webHostBuilder);
            var initializationTask = InitializeDatabase(testServer);
            initializationTask.ConfigureAwait(false);
            initializationTask.Wait();
            testServer.BaseAddress = new Uri("http://localhost");
            _testServer = testServer;
        }
    }
}

IntegrationTestsStartup.cs

public class IntegrationTestsStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<TokenClient>(() =>
        {
            var handler = TestUtilities.GetTestServer().CreateHandler();
            var client = new TokenClient(TokenEndpoint, clientId, clientSecret, innerHttpMessageHandler: handler);
            return client;
        };
        services.AddTransient<UserAccessToken>();
    }
}

LoginController.cs

public class LoginController : Controller
{
    private readonly UserAccessToken _userAccessToken;

    public LoginController(UserAccessToken userAccessToken)
    {
        _userAccessToken = userAccessToken;
    }

    [HttpPost]
    public async Task<IActionResult> Login([FromBody]LoginViewModel user)
    {
        var accessToken = await _userAccessToken .GenerateTokenAsync(user.Username, user.Password);
        var loginToken = JsonConvert.DeserializeObject(accessToken);
        return Ok(loginToken);
    }
}

Here's one of my GitHub projects that makes use of the TestServer class and shows how I'm using it. It's not using IdentityServer4, though.

Sign up to request clarification or add additional context in comments.

3 Comments

Thats is good but I want to use my project startup not a mock startup.
What I do in cases like this is that I create extension methods for my project configurations. They're called from my Startup / IntegrationTestStartup class and allow me to configure a few things, for example using TestServer http handlers and In Memory databases instead of the real things. It is, in my opinion, a good idea to invest a bit at the beginning of a project to make sure it's easily testable and configurable. You need some way to pass different HttpHandlers to your method, and a separate startup class is a very simple way to do that.
I completely agree with you to have a separate startup class.But for some reason I cannot have another startup class for integration test.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.