diff options
| author | Leitwolf | 2026-10-29 13:08:07 +0300 |
|---|---|---|
| committer | GitHub | 2026-10-29 13:08:07 +0300 |
| commit | 4b414909ac818a9178d72ffa6e1c396d5fa27a59 (patch) | |
| tree | 9801dc805a397052264cc5bc0a6c56cf870b179a /apps/api-tests | |
| parent | f22418235a0a87bc05b3bf9079fcdefba002d3b2 (diff) | |
| parent | 285e1024da64234b30b0fa425ffd6bf348119db4 (diff) | |
| download | progmaweb-4b414909ac818a9178d72ffa6e1c396d5fa27a59.tar.gz | |
Merge pull request #47 from Progmasoft/feature/viget-tazel-plugins
Add the Tazel plugin catalog of ViGet and open the Gradle catalog
Diffstat (limited to 'apps/api-tests')
| -rw-r--r-- | apps/api-tests/AccountServiceTests.cs | 6 | ||||
| -rw-r--r-- | apps/api-tests/AccountStoreSelectionTests.cs | 5 | ||||
| -rw-r--r-- | apps/api-tests/ViGetTests.cs | 269 |
3 files changed, 280 insertions, 0 deletions
diff --git a/apps/api-tests/AccountServiceTests.cs b/apps/api-tests/AccountServiceTests.cs index 28fd44f..0ff7b33 100644 --- a/apps/api-tests/AccountServiceTests.cs +++ b/apps/api-tests/AccountServiceTests.cs @@ -151,6 +151,12 @@ public sealed class AccountServiceTests public ValueTask<AccountRecord?> FindByIdAsync(Guid id, CancellationToken cancellationToken) => ValueTask.FromResult<AccountRecord?>(passwordAccount.Id == id ? passwordAccount : null); + public ValueTask<AccountRecord?> FindByAccountNameAsync( + string normalizedAccountName, + CancellationToken cancellationToken) => + ValueTask.FromResult<AccountRecord?>( + passwordAccount.NormalizedAccountName == normalizedAccountName ? passwordAccount : null); + public ValueTask<AccountRecord?> FindByGoogleSubjectAsync( string googleSubject, CancellationToken cancellationToken) => diff --git a/apps/api-tests/AccountStoreSelectionTests.cs b/apps/api-tests/AccountStoreSelectionTests.cs index 05ed31b..1087575 100644 --- a/apps/api-tests/AccountStoreSelectionTests.cs +++ b/apps/api-tests/AccountStoreSelectionTests.cs @@ -134,6 +134,11 @@ public sealed class ExpiredSessionCleanupTests public ValueTask<AccountRecord?> FindByIdAsync(Guid id, CancellationToken cancellationToken) => throw new NotSupportedException(); + public ValueTask<AccountRecord?> FindByAccountNameAsync( + string normalizedAccountName, + CancellationToken cancellationToken) => + throw new NotSupportedException(); + public ValueTask StoreSessionAsync(SessionRecord session, CancellationToken cancellationToken) => throw new NotSupportedException(); diff --git a/apps/api-tests/ViGetTests.cs b/apps/api-tests/ViGetTests.cs new file mode 100644 index 0000000..603ef2f --- /dev/null +++ b/apps/api-tests/ViGetTests.cs @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 + +using System.IO.Compression; +using System.Net; +using System.Security.Cryptography; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Progmasoft.Progmaweb.Api.Accounts; +using Progmasoft.Progmaweb.Api.ViGet; + +namespace Progmasoft.Progmaweb.Api.Tests; + +/// <summary>The Tazel plugin catalog of ViGet: the descriptor, publishing, the endpoints and the command.</summary> +public sealed class ViGetTests : IDisposable +{ + private readonly string artifactRoot = Path.Combine(Path.GetTempPath(), "viget-" + Guid.NewGuid().ToString("N")); + + /// <inheritdoc/> + public void Dispose() + { + if (Directory.Exists(artifactRoot)) + { + Directory.Delete(artifactRoot, recursive: true); + } + } + + [Fact] + public void PropertiesAreReadAsJavaReadsThem() + { + IReadOnlyDictionary<string, string> properties = JavaProperties.Parse( + "# a comment\n" + + "! another\n" + + "publisher=Progmasoft\n" + + "homepage = https\\://example.com/a\n" + + "description:Builds C \\u0026 C++,\\\n" + + " on every host.\n" + + "key\\ with\\ spaces value\n" + + "empty=\n"); + Assert.Equal("Progmasoft", properties["publisher"]); + Assert.Equal("https://example.com/a", properties["homepage"]); + Assert.Equal("Builds C & C++,on every host.", properties["description"]); + Assert.Equal("value", properties["key with spaces"]); + Assert.Equal(string.Empty, properties["empty"]); + Assert.Throws<FormatException>(() => JavaProperties.Parse("broken=\\u12")); + } + + [Fact] + public void TheDescriptorOfAPluginJarIsReadAndChecked() + { + Assert.Null(TazelPluginDescriptor.TryRead(Jar(), out TazelPluginDescriptor? descriptor)); + Assert.Equal( + new TazelPluginDescriptor( + "Progmasoft", "CMake", "0.1.0", 2, "Builds CMake projects.", ["First Author", "Second Author"], + "MPL-2.0", "https://example.com/cmake", null), + descriptor!, + new DescriptorComparer()); + + Assert.Equal("The JAR has no META-INF/services/com.progmasoft.api.tazel.dsl.plugin.ProjectPlugin.", + TazelPluginDescriptor.TryRead(Jar(service: false), out _)); + Assert.Equal("The descriptor needs an exact semantic version.", + TazelPluginDescriptor.TryRead(Jar(version: "1.0"), out _)); + Assert.Equal("The descriptor needs a name that is one identifier.", + TazelPluginDescriptor.TryRead(Jar(name: "c-make"), out _)); + Assert.Equal("The homepage of the descriptor is not an https address.", + TazelPluginDescriptor.TryRead(Jar(homepage: "http\\://example.com"), out _)); + Assert.Equal("The file is not a JAR.", TazelPluginDescriptor.TryRead("not a zip"u8.ToArray(), out _)); + } + + [Fact] + public async Task OnlyAnAllowedPublisherWithAnAccountPublishes() + { + (TazelPluginPublishing publishing, InMemoryAccountStore accounts, ITazelPluginCatalog catalog) = Publishing(); + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + + PublishResult noAccount = await publishing.PublishAsync(Jar(), cancellationToken); + Assert.Equal("No Progmasoft Account is named 'Progmasoft'.", noAccount.Error); + + await AddAccountAsync(accounts, "Progmasoft"); + await AddAccountAsync(accounts, "Someone"); + PublishResult notAllowed = await publishing.PublishAsync(Jar(publisher: "Someone"), cancellationToken); + Assert.Equal( + "Publishing Tazel plugins is open to Progmasoft for now; 'Someone' cannot publish yet.", + notAllowed.Error); + + byte[] jar = Jar(); + PublishResult published = await publishing.PublishAsync(jar, cancellationToken); + Assert.True(published.Published, published.Error); + string file = Path.Combine(artifactRoot, "dslplugins", "tazel", "Progmasoft", "CMake", "0.1.0", "CMake-0.1.0.jar"); + Assert.Equal(jar, await File.ReadAllBytesAsync(file, cancellationToken)); + TazelPlugin plugin = (await catalog.FindAsync("Progmasoft", "CMake", cancellationToken))!; + Assert.Equal(Convert.ToHexStringLower(SHA256.HashData(jar)), plugin.Versions.Single().Sha256); + Assert.Equal("MPL-2.0", plugin.License); + + PublishResult again = await publishing.PublishAsync(Jar(description: "Another build."), cancellationToken); + Assert.Equal("Progmasoft.CMake 0.1.0 is published already.", again.Error); + Assert.Equal(jar, await File.ReadAllBytesAsync(file, cancellationToken)); + + PublishResult folded = await publishing.PublishAsync(Jar(name: "CMAKE", version: "0.2.0"), cancellationToken); + Assert.Equal( + "Progmasoft has a plugin whose name differs from 'CMAKE' only in letter case.", folded.Error); + Assert.False(File.Exists(Path.Combine( + artifactRoot, "dslplugins", "tazel", "Progmasoft", "CMAKE", "0.2.0", "CMAKE-0.2.0.jar"))); + } + + [Fact] + public async Task ViGetNeedsADescriptionAnAuthorAndALicense() + { + (TazelPluginPublishing publishing, InMemoryAccountStore accounts, _) = Publishing(); + await AddAccountAsync(accounts, "Progmasoft"); + PublishResult result = await publishing.PublishAsync(Jar(license: null), TestContext.Current.CancellationToken); + Assert.Equal( + "ViGet needs a description, at least one author and a license in the descriptor.", result.Error); + } + + [Fact] + public async Task TheIndexIsWhatTazelReads() + { + await using AccountApiFactory factory = new(); + ITazelPluginCatalog catalog = factory.Services.GetRequiredService<ITazelPluginCatalog>(); + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + Assert.Null(TazelPluginDescriptor.TryRead(Jar(), out TazelPluginDescriptor? descriptor)); + await catalog.AddVersionAsync(descriptor!, new string('a', 64), 10, DateTimeOffset.UnixEpoch, cancellationToken); + await catalog.AddVersionAsync( + descriptor! with { Version = "0.2.0" }, new string('b', 64), 10, DateTimeOffset.UnixEpoch.AddDays(1), + cancellationToken); + await catalog.SetYankedAsync("Progmasoft", "CMake", "0.2.0", true, cancellationToken); + using HttpClient client = factory.CreateClient(); + + using JsonDocument index = JsonDocument.Parse(await client.GetStringAsync( + "/api/v1/viget/dslplugins/tazel/Progmasoft/CMake/index.json", cancellationToken)); + Assert.Equal("Progmasoft", index.RootElement.GetProperty("publisher").GetString()); + Assert.Equal("CMake", index.RootElement.GetProperty("name").GetString()); + JsonElement[] versions = [.. index.RootElement.GetProperty("versions").EnumerateArray()]; + Assert.Equal(["0.2.0", "0.1.0"], versions.Select(version => version.GetProperty("version").GetString())); + Assert.True(versions[0].GetProperty("yanked").GetBoolean()); + Assert.Equal(2, versions[1].GetProperty("apiVersion").GetInt32()); + Assert.Equal(new string('a', 64), versions[1].GetProperty("sha256").GetString()); + + using JsonDocument detail = JsonDocument.Parse(await client.GetStringAsync( + "/api/v1/viget/dslplugins/tazel/Progmasoft/CMake", cancellationToken)); + Assert.Equal("0.1.0", detail.RootElement.GetProperty("latestVersion").GetString()); + Assert.Equal( + "/dslplugins/tazel/Progmasoft/CMake/0.1.0/CMake-0.1.0.jar", + detail.RootElement.GetProperty("versions")[1].GetProperty("path").GetString()); + + using JsonDocument list = JsonDocument.Parse( + await client.GetStringAsync("/api/v1/viget/dslplugins/tazel", cancellationToken)); + Assert.Equal("CMake", list.RootElement.GetProperty("plugins")[0].GetProperty("name").GetString()); + + Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync( + "/api/v1/viget/dslplugins/tazel/Progmasoft/Missing/index.json", cancellationToken)).StatusCode); + Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync( + "/api/v1/viget/dslplugins/tazel/Progmasoft/not-a-name/index.json", cancellationToken)).StatusCode); + } + + [Fact] + public async Task TheCommandPublishesAndYanks() + { + (TazelPluginPublishing publishing, InMemoryAccountStore accounts, ITazelPluginCatalog catalog) = Publishing(); + await AddAccountAsync(accounts, "Progmasoft"); + ServiceProvider services = new ServiceCollection() + .AddSingleton(publishing) + .AddSingleton(catalog) + .BuildServiceProvider(); + string jar = Path.Combine(artifactRoot, "upload.jar"); + Directory.CreateDirectory(artifactRoot); + await File.WriteAllBytesAsync(jar, Jar(), TestContext.Current.CancellationToken); + StringWriter output = new(); + StringWriter errors = new(); + + Assert.Equal(0, await ViGetCommand.RunAsync( + services, ["publish-tazel-plugin", jar], output, errors, TestContext.Current.CancellationToken)); + Assert.Contains("Published Progmasoft.CMake 0.1.0", output.ToString(), StringComparison.Ordinal); + Assert.Equal(0, await ViGetCommand.RunAsync( + services, ["yank-tazel-plugin", "Progmasoft", "CMake", "0.1.0"], output, errors, + TestContext.Current.CancellationToken)); + Assert.True((await catalog.FindAsync("Progmasoft", "CMake", TestContext.Current.CancellationToken))! + .Versions.Single().Yanked); + Assert.Equal(1, await ViGetCommand.RunAsync( + services, ["publish-tazel-plugin", jar], output, errors, TestContext.Current.CancellationToken)); + Assert.Equal(2, await ViGetCommand.RunAsync( + services, ["publish"], output, errors, TestContext.Current.CancellationToken)); + } + + private (TazelPluginPublishing, InMemoryAccountStore, ITazelPluginCatalog) Publishing() + { + InMemoryAccountStore accounts = new(); + InMemoryTazelPluginCatalog catalog = new(); + TazelPluginPublishing publishing = new( + catalog, + accounts, + Options.Create(new ViGetOptions { ArtifactRoot = artifactRoot }), + TimeProvider.System); + return (publishing, accounts, catalog); + } + + private static async Task AddAccountAsync(InMemoryAccountStore accounts, string name) => + await accounts.CreateAsync( + new AccountRecord( + Guid.NewGuid(), + name, + AccountNamePolicy.NormalizeForLookup(name), + $"{name}@example.com", + $"{name}@example.com".ToUpperInvariant(), + null, + null, + DateTimeOffset.UnixEpoch), + TestContext.Current.CancellationToken); + + private static byte[] Jar( + string publisher = "Progmasoft", + string name = "CMake", + string version = "0.1.0", + string? description = "Builds CMake projects.", + string? license = "MPL-2.0", + string homepage = "https\\://example.com/cmake", + bool service = true) + { + using MemoryStream bytes = new(); + using (ZipArchive archive = new(bytes, ZipArchiveMode.Create, leaveOpen: true)) + { + List<string> lines = + [ + "publisher=" + publisher, + "name=" + name, + "version=" + version, + "apiVersion=2", + "author.0=First Author", + "author.1=Second Author", + "homepage=" + homepage + ]; + if (description is not null) + { + lines.Add("description=" + description); + } + + if (license is not null) + { + lines.Add("license=" + license); + } + + using (StreamWriter writer = new(archive.CreateEntry(TazelPluginDescriptor.DescriptorEntry).Open())) + { + writer.Write(string.Join('\n', lines) + "\n"); + } + + if (service) + { + using StreamWriter writer = new(archive.CreateEntry(TazelPluginDescriptor.ServiceEntry).Open()); + writer.Write("com.example.CMakePlugin\n"); + } + } + + return bytes.ToArray(); + } + + /// <summary>Compares descriptors by value, with their authors as sequences.</summary> + private sealed class DescriptorComparer : IEqualityComparer<TazelPluginDescriptor> + { + public bool Equals(TazelPluginDescriptor? left, TazelPluginDescriptor? right) => + left is not null && right is not null && + left with { Authors = [] } == right with { Authors = [] } && + left.Authors.SequenceEqual(right.Authors); + + public int GetHashCode(TazelPluginDescriptor descriptor) => descriptor.Name.GetHashCode(StringComparison.Ordinal); + } +} |