diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index d6de4d2b..309d95bd 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -52,7 +52,12 @@ jobs: run: sudo mkdir -p /var/lib/serval && sudo chmod 777 /var/lib/serval - name: Test - run: dotnet test --no-build --verbosity normal --filter "TestCategory!=slow&TestCategory=E2E" + run: dotnet test --no-build --verbosity normal --filter "TestCategory!=slow&TestCategory=E2E" --collect:"Xplat Code Coverage" + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Debug network (Post test) if: ${{ failure() }} diff --git a/.github/workflows/missing-services-e2e-tests.yml b/.github/workflows/missing-services-e2e-tests.yml index 33e4b9a9..478a202a 100644 --- a/.github/workflows/missing-services-e2e-tests.yml +++ b/.github/workflows/missing-services-e2e-tests.yml @@ -50,26 +50,26 @@ jobs: #Mongo - name: Test Working Mongo - run: dotnet test --no-build --verbosity normal --filter "TestCategory=MongoWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=MongoWorking" --collect:"Xplat Code Coverage" - name: Kill Mongo run: docker stop serval-mongo-1 - name: Test Not Working Mongo - run: dotnet test --no-build --verbosity normal --filter "TestCategory=MongoNotWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=MongoNotWorking" --collect:"Xplat Code Coverage" - name: Restart Mongo run: docker start serval-mongo-1 && sleep 20 #Engine Server - name: Test Working Engine Server - run: dotnet test --no-build --verbosity normal --filter "TestCategory=EngineServerWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=EngineServerWorking" --collect:"Xplat Code Coverage" - name: Kill Engine Server run: docker stop machine-engine-cntr - name: Test Not Working EngineServer - run: dotnet test --no-build --verbosity normal --filter "TestCategory=EngineServerNotWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=EngineServerNotWorking" --collect:"Xplat Code Coverage" - name: Restart Engine Server run: docker start machine-engine-cntr && sleep 5 @@ -81,7 +81,7 @@ jobs: ClearML_SecretKey: "not_the_right_key" - name: Test Not Working ClearML - run: dotnet test --no-build --verbosity normal --filter "TestCategory=ClearMLNotWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=ClearMLNotWorking" --collect:"Xplat Code Coverage" env: ClearML_SecretKey: "not_the_right_key" @@ -92,10 +92,16 @@ jobs: AWS_SECRET_ACCESS_KEY: "not_the_right_key" - name: Test Not Working AWS - run: dotnet test --no-build --verbosity normal --filter "TestCategory=AWSNotWorking" + run: dotnet test --no-build --verbosity normal --filter "TestCategory=AWSNotWorking" --collect:"Xplat Code Coverage" env: AWS_SECRET_ACCESS_KEY: "not_the_right_key" + #Coverage export + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + #Clean up - name: Debug network (Post test) if: ${{ failure() }} diff --git a/Serval.sln b/Serval.sln index 6ef54544..c850094d 100644 --- a/Serval.sln +++ b/Serval.sln @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serval.E2ETests", "tests\Se EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serval.DataFiles.Tests", "tests\Serval.DataFiles.Tests\Serval.DataFiles.Tests.csproj", "{63E4D71B-11BE-4D68-A876-5B1B5F0A4C88}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIL.DataAccess.Tests", "tests\SIL.DataAccess.Tests\SIL.DataAccess.Tests.csproj", "{71151518-8774-44D0-8E69-D77FA447BEFA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +116,10 @@ Global {63E4D71B-11BE-4D68-A876-5B1B5F0A4C88}.Debug|Any CPU.Build.0 = Debug|Any CPU {63E4D71B-11BE-4D68-A876-5B1B5F0A4C88}.Release|Any CPU.ActiveCfg = Release|Any CPU {63E4D71B-11BE-4D68-A876-5B1B5F0A4C88}.Release|Any CPU.Build.0 = Release|Any CPU + {71151518-8774-44D0-8E69-D77FA447BEFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71151518-8774-44D0-8E69-D77FA447BEFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71151518-8774-44D0-8E69-D77FA447BEFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71151518-8774-44D0-8E69-D77FA447BEFA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,6 +139,7 @@ Global {0C3DF75B-B022-4EFC-882C-F276F1EC8435} = {66246A1C-8D45-40FB-A660-C58577122CA7} {1F020042-D7B8-4541-9691-26ECFD1FFC73} = {66246A1C-8D45-40FB-A660-C58577122CA7} {63E4D71B-11BE-4D68-A876-5B1B5F0A4C88} = {66246A1C-8D45-40FB-A660-C58577122CA7} + {71151518-8774-44D0-8E69-D77FA447BEFA} = {66246A1C-8D45-40FB-A660-C58577122CA7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F18C25E-E140-43C3-B177-D562E1628370} diff --git a/src/SIL.DataAccess/CustomEnumConverterFactory.cs b/src/SIL.DataAccess/CustomEnumConverterFactory.cs deleted file mode 100644 index 8ec8e975..00000000 --- a/src/SIL.DataAccess/CustomEnumConverterFactory.cs +++ /dev/null @@ -1,160 +0,0 @@ -namespace SIL.DataAccess; - -public sealed class CustomEnumConverterFactory : JsonConverterFactory -{ - private readonly JsonNamingPolicy _namingPolicy; - - public CustomEnumConverterFactory(JsonNamingPolicy namingPolicy) - { - _namingPolicy = namingPolicy; - } - - public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum; - - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - object[]? knownValues = null; - - if (typeToConvert == typeof(BindingFlags)) - { - knownValues = new object[] { BindingFlags.CreateInstance | BindingFlags.DeclaredOnly }; - } - - return (JsonConverter) - Activator.CreateInstance( - typeof(CustomEnumConverter<>).MakeGenericType(typeToConvert), - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: new object?[] { _namingPolicy, options, knownValues }, - culture: null - )!; - } -} - -public sealed class CustomEnumConverter : JsonConverter - where T : Enum -{ - private readonly JsonNamingPolicy _namingPolicy; - - private readonly Dictionary _readCache = new(); - private readonly Dictionary _writeCache = new(); - - // This converter will only support up to 64 enum values (including flags) on serialization and deserialization - private const int NameCacheLimit = 64; - - private const string ValueSeparator = ", "; - - public CustomEnumConverter(JsonNamingPolicy namingPolicy, JsonSerializerOptions options, object[]? knownValues) - { - _namingPolicy = namingPolicy; - - bool continueProcessing = true; - for (int i = 0; i < knownValues?.Length; i++) - { - if (!TryProcessValue((T)knownValues[i])) - { - continueProcessing = false; - break; - } - } - - if (continueProcessing) - { - Array values = Enum.GetValues(typeof(T)); - - for (int i = 0; i < values.Length; i++) - { - T value = (T)values.GetValue(i)!; - - if (!TryProcessValue(value)) - { - break; - } - } - } - - bool TryProcessValue(T value) - { - if (_readCache.Count == NameCacheLimit) - { - Debug.Assert(_writeCache.Count == NameCacheLimit); - return false; - } - - FormatAndAddToCaches(value, options.Encoder); - return true; - } - } - - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string? json; - - if ( - reader.TokenType != JsonTokenType.String - || (json = reader.GetString()) == null - || !_readCache.TryGetValue(json, out T? value) - ) - { - throw new JsonException(); - } - - return value; - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (!_writeCache.TryGetValue(value, out JsonEncodedText formatted)) - { - if (_writeCache.Count == NameCacheLimit) - { - Debug.Assert(_readCache.Count == NameCacheLimit); - throw new ArgumentOutOfRangeException(); - } - - formatted = FormatAndAddToCaches(value, options.Encoder); - } - - writer.WriteStringValue(formatted); - } - - private JsonEncodedText FormatAndAddToCaches(T value, JavaScriptEncoder? encoder) - { - (string valueFormattedToStr, JsonEncodedText valueEncoded) = FormatEnumValue( - value.ToString(), - _namingPolicy, - encoder - ); - _readCache[valueFormattedToStr] = value; - _writeCache[value] = valueEncoded; - return valueEncoded; - } - - private ValueTuple FormatEnumValue( - string value, - JsonNamingPolicy namingPolicy, - JavaScriptEncoder? encoder - ) - { - string converted; - - if (!value.Contains(ValueSeparator)) - { - converted = namingPolicy.ConvertName(value); - } - else - { - // todo: optimize implementation here by leveraging https://github.com/dotnet/runtime/issues/934. - string[] enumValues = value.Split(ValueSeparator); - - for (int i = 0; i < enumValues.Length; i++) - { - enumValues[i] = namingPolicy.ConvertName(enumValues[i]); - } - - converted = string.Join(ValueSeparator, enumValues); - } - - return (converted, JsonEncodedText.Encode(converted, encoder)); - } -} diff --git a/src/Serval.Core/FileFormat.cs b/src/Serval.Core/FileFormat.cs deleted file mode 100644 index 8ebee9ae..00000000 --- a/src/Serval.Core/FileFormat.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Serval.Core -{ - public enum FileFormat - { - Text = 0, - Paratext = 1 - } -} diff --git a/src/Serval.Core/Serval.Core.csproj b/src/Serval.Core/Serval.Core.csproj deleted file mode 100644 index 7c97d07f..00000000 --- a/src/Serval.Core/Serval.Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - netstandard2.0 - true - 1591 - - - - - - - - - diff --git a/tests/SIL.DataAccess.Tests/MemoryRepositoryTests.cs b/tests/SIL.DataAccess.Tests/MemoryRepositoryTests.cs new file mode 100644 index 00000000..05c8d072 --- /dev/null +++ b/tests/SIL.DataAccess.Tests/MemoryRepositoryTests.cs @@ -0,0 +1,76 @@ +namespace SIL.DataAccess; + +[TestFixture] +public class MemoryRepositoryTests +{ + [Test] + public async Task AddAndRemove() + { + var mr = new MemoryRepository(); + mr.Init(); + mr.Add(new IntegerEntity(1) { Id = "1" }); + var entityToRemove = new IntegerEntity(2) { Id = "2" }; + mr.Add(entityToRemove); + mr.Remove(entityToRemove); + Assert.That((await mr.GetAllAsync(_ => true)).Count, Is.EqualTo(1)); + IntegerEntity entity = mr.Get("1"); + Assert.That(entity.Value, Is.EqualTo(1)); + mr.Add( + new IntegerEntity[] + { + new IntegerEntity(1) { Id = "3" }, + new IntegerEntity(2) { Id = "4" } + } + ); + Assert.That((await mr.GetAllAsync(_ => true)).Count, Is.EqualTo(3)); + Assert.That(await mr.ExistsAsync(entity => entity.Value == 2)); + } + + [Test] + public async Task InsertAndUpdate() + { + var mr = new MemoryRepository(); + mr.Init(); + await mr.InsertAllAsync( + new IntegerEntity[] + { + new IntegerEntity(1) { Id = "1" }, + new IntegerEntity(2) { Id = "2" } + } + ); + Assert.ThrowsAsync(async () => + { + await mr.InsertAllAsync( + new IntegerEntity[] + { + new IntegerEntity(1) { Id = "1" }, + new IntegerEntity(2) { Id = "2" } + } + ); + }); + Assert.That((await mr.GetAllAsync(_ => true)).Count, Is.EqualTo(2)); + await mr.UpdateAsync(e => e.Id == "0", e => e.Set(r => r.Value, 0), upsert: true); + Assert.That((await mr.GetAllAsync(_ => true)).Count, Is.EqualTo(3)); + await mr.UpdateAsync(e => e.Id == "0", e => e.Set(r => r.Value, 100)); + Assert.That((await mr.GetAllAsync(_ => true)).Count, Is.EqualTo(3)); + Assert.That(mr.Get("0").Value, Is.EqualTo(100)); + await mr.UpdateAsync(e => e.Id == "1", e => e.Set(r => r.Value, 100)); + await mr.UpdateAllAsync(e => e.Value == 100, e => e.Set(r => r.Value, -100)); + Assert.That(mr.Get("0").Value, Is.EqualTo(-100)); + Assert.That(mr.Get("1").Value, Is.EqualTo(-100)); + } + + private class IntegerEntity : IEntity + { + public string Id { get; set; } = default!; + public int Revision { get; set; } = 1; + public int Value { get; set; } + + public IntegerEntity(int value) + { + Value = value; + } + + public IntegerEntity() { } + } +} diff --git a/tests/SIL.DataAccess.Tests/SIL.DataAccess.Tests.csproj b/tests/SIL.DataAccess.Tests/SIL.DataAccess.Tests.csproj new file mode 100644 index 00000000..07e5f67e --- /dev/null +++ b/tests/SIL.DataAccess.Tests/SIL.DataAccess.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + false + SIL.DataAccess + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/SIL.DataAccess.Tests/Usings.cs b/tests/SIL.DataAccess.Tests/Usings.cs new file mode 100644 index 00000000..d7017560 --- /dev/null +++ b/tests/SIL.DataAccess.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using NUnit.Framework; +global using SIL.DataAccess; diff --git a/tests/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs b/tests/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs index ed5cd037..eac2bddc 100644 --- a/tests/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs +++ b/tests/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs @@ -276,6 +276,7 @@ public async Task DeleteEngineByIdAsync(IEnumerable scope, int expectedS [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 409, ECHO_ENGINE1_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task TranslateSegmentWithEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -308,6 +309,7 @@ await _env.Builds.InsertAsync( }); Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -325,6 +327,7 @@ await _env.Builds.InsertAsync( [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 409, ECHO_ENGINE1_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task TranslateNSegmentWithEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -362,6 +365,7 @@ await _env.Builds.InsertAsync( }); Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -379,6 +383,7 @@ await _env.Builds.InsertAsync( [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines, Scopes.UpdateTranslationEngines }, 409, ECHO_ENGINE1_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task GetWordGraphForSegmentByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -411,6 +416,7 @@ await _env.Builds.InsertAsync( }); Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -428,6 +434,7 @@ await _env.Builds.InsertAsync( [TestCase(new[] { Scopes.UpdateTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 409, ECHO_ENGINE1_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task TrainEngineByIdOnSegmentPairAsync( IEnumerable scope, int expectedStatusCode, @@ -461,6 +468,7 @@ await _env.Builds.InsertAsync( }); Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -477,6 +485,7 @@ await _env.Builds.InsertAsync( [Test] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 201, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task AddCorpusToEngineByIdAsync(IEnumerable scope, int expectedStatusCode, string engineId) { ITranslationEnginesClient client = _env!.CreateClient(scope); @@ -498,6 +507,7 @@ public async Task AddCorpusToEngineByIdAsync(IEnumerable scope, int expe Assert.That(engine.Corpora[0].TargetFiles[0].Filename, Is.EqualTo(FILE2_FILENAME)); }); break; + case 403: case 404: var ex = Assert.ThrowsAsync(async () => { @@ -527,6 +537,7 @@ public async Task AddCorpusToEngineByIdAsync(IEnumerable scope, int expe 404, DOES_NOT_EXIST_ENGINE_ID )] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task UpdateCorpusByIdForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -557,6 +568,7 @@ string engineId }); break; case 400: + case 403: case 404: var ex = Assert.ThrowsAsync(async () => { @@ -590,6 +602,7 @@ string engineId 404, DOES_NOT_EXIST_ENGINE_ID )] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task GetAllCorporaForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -610,6 +623,7 @@ string engineId Assert.That(resultAfterAdd.TargetLanguage, Is.EqualTo(result.TargetLanguage)); }); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -629,6 +643,7 @@ string engineId [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 404, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 404, ECHO_ENGINE1_ID, true)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task GetCorpusByIdForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -656,6 +671,7 @@ public async Task GetCorpusByIdForEngineByIdAsync( Assert.That(resultAfterAdd.TargetLanguage, Is.EqualTo(result.TargetLanguage)); }); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -674,6 +690,7 @@ public async Task GetCorpusByIdForEngineByIdAsync( [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 404, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines, Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task DeleteCorpusByIdForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -690,6 +707,7 @@ string engineId ICollection resultsAfterDelete = await client.GetAllCorporaAsync(engineId); Assert.That(resultsAfterDelete, Has.Count.EqualTo(0)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -818,6 +836,7 @@ public async Task GetAllPretranslationsAsync_TextIdDoesNotExist() [Test] [TestCase(new[] { Scopes.ReadTranslationEngines }, 200, SMT_ENGINE1_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID, false)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task GetAllBuildsForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -844,6 +863,7 @@ public async Task GetAllBuildsForEngineByIdAsync( Assert.That(results.First().State, Is.EqualTo(JobState.Pending)); }); break; + case 403: case 404: var ex = Assert.ThrowsAsync(async () => { @@ -861,6 +881,7 @@ public async Task GetAllBuildsForEngineByIdAsync( [TestCase(new[] { Scopes.ReadTranslationEngines }, 408, SMT_ENGINE1_ID, true)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID, false)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 404, SMT_ENGINE1_ID, false)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task GetBuildByIdForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -888,6 +909,7 @@ public async Task GetBuildByIdForEngineByIdAsync( Assert.That(result.State, Is.EqualTo(JobState.Pending)); }); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -925,10 +947,12 @@ public async Task GetBuildByIdForEngineByIdAsync( 400, ECHO_ENGINE1_ID )] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID)] //Arbitrary unrelated privilege public async Task StartBuildForEngineByIdAsync(IEnumerable scope, int expectedStatusCode, string engineId) { ITranslationEnginesClient client = _env!.CreateClient(scope); PretranslateCorpusConfig ptcc; + TrainingCorpusConfig tcc; TranslationBuildConfig tbc; switch (expectedStatusCode) { @@ -939,7 +963,18 @@ public async Task StartBuildForEngineByIdAsync(IEnumerable scope, int ex CorpusId = addedCorpus.Id, TextIds = new List { "all" } }; - tbc = new TranslationBuildConfig { Pretranslate = new List { ptcc } }; + tcc = new() + { + CorpusId = addedCorpus.Id, + TextIds = new List { "all" } + }; + tbc = new TranslationBuildConfig + { + Pretranslate = new List { ptcc }, + TrainOn = new List { tcc }, + Options = + "{\"max_steps\":10, \"use_key_terms\":false, \"some_double\":10.5, \"some_string\":\"string\"}" + }; TranslationBuild resultAfterStart; Assert.ThrowsAsync(async () => { @@ -953,6 +988,7 @@ public async Task StartBuildForEngineByIdAsync(IEnumerable scope, int ex Assert.That(build, Is.Not.Null); break; case 400: + case 403: case 404: ptcc = new PretranslateCorpusConfig { @@ -977,6 +1013,7 @@ public async Task StartBuildForEngineByIdAsync(IEnumerable scope, int ex [TestCase(new[] { Scopes.ReadTranslationEngines }, 408, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 204, ECHO_ENGINE1_ID, false)] [TestCase(new[] { Scopes.ReadTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID, false)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID, false)] //Arbitrary unrelated privilege public async Task GetCurrentBuildForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -1001,6 +1038,7 @@ public async Task GetCurrentBuildForEngineByIdAsync( Assert.That(result.Id, Is.EqualTo(build!.Id)); break; case 204: + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -1026,6 +1064,7 @@ public async Task GetCurrentBuildForEngineByIdAsync( [TestCase(new[] { Scopes.UpdateTranslationEngines }, 200, ECHO_ENGINE1_ID)] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 404, DOES_NOT_EXIST_ENGINE_ID, false)] [TestCase(new[] { Scopes.UpdateTranslationEngines }, 404, ECHO_ENGINE1_ID, false)] + [TestCase(new[] { Scopes.ReadFiles }, 403, ECHO_ENGINE1_ID, false)] //Arbitrary unrelated privilege public async Task CancelCurrentBuildForEngineByIdAsync( IEnumerable scope, int expectedStatusCode, @@ -1046,6 +1085,7 @@ public async Task CancelCurrentBuildForEngineByIdAsync( case 200: await client.CancelBuildAsync(engineId); break; + case 403: case 404: var ex = Assert.ThrowsAsync(async () => { @@ -1093,6 +1133,28 @@ public void AddCorpusWithSameSourceAndTargetLangs() Assert.That(ex.StatusCode, Is.EqualTo(422)); } + [Test] + [TestCase("Nmt")] + [TestCase("Echo")] + public async Task GetQueueAsync(string engineType) + { + ITranslationEnginesClient client = _env!.CreateClient(); + Client.Queue queue = await client.GetQueueAsync(engineType); + Assert.That(queue.Size, Is.EqualTo(0)); + } + + [Test] + public void GetQueueAsync_NotAuthorized() + { + ITranslationEnginesClient client = _env!.CreateClient(new string[] { Scopes.ReadFiles }); + ServalApiException? ex = Assert.ThrowsAsync(async () => + { + Client.Queue queue = await client.GetQueueAsync("Echo"); + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.StatusCode, Is.EqualTo(403)); + } + [TearDown] public void TearDown() { @@ -1266,6 +1328,9 @@ public TestEnvironment() EchoClient .TranslateAsync(Arg.Any(), null, null, Arg.Any()) .Returns(CreateAsyncUnaryCall(translateResponse)); + EchoClient + .GetQueueSizeAsync(Arg.Any(), null, null, Arg.Any()) + .Returns(CreateAsyncUnaryCall(new GetQueueSizeResponse() { Size = 0 })); NmtClient = Substitute.For(); NmtClient @@ -1286,6 +1351,9 @@ public TestEnvironment() NmtClient .TranslateAsync(Arg.Any(), null, null, Arg.Any()) .Returns(CreateAsyncUnaryCall(StatusCode.Unimplemented)); + NmtClient + .GetQueueSizeAsync(Arg.Any(), null, null, Arg.Any()) + .Returns(CreateAsyncUnaryCall(new GetQueueSizeResponse() { Size = 0 })); } ServalWebApplicationFactory Factory { get; } diff --git a/tests/Serval.ApiServer.IntegrationTests/WebhooksTests.cs b/tests/Serval.ApiServer.IntegrationTests/WebhooksTests.cs index c29a6a18..7ef693b0 100644 --- a/tests/Serval.ApiServer.IntegrationTests/WebhooksTests.cs +++ b/tests/Serval.ApiServer.IntegrationTests/WebhooksTests.cs @@ -29,6 +29,7 @@ public async Task Setup() [Test] [TestCase(null, 200)] //null gives all scope privileges + [TestCase(new string[] { Scopes.ReadFiles }, 403)] //Arbitrary unrelated privilege public async Task GetAllWebhooksAsync(IEnumerable? scope, int expectedStatusCode) { WebhooksClient client = _env!.CreateClient(scope); @@ -38,6 +39,14 @@ public async Task GetAllWebhooksAsync(IEnumerable? scope, int expectedSt Webhook result = (await client.GetAllAsync()).First(); Assert.That(result.Id, Is.EqualTo(ID)); break; + case 403: + ServalApiException? ex = Assert.ThrowsAsync(async () => + { + await client.GetAllAsync(); + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); + break; default: Assert.Fail("Unanticipated expectedStatusCode. Check test case for typo."); break; @@ -47,6 +56,7 @@ public async Task GetAllWebhooksAsync(IEnumerable? scope, int expectedSt [Test] [TestCase(null, 200, ID)] //null gives all scope privileges [TestCase(null, 404, DOES_NOT_EXIST_ID)] + [TestCase(new string[] { Scopes.ReadFiles }, 403, ID)] //Arbitrary unrelated privilege public async Task GetWebhookByIdAsync(IEnumerable? scope, int expectedStatusCode, string webhookId) { WebhooksClient client = _env!.CreateClient(scope); @@ -56,6 +66,7 @@ public async Task GetWebhookByIdAsync(IEnumerable? scope, int expectedSt Webhook result = await client.GetAsync(webhookId); Assert.That(result.Id, Is.EqualTo(ID)); break; + case 403: case 404: var ex = Assert.ThrowsAsync(async () => { @@ -72,6 +83,7 @@ public async Task GetWebhookByIdAsync(IEnumerable? scope, int expectedSt [Test] [TestCase(null, 200, ID)] //null gives all scope privileges [TestCase(null, 404, DOES_NOT_EXIST_ID)] + [TestCase(new string[] { Scopes.ReadFiles }, 403, ID)] //Arbitrary unrelated privilege public async Task DeleteWebhookByIdAsync(IEnumerable? scope, int expectedStatusCode, string webhookId) { WebhooksClient client = _env!.CreateClient(scope); @@ -86,6 +98,7 @@ public async Task DeleteWebhookByIdAsync(IEnumerable? scope, int expecte }); Assert.That(ex!.StatusCode, Is.EqualTo(404)); break; + case 403: case 404: ex = Assert.ThrowsAsync(async () => { @@ -101,6 +114,7 @@ public async Task DeleteWebhookByIdAsync(IEnumerable? scope, int expecte [Test] [TestCase(null, 201)] + [TestCase(new string[] { Scopes.ReadFiles }, 403)] //Arbitrary unrelated privilege public async Task CreateWebhookAsync(IEnumerable scope, int expectedStatusCode) { WebhooksClient client = _env!.CreateClient(scope); @@ -118,6 +132,21 @@ public async Task CreateWebhookAsync(IEnumerable scope, int expectedStat Webhook resultAfterCreate = await client.GetAsync(result.Id); Assert.That(resultAfterCreate.PayloadUrl, Is.EqualTo(result.PayloadUrl)); + break; + case 403: + ServalApiException? ex = Assert.ThrowsAsync(async () => + { + Webhook result = await client.CreateAsync( + new WebhookConfig + { + PayloadUrl = "/a/different/url", + Secret = "M0rEs3CreTz#", + Events = { WebhookEvent.TranslationBuildStarted } + } + ); + }); + Assert.That(ex, Is.Not.Null); + Assert.That(ex!.StatusCode, Is.EqualTo(expectedStatusCode)); break; default: Assert.Fail("Unanticipated expectedStatusCode. Check test case for typo."); diff --git a/tests/Serval.Translation.Tests/Services/PlatformServiceTests.cs b/tests/Serval.Translation.Tests/Services/PlatformServiceTests.cs new file mode 100644 index 00000000..bcad8171 --- /dev/null +++ b/tests/Serval.Translation.Tests/Services/PlatformServiceTests.cs @@ -0,0 +1,135 @@ +using MassTransit; +using Serval.Translation.V1; + +namespace Serval.Translation.Services; + +[TestFixture] +public class PlatformServiceTests +{ + [Test] + public async Task TestBuildStateTransitionsAsync() + { + var env = new TestEnvironment(); + await env.Engines.InsertAsync(new Engine() { Id = "e0" }); + await env.Builds.InsertAsync(new Build() { Id = "b0", EngineRef = "e0" }); + await env.PlatformService.BuildStarted(new BuildStartedRequest() { BuildId = "b0" }, env.ServerCallContext); + Assert.That((await env.Builds.GetAsync("b0"))!.State, Is.EqualTo(Shared.Contracts.JobState.Active)); + Assert.That((await env.Engines.GetAsync("e0"))!.IsBuilding); + + await env.PlatformService.BuildCanceled(new BuildCanceledRequest() { BuildId = "b0" }, env.ServerCallContext); + Assert.That((await env.Builds.GetAsync("b0"))!.State, Is.EqualTo(Shared.Contracts.JobState.Canceled)); + Assert.That(!(await env.Engines.GetAsync("e0"))!.IsBuilding); + + await env.PlatformService.BuildRestarting( + new BuildRestartingRequest() { BuildId = "b0" }, + env.ServerCallContext + ); + Assert.That((await env.Builds.GetAsync("b0"))!.State, Is.EqualTo(Shared.Contracts.JobState.Pending)); + Assert.That(!(await env.Engines.GetAsync("e0"))!.IsBuilding); + + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(0)); + await env.PlatformService.InsertPretranslations(new MockAsyncStreamReader("e0"), env.ServerCallContext); + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(1)); + + await env.PlatformService.BuildFaulted(new BuildFaultedRequest() { BuildId = "b0" }, env.ServerCallContext); + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(0)); + Assert.That((await env.Builds.GetAsync("b0"))!.State, Is.EqualTo(Shared.Contracts.JobState.Faulted)); + Assert.That(!(await env.Engines.GetAsync("e0"))!.IsBuilding); + + await env.PlatformService.BuildRestarting( + new BuildRestartingRequest() { BuildId = "b0" }, + env.ServerCallContext + ); + await env.PlatformService.InsertPretranslations(new MockAsyncStreamReader("e0"), env.ServerCallContext); + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(1)); + await env.PlatformService.BuildCompleted(new BuildCompletedRequest() { BuildId = "b0" }, env.ServerCallContext); + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(1)); + await env.PlatformService.BuildStarted(new BuildStartedRequest() { BuildId = "b0" }, env.ServerCallContext); + await env.PlatformService.InsertPretranslations(new MockAsyncStreamReader("e0"), env.ServerCallContext); + await env.PlatformService.BuildCompleted(new BuildCompletedRequest() { BuildId = "b0" }, env.ServerCallContext); + Assert.That((await env.Pretranslations.GetAllAsync()).Count, Is.EqualTo(1)); + } + + [Test] + public async Task UpdateBuildStatusAsync() + { + var env = new TestEnvironment(); + await env.Engines.InsertAsync(new Engine() { Id = "e0" }); + await env.Builds.InsertAsync(new Build() { Id = "b0", EngineRef = "e0" }); + Assert.That((await env.Builds.GetAsync("b0"))!.QueueDepth, Is.EqualTo(null)); + Assert.That((await env.Builds.GetAsync("b0"))!.PercentCompleted, Is.EqualTo(null)); + await env.PlatformService.UpdateBuildStatus( + new UpdateBuildStatusRequest() + { + BuildId = "b0", + QueueDepth = 1, + PercentCompleted = 0.5 + }, + env.ServerCallContext + ); + Assert.That((await env.Builds.GetAsync("b0"))!.QueueDepth, Is.EqualTo(1)); + Assert.That((await env.Builds.GetAsync("b0"))!.PercentCompleted, Is.EqualTo(0.5)); + } + + [Test] + public async Task IncrementCorpusSizeAsync() + { + var env = new TestEnvironment(); + await env.Engines.InsertAsync(new Engine() { Id = "e0" }); + Assert.That((await env.Engines.GetAsync("e0"))!.CorpusSize, Is.EqualTo(0)); + await env.PlatformService.IncrementTranslationEngineCorpusSize( + new IncrementTranslationEngineCorpusSizeRequest() { EngineId = "e0", Count = 1 }, + env.ServerCallContext + ); + Assert.That((await env.Engines.GetAsync("e0"))!.CorpusSize, Is.EqualTo(1)); + } + + private class TestEnvironment + { + public TestEnvironment() + { + Builds = new MemoryRepository(); + Engines = new MemoryRepository(); + Pretranslations = new MemoryRepository(); + DataAccessContext = Substitute.For(); + PublishEndpoint = Substitute.For(); + ServerCallContext = Substitute.For(); + PlatformService = new TranslationPlatformServiceV1( + Builds, + Engines, + Pretranslations, + DataAccessContext, + PublishEndpoint + ); + } + + public IRepository Builds { get; } + public IRepository Engines { get; } + public IRepository Pretranslations { get; } + public IDataAccessContext DataAccessContext { get; } + public IPublishEndpoint PublishEndpoint { get; } + public ServerCallContext ServerCallContext { get; } + public TranslationPlatformServiceV1 PlatformService { get; } + } + + private class MockAsyncStreamReader : IAsyncStreamReader + { + private bool _endOfStream; + + public MockAsyncStreamReader(string engineId) + { + _endOfStream = false; + EngineId = engineId; + } + + public string EngineId { get; } + public InsertPretranslationRequest Current => new InsertPretranslationRequest() { EngineId = EngineId }; + + public Task MoveNext(CancellationToken cancellationToken) + { + var ret = Task.FromResult(!_endOfStream); + _endOfStream = true; + return ret; + } + } +}