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
26 changes: 15 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
runtime: linux-x64
artifact: nfury-linux-x64
- os: ubuntu-latest
runtime: linux-arm64
artifact: nfury-linux-arm64
- os: windows-latest
runtime: win-x64
artifact: nfury-win-x64
- os: macos-latest
runtime: osx-x64
artifact: nfury-osx-x64
- os: macos-latest
runtime: osx-arm64
artifact: nfury-osx-arm64

steps:
- uses: actions/checkout@v4
Expand All @@ -30,23 +35,22 @@ jobs:
with:
dotnet-version: "10.0.x"

- name: Install cross-compilation tools (Linux ARM64)
if: matrix.runtime == 'linux-arm64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --configuration Release --no-restore

- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal

- name: Publish
- name: Publish Native AOT
shell: bash
run: |
dotnet publish src/NFury/NFury.csproj \
--configuration Release \
--runtime ${{ matrix.runtime }} \
--self-contained true \
-p:PublishSingleFile=true \
-p:PublishTrimmed=true \
-p:PublishAot=true \
--output ./publish/${{ matrix.artifact }}

- name: Upload artifact
Expand Down
2 changes: 1 addition & 1 deletion src/NFury/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"NFury": {
"commandName": "Project",
"commandLineArgs": "run -u 10 -d 30 http://localhost:5205/todos"
"commandLineArgs": "server"
}
}
}
1 change: 0 additions & 1 deletion src/NFury/Web/Data/SqliteDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ public class Project
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }

// Authentication endpoint configuration
public string? AuthUrl { get; set; }
public string? AuthMethod { get; set; }
public string? AuthContentType { get; set; }
Expand Down
9 changes: 0 additions & 9 deletions src/NFury/Web/JsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

namespace NFury.Web;

