Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ jobs:
shell: bash
run: |
node -e "const f=require('./index.js'); if(!f.FusionApp||!f.status) throw new Error('smoke failed'); console.log('ok', f.status.HTTP_SUCCESS)"
node --test ../../tests/node/unit/*.test.js

csharp:
name: C# package
Expand All @@ -152,3 +153,5 @@ jobs:
run: cargo build -p fusion-ffi --release
- name: Build FusionFramework
run: dotnet build -c Release bindings/csharp/FusionFramework/FusionFramework.csproj
- name: Test C# bindings
run: dotnet test tests/csharp/FusionFramework.Tests/FusionFramework.Tests.csproj -c Release
3 changes: 3 additions & 0 deletions bindings/csharp/FusionFramework/FusionFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<!-- Prefer installed targeting packs over downloading *.App.Ref from NuGet. -->
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="FusionFramework.Tests" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
<Content Include="runtimes/**" Pack="true" PackagePath="runtimes" CopyToOutputDirectory="PreserveNewest" />
Expand Down
16 changes: 16 additions & 0 deletions bindings/csharp/FusionFramework/Swagger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,22 @@ static JsonObject BuildOpenApi(SwaggerConfig swagger, string? version = null)
return spec;
}

internal static JsonObject CreateTestSpec(string? version = null)
{
var swagger = new SwaggerConfig
{
Path = "/swagger",
PageTitle = "Fusion API Docs",
Info = new JsonObject
{
["title"] = "fusion-framework",
["version"] = "1.0.0",
},
Ui = new JsonObject(),
};
return BuildOpenApi(swagger, version);
}

static void FillPaths(JsonObject paths, string? versionFilter)
{
foreach (var entry in Route.Snapshot())
Expand Down
11 changes: 11 additions & 0 deletions bindings/csharp/FusionFramework/Testing/FusionTestSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Nodes;

namespace FusionFramework.Testing;

/// <summary>Helpers used by the repository test suite (not required at runtime).</summary>
public static class FusionTestSupport
{
public static JsonObject OpenApiSpec(string? version = null) => SwaggerDocs.CreateTestSpec(version);

public static void ClearRoutes() => Route.Clear();
}
38 changes: 38 additions & 0 deletions crates/fusion-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,40 @@ function collectRouteVersions() {
return { versions, hasUnversioned }
}

function clearRouteRegistry() {
registry.length = 0
}

function testSwaggerConfig() {
return {
path: '/swagger',
info: { title: 'fusion-framework', version: '1.0.0' },
servers: [],
auth: { schemes: {}, global: [], oauth: {} },
navbar: {
enabled: true,
showUrlInput: false,
showUrlInputSet: true,
urlsSet: false,
urls: [],
},
ui: {},
pageTitle: 'Fusion API Docs',
}
}

function openapiSpec(version = null) {
return buildOpenApi(testSwaggerConfig(), version)
}

function routeVersions() {
return collectRouteVersions().versions
}

function hasUnversionedRoutes() {
return collectRouteVersions().hasUnversioned
}

function swaggerVersionUrls(prefix) {
const { versions, hasUnversioned } = collectRouteVersions()
const urls = versions.map((label) => ({
Expand Down Expand Up @@ -1468,6 +1502,10 @@ module.exports = {
parsePagination,
paginatedBody,
renderTemplate,
clearRouteRegistry,
openapiSpec,
routeVersions,
hasUnversionedRoutes,
getHttpMethods: () => HTTP_METHODS,
apiResourceNameJs: native.apiResourceNameJs,
resolveRoutePathJs: native.resolveRoutePathJs,
Expand Down
38 changes: 30 additions & 8 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ Central test layout for the monorepo. Binding code stays in `crates/` and `bindi

```
tests/
├── python/ # pytest — primary binding test suite
├── conftest.py # route/middleware isolation per test
│ └── unit/ # fast, no live server
├── fixtures/ # shared JSON, templates, sample apps
└── scripts/ # local runners (CI uses workflow steps)
├── python/ # pytest
├── node/ # node --test
── csharp/ # dotnet test (xUnit)
├── fixtures/
└── scripts/
```

| Layer | Location | Runner |
|-------|----------|--------|
| Rust core | `crates/fusion-core/src/**/*.rs` (`#[cfg(test)]`) | `cargo test -p fusion-core` |
| Python binding | `tests/python/` | `pytest` (see below) |
| Node | syntax + smoke in CI | `node --check crates/fusion-node/index.js` |
| C# | build in CI | `dotnet build bindings/csharp/...` |
| Python binding | `tests/python/` | `pytest` |
| Node binding | `tests/node/` | `node --test` (see below) |
| C# binding | `tests/csharp/` | `dotnet test` |

## Python (pytest)

Expand All @@ -43,6 +43,28 @@ Run one file:
pytest tests/python/unit/test_http_route.py -q
```

## Node (`node --test`)

Build the native addon first:

```bash
cd crates/fusion-node && npm install && npm run build:debug
```

Run tests:

```bash
./tests/scripts/run-node.sh
```

## C# (xUnit)

```bash
./tests/scripts/run-csharp.sh
```

Requires `fusion_ffi` built (`cargo build -p fusion-ffi`).

## Full local verification

```bash
Expand Down
22 changes: 22 additions & 0 deletions tests/csharp/FusionFramework.Tests/FusionFramework.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>FusionFramework.Tests</RootNamespace>
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../bindings/csharp/FusionFramework/FusionFramework.csproj" />
</ItemGroup>
</Project>
76 changes: 76 additions & 0 deletions tests/csharp/FusionFramework.Tests/MiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Text.Json.Nodes;
using FusionFramework;
using Xunit;

