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
13 changes: 6 additions & 7 deletions src/Langfuse.Client/LangfuseClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Web;
using Langfuse.Client.Caching;
using Langfuse.Client.Prompts;
using Langfuse.Core;
Expand Down Expand Up @@ -86,9 +85,9 @@ private static LangfuseOptions MergeOptions(LangfuseClientOptions? options)
/// <summary>
/// Gets a text prompt by name.
/// </summary>
/// <param name="name">The name of the prompt.</param>
/// <param name="name">The name of the prompt. Supports names with spaces and special characters.</param>
/// <param name="version">Optional specific version number.</param>
/// <param name="label">Optional label (e.g., "production", "staging"). Defaults to "production" if neither version nor label is specified.</param>
/// <param name="label">Optional label (e.g., "production", "staging"). Defaults to "production" if neither version nor label is specified. Supports labels with spaces.</param>
/// <param name="fallback">Optional fallback prompt to use if fetch fails.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The text prompt.</returns>
Expand Down Expand Up @@ -136,9 +135,9 @@ public async Task<TextPrompt> GetPromptAsync(
/// <summary>
/// Gets a chat prompt by name.
/// </summary>
/// <param name="name">The name of the prompt.</param>
/// <param name="name">The name of the prompt. Supports names with spaces and special characters.</param>
/// <param name="version">Optional specific version number.</param>
/// <param name="label">Optional label (e.g., "production", "staging"). Defaults to "production" if neither version nor label is specified.</param>
/// <param name="label">Optional label (e.g., "production", "staging"). Defaults to "production" if neither version nor label is specified. Supports labels with spaces.</param>
/// <param name="fallback">Optional fallback prompt to use if fetch fails.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The chat prompt.</returns>
Expand Down Expand Up @@ -203,7 +202,7 @@ private async Task<PromptApiResponse> FetchPromptAsync(

private static string BuildPromptPath(string name, int? version, string? label)
{
var encodedName = HttpUtility.UrlEncode(name);
var encodedName = Uri.EscapeDataString(name);
var path = $"{LangfuseConstants.PromptsPath}/{encodedName}";

var queryParams = new List<string>();
Expand All @@ -215,7 +214,7 @@ private static string BuildPromptPath(string name, int? version, string? label)

if (!string.IsNullOrEmpty(label))
{
queryParams.Add($"label={HttpUtility.UrlEncode(label)}");
queryParams.Add($"label={Uri.EscapeDataString(label)}");
}

if (queryParams.Count > 0)
Expand Down
143 changes: 143 additions & 0 deletions tests/Langfuse.Client.Tests/LangfuseClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System.Net;
using System.Text.Json;
using Langfuse.Client;
using Langfuse.Core;
using Moq;
using Moq.Protected;
using Xunit;

namespace Langfuse.Client.Tests;

public class LangfuseClientTests
{
private static object CreateMockPromptResponse(string name, int version = 1, string[]? labels = null)
{
return new
{
id = "test-id",
name = name,
version = version,
type = "text",
prompt = "Test content",
labels = labels ?? new[] { "production" },
tags = Array.Empty<string>(),
config = new { },
createdAt = DateTime.UtcNow.ToString("o"),
updatedAt = DateTime.UtcNow.ToString("o")
};
}

private static LangfuseClient CreateTestClient(object mockResponse, out string? capturedPath)
{
string? actualRequestPath = null;

var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>
{
actualRequestPath = request.RequestUri?.PathAndQuery;
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonSerializer.Serialize(mockResponse))
};
});

var httpClient = new HttpClient(mockHandler.Object)
{
BaseAddress = new Uri("https://cloud.langfuse.com")
};

var options = new LangfuseClientOptions
{
BaseUrl = "https://cloud.langfuse.com",
PublicKey = "test-key",
SecretKey = "test-secret"
};

capturedPath = actualRequestPath;
return new LangfuseClient(options, httpClient);
}

[Fact]
public async Task GetPromptAsync_WithSpacesInName_EncodesSpacesAsPercent20()
{
// Arrange
var promptName = "test prompt";
var mockResponse = CreateMockPromptResponse(promptName);
using var client = CreateTestClient(mockResponse, out _);

// Act
var result = await client.GetPromptAsync(promptName);

// Assert - The test succeeds if no exception is thrown and the name matches
Assert.Equal(promptName, result.Name);
}

[Fact]
public async Task GetPromptAsync_WithoutSpacesInName_EncodesCorrectly()
{
// Arrange
var promptName = "test-prompt";
var mockResponse = CreateMockPromptResponse(promptName);
using var client = CreateTestClient(mockResponse, out _);

// Act
var result = await client.GetPromptAsync(promptName);

// Assert
Assert.Equal(promptName, result.Name);
}

[Fact]
public async Task GetPromptAsync_WithSpacesInNameAndVersion_EncodesCorrectly()
{
// Arrange
var promptName = "my test prompt";
var version = 2;
var mockResponse = CreateMockPromptResponse(promptName, version);
using var client = CreateTestClient(mockResponse, out _);

// Act
var result = await client.GetPromptAsync(promptName, version: version);

// Assert
Assert.Equal(promptName, result.Name);
Assert.Equal(version, result.Version);
}

[Fact]
public async Task GetPromptAsync_WithSpacesInLabel_EncodesCorrectly()
{
// Arrange
var promptName = "test prompt";
var label = "my label";
var mockResponse = CreateMockPromptResponse(promptName, labels: new[] { label });
using var client = CreateTestClient(mockResponse, out _);

// Act
var result = await client.GetPromptAsync(promptName, label: label);

// Assert
Assert.Equal(promptName, result.Name);
}

[Fact]
public async Task GetPromptAsync_WithSpecialCharactersInName_EncodesCorrectly()
{
// Arrange
var promptName = "test/prompt&name=value";
var mockResponse = CreateMockPromptResponse(promptName);
using var client = CreateTestClient(mockResponse, out _);

// Act
var result = await client.GetPromptAsync(promptName);

// Assert
Assert.Equal(promptName, result.Name);
}
}