diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..82b8fd1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Build and test + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build library (all target frameworks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Build + run: dotnet build Rotativa.AspNetCore/Rotativa.AspNetCore.csproj -c Release + + test: + name: Unit and integration tests (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Build test image + run: docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . + + - name: Run tests + run: docker run --rm rotativa-tests diff --git a/.gitignore b/.gitignore index 6217e6c..1e9d50b 100644 --- a/.gitignore +++ b/.gitignore @@ -313,3 +313,7 @@ OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ + +# Local macOS/Linux wkhtmltopdf binaries (only the Windows .exe files are tracked) +**/wwwroot/Rotativa/wkhtmltopdf +**/wwwroot/Rotativa/wkhtmltoimage diff --git a/README.md b/README.md index 8377dc2..20bb63f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Create PDFs and images with .NET Use Rotativa to transform a Razor view into a PDF or image. -This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7 and .NET 8. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7, .NET 8 and .NET 10. ## Install with nuget.org: @@ -11,7 +11,7 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.NET 6 up to 8): +Basic configuration done in Program.cs (.NET 6 up to 10): ```csharp app.UseRotativa(); @@ -22,7 +22,7 @@ or, if using .NET Core 3.1 and .NET 5: app.UseRotativa(env); ``` -Make sure you have a folder with the wkhtmltopdf.exe file accessible by the process running the web app. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` +Make sure you have a folder with the wkhtmltopdf.exe file (and wkhtmltoimage.exe for images) accessible by the process running the web app. On Linux and macOS the files are named wkhtmltopdf and wkhtmltoimage, without the .exe extension: use the builds for your platform from https://wkhtmltopdf.org/downloads.html. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` ## Usage @@ -149,6 +149,21 @@ public IActionResult Invoice() ## Issues and Pull Request Contribution is welcomed. If you would like to provide a PR please add some testing. +### Running the tests +The integration tests use Selenium and Chrome against the demo app (`Rotativa.AspNetCore.DemoApp`). Start the demo app at `https://localhost:56246`, then run `dotnet test Rotativa.AspNetCore.Tests`. On Linux and macOS, copy the wkhtmltopdf and wkhtmltoimage binaries for your platform into `Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa` first. + +The tests can be configured with environment variables: +- `ROTATIVA_DEMO_URL`: the demo app address (default `https://localhost:56246`). +- `ROTATIVA_TESTS_HEADLESS`: set to `true` to run Chrome headless. This is automatic when `CI=true`. +- `ROTATIVA_CHROME_BINARY`: path to a Chrome or Chromium executable, if Selenium can't find one. + +To run the whole suite on Linux with Docker, from the repository root: + +``` +docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . +docker run --rm rotativa-tests +``` + ## rotativa.io diff --git a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj index 5693452..c57bf1f 100644 --- a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj +++ b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/Rotativa.AspNetCore.Tests/Dockerfile b/Rotativa.AspNetCore.Tests/Dockerfile new file mode 100644 index 0000000..6246045 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/Dockerfile @@ -0,0 +1,36 @@ +# Runs the full test suite (unit + integration) on Linux. +# Build from the repository root: +# docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . +# docker run --rm rotativa-tests +FROM debian:bookworm-slim + +ARG TARGETARCH +ARG WKHTMLTOX_VERSION=0.12.6.1-3 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl libicu72 chromium chromium-driver \ + && curl -fsSL -o /tmp/wkhtmltox.deb \ + "https://github.com/wkhtmltopdf/packaging/releases/download/${WKHTMLTOX_VERSION}/wkhtmltox_${WKHTMLTOX_VERSION}.bookworm_${TARGETARCH}.deb" \ + && apt-get install -y --no-install-recommends /tmp/wkhtmltox.deb \ + && rm -rf /tmp/wkhtmltox.deb /var/lib/apt/lists/* + +RUN curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \ + && bash /tmp/dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet \ + && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet \ + && rm /tmp/dotnet-install.sh + +ENV DOTNET_NOLOGO=1 \ + DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + CI=true \ + ROTATIVA_DEMO_URL=http://localhost:5000 \ + ROTATIVA_CHROME_BINARY=/usr/bin/chromium + +WORKDIR /src +COPY . . + +RUN dotnet build Rotativa.AspNetCore.DemoApp \ + && dotnet build Rotativa.AspNetCore.Tests \ + && ln -sf /usr/local/bin/wkhtmltopdf Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa/wkhtmltopdf \ + && ln -sf /usr/local/bin/wkhtmltoimage Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa/wkhtmltoimage + +CMD ["bash", "Rotativa.AspNetCore.Tests/run-tests.sh"] diff --git a/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore b/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore new file mode 100644 index 0000000..090b609 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore @@ -0,0 +1,7 @@ +**/bin +**/obj +**/.vs +.vscode +.git +**/wwwroot/Rotativa/wkhtmltopdf +**/wwwroot/Rotativa/wkhtmltoimage diff --git a/Rotativa.AspNetCore.Tests/PdfTester.cs b/Rotativa.AspNetCore.Tests/PdfTester.cs index aea4530..280c99c 100644 --- a/Rotativa.AspNetCore.Tests/PdfTester.cs +++ b/Rotativa.AspNetCore.Tests/PdfTester.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Rotativa.AspNetCore.Tests @@ -45,6 +46,8 @@ public bool PdfContains(string text) string currentText = PdfTextExtractor.GetTextFromPage(pdfDocument.GetPage(page), strategy); currentText = Encoding.UTF8.GetString(ASCIIEncoding.Convert(Encoding.Default, Encoding.UTF8, Encoding.Default.GetBytes(currentText))); + // Some wkhtmltopdf builds (e.g. macOS) separate words with tabs, so compare on normalized whitespace. + currentText = Regex.Replace(currentText, @"\s+", " "); if (currentText.Contains(text)) return true; pdfReader.Close(); diff --git a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj index 7a97c9f..46d892f 100644 --- a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj +++ b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable @@ -10,17 +10,16 @@ - - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs index 57f7f05..3c4f367 100644 --- a/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs +++ b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs @@ -15,14 +15,36 @@ namespace Rotativa.AspNetCore.Tests { [Trait("Rotativa.AspNetCore", "accessing the demo site home page")] - public class RotativaIntegrationTests + public class RotativaIntegrationTests : IDisposable { ChromeDriver selenium; StringBuilder verificationErrors; + // The demo site to test against. Override with ROTATIVA_DEMO_URL (e.g. "http://localhost:5000" on Linux/CI). + public static IEnumerable DemoSites => new[] + { + new object[] { Environment.GetEnvironmentVariable("ROTATIVA_DEMO_URL") ?? "https://localhost:56246", "Asp.net 10" } + }; + + // Run Chrome headless when ROTATIVA_TESTS_HEADLESS is set, or on CI servers (which set CI=true). + static bool Headless => + IsTrue(Environment.GetEnvironmentVariable("ROTATIVA_TESTS_HEADLESS")) || IsTrue(Environment.GetEnvironmentVariable("CI")); + + static bool IsTrue(string? value) => value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + public RotativaIntegrationTests() { - selenium = new ChromeDriver(); + var options = new ChromeOptions(); + if (Headless) + { + // --no-sandbox and --disable-dev-shm-usage let Chrome start inside containers. + options.AddArguments("--headless=new", "--no-sandbox", "--disable-dev-shm-usage"); + } + // Optional path to a Chrome/Chromium binary, for machines where Selenium can't find or download one. + var chromeBinary = Environment.GetEnvironmentVariable("ROTATIVA_CHROME_BINARY"); + if (!string.IsNullOrEmpty(chromeBinary)) options.BinaryLocation = chromeBinary; + + selenium = new ChromeDriver(options); //selenium = new InternetExplorerDriver(); selenium.Manage().Timeouts().ImplicitWait = new TimeSpan(0, 0, 10); verificationErrors = new StringBuilder(); @@ -37,7 +59,7 @@ public void Dispose() //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] //[InlineData("https://localhost:7059", "Asp.net 6")] - [InlineData("https://localhost:56246", "Asp.net 8")] + [MemberData(nameof(DemoSites))] public void Is_the_site_reachable(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -49,7 +71,7 @@ public void Is_the_site_reachable(string url, string site) //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] //[InlineData("https://localhost:7059", "Asp.net 6")] - [InlineData("https://localhost:56246", "Asp.net 8")] + [MemberData(nameof(DemoSites))] public async Task Contact_PDF_ViewData(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -73,7 +95,7 @@ public async Task Contact_PDF_ViewData(string url, string site) //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] //[InlineData("https://localhost:7059", "Asp.net 6")] - [InlineData("https://localhost:56246", "Asp.net 8")] + [MemberData(nameof(DemoSites))] public async Task Contact_PDF_SpecialCharacters(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -95,7 +117,7 @@ public async Task Contact_PDF_SpecialCharacters(string url, string site) //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] //[InlineData("https://localhost:7059", "Asp.net 6")] - [InlineData("https://localhost:56246", "Asp.net 8")] + [MemberData(nameof(DemoSites))] public async Task Can_create_png_image(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -106,10 +128,10 @@ public async Task Can_create_png_image(string url, string site) using (var wc = new HttpClient()) { var imageResult = await wc.GetAsync(new Uri(pdfHref)); - var image = Image.FromStream(imageResult.Content.ReadAsStream()); + var image = await imageResult.Content.ReadAsByteArrayAsync(); - Assert.NotNull(image); - Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Png); + // PNG file signature; checked directly because System.Drawing is Windows-only. + Assert.True(image.AsSpan().StartsWith(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A })); } } @@ -117,7 +139,7 @@ public async Task Can_create_png_image(string url, string site) //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] //[InlineData("https://localhost:7059", "Asp.net 6")] - [InlineData("https://localhost:56246", "Asp.net 8")] + [MemberData(nameof(DemoSites))] public async Task Can_create_jpg_image(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -128,10 +150,10 @@ public async Task Can_create_jpg_image(string url, string site) using (var wc = new HttpClient()) { var imageResult = await wc.GetAsync(new Uri(pdfHref)); - var image = Image.FromStream(imageResult.Content.ReadAsStream()); + var image = await imageResult.Content.ReadAsByteArrayAsync(); - Assert.NotNull(image); - Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Jpeg); + // JPEG SOI marker; checked directly because System.Drawing is Windows-only. + Assert.True(image.AsSpan().StartsWith(new byte[] { 0xFF, 0xD8, 0xFF })); } } diff --git a/Rotativa.AspNetCore.Tests/run-tests.sh b/Rotativa.AspNetCore.Tests/run-tests.sh new file mode 100644 index 0000000..1d8a690 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/run-tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Starts the demo app, runs the test suite against it, then stops the app. +# Run from the repository root. Expects the projects to be built already. +set -euo pipefail + +url="${ROTATIVA_DEMO_URL:-http://localhost:5000}" + +(cd Rotativa.AspNetCore.DemoApp && exec dotnet run --no-build --urls "$url") & +app_pid=$! +trap 'kill $app_pid 2>/dev/null || true' EXIT + +for _ in $(seq 1 60); do + curl -sf -o /dev/null "$url" && break + sleep 1 +done + +dotnet test Rotativa.AspNetCore.Tests --no-build --logger "console;verbosity=normal" diff --git a/Rotativa.AspNetCore/README.md b/Rotativa.AspNetCore/README.md index 994787c..51b17ce 100644 --- a/Rotativa.AspNetCore/README.md +++ b/Rotativa.AspNetCore/README.md @@ -1,7 +1,7 @@ # Create PDFs and images with .NET Use Rotativa to transform a Razor view into a PDF or image. -This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7 and .NET 8. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7, .NET 8 and .NET 10. ## Install with nuget.org: @@ -11,7 +11,7 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.NET 6 up to 8): +Basic configuration done in Program.cs (.NET 6 up to 10): ```csharp app.UseRotativa(); @@ -22,7 +22,7 @@ or, if using .NET Core 3.1 and .NET 5: app.UseRotativa(env); ``` -Make sure you have a folder with the wkhtmltopdf.exe file accessible by the process running the web app. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` +Make sure you have a folder with the wkhtmltopdf.exe file (and wkhtmltoimage.exe for images) accessible by the process running the web app. On Linux and macOS the files are named wkhtmltopdf and wkhtmltoimage, without the .exe extension: use the builds for your platform from https://wkhtmltopdf.org/downloads.html. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` ## Usage diff --git a/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj b/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj index 421b9c2..cf8d832 100644 --- a/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj +++ b/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj @@ -1,18 +1,18 @@  - netstandard2.0;netcoreapp3.1;net6;net5.0;net7;net8 + netstandard2.0;netcoreapp3.1;net6;net5.0;net7;net8;net10.0 true - 1.4.0 + 1.5.0 true https://github.com/webgio/Rotativa.AspNetCore https://github.com/webgio/Rotativa.AspNetCore Viva Software di Bozio Giorgio Giorgio Bozio - Rotativa: PDF easy creation for Asp.Net Core, Asp.Net 6 and Asp.Net 8 - Multi target enabling .net core 3.1, .net 5, .net 6, .net 7, .net 8 + Rotativa: PDF easy creation for Asp.Net Core, Asp.Net 6, Asp.Net 8 and Asp.Net 10 + Multi target enabling .net core 3.1, .net 5, .net 6, .net 7, .net 8, .net 10 PDF AspNetCore - 2024 Viva Software di Bozio Giorgio + 2026 Viva Software di Bozio Giorgio MIT README.md @@ -21,34 +21,12 @@ --> - - - - - - - - - - - - - - - - - - - - - + - +