// Response DTOs to replace anonymous types (required for Native AOT)
public record TestIdResponse(string TestId);
public record ErrorResponse(string Error);
public record IsRunningResponse(bool IsRunning);
Expand All @@ -19,43 +18,35 @@ public record ExecutionListResponse(List<TestExecution> Executions, int Total);
[JsonSerializable(typeof(StatusCodeResult))]
[JsonSerializable(typeof(RealTimeMetric))]
[JsonSerializable(typeof(TestProgressUpdate))]
// Response DTOs
[JsonSerializable(typeof(TestIdResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(IsRunningResponse))]
[JsonSerializable(typeof(ExecutionListResponse))]
// Entity types
[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(TestEndpoint))]
[JsonSerializable(typeof(TestExecution))]
[JsonSerializable(typeof(TestMetricSnapshot))]
// DTOs
[JsonSerializable(typeof(ProjectDto))]
[JsonSerializable(typeof(ProjectAuthDto))]
[JsonSerializable(typeof(EndpointDto))]
[JsonSerializable(typeof(EndpointTestStartRequest))]
[JsonSerializable(typeof(ExecutionStatistics))]
// Export/Import DTOs
[JsonSerializable(typeof(ProjectExportDto))]
[JsonSerializable(typeof(ProjectExportData))]
[JsonSerializable(typeof(EndpointExportData))]
[JsonSerializable(typeof(ExecutionExportData))]
[JsonSerializable(typeof(ProjectImportResult))]
[JsonSerializable(typeof(List<EndpointExportData>))]
[JsonSerializable(typeof(List<ExecutionExportData>))]
// Lists
[JsonSerializable(typeof(List<Project>))]
[JsonSerializable(typeof(List<TestEndpoint>))]
[JsonSerializable(typeof(List<TestExecution>))]
[JsonSerializable(typeof(List<TestMetricSnapshot>))]
// Dictionaries
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<int, StatusCodeResult>))]
// SignalR message types
[JsonSerializable(typeof(SignalRConnectedMessage))]
[JsonSerializable(typeof(SignalRTestIdMessage))]
[JsonSerializable(typeof(SignalRTestErrorMessage))]
// Basic types
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(int))]
Expand Down
49 changes: 45 additions & 4 deletions src/NFury/Web/Services/LoadTestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,25 @@ public async Task<AuthenticationResult> AuthenticateAsync(AuthenticationConfig c
/// Extracts a token from JSON response using a dot-notation path
/// </summary>
/// <param name="json">The JSON response content</param>
/// <param name="tokenPath">The dot-notation path to the token (e.g., "access_token" or "data.token")</param>
/// <param name="tokenPath">The dot-notation path to the token (e.g., "$.access_token", "access_token" or "data.token")</param>
/// <returns>The extracted token or null if not found</returns>
private static string? ExtractToken(string json, string tokenPath)
{
try
{
using var doc = JsonDocument.Parse(json);
var pathParts = tokenPath.Split('.');

var cleanPath = tokenPath;
if (cleanPath.StartsWith("$.", StringComparison.Ordinal))
{
cleanPath = cleanPath[2..];
}
else if (cleanPath.StartsWith('$'))
{
cleanPath = cleanPath[1..];
}

var pathParts = cleanPath.Split('.', StringSplitOptions.RemoveEmptyEntries);
JsonElement current = doc.RootElement;

foreach (var part in pathParts)
Expand Down Expand Up @@ -179,6 +190,26 @@ public async Task<string> StartEndpointTestAsync(int endpointId, int? usersOverr
{
authConfig = JsonSerializer.Deserialize<AuthenticationConfig>(endpoint.AuthenticationJson, AppJsonContext.Default.AuthenticationConfig);
}
else if (endpoint.RequiresAuth)
{
var project = await _projectService.GetProjectByIdAsync(endpoint.ProjectId);
if (project != null && !string.IsNullOrEmpty(project.AuthUrl))
{
authConfig = new AuthenticationConfig
{
Url = project.AuthUrl,
Method = project.AuthMethod ?? "POST",
Body = project.AuthBody,
ContentType = project.AuthContentType ?? "application/json",
Headers = !string.IsNullOrEmpty(project.AuthHeadersJson)
? JsonSerializer.Deserialize<Dictionary<string, string>>(project.AuthHeadersJson, AppJsonContext.Default.DictionaryStringString)
: null,
TokenPath = project.AuthTokenPath ?? "$.access_token",
HeaderName = project.AuthHeaderName ?? "Authorization",
HeaderPrefix = project.AuthHeaderPrefix ?? "Bearer"
};
}
}

var targetRequests = endpoint.Requests;
var targetDuration = endpoint.Duration;
Expand Down Expand Up @@ -217,7 +248,12 @@ public async Task<string> StartEndpointTestAsync(int endpointId, int? usersOverr
throw new InvalidOperationException($"Authentication failed: {authResult.Error}");
}

_authToken = $"{request.Authentication.HeaderPrefix}{authResult.Token}";
var prefix = request.Authentication.HeaderPrefix ?? string.Empty;
if (!string.IsNullOrEmpty(prefix) && !prefix.EndsWith(' '))
{
prefix += " ";
}
_authToken = $"{prefix}{authResult.Token}";
await _hubContext.Clients.All.SendAsync("AuthenticationSuccess", new SignalRTestIdMessage { TestId = _currentTestId });
}

Expand Down Expand Up @@ -265,7 +301,12 @@ public async Task<string> StartAdHocTestAsync(LoadTestRequest request)
throw new InvalidOperationException($"Authentication failed: {authResult.Error}");
}

_authToken = $"{request.Authentication.HeaderPrefix}{authResult.Token}";
var prefix = request.Authentication.HeaderPrefix ?? string.Empty;
if (!string.IsNullOrEmpty(prefix) && !prefix.EndsWith(' '))
{
prefix += " ";
}
_authToken = $"{prefix}{authResult.Token}";
await _hubContext.Clients.All.SendAsync("AuthenticationSuccess", new SignalRTestIdMessage { TestId = _currentTestId });
}

Expand Down
Loading