From 75d492df6589a8c2551ed35fa6466878251f639c Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Tue, 2 Jul 2024 03:05:49 -0300 Subject: [PATCH] Add grace period support post-install, simplify analyzer/generator By moving more code to the diagnostic manager we can simplify the consuming side. Added analyzer tests to ensure we cover all scenarios end to end. --- .github/workflows/os-matrix.json | 1 + src/SponsorLink/Analyzer/Analyzer.csproj | 18 +- .../Analyzer/StatusReportingGenerator.cs | 7 +- .../buildTransitive/SponsorableLib.targets | 2 +- src/SponsorLink/SponsorLink.targets | 6 + .../SponsorLink/DiagnosticsManager.cs | 117 ++++++--- src/SponsorLink/SponsorLink/SponsorLink.cs | 29 ++- .../SponsorLink/SponsorLink.csproj | 6 + .../SponsorLink/SponsorLinkAnalyzer.cs | 48 +--- src/SponsorLink/SponsorLink/SponsorStatus.cs | 4 + .../Devlooped.Sponsors.targets | 6 +- src/SponsorLink/Tests/AnalyzerTests.cs | 223 ++++++++++++++++++ src/SponsorLink/Tests/Resources.Designer.cs | 63 ----- src/SponsorLink/Tests/SponsorableManifest.cs | 32 ++- src/SponsorLink/Tests/Tests.csproj | 27 +-- src/SponsorLink/Tests/keys/kzu.key | Bin 0 -> 1767 bytes src/SponsorLink/Tests/keys/kzu.key.jwk | 11 + src/SponsorLink/Tests/keys/kzu.key.txt | 1 + src/SponsorLink/Tests/keys/kzu.pub | Bin 0 -> 398 bytes src/SponsorLink/Tests/keys/kzu.pub.jwk | 5 + src/SponsorLink/Tests/keys/kzu.pub.txt | 1 + src/SponsorLink/Tests/keys/sponsorlink.jwt | 1 + 22 files changed, 414 insertions(+), 194 deletions(-) create mode 100644 .github/workflows/os-matrix.json create mode 100644 src/SponsorLink/Tests/AnalyzerTests.cs delete mode 100644 src/SponsorLink/Tests/Resources.Designer.cs create mode 100644 src/SponsorLink/Tests/keys/kzu.key create mode 100644 src/SponsorLink/Tests/keys/kzu.key.jwk create mode 100644 src/SponsorLink/Tests/keys/kzu.key.txt create mode 100644 src/SponsorLink/Tests/keys/kzu.pub create mode 100644 src/SponsorLink/Tests/keys/kzu.pub.jwk create mode 100644 src/SponsorLink/Tests/keys/kzu.pub.txt create mode 100644 src/SponsorLink/Tests/keys/sponsorlink.jwt diff --git a/.github/workflows/os-matrix.json b/.github/workflows/os-matrix.json new file mode 100644 index 0000000..adbf722 --- /dev/null +++ b/.github/workflows/os-matrix.json @@ -0,0 +1 @@ +[ "ubuntu-latest", "macOS-latest", "windows-latest" ] \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/Analyzer.csproj b/src/SponsorLink/Analyzer/Analyzer.csproj index f65390a..168e9d1 100644 --- a/src/SponsorLink/Analyzer/Analyzer.csproj +++ b/src/SponsorLink/Analyzer/Analyzer.csproj @@ -9,6 +9,7 @@ $(MSBuildThisFileDirectory)..\SponsorLink.targets true disable + SponsorableLib @@ -22,16 +23,23 @@ - - - - - + + + + + + $([System.IO.File]::ReadAllText('$(MSBuildThisFileDirectory)..\Tests\keys\kzu.pub.jwk')) + + + + + + \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/StatusReportingGenerator.cs b/src/SponsorLink/Analyzer/StatusReportingGenerator.cs index 0a13b1c..a1437f9 100644 --- a/src/SponsorLink/Analyzer/StatusReportingGenerator.cs +++ b/src/SponsorLink/Analyzer/StatusReportingGenerator.cs @@ -10,10 +10,13 @@ public class StatusReportingGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterSourceOutput( - context.GetSponsorManifests(), + // this is required to ensure status is registered properly independently + // of analyzer runs. + context.GetSponsorAdditionalFiles().Combine(context.AnalyzerConfigOptionsProvider), (spc, source) => { - var status = Diagnostics.GetOrSetStatus(source); + var (manifests, options) = source; + var status = Diagnostics.GetOrSetStatus(manifests, options); spc.AddSource("StatusReporting.cs", $"// Status: {status}"); }); } diff --git a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets b/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets index 37585e8..8c0f8ef 100644 --- a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets +++ b/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink.targets b/src/SponsorLink/SponsorLink.targets index 4678d5d..9bbe3f4 100644 --- a/src/SponsorLink/SponsorLink.targets +++ b/src/SponsorLink/SponsorLink.targets @@ -14,6 +14,7 @@ $(Product) + $(PackageId) $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) @@ -83,13 +84,18 @@ + + + $(FundingProduct) namespace Devlooped.Sponsors%3B partial class SponsorLink { public partial class Funding { + public const string PackageId = "$(FundingPackageId)"%3B public const string Product = "$(FundingProduct)"%3B public const string Prefix = "$(FundingPrefix)"%3B public const int Grace = $(FundingGrace)%3B diff --git a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs index 96e7e14..8c191ec 100644 --- a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs +++ b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs @@ -9,6 +9,7 @@ using System.IO; using System.IO.MemoryMappedFiles; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using Humanizer; using Humanizer.Localisation; @@ -50,14 +51,15 @@ ConcurrentDictionary Diagnostics /// The removed diagnostic, or if none was previously pushed. public void ReportOnce(Action report, string product = Funding.Product) { - if (Diagnostics.TryRemove(product, out var diagnostic)) + if (Diagnostics.TryRemove(product, out var diagnostic) && + GetStatus(diagnostic) != SponsorStatus.Grace) { // Ensure only one such diagnostic is reported per product for the entire process, // so that we can avoid polluting the error list with duplicates across multiple projects. var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id); using var mutex = new Mutex(false, "mutex" + id); mutex.WaitOne(); - using var mmf = MemoryMappedFile.CreateOrOpen(id, 1); + using var mmf = CreateOrOpenMemoryMappedFile(id, 1); using var accessor = mmf.CreateViewAccessor(); if (accessor.ReadByte(0) == 0) { @@ -75,52 +77,61 @@ public void ReportOnce(Action report, string product = Funding.Produ /// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions). /// /// Optional that was reported, if any. + /// + /// The SponsorLinkAnalyzer.GetOrSetStatus uses diagnostic properties to store the + /// kind of diagnostic as a simple string instead of the enum. We do this so that + /// multiple analyzers or versions even across multiple products, which all would + /// have their own enum, can still share the same diagnostic kind. + /// public SponsorStatus? GetStatus() - { - // NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the - // kind of diagnostic as a simple string instead of the enum. We do this so that - // multiple analyzers or versions even across multiple products, which all would - // have their own enum, can still share the same diagnostic kind. - if (Diagnostics.TryGetValue(Funding.Product, out var diagnostic) && - diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)) - { - // Switch on value matching DiagnosticKind names - return value switch - { - nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown, - nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor, - nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring, - nameof(SponsorStatus.Expired) => SponsorStatus.Expired, - _ => null, - }; - } - - return null; - } + => Diagnostics.TryGetValue(Funding.Product, out var diagnostic) ? GetStatus(diagnostic) : null; /// /// Gets the status of the , or sets it from /// the given set of if not already set. /// - public SponsorStatus GetOrSetStatus(ImmutableArray manifests) - => GetOrSetStatus(() => manifests); + public SponsorStatus GetOrSetStatus(ImmutableArray manifests, AnalyzerConfigOptionsProvider options) + => GetOrSetStatus(() => manifests, () => options.GlobalOptions); /// /// Gets the status of the , or sets it from /// the given analyzer if not already set. /// public SponsorStatus GetOrSetStatus(Func options) - => GetOrSetStatus(() => options().GetSponsorManifests()); + => GetOrSetStatus(() => options().GetSponsorAdditionalFiles(), () => options()?.AnalyzerConfigOptionsProvider.GlobalOptions); - SponsorStatus GetOrSetStatus(Func> getManifests) + SponsorStatus GetOrSetStatus(Func> getAdditionalFiles, Func getGlobalOptions) { if (GetStatus() is { } status) return status; - if (!SponsorLink.TryRead(out var claims, getManifests().Select(text => + if (!SponsorLink.TryRead(out var claims, getAdditionalFiles().Where(x => x.Path.EndsWith(".jwt")).Select(text => (text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) || claims.GetExpiration() is not DateTime exp) { + var noGrace = getGlobalOptions() is { } globalOptions && + globalOptions.TryGetValue("build_property.SponsorLinkNoInstallGrace", out var value) && + bool.TryParse(value, out var skipCheck) && skipCheck; + + if (noGrace != true) + { + // Consider grace period if we can find the install time. + var installed = getAdditionalFiles() + .Where(x => x.Path.EndsWith(".dll")) + .Select(x => File.GetLastWriteTime(x.Path)) + .OrderByDescending(x => x) + .FirstOrDefault(); + + if (installed != default && ((DateTime.Now - installed).TotalDays <= Funding.Grace)) + { + // report unknown, either unparsed manifest or one with no expiration (which we never emit). + Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null, + properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Grace)), + Funding.Product, Sponsorables.Keys.Humanize(Resources.Or))); + return SponsorStatus.Grace; + } + } + // report unknown, either unparsed manifest or one with no expiration (which we never emit). Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null, properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)), @@ -169,7 +180,7 @@ Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product) var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id); using var mutex = new Mutex(false, "mutex" + id); mutex.WaitOne(); - using var mmf = MemoryMappedFile.CreateOrOpen(id, 1); + using var mmf = CreateOrOpenMemoryMappedFile(id, 1); using var accessor = mmf.CreateViewAccessor(); accessor.Write(0, 0); Tracing.Trace($"👉{diagnostic.Severity.ToString().ToLowerInvariant()}:{Process.GetCurrentProcess().Id}:{Process.GetCurrentProcess().ProcessName}:{product}:{diagnostic.Id}"); @@ -178,16 +189,46 @@ Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product) return diagnostic; } + SponsorStatus? GetStatus(Diagnostic? diagnostic) => diagnostic?.Properties.TryGetValue(nameof(SponsorStatus), out var value) == true + ? value switch + { + nameof(SponsorStatus.Grace) => SponsorStatus.Grace, + nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown, + nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor, + nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring, + nameof(SponsorStatus.Expired) => SponsorStatus.Expired, + _ => null, + } + : null; + + static MemoryMappedFile CreateOrOpenMemoryMappedFile(string mapName, long capacity) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return MemoryMappedFile.CreateOrOpen(mapName, capacity); + } + else + { + // On Linux, use a file-based memory-mapped file + string filePath = $"/tmp/{mapName}"; + using (var fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + fs.SetLength(capacity); + return MemoryMappedFile.CreateFromFile(fs, mapName, capacity, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, false); + } + } + } + internal static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new( - $"{prefix}100", - Resources.Sponsor_Title, - Resources.Sponsor_Message, - "SponsorLink", - DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: Resources.Sponsor_Description, - helpLinkUri: "https://github.com/devlooped#sponsorlink", - "DoesNotSupportF1Help"); + $"{prefix}100", + Resources.Sponsor_Title, + Resources.Sponsor_Message, + "SponsorLink", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: Resources.Sponsor_Description, + helpLinkUri: "https://github.com/devlooped#sponsorlink", + "DoesNotSupportF1Help"); internal static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new( $"{prefix}101", diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs index b3b1cf3..1efc487 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.cs +++ b/src/SponsorLink/SponsorLink/SponsorLink.cs @@ -64,31 +64,38 @@ static partial class SponsorLink .Max().DateTime is var exp && exp == DateTime.MinValue ? null : exp; /// - /// Gets all sponsor manifests from the provided analyzer options. + /// Gets all necessary additional files to determine status. /// - public static ImmutableArray GetSponsorManifests(this AnalyzerOptions? options) + public static ImmutableArray GetSponsorAdditionalFiles(this AnalyzerOptions? options) => options == null ? ImmutableArray.Create() : options.AdditionalFiles - .Where(x => - options.AnalyzerConfigOptionsProvider.GetOptions(x).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && - itemType == "SponsorManifest" && - Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(x.Path))) + .Where(x => x.IsSponsorManifest(options.AnalyzerConfigOptionsProvider) || x.IsSponsorableAnalyzer(options.AnalyzerConfigOptionsProvider)) .ToImmutableArray(); /// /// Gets all sponsor manifests from the provided analyzer options. /// - public static IncrementalValueProvider> GetSponsorManifests(this IncrementalGeneratorInitializationContext context) + public static IncrementalValueProvider> GetSponsorAdditionalFiles(this IncrementalGeneratorInitializationContext context) => context.AdditionalTextsProvider.Combine(context.AnalyzerConfigOptionsProvider) .Where(source => { - var (text, options) = source; - return options.GetOptions(text).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && - itemType == "SponsorManifest" && - Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(text.Path)); + var (text, provider) = source; + return text.IsSponsorManifest(provider) || text.IsSponsorableAnalyzer(provider); }) .Select((source, c) => source.Left) .Collect(); + static bool IsSponsorManifest(this AdditionalText text, AnalyzerConfigOptionsProvider provider) + => provider.GetOptions(text).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && + itemType == "SponsorManifest" && + Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(text.Path)); + + static bool IsSponsorableAnalyzer(this AdditionalText text, AnalyzerConfigOptionsProvider provider) + => provider.GetOptions(text) is { } options && + options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && + options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && + itemType == "Analyzer" && + packageId == Funding.PackageId; + /// /// Reads all manifests, validating their signatures. /// diff --git a/src/SponsorLink/SponsorLink/SponsorLink.csproj b/src/SponsorLink/SponsorLink/SponsorLink.csproj index 740b146..0821365 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.csproj +++ b/src/SponsorLink/SponsorLink/SponsorLink.csproj @@ -11,6 +11,7 @@ $(Product) + $(PackageId) $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) @@ -37,13 +38,18 @@ + + + $(FundingProduct) namespace Devlooped.Sponsors%3B partial class SponsorLink { public partial class Funding { + public const string PackageId = "$(FundingPackageId)"%3B public const string Product = "$(FundingProduct)"%3B public const string Prefix = "$(FundingPrefix)"%3B public const int Grace = $(FundingGrace)%3B diff --git a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs index 0cf507f..dcdf2e9 100644 --- a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs +++ b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs @@ -1,9 +1,6 @@ // #nullable enable -using System; using System.Collections.Immutable; -using System.IO; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using static Devlooped.Sponsors.SponsorLink; @@ -40,48 +37,11 @@ public override void Initialize(AnalysisContext context) // Never report any diagnostic unless we're in an editor. if (IsEditor) { + // NOTE: for multiple projects with the same product name, we only report one diagnostic, + // so it's expected to NOT get a diagnostic back. Also, we don't want to report + // multiple diagnostics for each project in a solution that uses the same product. ctx.RegisterCompilationEndAction(ctx => - { - // NOTE: for multiple projects with the same product name, we only report one diagnostic, - // so it's expected to NOT get a diagnostic back. Also, we don't want to report - // multiple diagnostics for each project in a solution that uses the same product. - Diagnostics.ReportOnce(diagnostic => - { - // For unknown (never sync'ed), only report if install grace period is over - if (status == SponsorStatus.Unknown) - { - var noGrace = ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.SponsorLinkNoInstallGrace", out var value) && - bool.TryParse(value, out var skipCheck) && skipCheck; - - // NOTE: we'll always report if noGrace is set to true, regardless of install time, for - // testing purposes. This can be achieved via MSBuild with: - // - // true - // - // - // - // - if (noGrace == false) - { - var installed = ctx.Options.AdditionalFiles.Where(x => - { - var options = ctx.Options.AnalyzerConfigOptionsProvider.GetOptions(x); - // In release builds, we'll have a single such item, since we IL-merge the analyzer. - return options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && - options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && - itemType == "Analyzer" && - packageId == Funding.Product; - }).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault(); - - // NOTE: if we can't determine install time, we'll always report. - if (installed != default && installed.AddDays(Funding.Grace) > DateTime.Now) - return; - } - } - - ctx.ReportDiagnostic(diagnostic); - }); - }); + Diagnostics.ReportOnce(diagnostic => ctx.ReportDiagnostic(diagnostic))); } }); #pragma warning restore RS1013 // Start action has no registered non-end actions diff --git a/src/SponsorLink/SponsorLink/SponsorStatus.cs b/src/SponsorLink/SponsorLink/SponsorStatus.cs index 6cdbc90..97b344e 100644 --- a/src/SponsorLink/SponsorLink/SponsorStatus.cs +++ b/src/SponsorLink/SponsorLink/SponsorStatus.cs @@ -11,6 +11,10 @@ public enum SponsorStatus /// Unknown, /// + /// Sponsorship status is unknown, but within the grace period. + /// + Grace, + /// /// The sponsors manifest is expired but within the grace period. /// Expiring, diff --git a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets index 9f843e2..6e4492a 100644 --- a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets +++ b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets @@ -51,15 +51,15 @@ - + - %(SponsorablePackageId.Identity) + %(FundingPackageId.Identity) - + diff --git a/src/SponsorLink/Tests/AnalyzerTests.cs b/src/SponsorLink/Tests/AnalyzerTests.cs new file mode 100644 index 0000000..daed0fb --- /dev/null +++ b/src/SponsorLink/Tests/AnalyzerTests.cs @@ -0,0 +1,223 @@ +extern alias Analyzer; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Analyzer::Devlooped.Sponsors; +using Devlooped.Sponsors; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Tests; + +public class AnalyzerTests : IDisposable +{ + static readonly SponsorableManifest sponsorable = new( + new Uri("https://sponsorlink.devlooped.com"), + [new Uri("https://github.com/sponsors/devlooped"), new Uri("https://github.com/sponsors/kzu")], + "a82350fb2bae407b3021", + new JsonWebKey(ThisAssembly.Resources.keys.kzu_key.Text)); + + public AnalyzerTests() + { + // Simulate being a VS IDE for analyzers to actually run. + if (Environment.GetEnvironmentVariable("VSAPPIDNAME") == null) + Environment.SetEnvironmentVariable("VSAPPIDNAME", "test"); + } + + void IDisposable.Dispose() + { + if (Environment.GetEnvironmentVariable("VSAPPIDNAME") == "test") + Environment.SetEnvironmentVariable("VSAPPIDNAME", null); + } + + [Fact] + public async Task WhenNoAdditionalFiles_ThenReportsUnknown() + { + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer()]); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.NotEmpty(diagnostics); + + var diagnostic = diagnostics.Single(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + + Assert.True(diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + var status = Enum.Parse(value); + + Assert.Equal(SponsorStatus.Unknown, status); + } + + [Fact] + public async Task WhenUnknownAndGrace_ThenDoesNotReport() + { + // simulate an analyzer file with the right metadata, which is recent and therefore + // within the grace period + var dll = Path.Combine(GetTempPath(), "FakeAnalyzer.dll"); + File.WriteAllText(dll, ""); + + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer()], new AnalyzerOptions([new AdditionalTextFile(dll)], new TestAnalyzerConfigOptionsProvider(new()) + { + { "build_metadata.Analyzer.ItemType", "Analyzer" }, + { "build_metadata.Analyzer.NuGetPackageId", "SponsorableLib" } + })); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WhenUnknownAndNoGraceOption_ThenReportsUnknown() + { + // simulate an analyzer file with the right metadata, which is recent and therefore + // within the grace period + var dll = Path.Combine(GetTempPath(), "FakeAnalyzer.dll"); + File.WriteAllText(dll, ""); + + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer()], new AnalyzerOptions([new AdditionalTextFile(dll)], new TestAnalyzerConfigOptionsProvider(new()) + { + { "build_property.SponsorLinkNoInstallGrace", "true" }, + { "build_metadata.Analyzer.ItemType", "Analyzer" }, + { "build_metadata.Analyzer.NuGetPackageId", "SponsorableLib" } + })); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.NotEmpty(diagnostics); + + var diagnostic = diagnostics.Single(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + + Assert.True(diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + var status = Enum.Parse(value); + + Assert.Equal(SponsorStatus.Unknown, status); + } + + [Fact] + public async Task WhenUnknownAndGraceExpired_ThenReportsUnknown() + { + // simulate an analyzer file with the right metadata, which is recent and therefore + // within the grace period + var dll = Path.Combine(GetTempPath(), "FakeAnalyzer.dll"); + File.WriteAllText(dll, ""); + File.SetLastWriteTimeUtc(dll, DateTime.UtcNow - TimeSpan.FromDays(30)); + + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer()], new AnalyzerOptions([new AdditionalTextFile(dll)], new TestAnalyzerConfigOptionsProvider(new()) + { + { "build_metadata.Analyzer.ItemType", "Analyzer" }, + { "build_metadata.Analyzer.NuGetPackageId", "SponsorableLib" } + })); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.NotEmpty(diagnostics); + + var diagnostic = diagnostics.Single(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + + Assert.True(diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + var status = Enum.Parse(value); + + Assert.Equal(SponsorStatus.Unknown, status); + } + + [Fact] + public async Task WhenSponsoring_ThenReportsSponsor() + { + var sponsor = sponsorable.Sign([], expiration: TimeSpan.FromMinutes(5)); + var jwt = Path.Combine(GetTempPath(), "kzu.jwt"); + File.WriteAllText(jwt, sponsor, Encoding.UTF8); + + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer()], new AnalyzerOptions([new AdditionalTextFile(jwt)], new TestAnalyzerConfigOptionsProvider(new()) + { + { "build_metadata.SponsorManifest.ItemType", "SponsorManifest" } + })); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.NotEmpty(diagnostics); + + var diagnostic = diagnostics.Single(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + + Assert.True(diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + var status = Enum.Parse(value); + + Assert.Equal(SponsorStatus.Sponsor, status); + } + + [Fact] + public async Task WhenMultipleAnalyzers_ThenReportsOnce() + { + var compilation = CSharpCompilation.Create("test", [CSharpSyntaxTree.ParseText("//")]) + .WithAnalyzers([new SponsorLinkAnalyzer(), new SponsorLinkAnalyzer()]); + + var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); + + Assert.NotEmpty(diagnostics); + + var diagnostic = diagnostics.Single(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + + Assert.True(diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)); + var status = Enum.Parse(value); + + Assert.Equal(SponsorStatus.Unknown, status); + } + + + string GetTempPath([CallerMemberName] string? test = default) + { + var path = Path.Combine(Path.GetTempPath(), test ?? nameof(AnalyzerTests)); + Directory.CreateDirectory(path); + return path; + } + + class AdditionalTextFile(string path) : AdditionalText + { + public override string Path => path; + public override SourceText GetText(CancellationToken cancellationToken) => SourceText.From(File.ReadAllText(Path), Encoding.UTF8); + } + + class TestAnalyzerConfigOptionsProvider(Dictionary options) : AnalyzerConfigOptionsProvider, IDictionary + { + AnalyzerConfigOptions analyzerOptions = new TestAnalyzerConfigOptions(options); + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => analyzerOptions; + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => analyzerOptions; + public void Add(string key, string value) => options.Add(key, value); + public bool ContainsKey(string key) => options.ContainsKey(key); + public bool Remove(string key) => options.Remove(key); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) => options.TryGetValue(key, out value); + public void Add(KeyValuePair item) => ((ICollection>)options).Add(item); + public void Clear() => ((ICollection>)options).Clear(); + public bool Contains(KeyValuePair item) => ((ICollection>)options).Contains(item); + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)options).CopyTo(array, arrayIndex); + public bool Remove(KeyValuePair item) => ((ICollection>)options).Remove(item); + public IEnumerator> GetEnumerator() => ((IEnumerable>)options).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)options).GetEnumerator(); + public override AnalyzerConfigOptions GlobalOptions => analyzerOptions; + public ICollection Keys => options.Keys; + public ICollection Values => options.Values; + public int Count => ((ICollection>)options).Count; + public bool IsReadOnly => ((ICollection>)options).IsReadOnly; + public string this[string key] { get => options[key]; set => options[key] = value; } + + class TestAnalyzerConfigOptions(Dictionary options) : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) => options.TryGetValue(key, out value); + } + } +} diff --git a/src/SponsorLink/Tests/Resources.Designer.cs b/src/SponsorLink/Tests/Resources.Designer.cs deleted file mode 100644 index 7824a60..0000000 --- a/src/SponsorLink/Tests/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Tests { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tests.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/src/SponsorLink/Tests/SponsorableManifest.cs b/src/SponsorLink/Tests/SponsorableManifest.cs index d65d0fb..907fc10 100644 --- a/src/SponsorLink/Tests/SponsorableManifest.cs +++ b/src/SponsorLink/Tests/SponsorableManifest.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Security.Cryptography; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -186,15 +187,25 @@ public string ToJwt(SigningCredentials? signing = default) /// Sign the JWT claims with the provided RSA key. /// public string Sign(IEnumerable claims, RSA rsa, TimeSpan? expiration = default) - => Sign(claims, new RsaSecurityKey(rsa), expiration); - - public string Sign(IEnumerable claims, RsaSecurityKey? key = default, TimeSpan? expiration = default) { - var rsa = key ?? SecurityKey as RsaSecurityKey; - if (rsa?.PrivateKeyStatus != PrivateKeyStatus.Exists) + var key = new RsaSecurityKey(rsa); + if (key.PrivateKeyStatus != PrivateKeyStatus.Exists) throw new NotSupportedException("No private key found or specified to sign the manifest."); - var signing = new SigningCredentials(rsa, SecurityAlgorithms.RsaSha256); + // Don't allow mismatches of public manifest key and the one used to sign, to avoid + // weird run-time errors verifiying manifests that were signed with a different key. + if (!rsa.ThumbprintEquals(SecurityKey)) + throw new ArgumentException($"Cannot sign with a private key that does not match the manifest public key."); + + return Sign(claims, key, expiration); + } + + /// + /// Sign the JWT claims, optionally overriding the used for signing. + /// + public string Sign(IEnumerable claims, SecurityKey? key = default, TimeSpan? expiration = default) + { + var credentials = new SigningCredentials(key ?? SecurityKey, SecurityAlgorithms.RsaSha256); var expirationDate = expiration != null ? DateTime.UtcNow.Add(expiration.Value) : @@ -240,11 +251,6 @@ public string Sign(IEnumerable claims, RsaSecurityKey? key = default, Tim tokenClaims.Insert(1, new(JwtRegisteredClaimNames.Aud, audience)); } - // Don't allow mismatches of public manifest key and the one used to sign, to avoid - // weird run-time errors verifiying manifests that were signed with a different key. - if (!rsa.ThumbprintEquals(SecurityKey)) - throw new ArgumentException($"Cannot sign with a private key that does not match the manifest public key."); - return new JsonWebTokenHandler { MapInboundClaims = false, @@ -254,7 +260,7 @@ public string Sign(IEnumerable claims, RsaSecurityKey? key = default, Tim Subject = new ClaimsIdentity(tokenClaims), IssuedAt = DateTime.UtcNow, Expires = expirationDate, - SigningCredentials = signing, + SigningCredentials = credentials, }); } @@ -333,7 +339,7 @@ public string ClientId internal set { clientId = value; - var thumb = JsonWebKeyConverter.ConvertFromSecurityKey(SecurityKey).ComputeJwkThumbprint(); + var thumb = SecurityKey.ComputeJwkThumbprint(); hashcode = new HashCode().Add(Issuer, ClientId, Convert.ToBase64String(thumb)).AddRange(Audience).ToHashCode(); } } diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj index 5082c97..f596efb 100644 --- a/src/SponsorLink/Tests/Tests.csproj +++ b/src/SponsorLink/Tests/Tests.csproj @@ -3,6 +3,7 @@ net8.0 true + CS8981;$(NoWarn) @@ -11,31 +12,28 @@ + + - + + + - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - + + + + + @@ -56,7 +54,7 @@ - + @@ -66,4 +64,5 @@ + \ No newline at end of file diff --git a/src/SponsorLink/Tests/keys/kzu.key b/src/SponsorLink/Tests/keys/kzu.key new file mode 100644 index 0000000000000000000000000000000000000000..cddc6c6e0ad0bb89a57298a83c7fea7e0ee2272d GIT binary patch literal 1767 zcmV{`FP^pd33!Zx?bmeP$q0NKRpx5>1@J>Pl}aicPfG zTwP9GqbNU__nG`E!!;0?KrF4vK#Bw0D}jj1m6nyxSh_MXDXWQ&!k*AjaWZjf_Kj3< z6$gF*$&H{2iR3YFjyu=3^)mpReo+RTA(LK7?RLI7%h96LKYpMbIn0GwdPodEo>#_V zuaK8@ljhi2EB-)Hwj`VpAZ#n!(rfEz?h3-e?KnK$k%|eewqyR~LC3Sj6hq|d##$Hm zfi?iGy2Q=4GV7i5YE!AV^e`wmnyh+M)YR}%>vM1B3a>Ar>%S2tV<8K&J(oPzR)*UPMinS- z*QMoZ1RgH!4xAc}SCRdFR##*r6b!rD1$yDe4JslCx~4{K4WH4hF<$$jm8%>P{ z%)dc!jvp|SGwp|BBJZkfihsVQt!=4aHLEU}{}F=I&XFCjfzEvb0|5X50)hd6I$-N7 zCv<9_z|UoL^e+VIpb{%d=Uj1=buu!VWi_;FF==XKXNU_KrUR#f`Fy870jjDGbK|}8 zZxLtLE~0G_XVjO78ii`$Qc14N{D4$ZVxWxP7>m;wLhMJWQ$|O$ARVN=`_k?dU|+-X z@`yqJVd9C|sCmtj$f*Q_Mvr$I$SGeT$DErB@c-j9N}oZlR{#?sk_> zaL%ghbY2ZMh8M0EV%a@W-*H<?Eu(1fnHj2kSucdkf$<;6a zWLW%ZUkGC=wlOqTL|sC40)fE*=#<~_2K0z@OX$J~v$G|TLV-4!z`OHedx6uc9CF(S z#Nohe=>~oBB4>$r-wcTA{A^XH4@&qO_JKDIu3XND7K?v%_9EBfaqrM~>28G9=UrFg z1Dhb})^@?LZn^GSo_=B{^vG`$xCpf(sS0#FBwkSb(?T5HuzXD0=Rph57!F>~>YYUw zoyUjrr^nSY!Ws{}+h_=!x99F_k4Pba_A}ehU2Nc7ahdOFv~GCn|H6^RFs8GfsDm=M z1xcMd0)fE*-8<_y#C?ivIuoK~RSplf_k_~=uOm(Pany^%g)0^BQ=IJUeB6BhOZLQdl|a^8x8r z;&w3aJf2pB`E!~qiNThADVDNQ(Tojp$GmkY56lTq${;{xrR`CGMHoCp_d46pC{h(c ziV1xHxjb2BtS_X1c1%i_M~!F}iVQB^2kU0)fCDFs%In zbsl|E87+>foY;hDO_PJu!_n;bu{(o$TjE%l(%}MN<|)+^XfrJDG!|9DTw$f;(@SrC z$ody~iL7~d%eBbgAy~SZC6fN_yB*y#eJ5HxGndOEzL}TESF}6n6zHVv)W#7kPxYRs{)+)E;#mT+GQKL5l@>DdVlX34VoYD|2R4cxFpkP}) z)$tGI2f@tI=56_`Jr!5s133qB`jS*KCc^5x_r=wFwe6SRELjZ%tD>e794z`Y$&|?( z);mkc3%KwmtbeC<{6_h-JYwA$2UnQ5(tc;O6v;|W*81F z&l50r?~|0nhL<8u=x@f}JZ{veREz4VgFXocEj(mn*|~H`j5nt0Z0gel0*`my)U3U_$mi- z%D8V+Kp4IVP6He=$-=;!5EOg`>iFh_>b!2@rW#I!5$qM8Kk5C^dJpU?P(shq#S0%H JhA%4x^(nLZWS9T| literal 0 HcmV?d00001 diff --git a/src/SponsorLink/Tests/keys/kzu.key.jwk b/src/SponsorLink/Tests/keys/kzu.key.jwk new file mode 100644 index 0000000..3589e3d --- /dev/null +++ b/src/SponsorLink/Tests/keys/kzu.key.jwk @@ -0,0 +1,11 @@ +{ + "d": "OmDrKyd0ap7Az2V09C8E6aASK0nnXHGUdTIymmU1tGoxaWpkZ4gLGaYDp4L5fKc-AaqqD3PjvfJvEWfXLqJtEWfUl4gahWrgUkmuzPyAVFFioIzeGIvTGELsR6lTRke0IB2kvfvS7hRgX8Py8ohCAGHiidmoec2SyKkEg0aPdxrIKV8hx5ybC_D_4zRKn0GuVwATIeVZzPpTcyJX_sn4NHDOqut0Xg02iHhMKpF850BSoC97xGMlcSjLocFSTwI63msz6jWQ-6LRVXsfRr2mqakAvsPpqEQ3Ytk9Ud9xW0ctuAWyo6UXev5w2XEL8cSXm33-57fi3ekC_jGCqW0KfAU4Cr2UbuTC0cv8Vv0F4Xm5FizolmuSBFOvf55-eqsjmpwf9hftYAiIlFF-49-P0DpeJejSeoL06BE3e3_IVu3g3HNnSWVUOLJ5Uk5FQ-ieHhf-r2Tq5qZ8_-losHekQbCxCMY2isc-r6V6BMnVL_9kWPxpXwhjKrYxNFZEXUJ1", + "dp": "HjCs_QF1Hn1SGS2OqZzYhGhNk4PTw9Hs97E7g3pb4liY0uECYOYp1RNoMyzvNBZVwlxhpeTTS299yPoXeYmseXfLtcjfIVi6mSWS_u27Hd0zfSdaPDOXyyK-mZfIV7Q76RTost0QY3LA0ciJbj3gJqpl38dhuNQ8h9Yqt-TFyb3CUaM3A_JUNKOTce8qnkLrasytPEuSroOBT8bgCWJIjw_mXWMGcoqRFWHw9Nyp9mIyvtPjUQ9ig3bGSP_-3LZf", + "dq": "IP6EsAZ_6psFdlQrvnugYFs91fEP5QfBzNHmbfmsPRVX4QM5B3L6klQyJsLqvPfF1Xu17ZffLFkNBKuiphIcLPo0yZTJG9Y7S8gLuPAmrH-ndfxG-bQ8Yt0ZB1pA77ILIS8bUTKrMqAWS-VcaxcSCIyhSusLEWYYDi3PEzB375OUw4aXIk3ob8bePG7UqFSL6qmDPgkGLTxkY9m5dEiOshHygtVY-H_jjOIawliEPgmgAr2M-zlXiphovDyAT0PV", + "e": "AQAB", + "kty": "RSA", + "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59", + "p": "6JTf8Qb0iHRL6MIIs7MlkEKBNpnAu_Nie4HTqhxy2wfE4cBr6QZ98iJniXffDIjq_GxVpw9K-Bv2gTcNrlzOiBaLf3X2Itfice_Qd-luhNbnXVfiA5sg6dZ2wbBuue5ann5iJ_TIbxO4CLUiqQp0PCReUPzTQhzesHxM2-dBC9AYDl7P6p1FF53Hh_Knx9UywhoPvNtoCJy35-5rj0ghgPYz289dbOBccZnvabRueOr_wpHGMKaznqiDMrcFSZ07", + "q": "3TvrN8R9imw6E6JkVQ4PtveE0vkvkSWHUpn9KwKFIJJiwL_HSS4z_8IYR1_0Q1OgK5-z-QcXhq9P7jTjz02I2uwWhP3RZQf99RZACfMaeIs8O2V-I89WdlJYOerzAelW4nYw7zyeVoT5c5osicGWfSmWslLRjA1yx7x1KA_MCU_KIEBlpe1RgEUYPET3OtvPKFIVQYoJfQC5PFlmrC-kgHZMSpdHjWgWi5gPn0fIBCKFsXcPrt2n_lKKGc4lFOen", + "qi": "m-tgdFqO1Ax3C00oe7kdkYLHMD56wkGARdqPCqS5IGhFVKCOA8U6O_s5bSL4r0TzPE0KrJ4A5QJEwjbH4bXssPaaAlv1ZdWjn8YMQCYFolg_pgUWYYI5vNxG1gIsLGXPTfE8a6SObkJ2Q9VC5ZZp14r4lPvJhwFICIGSRBKcvS-gO_gqB3LKuG9TQBi-CE4DHDLJwsCbEBR8Ber45oTqvG7hphpOhBHsFZ8_6f3Reg_sK1BCz9HFCx8hhi8rBfUp" +} \ No newline at end of file diff --git a/src/SponsorLink/Tests/keys/kzu.key.txt b/src/SponsorLink/Tests/keys/kzu.key.txt new file mode 100644 index 0000000..5fe8758 --- /dev/null +++ b/src/SponsorLink/Tests/keys/kzu.key.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/SponsorLink/Tests/keys/kzu.pub b/src/SponsorLink/Tests/keys/kzu.pub new file mode 100644 index 0000000000000000000000000000000000000000..55947976d8a88558c8bcc97df2160632317117a9 GIT binary patch literal 398 zcmV;90df8?f&q#Gf&qa5$o}4YMP;oMGY4(j&a1{rB z0LhJ@3W?+~Z;m_Hw)HaroPJRToFS85N$qyNIm^+a)IWZp968K|S$aqeKb}{{Vy}>w zb(7}USu6fPQMM$U5g=?U+tO?6XzmKa!Rc(0Z_klJ5 zt-8d`wleFT^K5t9I3iwD7r$+A@GUx z_Z4H)%<1In5cDu8IGU__RMgb)QR{PW<_fPbq3gd9C1W8AvOSkP)>ekw3q}1^lQZpyVj}OVY>I!rrmbzMUp1>Png0=j)6S6{uYt~e0s{d60mi7mZ~y=R literal 0 HcmV?d00001 diff --git a/src/SponsorLink/Tests/keys/kzu.pub.jwk b/src/SponsorLink/Tests/keys/kzu.pub.jwk new file mode 100644 index 0000000..b4bfb31 --- /dev/null +++ b/src/SponsorLink/Tests/keys/kzu.pub.jwk @@ -0,0 +1,5 @@ +{ + "e": "AQAB", + "kty": "RSA", + "n": "yP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9-oBw5zIVZekgMP55XxmKvkJd1k-bYWSv-QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y_7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe_bXDkR74yG3mmq_Ne0qhNk6wXuX-NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb-YKry-h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr-OPrxEdZzL9BcI4fMJMz7YdiIu-qbIp_vqatbalfNasumf8RgtPOkR2vgc59" +} \ No newline at end of file diff --git a/src/SponsorLink/Tests/keys/kzu.pub.txt b/src/SponsorLink/Tests/keys/kzu.pub.txt new file mode 100644 index 0000000..729ecd5 --- /dev/null +++ b/src/SponsorLink/Tests/keys/kzu.pub.txt @@ -0,0 +1 @@ +MIIBigKCAYEAyP71VgOgHDtGbxdyN31mIFFITmGYEk2cwepKbyqKTbTYXF1OXaMoP5n3mfwqwzUQmEAsrclAigPcK4GIy5WWlc5YujIxKauJjsKe0FBxMnFp9o1UcBUHfgDJjaAKieQxb44717b1MwCcflEGnCGTXkntdr45y9Gi1D9+oBw5zIVZekgMP55XxmKvkJd1k+bYWSv+QFG2JJwRIGwr29Jr62juCsLB7Tg83ZGKCa22Y/7lQcezxRRD5OrGWhf3gTYArbrEzbYy653zbHfbOCJeVBe/bXDkR74yG3mmq/Ne0qhNk6wXuX+NrKEvdPxRSRBF7C465fcVY9PM6eTqEPQwKDiarHpU1NTwUetzb+YKry+h678RJWMhC7I9lzzWVobbC0YVKG7XpeVqBB4u7Q6cGo5Xkf19VldkIxQMu9sFeuHGDSoiCLqmRmwNn9GsMV77oZWr+OPrxEdZzL9BcI4fMJMz7YdiIu+qbIp/vqatbalfNasumf8RgtPOkR2vgc59AgMBAAE= \ No newline at end of file diff --git a/src/SponsorLink/Tests/keys/sponsorlink.jwt b/src/SponsorLink/Tests/keys/sponsorlink.jwt new file mode 100644 index 0000000..b53fe62 --- /dev/null +++ b/src/SponsorLink/Tests/keys/sponsorlink.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTk4NjgyMzAsImlzcyI6Imh0dHBzOi8vc3BvbnNvcmxpbmsuZGV2bG9vcGVkLmNvbS8iLCJhdWQiOlsiaHR0cHM6Ly9naXRodWIuY29tL3Nwb25zb3JzL2t6dSIsImh0dHBzOi8vZ2l0aHViLmNvbS9zcG9uc29ycy9kZXZsb29wZWQiXSwiY2xpZW50X2lkIjoiYTgyMzUwZmIyYmFlNDA3YjMwMjEiLCJzdWJfandrIjp7ImUiOiJBUUFCIiwia3R5IjoiUlNBIiwibiI6InlQNzFWZ09nSER0R2J4ZHlOMzFtSUZGSVRtR1lFazJjd2VwS2J5cUtUYlRZWEYxT1hhTW9QNW4zbWZ3cXd6VVFtRUFzcmNsQWlnUGNLNEdJeTVXV2xjNVl1akl4S2F1SmpzS2UwRkJ4TW5GcDlvMVVjQlVIZmdESmphQUtpZVF4YjQ0NzE3YjFNd0NjZmxFR25DR1RYa250ZHI0NXk5R2kxRDktb0J3NXpJVlpla2dNUDU1WHhtS3ZrSmQxay1iWVdTdi1RRkcySkp3UklHd3IyOUpyNjJqdUNzTEI3VGc4M1pHS0NhMjJZXzdsUWNlenhSUkQ1T3JHV2hmM2dUWUFyYnJFemJZeTY1M3piSGZiT0NKZVZCZV9iWERrUjc0eUczbW1xX05lMHFoTms2d1h1WC1OcktFdmRQeFJTUkJGN0M0NjVmY1ZZOVBNNmVUcUVQUXdLRGlhckhwVTFOVHdVZXR6Yi1ZS3J5LWg2NzhSSldNaEM3STlsenpXVm9iYkMwWVZLRzdYcGVWcUJCNHU3UTZjR281WGtmMTlWbGRrSXhRTXU5c0ZldUhHRFNvaUNMcW1SbXdObjlHc01WNzdvWldyLU9QcnhFZFp6TDlCY0k0Zk1KTXo3WWRpSXUtcWJJcF92cWF0YmFsZk5hc3VtZjhSZ3RQT2tSMnZnYzU5In19.er4apYbEjHVKlQ_aMXoRhHYeR8N-3uIrCk3HX8UuZO7mb0CaS94-422EI3z5O9vRvckcGkNVoiSIX0ykZqUMHTZxBae-QZc1u_rhdBOChoaxWqpUiPXLZ5-yi7mcRwqg2DOUb2eHTNfRjwJ-0tjL1R1TqZw9d8Bgku1zw2ZTuJl_WsBRHKHTD_s5KyCP5yhSOUumrsf3nXYrc20fJ7ql0FsL0MP66utJk7TFYHGhQV3cfcXYqFEpv-k6tqB9k3Syc0UnepmQT0Y3dtcBzQzCOzfKQ8bdaAXVHjfp4VvXBluHmh9lP6TeZmpvlmQDFvyk0kp1diTbo9pqmX_llNDWNxBdvaSZGa7RZMG_dE2WJGtQNu0C_sbEZDPZsKncxdtm-j-6Y7GRqx7uxe4Py8tAZ7SxjiPgD64jf9KF2OT6f6drVtzohVzYCs6-vhcXzC2sQvd_gQ-SoFNTa1MEcMgGbL-fFWUC7-7bQV1DlSg2YFwrxEIwbM-gHpLZHyyJLvYD \ No newline at end of file