Added a home page prompt for missing API keys of enterprise providers (#1056)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Dominic NeuburgandThorsten Sommer authored and GitHub committed 2026-10-19 09:12:12 +02:00
1 parent 1f8d72aa45
commit 7d35536c87
29 files changed
+920 -31

No files matched your search

@@ -0,0 +1,44 @@
using AIStudio.Provider;
using AIStudio.Tools.Services;
namespace AIStudio.Tests.Tools;
/// <summary>
/// Checks which secrets AI Studio removes from the OS keyring when an organization changes its encryption secret.
/// </summary>
/// <remarks>
/// Only what the configuration of the organization brought along goes, since the next start of the configuration
/// stores it again with the new secret. A key the user entered for a provider which allows it has no such way
/// back: removing it made every user enter the key once more, and the home page asked them all again.
/// </remarks>
[TestFixture]
public sealed class EnterpriseManagedSecretsTests
{
private static readonly Guid PLUGIN_ID = new("44444444-4444-4444-4444-444444444444");
[Test]
public void TheKeyOfTheOrganizationGoes()
{
Assert.That(EnterpriseEnvironmentService.HoldsSecretOfTheOrganization(ProviderOfTheOrganization()), Is.True);
}
[Test]
public void TheKeyTheUserEnteredStays()
{
Assert.That(EnterpriseEnvironmentService.HoldsSecretOfTheOrganization(ProviderOfTheOrganization() with { AllowUserProvidedAPIKey = true }), Is.False);
}
[Test]
public void TheKeyTheHomePageAskedForStays()
{
Assert.That(EnterpriseEnvironmentService.HoldsSecretOfTheOrganization(ProviderOfTheOrganization() with { AllowUserProvidedAPIKey = true, PromptForUserAPIKey = true }), Is.False);
}
[Test]
public void AProviderOfTheUsersOwnIsNoConcernOfTheOrganization()
{
Assert.That(EnterpriseEnvironmentService.HoldsSecretOfTheOrganization(new AIStudio.Settings.Provider(1, Guid.NewGuid().ToString(), "My own provider", LLMProviders.OPEN_AI, new Model("gpt-6", "GPT-6"))), Is.False);
}
private static AIStudio.Settings.Provider ProviderOfTheOrganization() => new(1, Guid.NewGuid().ToString(), "Provider of the organization", LLMProviders.OPEN_AI, new Model("gpt-6", "GPT-6"), IsEnterpriseConfiguration: true, EnterpriseConfigurationPluginId: PLUGIN_ID);
}
@@ -206,6 +206,30 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
[TestCase(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER)]
public void AnUnencryptedAPIKeyIsLeftOut(PluginConfigurationObjectType type) => AssertAPIKeyLeftOut(Import(ExportProvider(type, API_KEY).Snippet, type));
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
[TestCase(PluginConfigurationObjectType.EMBEDDING_PROVIDER)]
[TestCase(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER)]
public void AnImportedProviderNeverAsksForTheAPIKeyOfTheUser(PluginConfigurationObjectType type)
{
//
// An export never writes the option, but a snippet may come from a configuration plugin of an
// organization. The option implies the user-provided key, so both have to go:
//
var snippet = ExportProvider(type, null).Snippet;
var tableStart = snippet.IndexOf("= {", StringComparison.Ordinal) + "= {".Length;
var snippetWithPrompt = snippet.Insert(tableStart, """
["PromptForUserAPIKey"] = true,
""");
var imported = (IUserProvidedAPIKey)Import(snippetWithPrompt, type).ConfigurationObject;
Assert.Multiple(() =>
{
Assert.That(imported.PromptForUserAPIKey, Is.False);
Assert.That(imported.AllowUserProvidedAPIKey, Is.False);
});
}
[Test]
public void TheTokenOfADataSourceIsDecrypted() => Assert.That(Import(ExportDataSource(AuthMethod.TOKEN).Snippet, PluginConfigurationObjectType.DATA_SOURCE).Secret, Is.EqualTo(TOKEN));
@@ -0,0 +1,45 @@
using AIStudio.Provider;
using AIStudio.Tools.UserAPIKeys;
namespace AIStudio.Tests.Tools.UserAPIKeys;
/// <summary>
/// Checks what AI Studio reads from the models a provider loads with the API key the user entered.
/// </summary>
/// <remarks>
/// Only a refusal keeps the key out of the OS keyring, and only a list with models counts as accepted. Many
/// providers succeed with an empty list whatever the key, for example for a kind of model they do not offer;
/// telling the user that such a key was accepted would be a promise nobody checked.
/// </remarks>
[TestFixture]
public sealed class UserAPIKeyCheckTests
{
[TestCase(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY)]
[TestCase(ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR)]
public void ARefusedSignInRejectsTheKey(ModelLoadFailureReason failureReason)
{
Assert.That(PendingUserAPIKeys.ClassifyCheck(ModelLoadResult.Failure(failureReason)), Is.EqualTo(UserAPIKeyCheckOutcome.REJECTED));
}
[Test]
public void ModelsLoadedWithTheKeyAcceptIt()
{
Assert.That(PendingUserAPIKeys.ClassifyCheck(ModelLoadResult.FromModels([new Model("gpt-6", "GPT-6")])), Is.EqualTo(UserAPIKeyCheckOutcome.ACCEPTED));
}
[Test]
public void AnEmptyListTellsNothingAboutTheKey()
{
Assert.That(PendingUserAPIKeys.ClassifyCheck(ModelLoadResult.FromModels([])), Is.EqualTo(UserAPIKeyCheckOutcome.NOT_CHECKED));
}
[TestCase(ModelLoadFailureReason.INSUFFICIENT_QUOTA)]
[TestCase(ModelLoadFailureReason.TOO_MANY_REQUESTS)]
[TestCase(ModelLoadFailureReason.PROVIDER_UNAVAILABLE)]
[TestCase(ModelLoadFailureReason.INVALID_RESPONSE)]
[TestCase(ModelLoadFailureReason.UNKNOWN)]
public void AnyOtherFailureTellsNothingAboutTheKey(ModelLoadFailureReason failureReason)
{
Assert.That(PendingUserAPIKeys.ClassifyCheck(ModelLoadResult.Failure(failureReason)), Is.EqualTo(UserAPIKeyCheckOutcome.NOT_CHECKED));
}
}