Add an explicit local artifact download proxy flag for development and tests. Keep the public download endpoint as a redirect while routing enabled loopback artifacts through the internal proxy.
222 lines
9.8 KiB
C#
222 lines
9.8 KiB
C#
using Dapper;
|
|
using Microsoft.AspNetCore.OutputCaching;
|
|
using Newtonsoft.Json;
|
|
using PluginBuilder.APIModels;
|
|
using PluginBuilder.DataModels;
|
|
using PluginBuilder.Services;
|
|
using PluginBuilder.Util.Extensions;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace PluginBuilder.Tests;
|
|
|
|
public class UnitTest1 : UnitTestBase
|
|
{
|
|
public UnitTest1(ITestOutputHelper logs) : base(logs)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Test1()
|
|
{
|
|
await using var tester = await Start();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PluginsSearchWithNullByte_DoesNotReturnServerError()
|
|
{
|
|
await using var tester = await Start();
|
|
|
|
var ownerId = await tester.CreateFakeUserAsync();
|
|
await tester.CreateAndBuildPluginAsync(ownerId);
|
|
|
|
var client = tester.CreateHttpClient();
|
|
var urls = new[]
|
|
{
|
|
"/public/plugins?searchPluginName=%00",
|
|
"/api/v1/plugins?searchPluginName=%00"
|
|
};
|
|
|
|
foreach (var url in urls)
|
|
{
|
|
var response = await client.GetAsync(url);
|
|
Assert.True(response.IsSuccessStatusCode,
|
|
$"Expected successful response for {url}, but got {(int)response.StatusCode} ({response.StatusCode}).");
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("test-6", true)]
|
|
[InlineData("test-6-", false)]
|
|
[InlineData("6test-6", false)]
|
|
[InlineData("-test-6", false)]
|
|
[InlineData("te", false)]
|
|
[InlineData("teqoeteqoeteqoeteqoeteqoeteqoee", false)]
|
|
[InlineData("teqoeteqoeteqoeteqoeteqoet", true)]
|
|
public void IsValidSlugTest(string slug, bool expected)
|
|
{
|
|
Assert.Equal(expected, PluginSlug.IsValidSlugName(slug));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanPackPlugin()
|
|
{
|
|
await using var tester = Create();
|
|
tester.ReuseDatabase = false;
|
|
await tester.Start();
|
|
|
|
await using var conn = await tester.GetService<DBConnectionFactory>().Open();
|
|
//https://github.com/NicolasDorier/btcpayserver/tree/plugins/collection2/Plugins/BTCPayServer.Plugins.RockstarStylist
|
|
var ownerId = await tester.CreateFakeUserAsync();
|
|
var fullBuildId = await tester.CreateAndBuildPluginAsync(ownerId);
|
|
|
|
var client = tester.CreateHttpClient();
|
|
var versions = await client.GetPublishedVersions("1.4.6.0", true);
|
|
var version = Assert.Single(versions);
|
|
Assert.NotNull(version);
|
|
var prev = version;
|
|
version = await client.GetPlugin(version.ProjectSlug, version.Version);
|
|
Assert.NotNull(version);
|
|
Assert.Equal(JsonConvert.SerializeObject(version), JsonConvert.SerializeObject(prev));
|
|
|
|
Assert.Null(await client.GetPlugin(version.ProjectSlug, "10.0.0.1"));
|
|
Assert.Equal("1.0.2.0", version.Version);
|
|
versions = await client.GetPublishedVersions("1.4.5.9", true);
|
|
Assert.Empty(versions);
|
|
versions = await client.GetPublishedVersions("1.4.6.0", false);
|
|
Assert.Empty(versions);
|
|
|
|
// Can download the project?
|
|
var b1 = await client.DownloadPlugin(new PluginSelectorBySlug("rockstar-stylist"), PluginVersion.Parse("1.0.2.0"));
|
|
var b2 = await client.DownloadPlugin(new PluginSelectorByIdentifier("BTCPayServer.Plugins.RockstarStylist"), PluginVersion.Parse("1.0.2.0"));
|
|
Assert.NotNull(b1);
|
|
Assert.NotNull(b2);
|
|
Assert.Equal(b1.Length, b2.Length);
|
|
|
|
|
|
var manifest = PluginManifest.Parse(version.ManifestInfo.ToString());
|
|
|
|
// Nothing changed
|
|
Assert.False(await conn.SetVersionBuild(fullBuildId, manifest.Version, manifest.BTCPayMinVersion, manifest.BTCPayMaxVersion, true));
|
|
// Can change BTCPayMinVersion
|
|
Assert.True(await conn.SetVersionBuild(fullBuildId, manifest.Version, null, manifest.BTCPayMaxVersion, true));
|
|
// Can remove pre-release
|
|
Assert.True(await conn.SetVersionBuild(fullBuildId, manifest.Version, manifest.BTCPayMinVersion, manifest.BTCPayMaxVersion, false));
|
|
|
|
// Can't put back in pre-release
|
|
Assert.False(await conn.SetVersionBuild(fullBuildId, manifest.Version, manifest.BTCPayMinVersion, manifest.BTCPayMaxVersion, true));
|
|
// Can't modify pre-release
|
|
Assert.False(await conn.SetVersionBuild(fullBuildId, manifest.Version, null, manifest.BTCPayMaxVersion, false));
|
|
|
|
|
|
// Another plugin slug try to hijack the package
|
|
await tester.CreateAndBuildPluginAsync(
|
|
ownerId,
|
|
"rockstar-stylist-fake",
|
|
"plugins/collection2",
|
|
"Plugins/BTCPayServer.Plugins.RockstarStylist"
|
|
);
|
|
|
|
var rockstarPlugins =
|
|
await conn.QueryAsync<string?>("SELECT slug FROM plugins WHERE identifier='BTCPayServer.Plugins.RockstarStylist'");
|
|
var p = Assert.Single(rockstarPlugins);
|
|
Assert.Equal("rockstar-stylist", p);
|
|
versions = await client.GetPublishedVersions("1.4.6.0", true);
|
|
version = Assert.Single(versions);
|
|
Assert.Equal("rockstar-stylist", version.ProjectSlug);
|
|
|
|
// Let's see what happen if there is two versions of the same plugin
|
|
await conn.ExecuteAsync("""
|
|
INSERT INTO versions (plugin_slug, ver, build_id, btcpay_min_ver, btcpay_max_ver, pre_release, updated_at, signatureproof)
|
|
VALUES ('rockstar-stylist', ARRAY[1,0,2,1], 0, ARRAY[1,4,6,0], NULL, 'f', CURRENT_TIMESTAMP, NULL)
|
|
""");
|
|
var outputCacheStore = tester.GetService<IOutputCacheStore>();
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
versions = await client.GetPublishedVersions("1.4.6.0", true);
|
|
version = Assert.Single(versions);
|
|
Assert.Equal("1.0.2.1", version.Version);
|
|
versions = await client.GetPublishedVersions("1.4.6.0", true, true);
|
|
Assert.Equal("1.0.2.1", versions[1].Version);
|
|
Assert.Equal("1.0.2.0", versions[0].Version);
|
|
|
|
// listed - always render
|
|
await conn.ExecuteAsync("UPDATE plugins SET visibility = 'listed' WHERE slug = 'rockstar-stylist'");
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
var res = await client.GetPublishedVersions("2.1.0.0", false);
|
|
Assert.Contains(res, p => p.ProjectSlug == "rockstar-stylist");
|
|
|
|
// unlisted - only render with compatible search term or legacy versions
|
|
await conn.ExecuteAsync("UPDATE plugins SET visibility = 'unlisted' WHERE slug = 'rockstar-stylist'");
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
res = await client.GetPublishedVersions("2.1.0.0", false);
|
|
Assert.DoesNotContain(res, p => p.ProjectSlug == "rockstar-stylist");
|
|
|
|
res = await client.GetPublishedVersions("2.1.0.0", false, searchPluginName: "rockstar");
|
|
Assert.Contains(res, p => p.ProjectSlug == "rockstar-stylist");
|
|
|
|
var raw = await client.GetStringAsync("/api/v1/plugins");
|
|
var legacyRes = JsonConvert.DeserializeObject<PublishedVersion[]>(raw);
|
|
Assert.Contains(legacyRes, p => p.ProjectSlug == "rockstar-stylist");
|
|
|
|
// hidden - never render
|
|
await conn.ExecuteAsync("UPDATE plugins SET visibility = 'hidden' WHERE slug = 'rockstar-stylist'");
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
res = await client.GetPublishedVersions("2.1.0.0", false);
|
|
Assert.DoesNotContain(res, p => p.ProjectSlug == "rockstar-stylist");
|
|
|
|
res = await client.GetPublishedVersions("2.1.0.0", false, searchPluginName: "rockstar");
|
|
Assert.DoesNotContain(res, p => p.ProjectSlug == "rockstar-stylist");
|
|
}
|
|
[Fact]
|
|
public async Task DownloadEndpoint_UsesInternalLoopbackRedirectWhenLocalArtifactProxyEnabled()
|
|
{
|
|
await using var tester = Create();
|
|
tester.ReuseDatabase = false;
|
|
tester.EnableLocalArtifactDownloadProxy = true;
|
|
await tester.Start();
|
|
|
|
var ownerId = await tester.CreateFakeUserAsync();
|
|
await tester.CreateAndBuildPluginAsync(ownerId);
|
|
|
|
using var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
|
|
client.BaseAddress = new Uri(tester.WebApp.Urls.First(), UriKind.Absolute);
|
|
|
|
using var response = await client.GetAsync("api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download");
|
|
|
|
Assert.Equal(System.Net.HttpStatusCode.Found, response.StatusCode);
|
|
Assert.NotNull(response.Headers.Location);
|
|
Assert.Equal(
|
|
"/api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download-loopback",
|
|
response.Headers.Location!.OriginalString);
|
|
|
|
using var proxiedResponse = await client.GetAsync(response.Headers.Location);
|
|
|
|
Assert.Equal(System.Net.HttpStatusCode.OK, proxiedResponse.StatusCode);
|
|
Assert.Equal("application/zip", proxiedResponse.Content.Headers.ContentType?.MediaType);
|
|
Assert.True((await proxiedResponse.Content.ReadAsByteArrayAsync()).Length > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadEndpoint_DoesNotUseInternalLoopbackRedirectWhenLocalArtifactProxyDisabled()
|
|
{
|
|
await using var tester = Create();
|
|
tester.ReuseDatabase = false;
|
|
await tester.Start();
|
|
|
|
var ownerId = await tester.CreateFakeUserAsync();
|
|
await tester.CreateAndBuildPluginAsync(ownerId);
|
|
|
|
using var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
|
|
client.BaseAddress = new Uri(tester.WebApp.Urls.First(), UriKind.Absolute);
|
|
|
|
using var response = await client.GetAsync("api/v1/plugins/rockstar-stylist/versions/1.0.2.0/download");
|
|
|
|
Assert.Equal(System.Net.HttpStatusCode.Found, response.StatusCode);
|
|
Assert.NotNull(response.Headers.Location);
|
|
Assert.DoesNotContain("download-loopback", response.Headers.Location!.OriginalString, StringComparison.Ordinal);
|
|
Assert.True(response.Headers.Location.IsAbsoluteUri);
|
|
Assert.True(response.Headers.Location.IsLoopback);
|
|
}
|
|
|
|
}
|