From 07aed080a8a9ada403135ea448198be7b994e394 Mon Sep 17 00:00:00 2001 From: Tomas Bezouska Date: Tue, 15 Oct 2024 15:01:15 +0200 Subject: [PATCH 1/2] feat(nip62): request to vanish --- README.md | 7 +- src/Netstr/Extensions/HttpExtensions.cs | 13 + src/Netstr/Extensions/MessagingExtensions.cs | 3 + .../Events/Handlers/DeleteEventHandler.cs | 10 +- .../Events/Handlers/VanishEventHandler.cs | 77 +++ .../Validators/UserVanishedValidator.cs | 35 + .../MessageHandlers/AuthMessageHandler.cs | 8 +- src/Netstr/Messaging/Messages.cs | 2 +- src/Netstr/Messaging/Models/Event.cs | 15 + src/Netstr/Messaging/Models/EventKind.cs | 2 + src/Netstr/Messaging/Models/User.cs | 11 + src/Netstr/Messaging/UserCache.cs | 53 ++ .../Middleware/UserCacheStartupService.cs | 55 ++ src/Netstr/Program.cs | 2 + src/Netstr/appsettings.json | 2 +- .../Events/EventVerificationTests.cs | 1 + test/Netstr.Tests/NIPs/62.feature | 109 ++++ test/Netstr.Tests/NIPs/62.feature.cs | 606 ++++++++++++++++++ test/Netstr.Tests/NIPs/70.feature.cs | 52 +- 19 files changed, 1024 insertions(+), 39 deletions(-) create mode 100644 src/Netstr/Extensions/HttpExtensions.cs create mode 100644 src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs create mode 100644 src/Netstr/Messaging/Events/Validators/UserVanishedValidator.cs create mode 100644 src/Netstr/Messaging/Models/User.cs create mode 100644 src/Netstr/Messaging/UserCache.cs create mode 100644 src/Netstr/Middleware/UserCacheStartupService.cs create mode 100644 test/Netstr.Tests/NIPs/62.feature create mode 100644 test/Netstr.Tests/NIPs/62.feature.cs diff --git a/README.md b/README.md index e14aaf4..5a13fb0 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ ![netstr logo](art/logo.jpg) -Netstr is a relay for the [nostr protocol](https://github.com/nostr-protocol/nostr) written in C#. Currently in early stages of development. +Netstr is a modern relay for the [nostr protocol](https://github.com/nostr-protocol/nostr) written in C#. - * Prod instance: https://relay.netstr.io/ - * Dev instance: https://relay-dev.netstr.io/ + * **Prod** instance: https://relay.netstr.io/ + * **Dev** instance: https://relay-dev.netstr.io/ (feel free to play with it / try to break it, just report if you find anything that needs fixing) ## Features @@ -24,6 +24,7 @@ NIPs with a relay-specific implementation are listed here. - [x] NIP-42: [Authentication of clients to relays](https://github.com/nostr-protocol/nips/blob/master/42.md) - [x] NIP-45: [Counting results](https://github.com/nostr-protocol/nips/blob/master/45.md) - [ ] NIP-50: [Search Capability](https://github.com/nostr-protocol/nips/blob/master/50.md) +- [x] NIP-62: [Request to Vanish](https://github.com/vitorpamplona/nips/blob/right-to-vanish/62.md) - [x] NIP-70: [Protected events](https://github.com/nostr-protocol/nips/blob/master/70.md) ## Tests diff --git a/src/Netstr/Extensions/HttpExtensions.cs b/src/Netstr/Extensions/HttpExtensions.cs new file mode 100644 index 0000000..869bcca --- /dev/null +++ b/src/Netstr/Extensions/HttpExtensions.cs @@ -0,0 +1,13 @@ +namespace Netstr.Extensions +{ + public static class HttpExtensions + { + /// + /// Gets the current normalized URL (host+path) where the relay is running. + /// + public static string GetNormalizedUrl(this HttpRequest ctx) + { + return $"{ctx.Host}{ctx.Path}".TrimEnd('/'); + } + } +} diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 1325c48..59d1164 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -17,6 +17,7 @@ public static IServiceCollection AddMessaging(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // message services.AddSingleton(); @@ -32,6 +33,7 @@ public static IServiceCollection AddMessaging(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // RegularEventHandler needs to go last services.AddSingleton(); @@ -51,6 +53,7 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs index ba207f9..3790bcb 100644 --- a/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs @@ -13,6 +13,8 @@ namespace Netstr.Messaging.Events.Handlers /// public class DeleteEventHandler : EventHandlerBase { + private static readonly long[] CannotDeleteKinds = [ EventKind.Delete, EventKind.RequestToVanish ]; + private record ReplaceableEventRef(int Kind, string PublicKey, string? Deduplication) { } private readonly IDbContextFactory db; @@ -45,15 +47,15 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve .Select(x => new { x.Id, - WrongKey = x.EventPublicKey != e.PublicKey, // only delete own events - WrongKind = x.EventKind == EventKind.Delete, // cannnot delete a delete event - AlreadyDeleted = x.DeletedAt.HasValue // was previously deleted + WrongKey = x.EventPublicKey != e.PublicKey, // only delete own events + WrongKind = CannotDeleteKinds.Contains(x.EventKind), // cannnot delete some events + AlreadyDeleted = x.DeletedAt.HasValue // was previously deleted }) .ToArrayAsync(); if (events.Any(x => x.WrongKey || x.WrongKind)) { - this.logger.LogWarning("Someone's trying to delete someone else's event or a deletion."); + this.logger.LogWarning("Someone's trying to delete someone else's or undeletable event."); await sender.SendNotOkAsync(e.Id, Messages.InvalidCannotDelete); return; } diff --git a/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs new file mode 100644 index 0000000..48774bf --- /dev/null +++ b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Netstr.Data; +using Netstr.Extensions; +using Netstr.Messaging.Models; +using Netstr.Options; + +namespace Netstr.Messaging.Events.Handlers +{ + public class VanishEventHandler : EventHandlerBase + { + private readonly IDbContextFactory db; + private readonly IUserCache userCache; + private readonly IHttpContextAccessor http; + + private readonly static string AllRelaysValue = "ALL_RELAYS"; + + public VanishEventHandler( + ILogger logger, + IOptions auth, + IWebSocketAdapterCollection adapters, + IDbContextFactory db, + IUserCache userCache, + IHttpContextAccessor http) + : base(logger, auth, adapters) + { + this.db = db; + this.userCache = userCache; + this.http = http; + } + + public override bool CanHandleEvent(Event e) => e.IsRequestToVanish(); + + protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Event e) + { + var ctx = this.http.HttpContext?.Request ?? throw new InvalidOperationException("HttpContext not set"); + var user = this.userCache.GetByPublicKey(e.PublicKey); + + var path = ctx.GetNormalizedUrl(); + var relays = e.GetNormalizedRelayValues(); + + // check 'relay' tag matches current url or is set to ALL_RELAYS + if (!relays.Any(x => x == path || x == AllRelaysValue)) + { + throw new MessageProcessingException(e, string.Format(Messages.AuthRequiredWrongTags, EventTag.Relay)); + } + + using var db = this.db.CreateDbContext(); + using var tx = db.Database.BeginTransaction(); + + // delete all user's events (or tagged GiftWraps) from before the vanish event + await db.Events + .Include(x => x.Tags) + .Where(x => + (x.EventPublicKey == e.PublicKey || + (x.EventKind == EventKind.GiftWrap && x.Tags.Any(t => t.Name == EventTag.PublicKey && t.Value == e.PublicKey))) && + x.EventCreatedAt <= e.CreatedAt) + .ExecuteDeleteAsync(); + + // insert vanish entity to db + db.Events.Add(e.ToEntity(DateTimeOffset.UtcNow)); + + // save + await db.SaveChangesAsync(); + await tx.CommitAsync(); + + // set vanished in cache + this.userCache.Vanish(e.PublicKey, e.CreatedAt); + + // reply + await sender.SendOkAsync(e.Id); + + // broadcast + await BroadcastEventAsync(e); + } + } +} diff --git a/src/Netstr/Messaging/Events/Validators/UserVanishedValidator.cs b/src/Netstr/Messaging/Events/Validators/UserVanishedValidator.cs new file mode 100644 index 0000000..2213f58 --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/UserVanishedValidator.cs @@ -0,0 +1,35 @@ +using Netstr.Messaging.Models; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Ensure older events cannot be republished if user vanished. + /// + public class UserVanishedValidator : IEventValidator + { + private readonly ILogger logger; + private readonly IUserCache userCache; + + public UserVanishedValidator( + ILogger logger, + IUserCache userCache) + { + this.logger = logger; + this.userCache = userCache; + } + + public string? Validate(Event e, ClientContext context) + { + var user = this.userCache.GetByPublicKey(e.PublicKey); + var vanished = user?.LastVanished ?? DateTimeOffset.MinValue; + + if (e.CreatedAt <= vanished) + { + this.logger.LogInformation($"Event {e.Id} is from user who already vanished on {vanished} (this event is from {e.CreatedAt})"); + return Messages.InvalidDeletedEvent; + } + + return null; + } + } +} diff --git a/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs b/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs index 8d21354..3cdd094 100644 --- a/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs +++ b/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs @@ -68,10 +68,10 @@ private Event ValidateAuthEvent(JsonDocument[] parameters, ClientContext context throw new MessageProcessingException(e, Messages.AuthRequiredWrongTags); } - var path = $"{ctx.Host}{ctx.Path}".TrimEnd('/'); - var relayTag = e.Tags.FirstOrDefault(x => x.Length == 2 && x[0] == EventTag.Relay); - var relay = relayTag?[1].Split("://")[1].TrimEnd('/'); - if (relayTag == null || relay != path) + var path = ctx.GetNormalizedUrl(); + var relays = e.GetNormalizedRelayValues(); + + if (!relays.Any(x => x == path)) { throw new MessageProcessingException(e, Messages.AuthRequiredWrongTags); } diff --git a/src/Netstr/Messaging/Messages.cs b/src/Netstr/Messaging/Messages.cs index 497df84..ada2967 100644 --- a/src/Netstr/Messaging/Messages.cs +++ b/src/Netstr/Messaging/Messages.cs @@ -22,7 +22,7 @@ public static class Messages public const string AuthRequiredPublishing = "auth-required: we only allow publishing to authenticated clients"; public const string AuthRequiredKind = "auth-required: subscribing to specified kind(s) requires authentication"; public const string AuthRequiredWrongKind = "auth-required: event has a wrong kind"; - public const string AuthRequiredWrongTags = "auth-required: event has a challenge or relay"; + public const string AuthRequiredWrongTags = "auth-required: event has a wrong challenge or relay"; public const string DuplicateEvent = "duplicate: already have this event"; public const string DuplicateReplaceableEvent = "duplicate: already have a newer version of this event"; public const string PowNotEnough = "pow: difficulty {0} is less than {1}"; diff --git a/src/Netstr/Messaging/Models/Event.cs b/src/Netstr/Messaging/Models/Event.cs index 827ce97..6ac1b53 100644 --- a/src/Netstr/Messaging/Models/Event.cs +++ b/src/Netstr/Messaging/Models/Event.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Netstr.Json; +using System.Linq; using System.Numerics; using System.Text.Json.Serialization; @@ -42,6 +43,8 @@ public record Event public bool IsDelete() => Kind == EventKind.Delete; + public bool IsRequestToVanish() => Kind == EventKind.RequestToVanish; + public bool IsProtected() => Tags.Any(x => x.Length >= 1 && x[0] == EventTag.Protected); public string ToStringUnique() @@ -76,6 +79,11 @@ public int GetDifficulty() return GetTagValue(EventTag.Deduplication); } + public IEnumerable GetNormalizedRelayValues() + { + return GetTagValues(EventTag.Relay).Select(x => x.Contains("://") ? x.Split("://")[1].TrimEnd('/') : x); + } + public DateTimeOffset? GetExpirationValue() { if (long.TryParse(GetTagValue(EventTag.Expiration), out var exp) && exp > 0) @@ -90,5 +98,12 @@ public int GetDifficulty() { return Tags.FirstOrDefault(x => x.Length > 1 && x.FirstOrDefault() == tag)?[1]; } + + public IEnumerable GetTagValues(string tag) + { + return Tags + .Where(x => x.Length > 1 && x.FirstOrDefault() == tag) + .Select(x => x[1]); + } } } diff --git a/src/Netstr/Messaging/Models/EventKind.cs b/src/Netstr/Messaging/Models/EventKind.cs index dcdbd54..04f94fa 100644 --- a/src/Netstr/Messaging/Models/EventKind.cs +++ b/src/Netstr/Messaging/Models/EventKind.cs @@ -4,6 +4,8 @@ public static class EventKind { public static int UserMetadata = 0; public static int Delete = 5; + public static int RequestToVanish = 62; + public static int GiftWrap = 1059; public static int Auth = 22242; } } diff --git a/src/Netstr/Messaging/Models/User.cs b/src/Netstr/Messaging/Models/User.cs new file mode 100644 index 0000000..36bf28d --- /dev/null +++ b/src/Netstr/Messaging/Models/User.cs @@ -0,0 +1,11 @@ +namespace Netstr.Messaging.Models +{ + public record User + { + public required string PublicKey { get; init; } + + public string? EventId { get; init; } + + public DateTimeOffset? LastVanished { get; init; } + } +} diff --git a/src/Netstr/Messaging/UserCache.cs b/src/Netstr/Messaging/UserCache.cs new file mode 100644 index 0000000..c7b36ae --- /dev/null +++ b/src/Netstr/Messaging/UserCache.cs @@ -0,0 +1,53 @@ +using Netstr.Messaging.Models; +using System.Collections.Concurrent; + +namespace Netstr.Messaging +{ + public interface IUserCache + { + void Initialize(IEnumerable users); + + User SetFromEvent(Event e); + + User? GetByPublicKey(string publicKey); + + User Vanish(string publicKey, DateTimeOffset timestamp); + } + + public class UserCache : IUserCache + { + // Use MemoryCache with CacheItemPolicy NotRemovable for users which vanished? + private readonly ConcurrentDictionary users = new(); + + public User? GetByPublicKey(string publicKey) + { + this.users.TryGetValue(publicKey, out var user); + + return user; + } + + public void Initialize(IEnumerable users) + { + foreach (var user in users) + { + this.users.TryAdd(user.PublicKey, user); + } + } + + public User SetFromEvent(Event e) + { + return this.users.AddOrUpdate( + e.PublicKey, + key => new User { PublicKey = key, EventId = e.Id }, + (key, user) => user with { EventId = e.Id }); + } + + public User Vanish(string publicKey, DateTimeOffset timestamp) + { + return this.users.AddOrUpdate( + publicKey, + key => new User { PublicKey = key, LastVanished = timestamp }, + (key, user) => user with { LastVanished = timestamp }); + } + } +} \ No newline at end of file diff --git a/src/Netstr/Middleware/UserCacheStartupService.cs b/src/Netstr/Middleware/UserCacheStartupService.cs new file mode 100644 index 0000000..3af8261 --- /dev/null +++ b/src/Netstr/Middleware/UserCacheStartupService.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using Netstr.Data; +using Netstr.Messaging; +using Netstr.Messaging.Models; + +namespace Netstr.Middleware +{ + /// + /// Initialize cache when the app starts. + /// + public class UserCacheStartupService : IHostedService + { + private readonly ILogger logger; + private readonly IDbContextFactory db; + private readonly IUserCache cache; + + public UserCacheStartupService + (ILogger logger, + IDbContextFactory db, + IUserCache cache) + { + this.logger = logger; + this.db = db; + this.cache = cache; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + this.logger.LogInformation("Initializing user cache started"); + + using var db = this.db.CreateDbContext(); + + // for each user take their last 'request to vanish' event + var events = await db.Events + .AsNoTracking() + .GroupBy(x => new { x.EventKind, x.EventPublicKey }) + .Where(x => x.Key.EventKind == EventKind.RequestToVanish) + .Select(x => new { x.Key.EventPublicKey, VanishedAt = x.Max(x => x.EventCreatedAt) }) + .ToArrayAsync(cancellationToken); + + var users = events + .Select(x => new User { PublicKey = x.EventPublicKey, LastVanished = x.VanishedAt }) + .ToArray(); + + this.cache.Initialize(users); + + this.logger.LogInformation($"Initializing user cache done with {users.Length} users"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index 3ab34da..7414432 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using Netstr.Data; using Netstr.Extensions; +using Netstr.Middleware; using Netstr.Options; using Netstr.RelayInformation; using Serilog; @@ -22,6 +23,7 @@ .AddApplicationOptions("Limits") .AddApplicationOptions("Auth") .AddMessaging() + .AddHostedService() .AddScoped() .AddDbContextFactory(x => x.UseNpgsql(connectionString)); diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 6e2a1b8..71d0c23 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -50,7 +50,7 @@ "Description": "A nostr relay", "PublicKey": "01eb82fef924e5f8c79abf69cfa5ad5508e784af728403692a8bb5890e7e77b5", "Contact": "bezysoftware@outlook.com", - "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 70 ], + "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 62, 70 ], "Version": "v0.0.0" } } diff --git a/test/Netstr.Tests/Events/EventVerificationTests.cs b/test/Netstr.Tests/Events/EventVerificationTests.cs index 690138a..08b91bd 100644 --- a/test/Netstr.Tests/Events/EventVerificationTests.cs +++ b/test/Netstr.Tests/Events/EventVerificationTests.cs @@ -23,6 +23,7 @@ public EventVerificationTests() .AddOptions().Services .AddLogging() .AddEventValidators() + .AddSingleton() .BuildServiceProvider() .GetRequiredService>(); } diff --git a/test/Netstr.Tests/NIPs/62.feature b/test/Netstr.Tests/NIPs/62.feature new file mode 100644 index 0000000..b4330d5 --- /dev/null +++ b/test/Netstr.Tests/NIPs/62.feature @@ -0,0 +1,109 @@ +Feature: NIP-62 + Nostr-native way to request a complete reset of a key's fingerprint on the web. + This procedure is legally binding in some jurisdictions, and thus, supporters of this NIP should truly delete events from their database. + +Background: + Given a relay is running + And Alice is connected to relay + | PublicKey | PrivateKey | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02 | + And Bob is connected to relay + | PublicKey | PrivateKey | + | 5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627 | 3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29 | + And Charlie is connected to relay + | PublicKey | PrivateKey | + | fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614 | f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a | + +Scenario: Request to Vanish deletes user's data + Only requestor's data is deleted, including GiftWraps where they are tagged + Only events from before the request's createdAt timestamp is deleted + No-one else's events are deleted + When Bob publishes events + | Id | Content | Kind | Tags | CreatedAt | + | 1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643 | Hello | 1 | | 1728905459 | + | bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957 | Hello Later | 1 | | 1728905481 | + | 5c19b5808ee4ad3d31e4129cc112679147e28f3d88e24683a3afa327ba0a2ee8 | DM | 1059 | [["p","5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"]] | 1728905459 | + | 78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f | DM Late | 1059 | [["p","5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"]] | 1728905480 | + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | Hello | 1 | | 1728905459 | + | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | Hello Later | 1 | | 1728905480 | + | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | I'm outta here | 62 | [["relay","ALL_RELAYS"]] | 1728905470 | + And Charlie sends a subscription request abcd + | Authors | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627 | + Then Charlie receives messages + | Type | Id | EventId | + | EVENT | abcd | bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957 | + | EVENT | abcd | 78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f | + | EVENT | abcd | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | + | EVENT | abcd | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | + | EVENT | abcd | 1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643 | + | EOSE | abcd | | + +Scenario: Old events published after Request to Vanish are rejected + After Request to Vanish events older than it cannot be re-published. Newer ones can be published normally. + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | Hello | 1 | | 1728905459 | + | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | I'm outta here | 62 | [["relay","ALL_RELAYS"]] | 1728905470 | + | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | Hello | 1 | | 1728905459 | + | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | Hello Later | 1 | | 1728905480 | + Then Alice receives messages + | Type | EventId | Success | + | OK | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | true | + | OK | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | true | + | OK | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | false | + | OK | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | true | + +Scenario: Deleting Request to Vanish is rejected + Publishing a deletion request event (Kind 5) against a request to vanish has no effect. + Clients and relays are not obliged to support "unrequest vanish" functionality. + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | I'm outta here | 62 | [["relay","ALL_RELAYS"]] | 1728905470 | + | bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a | | 5 | [["e", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e"]] | 1728905471 | + Then Alice receives messages + | Type | EventId | Success | + | OK | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | true | + | OK | bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a | false | + +Scenario: Older Request to Vanish does nothing, newer deletes newer events + First vanish request works as expected. + Second (older) one should be ignored and old events should still be rejetected. + Third (newer) is accepted and its CreatedAt is used to reject old events. + Newer events are still accepted. + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | Hello | 1 | | 1728905459 | + | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | Hello Later | 1 | | 1728905480 | + | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | I'm outta here | 62 | [["relay","ALL_RELAYS"]] | 1728905470 | + | 2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9 | I'm outta here sooner | 62 | [["relay","ALL_RELAYS"]] | 1728905460 | + | 8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173 | Hello | 1 | | 1728905465 | + | e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590 | I'm outta here later | 62 | [["relay","ALL_RELAYS"]] | 1728905490 | + | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | Hello Later | 1 | | 1728905480 | + | e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc | Hello | 1 | | 1728905495 | + Then Alice receives messages + | Type | EventId | Success | + | OK | ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2 | true | + | OK | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | true | + | OK | 9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e | true | + | OK | 2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9 | false | + | OK | 8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173 | false | + | OK | e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590 | true | + | OK | f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd | false | + | OK | e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc | true | + +Scenario: Request to Vanish is ignored when relay tag doesn't match current relay + Event is rejected for missing or incorrect relay tag. + Correct one assumes the connection is on ws://localhost/. Relay should be able to normalize its own URL and the one in tag (e.g. trim ws:// or wss://, trailing / etc) + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | 95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72 | I'm outta here | 62 | | 1728905470 | + | 7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc | I'm outta here | 62 | [["relay","blabla"]] | 1728905470 | + | 845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020 | I'm outta here | 62 | [["relay","ws://localhost/"]] | 1728905470 | + Then Alice receives messages + | Type | EventId | Success | + | OK | 95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72 | false | + | OK | 7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc | false | + | OK | 845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020 | true | \ No newline at end of file diff --git a/test/Netstr.Tests/NIPs/62.feature.cs b/test/Netstr.Tests/NIPs/62.feature.cs new file mode 100644 index 0000000..4659058 --- /dev/null +++ b/test/Netstr.Tests/NIPs/62.feature.cs @@ -0,0 +1,606 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace Netstr.Tests.NIPs +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public partial class NIP_62Feature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "62.feature" +#line hidden + + public NIP_62Feature(NIP_62Feature.FixtureData fixtureData, Netstr_Tests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + this.TestInitialize(); + } + + public static void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-62", "\tNostr-native way to request a complete reset of a key\'s fingerprint on the web. " + + "\r\n\tThis procedure is legally binding in some jurisdictions, and thus, supporters" + + " of this NIP should truly delete events from their database.", ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + public void TestInitialize() + { + } + + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + public virtual void FeatureBackground() + { +#line 5 +#line hidden +#line 6 + testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden + TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table121.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); +#line 7 + testRunner.And("Alice is connected to relay", ((string)(null)), table121, "And "); +#line hidden + TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table122.AddRow(new string[] { + "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", + "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); +#line 10 + testRunner.And("Bob is connected to relay", ((string)(null)), table122, "And "); +#line hidden + TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table123.AddRow(new string[] { + "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", + "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); +#line 13 + testRunner.And("Charlie is connected to relay", ((string)(null)), table123, "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Request to Vanish deletes user\'s data")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-62")] + [Xunit.TraitAttribute("Description", "Request to Vanish deletes user\'s data")] + public void RequestToVanishDeletesUsersData() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Request to Vanish deletes user\'s data", "\tOnly requestor\'s data is deleted, including GiftWraps where they are tagged\r\n\tOn" + + "ly events from before the request\'s createdAt timestamp is deleted\r\n\tNo-one else" + + "\'s events are deleted", tagsOfScenario, argumentsOfScenario, featureTags); +#line 17 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 5 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table124.AddRow(new string[] { + "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643", + "Hello", + "1", + "", + "1728905459"}); + table124.AddRow(new string[] { + "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957", + "Hello Later", + "1", + "", + "1728905481"}); + table124.AddRow(new string[] { + "5c19b5808ee4ad3d31e4129cc112679147e28f3d88e24683a3afa327ba0a2ee8", + "DM", + "1059", + "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", + "1728905459"}); + table124.AddRow(new string[] { + "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f", + "DM Late", + "1059", + "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", + "1728905480"}); +#line 21 + testRunner.When("Bob publishes events", ((string)(null)), table124, "When "); +#line hidden + TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table125.AddRow(new string[] { + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "Hello", + "1", + "", + "1728905459"}); + table125.AddRow(new string[] { + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "Hello Later", + "1", + "", + "1728905480"}); + table125.AddRow(new string[] { + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "I\'m outta here", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905470"}); +#line 27 + testRunner.When("Alice publishes events", ((string)(null)), table125, "When "); +#line hidden + TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { + "Authors"}); + table126.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); +#line 32 + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table126, "And "); +#line hidden + TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table127.AddRow(new string[] { + "EVENT", + "abcd", + "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957"}); + table127.AddRow(new string[] { + "EVENT", + "abcd", + "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f"}); + table127.AddRow(new string[] { + "EVENT", + "abcd", + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd"}); + table127.AddRow(new string[] { + "EVENT", + "abcd", + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e"}); + table127.AddRow(new string[] { + "EVENT", + "abcd", + "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643"}); + table127.AddRow(new string[] { + "EOSE", + "abcd", + ""}); +#line 35 + testRunner.Then("Charlie receives messages", ((string)(null)), table127, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Old events published after Request to Vanish are rejected")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-62")] + [Xunit.TraitAttribute("Description", "Old events published after Request to Vanish are rejected")] + public void OldEventsPublishedAfterRequestToVanishAreRejected() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Old events published after Request to Vanish are rejected", "\tAfter Request to Vanish events older than it cannot be re-published. Newer ones " + + "can be published normally.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 44 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 5 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table128.AddRow(new string[] { + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "Hello", + "1", + "", + "1728905459"}); + table128.AddRow(new string[] { + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "I\'m outta here", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905470"}); + table128.AddRow(new string[] { + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "Hello", + "1", + "", + "1728905459"}); + table128.AddRow(new string[] { + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "Hello Later", + "1", + "", + "1728905480"}); +#line 46 + testRunner.When("Alice publishes events", ((string)(null)), table128, "When "); +#line hidden + TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "EventId", + "Success"}); + table129.AddRow(new string[] { + "OK", + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "true"}); + table129.AddRow(new string[] { + "OK", + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "true"}); + table129.AddRow(new string[] { + "OK", + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "false"}); + table129.AddRow(new string[] { + "OK", + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "true"}); +#line 52 + testRunner.Then("Alice receives messages", ((string)(null)), table129, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Deleting Request to Vanish is rejected")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-62")] + [Xunit.TraitAttribute("Description", "Deleting Request to Vanish is rejected")] + public void DeletingRequestToVanishIsRejected() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Deleting Request to Vanish is rejected", "\tPublishing a deletion request event (Kind 5) against a request to vanish has no " + + "effect. \r\n\tClients and relays are not obliged to support \"unrequest vanish\" func" + + "tionality.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 59 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 5 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table130.AddRow(new string[] { + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "I\'m outta here", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905470"}); + table130.AddRow(new string[] { + "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", + "", + "5", + "[[\"e\", \"9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e\"]]", + "1728905471"}); +#line 62 + testRunner.When("Alice publishes events", ((string)(null)), table130, "When "); +#line hidden + TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "EventId", + "Success"}); + table131.AddRow(new string[] { + "OK", + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "true"}); + table131.AddRow(new string[] { + "OK", + "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", + "false"}); +#line 66 + testRunner.Then("Alice receives messages", ((string)(null)), table131, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Older Request to Vanish does nothing, newer deletes newer events")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-62")] + [Xunit.TraitAttribute("Description", "Older Request to Vanish does nothing, newer deletes newer events")] + public void OlderRequestToVanishDoesNothingNewerDeletesNewerEvents() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Older Request to Vanish does nothing, newer deletes newer events", "\tFirst vanish request works as expected. \r\n\tSecond (older) one should be ignored " + + "and old events should still be rejetected.\r\n\tThird (newer) is accepted and its C" + + "reatedAt is used to reject old events.\r\n\tNewer events are still accepted.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 71 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 5 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table132.AddRow(new string[] { + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "Hello", + "1", + "", + "1728905459"}); + table132.AddRow(new string[] { + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "Hello Later", + "1", + "", + "1728905480"}); + table132.AddRow(new string[] { + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "I\'m outta here", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905470"}); + table132.AddRow(new string[] { + "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", + "I\'m outta here sooner", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905460"}); + table132.AddRow(new string[] { + "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", + "Hello", + "1", + "", + "1728905465"}); + table132.AddRow(new string[] { + "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", + "I\'m outta here later", + "62", + "[[\"relay\",\"ALL_RELAYS\"]]", + "1728905490"}); + table132.AddRow(new string[] { + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "Hello Later", + "1", + "", + "1728905480"}); + table132.AddRow(new string[] { + "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", + "Hello", + "1", + "", + "1728905495"}); +#line 76 + testRunner.When("Alice publishes events", ((string)(null)), table132, "When "); +#line hidden + TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "EventId", + "Success"}); + table133.AddRow(new string[] { + "OK", + "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", + "true"}); + table133.AddRow(new string[] { + "OK", + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "true"}); + table133.AddRow(new string[] { + "OK", + "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", + "true"}); + table133.AddRow(new string[] { + "OK", + "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", + "false"}); + table133.AddRow(new string[] { + "OK", + "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", + "false"}); + table133.AddRow(new string[] { + "OK", + "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", + "true"}); + table133.AddRow(new string[] { + "OK", + "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", + "false"}); + table133.AddRow(new string[] { + "OK", + "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", + "true"}); +#line 86 + testRunner.Then("Alice receives messages", ((string)(null)), table133, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Request to Vanish is ignored when relay tag doesn\'t match current relay")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-62")] + [Xunit.TraitAttribute("Description", "Request to Vanish is ignored when relay tag doesn\'t match current relay")] + public void RequestToVanishIsIgnoredWhenRelayTagDoesntMatchCurrentRelay() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Request to Vanish is ignored when relay tag doesn\'t match current relay", "\tEvent is rejected for missing or incorrect relay tag.\r\n\tCorrect one assumes the " + + "connection is on ws://localhost/. Relay should be able to normalize its own URL " + + "and the one in tag (e.g. trim ws:// or wss://, trailing / etc)", tagsOfScenario, argumentsOfScenario, featureTags); +#line 97 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 5 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table134.AddRow(new string[] { + "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", + "I\'m outta here", + "62", + "", + "1728905470"}); + table134.AddRow(new string[] { + "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", + "I\'m outta here", + "62", + "[[\"relay\",\"blabla\"]]", + "1728905470"}); + table134.AddRow(new string[] { + "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", + "I\'m outta here", + "62", + "[[\"relay\",\"ws://localhost/\"]]", + "1728905470"}); +#line 100 + testRunner.When("Alice publishes events", ((string)(null)), table134, "When "); +#line hidden + TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "EventId", + "Success"}); + table135.AddRow(new string[] { + "OK", + "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", + "false"}); + table135.AddRow(new string[] { + "OK", + "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", + "false"}); + table135.AddRow(new string[] { + "OK", + "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", + "true"}); +#line 105 + testRunner.Then("Alice receives messages", ((string)(null)), table135, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class FixtureData : System.IDisposable + { + + public FixtureData() + { + NIP_62Feature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_62Feature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/70.feature.cs b/test/Netstr.Tests/NIPs/70.feature.cs index 5a27645..4176ea1 100644 --- a/test/Netstr.Tests/NIPs/70.feature.cs +++ b/test/Netstr.Tests/NIPs/70.feature.cs @@ -83,14 +83,14 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table121.AddRow(new string[] { + table136.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table121, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table136, "And "); #line hidden } @@ -120,35 +120,35 @@ public void NotAuthenticatedClientTriesToPublishProtectedEvent() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table122.AddRow(new string[] { + table137.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 13 - testRunner.When("Alice publishes an event", ((string)(null)), table122, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table137, "When "); #line hidden - TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table123.AddRow(new string[] { + table138.AddRow(new string[] { "AUTH", "*", ""}); - table123.AddRow(new string[] { + table138.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "false"}); #line 16 - testRunner.Then("Alice receives messages", ((string)(null)), table123, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table138, "Then "); #line hidden } this.ScenarioCleanup(); @@ -178,39 +178,39 @@ public void AuthenticatedClientPublishesTheirProtectedEvent() #line 23 testRunner.When("Alice publishes an AUTH event for the challenge sent by relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden - TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table124.AddRow(new string[] { + table139.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 24 - testRunner.When("Alice publishes an event", ((string)(null)), table124, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table139, "When "); #line hidden - TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table125.AddRow(new string[] { + table140.AddRow(new string[] { "AUTH", "*", ""}); - table125.AddRow(new string[] { + table140.AddRow(new string[] { "OK", "*", "true"}); - table125.AddRow(new string[] { + table140.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "true"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table125, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table140, "Then "); #line hidden } this.ScenarioCleanup(); @@ -240,14 +240,14 @@ public void AuthenticatedClientTriesToPublishSomeoneElsesProtectedEvent() #line 35 testRunner.When("Alice publishes an AUTH event for the challenge sent by relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden - TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { "Id", "PublicKey", "Content", "Kind", "Tags", "CreatedAt"}); - table126.AddRow(new string[] { + table141.AddRow(new string[] { "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "Protected", @@ -255,26 +255,26 @@ public void AuthenticatedClientTriesToPublishSomeoneElsesProtectedEvent() "[[ \"-\" ]]", "1722337837"}); #line 36 - testRunner.When("Alice publishes an event", ((string)(null)), table126, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table141, "When "); #line hidden - TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table127.AddRow(new string[] { + table142.AddRow(new string[] { "AUTH", "*", ""}); - table127.AddRow(new string[] { + table142.AddRow(new string[] { "OK", "*", "true"}); - table127.AddRow(new string[] { + table142.AddRow(new string[] { "OK", "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "false"}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table127, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table142, "Then "); #line hidden } this.ScenarioCleanup(); From f84d68e285a66f8d975d5eb6fabce5ba37a3c540 Mon Sep 17 00:00:00 2001 From: Tomas Bezouska Date: Tue, 15 Oct 2024 15:19:21 +0200 Subject: [PATCH 2/2] feat(nip62): fix error message --- src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs | 2 +- src/Netstr/Messaging/Messages.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs index 48774bf..cd991c6 100644 --- a/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs @@ -42,7 +42,7 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve // check 'relay' tag matches current url or is set to ALL_RELAYS if (!relays.Any(x => x == path || x == AllRelaysValue)) { - throw new MessageProcessingException(e, string.Format(Messages.AuthRequiredWrongTags, EventTag.Relay)); + throw new MessageProcessingException(e, string.Format(Messages.InvalidWrongTagValue, EventTag.Relay)); } using var db = this.db.CreateDbContext(); diff --git a/src/Netstr/Messaging/Messages.cs b/src/Netstr/Messaging/Messages.cs index ada2967..93b1ef3 100644 --- a/src/Netstr/Messaging/Messages.cs +++ b/src/Netstr/Messaging/Messages.cs @@ -17,6 +17,7 @@ public static class Messages public const string InvalidTooManyTags = "invalid: too many tags"; public const string InvalidCannotDelete = "invalid: cannot delete deletions and someone else's events"; public const string InvalidDeletedEvent = "invalid: this event was already deleted"; + public const string InvalidWrongTagValue = "invalid: this event has an unexpected value of tag {0}"; public const string AuthRequired = "auth-required: we only allow publishing and subscribing to authenticated clients"; public const string AuthRequiredProtected = "auth-required: this event may only be published by its author"; public const string AuthRequiredPublishing = "auth-required: we only allow publishing to authenticated clients";