diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index d26b5c235a6b458734c224bda5ae2ec789618ed7..3129fcdd0dd346530297e6975fc98a67a5c8fd1c 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -44,7 +44,7 @@ public class SourceFileWatcher : IFileWatcher fileWatcher = new FileSystemWatcher { Path = SourceAbsolutePath, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, IncludeSubdirectories = true, EnableRaisingEvents = true }; diff --git a/source/Models/IFile.cs b/source/Models/IFile.cs index d3f007c6ca9ef53ae2cbd08c7aeff5349b30b4e0..c7cf50fec6fb6f93e61343c7826fd68c80460f13 100644 --- a/source/Models/IFile.cs +++ b/source/Models/IFile.cs @@ -1,5 +1,7 @@ using System.IO; -using Microsoft.AspNetCore.StaticFiles; +using FolkerKinzel.MimeTypes; + +// using Microsoft.AspNetCore.StaticFiles; using SuCoS.Helpers; namespace SuCoS.Models; @@ -42,18 +44,7 @@ public interface IFile /// /// File MIME type. /// - string MimeType - { - get - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(SourceFullPath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType; - } - } + string MimeType => MimeString.FromFileName(SourceFullPath) ?? "application/octet-stream"; /// /// File size in bytes. diff --git a/source/Program.cs b/source/Program.cs index d067a46651f1c4c968cbee67c4d888a55e462a94..a85f7da06f7b5bd0734ddf34d18c172241a5d5ff 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -115,7 +115,7 @@ public class Program }; var serveCommand = new ServeCommand(serverOptions, logger, new SourceFileWatcher()); - await serveCommand.RunServer(); + serveCommand.StartServer(); await Task.Delay(-1); // Wait forever. }, sourceOption, draftOption, futureOption, expiredOption, verboseOption); diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index eba8335bfd4785109d866453d6a841658998c4d5..e0b66684283d15f7001d41542a1ddcfd2ff3be2e 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -1,10 +1,10 @@ using System; using System.IO; +using System.Net; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Microsoft.VisualBasic; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; @@ -28,23 +28,6 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private readonly ServeOptions options; - /// - /// The global base URL for the server, combined with the port number. - /// This is constructed in the ServeCommand constructor using the provided base URL and port number, - /// and is subsequently used when starting or restarting the server. - /// - private readonly string baseURLGlobal; - - /// - /// An instance of IWebHost, which represents the running web server. - /// This instance is created every time the server starts or restarts and is used to manage - /// the lifecycle of the server. It's nullable because there may be periods when there is no running server, - /// such as during a restart operation or before the server has been initially started. - /// - private IWebHost? host; - - private IServerHandlers[]? handlers; - /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// When a change is detected, this triggers a server restart to ensure the served content @@ -61,29 +44,37 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private Timer? debounceTimer; - /// - /// A boolean flag indicating whether a server restart is currently in progress. - /// This is used to prevent overlapping restarts when file changes are detected in quick succession. - /// - private volatile bool restartInProgress; - /// /// A SemaphoreSlim used to ensure that server restarts due to file changes occur sequentially, /// not concurrently. This is necessary because a restart involves stopping the current server /// and starting a new one, which would not be thread-safe without some form of synchronization. /// - private readonly SemaphoreSlim restartServerLock = new(1, 1); + // private readonly SemaphoreSlim restartServerLock = new(1, 1); + private Task lastRestartTask = Task.CompletedTask; + + private HttpListener? listener; + + private IServerHandlers[]? handlers; private DateTime serverStartTime; + private Task? loop; + /// - /// Method to start the server explicitly + /// Constructor for the ServeCommand class. /// - /// - public async Task RunServer() + /// ServeOptions object specifying the serve options. + /// The logger instance. Injectable for testing + /// + public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) { - // Start the server! - await StartServer("http://localhost", 1122); + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); + + // Watch for file changes in the specified path + var SourceAbsolutePath = Path.GetFullPath(options.Source); + logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); + fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); } /// @@ -92,7 +83,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// The base URL for the server. /// The port number for the server. /// A Task representing the asynchronous operation. - private async Task StartServer(string baseURL, int port) + public void StartServer(string baseURL = baseURLDefault, int port = portDefault) { logger.Information("Starting server..."); @@ -108,99 +99,120 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable new RegisteredPageRequest(site), new RegisteredPageResourceRequest(site) }; + listener = new HttpListener(); + listener.Prefixes.Add($"{baseURL}:{port}/"); + listener.Start(); - host = new WebHostBuilder() - .UseKestrel() - .UseUrls(baseURLGlobal) - .UseContentRoot(options.Source) // Set the content root path - .UseEnvironment("Debug") // Set the hosting environment to Debug - .Configure(app => - { - app.Run(HandleRequest); // Call the custom method for handling requests - }) - .Build(); - - await host.StartAsync(); logger.Information("You site is live: {baseURL}:{port}", baseURL, port); + loop = Task.Run(async () => + { + while (listener is not null && listener.IsListening) + { + try + { + var context = await listener.GetContextAsync(); + await HandleRequest(context); + } + catch (HttpListenerException ex) + { + if (listener.IsListening) + { + logger.Error(ex, "Unexpected listener error."); + } + break; + } + catch (Exception ex) + { + if (listener.IsListening) + { + logger.Error(ex, "Error processing request."); + } + break; + } + } + }); } /// public void Dispose() { - host?.Dispose(); + listener?.Stop(); + listener?.Close(); fileWatcher.Stop(); debounceTimer?.Dispose(); GC.SuppressFinalize(this); } - /// - /// Constructor for the ServeCommand class. - /// - /// ServeOptions object specifying the serve options. - /// The logger instance. Injectable for testing - /// - public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) - { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); - baseURLGlobal = $"{baseURLDefault}:{portDefault}"; - - // Watch for file changes in the specified path - var SourceAbsolutePath = Path.GetFullPath(options.Source); - logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); - fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); - } - /// /// Restarts the server asynchronously. /// private async Task RestartServer() { - // Check if another restart is already in progress - await restartServerLock.WaitAsync(); - - try + await lastRestartTask.ContinueWith(async _ => { - site = SiteHelper.Init(configFile, options, frontMatterParser, WhereParamsFilter, logger, stopwatch); + logger.Information($"Restarting server..."); - // Stop the server - if (host != null) + if (listener != null && listener.IsListening) { - await host.StopAsync(TimeSpan.FromSeconds(1)); - host.Dispose(); + listener.Stop(); + listener.Close(); + + if (loop is not null) + { + // Wait for the loop to finish processing any ongoing requests. + await loop; + loop.Dispose(); + } } - await StartServer("http://localhost", 1122); - } - finally - { - _ = restartServerLock.Release(); - } + + // Reinitialize the site + site = SiteHelper.Init(configFile, options, frontMatterParser, WhereParamsFilter, logger, stopwatch); + + StartServer(baseURLDefault, portDefault); + }); + + lastRestartTask = lastRestartTask.ContinueWith(t => t.Exception != null + ? throw t.Exception + : Task.CompletedTask); } /// /// Handles the HTTP request asynchronously. /// /// The HttpContext representing the current request. - private async Task HandleRequest(HttpContext context) + private async Task HandleRequest(HttpListenerContext context) { - var requestPath = context.Request.Path.Value; - if (string.IsNullOrEmpty(Path.GetExtension(context.Request.Path.Value)) && requestPath.Length > 1) - { - requestPath = requestPath.TrimEnd('/'); - } + var requestPath = context.Request.Url?.AbsolutePath ?? string.Empty; string? resultType = null; if (handlers is not null) { - foreach (var item in handlers) + try { - if (item.Check(requestPath)) + var response = new HttpListenerResponseWrapper(context.Response); + foreach (var item in handlers) { - resultType = await item.Handle(context, requestPath, serverStartTime); + if (!item.Check(requestPath)) + { + continue; + } + + try + { + resultType = await item.Handle(response, requestPath, serverStartTime); + } + catch (Exception ex) + { + logger.Debug(ex, "Error handling the request."); + } break; } } + finally + { + context.Response.OutputStream.Close(); + } } if (resultType is null) @@ -211,10 +223,11 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable logger.Debug("Request {type}\tfor {RequestPath}", resultType, requestPath); } - private static async Task HandleNotFoundRequest(HttpContext context) + private static async Task HandleNotFoundRequest(HttpListenerContext context) { context.Response.StatusCode = 404; - await context.Response.WriteAsync("404 - File Not Found"); + using var writer = new StreamWriter(context.Response.OutputStream); + await writer.WriteAsync("404 - File Not Found"); } /// @@ -224,43 +237,16 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// The FileSystemEventArgs containing information about the file change. private void OnSourceFileChanged(object sender, FileSystemEventArgs e) { + if (e.FullPath.Contains("\\.git\\", StringComparison.InvariantCulture)) return; + // File changes are firing multiple events in a short time. // Debounce the event handler to prevent multiple events from firing in a short time debounceTimer?.Dispose(); debounceTimer = new Timer(DebounceCallback, e, TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan); } - private void DebounceCallback(object? state) - { - if (state is not FileSystemEventArgs e) - { - return; - } - HandleFileChangeAsync(e).GetAwaiter().GetResult(); - } - - private async Task HandleFileChangeAsync(FileSystemEventArgs e) + private async void DebounceCallback(object? state) { - if (restartInProgress) - { - return; - } - - logger.Information("File change detected: {FullPath}", e.FullPath); - - restartInProgress = true; - try - { - await RestartServer(); - } - catch (Exception ex) - { - logger.Error(ex, "Failed to restart server."); - throw; - } - finally - { - restartInProgress = false; - } + await RestartServer(); } } diff --git a/source/ServerHandlers/HttpListenerResponseWrapper.cs b/source/ServerHandlers/HttpListenerResponseWrapper.cs new file mode 100644 index 0000000000000000000000000000000000000000..5bd1cd697c36517ed0e2ec369ff1c7f52c7c2e1e --- /dev/null +++ b/source/ServerHandlers/HttpListenerResponseWrapper.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Net; + +namespace SuCoS.ServerHandlers; + +/// +/// Provides a wrapper for the . +/// +public class HttpListenerResponseWrapper : IHttpListenerResponse +{ + private readonly HttpListenerResponse response; + + /// + public Stream OutputStream => response.OutputStream; + + /// + public string? ContentType + { + get => response.ContentType; + set => response.ContentType = value; + } + + /// + public long ContentLength64 + { + get => response.ContentLength64; + set => response.ContentLength64 = value; + } + + /// + /// Wrap the HttpListenerResponse + /// + /// + public HttpListenerResponseWrapper(HttpListenerResponse response) + { + this.response = response; + } +} diff --git a/source/ServerHandlers/IHttpListenerResponse.cs b/source/ServerHandlers/IHttpListenerResponse.cs new file mode 100644 index 0000000000000000000000000000000000000000..34f1a8228a00ad3518bd5bb29525268fa9a96b30 --- /dev/null +++ b/source/ServerHandlers/IHttpListenerResponse.cs @@ -0,0 +1,24 @@ +using System.IO; + +namespace SuCoS.ServerHandlers; + +/// +/// Defines the contract for an HTTP listener response. +/// +public interface IHttpListenerResponse +{ + /// + /// Gets the output stream for writing the response body. + /// + Stream OutputStream { get; } + + /// + /// Gets or sets the content type of the response. + /// + string? ContentType { get; set; } + + /// + /// Gets or sets the length of the content to be sent. + /// + long ContentLength64 { get; set; } +} diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index 38887009030528fe97c885ebbd6a017a36a71272..a354f51cfec78e39c9685da578c6aab1ab85350a 100644 --- a/source/ServerHandlers/IServerHandlers.cs +++ b/source/ServerHandlers/IServerHandlers.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; namespace SuCoS.ServerHandlers; @@ -19,9 +18,9 @@ public interface IServerHandlers /// /// Process the request /// - /// + /// /// /// /// - Task Handle(HttpContext context, string requestPath, DateTime serverStartTime); + Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime); } diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs index 4ae76ff54e46b19677ed2efa86d57c4c38202788..74655850a09f20597d3ca45c1d3b2e3241b8a025 100644 --- a/source/ServerHandlers/PingRequests.cs +++ b/source/ServerHandlers/PingRequests.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.IO; namespace SuCoS.ServerHandlers; @@ -16,14 +16,17 @@ public class PingRequests : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } var content = serverStartTime.ToString("o"); - await context.Response.WriteAsync(content); + + using var writer = new StreamWriter(response.OutputStream, leaveOpen: true); + await writer.WriteAsync(content); + await writer.FlushAsync(); return "ping"; } diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index b98870c6e461eb501f3c9e57a9a8b58b17456c04..ad6e48e3a269adccf93d7f7a1d1d7a8fcd97c3c8 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,18 +33,18 @@ public class RegisteredPageRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } - if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IPage page) { var content = page.CompleteContent; content = InjectReloadScript(content); - await context.Response.WriteAsync(content); + using var writer = new StreamWriter(response.OutputStream, leaveOpen: true); + await writer.WriteAsync(content); return "dict"; } else diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs index 595c02e27771f3e0c9f2a43cafed5a69421e634a..c61183a2e66d582fb6e7d65f6d7f9a142d093f33 100644 --- a/source/ServerHandlers/RegisteredPageResourceRequest.cs +++ b/source/ServerHandlers/RegisteredPageResourceRequest.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,18 +33,18 @@ public class RegisteredPageResourceRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IResource resource) { - context.Response.ContentType = resource.MimeType; + response.ContentType = resource.MimeType; await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); + await fileStream.CopyToAsync(response.OutputStream); return "resource"; } else diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index 9cf0e9100c9215c940fd635b47c37775a763638c..16e6a0b7eebe3a8df3a34c7569a458eb00a79d2f 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -1,8 +1,8 @@ using System; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles; +using System.Net; +using FolkerKinzel.MimeTypes; namespace SuCoS.ServerHandlers; @@ -37,21 +37,22 @@ public class StaticFileRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { if (requestPath is null) { throw new ArgumentNullException(nameof(requestPath)); } - - var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } - context.Response.ContentType = GetContentType(fileAbsolutePath!); + + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + response.ContentType = GetContentType(fileAbsolutePath!); await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); + response.ContentLength64 = fileStream.Length; + await fileStream.CopyToAsync(response.OutputStream); return inTheme ? "themeSt" : "static"; } @@ -61,13 +62,6 @@ public class StaticFileRequest : IServerHandlers /// /// The path of the file. /// The content type of the file. - private static string GetContentType(string filePath) - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(filePath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType ?? "application/octet-stream"; - } + private static string GetContentType(string filePath) => + MimeString.FromFileName(filePath) ?? "application/octet-stream"; } diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index 62593b26997502326da0825e40badc200c69ab85..18fec0545ce78ee952d0679284e0826c2483c27f 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -14,13 +14,14 @@ + - - + - + + diff --git a/test/ServerHandlers/PingRequestHandlerTests.cs b/test/ServerHandlers/PingRequestHandlerTests.cs index af5cfb2d3321f850b639ad8cf90fbd90b23524ce..0d7ad20da875dce888e30772c2173babd433c888 100644 --- a/test/ServerHandlers/PingRequestHandlerTests.cs +++ b/test/ServerHandlers/PingRequestHandlerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.ServerHandlers; using Xunit; @@ -10,15 +10,14 @@ public class PingRequestHandlerTests : TestSetup public async Task Handle_ReturnsServerStartupTimestamp() { // Arrange - var context = new DefaultHttpContext(); - + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); var pingRequests = new PingRequests(); // Act - await pingRequests.Handle(context, "ping", todayDate); + var code = await pingRequests.Handle(response, "ping", todayDate); // Assert stream.Seek(0, SeekOrigin.Begin); @@ -26,8 +25,9 @@ public class PingRequestHandlerTests : TestSetup var content = await reader.ReadToEndAsync(); Assert.Equal(todayDate.ToString("o"), content); - } + Assert.Equal("ping", code); + } [Theory] [InlineData("/ping", true)] diff --git a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs index eb2a175675819cb9594e9060d79671b7bb2adb71..bce4b153dbdb7b7372738bea8eeb2d3fecb7e8e6 100644 --- a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs +++ b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs @@ -1,12 +1,15 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.Models.CommandLineOptions; using SuCoS.ServerHandlers; +using System.Net; using Xunit; namespace Tests.ServerHandlers; public class RegisteredPageRequestHandlerTests : TestSetup { + private readonly HttpClient _httpClient = new HttpClient(); + [Theory] [InlineData("/", true)] [InlineData("/testPage", false)] @@ -40,20 +43,23 @@ public class RegisteredPageRequestHandlerTests : TestSetup }; var registeredPageRequest = new RegisteredPageRequest(site); - var context = new DefaultHttpContext(); + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); // Act site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); registeredPageRequest.Check(requestPath); - await registeredPageRequest.Handle(context, requestPath, DateTime.Now); + var code = await registeredPageRequest.Handle(response, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(stream); var content = await reader.ReadToEndAsync(); + Assert.Equal("dict", code); + + // Assert // You may want to adjust this assertion depending on the actual format of your injected script if (contains) { @@ -66,7 +72,5 @@ public class RegisteredPageRequestHandlerTests : TestSetup Assert.DoesNotContain("", content, StringComparison.InvariantCulture); } Assert.Contains("Index Content", content, StringComparison.InvariantCulture); - } - } diff --git a/test/ServerHandlers/StaticFileRequestHandlerTests.cs b/test/ServerHandlers/StaticFileRequestHandlerTests.cs index eea246a045f4bdfe54c154edb00e50c96adef2ef..6e6183922ad401f8b27c8ab175b1f20a3f223129 100644 --- a/test/ServerHandlers/StaticFileRequestHandlerTests.cs +++ b/test/ServerHandlers/StaticFileRequestHandlerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.ServerHandlers; using Xunit; @@ -7,6 +7,7 @@ namespace Tests.ServerHandlers; public class StaticFileRequestHandlerTests : TestSetup, IDisposable { private readonly string tempFilePath; + private readonly HttpClient _httpClient = new HttpClient(); public StaticFileRequestHandlerTests() : base() { @@ -26,7 +27,7 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable var staticFileRequest = new StaticFileRequest(basePath, false); // Act - var result = staticFileRequest.Check(requestPath); + var result = staticFileRequest.Check(requestPath: requestPath); // Assert Assert.True(result); @@ -41,13 +42,14 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); var staticFileRequest = new StaticFileRequest(basePath, true); - var context = new DefaultHttpContext(); + + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); // Act staticFileRequest.Check(requestPath); - await staticFileRequest.Handle(context, requestPath, DateTime.Now); + var code = await staticFileRequest.Handle(response, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); @@ -55,6 +57,8 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable var content = await reader.ReadToEndAsync(); Assert.Equal("test", content); + + Assert.Equal("themeSt", code); } public void Dispose()