diff --git a/.netconfig b/.netconfig index a2b13ce6..9a116da9 100644 --- a/.netconfig +++ b/.netconfig @@ -63,48 +63,6 @@ sha = f08c3f28e46e28eb31e70846d65e57aa9553ce56 etag = 567444486383d032c1c5fbc538f07e860f92b1d08c66ac6ffb1db64ca539251c weak -[file "src/App/System/QuaranTime.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/QuaranTime.cs - sha = 83ae670d5c69de16dd0d3a15448bdefa2ef6cdfe - etag = 2de43ea10bb6dca91326c303fd4daf1c6493d3bc63511f3fd906c7f75117f49e - weak -[file "src/Tests/System/QuaranTime.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/QuaranTime.cs - sha = 83ae670d5c69de16dd0d3a15448bdefa2ef6cdfe - etag = 2de43ea10bb6dca91326c303fd4daf1c6493d3bc63511f3fd906c7f75117f49e - weak -[file "src/Tests/System/Base62.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/Base62.cs - sha = 43712304cdfff2c34ba01197cc2ccc0e16e7af08 - etag = 045b9e458f11f846ae9a3979b8122c68173405fb5451083d4993f94004e60e76 - weak -[file "src/App/System/Base62.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/Base62.cs - sha = 43712304cdfff2c34ba01197cc2ccc0e16e7af08 - etag = 045b9e458f11f846ae9a3979b8122c68173405fb5451083d4993f94004e60e76 - weak -[file "src/Package/Properties/."] - url = https://github.com/devlooped/SponsorLink/tree/main/loc -[file "src/Package/Properties/Resources.es.resx"] - url = https://github.com/devlooped/SponsorLink/blob/main/loc/Resources.es.resx - sha = da76fbaadfb524ea6c58715f60b4d9c52e152977 - etag = 2e8130a9a1799eaf05691ed68719146097797abbbb6214e12ba65917c36c376c - weak -[file "src/Package/Properties/Resources.resx"] - url = https://github.com/devlooped/SponsorLink/blob/main/loc/Resources.resx - sha = da76fbaadfb524ea6c58715f60b4d9c52e152977 - etag = 9c3302f054c55692be6fbae25f0f1af9b96803a40144dcef64885a9da7cb562c - weak -[file "src/SponsorLink/System/Base62.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/Base62.cs - sha = 43712304cdfff2c34ba01197cc2ccc0e16e7af08 - etag = 045b9e458f11f846ae9a3979b8122c68173405fb5451083d4993f94004e60e76 - weak -[file "src/Package/System/Base62.cs"] - url = https://github.com/devlooped/catbag/blob/main/System/Base62.cs - sha = 43712304cdfff2c34ba01197cc2ccc0e16e7af08 - etag = 045b9e458f11f846ae9a3979b8122c68173405fb5451083d4993f94004e60e76 - weak [file ".github/workflows/changelog.config"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.config sha = 055a8b7c94b74ae139cce919d60b83976d2a9942 diff --git a/SponsorLink.sln b/SponsorLink.sln index c5006380..c9a122b8 100644 --- a/SponsorLink.sln +++ b/SponsorLink.sln @@ -20,8 +20,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution src\readme.md = src\readme.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "src\SponsorLink\SponsorLink.csproj", "{C04E6EF3-CD66-4E4C-8A55-77B31DFBDF88}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Package", "src\Package\Package.csproj", "{E2813343-FDF5-4870-9DA5-35A1A3EB2CA6}" EndProject Global @@ -38,10 +36,6 @@ Global {638E2879-1321-495B-828B-785B6B5E9921}.Debug|Any CPU.Build.0 = Debug|Any CPU {638E2879-1321-495B-828B-785B6B5E9921}.Release|Any CPU.ActiveCfg = Release|Any CPU {638E2879-1321-495B-828B-785B6B5E9921}.Release|Any CPU.Build.0 = Release|Any CPU - {C04E6EF3-CD66-4E4C-8A55-77B31DFBDF88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C04E6EF3-CD66-4E4C-8A55-77B31DFBDF88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C04E6EF3-CD66-4E4C-8A55-77B31DFBDF88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C04E6EF3-CD66-4E4C-8A55-77B31DFBDF88}.Release|Any CPU.Build.0 = Release|Any CPU {E2813343-FDF5-4870-9DA5-35A1A3EB2CA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E2813343-FDF5-4870-9DA5-35A1A3EB2CA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2813343-FDF5-4870-9DA5-35A1A3EB2CA6}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/App/App.csproj b/src/App/App.csproj index ce5f654d..0256618f 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -9,6 +9,7 @@ + @@ -18,18 +19,25 @@ - + + + - - - - + + + + + + + + + diff --git a/src/App/DependencyStartup.cs b/src/App/DependencyStartup.cs deleted file mode 100644 index 68188016..00000000 --- a/src/App/DependencyStartup.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Devlooped; -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -[assembly: FunctionsStartup(typeof(DependencyStartup))] - -public class DependencyStartup : FunctionsStartup -{ - public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder) - { - builder.ConfigurationBuilder.AddUserSecrets(ThisAssembly.Project.UserSecretsId); - } - - public override void Configure(IFunctionsHostBuilder builder) - { - builder.Services.AddServices(); - builder.Services.AddHttpClient(); - - var storage = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" - ? CloudStorageAccount.DevelopmentStorageAccount - : CloudStorageAccount.Parse( - builder.GetContext().Configuration["AppStorage"] ?? - builder.GetContext().Configuration["AzureWebJobsStorage"] ?? - throw new InvalidOperationException("Missing AppStorage configuration.")); - - builder.Services.AddSingleton(storage); - - builder.Services.AddSingleton(sp => new TableConnection( - sp.GetRequiredService(), "SponsorLink")); - - var config = builder.Services.FirstOrDefault(d => d.ServiceType == typeof(TelemetryConfiguration)); - - Console.WriteLine(config?.ImplementationInstance); - } -} \ No newline at end of file diff --git a/src/App/Functions.cs b/src/App/Functions.cs deleted file mode 100644 index 3b15efa4..00000000 --- a/src/App/Functions.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System.Globalization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.EventGrid.Models; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.EventGrid; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.WindowsAzure.Storage; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Devlooped.SponsorLink; - -public class Functions -{ - readonly SponsorsManager manager; - readonly IConfiguration configuration; - readonly IHttpClientFactory clientFactory; - readonly IEventStream events; - readonly ITablePartition webhooks; - - public Functions(SponsorsManager manager, IConfiguration configuration, IHttpClientFactory clientFactory, IEventStream events, CloudStorageAccount account) - => (this.manager, this.configuration, this.clientFactory, this.events, webhooks) - = (manager, configuration, clientFactory, events, TablePartition.Create(account, "Webhook", "SponsorLink", x => x.Id)); - - record EmailInfo(string email, bool verified); - - public record PingCompleted(DateTimeOffset When); - - [FunctionName("ping")] - public async Task Ping([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "ping")] HttpRequest req) - { - await events.PushAsync(new PingCompleted(DateTimeOffset.Now)); - return new OkObjectResult("pong"); - } - - [FunctionName("expirations")] - public async Task CheckExpirationsAsync([TimerTrigger("0 0 0 * * *")] TimerInfo timer) - => await manager.UnsponsorExpiredAsync(DateOnly.FromDateTime(DateTime.Today)); - - [FunctionName("authorize")] - public async Task AuthorizeAppAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "authorize/{kind}")] HttpRequest req, string kind) - { - var appKind = Enum.Parse(kind, true); - var code = req.Query["code"].ToString(); - - // TODO: the installation id can be used to request an installation token to perform actions - // authorized to the app on behalf of the authorizing user. - // This will be necessary for the sponsorable account if we ever figure out how to run GraphQL - // queries, but for now, it's impossible. - //var installation = req.Query["installation_id"].ToString(); - - await manager.AuthorizeAsync(appKind, code); - - return new RedirectResult($"https://devlooped.com/?{kind}"); - } - - [FunctionName("app")] - public async Task AppHookAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "app/{kind}")] HttpRequestMessage req, string kind) - { - var body = await req.Content!.ReadAsStringAsync(); - - if (!SecurityManager.VerifySignature(body, configuration["GitHub:WebhookSecret"], req.Headers.GetValues("x-hub-signature-256").FirstOrDefault())) - return new BadRequestResult(); - - dynamic? payload = JsonConvert.DeserializeObject(body); - if (payload == null) - return new BadRequestObjectResult("Could not deserialize payload as JSON"); - - await webhooks.PutAsync(new( - req.Headers.GetValues("X-GitHub-Delivery").FirstOrDefault() ?? Guid.NewGuid().ToString(), - ((JToken)payload).ToString(Formatting.Indented))); - - string action = payload.action; - var appKind = Enum.Parse(kind, true); - var id = new AccountId((string)payload.installation.account.node_id, (string)payload.installation.account.login); - var note = $"App {appKind} {action} on {payload.installation.account.login} by {payload.sender.login}"; - - await (action switch - { - "created" => manager.AppInstallAsync(appKind, id, note), - "deleted" => manager.AppUninstallAsync(appKind, id, note), - "suspend" => manager.AppSuspendAsync(appKind, id, note), - "unsuspend" => manager.AppUnsuspendAsync(appKind, id, note), - _ => Task.CompletedTask, - }); - - await PushoverAsync(new Dictionary - { - ["title"] = $"SponsorLink {appKind} App {CultureInfo.CurrentCulture.TextInfo.ToTitleCase(action switch - { - "created" => "installed", - "deleted" => "uninstalled", - "suspend" => "suspended", - "unsuspend" => "unsuspended", - _ => action - })}", - ["url"] = $"https://github.com/{id.Login}", - ["url_title"] = "View profile", - ["message"] = note, - }); - - return new OkObjectResult(note); - } - - [FunctionName("sponsor")] - public async Task SponsorHookAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "sponsor/{account}")] HttpRequestMessage req, string account) - { - var body = await req.Content!.ReadAsStringAsync(); - dynamic? payload = JsonConvert.DeserializeObject(body); - if (payload == null) - return new BadRequestObjectResult("Could not deserialize payload as JSON"); - - string action = payload.action; - if (action == null) - return new OkObjectResult("Nothing to do :)"); - - var sponsorable = new AccountId((string)payload.sponsorship.sponsorable.node_id, (string)payload.sponsorship.sponsorable.login); - var sponsor = new AccountId((string)payload.sponsorship.sponsor.node_id, (string)payload.sponsorship.sponsor.login); - int amount = payload.sponsorship.tier.monthly_price_in_dollars; - bool oneTime = payload.sponsorship.tier.is_one_time; - DateTime date = payload.sponsorship.created_at; - - var installation = await manager.FindAppAsync(AppKind.Sponsorable, sponsorable); - if (installation == null) - { - await PushoverAsync(new Dictionary - { - ["title"] = $"{account}: Sponsors webhook", - ["url"] = $"https://github.com/{account}", - ["url_title"] = "View profile", - ["message"] = $"Sponsors webhook invoked by {account} without an admin install of the SponsorLink admin app.", - }); - - return new BadRequestObjectResult($"No SponsorLink Admin installation found for {account}. See https://github.com/apps/sponsorlink-admin"); - } - - // We require the installation to be present and enabled to receive sponsorships - if (installation == null || - !SecurityManager.VerifySignature(body, installation.Secret, req.Headers.GetValues("x-hub-signature-256").FirstOrDefault())) - { - await PushoverAsync(new Dictionary - { - ["title"] = $"{account}: Sponsors webhook", - ["url"] = $"https://github.com/{account}", - ["url_title"] = "View profile", - ["message"] = $"Sponsors webhook invoked by {account} with invalid payload signature.", - }); - - return new BadRequestObjectResult($"Could not verify signature payload signature from {account}. See https://github.com/apps/sponsorlink-admin"); - } - - await webhooks.PutAsync(new( - req.Headers.GetValues("X-GitHub-Delivery").FirstOrDefault() ?? Guid.NewGuid().ToString(), - ((JToken)payload).ToString(Formatting.Indented))); - - if (action == "created") - { - var note = $"{sponsor.Login} started sponsoring {sponsorable.Login} with ${amount}"; - if (oneTime) - note += " (one-time)"; - - await manager.SponsorAsync(sponsorable, sponsor, amount, oneTime ? DateOnly.FromDateTime(date).AddDays(30) : null, note); - - await PushoverAsync(new Dictionary - { - ["title"] = $"{account}: New Sponsor", - ["url"] = $"https://github.com/{sponsor.Login}", - ["url_title"] = "View profile", - ["message"] = note, - }); - } - else if (action == "cancelled") - { - await manager.UnsponsorAsync(sponsorable, sponsor, - $"{sponsor.Login} cancelled sponsorship of {sponsorable.Login}."); - - await PushoverAsync(new Dictionary - { - ["title"] = $"{account}: Lost Sponsor", - ["url"] = $"https://github.com/{sponsor.Login}", - ["url_title"] = "View profile", - ["message"] = $"{sponsor.Login} cancelled sponsorship of {sponsorable.Login}.", - }); - } - else if (action == "tier_changed") - { - int from = payload.changes.tier.from.monthly_price_in_dollars; - var note = $"{sponsor.Login} updated sponsorship of {sponsorable.Login} from ${from} to ${amount}"; - - await manager.SponsorUpdateAsync(sponsorable, sponsor, amount, note); - - await PushoverAsync(new Dictionary - { - ["title"] = $"{account}: Updated Sponsor", - ["url"] = $"https://github.com/{sponsor.Login}", - ["url_title"] = "View profile", - ["message"] = note, - }); - } - - // TODO: if sponsorable has not installed the admin app or is - // not sponsoring devlooped with at least $x, return OK with content explaining - // this and that checks won't work until they do. - - return new OkResult(); - } - - [FunctionName("refresh")] - public async Task RefreshUserAsync([EventGridTrigger] EventGridEvent e) - { - var message = JsonConvert.DeserializeObject(e.Data.ToString() ?? "{ }"); - if (message == null) - return; - - if (message.Attempt >= 3) - return; - - var done = await manager.SyncUserAsync(new AccountId(message.Account, message.Login), message.Sponsorable, message.Unregister); - if (!done) - await events.PushAsync(message with { Attempt = message.Attempt + 1 }); - } - - [FunctionName("refreshall")] - public async Task RefreshAllAsync( - [HttpTrigger(AuthorizationLevel.Function, "get", "refresh")] HttpRequestMessage req) - { - int count = 0; - - await foreach (var installation in manager.EnumerateInstallationsAsync(AppKind.Sponsor)) - { - if (installation.State != AppState.Installed) - continue; - - await events.PushAsync(new UserRefreshPending(installation.Account, installation.Login, 0, "Forced refresh")); - count++; - } - - await foreach (var installation in manager.EnumerateInstallationsAsync(AppKind.Sponsorable)) - { - if (installation.State != AppState.Installed) - continue; - - await events.PushAsync(new UserRefreshPending(installation.Account, installation.Login, 0, "Forced refresh")); - count++; - } - - return new OkObjectResult($"Scheduled {count} users for refresh"); - } - - async Task PushoverAsync(Dictionary payload) - { - if (configuration["Pushover:Key"] is string pushKey && - configuration["Pushover:User"] is string pushUser) - { - payload["token"] = pushKey; - payload["user"] = pushUser; - - var client = clientFactory.CreateClient(); - await client.PostAsync("https://api.pushover.net/1/messages.json", new FormUrlEncodedContent(payload)); - } - } -} diff --git a/src/App/HttpClientExtensions.cs b/src/App/HttpClientExtensions.cs deleted file mode 100644 index fe8c5023..00000000 --- a/src/App/HttpClientExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net.Http.Headers; - -namespace App; - -static class HttpClientExtensions -{ - public static Task PostAsync(this HttpClient http, string? requestUri, HttpContent? content, string? bearerToken) - { - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = content; - if (!string.IsNullOrEmpty(bearerToken)) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); - - return http.SendAsync(request); - } -} diff --git a/src/App/Stats.cs b/src/App/Stats.cs deleted file mode 100644 index 6a73e804..00000000 --- a/src/App/Stats.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Azure.Identity; -using Azure.Monitor.Query; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; - -namespace Devlooped.SponsorLink; - -public class Stats -{ - readonly CloudStorageAccount storageAccount; - readonly ILogger logger; - - - public Stats(CloudStorageAccount storageAccount, ILogger logger) - => (this.storageAccount, this.logger) - = (storageAccount, logger); - - [FunctionName("users")] - public async Task RunAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stats/{query}")] HttpRequestMessage req, string query) - { - var blobs = storageAccount.CreateBlobServiceClient().GetBlobContainerClient("sponsorlink"); - if (await blobs.ExistsAsync() == false) - return new NotFoundResult(); - - var blob = blobs.GetBlobClient($"queries/{query}.kql"); - if (await blob.ExistsAsync() == false) - { - logger.LogWarning("Got stats query {query} which doesn't exist in storage.", query); - return new OkObjectResult(new - { - schemaVersion = 1, - isError = true, - label = "error", - message = "404", - }); - } - - var kql = await blob.DownloadContentAsync(); - var creds = new DefaultAzureCredential(); - var client = new LogsQueryClient(creds); - - var result = await client.QueryWorkspaceAsync( - Constants.LogAnalyticsWorkspaceId, kql.Value.Content.ToString(), - QueryTimeRange.All); - - if (result.Value.Count == 0) - { - logger.LogWarning("Query {query}.kql resulted in an empty response.", query); - return new OkObjectResult(new - { - schemaVersion = 1, - isError = true, - label = "empty", - message = "204", - }); - } - - return new OkObjectResult(new - { - schemaVersion = 1, - label = "", - message = result.Value[0].ToString(), - }); - } -} diff --git a/src/App/System/Base62.cs b/src/App/System/Base62.cs deleted file mode 100644 index 7eb3913e..00000000 --- a/src/App/System/Base62.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -#region License -// MIT License -// -// Copyright (c) Daniel Cazzulino -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -#endregion - -#nullable enable -using System.Linq; -using System.Numerics; -using System.Text; - -namespace System -{ - /// - /// Inspired in a bunch of searches, samples and snippets on various languages - /// and blogs and what-not on doing URL shortering :), heavily tweaked to make - /// it fully idiomatic in C#. - /// - static partial class Base62 - { - /// - /// Encodes a numeric value into a base62 string. - /// - public static string Encode(BigInteger value) - { - // TODO: I'm almost sure there's a more succint way - // of doing this with LINQ and Aggregate, but just - // can't figure it out... - var sb = new StringBuilder(); - - while (value != 0) - { - sb = sb.Append(ToBase62(value % 62)); - value /= 62; - } - - return new string(sb.ToString().Reverse().ToArray()); - } - - /// - /// Decodes a base62 string into its original numeric value. - /// - public static BigInteger Decode(string value) - => value.Aggregate(new BigInteger(0), (current, c) => current * 62 + FromBase62(c)); - - static char ToBase62(BigInteger d) => d switch - { - BigInteger v when v < 10 => (char)('0' + d), - BigInteger v when v < 36 => (char)('A' + d - 10), - BigInteger v when v < 62 => (char)('a' + d - 36), - _ => throw new ArgumentException($"Cannot encode digit {d} to base 62.", nameof(d)), - }; - - static BigInteger FromBase62(char c) => c switch - { - char v when c >= 'a' && v <= 'z' => 36 + c - 'a', - char v when c >= 'A' && v <= 'Z' => 10 + c - 'A', - char v when c >= '0' && v <= '9' => c - '0', - _ => throw new ArgumentException($"Cannot decode char '{c}' from base 62.", nameof(c)), - }; - } -} \ No newline at end of file diff --git a/src/App/System/QuaranTime.cs b/src/App/System/QuaranTime.cs deleted file mode 100644 index 6eb9120b..00000000 --- a/src/App/System/QuaranTime.cs +++ /dev/null @@ -1,137 +0,0 @@ -namespace System -{ - /// - /// Provides the constant that signaled the - /// start of the Quarantime time and methods to convert to and from - /// Quarantine-relative values. - /// - static partial class QuaranTime - { - // From DateTime - internal const long DateTimeMinTicks = 0L; - internal const long DateTimeMaxTicks = 3155378975999999999L; - - internal const long QuarantineMinSeconds = DateTimeMinTicks / TimeSpan.TicksPerSecond - QuarantineEpochSeconds; - internal const long QuarantineMaxSeconds = DateTimeMaxTicks / TimeSpan.TicksPerSecond - QuarantineEpochSeconds; - - internal const long QuarantineEpochTicks = 637202700000000000; - internal const long QuarantineEpochSeconds = QuarantineEpochTicks / TimeSpan.TicksPerSecond; - internal const long QuarantineEpochMiliseconds = QuarantineEpochTicks / TimeSpan.TicksPerMillisecond; - - /// - /// The value of this constant is equivalent to 00:00:00 GMT-0300, March 3, 2020, in the Gregorian calendar. - /// defines the point in time when the quarantine started in Argentina. - /// - public static DateTimeOffset Epoch { get; } = new DateTimeOffset(637202592000000000, TimeSpan.FromHours(-3)); - - /// - /// Converts a Quarantine time expressed as the number of seconds that have elapsed since - /// since 2020-03-20T00:00:00-03:00 to a value. - /// - /// - /// A Quarantine time, expressed as the number of seconds that have elapsed since - /// 2020-03-20T00:00:00-03:00 (March 3, 2020, at 12:00 AM GMT-0300). For Quarantine times - /// before this date, its value is negative. - /// - /// - /// A date and time value that represents the same moment in time as the Quarantine time. - /// - /// - /// The property value of the returned - /// instance is , which represents Coordinated Universal Time. - /// You can convert it to the time in a specific time zone by calling the - /// . - /// - /// - /// seconds is less than -63,720,270,000 or greater than 251,817,627,599. - /// - public static DateTimeOffset FromQuaranTimeSeconds(long seconds) - { - if (seconds < QuarantineMinSeconds || seconds > QuarantineMaxSeconds) - { - throw new ArgumentOutOfRangeException(nameof(seconds)); - } - - var ticks = seconds * TimeSpan.TicksPerSecond + QuarantineEpochTicks; - return new DateTimeOffset(ticks, TimeSpan.Zero); - } - - /// - /// Converts a Quarantine time expressed as the number of milliseconds that have elapsed - /// since 2020-03-20T00:00:00-03:00 to a value. - /// - /// - /// A Quarantine time, expressed as the number of milliseconds that have elapsed since - /// 2020-03-20T00:00:00-03:00 (March 3, 2020, at 12:00 AM GMT-0300). For Quarantine times - /// before this date, its value is negative. - /// - /// - /// A date and time value that represents the same moment in time as the Quarantine time. - /// - /// - /// The property value of the returned - /// instance is , which represents Coordinated Universal Time. - /// You can convert it to the time in a specific time zone by calling the - /// . - /// - /// - /// seconds is less than -63,720,270,000,000 or greater than 251,817,627,599,999. - /// - public static DateTimeOffset FromQuaranTimeMilliseconds(long milliseconds) - { - const long MinMilliseconds = DateTimeMinTicks / TimeSpan.TicksPerMillisecond - QuarantineEpochMiliseconds; - const long MaxMilliseconds = DateTimeMaxTicks / TimeSpan.TicksPerMillisecond - QuarantineEpochMiliseconds; - - if (milliseconds < MinMilliseconds || milliseconds > MaxMilliseconds) - { - throw new ArgumentOutOfRangeException(nameof(milliseconds)); - } - - var ticks = milliseconds * TimeSpan.TicksPerMillisecond + QuarantineEpochTicks; - return new DateTimeOffset(ticks, TimeSpan.Zero); - } - - /// - /// Returns the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00. - /// - /// - /// Quarantine time represents the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00 - /// (March 3, 2020, at 12:00 AM GMT-0300). It does not take leap seconds into account. This method - /// returns the number of seconds in Quarantine time. - /// - /// This method first converts the current instance to UTC before returning its Quarantine time. - /// For date and time values before 2020-03-20T00:00:00-03:00, this method returns a negative value. - /// - /// - /// The number of seconds that have elapsed since 2020-03-20T00:00:00-03:00. - public static long ToQuaranTimeSeconds(this DateTimeOffset dateTime) - { - // Truncate just like ToUnixTimeSeconds does, see https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L583 - - var seconds = dateTime.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; - return seconds - QuarantineEpochSeconds; - } - - /// - /// Returns the number of milliseconds that have elapsed since 2020-03-20T00:00:00-03:00. - /// - /// - /// Quarantine time represents the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00 - /// (March 3, 2020, at 12:00 AM GMT-0300). It does not take leap seconds into account. This method - /// returns the number of milliseconds in Quarantine time. - /// - /// This method first converts the current instance to UTC before returning the number of milliseconds - /// in its Quarantine time. For date and time values before 2020-03-20T00:00:00-03:00, this method returns - /// a negative value. - /// - /// - /// The number of miliseconds that have elapsed since 2020-03-20T00:00:00-03:00. - public static long ToQuaranTimeMilliseconds(this DateTimeOffset dateTime) - { - // Truncate just like ToUnixTimeMiliseconds does, see https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L605 - - var seconds = dateTime.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; - return seconds - QuarantineEpochMiliseconds; - } - } -} diff --git a/src/App/host.json b/src/App/host.json index 611e34e5..85775bf3 100644 --- a/src/App/host.json +++ b/src/App/host.json @@ -10,7 +10,8 @@ "samplingSettings": { "isEnabled": true, "excludedTypes": "Request" - } + }, + "enableLiveMetricsFilters": true } } } \ No newline at end of file diff --git a/src/Package/Package.csproj b/src/Package/Package.csproj index d50eb301..fd6bcfec 100644 --- a/src/Package/Package.csproj +++ b/src/Package/Package.csproj @@ -38,6 +38,10 @@ + + + + diff --git a/src/Package/SessionManager.cs b/src/Package/SessionManager.cs index bd9aafc2..2c397369 100644 --- a/src/Package/SessionManager.cs +++ b/src/Package/SessionManager.cs @@ -31,7 +31,7 @@ static SessionManager() // Rider sets the parent process ID, but the child process lingers anyway, so we must ensure // we exit the process or we'll never re-check regardless of how many times Rider itself is restarted. - if (Environment.GetEnvironmentVariable("MSBUILD_TASK_PARENT_PROCESS_PID") is string parentId && + if (Environment.GetEnvironmentVariable("MSBUILD_TASK_PARENT_PROCESS_PID") is string parentId && int.TryParse(parentId, out var processId)) { timer = new Timer(_ => @@ -64,7 +64,7 @@ static SessionManager() Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") != null; public static bool IsCI => - (bool.TryParse(Environment.GetEnvironmentVariable("CI"), out var ci) && ci) || + (bool.TryParse(Environment.GetEnvironmentVariable("CI"), out var ci) && ci) || (bool.TryParse(Environment.GetEnvironmentVariable("TF_BUILD"), out ci) && ci) || (bool.TryParse(Environment.GetEnvironmentVariable("TRAVIS"), out ci) && ci) || (bool.TryParse(Environment.GetEnvironmentVariable("BUDDY"), out ci) && ci) || diff --git a/src/Package/SponsorCheck.cs b/src/Package/SponsorCheck.cs index 6ca262b6..5be38784 100644 --- a/src/Package/SponsorCheck.cs +++ b/src/Package/SponsorCheck.cs @@ -52,8 +52,8 @@ static SponsorCheck() .Append("&editor.sku=") .Append(Environment.GetEnvironmentVariable("VSSKUEDITION")?.ToLowerInvariant()); - if (Environment.GetEnvironmentVariable("VSAPPIDDIR") is string appdir && - Directory.Exists(appdir) && + if (Environment.GetEnvironmentVariable("VSAPPIDDIR") is string appdir && + Directory.Exists(appdir) && File.Exists(Path.Combine(appdir, "devenv.isolation.ini"))) { var value = File.ReadAllLines(Path.Combine(appdir, "devenv.isolation.ini")) @@ -69,8 +69,8 @@ static SponsorCheck() else if (SessionManager.IsRider) { sb.Append("&editor=rider"); - if (Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") is string ideadir && - Regex.Match(ideadir, "\\d\\d\\d\\d\\.\\d\\d?") is Match match && + if (Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") is string ideadir && + Regex.Match(ideadir, "\\d\\d\\d\\d\\.\\d\\d?") is Match match && match.Success) { sb.Append("&editor.version=").Append(match.Value); @@ -112,8 +112,8 @@ static SponsorCheck() var data = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(email)); var hash = Base62.Encode(BigInteger.Abs(new BigInteger(data))); - var query = $"account={sponsorable}&product={product}&package={packageId}&version={version}" + - $"&noreply=" + email!.EndsWith("@users.noreply.github.com").ToString().ToLowerInvariant() + + var query = $"account={sponsorable}&product={product}&package={packageId}&version={version}" + + $"&noreply=" + email!.EndsWith("@users.noreply.github.com").ToString().ToLowerInvariant() + ContextQuery; // Check app install and sponsoring status @@ -152,7 +152,7 @@ static internal void ReportBroken( var data = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(email)); var hash = Base62.Encode(BigInteger.Abs(new BigInteger(data))); var query = $"reason={reason}&account={settings.Sponsorable}&product={settings.Product}&package={settings.PackageId}&version={settings.Version}" + - "&noreply=" + email!.EndsWith("@users.noreply.github.com").ToString().ToLowerInvariant() + + "&noreply=" + email!.EndsWith("@users.noreply.github.com").ToString().ToLowerInvariant() + ContextQuery; CheckUrlAsync(http ?? HttpClientFactory.Default, @@ -195,7 +195,7 @@ static internal void ReportBroken( var response = await http.GetAsync(url, cancellation); return response.IsSuccessStatusCode; } - catch (Exception ex) + catch (Exception ex) { if (!cancellation.IsCancellationRequested) Tracing.Trace($"{nameof(CheckUrlAsync)}({url}): \r\n{ex}"); diff --git a/src/Package/SponsorLink.cs b/src/Package/SponsorLink.cs index 414a81ec..67a6b6bf 100644 --- a/src/Package/SponsorLink.cs +++ b/src/Package/SponsorLink.cs @@ -47,9 +47,9 @@ static SponsorLink() // .OrderBy(x => x.Key) // .Where(x => !"Path".Equals(x.Key)) // .Select(x => $"{x.Key}={x.Value}"))); - + AppDomain.CurrentDomain.ProcessExit += (sender, args) => Trace("ProcessExit"); - + http = HttpClientFactory.Create(NetworkTimeout); // Reads settings from storage, best-effort @@ -110,12 +110,12 @@ protected SponsorLink(SponsorLinkSettings settings) { sponsorable = settings.Sponsorable; product = settings.Product; - + // Add the built-in ones to the dynamic diagnostics. diagnostics = settings.SupportedDiagnostics .Add(DiagnosticsManager.MissingProject) .Add(DiagnosticsManager.MissingDesignTimeBuild); - + this.settings = settings; } @@ -253,7 +253,7 @@ void AnalyzeSponsors(CompilationAnalysisContext context) // is in this particular scenario. This allows non-debugging runs of the analyzer to // behave as it would for end consumers, even when building from within the sponsorable // analyzer solution while not debugging it. - if (Debugger.IsAttached && + if (Debugger.IsAttached && globalOptions.TryGetValue("build_property.DebugSponsorLink", out var dsl) && bool.TryParse(dsl, out forceRun) && forceRun) // Reset value to what it is in CLI builds diff --git a/src/Package/SponsorLinkSettings.cs b/src/Package/SponsorLinkSettings.cs index 33369ae7..8976d8bd 100644 --- a/src/Package/SponsorLinkSettings.cs +++ b/src/Package/SponsorLinkSettings.cs @@ -54,7 +54,7 @@ public static SponsorLinkSettings Create(string sponsorable, string product) public static SponsorLinkSettings Create(string sponsorable, string product, string? packageId, string? diagnosticsIdPrefix, - int pauseMin, + int pauseMin, int pauseMax) => Create(sponsorable, product, packageId: packageId, diagnosticsIdPrefix: diagnosticsIdPrefix, diff --git a/src/Package/Tracing.cs b/src/Package/Tracing.cs index 289c8d56..922b8d74 100644 --- a/src/Package/Tracing.cs +++ b/src/Package/Tracing.cs @@ -28,7 +28,7 @@ public static void Trace([CallerMemberName] string? message = null, [CallerFileP .Append($" {message} ") .AppendLine($" -> {filePath}({lineNumber})") .ToString(); - + var dir = Environment.ExpandEnvironmentVariables(@"%TEMP%\SponsorLink"); Directory.CreateDirectory(dir); diff --git a/src/SponsorLink.pub b/src/SponsorLink.pub new file mode 100644 index 00000000..51932cd4 Binary files /dev/null and b/src/SponsorLink.pub differ diff --git a/src/Tests/ManualSponsors.cs b/src/Tests/ManualSponsors.cs deleted file mode 100644 index e98ef3aa..00000000 --- a/src/Tests/ManualSponsors.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.Configuration; -using Moq; - -namespace Devlooped.SponsorLink.GraphQL; - -public record SponsorsResult(SponsorsData Data); -public record SponsorsData(Sponsorable? Organization, Sponsorable? User); -public record Sponsorable(string Id, string Login, Sponsorships SponsorshipsAsMaintainer); -public record Sponsorships(Sponsorship[] Nodes); -public record Sponsorship(DateTime CreatedAt, bool IsOneTimePayment, Sponsor SponsorEntity, Tier Tier); -public record Sponsor(string Id, string Login); -public record Tier(int MonthlyPriceInDollars); - -public record ManualSponsors(ITestOutputHelper Output) -{ - [LocalFact] - public async Task KzuSponsorsDevlooped() - { - var config = new ConfigurationBuilder().AddUserSecrets(ThisAssembly.Project.UserSecretsId).Build(); - if (!CloudStorageAccount.TryParse(config["ProductionStorageAccount"], out var storage)) - { - Output.WriteLine("Did not find 'ProductionStorageAccount' secret. Cannot run kzu>devlooped sponsorship"); - return; - } - - var manager = new SponsorsManager( - Mock.Of(), - new SecurityManager(config), - new TableConnection(storage, nameof(SponsorLink)), - Mock.Of(), - new SponsorsRegistry(storage, Mock.Of())); - - // My account info from https://docs.github.com/en/graphql/overview/explorer - await manager.SponsorAsync(Constants.DevloopedAccount, - new AccountId("MDQ6VXNlcjE2OTcwNw==", "kzu"), 10); - - await manager.SponsorAsync(Constants.DevloopedAccount, - new AccountId("MDQ6VXNlcjUyNDMxMDY0", "anycarvallo"), 10); - } - - /// - /// Run this test manually to initialize a sponsorable account that is being onboarded to - /// SponsorLink which has existing sponsors. This is because for now we don't have a way - /// to query existing sponsors via GraphQL, so we need to manually initialize the account - /// with a JSON executed by the customer himself. - /// - [InlineData("")] - [LocalTheory] - public async Task SponsorableInit(string fileName) - { - // NOTE: set the file name/path to the JSON file with the sponsors data. - if (string.IsNullOrEmpty(fileName)) - return; - - Assert.True(File.Exists(fileName), $"File path '{fileName}' does not exist."); - - var json = File.ReadAllText(fileName); - var result = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - Assert.NotNull(result); - - var config = new ConfigurationBuilder().AddUserSecrets(ThisAssembly.Project.UserSecretsId).Build(); - if (!CloudStorageAccount.TryParse(config["ProductionStorageAccount"], out var account)) - Assert.Fail("Did not find a user secret named 'ProductionStorageAccount' to run the query against."); - - var events = new EventStream(config, new Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration()); - - var manager = new SponsorsManager( - Mock.Of(), - new SecurityManager(config), - new TableConnection(account, "SponsorLink"), - //new EventStream(config, new Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration()), - events, - new SponsorsRegistry(account, events)); - - if (result.Data.Organization != null) - { - var sponsorable = new AccountId(result.Data.Organization.Id, result.Data.Organization.Login); - foreach (var sponsorship in result.Data.Organization.SponsorshipsAsMaintainer.Nodes) - { - var sponsor = new AccountId(sponsorship.SponsorEntity.Id, sponsorship.SponsorEntity.Login); - var note = $"Existing sponsor {sponsor.Login} for {sponsorable.Login} with ${sponsorship.Tier.MonthlyPriceInDollars} {(sponsorship.IsOneTimePayment ? "one-time" : "monthly")} payment"; - - await manager.SponsorAsync(sponsorable, sponsor, - sponsorship.Tier.MonthlyPriceInDollars, - sponsorship.IsOneTimePayment ? DateOnly.FromDateTime(sponsorship.CreatedAt.AddDays(30)) : null, - note); - } - } - } -} diff --git a/src/Tests/Misc.cs b/src/Tests/Misc.cs deleted file mode 100644 index 553ee552..00000000 --- a/src/Tests/Misc.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System.IO.Compression; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Numerics; -using System.Security.Cryptography; -using System.Text; -using Azure; -using Azure.Identity; -using Azure.Monitor.Query; -using Devlooped; -using Devlooped.SponsorLink; -using Microsoft.Extensions.Configuration; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Octokit; - -namespace Tests; - -//public record SponsorData(string Action, ) - -public record Misc(ITestOutputHelper Output) -{ - [Fact] - public async Task UnsponsorRegistry() - { - var config = new ConfigurationBuilder().AddUserSecrets(ThisAssembly.Project.UserSecretsId).Build(); - if (!CloudStorageAccount.TryParse(config["RealStorageAccountForTests"], out var account)) - return; - - var container = account.CreateBlobServiceClient().GetBlobContainerClient("sponsorlink"); - - try - { - while (true) - { - try - { - await container.CreateIfNotExistsAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob); - break; - } - catch (RequestFailedException ex) when (ex.Status == 409 && ex.ErrorCode == "ContainerBeingDeleted") - { - // Allow some time for the blob container deletion to complete, across test runs. - Thread.Sleep(100); - } - } - - var sponsorable = new AccountId("MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", "devlooped"); - var sponsor = new AccountId("MDQ6VXNlcjg3OTU5NTQx", "devlooped-bot"); - var registry = new SponsorsRegistry(account, Mock.Of()); - - await registry.RegisterSponsorAsync(sponsorable, sponsor, new[] { "kzu@github.com", "test@github.com" }); - - await registry.UnregisterSponsorAsync(sponsorable, sponsor); - } - finally - { - await container.DeleteIfExistsAsync(); - } - } - - [Fact] - public void EncodeTicks() - { - Output.WriteLine(Base62.Encode(DateTimeOffset.Now.UtcTicks - QuaranTime.Epoch.UtcTicks)); - Output.WriteLine(Base62.Encode(DateTimeOffset.Now.UtcTicks - QuaranTime.Epoch.UtcTicks)); - Output.WriteLine(Base62.Encode(DateTimeOffset.Now.UtcTicks - QuaranTime.Epoch.UtcTicks)); - } - - [Fact] - public void Encode() - { - var code = Base62.Encode(DateTimeOffset.UtcNow.ToQuaranTimeMilliseconds()); - Output.WriteLine(Base62.Encode(Thread.CurrentThread.ManagedThreadId) + "." + code); - - Output.WriteLine(Base62.Encode(BigInteger.Abs(new BigInteger(Guid.NewGuid().ToByteArray())))); - - var payload = ThisAssembly.Resources.webhook_created.Text; - var mem = new MemoryStream(); - using (var compressor = new DeflateStream(mem, CompressionMode.Compress, true)) - using (var writer = new StreamWriter(compressor)) - writer.Write(payload); - - var hash = SHA1.HashData(Encoding.UTF8.GetBytes(payload)); - var big = BigInteger.Abs(new BigInteger(hash)); - Output.WriteLine($"{Encoding.UTF8.GetByteCount(payload)} bytes, {hash.Length} hashed: {Base62.Encode(big)}"); - - hash = SHA1.HashData(mem.ToArray()); - big = BigInteger.Abs(new BigInteger(hash)); - Output.WriteLine($"{mem.Length} bytes, {hash.Length} hashed: {Base62.Encode(big)}"); - - Output.WriteLine("Email hash: " + Base62.Encode(BigInteger.Abs(new BigInteger(SHA256.HashData(Encoding.UTF8.GetBytes("daniel@cazzulino.com")))))); - } - - [Fact] - public void Deserialize() - { - dynamic data = JsonConvert.DeserializeObject(ThisAssembly.Resources.sponsors_created.Text)!; - - DateTime date = data.sponsorship.created_at; - - Output.WriteLine(date.AddDays(30).ToString("o")); - } - - [Fact] - public void ParseIni() - { - var text = ThisAssembly.Resources.settings.Text; - var values = text.Split(new[] { "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries) - .Where(x => x[0] != '#') - .Select(x => x.Split(new[] { '=' }, 2)) - .ToDictionary(x => x[0].Trim(), x => x[1].Trim()); - - foreach (var pair in values) - { - Output.WriteLine($"{pair.Key} = {pair.Value}"); - } - } - - [Fact] - public async Task GetInstallations() - { - var config = new ConfigurationBuilder() - .AddUserSecrets(ThisAssembly.Project.UserSecretsId) - .Build(); - - var accessToken = config["AccessToken"]; - - if (string.IsNullOrEmpty(accessToken)) - return; - - var octo = new GitHubClient(new Octokit.ProductHeaderValue("SponsorLink", new Version(ThisAssembly.Info.Version).ToString(2))) - { - Credentials = new Credentials(accessToken) - }; - - //var installations = await octo.GitHubApps.GetAllInstallationsForCurrentUser(); - //Output.WriteLine(JsonConvert.SerializeObject(installations, Formatting.Indented)); - - //resp = await http.GetAsync("https://api.github.com/app/installations", jwt); - //var body = await resp.Content.ReadAsStringAsync(); - - //resp = await http.PostAsync($"https://api.github.com/app/installations/{installation}/access_tokens", null, jwt); - //body = await resp.Content.ReadAsStringAsync(); - //data = JsonConvert.DeserializeObject(body) ?? throw new InvalidOperationException(); - - //var query = - // """ - // query ($owner: String!, $endCursor: String) { - // user(login: $owner) { - // sponsorshipsAsSponsor(first: 100, after: $endCursor) { - // nodes { - // privacyLevel - // tier { - // monthlyPriceInDollars - // } - // sponsorable { - // ... on User { - // id - // login - // } - // ... on Organization { - // id - // login - // } - // } - // } - // } - // } - // } - // """; - - //http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string)data.token); - //var response = await http.PostAsJsonAsync("https://api.github.com/graphql", - // new - // { - // query, - // variables = new - // { - // owner = user.Login - // } - // }); - //body = await response.Content.ReadAsStringAsync(); - - var query = - """ - query { - viewer{ - sponsoring(first: 100) { - nodes { - ... on User { - login - } - ... on Organization { - login - } - } - } - } - } - """; - - using var http = new HttpClient(); - http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SponsorLink", new Version(ThisAssembly.Info.Version).ToString(2))); - - var owner = await octo.User.Current(); - - var response = await http.PostAsJsonAsync("https://api.github.com/graphql", - new - { - query, - variables = new - { - owner = owner.Login - } - }); - - var body = await response.Content.ReadAsStringAsync(); - } - - [Fact] - public async Task GetOrganizations() - { - var config = new ConfigurationBuilder() - .AddUserSecrets(ThisAssembly.Project.UserSecretsId) - .Build(); - - var accessToken = config["AccessToken"]; - if (string.IsNullOrEmpty(accessToken)) - return; - - var octo = new GitHubClient(new Octokit.ProductHeaderValue("SponsorLink", new Version(ThisAssembly.Info.Version).ToString(2))) - { - Credentials = new Credentials(accessToken) - }; - var user = await octo.User.Current(); - Console.WriteLine($"{user.Login}"); - - var query = - """ - query { - viewer{ - organizations(first: 100) { - nodes { - id - login - } - } - } - } - """; - - using var http = new HttpClient(); - http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SponsorLink", new Version(ThisAssembly.Info.Version).ToString(2))); - - var response = await http.PostAsJsonAsync("https://api.github.com/graphql", new { query }); - - Assert.True(response.IsSuccessStatusCode); - - var body = await response.Content.ReadAsStringAsync(); - foreach (var account in JObject.Parse(body) - .SelectTokens("$.data.viewer.organizations.nodes[*]") - .Select(j => j.ToString()) - .Select(JsonConvert.DeserializeObject)) - { - Output.WriteLine($"{account?.Login} ({account?.Id})"); - } - } - - [Fact] - public async Task QueryLogs() - { - var creds = new DefaultAzureCredential(new DefaultAzureCredentialOptions - { - }); - - var client = new LogsQueryClient(creds); - var kql = - """ - StorageBlobLogs - | where TimeGenerated > ago(6d) - | where OperationName == "GetBlobProperties" - | extend Parts=split(ObjectKey, '/') - | extend Project=tostring(Parts[2]), Container=tostring(Parts[3]), Email=tostring(Parts[4]) - | where Project == "sponsorlink" and Container == "apps" - | extend Url=parse_url(Uri) - | extend Account=tostring(Url["Query Parameters"]["account"]), - Product=tostring(Url["Query Parameters"]["product"]), - Package=tostring(Url["Query Parameters"]["package"]), - Version=tostring(Url["Query Parameters"]["version"]) - | where not(isempty(Account)) and not(isempty(Product)) and Account != "kzu" and Account != "sample" - | project TimeGenerated, Email, - Account=iif(isempty(Account), "devlooped", Account), - Product=iif(isempty(Product), "unknown", Product), - Package=iif(isempty(Package), iif(isempty(Product), "unknown", Product), Package), - Version - | summarize Count=count() by Account, Product, Package, Version, Email - """; - - var result = await client.QueryWorkspaceAsync("b5823173-1be6-4d23-92e6-4d2a4a89ad20", kql, - new QueryTimeRange(DateTimeOffset.UtcNow.AddDays(-6), DateTimeOffset.UtcNow)); - - } - - public class LogResult - //(string? Account, string? Product, string? Package, string? Version, string? Email, long? Count) - { - public string? Account { get; set; } - public string? Product { get; set; } - public string? Package { get; set; } - public string? Version { get; set; } - public string? Email { get; set; } - public long? Count { get; set; } - - } -} \ No newline at end of file diff --git a/src/Tests/Queries/accounts.kql b/src/Tests/Queries/accounts.kql deleted file mode 100644 index 7198dbd6..00000000 --- a/src/Tests/Queries/accounts.kql +++ /dev/null @@ -1,17 +0,0 @@ -let toquery = (uri: string) { - let query = parse_url(uri)["Query Parameters"]; - iif(tostring(query["account"]) contains "%26", - parse_urlquery(url_decode(strcat("account=", query["account"])))["Query Parameters"], - query); -}; -StorageBlobLogs -| where TimeGenerated > ago(30d) -| where OperationName == "GetBlob" or OperationName == "GetBlobProperties" -| extend Parts=split(ObjectKey, '/') -| extend Project=tostring(Parts[2]), Container=tostring(Parts[3]) -| where Project == "sponsorlink" -| extend Query=toquery(Uri) -| extend Account=tostring(Query["account"]) -| where Account != "" and Account != "kzu" and Account != "sample" and Account != "test" and Account != "testing" -| summarize by Account -| count \ No newline at end of file diff --git a/src/Tests/Queries/projects.kql b/src/Tests/Queries/projects.kql deleted file mode 100644 index a3040b51..00000000 --- a/src/Tests/Queries/projects.kql +++ /dev/null @@ -1,19 +0,0 @@ -let toquery = (uri: string) { - let query = parse_url(uri)["Query Parameters"]; - iif(tostring(query["account"]) contains "%26", - parse_urlquery(url_decode(strcat("account=", query["account"])))["Query Parameters"], - query); -}; -StorageBlobLogs -| where TimeGenerated > ago(30d) -| where OperationName == "GetBlob" or OperationName == "GetBlobProperties" -| extend Parts=split(ObjectKey, '/') -| extend Project=tostring(Parts[2]), Container=tostring(Parts[3]) -| where Project == "sponsorlink" -| extend Query=toquery(Uri) -| extend Account=tostring(Query["account"]), - Product=tostring(Query["product"]) -| where Account != "" and Account != "kzu" and Account != "sample" and Account != "test" and Account != "testing" -| project AccountProject=strcat(Account, "|", Product) -| summarize by AccountProject -| count \ No newline at end of file diff --git a/src/Tests/Queries/users.kql b/src/Tests/Queries/users.kql deleted file mode 100644 index 4ed0b6cc..00000000 --- a/src/Tests/Queries/users.kql +++ /dev/null @@ -1,8 +0,0 @@ -StorageBlobLogs -| where TimeGenerated > ago(30d) -| where OperationName == "GetBlob" or OperationName == "GetBlobProperties" -| extend Parts=split(ObjectKey, '/') -| extend Project=tostring(Parts[2]), Container=tostring(Parts[3]), User=tostring(Parts[4]) -| where Project == "sponsorlink" and Container == "apps" -| summarize by User -| count \ No newline at end of file diff --git a/src/Tests/RunBadgeQueries.cs b/src/Tests/RunBadgeQueries.cs deleted file mode 100644 index e6f08fd1..00000000 --- a/src/Tests/RunBadgeQueries.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Runtime.CompilerServices; -using Azure.Identity; -using Azure.Monitor.Query; -using Microsoft.Extensions.Configuration; - -namespace Devlooped.SponsorLink; - -#pragma warning disable xUnit1013 // Public method should be marked as test -public record RunBadgeQueries(ITestOutputHelper Output) -#pragma warning restore xUnit1013 // Public method should be marked as test -{ - [Fact] - public Task users() => RunQuery(); - - [Fact] - public Task projects() => RunQuery(); - - [Fact] - public Task accounts() => RunQuery(); - - async Task RunQuery([CallerMemberName] string query = "") - { - var config = new ConfigurationBuilder() - .AddUserSecrets(ThisAssembly.Project.UserSecretsId) - .Build(); - - Environment.SetEnvironmentVariable("AZURE_CLIENT_ID", config["AZURE_CLIENT_ID"]); - Environment.SetEnvironmentVariable("AZURE_CLIENT_SECRET", config["AZURE_CLIENT_SECRET"]); - Environment.SetEnvironmentVariable("AZURE_TENANT_ID", config["AZURE_TENANT_ID"]); - - var kql = File.ReadAllText(@$"Queries\{query}.kql"); - var creds = new DefaultAzureCredential(new DefaultAzureCredentialOptions - { - ExcludeAzureCliCredential = true, - ExcludeAzurePowerShellCredential = true, - ExcludeInteractiveBrowserCredential = true, - ExcludeManagedIdentityCredential = true, - ExcludeSharedTokenCacheCredential = true, - ExcludeVisualStudioCodeCredential = true, - ExcludeVisualStudioCredential = true - }); - var client = new LogsQueryClient(creds); - - var result = await client.QueryWorkspaceAsync( - Constants.LogAnalyticsWorkspaceId, kql, - QueryTimeRange.All); - - Output.WriteLine(result.Value.FirstOrDefault(-1).ToString()); - } -} diff --git a/src/Tests/SponsorsManagerTests.cs b/src/Tests/SponsorsManagerTests.cs deleted file mode 100644 index 3d003cb7..00000000 --- a/src/Tests/SponsorsManagerTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using ScenarioTests; - -namespace Devlooped.SponsorLink; - -public sealed partial class SponsorsManagerTests : IDisposable -{ - readonly TableConnection connection = new(CloudStorageAccount.DevelopmentStorageAccount, nameof(SponsorsManagerTests)); - - public ITestOutputHelper Output { get; } - - public SponsorsManagerTests(ITestOutputHelper output) - { - Output = output; - // Ensure table is created; - _ = connection.GetTableAsync().Result; - } - - public void Dispose() - { - connection.StorageAccount.CreateTableServiceClient().DeleteTable(connection.TableName); - connection.StorageAccount.CreateTableServiceClient().DeleteTable("Sponsorship"); - } - - [Fact(Skip = "Manual run")] - public async Task DeleteDevelopmentTables() - { - var client = CloudStorageAccount.DevelopmentStorageAccount.CreateTableServiceClient(); - await foreach (var table in client.QueryAsync()) - await client.DeleteTableAsync(table.Name); - } - - [Fact] - public async Task SaveEmail() - { - var byEmail = TableRepository.Create( - CloudStorageAccount.DevelopmentStorageAccount, - partitionKey: x => x.Email, - rowKey: x => x.Id); - - var byAccount = TableRepository.Create( - CloudStorageAccount.DevelopmentStorageAccount, - partitionKey: x => x.Id, - rowKey: x => x.Email); - - var account = new Account("asdf", "kzu", "kzu@github.com"); - - await byEmail.PutAsync(account); - await byAccount.PutAsync(account); - - Assert.NotNull(await byEmail.GetAsync("kzu@github.com", "asdf")); - Assert.NotNull(await byAccount.GetAsync("asdf", "kzu@github.com")); - } - - [Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] - public async Task AppUsage(ScenarioContext scenario) - { - var config = new ConfigurationBuilder().AddUserSecrets(ThisAssembly.Project.UserSecretsId).Build(); - var repo = TablePartition.Create(connection, "Sponsorable"); - - var manager = new SponsorsManager( - Mock.Of(), - new SecurityManager(config), - connection, - Mock.Of(), - new SponsorsRegistry(connection.StorageAccount, Mock.Of())); - - var id = new AccountId("1234", "kzu"); - - await manager.AppInstallAsync(AppKind.Sponsorable, id); - - await scenario.Fact("App is installed after install", async () => - { - var account = await repo.GetAsync(id.Id); - - Assert.NotNull(account); - Assert.Equal(AppState.Installed, account.State); - }); - - await scenario.Fact("Cannot perform operations unless sponsorable is sponsoring devlooped", async () => - { - var ex = await Assert.ThrowsAsync(() => manager.SyncSponsorableAsync(id, false)); - Assert.Contains("SponsorLink usage requires an active sponsorship", ex.Message); - }); - - await manager.SponsorAsync(Constants.DevloopedAccount, id, 5); - - scenario.Fact("Suspending non-installed app throws", async () => - await Assert.ThrowsAsync(() => manager.AppSuspendAsync(AppKind.Sponsorable, new AccountId("456", "asdf"))) - ); - - scenario.Fact("Uninstalling non-installed app throws", async () => - await Assert.ThrowsAsync(() => manager.AppUninstallAsync(AppKind.Sponsorable, new AccountId("456", "asdf"))) - ); - - await manager.AppSuspendAsync(AppKind.Sponsorable, id); - - await scenario.Fact("App is inactive and suspended when suspending", async () => - { - var account = await repo.GetAsync(id.Id); - - Assert.Equal(AppState.Suspended, account!.State); - }); - - await manager.AppUnsuspendAsync(AppKind.Sponsorable, id); - - await scenario.Fact("App is installed when unsuspended", async () => - { - var account = await repo.GetAsync(id.Id); - - Assert.Equal(AppState.Installed, account!.State); - }); - - await manager.AppUninstallAsync(AppKind.Sponsorable, id); - - await scenario.Fact("App is marked deleted when uninstalled", async () => - { - var account = await repo.GetAsync(id.Id); - - Assert.Equal(AppState.Deleted, account!.State); - }); - } - - //[Scenario(NamingPolicy = ScenarioTestMethodNamingPolicy.Test)] - //public async Task SponsorsUsage(ScenarioContext scenario) - //{ - // var config = new ConfigurationBuilder().AddUserSecrets(ThisAssembly.Project.UserSecretsId).Build(); - // if (!CloudStorageAccount.TryParse(config["RealStorageAccountForTests"], out var account)) - // return; - - // var manager = new SponsorsManager( - // Mock.Of(), - // new SecurityManager(config), - // connection.StorageAccount, connection, - // Mock.Of()); - - // await Task.CompletedTask; - //} -} diff --git a/src/Tests/System/Base62.cs b/src/Tests/System/Base62.cs deleted file mode 100644 index 7eb3913e..00000000 --- a/src/Tests/System/Base62.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -#region License -// MIT License -// -// Copyright (c) Daniel Cazzulino -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -#endregion - -#nullable enable -using System.Linq; -using System.Numerics; -using System.Text; - -namespace System -{ - /// - /// Inspired in a bunch of searches, samples and snippets on various languages - /// and blogs and what-not on doing URL shortering :), heavily tweaked to make - /// it fully idiomatic in C#. - /// - static partial class Base62 - { - /// - /// Encodes a numeric value into a base62 string. - /// - public static string Encode(BigInteger value) - { - // TODO: I'm almost sure there's a more succint way - // of doing this with LINQ and Aggregate, but just - // can't figure it out... - var sb = new StringBuilder(); - - while (value != 0) - { - sb = sb.Append(ToBase62(value % 62)); - value /= 62; - } - - return new string(sb.ToString().Reverse().ToArray()); - } - - /// - /// Decodes a base62 string into its original numeric value. - /// - public static BigInteger Decode(string value) - => value.Aggregate(new BigInteger(0), (current, c) => current * 62 + FromBase62(c)); - - static char ToBase62(BigInteger d) => d switch - { - BigInteger v when v < 10 => (char)('0' + d), - BigInteger v when v < 36 => (char)('A' + d - 10), - BigInteger v when v < 62 => (char)('a' + d - 36), - _ => throw new ArgumentException($"Cannot encode digit {d} to base 62.", nameof(d)), - }; - - static BigInteger FromBase62(char c) => c switch - { - char v when c >= 'a' && v <= 'z' => 36 + c - 'a', - char v when c >= 'A' && v <= 'Z' => 10 + c - 'A', - char v when c >= '0' && v <= '9' => c - '0', - _ => throw new ArgumentException($"Cannot decode char '{c}' from base 62.", nameof(c)), - }; - } -} \ No newline at end of file diff --git a/src/Tests/System/QuaranTime.cs b/src/Tests/System/QuaranTime.cs deleted file mode 100644 index 6eb9120b..00000000 --- a/src/Tests/System/QuaranTime.cs +++ /dev/null @@ -1,137 +0,0 @@ -namespace System -{ - /// - /// Provides the constant that signaled the - /// start of the Quarantime time and methods to convert to and from - /// Quarantine-relative values. - /// - static partial class QuaranTime - { - // From DateTime - internal const long DateTimeMinTicks = 0L; - internal const long DateTimeMaxTicks = 3155378975999999999L; - - internal const long QuarantineMinSeconds = DateTimeMinTicks / TimeSpan.TicksPerSecond - QuarantineEpochSeconds; - internal const long QuarantineMaxSeconds = DateTimeMaxTicks / TimeSpan.TicksPerSecond - QuarantineEpochSeconds; - - internal const long QuarantineEpochTicks = 637202700000000000; - internal const long QuarantineEpochSeconds = QuarantineEpochTicks / TimeSpan.TicksPerSecond; - internal const long QuarantineEpochMiliseconds = QuarantineEpochTicks / TimeSpan.TicksPerMillisecond; - - /// - /// The value of this constant is equivalent to 00:00:00 GMT-0300, March 3, 2020, in the Gregorian calendar. - /// defines the point in time when the quarantine started in Argentina. - /// - public static DateTimeOffset Epoch { get; } = new DateTimeOffset(637202592000000000, TimeSpan.FromHours(-3)); - - /// - /// Converts a Quarantine time expressed as the number of seconds that have elapsed since - /// since 2020-03-20T00:00:00-03:00 to a value. - /// - /// - /// A Quarantine time, expressed as the number of seconds that have elapsed since - /// 2020-03-20T00:00:00-03:00 (March 3, 2020, at 12:00 AM GMT-0300). For Quarantine times - /// before this date, its value is negative. - /// - /// - /// A date and time value that represents the same moment in time as the Quarantine time. - /// - /// - /// The property value of the returned - /// instance is , which represents Coordinated Universal Time. - /// You can convert it to the time in a specific time zone by calling the - /// . - /// - /// - /// seconds is less than -63,720,270,000 or greater than 251,817,627,599. - /// - public static DateTimeOffset FromQuaranTimeSeconds(long seconds) - { - if (seconds < QuarantineMinSeconds || seconds > QuarantineMaxSeconds) - { - throw new ArgumentOutOfRangeException(nameof(seconds)); - } - - var ticks = seconds * TimeSpan.TicksPerSecond + QuarantineEpochTicks; - return new DateTimeOffset(ticks, TimeSpan.Zero); - } - - /// - /// Converts a Quarantine time expressed as the number of milliseconds that have elapsed - /// since 2020-03-20T00:00:00-03:00 to a value. - /// - /// - /// A Quarantine time, expressed as the number of milliseconds that have elapsed since - /// 2020-03-20T00:00:00-03:00 (March 3, 2020, at 12:00 AM GMT-0300). For Quarantine times - /// before this date, its value is negative. - /// - /// - /// A date and time value that represents the same moment in time as the Quarantine time. - /// - /// - /// The property value of the returned - /// instance is , which represents Coordinated Universal Time. - /// You can convert it to the time in a specific time zone by calling the - /// . - /// - /// - /// seconds is less than -63,720,270,000,000 or greater than 251,817,627,599,999. - /// - public static DateTimeOffset FromQuaranTimeMilliseconds(long milliseconds) - { - const long MinMilliseconds = DateTimeMinTicks / TimeSpan.TicksPerMillisecond - QuarantineEpochMiliseconds; - const long MaxMilliseconds = DateTimeMaxTicks / TimeSpan.TicksPerMillisecond - QuarantineEpochMiliseconds; - - if (milliseconds < MinMilliseconds || milliseconds > MaxMilliseconds) - { - throw new ArgumentOutOfRangeException(nameof(milliseconds)); - } - - var ticks = milliseconds * TimeSpan.TicksPerMillisecond + QuarantineEpochTicks; - return new DateTimeOffset(ticks, TimeSpan.Zero); - } - - /// - /// Returns the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00. - /// - /// - /// Quarantine time represents the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00 - /// (March 3, 2020, at 12:00 AM GMT-0300). It does not take leap seconds into account. This method - /// returns the number of seconds in Quarantine time. - /// - /// This method first converts the current instance to UTC before returning its Quarantine time. - /// For date and time values before 2020-03-20T00:00:00-03:00, this method returns a negative value. - /// - /// - /// The number of seconds that have elapsed since 2020-03-20T00:00:00-03:00. - public static long ToQuaranTimeSeconds(this DateTimeOffset dateTime) - { - // Truncate just like ToUnixTimeSeconds does, see https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L583 - - var seconds = dateTime.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; - return seconds - QuarantineEpochSeconds; - } - - /// - /// Returns the number of milliseconds that have elapsed since 2020-03-20T00:00:00-03:00. - /// - /// - /// Quarantine time represents the number of seconds that have elapsed since 2020-03-20T00:00:00-03:00 - /// (March 3, 2020, at 12:00 AM GMT-0300). It does not take leap seconds into account. This method - /// returns the number of milliseconds in Quarantine time. - /// - /// This method first converts the current instance to UTC before returning the number of milliseconds - /// in its Quarantine time. For date and time values before 2020-03-20T00:00:00-03:00, this method returns - /// a negative value. - /// - /// - /// The number of miliseconds that have elapsed since 2020-03-20T00:00:00-03:00. - public static long ToQuaranTimeMilliseconds(this DateTimeOffset dateTime) - { - // Truncate just like ToUnixTimeMiliseconds does, see https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L605 - - var seconds = dateTime.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; - return seconds - QuarantineEpochMiliseconds; - } - } -} diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 5ad4939b..53bf44b1 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -29,23 +29,9 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Tests/sponsors-created.json b/src/Tests/sponsors-created.json deleted file mode 100644 index a2cadff2..00000000 --- a/src/Tests/sponsors-created.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "action": "created", - "sponsorship": { - "node_id": "S_kwHOA6rues4AAjj7", - "created_at": "2023-01-10T15:26:29+00:00", - "sponsorable": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "maintainer": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "sponsor": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - }, - "privacy_level": "private", - "tier": { - "node_id": "MDEyOlNwb25zb3JzVGllcjUwNDc1", - "created_at": "2020-12-16T00:50:55Z", - "description": "☕ You want to buy me a Nespresso capsule, and everyone should be allowed to do that 🤗", - "monthly_price_in_cents": 100, - "monthly_price_in_dollars": 1, - "name": "$1 a month", - "is_one_time": false, - "is_custom_amount": false - } - }, - "sender": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/Tests/sponsors-onetime.json b/src/Tests/sponsors-onetime.json deleted file mode 100644 index 711a3822..00000000 --- a/src/Tests/sponsors-onetime.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "action": "created", - "sponsorship": { - "node_id": "S_kwHOA6rues4AAjoF", - "created_at": "2023-01-11T14:21:37+00:00", - "sponsorable": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "maintainer": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "sponsor": { - "login": "aguskzu", - "id": 5412683, - "node_id": "MDQ6VXNlcjU0MTI2ODM=", - "avatar_url": "https://avatars.githubusercontent.com/u/5412683?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/aguskzu", - "html_url": "https://github.com/aguskzu", - "followers_url": "https://api.github.com/users/aguskzu/followers", - "following_url": "https://api.github.com/users/aguskzu/following{/other_user}", - "gists_url": "https://api.github.com/users/aguskzu/gists{/gist_id}", - "starred_url": "https://api.github.com/users/aguskzu/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/aguskzu/subscriptions", - "organizations_url": "https://api.github.com/users/aguskzu/orgs", - "repos_url": "https://api.github.com/users/aguskzu/repos", - "events_url": "https://api.github.com/users/aguskzu/events{/privacy}", - "received_events_url": "https://api.github.com/users/aguskzu/received_events", - "type": "User", - "site_admin": false - }, - "privacy_level": "private", - "tier": { - "node_id": "ST_kwHOA6rues4AA-Np", - "created_at": "2023-01-11T14:21:37Z", - "description": "", - "monthly_price_in_cents": 100, - "monthly_price_in_dollars": 1, - "name": "$1 one time", - "is_one_time": true, - "is_custom_amount": true - } - }, - "sender": { - "login": "aguskzu", - "id": 5412683, - "node_id": "MDQ6VXNlcjU0MTI2ODM=", - "avatar_url": "https://avatars.githubusercontent.com/u/5412683?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/aguskzu", - "html_url": "https://github.com/aguskzu", - "followers_url": "https://api.github.com/users/aguskzu/followers", - "following_url": "https://api.github.com/users/aguskzu/following{/other_user}", - "gists_url": "https://api.github.com/users/aguskzu/gists{/gist_id}", - "starred_url": "https://api.github.com/users/aguskzu/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/aguskzu/subscriptions", - "organizations_url": "https://api.github.com/users/aguskzu/orgs", - "repos_url": "https://api.github.com/users/aguskzu/repos", - "events_url": "https://api.github.com/users/aguskzu/events{/privacy}", - "received_events_url": "https://api.github.com/users/aguskzu/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/Tests/sponsors-pending-cancellation.json b/src/Tests/sponsors-pending-cancellation.json deleted file mode 100644 index b5944564..00000000 --- a/src/Tests/sponsors-pending-cancellation.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "action": "pending_cancellation", - "sponsorship": { - "node_id": "S_kwHOA6rues4AAjj7", - "created_at": "2023-01-10T15:26:29+00:00", - "sponsorable": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "maintainer": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "sponsor": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - }, - "privacy_level": "private", - "tier": { - "node_id": "MDEyOlNwb25zb3JzVGllcjUwNDc1", - "created_at": "2020-12-16T00:50:55Z", - "description": "☕ You want to buy me a Nespresso capsule, and everyone should be allowed to do that 🤗", - "monthly_price_in_cents": 100, - "monthly_price_in_dollars": 1, - "name": "$1 a month", - "is_one_time": false, - "is_custom_amount": false - } - }, - "effective_date": "2023-02-10T00:00:00+00:00", - "sender": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/Tests/sponsors-tierchanged.json b/src/Tests/sponsors-tierchanged.json deleted file mode 100644 index 75a80cc9..00000000 --- a/src/Tests/sponsors-tierchanged.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "action": "tier_changed", - "sponsorship": { - "node_id": "S_kwHOA6rues4AAjj7", - "created_at": "2023-01-10T15:26:29+00:00", - "sponsorable": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "maintainer": { - "login": "devlooped", - "id": 61533818, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjYxNTMzODE4", - "url": "https://api.github.com/orgs/devlooped", - "repos_url": "https://api.github.com/orgs/devlooped/repos", - "events_url": "https://api.github.com/orgs/devlooped/events", - "hooks_url": "https://api.github.com/orgs/devlooped/hooks", - "issues_url": "https://api.github.com/orgs/devlooped/issues", - "members_url": "https://api.github.com/orgs/devlooped/members{/member}", - "public_members_url": "https://api.github.com/orgs/devlooped/public_members{/member}", - "avatar_url": "https://avatars.githubusercontent.com/u/61533818?v=4", - "description": "Daniel Cazzulino sponsorship organization, a.k.a. @kzu. " - }, - "sponsor": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - }, - "privacy_level": "private", - "tier": { - "node_id": "MDEyOlNwb25zb3JzVGllcjUwNDc2", - "created_at": "2020-12-16T00:51:03Z", - "description": "☕☕ You want to buy me two Nespresso capsules, affordable and twice the punch 👊", - "monthly_price_in_cents": 200, - "monthly_price_in_dollars": 2, - "name": "$2 a month", - "is_one_time": false, - "is_custom_amount": false - } - }, - "changes": { - "tier": { - "from": { - "node_id": "MDEyOlNwb25zb3JzVGllcjUwNDc1", - "created_at": "2020-12-16T00:50:55Z", - "description": "☕ You want to buy me a Nespresso capsule, and everyone should be allowed to do that 🤗", - "monthly_price_in_cents": 100, - "monthly_price_in_dollars": 1, - "name": "$1 a month", - "is_one_time": false, - "is_custom_amount": false - } - } - }, - "sender": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/Tests/webhook-created.json b/src/Tests/webhook-created.json deleted file mode 100644 index e556f04a..00000000 --- a/src/Tests/webhook-created.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "action": "created", - "installation": { - "id": 32997332, - "account": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - }, - "repository_selection": "selected", - "access_tokens_url": "https://api.github.com/app/installations/32997332/access_tokens", - "repositories_url": "https://api.github.com/installation/repositories", - "html_url": "https://github.com/settings/installations/32997332", - "app_id": 279204, - "app_slug": "SponsorLink", - "target_id": 52431064, - "target_type": "User", - "permissions": { - }, - "events": [ - ], - "created_at": "2023-01-10T11:36:50.000-03:00", - "updated_at": "2023-01-10T11:36:50.000-03:00", - "single_file_name": null, - "has_multiple_single_files": false, - "single_file_paths": [ - ], - "suspended_by": null, - "suspended_at": null - }, - "repositories": [ - ], - "requester": null, - "sender": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file diff --git a/src/Tests/webhook-deleted.json b/src/Tests/webhook-deleted.json deleted file mode 100644 index 93f2bc53..00000000 --- a/src/Tests/webhook-deleted.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "action": "deleted", - "installation": { - "id": 32997311, - "account": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - }, - "repository_selection": "selected", - "access_tokens_url": "https://api.github.com/app/installations/32997311/access_tokens", - "repositories_url": "https://api.github.com/installation/repositories", - "html_url": "https://github.com/settings/installations/32997311", - "app_id": 279204, - "app_slug": "SponsorLink", - "target_id": 52431064, - "target_type": "User", - "permissions": { - }, - "events": [ - ], - "created_at": "2023-01-10T14:36:15.000Z", - "updated_at": "2023-01-10T14:36:16.000Z", - "single_file_name": null, - "has_multiple_single_files": false, - "single_file_paths": [ - ], - "suspended_by": null, - "suspended_at": null - }, - "repositories": [ - ], - "sender": { - "login": "anycarvallo", - "id": 52431064, - "node_id": "MDQ6VXNlcjUyNDMxMDY0", - "avatar_url": "https://avatars.githubusercontent.com/u/52431064?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/anycarvallo", - "html_url": "https://github.com/anycarvallo", - "followers_url": "https://api.github.com/users/anycarvallo/followers", - "following_url": "https://api.github.com/users/anycarvallo/following{/other_user}", - "gists_url": "https://api.github.com/users/anycarvallo/gists{/gist_id}", - "starred_url": "https://api.github.com/users/anycarvallo/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/anycarvallo/subscriptions", - "organizations_url": "https://api.github.com/users/anycarvallo/orgs", - "repos_url": "https://api.github.com/users/anycarvallo/repos", - "events_url": "https://api.github.com/users/anycarvallo/events{/privacy}", - "received_events_url": "https://api.github.com/users/anycarvallo/received_events", - "type": "User", - "site_admin": false - } -} \ No newline at end of file