namespace FusionFramework.Tests;

public class MiddlewareTests
{
static object Handler(FusionRequest request) =>
new Dictionary<string, object?>
{
["status"] = 200,
["body"] = new Dictionary<string, object?> { ["state"] = request.State },
};

[Fact]
public void BearerJwt_populates_state()
{
const string token =
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZXMiOlsiYWRtaW4iXX0.";
var request = new FusionRequest
{
Headers = new Dictionary<string, string> { ["Authorization"] = $"Bearer {token}" },
};

var result = Middleware.RunChain(
request,
new[] { Middleware.BearerJwt() },
Handler) as Dictionary<string, object?>;

Assert.NotNull(result);
Assert.Equal(200, result["status"]);
var body = Assert.IsType<Dictionary<string, object?>>(result["body"]);
var state = Assert.IsType<Dictionary<string, JsonNode?>>(body["state"]);
Assert.Equal("1", state["jwt"]!["sub"]!.GetValue<string>());
}

[Fact]
public void RequireRoles_blocks_missing_role()
{
var request = new FusionRequest
{
State = new Dictionary<string, JsonNode?>
{
["jwt"] = System.Text.Json.Nodes.JsonNode.Parse("{\"roles\":[\"user\"]}"),
},
};

var result = Middleware.RunChain(
request,
new[] { Middleware.RequireRoles("admin") },
Handler) as Dictionary<string, object?>;

Assert.NotNull(result);
Assert.Equal(403, result["status"]);
}

[Fact]
public void Cors_answers_options_with_204()
{
var request = new FusionRequest
{
Method = "OPTIONS",
Path = "/api",
Headers = new Dictionary<string, string> { ["Origin"] = "https://example.com" },
};

var result = Middleware.RunChain(request, new[] { Middleware.Cors() }, Handler)
as Dictionary<string, object?>;

Assert.NotNull(result);
Assert.Equal(204, result["status"]);
var headers = Assert.IsType<Dictionary<string, string>>(result["headers"]);
Assert.False(string.IsNullOrEmpty(headers["Access-Control-Allow-Origin"]));
}
}
77 changes: 77 additions & 0 deletions tests/csharp/FusionFramework.Tests/RouteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Text.Json.Nodes;
using FusionFramework;
using FusionFramework.Testing;
using Xunit;

namespace FusionFramework.Tests;

public class RouteTests
{
public RouteTests() => FusionTestSupport.ClearRoutes();

[Fact]
public void ResolvePath_expands_module_token()
{
var path = Route.ResolvePath("/api/[module]", "ProductModule");
Assert.Equal("/api/product", path);
}

[Fact]
public void Register_mounts_convention_and_custom_http_slots()
{
Route.Register(typeof(ProductModule), "/api/[module]");

var entry = Assert.Single(Route.Snapshot());
Assert.Equal("/api/product", entry.ClassBasePath);
Assert.Contains(entry.Slots, s => s.Path == "/api/product" && s.HttpMethod == "get");
Assert.Contains(entry.Slots, s => s.Path == "/api/product/catalog/catalog" && s.HttpMethod == "get");
}

[Fact]
public void OpenApi_lists_registered_api_paths()
{
Route.Register(typeof(ProductModule), "/api/[module]", tags: new[] { "products" });

var spec = FusionTestSupport.OpenApiSpec();
var paths = spec["paths"]!.AsObject();
Assert.True(paths.ContainsKey("/api/product"));
Assert.True(paths["/api/product"]!.AsObject().ContainsKey("get"));
}

[Fact]
public void Template_routes_are_omitted_from_openapi()
{
Route.Register(typeof(SampleHomePage), "/pages/home");
Route.Register(typeof(ProductModule), "/api/[module]", version: "v1");

var combined = FusionTestSupport.OpenApiSpec();
Assert.False(combined["paths"]!.AsObject().ContainsKey("/pages/home"));

var v1 = FusionTestSupport.OpenApiSpec("v1");
Assert.False(v1["paths"]!.AsObject().ContainsKey("/pages/home"));
Assert.True(v1["paths"]!.AsObject().ContainsKey("/v1/api/product"));
}

[Route("/api/[module]")]
sealed class ProductModule : FusionBaseApi
{
public object Get() => Response(new { ok = true });

[HttpGet("catalog/[action]")]
public object CatalogAction() => Response(new { items = Array.Empty<object>() });
}

[Route("/pages/home")]
sealed class SampleHomePage : FusionBaseTemplate
{
static SampleHomePage()
{
Template = "home/index.html";
}

public override Dictionary<string, JsonNode?> Context() => new()
{
["title"] = "Home",
};
}
}
4 changes: 4 additions & 0 deletions tests/node/helpers/load-fusion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const path = require('path')

/** Resolve the local fusion-framework package from the monorepo. */
module.exports = require(path.resolve(__dirname, '../../../crates/fusion-node'))
Loading
Loading