Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2f708a4
Implement UpstageConnector using OpenAI SDK compatibility
gremh97 Sep 21, 2025
114e6e1
Merge branch 'main' into feature/235-connector-implementation-inherit…
gremh97 Sep 23, 2025
095a6d1
Add UpstageConnector unit tests
gremh97 Sep 23, 2025
90aec64
#235 Replace magic strings with constants in UpstageConnectorTests
gremh97 Sep 28, 2025
8d7f18b
#235 Refactor test assertions from xUnit Assert.Throws to Shouldly pa…
gremh97 Sep 28, 2025
52f4d46
Merge upstream/main into feature/235-connector-implementation-inherit…
gremh97 Sep 28, 2025
ae18a9b
#235 Add inheritance relationship test for UpstageConnector
gremh97 Sep 28, 2025
23cd10c
#235 Add comprehensive test coverage for UpstageConnector
gremh97 Sep 30, 2025
61778c2
#235 Refactor test structure to separate Act and Assert sections
gremh97 Sep 30, 2025
f3c67a9
Merge branch 'main' into feature/235-connector-implementation-inherit…
gremh97 Sep 30, 2025
1c9a8b9
fix: Update UpstageConnector exception test to expect InvalidOperatio…
gremh97 Sep 30, 2025
ff2b2dc
refactor: Reorganize UpstageConnector test methods for better readabi…
gremh97 Oct 3, 2025
e0183f3
test: Enhance UpstageConnector CreateChatClientAsync validation tests
gremh97 Oct 3, 2025
6d3c438
test: Standardize UpstageConnector error message format with OpenAI p…
gremh97 Oct 4, 2025
982f381
Merge branch 'main' into feature/235-connector-implementation-inherit…
gremh97 Oct 4, 2025
3d598d2
test: Add comprehensive whitespace validation tests for CreateChatCli…
gremh97 Oct 4, 2025
5f5639e
refactor: improve parameter naming in UpstageConnector tests
gremh97 Oct 4, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public static async Task<IChatClient> CreateChatClientAsync(AppSettings settings
ConnectorType.GitHubModels => new GitHubModelsConnector(settings),
ConnectorType.HuggingFace => new HuggingFaceConnector(settings),
ConnectorType.OpenAI => new OpenAIConnector(settings),
ConnectorType.Upstage => new UpstageConnector(settings),
_ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.")
};

Expand Down
64 changes: 64 additions & 0 deletions src/OpenChat.PlaygroundApp/Connectors/UpstageConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.ClientModel;

using Microsoft.Extensions.AI;

using OpenAI;

using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;

namespace OpenChat.PlaygroundApp.Connectors;

