btcpayserver-plugin-builder/PluginBuilder.Tests/UnitTest1.cs
thgO.O 04a64d9178
Handle local loopback plugin downloads
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.
2026-04-26 12:45:37 -03:00

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);
}
}