{"repo": "aiursoftweb/ollamagateway", "files": {"tests/IntegrationTests/SelfReferentialProviderTests.cs": {"5a0e38a376ad629e894ea2615758c12b9e34c37d": {"t": 1785746195.2688074, "n": 7137, "h": "fe5eaf6f3f2841aa", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}, "b5ca166a370060c2f18918f788b4041e96bf8941": {"t": 1785760519.0438263, "n": 7137, "h": "fe5eaf6f3f2841aa", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}, "b4c09d75dfb815a07a0aff8003d774a2d93149dc": {"t": 1785760520.3515453, "n": 7143, "h": "85a3afdece1633bb", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}, "db9f97ed559e6931fee62a621b0a792ba24e7ea6": {"t": 1785760523.2134275, "n": 7156, "h": "796cda52e1fa8b71", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}, "0f4b30616511928e41bfea48cd54351f896e283d": {"t": 1786067627.9516592, "n": 7137, "h": "fe5eaf6f3f2841aa", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}, "a3a1c02f9339e63ba0d9ba7f1739b0296fd2af0e": {"t": 1786068388.2958484, "n": 7137, "h": "fe5eaf6f3f2841aa", "c": "using System.Net;\nusing System.Text.Json;\nusing Aiursoft.DbTools;\nusing Aiursoft.OllamaGateway.Configuration;\nusing Aiursoft.OllamaGateway.Entities;\nusing Aiursoft.OllamaGateway.Services;\nusing Moq;\nusing static Aiursoft.WebTools.Extends;\n\nnamespace Aiursoft.OllamaGateway.Tests.IntegrationTests;\n\n/// \n/// Regression tests for the infinite-recursion bug caused by self-referential Ollama providers.\n///\n/// Scenario that triggered the bug:\n/// 1. Provider A = deepseek (OpenAI type) → virtual model \"my-deepseek:latest\"\n/// 2. Provider B = localhost:5000 (Ollama type, pointing at THIS gateway)\n/// — the gateway then exposes \"my-deepseek:latest\" via /api/tags\n/// 3. A second virtual model \"chat2:latest\" backed by Provider B / \"my-deepseek:latest\"\n///\n/// When /api/tags or /api/ps was requested, the old code called\n/// GetDetailedModelsAsync / GetRunningModelsAsync on every Ollama-type backend, which would\n/// issue HTTP GET localhost:5000/api/tags → hitting itself again → infinite recursion →\n/// connection pool exhaustion (observed as \"SQL infinite recursion\").\n///\n/// These tests verify that /api/tags and /api/ps NEVER call any upstream provider method,\n/// so self-referential providers are inherently safe.\n/// \n[TestClass]\npublic class SelfReferentialProviderTests : TestBase\n{\n [TestInitialize]\n public override async Task CreateServer()\n {\n // Reset the mock — no upstream calls should be configured at all\n TestStartup.MockOllamaService.Reset();\n\n Server = await AppAsync([], port: Port);\n await Server.UpdateDbAsync();\n await Server.SeedAsync();\n await Server.StartAsync();\n\n // Allow anonymous API access so tests don't need to log in\n using var scope = Server.Services.CreateScope();\n var settings = scope.ServiceProvider.GetRequiredService();\n await settings.UpdateSettingAsync(SettingsMap.AllowAnonymousApiCall, \"True\");\n }\n\n private async Task SeedSelfReferentialSetupAsync(int selfPort)\n {\n using var scope = Server!.Services.CreateScope();\n var db = scope.ServiceProvider.GetRequiredService();\n\n // Provider A: upstream DeepSeek (OpenAI type)\n var deepseekProvider = new OllamaProvider\n {\n Name = \"deepseek\",\n BaseUrl = \"https://api.deepseek.com\",\n ProviderType = ProviderType.OpenAI\n };\n db.OllamaProviders.Add(deepseekProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider A\n var myDeepseek = new VirtualModel { Name = \"my-deepseek:latest\", Type = ModelType.Chat };\n myDeepseek.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = deepseekProvider.Id,\n UnderlyingModelName = \"deepseek-chat\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(myDeepseek);\n await db.SaveChangesAsync();\n\n // Provider B: this gateway itself (Ollama type — the self-referential case)\n var selfProvider = new OllamaProvider\n {\n Name = \"self\",\n BaseUrl = $\"http://localhost:{selfPort}\",\n ProviderType = ProviderType.Ollama\n };\n db.OllamaProviders.Add(selfProvider);\n await db.SaveChangesAsync();\n\n // Virtual model backed by Provider B using the name exposed by /api/tags\n var chat2 = new VirtualModel { Name = \"chat2:latest\", Type = ModelType.Chat };\n chat2.VirtualModelBackends.Add(new VirtualModelBackend\n {\n ProviderId = selfProvider.Id,\n UnderlyingModelName = \"my-deepseek:latest\",\n Enabled = true,\n IsHealthy = true\n });\n db.VirtualModels.Add(chat2);\n await db.SaveChangesAsync();\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Tags_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/tags\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetDetailedModelsAsync(It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/tags must not call GetDetailedModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_ReturnsAllVirtualModels()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/ps\");\n\n Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);\n\n var content = await response.Content.ReadAsStringAsync();\n var json = JsonDocument.Parse(content);\n var models = json.RootElement.GetProperty(\"models\").EnumerateArray().Select(m => m.GetProperty(\"name\").GetString()).ToList();\n\n CollectionAssert.Contains(models, \"my-deepseek:latest\");\n CollectionAssert.Contains(models, \"chat2:latest\");\n }\n\n [TestMethod]\n public async Task Ps_WithSelfReferentialProvider_NeverCallsUpstreamProvider()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n await Http.GetAsync(\"/api/ps\");\n\n // If this is called, the old recursive code path is still present\n TestStartup.MockOllamaService.Verify(\n s => s.GetRunningModelsAsync(It.IsAny(), It.IsAny(), It.IsAny()),\n Times.Never,\n \"GET /api/ps must not call GetRunningModelsAsync on any upstream provider. \" +\n \"Doing so causes infinite recursion when a provider points back at this gateway.\");\n }\n\n [TestMethod]\n public async Task Tags_ResponseHasSnakeCaseProperties()\n {\n await SeedSelfReferentialSetupAsync(Port);\n\n var response = await Http.GetAsync(\"/api/tags\");\n var content = await response.Content.ReadAsStringAsync();\n\n Assert.IsTrue(content.Contains(\"\\\"name\\\"\"), \"Expected snake_case 'name' property\");\n Assert.IsTrue(content.Contains(\"\\\"modified_at\\\"\"), \"Expected snake_case 'modified_at' property\");\n Assert.IsFalse(content.Contains(\"\\\"Name\\\"\"), \"Must not contain PascalCase 'Name'\");\n Assert.IsFalse(content.Contains(\"\\\"ModifiedAt\\\"\"), \"Must not contain PascalCase 'ModifiedAt'\");\n }\n}\n"}}}}