使用 cookie 身份验证为 REST 服务设置 MSTest 的最佳方法?

Best way to setup MSTest for REST service with cookie-authentication?

背景:我正在使用 ASP.NET Core 3.1,并集成测试需要 cookie 身份验证的 REST 服务。

下面的候选解决方案。

注:

using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi; // Contains my Startup.cs

namespace WebApiTest
{
    [TestClass]
    public class UserTest
    {
        static IHost HttpHost;

        [ClassInitialize]
        public static async Task ClassStartup(TestContext context)
        {
            HttpHost = Host.CreateDefaultBuilder()
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                })
                .Build();
            await HttpHost.StartAsync();
        }

        [ClassCleanup]
        public static async Task ClassCleanup()
        {
            await HttpHost.StopAsync();
        }

        public static HttpContent GetHttpContent(object content)
        {
            HttpContent httpContent = null;

            if (content != null)
            {
                httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            }

            return httpContent;
        }

        public static HttpClient GetCookieHttpClient()
        {
            SocketsHttpHandler handler = new SocketsHttpHandler
            {
                AllowAutoRedirect = false,
                CookieContainer = new CookieContainer(),
                UseCookies = true
            };

            return new HttpClient(handler);
        }

        [TestMethod]
        public async Task GetUserData_ReturnsSuccess()
        {
            using (HttpClient client = GetCookieHttpClient())
            {
                var credentials = new
                {
                    Email = "test@test.com",
                    Password = "password123",
                };

                HttpResponseMessage response = await client.PostAsync("http://localhost:5000/api/auth/login", GetHttpContent(credentials));
                response = await client.GetAsync(String.Format("http://localhost:5000/api/users/{0}", credentials.Email));
                Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
            }
        }
    }
}

HttpClient是一个thin-client;除非您明确告诉它,否则它什么都不做。换句话说,它永远不会为您发送 cookie;您必须为每个请求 添加一个 Cookie header 到具有 cookie 值 的请求。测试服务器 "client" 只是一个 HttpClient 实例,设置为代理对测试服务器的请求。您应该按照规定使用测试服务器及其客户端,然后添加 Cookie header 您使用它发出的请求。

基于 Chris Pratt 的建议的解决方案

经过深挖,微软为此提供了解决方案(WebApplicationFactory):

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi;

namespace WebApiTest
{
    [TestClass]
    public class Class2
    {
        static WebApplicationFactory<Startup> Factory;
        static WebApplicationFactoryClientOptions ClientOptions;

        [ClassInitialize]
        public static async Task ClassStartup(TestContext context)
        {
            Factory = new WebApplicationFactory<Startup>();
            ClientOptions = new WebApplicationFactoryClientOptions();
            ClientOptions.AllowAutoRedirect = false;
            ClientOptions.HandleCookies = true;
            ClientOptions.BaseAddress = new Uri("http://localhost:5000");
        }

        public static HttpContent GetHttpContent(object content)
        {
            HttpContent httpContent = null;

            if (content != null)
            {
                httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
                httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            }

            return httpContent;
        }

        [TestMethod]
        public async Task GetUserData_ReturnsSuccess()
        {
            using (HttpClient client = Factory.CreateClient(ClientOptions))
            {
                var credentials = new
                {
                    Email = "test@test.com",
                    Password = "password123",
                };

                HttpResponseMessage response = await client.PostAsync("http://localhost:5000/api/auth/login", GetHttpContent(credentials));
                response = await client.GetAsync(String.Format("http://localhost:5000/api/users/{0}", credentials.Email));
                Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
            }

        }
    }
}

如果您想坚持使用 TestServer,这里有一个手动 Cookie 传递实现:

using Microsoft.AspNetCore.TestHost;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi;

namespace WebApiTest
{
    public class CookieHttpClient : IDisposable
    {
        private static HttpContent GetHttpContent(object content)
        {
            HttpContent httpContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(content, content.GetType()));
            httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            return httpContent;
        }

        private static IEnumerable<string> GetCookieStrings(CookieCollection collection)
        {
            List<string> output = new List<string>(collection.Count);
            foreach (Cookie cookie in collection)
            {
                output.Add(cookie.Name + "=" + cookie.Value);
            }
            return output;
        }

        private HttpClient client;
        private CookieContainer container;

        public CookieHttpClient(HttpClient client)
        {
            this.client = client;
            this.container = new CookieContainer();
        }

        public async Task<HttpResponseMessage> SendAsync(HttpMethod method, Uri uri)
        {
            return await this.SendAsync(method, uri, null);
        }

        public async Task<HttpResponseMessage> SendAsync(HttpMethod method, Uri uri, object data)
        {
            HttpRequestMessage request = new HttpRequestMessage(method, uri);

            // Add data
            if (data != null)
            {
                request.Content = GetHttpContent(data);
            }

            // Add cookies
            CookieCollection collection = this.container.GetCookies(uri);
            if (collection.Count > 0)
            {
                request.Headers.Add("Cookie", GetCookieStrings(collection));
            }

            HttpResponseMessage response = await this.client.SendAsync(request);

            // Remember cookies before returning
            if (response.Headers.Contains("Set-Cookie"))
            {
                foreach (string s in response.Headers.GetValues("Set-Cookie"))
                {
                    this.container.SetCookies(uri, s);
                }
            }

            return response;
        }

        public void Dispose()
        {
            this.client.Dispose();
        }
    }

    [TestClass]
    public class Class1
    {
        static TestServer TestServer;

        [ClassInitialize]
        public static async Task ClassStartup(TestContext context)
        {
            IWebHostBuilder builder = new WebHostBuilder()
                .UseStartup<Startup>();
            TestServer = new TestServer(builder);
        }

        [TestMethod]
        public async Task GetUserData_ReturnsSuccess()
        {
            using (CookieHttpClient client = new CookieHttpClient(TestServer.CreateClient()))
            {
                var credentials = new
                {
                    Email = "test@test.com",
                    Password = "password123",
                };

                HttpResponseMessage response = await client.SendAsync(HttpMethod.Post, new Uri("http://localhost:5000/api/auth/login"), credentials);
                response = await client.SendAsync(HttpMethod.Get, new Uri("http://localhost:5000/api/users/" + credentials.Email));
                Assert.IsTrue(response.StatusCode == HttpStatusCode.OK);
            }
        }
    }
}