/// <summary>
/// This represents the connector entity for Upstage.
/// </summary>
public class UpstageConnector(AppSettings settings) : LanguageModelConnector(settings.Upstage)
{
/// <inheritdoc/>
public override bool EnsureLanguageModelSettingsValid()
{
var settings = this.Settings as UpstageSettings;
if (settings is null)
{
throw new InvalidOperationException("Missing configuration: Upstage.");
}

if (string.IsNullOrWhiteSpace(settings.BaseUrl?.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl.");
}

if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: Upstage:ApiKey.");
}

if (string.IsNullOrWhiteSpace(settings.Model?.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: Upstage:Model.");
}

return true;
}

/// <inheritdoc/>
public override async Task<IChatClient> GetChatClientAsync()
{
var settings = this.Settings as UpstageSettings;

var credential = new ApiKeyCredential(settings?.ApiKey ??
throw new InvalidOperationException("Missing configuration: Upstage:ApiKey."));

var options = new OpenAIClientOptions
{
Endpoint = new Uri(settings.BaseUrl ??
throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl."))
};

var client = new OpenAIClient(credential, options);
var chatClient = client.GetChatClient(settings.Model)
.AsIChatClient();

return await Task.FromResult(chatClient).ConfigureAwait(false);
}
}
261 changes: 261 additions & 0 deletions test/OpenChat.PlaygroundApp.Tests/Connectors/UpstageConnectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
using OpenChat.PlaygroundApp.Abstractions;
using OpenChat.PlaygroundApp.Configurations;
using OpenChat.PlaygroundApp.Connectors;

namespace OpenChat.PlaygroundApp.Tests.Connectors;



public class UpstageConnectorTests
{
private const string BaseUrl = "https://api.upstage.ai/v1/solar";
private const string ApiKey = "test-api-key";
private const string Model = "solar-1-mini-chat";

private static AppSettings BuildAppSettings(string? baseUrl = BaseUrl, string? apiKey = ApiKey, string? model = Model)
{
return new AppSettings
{
ConnectorType = ConnectorType.Upstage,
Upstage = new UpstageSettings
{
BaseUrl = baseUrl,
ApiKey = apiKey,
Model = model
}
};
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(typeof(LanguageModelConnector), typeof(UpstageConnector), true)]
[InlineData(typeof(UpstageConnector), typeof(LanguageModelConnector), false)]
public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected)
{
// Act
var result = baseType.IsAssignableFrom(derivedType);

// Assert
result.ShouldBe(expected);
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw()
{
// Arrange
var appSettings = new AppSettings { ConnectorType = ConnectorType.Upstage, Upstage = null };
var connector = new UpstageConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain("Missing configuration: Upstage.");
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("", typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData(" ", typeof(InvalidOperationException), "Upstage:BaseUrl")]
public void Given_Invalid_BaseUrl_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? baseUrl, Type expectedType, string expectedMessage)
{
// Arrange
var appSettings = BuildAppSettings(baseUrl: baseUrl);
var connector = new UpstageConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow(expectedType)
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("", typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData(" ", typeof(InvalidOperationException), "Upstage:ApiKey")]
public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedType, string expectedMessage)
{
// Arrange
var appSettings = BuildAppSettings(apiKey: apiKey);
var connector = new UpstageConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow(expectedType)
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "Upstage:Model")]
[InlineData("", typeof(InvalidOperationException), "Upstage:Model")]
[InlineData(" ", typeof(InvalidOperationException), "Upstage:Model")]
public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage)
{
// Arrange
var appSettings = BuildAppSettings(model: model);
var connector = new UpstageConnector(appSettings);

// Act
Action action = () => connector.EnsureLanguageModelSettingsValid();

// Assert
action.ShouldThrow(expectedType)
.Message.ShouldContain(expectedMessage);
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True()
{
// Arrange
var appSettings = BuildAppSettings();
var connector = new UpstageConnector(appSettings);

// Act
var result = connector.EnsureLanguageModelSettingsValid();

// Assert
result.ShouldBeTrue();
}

[Trait("Category", "UnitTest")]
[Fact]
public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should_Return_ChatClient()
{
// Arrange
var settings = BuildAppSettings();
var connector = new UpstageConnector(settings);

// Act
var client = await connector.GetChatClientAsync();

// Assert
client.ShouldNotBeNull();
}

[Trait("Category", "UnitTest")]
[Fact]
public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Should_Throw()
{
// Arrange
var appSettings = new AppSettings { ConnectorType = ConnectorType.Upstage, Upstage = null };
var connector = new UpstageConnector(appSettings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow<InvalidOperationException>()
.Message.ShouldContain("Missing configuration: Upstage:ApiKey.");
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("", typeof(ArgumentException), "key")]
public void Given_Missing_ApiKey_When_GetChatClient_Invoked_Then_It_Should_Throw(string? apiKey, Type expected, string message)
{
// Arrange
var settings = BuildAppSettings(apiKey: apiKey);
var connector = new UpstageConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("", typeof(UriFormatException), "empty")]
public void Given_Missing_BaseUrl_When_GetChatClient_Invoked_Then_It_Should_Throw(string? baseUrl, Type expected, string message)
{
// Arrange
var settings = BuildAppSettings(baseUrl: baseUrl);
var connector = new UpstageConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData(null, typeof(ArgumentNullException), "model")]
[InlineData("", typeof(ArgumentException), "model")]
public void Given_Missing_Model_When_GetChatClient_Invoked_Then_It_Should_Throw(string? model, Type expected, string message)
{
// Arrange
var settings = BuildAppSettings(model: model);
var connector = new UpstageConnector(settings);

// Act
Func<Task> func = async () => await connector.GetChatClientAsync();

// Assert
func.ShouldThrow(expected)
.Message.ShouldContain(message);
}

[Trait("Category", "UnitTest")]
[Fact]
public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Return_ChatClient()
{
// Arrange
var settings = BuildAppSettings();

// Act
var client = await LanguageModelConnector.CreateChatClientAsync(settings);

// Assert
client.ShouldNotBeNull();
}

[Trait("Category", "UnitTest")]
[Theory]
[InlineData("apiKey", null, typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("apiKey", "", typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("apiKey", " ", typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("apiKey", "\t\n\r", typeof(InvalidOperationException), "Upstage:ApiKey")]
[InlineData("baseUrl", null, typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("baseUrl", "", typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("baseUrl", " ", typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("baseUrl", "\t\n\r", typeof(InvalidOperationException), "Upstage:BaseUrl")]
[InlineData("model", null, typeof(InvalidOperationException), "Upstage:Model")]
[InlineData("model", "", typeof(InvalidOperationException), "Upstage:Model")]
[InlineData("model", " ", typeof(InvalidOperationException), "Upstage:Model")]
[InlineData("model", "\t\n\r", typeof(InvalidOperationException), "Upstage:Model")]
public void Given_Invalid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw(string parameterName, string? invalidValue, Type expectedType, string expectedMessage)
{
// Arrange
var settings = parameterName switch
{
"apiKey" => BuildAppSettings(apiKey: invalidValue),
"baseUrl" => BuildAppSettings(baseUrl: invalidValue),
"model" => BuildAppSettings(model: invalidValue),
_ => throw new ArgumentException($"Unknown parameter: {parameterName}")
};

// Act
Func<Task> func = async () => await LanguageModelConnector.CreateChatClientAsync(settings);

// Assert
func.ShouldThrow(expectedType)
.Message.ShouldContain(expectedMessage);
}
}