From ce945ce58af292c96e046324fa4cb4ed316fe054 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:06:31 -0400 Subject: [PATCH 01/49] feat: add NIP-57 zap handlers and validation - add dedicated zap request/receipt event handler and validator - extend event kinds/tags with zap-specific constants and helper extensions - add NIP-57 feature scenarios and implementation notes --- NIP57Zaps.md | 103 ++++++++++++++++++ src/Netstr/Extensions/MessagingExtensions.cs | 4 +- .../Events/Handlers/ZapEventHandler.cs | 65 +++++++++++ .../Events/Validators/ZapEventValidator.cs | 45 ++++++++ src/Netstr/Messaging/Models/EventKind.cs | 6 +- src/Netstr/Messaging/Models/EventTag.cs | 8 ++ .../Messaging/Models/ZapEventExtensions.cs | 45 ++++++++ test/Netstr.Tests/NIPs/57.feature | 29 +++++ 8 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 NIP57Zaps.md create mode 100644 src/Netstr/Messaging/Events/Handlers/ZapEventHandler.cs create mode 100644 src/Netstr/Messaging/Events/Validators/ZapEventValidator.cs create mode 100644 src/Netstr/Messaging/Models/ZapEventExtensions.cs create mode 100644 test/Netstr.Tests/NIPs/57.feature diff --git a/NIP57Zaps.md b/NIP57Zaps.md new file mode 100644 index 0000000..465dd0a --- /dev/null +++ b/NIP57Zaps.md @@ -0,0 +1,103 @@ +# NIP-57: Lightning Zaps Implementation + +This document describes the implementation of [NIP-57 Lightning Zaps](https://github.com/nostr-protocol/nips/blob/master/57.md) in the Netstr relay. + +## Overview + +NIP-57 defines two new event types for recording lightning payments between users: +- **Zap Request (Kind 9734)**: Represents a payer's request to a recipient's lightning wallet for an invoice +- **Zap Receipt (Kind 9735)**: Represents confirmation that an invoice has been paid + +## Implementation Details + +### Event Kinds + +Two new event kinds have been added to the `EventKind` enum: +```csharp +// NIP-57 Lightning Zaps +ZapRequest = 9734, +ZapReceipt = 9735, +``` + +### Event Tags + +New tag constants have been added to the `EventTag` class: +```csharp +// NIP-57 Zap tags +public const string Amount = "amount"; +public const string Bolt11 = "bolt11"; +public const string Description = "description"; +public const string Preimage = "preimage"; +public const string Lnurl = "lnurl"; +public const string Relays = "relays"; +``` + +### Validation + +A new `ZapEventValidator` class has been created to validate Zap events: +- For Zap Requests (9734), it validates the presence of required tags: `p` (recipient) and `relays` +- For Zap Receipts (9735), it validates the presence of required tags: `p` (recipient), `bolt11`, and `description` + +### Event Handling + +A new `ZapEventHandler` class has been created to handle Zap events. Unlike NIP-51 list events, Zap events are not replaceable or addressable, so they are handled as regular events with the following flow: +1. Check if the event has been deleted +2. Check for duplicates +3. Save the event to the database +4. Send OK response to the client +5. Broadcast the event to other clients + +### Extension Methods + +A set of extension methods have been added in the `ZapEventExtensions` class to make working with Zap events easier: +- `IsZapRequest(this Event e)`: Determines if the event is a Zap Request +- `IsZapReceipt(this Event e)`: Determines if the event is a Zap Receipt +- `GetRecipientPubkey(this Event e)`: Gets the recipient's public key +- `GetBolt11(this Event e)`: Gets the bolt11 invoice +- `GetAmount(this Event e)`: Gets the amount in millisats +- `GetRelayUrls(this Event e)`: Gets the relay URLs from a Zap Request + +## Testing + +Tests for NIP-57 have been added in `test/Netstr.Tests/NIPs/57.feature` to verify: +1. Creating and retrieving Zap Requests +2. Creating and retrieving Zap Receipts + +## Protocol Flow + +The complete protocol flow for NIP-57 is as follows: + +1. Client calculates a recipient's lnurl pay request url from the zap tag on the event being zapped, or from the recipient's profile. +2. Client sends a GET request to this url and parses the response. +3. When a user wants to send a zap, the client creates a zap request event (kind 9734). +4. Instead of publishing the zap request, it's sent to the recipient's lnurl pay callback url. +5. The recipient's lnurl server validates the zap request. +6. If valid, the server returns an invoice where the description is the zap request note. +7. The client pays the invoice. +8. Once paid, the recipient's lnurl server generates a zap receipt (kind 9735) and publishes it to the relays specified in the zap request. +9. Clients can fetch zap receipts on posts and profiles, and validate them. + +## Comparison with NIP-51 Implementation + +While NIP-51 and NIP-57 serve different purposes, the implementation approach is similar: + +1. **Event Validation**: Both require specific validators to check for required tags +2. **Event Handling**: + - NIP-51 uses replaceable/addressable event handlers + - NIP-57 uses a regular event handler (not replaceable) +3. **Database Storage**: Both store events with their tags in the same database structure +4. **Tag Handling**: Both require specific tag validation and processing + +## Key Differences from NIP-51 + +1. **Event Types**: + - NIP-51: Replaceable (10000-10999) or Addressable (30000-30999) + - NIP-57: Regular events (9734, 9735) + +2. **Replacement Logic**: + - NIP-51: Events can be replaced based on pubkey+kind or pubkey+kind+d-tag + - NIP-57: Events are not replaceable, each zap request/receipt is unique + +3. **Tag Requirements**: + - NIP-51: Various tag requirements based on list type + - NIP-57: Specific tag requirements for zap requests and receipts diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 990f6a5..84fee30 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -1,4 +1,4 @@ -using Netstr.Messaging; +using Netstr.Messaging; using Netstr.Messaging.Events; using Netstr.Messaging.Events.Handlers; using Netstr.Messaging.Events.Handlers.Replaceable; @@ -48,6 +48,7 @@ public static IServiceCollection AddMessaging(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // RegularEventHandler needs to go last services.AddSingleton(); @@ -68,6 +69,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/ZapEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/ZapEventHandler.cs new file mode 100644 index 0000000..f5a5814 --- /dev/null +++ b/src/Netstr/Messaging/Events/Handlers/ZapEventHandler.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Netstr.Data; +using Netstr.Messaging.Models; +using Netstr.Options; + +namespace Netstr.Messaging.Events.Handlers +{ + /// + /// Handles NIP-57 Zap events (ZapRequest and ZapReceipt). + /// + public class ZapEventHandler : EventHandlerBase + { + private readonly IDbContextFactory db; + + public ZapEventHandler( + ILogger logger, + IOptions auth, + IWebSocketAdapterCollection adapters, + IDbContextFactory db) + : base(logger, auth, adapters) + { + this.db = db; + } + + public override bool CanHandleEvent(Event e) => + e.Kind == (long)EventKind.ZapRequest || e.Kind == (long)EventKind.ZapReceipt; + + protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Event e) + { + using var db = this.db.CreateDbContext(); + + if (await db.Events.IsDeleted(e.Id)) + { + this.logger.LogInformation($"Event {e.Id} was already deleted"); + sender.SendNotOk(e.Id, Messages.InvalidDeletedEvent); + return; + } + + var newEntity = e.ToEntity(DateTimeOffset.UtcNow); + + // Check for duplicates + var existing = await db.Events + .AsNoTracking() + .Where(x => x.EventId == e.Id) + .FirstOrDefaultAsync(); + + if (existing != null) + { + this.logger.LogInformation($"Event {e.Id} already exists"); + sender.SendOk(e.Id); // Still return OK for duplicates + return; + } + + db.Add(newEntity); + await db.SaveChangesAsync(); + + // Reply + sender.SendOk(e.Id); + + // Broadcast + BroadcastEvent(e); + } + } +} diff --git a/src/Netstr/Messaging/Events/Validators/ZapEventValidator.cs b/src/Netstr/Messaging/Events/Validators/ZapEventValidator.cs new file mode 100644 index 0000000..9675530 --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/ZapEventValidator.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using Netstr.Messaging.Models; +using Netstr.Options; +using System.Linq; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Validates NIP-57 Zap events. + /// + public class ZapEventValidator : IEventValidator + { + private const string InvalidZapRequestTags = "invalid: zap request missing required tags"; + private const string InvalidZapReceiptTags = "invalid: zap receipt missing required tags"; + + public string? Validate(Event e, ClientContext context) + { + return (EventKind)e.Kind switch + { + EventKind.ZapRequest => ValidateZapRequest(e), + EventKind.ZapReceipt => ValidateZapReceipt(e), + _ => null // Not a zap event + }; + } + + private static string? ValidateZapRequest(Event e) + { + // Validate required tags: p (recipient), relays + bool hasRecipient = e.Tags.Any(t => t.Length > 0 && t[0] == EventTag.PublicKey); + bool hasRelays = e.Tags.Any(t => t.Length > 0 && t[0] == EventTag.Relays); + + return (hasRecipient && hasRelays) ? null : InvalidZapRequestTags; + } + + private static string? ValidateZapReceipt(Event e) + { + // Validate required tags: p (recipient), bolt11, description + bool hasRecipient = e.Tags.Any(t => t.Length > 0 && t[0] == EventTag.PublicKey); + bool hasBolt11 = e.Tags.Any(t => t.Length > 0 && t[0] == EventTag.Bolt11); + bool hasDescription = e.Tags.Any(t => t.Length > 0 && t[0] == EventTag.Description); + + return (hasRecipient && hasBolt11 && hasDescription) ? null : InvalidZapReceiptTags; + } + } +} diff --git a/src/Netstr/Messaging/Models/EventKind.cs b/src/Netstr/Messaging/Models/EventKind.cs index d2b4f97..7606044 100644 --- a/src/Netstr/Messaging/Models/EventKind.cs +++ b/src/Netstr/Messaging/Models/EventKind.cs @@ -1,4 +1,4 @@ -namespace Netstr.Messaging.Models +namespace Netstr.Messaging.Models { /// /// Represents the different kinds of events in the NOSTR protocol. @@ -11,6 +11,10 @@ public enum EventKind RequestToVanish = 62, GiftWrap = 1059, Auth = 22242, + + // NIP-57 Lightning Zaps + ZapRequest = 9734, + ZapReceipt = 9735, // NIP-51 Standard Lists (10000-10999) MuteList = 10000, diff --git a/src/Netstr/Messaging/Models/EventTag.cs b/src/Netstr/Messaging/Models/EventTag.cs index 78990c6..216fe8b 100644 --- a/src/Netstr/Messaging/Models/EventTag.cs +++ b/src/Netstr/Messaging/Models/EventTag.cs @@ -11,5 +11,13 @@ public static class EventTag public const string Relay = "r"; public const string Protected = "-"; public const string Expiration = "expiration"; + + // NIP-57 Zap tags + public const string Amount = "amount"; + public const string Bolt11 = "bolt11"; + public const string Description = "description"; + public const string Preimage = "preimage"; + public const string Lnurl = "lnurl"; + public const string Relays = "relays"; } } diff --git a/src/Netstr/Messaging/Models/ZapEventExtensions.cs b/src/Netstr/Messaging/Models/ZapEventExtensions.cs new file mode 100644 index 0000000..4cf8cbf --- /dev/null +++ b/src/Netstr/Messaging/Models/ZapEventExtensions.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Netstr.Messaging.Models +{ + /// + /// Extension methods for working with NIP-57 Zap events. + /// + public static class ZapEventExtensions + { + /// + /// Determines if the event is a Zap Request. + /// + public static bool IsZapRequest(this Event e) => e.Kind == (long)EventKind.ZapRequest; + + /// + /// Determines if the event is a Zap Receipt. + /// + public static bool IsZapReceipt(this Event e) => e.Kind == (long)EventKind.ZapReceipt; + + /// + /// Gets the recipient's public key from a Zap event. + /// + public static string? GetRecipientPubkey(this Event e) => + e.Tags.FirstOrDefault(t => t.Length > 1 && t[0] == EventTag.PublicKey)?[1]; + + /// + /// Gets the bolt11 invoice from a Zap Receipt event. + /// + public static string? GetBolt11(this Event e) => + e.Tags.FirstOrDefault(t => t.Length > 1 && t[0] == EventTag.Bolt11)?[1]; + + /// + /// Gets the amount in millisats from a Zap event. + /// + public static string? GetAmount(this Event e) => + e.Tags.FirstOrDefault(t => t.Length > 1 && t[0] == EventTag.Amount)?[1]; + + /// + /// Gets the relay URLs from a Zap Request event. + /// + public static IEnumerable GetRelayUrls(this Event e) => + e.Tags.FirstOrDefault(t => t.Length > 1 && t[0] == EventTag.Relays)?.Skip(1) ?? Array.Empty(); + } +} diff --git a/test/Netstr.Tests/NIPs/57.feature b/test/Netstr.Tests/NIPs/57.feature new file mode 100644 index 0000000..713d474 --- /dev/null +++ b/test/Netstr.Tests/NIPs/57.feature @@ -0,0 +1,29 @@ +Feature: NIP-57 Lightning Zaps + Tests for NIP-57 Lightning Zaps implementation + + Background: + Given a relay at "wss://localhost:5001" + And a user Alice + And Alice is connected to the relay + + Scenario: Create and retrieve a zap request + When Alice publishes an event with kind 9734 and tags: + | relays | wss://relay1.example.com | wss://relay2.example.com | + | amount | 21000 | + | lnurl | lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp | + | p | 04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9 | + Then the relay accepts the event + When Alice subscribes to events with kind 9734 + Then Alice receives 1 event + And the event has tag "p" with value "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9" + + Scenario: Create and retrieve a zap receipt + When Alice publishes an event with kind 9735 and tags: + | p | 32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245 | + | bolt11 | lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0 | + | description | {"pubkey":"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245","content":"","id":"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d","created_at":1674164539,"sig":"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d","kind":9734,"tags":[["e","3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"],["p","32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],["relays","wss://relay.damus.io","wss://nostr-relay.wlvs.space","wss://nostr.fmt.wiz.biz","wss://relay.nostr.bg","wss://nostr.oxtr.dev","wss://nostr.v0l.io","wss://brb.io","wss://nostr.bitcoiner.social","ws://monad.jb55.com:8080","wss://relay.snort.social"]]} | + | preimage | 5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f | + Then the relay accepts the event + When Alice subscribes to events with kind 9735 + Then Alice receives 1 event + And the event has tag "bolt11" with value starting with "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0" From b036b63794f6b143b5ce311c8902c2deea5839a2 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 19:23:17 -0400 Subject: [PATCH 02/49] feat: add public-key whitelist validation for publish and subscribe - introduce whitelist options, event/subscription validators, and error messaging - wire whitelist validators into dependency registration and configuration - add whitelist docs and unit/integration test coverage --- README.md | 4 + docs/Whitelist.md | 115 +++++++++++++ .../Data/DbUpdateExceptionExtensions.cs | 3 +- src/Netstr/Data/EntityMapping.cs | 15 +- src/Netstr/Extensions/MessagingExtensions.cs | 2 + src/Netstr/Extensions/OptionsExtensions.cs | 3 +- .../Events/Validators/WhitelistValidator.cs | 45 +++++ src/Netstr/Messaging/Messages.cs | 1 + .../WhitelistSubscriptionValidator.cs | 51 ++++++ src/Netstr/Options/WhitelistOptions.cs | 25 +++ src/Netstr/appsettings.json | 14 +- .../Events/WhitelistValidatorTests.cs | 125 ++++++++++++++ .../WhitelistSubscriptionValidatorTests.cs | 132 ++++++++++++++ test/Netstr.Tests/WhitelistTests.cs | 162 ++++++++++++++++++ 14 files changed, 684 insertions(+), 13 deletions(-) create mode 100644 docs/Whitelist.md create mode 100644 src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs create mode 100644 src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs create mode 100644 src/Netstr/Options/WhitelistOptions.cs create mode 100644 test/Netstr.Tests/Events/WhitelistValidatorTests.cs create mode 100644 test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs create mode 100644 test/Netstr.Tests/WhitelistTests.cs diff --git a/README.md b/README.md index 4bfaa65..2c58511 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ NIPs with a relay-specific implementation are listed here. - [x] NIP-77: [Negentropy syncing](https://github.com/nostr-protocol/nips/pull/1494) - [x] NIP-119: [AND operator for filters](https://github.com/nostr-protocol/nips/pull/1365) +## Additional Features + +- [x] **Public Key Whitelist**: Restrict which public keys can publish events and/or subscribe to your relay. [Learn more](docs/Whitelist.md) + ## Tests Each supported NIP has a set of tests written in [Specflow / Gherkin language](https://docs.specflow.org/projects/specflow/en/latest/Gherkin/Gherkin-Reference.html). diff --git a/docs/Whitelist.md b/docs/Whitelist.md new file mode 100644 index 0000000..8866d7a --- /dev/null +++ b/docs/Whitelist.md @@ -0,0 +1,115 @@ +# Public Key Whitelist + +The Netstr relay supports a whitelist feature that allows you to restrict which public keys can interact with your relay. This document explains how to configure and use this feature. + +## Overview + +The whitelist feature allows you to: + +1. Restrict which public keys can publish events to your relay +2. Optionally restrict which public keys can subscribe to events from your relay +3. Enable or disable the whitelist feature without changing your configuration + +## Configuration + +The whitelist is configured in the `appsettings.json` and `appsettings.Development.json` files under the `Whitelist` section: + +```json +"Whitelist": { + "Enabled": true, + "AllowedPublicKeys": [ + "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9" + ], + "RestrictPublishing": true, + "RestrictSubscribing": false +} +``` + +### Configuration Options + +- `Enabled`: When set to `true`, the whitelist feature is active. When set to `false`, the whitelist is ignored and all public keys are allowed. +- `AllowedPublicKeys`: An array of public keys that are allowed to interact with the relay. +- `RestrictPublishing`: When set to `true`, only whitelisted public keys can publish events to the relay. +- `RestrictSubscribing`: When set to `true`, only whitelisted public keys can subscribe to events from the relay. + +## How It Works + +### Publishing Events + +When a client attempts to publish an event to the relay: + +1. If `Enabled` is `false`, the event is accepted (subject to other validation rules). +2. If `RestrictPublishing` is `false`, the event is accepted (subject to other validation rules). +3. If the event's public key is in the `AllowedPublicKeys` list, the event is accepted (subject to other validation rules). +4. Otherwise, the event is rejected with the message: `restricted: your public key is not in the whitelist`. + +### Subscribing to Events + +When a client attempts to subscribe to events from the relay: + +1. If `Enabled` is `false`, the subscription is accepted (subject to other validation rules). +2. If `RestrictSubscribing` is `false`, the subscription is accepted (subject to other validation rules). +3. If the client is not authenticated, the subscription is rejected with the message: `auth-required: authentication required for subscription`. +4. If the client's public key is in the `AllowedPublicKeys` list, the subscription is accepted (subject to other validation rules). +5. Otherwise, the subscription is rejected with the message: `restricted: your public key is not in the whitelist`. + +## Authentication Requirement + +For subscription restrictions to work, clients must authenticate using the `AUTH` message as defined in [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md). This is because the relay needs to know the client's public key to check against the whitelist. + +## Interaction with Auth Mode + +The whitelist feature works alongside the existing authentication modes: + +- If `Auth.Mode` is set to `Always` or `Publishing`, clients must still authenticate regardless of the whitelist settings. +- If `Auth.Mode` is set to `WhenNeeded` or `Disabled`, clients only need to authenticate if they want to subscribe and `Whitelist.RestrictSubscribing` is `true`. + +## Best Practices + +1. **Start with a restrictive configuration**: Enable the whitelist with a small set of trusted public keys. +2. **Monitor logs**: The relay logs when events or subscriptions are rejected due to whitelist restrictions. +3. **Consider your use case**: For private relays, you might want to restrict both publishing and subscribing. For public relays that want to limit spam, you might only want to restrict publishing. + +## Example Configurations + +### Private Relay + +```json +"Whitelist": { + "Enabled": true, + "AllowedPublicKeys": [ + "pubkey1", + "pubkey2", + "pubkey3" + ], + "RestrictPublishing": true, + "RestrictSubscribing": true +} +``` + +### Anti-Spam Configuration + +```json +"Whitelist": { + "Enabled": true, + "AllowedPublicKeys": [ + "pubkey1", + "pubkey2", + "pubkey3" + ], + "RestrictPublishing": true, + "RestrictSubscribing": false +} +``` + +### Disabled Whitelist + +```json +"Whitelist": { + "Enabled": false, + "AllowedPublicKeys": [], + "RestrictPublishing": true, + "RestrictSubscribing": false +} +``` diff --git a/src/Netstr/Data/DbUpdateExceptionExtensions.cs b/src/Netstr/Data/DbUpdateExceptionExtensions.cs index f42325e..d7f926e 100644 --- a/src/Netstr/Data/DbUpdateExceptionExtensions.cs +++ b/src/Netstr/Data/DbUpdateExceptionExtensions.cs @@ -7,7 +7,8 @@ public static class DbUpdateExceptionExtensions private static readonly string[] UniqueIndexNames = [ "UNIQUE", NetstrDbContext.EventIdIndexName, - NetstrDbContext.ReplaceableUniqueIndexName + NetstrDbContext.ReplaceableUniqueIndexName, + NetstrDbContext.TagValueIndexName ]; public static bool IsUniqueIndexViolation(this DbUpdateException exception) diff --git a/src/Netstr/Data/EntityMapping.cs b/src/Netstr/Data/EntityMapping.cs index b9ad3a7..bd42c3d 100644 --- a/src/Netstr/Data/EntityMapping.cs +++ b/src/Netstr/Data/EntityMapping.cs @@ -19,12 +19,15 @@ public static EventEntity ToEntity(this Event e, DateTimeOffset firstSeen) EventDeduplication = e.IsAddressable() ? e.GetDeduplicationValue() : null, - Tags = e.Tags.Select(x => new TagEntity - { - Name = x.First(), - Value = x.Skip(1).FirstOrDefault(), - OtherValues = x.Skip(2).ToArray() - }).ToArray(), + Tags = e.Tags + .GroupBy(x => new { Name = x.First(), Value = x.Skip(1).FirstOrDefault() }) + .Select(g => new TagEntity + { + Name = g.Key.Name, + Value = g.Key.Value, + OtherValues = g.First().Skip(2).ToArray() + }) + .ToArray(), }; } } diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 84fee30..878d6ae 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -70,6 +70,7 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } @@ -78,6 +79,7 @@ public static IServiceCollection AddSubscriptionValidators(this IServiceCollecti services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Netstr/Extensions/OptionsExtensions.cs b/src/Netstr/Extensions/OptionsExtensions.cs index 2150ef0..a3ac867 100644 --- a/src/Netstr/Extensions/OptionsExtensions.cs +++ b/src/Netstr/Extensions/OptionsExtensions.cs @@ -21,7 +21,8 @@ public static IServiceCollection AddApplicationsOptions(this IServiceCollection .AddApplicationOptions("RelayInformation") .AddApplicationOptions("Limits") .AddApplicationOptions("Auth") - .AddApplicationOptions("Cleanup"); + .AddApplicationOptions("Cleanup") + .AddApplicationOptions("Whitelist"); } } } diff --git a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs new file mode 100644 index 0000000..f52cefe --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using Netstr.Messaging.Models; +using Netstr.Options; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Validates that the event's public key is in the whitelist if whitelist is enabled. + /// + public class WhitelistValidator : IEventValidator + { + private readonly ILogger logger; + private readonly IOptions options; + private readonly HashSet allowedPublicKeys; + + public WhitelistValidator( + ILogger logger, + IOptions options) + { + this.logger = logger; + this.options = options; + this.allowedPublicKeys = new HashSet( + options.Value.AllowedPublicKeys ?? Array.Empty(), + StringComparer.OrdinalIgnoreCase); + } + + public string? Validate(Event e, ClientContext context) + { + var whitelistOptions = this.options.Value; + + if (!whitelistOptions.Enabled || !whitelistOptions.RestrictPublishing) + { + return null; + } + + if (!this.allowedPublicKeys.Contains(e.PublicKey)) + { + this.logger.LogWarning($"Rejected event from non-whitelisted public key: {e.PublicKey}"); + return Messages.WhitelistRestricted; + } + + return null; + } + } +} diff --git a/src/Netstr/Messaging/Messages.cs b/src/Netstr/Messaging/Messages.cs index 40e6214..a1cc8fe 100644 --- a/src/Netstr/Messaging/Messages.cs +++ b/src/Netstr/Messaging/Messages.cs @@ -31,6 +31,7 @@ public static class Messages public const string PowNoMatch = "pow: difficulty {0} doesn't match target of {1}"; public const string UnsupportedFilter = "unsupported: filter contains unknown elements"; public const string RateLimited = "rate-limited: slow down there chief"; + public const string WhitelistRestricted = "restricted: your public key is not in the whitelist"; public const string CannotParseMessage = "unable to parse the message"; public const string CannotProcessMessageType = "unknown message type"; diff --git a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs new file mode 100644 index 0000000..b6022aa --- /dev/null +++ b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Options; +using Netstr.Messaging.Models; +using Netstr.Options; + +namespace Netstr.Messaging.Subscriptions.Validators +{ + /// + /// Validates that the subscriber's public key is in the whitelist if whitelist is enabled. + /// + public class WhitelistSubscriptionValidator : ISubscriptionRequestValidator + { + private readonly ILogger logger; + private readonly IOptions options; + private readonly HashSet allowedPublicKeys; + + public WhitelistSubscriptionValidator( + ILogger logger, + IOptions options) + { + this.logger = logger; + this.options = options; + this.allowedPublicKeys = new HashSet( + options.Value.AllowedPublicKeys ?? Array.Empty(), + StringComparer.OrdinalIgnoreCase); + } + + public string? Validate(ClientContext context, SubscriptionFilter[] filters) + { + var whitelistOptions = this.options.Value; + + if (!whitelistOptions.Enabled || !whitelistOptions.RestrictSubscribing) + { + return null; + } + + // If client is not authenticated, we can't check the public key + if (!context.IsAuthenticated()) + { + return "auth-required: authentication required for subscription"; + } + + if (!this.allowedPublicKeys.Contains(context.PublicKey)) + { + this.logger.LogWarning($"Rejected subscription from non-whitelisted public key: {context.PublicKey}"); + return Messages.WhitelistRestricted; + } + + return null; + } + } +} diff --git a/src/Netstr/Options/WhitelistOptions.cs b/src/Netstr/Options/WhitelistOptions.cs new file mode 100644 index 0000000..1d4ba62 --- /dev/null +++ b/src/Netstr/Options/WhitelistOptions.cs @@ -0,0 +1,25 @@ +namespace Netstr.Options +{ + public record WhitelistOptions + { + /// + /// Whether the whitelist is enabled. + /// + public bool Enabled { get; init; } = false; + + /// + /// List of public keys that are allowed to interact with the relay. + /// + public string[] AllowedPublicKeys { get; init; } = []; + + /// + /// Whether to apply the whitelist to publishing events. + /// + public bool RestrictPublishing { get; init; } = true; + + /// + /// Whether to apply the whitelist to subscribing. + /// + public bool RestrictSubscribing { get; init; } = false; + } +} diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index ce0bbcd..19ce5c5 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -24,8 +24,8 @@ "WebSocketsPath": "/" }, "Auth": { - "Mode": "WhenNeeded", - "ProtectedKinds": [ 4, 1059 ] + "Mode": "Publishing", + "ProtectedKinds": [] }, "Limits": { "MaxPayloadSize": 524288, @@ -77,9 +77,13 @@ "Description": "A nostr relay", "PublicKey": "NA", "Contact": "NA", - "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 51, 62, 65, 70, 77, 119 ], + "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 51, 57, 62, 65, 70, 77, 119 ], "Version": "v2.0.1" + }, + "Whitelist": { + "Enabled": false, + "AllowedPublicKeys": [], + "RestrictPublishing": true, + "RestrictSubscribing": false } } - - diff --git a/test/Netstr.Tests/Events/WhitelistValidatorTests.cs b/test/Netstr.Tests/Events/WhitelistValidatorTests.cs new file mode 100644 index 0000000..b22f269 --- /dev/null +++ b/test/Netstr.Tests/Events/WhitelistValidatorTests.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Netstr.Messaging; +using Netstr.Messaging.Events.Validators; +using Netstr.Messaging.Models; +using Netstr.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Netstr.Tests.Events +{ + public class WhitelistValidatorTests + { + private readonly Mock> loggerMock; + private readonly Mock> optionsMock; + private readonly WhitelistOptions options; + private readonly WhitelistValidator validator; + + public WhitelistValidatorTests() + { + loggerMock = new Mock>(); + optionsMock = new Mock>(); + options = new WhitelistOptions + { + Enabled = true, + AllowedPublicKeys = new[] { "allowed_pubkey1", "allowed_pubkey2" }, + RestrictPublishing = true, + RestrictSubscribing = true + }; + optionsMock.Setup(x => x.Value).Returns(options); + validator = new WhitelistValidator(loggerMock.Object, optionsMock.Object); + } + + [Fact] + public void Validate_WhitelistDisabled_ReturnsNull() + { + // Arrange + options.Enabled = false; + var e = CreateEvent("not_allowed_pubkey"); + var context = new ClientContext("client1", "127.0.0.1"); + + // Act + var result = validator.Validate(e, context); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_RestrictPublishingDisabled_ReturnsNull() + { + // Arrange + options.RestrictPublishing = false; + var e = CreateEvent("not_allowed_pubkey"); + var context = new ClientContext("client1", "127.0.0.1"); + + // Act + var result = validator.Validate(e, context); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_AllowedPublicKey_ReturnsNull() + { + // Arrange + var e = CreateEvent("allowed_pubkey1"); + var context = new ClientContext("client1", "127.0.0.1"); + + // Act + var result = validator.Validate(e, context); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_NotAllowedPublicKey_ReturnsError() + { + // Arrange + var e = CreateEvent("not_allowed_pubkey"); + var context = new ClientContext("client1", "127.0.0.1"); + + // Act + var result = validator.Validate(e, context); + + // Assert + Assert.Equal(Messages.WhitelistRestricted, result); + } + + [Fact] + public void Validate_CaseInsensitiveMatch_ReturnsNull() + { + // Arrange + var e = CreateEvent("ALLOWED_PUBKEY1"); + var context = new ClientContext("client1", "127.0.0.1"); + + // Act + var result = validator.Validate(e, context); + + // Assert + Assert.Null(result); + } + + private Event CreateEvent(string publicKey) + { + return new Event + { + Id = "event_id", + PublicKey = publicKey, + Kind = 1, + Tags = Array.Empty(), + Content = "content", + Signature = "signature", + CreatedAt = DateTimeOffset.UtcNow + }; + } + } +} diff --git a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs new file mode 100644 index 0000000..5921c88 --- /dev/null +++ b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs @@ -0,0 +1,132 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Netstr.Messaging; +using Netstr.Messaging.Models; +using Netstr.Messaging.Subscriptions.Validators; +using Netstr.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Netstr.Tests.Subscriptions +{ + public class WhitelistSubscriptionValidatorTests + { + private readonly Mock> loggerMock; + private readonly Mock> optionsMock; + private readonly WhitelistOptions options; + private readonly WhitelistSubscriptionValidator validator; + + public WhitelistSubscriptionValidatorTests() + { + loggerMock = new Mock>(); + optionsMock = new Mock>(); + options = new WhitelistOptions + { + Enabled = true, + AllowedPublicKeys = new[] { "allowed_pubkey1", "allowed_pubkey2" }, + RestrictPublishing = true, + RestrictSubscribing = true + }; + optionsMock.Setup(x => x.Value).Returns(options); + validator = new WhitelistSubscriptionValidator(loggerMock.Object, optionsMock.Object); + } + + [Fact] + public void Validate_WhitelistDisabled_ReturnsNull() + { + // Arrange + options.Enabled = false; + var context = CreateAuthenticatedContext("not_allowed_pubkey"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_RestrictSubscribingDisabled_ReturnsNull() + { + // Arrange + options.RestrictSubscribing = false; + var context = CreateAuthenticatedContext("not_allowed_pubkey"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_NotAuthenticated_ReturnsAuthRequiredError() + { + // Arrange + var context = new ClientContext("client1", "127.0.0.1"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Equal("auth-required: authentication required for subscription", result); + } + + [Fact] + public void Validate_AllowedPublicKey_ReturnsNull() + { + // Arrange + var context = CreateAuthenticatedContext("allowed_pubkey1"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Validate_NotAllowedPublicKey_ReturnsError() + { + // Arrange + var context = CreateAuthenticatedContext("not_allowed_pubkey"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Equal(Messages.WhitelistRestricted, result); + } + + [Fact] + public void Validate_CaseInsensitiveMatch_ReturnsNull() + { + // Arrange + var context = CreateAuthenticatedContext("ALLOWED_PUBKEY1"); + var filters = Array.Empty(); + + // Act + var result = validator.Validate(context, filters); + + // Assert + Assert.Null(result); + } + + private ClientContext CreateAuthenticatedContext(string publicKey) + { + var context = new ClientContext("client1", "127.0.0.1"); + context.Authenticate(publicKey); + return context; + } + } +} diff --git a/test/Netstr.Tests/WhitelistTests.cs b/test/Netstr.Tests/WhitelistTests.cs new file mode 100644 index 0000000..114af73 --- /dev/null +++ b/test/Netstr.Tests/WhitelistTests.cs @@ -0,0 +1,162 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Netstr.Messaging; +using Netstr.Messaging.Models; +using Netstr.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace Netstr.Tests +{ + public class WhitelistTests : IClassFixture + { + private readonly WebApplicationFactory factory; + + public WhitelistTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Fact] + public async Task WhitelistedPublicKey_CanPublishEvents() + { + // Arrange + var options = new WhitelistOptions + { + Enabled = true, + AllowedPublicKeys = new[] { Alice.PublicKey }, + RestrictPublishing = true, + RestrictSubscribing = false + }; + + using var client = factory.CreateClient(); + using var ws = await client.ConnectWebSocketAsync(); + + // Override the whitelist options for this test + factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; + factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; + factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; + factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + + // Act + var e = Alice.CreateEvent(1, "Hello from whitelisted user"); + await ws.SendEventAsync(e); + + // Assert + var response = await ws.ReceiveMessageAsync(); + var okMessage = JsonDocument.Parse(response); + var messageType = okMessage.RootElement[0].GetString(); + var eventId = okMessage.RootElement[1].GetString(); + var success = okMessage.RootElement[2].GetBoolean(); + + Assert.Equal("OK", messageType); + Assert.Equal(e.Id, eventId); + Assert.True(success); + } + + [Fact] + public async Task NonWhitelistedPublicKey_CannotPublishEvents() + { + // Arrange + var options = new WhitelistOptions + { + Enabled = true, + AllowedPublicKeys = new[] { Alice.PublicKey }, + RestrictPublishing = true, + RestrictSubscribing = false + }; + + using var client = factory.CreateClient(); + using var ws = await client.ConnectWebSocketAsync(); + + // Override the whitelist options for this test + factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; + factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; + factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; + factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + + // Act + var e = new Event + { + Id = "non_whitelisted_event_id", + PublicKey = "non_whitelisted_pubkey", + Kind = 1, + Tags = Array.Empty(), + Content = "Hello from non-whitelisted user", + Signature = "fake_signature", + CreatedAt = DateTimeOffset.UtcNow + }; + await ws.SendEventAsync(e); + + // Assert + var response = await ws.ReceiveMessageAsync(); + var okMessage = JsonDocument.Parse(response); + var messageType = okMessage.RootElement[0].GetString(); + var eventId = okMessage.RootElement[1].GetString(); + var success = okMessage.RootElement[2].GetBoolean(); + var message = okMessage.RootElement[3].GetString(); + + Assert.Equal("OK", messageType); + Assert.Equal(e.Id, eventId); + Assert.False(success); + Assert.Equal(Messages.WhitelistRestricted, message); + } + + [Fact] + public async Task WhitelistDisabled_AllowsAnyPublicKey() + { + // Arrange + var options = new WhitelistOptions + { + Enabled = false, + AllowedPublicKeys = new[] { Alice.PublicKey }, + RestrictPublishing = true, + RestrictSubscribing = false + }; + + using var client = factory.CreateClient(); + using var ws = await client.ConnectWebSocketAsync(); + + // Override the whitelist options for this test + factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; + factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; + factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; + factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + + // Act + var e = new Event + { + Id = "non_whitelisted_event_id", + PublicKey = "non_whitelisted_pubkey", + Kind = 1, + Tags = Array.Empty(), + Content = "Hello with whitelist disabled", + Signature = "fake_signature", + CreatedAt = DateTimeOffset.UtcNow + }; + await ws.SendEventAsync(e); + + // Assert + var response = await ws.ReceiveMessageAsync(); + var okMessage = JsonDocument.Parse(response); + var messageType = okMessage.RootElement[0].GetString(); + var eventId = okMessage.RootElement[1].GetString(); + + // Note: This might fail due to other validations like signature check + // We're just checking that it doesn't fail with the whitelist error + Assert.Equal("OK", messageType); + Assert.Equal(e.Id, eventId); + if (okMessage.RootElement.GetArrayLength() > 3) + { + var message = okMessage.RootElement[3].GetString(); + Assert.NotEqual(Messages.WhitelistRestricted, message); + } + } + } +} From b01f8818534273d12302dd71144ade725d7cc4ed Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 20:14:10 -0400 Subject: [PATCH 03/49] fix: align whitelist subscription validator with filter-handler contract - implement IsApplicable and CanSubscribe signatures expected by subscription handlers - update whitelist subscription validator tests to use contract method names - keep zap event validator disabled in DI as in source patch --- src/Netstr/Extensions/MessagingExtensions.cs | 2 +- .../WhitelistSubscriptionValidator.cs | 9 ++++- .../WhitelistSubscriptionValidatorTests.cs | 38 +++++++++++++------ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 878d6ae..8c0d94f 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -69,7 +69,7 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + //services.AddSingleton(); services.AddSingleton(); return services; } diff --git a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs index b6022aa..7bd872e 100644 --- a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs +++ b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using Netstr.Messaging.MessageHandlers; using Netstr.Messaging.Models; using Netstr.Options; @@ -24,7 +25,13 @@ public WhitelistSubscriptionValidator( StringComparer.OrdinalIgnoreCase); } - public string? Validate(ClientContext context, SubscriptionFilter[] filters) + public bool IsApplicable(FilterMessageHandlerBase handler) + { + // This validator is applicable to all filter message handlers + return true; + } + + public string? CanSubscribe(string id, ClientContext context, IEnumerable filters) { var whitelistOptions = this.options.Value; diff --git a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs index 5921c88..9546335 100644 --- a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs +++ b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using Moq; using Netstr.Messaging; +using Netstr.Messaging.MessageHandlers; using Netstr.Messaging.Models; using Netstr.Messaging.Subscriptions.Validators; using Netstr.Options; @@ -37,7 +38,20 @@ public WhitelistSubscriptionValidatorTests() } [Fact] - public void Validate_WhitelistDisabled_ReturnsNull() + public void IsApplicable_AlwaysReturnsTrue() + { + // Arrange + var handlerMock = new Mock(); + + // Act + var result = validator.IsApplicable(handlerMock.Object); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanSubscribe_WhitelistDisabled_ReturnsNull() { // Arrange options.Enabled = false; @@ -45,14 +59,14 @@ public void Validate_WhitelistDisabled_ReturnsNull() var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Null(result); } [Fact] - public void Validate_RestrictSubscribingDisabled_ReturnsNull() + public void CanSubscribe_RestrictSubscribingDisabled_ReturnsNull() { // Arrange options.RestrictSubscribing = false; @@ -60,63 +74,63 @@ public void Validate_RestrictSubscribingDisabled_ReturnsNull() var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Null(result); } [Fact] - public void Validate_NotAuthenticated_ReturnsAuthRequiredError() + public void CanSubscribe_NotAuthenticated_ReturnsAuthRequiredError() { // Arrange var context = new ClientContext("client1", "127.0.0.1"); var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Equal("auth-required: authentication required for subscription", result); } [Fact] - public void Validate_AllowedPublicKey_ReturnsNull() + public void CanSubscribe_AllowedPublicKey_ReturnsNull() { // Arrange var context = CreateAuthenticatedContext("allowed_pubkey1"); var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Null(result); } [Fact] - public void Validate_NotAllowedPublicKey_ReturnsError() + public void CanSubscribe_NotAllowedPublicKey_ReturnsError() { // Arrange var context = CreateAuthenticatedContext("not_allowed_pubkey"); var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Equal(Messages.WhitelistRestricted, result); } [Fact] - public void Validate_CaseInsensitiveMatch_ReturnsNull() + public void CanSubscribe_CaseInsensitiveMatch_ReturnsNull() { // Arrange var context = CreateAuthenticatedContext("ALLOWED_PUBKEY1"); var filters = Array.Empty(); // Act - var result = validator.Validate(context, filters); + var result = validator.CanSubscribe("test_id", context, filters); // Assert Assert.Null(result); From cc6be69e1036532ae021042a44130a7e9e00511c Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:03:27 -0500 Subject: [PATCH 04/49] chore: ignore development appsettings file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8a30d25..3a0fc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# Local relay development overrides +src/Netstr/appsettings.Development.json From bd6c6d6dcf751964de91e5b21f6c302c0ff23426 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 20:14:56 -0400 Subject: [PATCH 05/49] fix: merge zap and whitelist validator registrations - keep whitelist event validator enabled in the validation pipeline - re-enable zap event validator registration alongside whitelist validator --- src/Netstr/Extensions/MessagingExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 8c0d94f..f4a988a 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -69,8 +69,12 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + //services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + return services; } From 07bdcb56b1ec1def4b0898c249baa77c80b2cc1c Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 20:45:53 -0400 Subject: [PATCH 06/49] chore: keep development appsettings local-only - stop tracking src/Netstr/appsettings.Development.json in git - rely on .gitignore rule so local development settings remain uncommitted --- src/Netstr/appsettings.Development.json | 84 ------------------------- 1 file changed, 84 deletions(-) delete mode 100644 src/Netstr/appsettings.Development.json diff --git a/src/Netstr/appsettings.Development.json b/src/Netstr/appsettings.Development.json deleted file mode 100644 index 51ca043..0000000 --- a/src/Netstr/appsettings.Development.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "Serilog": { - "MinimumLevel": { - "Default": "Debug", - "Override": { - "Microsoft": "Warning", - "System": "Information" - } - }, - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], - "WriteTo": [ - { "Name": "Console" }, - { - "Name": "File", - "Args": { - "path": "logs/log.txt", - "rollingInterval": "Day" - } - } - ] - }, - "AllowedHosts": "*", - "Connection": { - "WebSocketsPath": "/" - }, - "Auth": { - "Mode": "WhenNeeded", - "ProtectedKinds": [ 4, 1059 ] - }, - "Limits": { - "MaxPayloadSize": 524288, - "Events": { - "MinPowDifficulty": 0, - "MaxEventTags": 1000, - "MaxCreatedAtLowerOffset": 31536000, - "MaxCreatedAtUpperOffset": 60, - "MaxPendingEvents": 1024, - "MaxEventsPerMinute": 300 - }, - "Subscriptions": { - "MaxInitialLimit": 1000, - "MaxFilters": 20, - "MaxSubscriptions": 50, - "MaxSubscriptionsPerMinute": 60, - "MaxSubscriptionIdLength": 128 - }, - "Negentropy": { - "MaxFilters": 20, - "MaxSubscriptionsPerMinute": 5, - "MaxSubscriptionIdLength": 128, - "MaxInitialLimit": 500000, - "MaxSubscriptions": 1, - "StaleSubscriptionLimitSeconds": 30, - "MaxSubscriptionAgeSeconds": 300, - "StaleSubscriptionPeriodSeconds": 60, - "FrameSizeLimit": 524288 - } - }, - "Cleanup": { - "DeleteDeletedEventsAfterDays": 7, - "DeleteExpiredEventsAfterDays": 7, - "DeleteEventsRules": [ - { - "Kinds": [ "17" ], - "DeleteAfterDays": 14 - }, - { - "Kinds": [ "40000-" ], - "DeleteAfterDays": 7 - } - ] - }, - "ConnectionStrings": { - "NetstrDatabase": "Host=localhost:5432;Database=Netsrt;Username=netstr;Password=Netstr" - }, - "RelayInformation": { - "Name": "netstr.io", - "Description": "A nostr relay", - "PublicKey": "NA", - "Contact": "NA", - "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 51, 62, 65, 70, 77, 119 ], - "Version": "v2.0.1" - } -} From a57c4777d67871170f3664db4a3de6007dc887de Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 20:46:23 -0400 Subject: [PATCH 07/49] chore: merge whitelist and zap integration updates From eadf88178ce1c1d1041e0d0b322f54d80d5a3ee8 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 13 May 2025 20:54:13 -0400 Subject: [PATCH 08/49] feat: add whitelist management API and dynamic option updates - add whitelist controller endpoints and configuration writer service - support owner public key and live option-monitor updates in whitelist validators - extend whitelist documentation and default configuration fields --- docs/Whitelist.md | 70 ++++++- src/Netstr/Controllers/WhitelistController.cs | 174 ++++++++++++++++++ .../Events/Validators/WhitelistValidator.cs | 22 ++- .../WhitelistSubscriptionValidator.cs | 22 ++- src/Netstr/Options/WhitelistOptions.cs | 5 + src/Netstr/Program.cs | 5 +- src/Netstr/Services/ConfigurationWriter.cs | 128 +++++++++++++ src/Netstr/appsettings.json | 3 +- 8 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 src/Netstr/Controllers/WhitelistController.cs create mode 100644 src/Netstr/Services/ConfigurationWriter.cs diff --git a/docs/Whitelist.md b/docs/Whitelist.md index 8866d7a..540ef05 100644 --- a/docs/Whitelist.md +++ b/docs/Whitelist.md @@ -9,6 +9,7 @@ The whitelist feature allows you to: 1. Restrict which public keys can publish events to your relay 2. Optionally restrict which public keys can subscribe to events from your relay 3. Enable or disable the whitelist feature without changing your configuration +4. Designate an owner public key that cannot be removed from the whitelist ## Configuration @@ -22,7 +23,8 @@ The whitelist is configured in the `appsettings.json` and `appsettings.Developme "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9" ], "RestrictPublishing": true, - "RestrictSubscribing": false + "RestrictSubscribing": false, + "OwnerPublicKey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb" } ``` @@ -32,6 +34,7 @@ The whitelist is configured in the `appsettings.json` and `appsettings.Developme - `AllowedPublicKeys`: An array of public keys that are allowed to interact with the relay. - `RestrictPublishing`: When set to `true`, only whitelisted public keys can publish events to the relay. - `RestrictSubscribing`: When set to `true`, only whitelisted public keys can subscribe to events from the relay. +- `OwnerPublicKey`: The public key of the relay owner. This key cannot be removed from the whitelist, ensuring the owner always has access to the relay. ## How It Works @@ -113,3 +116,68 @@ The whitelist feature works alongside the existing authentication modes: "RestrictSubscribing": false } ``` + +## API Endpoints + +The relay provides a set of API endpoints to manage the whitelist. These endpoints allow you to get, add, and remove public keys from the whitelist, as well as update whitelist settings. + +### Get Whitelist Settings + +``` +GET /api/whitelist +``` + +Returns the current whitelist settings, including whether the whitelist is enabled, the list of allowed public keys, and the restriction settings. + +### Get Whitelisted Keys + +``` +GET /api/whitelist/keys +``` + +Returns the list of public keys currently in the whitelist. + +### Add Public Key to Whitelist + +``` +POST /api/whitelist/keys +Content-Type: application/json + +"" +``` + +Adds a public key to the whitelist. The public key should be provided as a JSON string in the request body. + +### Remove Public Key from Whitelist + +``` +DELETE /api/whitelist/keys/{publicKey} +``` + +Removes a public key from the whitelist. The public key is provided as a path parameter. Note that the owner's public key cannot be removed. + +### Update Whitelist Settings + +``` +PUT /api/whitelist/settings +Content-Type: application/json + +{ + "enabled": true, + "restrictPublishing": true, + "restrictSubscribing": false +} +``` + +Updates the whitelist settings. The settings are provided as a JSON object in the request body. + +### Set Owner Public Key + +``` +PUT /api/whitelist/owner +Content-Type: application/json + +"" +``` + +Sets the owner's public key. The public key should be provided as a JSON string in the request body. The owner's public key cannot be removed from the whitelist. diff --git a/src/Netstr/Controllers/WhitelistController.cs b/src/Netstr/Controllers/WhitelistController.cs new file mode 100644 index 0000000..543c144 --- /dev/null +++ b/src/Netstr/Controllers/WhitelistController.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Netstr.Options; +using Netstr.Services; +using System.Collections.Generic; +using System.Linq; + +namespace Netstr.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class WhitelistController : ControllerBase + { + private readonly IOptionsMonitor _whitelistOptions; + private readonly ILogger _logger; + private readonly IConfigurationWriter _configWriter; + + public WhitelistController( + IOptionsMonitor whitelistOptions, + ILogger logger, + IConfigurationWriter configWriter) + { + _whitelistOptions = whitelistOptions; + _logger = logger; + _configWriter = configWriter; + } + + [HttpGet] + public ActionResult GetWhitelistSettings() + { + return Ok(_whitelistOptions.CurrentValue); + } + + [HttpGet("keys")] + public ActionResult> GetWhitelistedKeys() + { + return Ok(_whitelistOptions.CurrentValue.AllowedPublicKeys); + } + + [HttpPost("keys")] + public async Task AddPublicKey([FromBody] string publicKey) + { + if (string.IsNullOrWhiteSpace(publicKey)) + { + return BadRequest("Public key cannot be empty"); + } + + try + { + var currentKeys = _whitelistOptions.CurrentValue.AllowedPublicKeys.ToList(); + + if (currentKeys.Contains(publicKey, StringComparer.OrdinalIgnoreCase)) + { + return Ok("Public key already in whitelist"); + } + + currentKeys.Add(publicKey); + + await _configWriter.UpdateConfigurationAsync("Whitelist:AllowedPublicKeys", currentKeys); + + _logger.LogInformation("Added public key to whitelist: {PublicKey}", publicKey); + return Ok("Public key added to whitelist"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add public key to whitelist: {PublicKey}", publicKey); + return StatusCode(500, "Failed to update whitelist"); + } + } + + [HttpDelete("keys/{publicKey}")] + public async Task RemovePublicKey(string publicKey) + { + if (string.IsNullOrWhiteSpace(publicKey)) + { + return BadRequest("Public key cannot be empty"); + } + + try + { + var whitelistOptions = _whitelistOptions.CurrentValue; + var currentKeys = whitelistOptions.AllowedPublicKeys.ToList(); + var ownerKey = whitelistOptions.OwnerPublicKey; + + // Check if trying to remove owner key + if (!string.IsNullOrEmpty(ownerKey) && + string.Equals(publicKey, ownerKey, StringComparison.OrdinalIgnoreCase)) + { + return BadRequest("Cannot remove owner's public key from whitelist"); + } + + // Check if key exists + if (!currentKeys.Contains(publicKey, StringComparer.OrdinalIgnoreCase)) + { + return NotFound("Public key not found in whitelist"); + } + + // Remove the key + currentKeys.RemoveAll(k => string.Equals(k, publicKey, StringComparison.OrdinalIgnoreCase)); + + // Update configuration + await _configWriter.UpdateConfigurationAsync("Whitelist:AllowedPublicKeys", currentKeys); + + _logger.LogInformation("Removed public key from whitelist: {PublicKey}", publicKey); + return Ok("Public key removed from whitelist"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove public key from whitelist: {PublicKey}", publicKey); + return StatusCode(500, "Failed to update whitelist"); + } + } + + [HttpPut("settings")] + public async Task UpdateSettings([FromBody] WhitelistSettingsDto settings) + { + try + { + await _configWriter.UpdateConfigurationAsync("Whitelist:Enabled", settings.Enabled); + await _configWriter.UpdateConfigurationAsync("Whitelist:RestrictPublishing", settings.RestrictPublishing); + await _configWriter.UpdateConfigurationAsync("Whitelist:RestrictSubscribing", settings.RestrictSubscribing); + + _logger.LogInformation("Updated whitelist settings: Enabled={Enabled}, RestrictPublishing={RestrictPublishing}, RestrictSubscribing={RestrictSubscribing}", + settings.Enabled, settings.RestrictPublishing, settings.RestrictSubscribing); + + return Ok("Whitelist settings updated"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update whitelist settings"); + return StatusCode(500, "Failed to update whitelist settings"); + } + } + + [HttpPut("owner")] + public async Task SetOwnerPublicKey([FromBody] string ownerPublicKey) + { + if (string.IsNullOrWhiteSpace(ownerPublicKey)) + { + return BadRequest("Owner public key cannot be empty"); + } + + try + { + var currentKeys = _whitelistOptions.CurrentValue.AllowedPublicKeys.ToList(); + + // Ensure owner key is in the whitelist + if (!currentKeys.Contains(ownerPublicKey, StringComparer.OrdinalIgnoreCase)) + { + currentKeys.Add(ownerPublicKey); + await _configWriter.UpdateConfigurationAsync("Whitelist:AllowedPublicKeys", currentKeys); + } + + // Set the owner key + await _configWriter.UpdateConfigurationAsync("Whitelist:OwnerPublicKey", ownerPublicKey); + + _logger.LogInformation("Set owner public key: {PublicKey}", ownerPublicKey); + return Ok("Owner public key set successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to set owner public key: {PublicKey}", ownerPublicKey); + return StatusCode(500, "Failed to update whitelist"); + } + } + } + + public class WhitelistSettingsDto + { + public bool Enabled { get; set; } + public bool RestrictPublishing { get; set; } + public bool RestrictSubscribing { get; set; } + } +} diff --git a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs index f52cefe..709bcef 100644 --- a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs +++ b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs @@ -10,23 +10,35 @@ namespace Netstr.Messaging.Events.Validators public class WhitelistValidator : IEventValidator { private readonly ILogger logger; - private readonly IOptions options; - private readonly HashSet allowedPublicKeys; + private readonly IOptionsMonitor options; + private HashSet allowedPublicKeys; public WhitelistValidator( ILogger logger, - IOptions options) + IOptionsMonitor options) { this.logger = logger; this.options = options; + + // Initialize the whitelist + this.UpdateAllowedPublicKeys(options.CurrentValue); + + // Subscribe to changes + options.OnChange(UpdateAllowedPublicKeys); + } + + private void UpdateAllowedPublicKeys(WhitelistOptions options) + { this.allowedPublicKeys = new HashSet( - options.Value.AllowedPublicKeys ?? Array.Empty(), + options.AllowedPublicKeys ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + + this.logger.LogInformation("Whitelist updated with {Count} public keys", this.allowedPublicKeys.Count); } public string? Validate(Event e, ClientContext context) { - var whitelistOptions = this.options.Value; + var whitelistOptions = this.options.CurrentValue; if (!whitelistOptions.Enabled || !whitelistOptions.RestrictPublishing) { diff --git a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs index 7bd872e..68d8493 100644 --- a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs +++ b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs @@ -11,18 +11,30 @@ namespace Netstr.Messaging.Subscriptions.Validators public class WhitelistSubscriptionValidator : ISubscriptionRequestValidator { private readonly ILogger logger; - private readonly IOptions options; - private readonly HashSet allowedPublicKeys; + private readonly IOptionsMonitor options; + private HashSet allowedPublicKeys; public WhitelistSubscriptionValidator( ILogger logger, - IOptions options) + IOptionsMonitor options) { this.logger = logger; this.options = options; + + // Initialize the whitelist + this.UpdateAllowedPublicKeys(options.CurrentValue); + + // Subscribe to changes + options.OnChange(UpdateAllowedPublicKeys); + } + + private void UpdateAllowedPublicKeys(WhitelistOptions options) + { this.allowedPublicKeys = new HashSet( - options.Value.AllowedPublicKeys ?? Array.Empty(), + options.AllowedPublicKeys ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + + this.logger.LogInformation("Subscription whitelist updated with {Count} public keys", this.allowedPublicKeys.Count); } public bool IsApplicable(FilterMessageHandlerBase handler) @@ -33,7 +45,7 @@ public bool IsApplicable(FilterMessageHandlerBase handler) public string? CanSubscribe(string id, ClientContext context, IEnumerable filters) { - var whitelistOptions = this.options.Value; + var whitelistOptions = this.options.CurrentValue; if (!whitelistOptions.Enabled || !whitelistOptions.RestrictSubscribing) { diff --git a/src/Netstr/Options/WhitelistOptions.cs b/src/Netstr/Options/WhitelistOptions.cs index 1d4ba62..6996c08 100644 --- a/src/Netstr/Options/WhitelistOptions.cs +++ b/src/Netstr/Options/WhitelistOptions.cs @@ -21,5 +21,10 @@ public record WhitelistOptions /// Whether to apply the whitelist to subscribing. /// public bool RestrictSubscribing { get; init; } = false; + + /// + /// The owner's public key that cannot be removed from the whitelist. + /// + public string OwnerPublicKey { get; init; } = string.Empty; } } diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index be7ea6f..a1b8c1a 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -23,7 +23,8 @@ .AddHostedService() .AddHostedService() .AddScoped() - .AddDbContextFactory(x => x.UseNpgsql(connectionString)); + .AddDbContextFactory(x => x.UseNpgsql(connectionString)) + .AddSingleton(); var app = builder.Build(); var options = app.Services.GetRequiredService>(); @@ -45,4 +46,4 @@ app.Run(); // Required for tests -public partial class Program { } \ No newline at end of file +public partial class Program { } diff --git a/src/Netstr/Services/ConfigurationWriter.cs b/src/Netstr/Services/ConfigurationWriter.cs new file mode 100644 index 0000000..736d8cc --- /dev/null +++ b/src/Netstr/Services/ConfigurationWriter.cs @@ -0,0 +1,128 @@ +using System.Text.Json; + +namespace Netstr.Services +{ + public interface IConfigurationWriter + { + Task UpdateConfigurationAsync(string section, object value); + } + + public class ConfigurationWriter : IConfigurationWriter + { + private readonly IHostEnvironment _environment; + private readonly ILogger _logger; + + public ConfigurationWriter(IHostEnvironment environment, ILogger logger) + { + _environment = environment; + _logger = logger; + } + + public async Task UpdateConfigurationAsync(string section, object value) + { + try + { + // Determine which settings file to update + string configFile = _environment.IsDevelopment() + ? "appsettings.Development.json" + : "appsettings.json"; + + string filePath = Path.Combine(_environment.ContentRootPath, configFile); + + // Read the current config + string json = await File.ReadAllTextAsync(filePath); + var options = new JsonSerializerOptions { WriteIndented = true }; + var config = JsonSerializer.Deserialize(json); + + // Convert to dictionary for easier manipulation + var configDict = JsonToDictionary(config); + + // Update the specified section + UpdateSection(configDict, section, value); + + // Write back to file + string updatedJson = JsonSerializer.Serialize(configDict, options); + await File.WriteAllTextAsync(filePath, updatedJson); + + _logger.LogInformation("Updated configuration section {Section} in {File}", section, configFile); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update configuration section {Section}", section); + throw; + } + } + + private Dictionary JsonToDictionary(JsonElement element) + { + var dict = new Dictionary(); + + if (element.ValueKind == JsonValueKind.Object) + { + foreach (var property in element.EnumerateObject()) + { + dict[property.Name] = property.Value.ValueKind == JsonValueKind.Object + ? JsonToDictionary(property.Value) + : property.Value.ValueKind == JsonValueKind.Array + ? JsonToList(property.Value) + : GetValue(property.Value); + } + } + + return dict; + } + + private List JsonToList(JsonElement element) + { + var list = new List(); + + if (element.ValueKind == JsonValueKind.Array) + { + foreach (var item in element.EnumerateArray()) + { + list.Add(item.ValueKind == JsonValueKind.Object + ? JsonToDictionary(item) + : item.ValueKind == JsonValueKind.Array + ? JsonToList(item) + : GetValue(item)); + } + } + + return list; + } + + private object GetValue(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out long l) ? l : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.ToString() + }; + } + + private void UpdateSection(Dictionary config, string section, object value) + { + var parts = section.Split(':', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 1) + { + config[parts[0]] = value; + return; + } + + if (!config.ContainsKey(parts[0])) + { + config[parts[0]] = new Dictionary(); + } + + if (config[parts[0]] is Dictionary dict) + { + UpdateSection(dict, string.Join(':', parts.Skip(1)), value); + } + } + } +} diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 19ce5c5..86a0794 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -84,6 +84,7 @@ "Enabled": false, "AllowedPublicKeys": [], "RestrictPublishing": true, - "RestrictSubscribing": false + "RestrictSubscribing": false, + "OwnerPublicKey": "" } } From 9507cd41eb7398ed4073ba5628f732d28e5b3a8e Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sat, 17 May 2025 21:13:54 -0400 Subject: [PATCH 09/49] chore: update gitignore for local editor files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3a0fc0d..4bcf0b2 100644 --- a/.gitignore +++ b/.gitignore @@ -399,3 +399,6 @@ FodyWeavers.xsd # Local relay development overrides src/Netstr/appsettings.Development.json + +.vscode/tasks.json +.vscode/launch.json From c3cdc50f608c65ce763d5c77dab4e89f0685df22 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Mon, 26 May 2025 17:20:17 -0400 Subject: [PATCH 10/49] fix: improve negentropy watcher shutdown and local launch settings - move stale-subscription delay to loop tail with cancellation-safe handling - add service namespace import required by configuration-writer wiring - update local launch profile HTTP ports for development --- .../Middleware/NegentropyBackgroundWatcher.cs | 16 ++++++++++++---- src/Netstr/Program.cs | 1 + src/Netstr/Properties/launchSettings.json | 6 +++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Netstr/Middleware/NegentropyBackgroundWatcher.cs b/src/Netstr/Middleware/NegentropyBackgroundWatcher.cs index 9b58bc9..665a732 100644 --- a/src/Netstr/Middleware/NegentropyBackgroundWatcher.cs +++ b/src/Netstr/Middleware/NegentropyBackgroundWatcher.cs @@ -26,10 +26,8 @@ public NegentropyBackgroundWatcher( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - do + while (!stoppingToken.IsCancellationRequested) { - await Task.Delay(TimeSpan.FromSeconds(this.options.Value.Negentropy.StaleSubscriptionPeriodSeconds), stoppingToken); - this.logger.LogInformation("Checking stale negentropy subscriptions"); // get all active websockets @@ -37,7 +35,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { ws.Negentropy.DisposeStaleSubscriptions(); } - } while (!stoppingToken.IsCancellationRequested); + + try + { + await Task.Delay(TimeSpan.FromSeconds(this.options.Value.Negentropy.StaleSubscriptionPeriodSeconds), stoppingToken); + } + catch (TaskCanceledException) + { + // This is expected during shutdown, so we can just break out of the loop + break; + } + } } } } diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index a1b8c1a..8d1ac6a 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -5,6 +5,7 @@ using Netstr.Middleware; using Netstr.Options; using Netstr.RelayInformation; +using Netstr.Services; using Serilog; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Netstr/Properties/launchSettings.json b/src/Netstr/Properties/launchSettings.json index 5a74d2b..38963da 100644 --- a/src/Netstr/Properties/launchSettings.json +++ b/src/Netstr/Properties/launchSettings.json @@ -6,7 +6,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:8080;https://localhost:8443" + "applicationUrl": "http://localhost:8081;https://localhost:8443" }, "IIS Express": { "commandName": "IISExpress", @@ -26,7 +26,7 @@ "launchUrl": "https://localhost:8443", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "http://localhost:8080;https://localhost:8443" + "ASPNETCORE_URLS": "http://localhost:8081;https://localhost:8443" }, "distributionName": "" } @@ -36,7 +36,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:8081", + "applicationUrl": "http://localhost:8082", "sslPort": 0 } } From 6d621edecc5ad3af1d36da8e780e19dd526fb188 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Tue, 27 May 2025 02:24:37 -0400 Subject: [PATCH 11/49] feat: allow whitelist-exempt event kinds - add ExemptKinds to whitelist options and enforce exemptions in validator - extend whitelist controller and docs for exempt-kind management - keep development appsettings local-only and untracked --- docs/Whitelist.md | 62 ++++++++++++++- src/Netstr/Controllers/WhitelistController.cs | 75 +++++++++++++++++++ .../Events/Validators/WhitelistValidator.cs | 7 ++ src/Netstr/Options/WhitelistOptions.cs | 5 ++ 4 files changed, 147 insertions(+), 2 deletions(-) diff --git a/docs/Whitelist.md b/docs/Whitelist.md index 540ef05..89910cf 100644 --- a/docs/Whitelist.md +++ b/docs/Whitelist.md @@ -35,6 +35,7 @@ The whitelist is configured in the `appsettings.json` and `appsettings.Developme - `RestrictPublishing`: When set to `true`, only whitelisted public keys can publish events to the relay. - `RestrictSubscribing`: When set to `true`, only whitelisted public keys can subscribe to events from the relay. - `OwnerPublicKey`: The public key of the relay owner. This key cannot be removed from the whitelist, ensuring the owner always has access to the relay. +- `ExemptKinds`: An array of event kinds that are exempt from whitelist restrictions. Events of these kinds can be published by any public key, even if the whitelist is enabled and the public key is not in the whitelist. ## How It Works @@ -44,8 +45,9 @@ When a client attempts to publish an event to the relay: 1. If `Enabled` is `false`, the event is accepted (subject to other validation rules). 2. If `RestrictPublishing` is `false`, the event is accepted (subject to other validation rules). -3. If the event's public key is in the `AllowedPublicKeys` list, the event is accepted (subject to other validation rules). -4. Otherwise, the event is rejected with the message: `restricted: your public key is not in the whitelist`. +3. If the event's kind is in the `ExemptKinds` list, the event is accepted (subject to other validation rules). +4. If the event's public key is in the `AllowedPublicKeys` list, the event is accepted (subject to other validation rules). +5. Otherwise, the event is rejected with the message: `restricted: your public key is not in the whitelist`. ### Subscribing to Events @@ -117,6 +119,24 @@ The whitelist feature works alongside the existing authentication modes: } ``` +### Whitelist with Exempt Kinds + +```json +"Whitelist": { + "Enabled": true, + "AllowedPublicKeys": [ + "pubkey1", + "pubkey2", + "pubkey3" + ], + "RestrictPublishing": true, + "RestrictSubscribing": false, + "ExemptKinds": [9735, 1059] +} +``` + +In this configuration, only whitelisted public keys can publish most event kinds, but any public key can publish events of kind 9735 (zaps) and 1059 (without being restricted by the whitelist). + ## API Endpoints The relay provides a set of API endpoints to manage the whitelist. These endpoints allow you to get, add, and remove public keys from the whitelist, as well as update whitelist settings. @@ -181,3 +201,41 @@ Content-Type: application/json ``` Sets the owner's public key. The public key should be provided as a JSON string in the request body. The owner's public key cannot be removed from the whitelist. + +### Get Exempt Kinds + +``` +GET /api/whitelist/exempt-kinds +``` + +Returns the list of event kinds that are exempt from whitelist restrictions. + +### Add Exempt Kind + +``` +POST /api/whitelist/exempt-kinds +Content-Type: application/json + +9735 +``` + +Adds an event kind to the list of exempt kinds. The event kind should be provided as a JSON number in the request body. + +### Remove Exempt Kind + +``` +DELETE /api/whitelist/exempt-kinds/{kind} +``` + +Removes an event kind from the list of exempt kinds. The event kind is provided as a path parameter. + +### Update Exempt Kinds + +``` +PUT /api/whitelist/exempt-kinds +Content-Type: application/json + +[9735, 1059] +``` + +Updates the entire list of exempt kinds. The exempt kinds are provided as a JSON array of numbers in the request body. diff --git a/src/Netstr/Controllers/WhitelistController.cs b/src/Netstr/Controllers/WhitelistController.cs index 543c144..ece8ce5 100644 --- a/src/Netstr/Controllers/WhitelistController.cs +++ b/src/Netstr/Controllers/WhitelistController.cs @@ -163,6 +163,81 @@ public async Task SetOwnerPublicKey([FromBody] string ownerPublicK return StatusCode(500, "Failed to update whitelist"); } } + + [HttpGet("exempt-kinds")] + public ActionResult> GetExemptKinds() + { + return Ok(_whitelistOptions.CurrentValue.ExemptKinds); + } + + [HttpPost("exempt-kinds")] + public async Task AddExemptKind([FromBody] long kind) + { + try + { + var currentExemptKinds = _whitelistOptions.CurrentValue.ExemptKinds.ToList(); + + if (currentExemptKinds.Contains(kind)) + { + return Ok($"Event kind {kind} is already exempt from whitelist"); + } + + currentExemptKinds.Add(kind); + + await _configWriter.UpdateConfigurationAsync("Whitelist:ExemptKinds", currentExemptKinds); + + _logger.LogInformation("Added event kind {Kind} to whitelist exemptions", kind); + return Ok($"Event kind {kind} added to whitelist exemptions"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add event kind {Kind} to whitelist exemptions", kind); + return StatusCode(500, "Failed to update whitelist exemptions"); + } + } + + [HttpDelete("exempt-kinds/{kind}")] + public async Task RemoveExemptKind(long kind) + { + try + { + var currentExemptKinds = _whitelistOptions.CurrentValue.ExemptKinds.ToList(); + + if (!currentExemptKinds.Contains(kind)) + { + return NotFound($"Event kind {kind} not found in whitelist exemptions"); + } + + currentExemptKinds.Remove(kind); + + await _configWriter.UpdateConfigurationAsync("Whitelist:ExemptKinds", currentExemptKinds); + + _logger.LogInformation("Removed event kind {Kind} from whitelist exemptions", kind); + return Ok($"Event kind {kind} removed from whitelist exemptions"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove event kind {Kind} from whitelist exemptions", kind); + return StatusCode(500, "Failed to update whitelist exemptions"); + } + } + + [HttpPut("exempt-kinds")] + public async Task UpdateExemptKinds([FromBody] List exemptKinds) + { + try + { + await _configWriter.UpdateConfigurationAsync("Whitelist:ExemptKinds", exemptKinds); + + _logger.LogInformation("Updated whitelist exempt kinds: {ExemptKinds}", string.Join(", ", exemptKinds)); + return Ok("Whitelist exempt kinds updated"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update whitelist exempt kinds"); + return StatusCode(500, "Failed to update whitelist exempt kinds"); + } + } } public class WhitelistSettingsDto diff --git a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs index 709bcef..c98cfcd 100644 --- a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs +++ b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs @@ -45,6 +45,13 @@ private void UpdateAllowedPublicKeys(WhitelistOptions options) return null; } + // Check if this event kind is exempt from whitelist restrictions + if (whitelistOptions.ExemptKinds.Contains(e.Kind)) + { + this.logger.LogInformation($"Event kind {e.Kind} is exempt from whitelist restrictions"); + return null; + } + if (!this.allowedPublicKeys.Contains(e.PublicKey)) { this.logger.LogWarning($"Rejected event from non-whitelisted public key: {e.PublicKey}"); diff --git a/src/Netstr/Options/WhitelistOptions.cs b/src/Netstr/Options/WhitelistOptions.cs index 6996c08..e388142 100644 --- a/src/Netstr/Options/WhitelistOptions.cs +++ b/src/Netstr/Options/WhitelistOptions.cs @@ -26,5 +26,10 @@ public record WhitelistOptions /// The owner's public key that cannot be removed from the whitelist. /// public string OwnerPublicKey { get; init; } = string.Empty; + + /// + /// List of event kinds that are exempt from whitelist restrictions. + /// + public long[] ExemptKinds { get; init; } = []; } } From 42e1d5aa1fc21dc158c28463c11ac0d173496c4e Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:05:00 -0400 Subject: [PATCH 12/49] feat: implement core NIP validation and filtering expansions - add NIP-05 verification models, validator, and verification service wiring - expand subscription/filter matching with search and metadata-aware support - extend NIP spec coverage files and test step definitions --- .claude/settings.local.json | 14 + CLAUDE.md | 94 +++ docs/NIP-Implementation-Guides.md | 492 ++++++++++++++++ docs/Priority-NIPs-Implementation.md | 544 ++++++++++++++++++ packages-microsoft-prod.deb | Bin 0 -> 3690 bytes src/Netstr/Extensions/MessagingExtensions.cs | 5 + src/Netstr/Messaging/Events/DbExtensions.cs | 45 ++ .../Events/Validators/Nip05Validator.cs | 73 +++ .../FilterMessageHandlerBase.cs | 1 + src/Netstr/Messaging/Models/EventKind.cs | 5 +- .../Messaging/Models/Nip05/Nip05Response.cs | 23 + .../Messaging/Models/Nip05/Nip05Result.cs | 22 + .../Messaging/Models/SubscriptionFilter.cs | 6 +- src/Netstr/Messaging/Models/UserMetadata.cs | 37 ++ .../Subscriptions/MatchingExtensions.cs | 2 + .../Messaging/Subscriptions/SearchMatcher.cs | 156 +++++ .../SubscriptionFilterMatcher.cs | 1 + src/Netstr/Options/Limits/SearchLimits.cs | 33 ++ src/Netstr/Options/LimitsOptions.cs | 3 + .../Services/Nip05VerificationService.cs | 199 +++++++ src/Netstr/appsettings.json | 9 +- test/Netstr.Tests/NIPs/01.feature.cs | 8 +- test/Netstr.Tests/NIPs/04.feature.cs | 4 +- test/Netstr.Tests/NIPs/05.feature | 28 + test/Netstr.Tests/NIPs/05.feature.cs | 252 ++++++++ test/Netstr.Tests/NIPs/09.feature.cs | 4 +- test/Netstr.Tests/NIPs/11.feature.cs | 6 +- test/Netstr.Tests/NIPs/13.feature.cs | 8 +- test/Netstr.Tests/NIPs/40.feature.cs | 4 +- test/Netstr.Tests/NIPs/45.feature.cs | 6 +- test/Netstr.Tests/NIPs/51.feature | 4 +- test/Netstr.Tests/NIPs/51.feature.cs | 425 ++++++++++++++ test/Netstr.Tests/NIPs/57.feature | 2 +- test/Netstr.Tests/NIPs/57.feature.cs | 222 +++++++ test/Netstr.Tests/NIPs/62.feature.cs | 188 +++--- test/Netstr.Tests/NIPs/65.feature.cs | 26 +- test/Netstr.Tests/NIPs/70.feature.cs | 56 +- test/Netstr.Tests/NIPs/Steps/05.cs | 75 +++ .../Netstr.Tests/NIPs/Steps/RelayListSteps.cs | 90 +-- 39 files changed, 2977 insertions(+), 195 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 docs/NIP-Implementation-Guides.md create mode 100644 docs/Priority-NIPs-Implementation.md create mode 100644 packages-microsoft-prod.deb create mode 100644 src/Netstr/Messaging/Events/Validators/Nip05Validator.cs create mode 100644 src/Netstr/Messaging/Models/Nip05/Nip05Response.cs create mode 100644 src/Netstr/Messaging/Models/Nip05/Nip05Result.cs create mode 100644 src/Netstr/Messaging/Models/UserMetadata.cs create mode 100644 src/Netstr/Messaging/Subscriptions/SearchMatcher.cs create mode 100644 src/Netstr/Options/Limits/SearchLimits.cs create mode 100644 src/Netstr/Services/Nip05VerificationService.cs create mode 100644 test/Netstr.Tests/NIPs/05.feature create mode 100644 test/Netstr.Tests/NIPs/05.feature.cs create mode 100644 test/Netstr.Tests/NIPs/51.feature.cs create mode 100644 test/Netstr.Tests/NIPs/57.feature.cs create mode 100644 test/Netstr.Tests/NIPs/Steps/05.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c25d101 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:www.e2encrypted.com)", + "WebFetch(domain:github.com)", + "Bash(dotnet build)", + "Bash(dotnet test:*)", + "Bash(export PATH=\"$PATH:$HOME/.dotnet\")", + "Bash(dotnet --version)", + "Bash(dotnet build:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b521a3c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Build and Run +- `dotnet run --project .\src\Netstr\Netstr.csproj` - Run the main application +- `dotnet build` - Build the solution +- `dotnet build --configuration Release` - Build for release + +### Testing +- `dotnet test` - Run all tests +- `dotnet test --filter "DisplayName~"` - Run specific SpecFlow scenario +- `dotnet test test/Netstr.Tests/Netstr.Tests.csproj` - Run tests for specific project + +### Database +- `dotnet ef migrations add --project src/Netstr` - Add new EF migration +- `dotnet ef database update --project src/Netstr` - Apply migrations + +### Docker +- `docker build -t netstr .` - Build Docker image +- `docker compose up` - Run with Docker Compose (includes PostgreSQL) + +## Architecture Overview + +Netstr is a modern Nostr relay written in C# using ASP.NET Core, targeting .NET 9.0. It implements multiple NIPs (Nostr Implementation Possibilities) for the decentralized nostr protocol. + +### Core Components + +**WebSocket Message Processing Pipeline:** +1. `WebSocketAdapter` - Handles WebSocket connections and message routing +2. `MessageDispatcher` - Routes incoming messages to appropriate handlers based on message type +3. `EventDispatcher` - Routes EVENT messages to specific event handlers based on event kind + +**Message Handlers** (in `src/Netstr/Messaging/MessageHandlers/`): +- `SubscribeMessageHandler` - Handles REQ (subscription) messages +- `UnsubscribeMessageHandler` - Handles CLOSE messages +- `AuthMessageHandler` - Handles AUTH messages for NIP-42 +- `CountMessageHandler` - Handles COUNT messages for NIP-45 +- `NegentropyMessageHandler` / `NegentropyOpenHandler` / `NegentropyCloseHandler` - Handle negentropy sync (NIP-77) + +**Event Handlers** (in `src/Netstr/Messaging/Events/Handlers/`): +- `RegularEventHandler` - Standard events (kind 1, etc.) +- `DeleteEventHandler` - Event deletion (NIP-09) +- `EphemeralEventHandler` - Ephemeral events (kinds 20000-29999) +- `ReplaceableEventHandler` - Replaceable events (kinds 10000-19999) +- `AddressableEventHandler` - Addressable events (kinds 30000-39999) +- `ZapEventHandler` - Zap events (NIP-57) +- `RelayListEventHandler` - Relay list events +- `VanishEventHandler` - Vanish requests (NIP-62) + +**Data Layer:** +- Uses Entity Framework Core with PostgreSQL +- `NetstrDbContext` - Main database context +- `EventEntity`, `TagEntity`, `RelayConfigEntity` - Core data models +- Migrations in `src/Netstr/Data/Migrations/` + +**Configuration:** +- `appsettings.json` / `appsettings.Development.json` - Main configuration +- Options pattern used throughout (`AuthOptions`, `LimitsOptions`, `WhitelistOptions`, etc.) +- `ConnectionOptions` for WebSocket configuration + +### Key Features +- **Whitelist Management:** Public key-based access control for publishing/subscribing +- **Event Validation:** Multi-layer validation including signatures, PoW, timestamps, and custom validators +- **Subscription Management:** Efficient filtering and real-time event delivery +- **Negentropy Sync:** Advanced synchronization protocol for relay-to-relay communication +- **Rate Limiting:** Built-in limits for events, subscriptions, and payload sizes +- **Authentication:** NIP-42 auth support with challenge-response + +### Testing Strategy +- Uses SpecFlow with Gherkin scenarios for behavior-driven testing +- Test scenarios written in plain English in `.feature` files +- Step definitions in `test/Netstr.Tests/NIPs/Steps/` +- Each NIP has dedicated test scenarios +- Uses xUnit as the underlying test framework +- Test data in `test/Netstr.Tests/Resources/Events.json` + +### Message Flow +1. WebSocket connection established → `WebSocketAdapter.StartAsync()` +2. Raw message received → `MessageDispatcher.DispatchMessageAsync()` +3. Message parsed and routed to appropriate `IMessageHandler` +4. If EVENT message → `EventDispatcher.DispatchEventAsync()` → specific `IEventHandler` +5. Event validation through multiple `IEventValidator` implementations +6. Event stored to database or processed ephemerally +7. Event distributed to matching subscriptions + +### Development Notes +- Uses dependency injection throughout +- Logging via Serilog +- Database migrations handled automatically on startup +- WebSocket adapters managed in collections for broadcast scenarios +- Custom JSON serialization for Nostr protocol compatibility \ No newline at end of file diff --git a/docs/NIP-Implementation-Guides.md b/docs/NIP-Implementation-Guides.md new file mode 100644 index 0000000..a16f993 --- /dev/null +++ b/docs/NIP-Implementation-Guides.md @@ -0,0 +1,492 @@ +# NIP Implementation Guides + +This document provides structural implementation guides for different categories of NIPs based on the patterns used in Netstr. + +## Event Kind Ranges Overview + +Understanding event kind ranges is crucial for proper NIP implementation: + +- **0-999**: Protocol events (metadata, notes, DMs, reactions, follows) +- **1000-9999**: Special protocol events (mute, auth, zaps) +- **10000-19999**: Replaceable events (lists, settings) - One per pubkey+kind +- **20000-29999**: Ephemeral events (presence, typing) - No storage +- **30000-39999**: Addressable replaceable events (profiles, sets) - One per pubkey+kind+d_tag + +## Core Architectural Patterns + +### Message Flow Pattern +All client-relay interactions follow the EVENT → OK/NOTICE pattern: +1. Client sends EVENT message +2. Relay validates and processes +3. Relay responds with OK (success) or NOTICE (error) +4. Relay broadcasts to matching subscriptions + +### Event Categories +- **Regular Events**: Stored normally, can have duplicates +- **Replaceable Events**: Replace previous event of same kind by same author +- **Ephemeral Events**: Not stored, only broadcast in real-time +- **Addressable Events**: Replace previous event with same kind+pubkey+d_tag + +## 1. Basic Protocol NIPs (1, 2, 11) + +### NIP-01: Basic Protocol Flow +**Core Components Required:** + +1. **Event Handler (`RegularEventHandler`)** +```csharp +public class RegularEventHandler : EventHandlerBase, IEventHandler +{ + public bool CanHandleEvent(Event e) => true; // Fallback handler + + public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) + { + // 1. Validate event wasn't deleted + // 2. Store in database with duplicate prevention + // 3. Send OK response + // 4. Broadcast to matching subscriptions + } +} +``` + +2. **Message Handlers** +```csharp +// SubscribeMessageHandler - Handle REQ messages +// UnsubscribeMessageHandler - Handle CLOSE messages +// EventParser - Parse EVENT messages +``` + +3. **Database Schema** +```sql +-- EventEntity: Core event storage +-- TagEntity: Event tags for filtering +-- Proper indexing on pubkey, kind, created_at +``` + +4. **Key Implementation Steps:** + - WebSocket message routing via `MessageDispatcher` + - Event validation through validator chain + - Database storage with EF Core + - Real-time broadcasting via `SubscriptionsAdapter` + +### NIP-02: Contact Lists / Following +**Uses existing replaceable event infrastructure (kind 3)** + +### NIP-11: Relay Information Document +**Implementation in `RelayInformationService`:** +```csharp +// Serves JSON at /.well-known/nostr.json +// Returns relay capabilities, supported NIPs, contact info +``` + +## 2. List Management NIPs (51) + +### NIP-51: Lists Implementation Pattern + +**Event Handler Architecture:** +```csharp +public class ListEventHandler : ReplaceableEventHandlerBase +{ + // Standard Lists (10000-10999): One per user per kind + // Sets (30000-30999): Multiple per user, identified by 'd' tag +} +``` + +**Key Components:** + +1. **EventKind Definitions** +```csharp +// Standard Lists +MuteList = 10000, +PinnedNotes = 10001, +RelayList = 10002, +Bookmarks = 10003, +// ... additional list kinds + +// Sets +FollowSets = 30000, +RelaySets = 30002, +BookmarkSets = 30003, +// ... additional set kinds +``` + +2. **Storage Pattern** +```csharp +// Standard lists: Replace by pubkey + kind +// Sets: Replace by pubkey + kind + d_tag_value +// Private items: Encrypted in content field (NIP-04) +// Public items: Stored in tags array +``` + +3. **Implementation Steps:** + - Extend `ReplaceableEventHandler` or `AddressableEventHandler` + - Add specific list kinds to `EventKind` enum + - Implement tag parsing for list items + - Handle encryption/decryption for private items + - Support both public and private list items + +## 3. Relay Metadata NIP (65) + +### NIP-65: Relay List Metadata + +**Specialized Handler:** +```csharp +public class RelayListEventHandler : ReplaceableEventHandlerBase +{ + // Kind 10002 - Relay lists + + protected override async Task ProcessEventAsync(...) + { + // 1. Parse relay tags (read/write markers) + // 2. Update RelayConfigs table + // 3. Store event normally + // 4. Update user's relay configuration + } +} +``` + +**Database Schema:** +```csharp +public class RelayConfigEntity +{ + public string UserId { get; set; } + public string RelayUrl { get; set; } + public bool Read { get; set; } + public bool Write { get; set; } + // Additional relay metadata +} +``` + +**Implementation Pattern:** +1. **Tag Structure:** `["r", "relay_url", "read|write"]` +2. **Storage:** Dual storage in events table + relay configs table +3. **Processing:** Parse tags → Update relay configs → Store event +4. **Usage:** Query relay configs for user publishing/reading preferences + +## 4. Authentication NIPs (42, 70) + +### NIP-42: Authentication of Clients to Relays + +**Key Components:** + +1. **Auth Message Handler** +```csharp +public class AuthMessageHandler : IMessageHandler +{ + // Handle AUTH responses from clients + // Verify signed events for authentication + // Update ClientContext with authenticated pubkey +} +``` + +2. **Challenge System** +```csharp +public class ClientContext +{ + public string Challenge { get; } // Random challenge string + public User? User { get; set; } // Set after successful auth +} +``` + +3. **Configuration** +```csharp +public class AuthOptions +{ + public AuthMode Mode { get; set; } // Always, Publishing, WhenNeeded, Disabled + public long[] ProtectedKinds { get; set; } // Event kinds requiring auth +} +``` + +### NIP-70: Protected Events + +**Implementation in Event Validation:** +```csharp +public class ProtectedEventValidator : IEventValidator +{ + public ValidationResult ValidateEvent(Event e, ClientContext context) + { + if (IsProtectedKind(e.Kind) && !context.IsAuthenticated) + return ValidationResult.Fail("auth-required"); + + return ValidationResult.Success(); + } +} +``` + +## 5. Event Modification NIPs (9, 40) + +### NIP-09: Event Deletion + +**Specialized Handler Pattern:** +```csharp +public class DeleteEventHandler : EventHandlerBase +{ + protected override async Task ProcessEventAsync(...) + { + // 1. Parse 'e' tags for event IDs to delete + // 2. Parse 'a' tags for addressable event references + // 3. Verify user owns events to be deleted + // 4. Mark events as deleted (soft delete) + // 5. Store deletion event + } +} +``` + +**Key Features:** +- Soft deletion (mark as deleted, don't remove) +- Reference parsing: `e` tags for IDs, `a` tags for addressable events +- Ownership verification +- Transaction-based consistency + +### NIP-40: Expiration Timestamp + +**Implementation in Event Processing:** +```csharp +public class ExpiredEventValidator : IEventValidator +{ + public ValidationResult ValidateEvent(Event e, ClientContext context) + { + var expirationTag = e.Tags.FirstOrDefault(t => t.Name == "expiration"); + if (expirationTag != null && IsExpired(expirationTag.Value)) + return ValidationResult.Fail("event expired"); + } +} +``` + +**Cleanup Service:** +```csharp +public class CleanupBackgroundService : BackgroundService +{ + // Periodically remove expired events based on 'expiration' tags + // Configurable cleanup intervals and retention policies +} +``` + +## 6. Messaging NIPs (4, 17, 59) + +### NIP-04: Encrypted Direct Messages (Deprecated) +**Standard event handling with encrypted content** + +### NIP-17: Private Direct Messages +**Uses replaceable events with specific kind ranges and validation** + +### NIP-59: Gift Wrapping + +**Event Kind Definition:** +```csharp +GiftWrap = 1059, // In EventKind enum +``` + +**Configuration:** +```csharp +// Add to ProtectedKinds - requires authentication +"ProtectedKinds": [1059] +``` + +**Processing:** +- Standard event handling with authentication requirement +- Content remains encrypted (relay doesn't decrypt) +- Proper routing based on recipient information + +## 7. Special Feature NIPs (13, 45, 57, 77, 119) + +### NIP-13: Proof of Work + +**Validator Implementation:** +```csharp +public class EventPowValidator : IEventValidator +{ + public ValidationResult ValidateEvent(Event e, ClientContext context) + { + var nonceTag = e.Tags.FirstOrDefault(t => t.Name == "nonce"); + if (nonceTag != null) + { + var difficulty = CalculateDifficulty(e.Id); + if (difficulty < requiredDifficulty) + return ValidationResult.Fail("insufficient pow"); + } + } +} +``` + +### NIP-45: Counting Results + +**Message Handler:** +```csharp +public class CountMessageHandler : FilterMessageHandlerBase +{ + // Handle COUNT messages + // Return count of matching events instead of events themselves + // Use same filter logic as subscription system +} +``` + +### NIP-57: Lightning Zaps + +**Specialized Handler:** +```csharp +public class ZapEventHandler : EventHandlerBase +{ + // Handle kinds 9734 (ZapRequest) and 9735 (ZapReceipt) + // Enhanced duplicate detection + // Standard storage and broadcasting +} +``` + +### NIP-77: Negentropy Sync + +**Complex Multi-Component Implementation:** +```csharp +// NegentropyAdapter - Manages sync state +// NegentropyMessageHandler - Handles NEG-MSG, NEG-OPEN, NEG-CLOSE +// Background processing for efficient set reconciliation +``` + +### NIP-119: AND Operator for Filters +**Implementation in subscription filter matching logic** + +## General Implementation Checklist + +### For Any New NIP: + +1. **Define Event Kinds** (if applicable) + - Add to `EventKind` enum + - Document expected tag structure + +2. **Create Event Handler** (if new event types) + - Inherit from appropriate base class + - Implement `CanHandleEvent()` and `HandleEventAsync()` + - Handle storage, validation, and broadcasting + +3. **Add Validators** (if special validation needed) + - Implement `IEventValidator` + - Add to validation chain in DI + +4. **Update Configuration** + - Add to `SupportedNips` array + - Add any NIP-specific options + +5. **Create Tests** + - Write SpecFlow scenarios in `.feature` files + - Implement step definitions + - Test both success and failure cases + +6. **Database Changes** (if needed) + - Create new entities/tables + - Add migrations + - Update indexes for performance + +7. **Message Handlers** (if new message types) + - Implement `IMessageHandler` + - Add to DI container + - Handle JSON parsing and response + +## 8. Commonly Requested NIPs (Not Yet Implemented) + +### NIP-50: Search Capability + +**Implementation Requirements:** +```csharp +public class SearchMessageHandler : IMessageHandler +{ + public bool CanHandleMessage(string type) => type == "REQ"; + + public async Task HandleMessageAsync(IWebSocketAdapter sender, JsonDocument[] parts) + { + // Parse REQ message for 'search' field + // Implement full-text search against event content + // Return matching events sorted by relevance + } +} +``` + +**Key Features:** +- Add `search` field to subscription filters +- Implement full-text search against event content +- Support search extensions: `include:spam`, `domain:`, `language:` +- Sort results by relevance rather than chronological order + +### NIP-96: HTTP File Storage + +**Implementation Architecture:** +```csharp +[ApiController] +[Route("/.well-known/nostr/nip96.json")] +public class FileStorageController : ControllerBase +{ + // Server configuration endpoint + + [HttpPost("/upload")] + public async Task UploadFile([FromForm] FileUploadRequest request) + { + // Validate NIP-98 authorization header + // Store file with SHA-256 hash as identifier + // Return file URL and metadata + } + + [HttpGet("/{hash}")] + public async Task DownloadFile(string hash) + { + // Serve file by hash + // Support optional transformations + } +} +``` + +**Dependencies:** +- **NIP-98**: HTTP Authorization for uploads +- File storage backend (local/cloud) +- Image processing for transformations + +### NIP-05: DNS-based Identities + +**Implementation Pattern:** +```csharp +public class Nip05Validator : IEventValidator +{ + public async Task ValidateEventAsync(Event e, ClientContext context) + { + // Check for NIP-05 identifier in metadata events (kind 0) + // Validate against /.well-known/nostr.json + // Cache verification results + } +} +``` + +**Key Components:** +- HTTP client for DNS verification +- Caching layer for verification results +- Integration with user profiles (kind 0 events) + +### NIP-78: Application-specific Data + +**Storage Pattern:** +```csharp +// Use addressable events (kind 30078) with 'd' tag for app identifier +// Store app preferences and settings +// Support encrypted content for private settings +``` + +## 9. Advanced Implementation Patterns + +### Multi-NIP Integration +Some features require combining multiple NIPs: + +**Example: Private Groups with File Sharing** +- NIP-17: Private messaging +- NIP-59: Gift wrapping +- NIP-96: File storage +- NIP-98: HTTP authorization + +### Performance Optimizations +```csharp +// Database indexing strategy for large-scale deployments +// Event caching patterns +// Subscription optimization for high-throughput scenarios +``` + +### Backwards Compatibility +- Maintain support for deprecated NIPs during transition periods +- Implement feature flags for experimental NIPs +- Version negotiation for client compatibility + +This guide provides the foundational patterns used in Netstr for implementing NIPs systematically and consistently. \ No newline at end of file diff --git a/docs/Priority-NIPs-Implementation.md b/docs/Priority-NIPs-Implementation.md new file mode 100644 index 0000000..e7ab629 --- /dev/null +++ b/docs/Priority-NIPs-Implementation.md @@ -0,0 +1,544 @@ +# Priority NIPs Implementation Guide + +This document provides detailed, step-by-step implementation guides for the high-impact NIPs that would significantly improve Netstr's client compatibility and ecosystem integration. + +## Priority 1: High-Impact NIPs + +### NIP-50: Search Capability + +**Status**: Expected by most clients | **Impact**: High | **Difficulty**: Medium + +#### Implementation Overview +NIP-50 adds a `search` field to REQ messages, enabling full-text search across event content. + +#### Step-by-Step Implementation + +**1. Extend SubscriptionFilter Model** +```csharp +// In src/Netstr/Messaging/Models/SubscriptionFilter.cs +public class SubscriptionFilter +{ + // Existing properties... + + [JsonPropertyName("search")] + public string? Search { get; set; } +} +``` + +**2. Update Filter Parsing** +```csharp +// In src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs +private SubscriptionFilter ParseFilter(JsonElement filterElement) +{ + var filter = new SubscriptionFilter(); + + // Existing parsing... + + if (filterElement.TryGetProperty("search", out var searchElement)) + { + filter.Search = searchElement.GetString(); + } + + return filter; +} +``` + +**3. Implement Search Matcher** +```csharp +// Create new file: src/Netstr/Messaging/Subscriptions/SearchMatcher.cs +public static class SearchMatcher +{ + public static bool MatchesSearch(Event eventItem, string searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + return true; + + var content = eventItem.Content?.ToLowerInvariant() ?? ""; + var terms = searchTerm.ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries); + + // Basic implementation: all terms must be present + return terms.All(term => content.Contains(term)); + } + + // Advanced: Support search extensions + public static bool MatchesAdvancedSearch(Event eventItem, string searchTerm) + { + // Parse extensions like "include:spam", "domain:example.com" + var (cleanTerm, extensions) = ParseSearchExtensions(searchTerm); + + if (!MatchesSearch(eventItem, cleanTerm)) + return false; + + // Apply extensions + foreach (var ext in extensions) + { + if (!ApplySearchExtension(eventItem, ext)) + return false; + } + + return true; + } +} +``` + +**4. Update Database Query for Performance** +```csharp +// In src/Netstr/Messaging/Events/DbExtensions.cs +public static IQueryable WhereMatchesSearch( + this IQueryable query, + string searchTerm) +{ + if (string.IsNullOrEmpty(searchTerm)) + return query; + + // Use PostgreSQL full-text search for performance + return query.Where(e => EF.Functions.ToTsVector("english", e.Content) + .Matches(EF.Functions.ToTsQuery("english", searchTerm))); +} +``` + +**5. Add PostgreSQL Full-Text Search Index** +```csharp +// Create migration: Add_Search_Index +protected override void Up(MigrationBuilder migrationBuilder) +{ + migrationBuilder.Sql( + "CREATE INDEX IF NOT EXISTS ix_events_content_fts ON events " + + "USING gin(to_tsvector('english', content))"); +} +``` + +**6. Update Subscription Matching** +```csharp +// In src/Netstr/Messaging/Subscriptions/SubscriptionFilterMatcher.cs +public static bool EventMatchesFilter(Event eventItem, SubscriptionFilter filter) +{ + // Existing checks... + + if (!string.IsNullOrEmpty(filter.Search)) + { + if (!SearchMatcher.MatchesAdvancedSearch(eventItem, filter.Search)) + return false; + } + + return true; +} +``` + +**7. Configuration and Limits** +```csharp +// In src/Netstr/Options/LimitsOptions.cs +public class SearchLimits +{ + public int MaxSearchTermLength { get; set; } = 100; + public int MaxSearchResults { get; set; } = 1000; + public bool EnableAdvancedSearch { get; set; } = true; +} +``` + +--- + +### NIP-96: HTTP File Storage + +**Status**: Essential for media clients | **Impact**: Very High | **Difficulty**: High + +#### Implementation Overview +Provides REST API for file uploads/downloads with Nostr authentication integration. + +#### Step-by-Step Implementation + +**1. Create File Storage Models** +```csharp +// Create new file: src/Netstr/Models/FileStorage/UploadRequest.cs +public class FileUploadRequest +{ + public IFormFile File { get; set; } + public string? Caption { get; set; } + public long? Expiration { get; set; } + public string? MediaType { get; set; } + public string? Alt { get; set; } +} + +public class FileMetadata +{ + public string Hash { get; set; } + public string Url { get; set; } + public string MimeType { get; set; } + public long Size { get; set; } + public DateTime UploadedAt { get; set; } + public string UploadedBy { get; set; } + public DateTime? ExpiresAt { get; set; } +} +``` + +**2. Create File Storage Service** +```csharp +// Create new file: src/Netstr/Services/FileStorageService.cs +public interface IFileStorageService +{ + Task StoreFileAsync(IFormFile file, string userPubkey, FileUploadRequest request); + Task GetFileAsync(string hash); + Task GetFileMetadataAsync(string hash); + Task DeleteFileAsync(string hash, string userPubkey); +} + +public class FileStorageService : IFileStorageService +{ + private readonly string _storageRoot; + private readonly ILogger _logger; + + public async Task StoreFileAsync(IFormFile file, string userPubkey, FileUploadRequest request) + { + // 1. Calculate SHA-256 hash + var hash = await CalculateFileHashAsync(file); + + // 2. Check if file already exists + if (await FileExistsAsync(hash)) + return await GetFileMetadataAsync(hash); + + // 3. Store file + var filePath = Path.Combine(_storageRoot, hash); + using var stream = File.Create(filePath); + await file.CopyToAsync(stream); + + // 4. Store metadata in database + var metadata = new FileMetadata + { + Hash = hash, + Url = $"/files/{hash}", + MimeType = file.ContentType, + Size = file.Length, + UploadedAt = DateTime.UtcNow, + UploadedBy = userPubkey, + ExpiresAt = request.Expiration.HasValue ? + DateTimeOffset.FromUnixTimeSeconds(request.Expiration.Value).DateTime : null + }; + + await StoreMetadataAsync(metadata); + return metadata; + } +} +``` + +**3. Add Database Entities** +```csharp +// In src/Netstr/Data/FileEntity.cs +public class FileEntity +{ + public string Hash { get; set; } // Primary key + public string MimeType { get; set; } + public long Size { get; set; } + public DateTime UploadedAt { get; set; } + public string UploadedBy { get; set; } + public DateTime? ExpiresAt { get; set; } + public string? Caption { get; set; } + public string? Alt { get; set; } +} +``` + +**4. Create File Storage Controller** +```csharp +// Create new file: src/Netstr/Controllers/FileStorageController.cs +[ApiController] +public class FileStorageController : ControllerBase +{ + private readonly IFileStorageService _fileStorage; + private readonly INip98AuthService _auth; + + [HttpGet("/.well-known/nostr/nip96.json")] + public IActionResult GetServerInfo() + { + return Ok(new + { + api_url = $"{Request.Scheme}://{Request.Host}/api/v1/upload", + download_url = $"{Request.Scheme}://{Request.Host}/files", + supported_nips = new[] { 96, 98 }, + tos_url = "https://yoursite.com/tos", + content_types = new[] { "image/*", "video/*", "audio/*" }, + plans = new + { + free = new + { + name = "Free", + max_byte_size = 10_000_000, // 10MB + file_expiry = new[] { 86400, 604800 }, // 1 day, 1 week + media_transformations = new + { + image = new[] { "resizing" } + } + } + } + }); + } + + [HttpPost("/api/v1/upload")] + public async Task UploadFile([FromForm] FileUploadRequest request) + { + // 1. Validate NIP-98 authorization + var authResult = await _auth.ValidateAuthorizationAsync(Request); + if (!authResult.IsValid) + return Unauthorized(new { status = "error", message = "auth-required" }); + + // 2. Validate file + if (request.File == null || request.File.Length == 0) + return BadRequest(new { status = "error", message = "No file provided" }); + + if (request.File.Length > 10_000_000) // 10MB limit + return BadRequest(new { status = "error", message = "File too large" }); + + // 3. Store file + try + { + var metadata = await _fileStorage.StoreFileAsync(request.File, authResult.Pubkey, request); + + return Ok(new + { + status = "success", + message = "Upload successful", + nip94_event = new + { + tags = new[] + { + new[] { "url", metadata.Url }, + new[] { "x", metadata.Hash }, + new[] { "size", metadata.Size.ToString() }, + new[] { "m", metadata.MimeType } + } + }, + url = metadata.Url + }); + } + catch (Exception ex) + { + return StatusCode(500, new { status = "error", message = "Upload failed" }); + } + } + + [HttpGet("/files/{hash}")] + public async Task DownloadFile(string hash) + { + var stream = await _fileStorage.GetFileAsync(hash); + if (stream == null) + return NotFound(); + + var metadata = await _fileStorage.GetFileMetadataAsync(hash); + return File(stream, metadata.MimeType); + } +} +``` + +**5. Implement NIP-98 Authorization Service** +```csharp +// Create new file: src/Netstr/Services/Nip98AuthService.cs +public interface INip98AuthService +{ + Task ValidateAuthorizationAsync(HttpRequest request); +} + +public class Nip98AuthService : INip98AuthService +{ + public async Task ValidateAuthorizationAsync(HttpRequest request) + { + // 1. Get Authorization header + if (!request.Headers.TryGetValue("Authorization", out var authHeader)) + return AuthResult.Fail("Missing authorization header"); + + var headerValue = authHeader.ToString(); + if (!headerValue.StartsWith("Nostr ")) + return AuthResult.Fail("Invalid authorization format"); + + // 2. Decode base64 event + var base64Event = headerValue.Substring(6); + var eventJson = Encoding.UTF8.GetString(Convert.FromBase64String(base64Event)); + var authEvent = JsonSerializer.Deserialize(eventJson); + + // 3. Validate auth event + if (authEvent.Kind != 27235) + return AuthResult.Fail("Invalid auth event kind"); + + // 4. Validate signature + if (!await ValidateEventSignature(authEvent)) + return AuthResult.Fail("Invalid signature"); + + // 5. Check timestamp (within 60 seconds) + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (Math.Abs(now - authEvent.CreatedAt) > 60) + return AuthResult.Fail("Auth event too old"); + + // 6. Validate URL and method tags + var urlTag = authEvent.Tags.FirstOrDefault(t => t.Name == "u"); + var methodTag = authEvent.Tags.FirstOrDefault(t => t.Name == "method"); + + if (urlTag?.Value != GetFullUrl(request)) + return AuthResult.Fail("URL mismatch"); + + if (methodTag?.Value != request.Method) + return AuthResult.Fail("Method mismatch"); + + return AuthResult.Success(authEvent.Pubkey); + } +} +``` + +--- + +### NIP-05: DNS-based Identities + +**Status**: Widely used for verification | **Impact**: High | **Difficulty**: Low + +#### Step-by-Step Implementation + +**1. Create NIP-05 Verification Service** +```csharp +// Create new file: src/Netstr/Services/Nip05VerificationService.cs +public interface INip05VerificationService +{ + Task VerifyIdentifierAsync(string identifier, string pubkey); + Task GetVerifiedIdentifierAsync(string pubkey); +} + +public class Nip05VerificationService : INip05VerificationService +{ + private readonly HttpClient _httpClient; + private readonly IMemoryCache _cache; + + public async Task VerifyIdentifierAsync(string identifier, string pubkey) + { + try + { + // 1. Parse identifier (user@domain.com or _@domain.com) + var parts = identifier.Split('@'); + if (parts.Length != 2) + return Nip05Result.Invalid("Invalid identifier format"); + + var (user, domain) = (parts[0], parts[1]); + + // 2. Fetch .well-known/nostr.json + var url = $"https://{domain}/.well-known/nostr.json?name={user}"; + var cacheKey = $"nip05:{domain}:{user}"; + + if (_cache.TryGetValue(cacheKey, out Nip05Response? cached)) + { + return ValidateResponse(cached, user, pubkey); + } + + var response = await _httpClient.GetStringAsync(url); + var nostrJson = JsonSerializer.Deserialize(response); + + // 3. Cache for 1 hour + _cache.Set(cacheKey, nostrJson, TimeSpan.FromHours(1)); + + return ValidateResponse(nostrJson, user, pubkey); + } + catch (Exception ex) + { + return Nip05Result.Invalid($"Verification failed: {ex.Message}"); + } + } + + private Nip05Result ValidateResponse(Nip05Response response, string user, string pubkey) + { + if (response?.Names?.TryGetValue(user, out var storedPubkey) == true) + { + if (storedPubkey == pubkey) + return Nip05Result.Valid(); + else + return Nip05Result.Invalid("Pubkey mismatch"); + } + + return Nip05Result.Invalid("Name not found"); + } +} + +public class Nip05Response +{ + [JsonPropertyName("names")] + public Dictionary? Names { get; set; } + + [JsonPropertyName("relays")] + public Dictionary? Relays { get; set; } +} +``` + +**2. Add NIP-05 Validation to Event Processing** +```csharp +// Create new file: src/Netstr/Messaging/Events/Validators/Nip05Validator.cs +public class Nip05Validator : IEventValidator +{ + private readonly INip05VerificationService _nip05Service; + + public async Task ValidateEventAsync(Event e, ClientContext context) + { + // Only validate kind 0 (metadata) events + if (e.Kind != 0) + return ValidationResult.Success(); + + try + { + var content = JsonSerializer.Deserialize(e.Content); + if (!string.IsNullOrEmpty(content?.Nip05)) + { + var result = await _nip05Service.VerifyIdentifierAsync(content.Nip05, e.Pubkey); + if (!result.IsValid) + { + // Don't reject, just log for monitoring + _logger.LogWarning($"NIP-05 verification failed for {e.Pubkey}: {result.Error}"); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"NIP-05 validation error for event {e.Id}"); + } + + return ValidationResult.Success(); + } +} + +public class UserMetadata +{ + [JsonPropertyName("nip05")] + public string? Nip05 { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + // Other metadata fields... +} +``` + +--- + +## Priority 2: Ecosystem Integration NIPs + +### NIP-98: HTTP Authorization + +**Required for NIP-96 file uploads** + +Implementation details included in NIP-96 section above (`Nip98AuthService`). + +### NIP-78: Application-specific Data + +**Status**: Better client experience | **Impact**: Medium | **Difficulty**: Low + +#### Implementation +Uses addressable events (kind 30078) - leverage existing `AddressableEventHandler`: + +```csharp +// Add to EventKind enum +ApplicationSpecificData = 30078, + +// No additional handler needed - AddressableEventHandler handles it +// Events use 'd' tag with app identifier +// Content can be encrypted for private app data +``` + +## Implementation Priority Order + +1. **NIP-05** (Low effort, high adoption impact) +2. **NIP-50** (Medium effort, widely expected feature) +3. **NIP-98** (Required for file storage) +4. **NIP-96** (High effort, high value for media clients) +5. **NIP-78** (Low effort, nice-to-have) + +Each implementation can be done independently, leveraging Netstr's excellent architectural foundation. \ No newline at end of file diff --git a/packages-microsoft-prod.deb b/packages-microsoft-prod.deb new file mode 100644 index 0000000000000000000000000000000000000000..40874bc7face2e6f3b47dbd8eaff9cca5ab8ca6c GIT binary patch literal 3690 zcmai12{aUJ+g1o8yX*=HWt$m>k-}gsW1BISF(Jz^3$vIR23ewb%Th0jEa6o|D3r<; zAreW+lC`2DTe60k`Fr2a|NrNF=ljlgJ?Gr#KF@RC=XuV#&U0PoRz2lU4XE}(pOOds{B8G=f9(oq2WIVzvJJj9{{We1O!qkOgc4GkLgd>!$<4?*M1P# z$oTgSet;vXf;?hErvrF+m%PZ-aU=15JUgWl8yX_Mz54X-no`sL>xkk5WG- zm0_K5J1?i28+#3BnmyC$Y^phWR{*Gkn(a6m)oglLch_%)X7~xX$bk%VoAAcpCq@~G zZRz1rN==-77k6csJ``sFgwFFmYiPVb^=O5Q!bIq5RnD4qKO(b_b}Qa%UDzq~jmKXF z>#io<=dR%+UY1}RU{;Zd35j`~@GVvr=VdqBI?U_U6^3~Z&y4F7}vV#`VX|*=H0+&#FL;~h zVNIICI$g84Ue$oi%5|+jwLeYc2H-h3IiuulK?#i9AUVJ9r` zAocZByGcu3;lKe*guUhaYxJwJq3^l(6rU{YPOo*dxmtpD)@m0^MsM9K+TOQ$TA>N` zAy7%8Xon|z((W%_KJ{{+xusXgpfxc!L7Vc2D_})vI8jM_Q4{4@N1Xeqt1R@=9tlv$UKft0C&NmU+Xp7(}zq(YWN8I#bgx8`u4QI?9V)86V}r6efAgSpLS>?`T;0c`>$$$Ntyr5}}_RH3=0R@0Y#H z-k)?4ye(1n@~;X;f}qbwwT&oI+2-SoJBMN}G>GpkdU?0-`GSYCvu*RxMoFaO7=A(5 zXRpIQM;(5J{dg{?dIKVE_1fIc8}^IH(kn6tee^30Co8r`A2G^;oyHDBHX18ZPApAs zD1eVEDp#JP0F{GZ71o>RuRTAz7QRQ=y1H~N-q5(LN2@RlI1Z?3ke{p24gVwBQKs+p z#?phJT;7uBNR4B2qs50xJfA}3JuVuo^mBB(-c+I;K%NCYHepR~T0zN*`zW*Ekq61C zX{w0vi7sz`&hm70OF^r6W7$|pLsy)IFk1A9Ls?srhXCR3TFO}hL^=vh(oLM^#(az; zx98tK6_{JXl)W7rUFYI#R!>%_jc0C|mrR5l8DEJXX17Y&zM115;eEUkfAYeZ$-4Ra%iH}QoHELM1f)Uqg{iQ46`Ls;zyVncXlc}e1^O`6Ya+DytKw{4 zsN^8>9~~q=cG^`F(#hXVmcFVv)KXZa#{+is+ZuBeC$U0&71L7FnPAb z>B;+`fmpWuZHsvHK$SUM&h8juwO$Us<{qmkIyrJyC&%GDr>vNl9eyKIT}v(P%e=ki z5cA>J*92*dMh3II^^*N&^+rx_XMl|QVpGw3&;{@KLka+0Tf5ROsW%lkN!G=8E!)Ev zlPnbShS7ioi>q245)din`|XMM)tURP%J=9qs&)0Pk6SVVIZ;E1Z-wt6xap+cyWnhG!&(1o zJI%TuMep29aaXn7D1ts`qvl7MEg|-_&^D9hDY#;xHcl+~h~9)mEq-kBR^gj297vz` zlu}SS#yS|?qaV_~Dik#>6WKRP-s;)}6V@Sz;hL_{$4_M!{%7#sX$SwPWQRx%U+LYjhMd zZ)iH?>XX$U1s+S0GI1SqXoUv$ z!4%x|IYVUxTwhx9&e_Qte~u24da)De{U0ZPSMQS(iZ{k=_C^&z3BA z<9Ue|LcFMK;i6Yi#pN?-c^(0sd~8iBBMv_~^6}2wuE~san{-C6%qZ2%+sH&$a@t+G zDZ6HJ8;H6l9cQrpW6H^yCE1^h@tnTy=@>rRt^v)dt`V-cQidmz<5Wwv&9935oUi!Q zBAFvSG$ZmwzKA_+ap&EOsluSXpS>HAExl`<+F{QjdqbsC9ZJH@x6LW*-A7K~_4LnW zBDuD%GV#SCz!xCoESJ7#t0Az_dYtgC=&g$S*j3cPh1DaMmD0Gm7iHM>a?XK*Q(|EQ z7dBf34{(fymNqoikeQusT0{7ZUth3P4alJ?_}+YcJU+dSwnt`NNplc%UQd zz=VaoxUNf5hO%szP(k;lgi3MOAxzzByWlKDw)xbvYk_)$q~nK6?nLPe)HV3=)Hyud zJ_}zTaindnnIB9IQsL$&4)3u1>V3Q5TN^n&LB+ow?K9ZQ8ceu3#9jKr^}`$xhOw!D zw!P^OWg#3-$sK|uIwbnb`Tjlq48+*>SuMO#8&|_mbuiA;LYHw^7&sx$&)#WXE;&>g1m}Bx~|qCd+iBjh;GAnRWTfdd`T^LhOA-xBsCBF`F`Fa87#!AEiAE@ zezWS^>@48_;&Zfd_h0)Hlhl5;PL1cQZ17zkkV|05`XQw6+}Q2IQEtOjZ|Kj`V{`8b z@;lAH(mjB!Ax}&nf2eSjeG??Hq zPiar!?7aPt3uOY_aErh=4NF|NW=Sg`!%L!hF2>3&5hq( zvxZ}1p14JPAGx%L8&g}c!^PYUf6`9r1hf`Q)sG8obLB$U%h}VfHdJnIx3YJ{y12ma zt@b<%=50G-H5^`^|A46&&b||mP_jJ8Uru%wPs3B`L_Cr5Yi#%hkdYzums^2MAph%X z|1vq4fd8rcdvehI8xT0Bq2VP#XjNNO~xiYJfyWGXjY2My^a-n7+eSQHfFj03Zvp@I5LI27y!CfhhV1wu^Fp$K|dfQcQ-9vo;G9S|LeG;|RGMBwe5 zNC7Zg7qq(}&C5L?5E{nvpn$xb(O8s$i={Tj6&gu0z>=ZvCPDaUD4nh!O(enmqgi-Q zR1}%+AOZ-YvApO+k`oe(CmB(3WG3DKZHS`LEg``a5{M8+z&W@$Q=DN|BnQioASWo$ io{6<&5v{^NmOu;)c3K2*nnp!nM1Io=ceehwWd0A>I)OX@ literal 0 HcmV?d00001 diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index f4a988a..2accc3f 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -10,6 +10,7 @@ using Netstr.Messaging.Subscriptions.Validators; using Netstr.Messaging.WebSockets; using Netstr.Middleware; +using Netstr.Services; namespace Netstr.Extensions { @@ -21,6 +22,9 @@ public static IServiceCollection AddMessaging(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + + // NIP-05 verification service + services.AddHttpClient(); // message services.AddSingleton(); @@ -72,6 +76,7 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv //services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); diff --git a/src/Netstr/Messaging/Events/DbExtensions.cs b/src/Netstr/Messaging/Events/DbExtensions.cs index a78bdef..b92d4cb 100644 --- a/src/Netstr/Messaging/Events/DbExtensions.cs +++ b/src/Netstr/Messaging/Events/DbExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Netstr.Data; +using System.Linq.Expressions; namespace Netstr.Messaging.Events { @@ -9,5 +10,49 @@ public static Task IsDeleted(this DbSet db, string id) { return db.AnyAsync(x => x.EventId == id && x.DeletedAt.HasValue); } + + /// + /// Filters events by search term using PostgreSQL full-text search (NIP-50) + /// + public static IQueryable WhereMatchesSearch( + this IQueryable query, + string? searchTerm) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + return query; + + var normalizedSearchTerm = searchTerm.Trim(); + + // Use PostgreSQL full-text search for better performance + try + { + // Convert search term to tsquery format + var tsQuery = ConvertToTsQuery(normalizedSearchTerm); + + return query.Where(e => + EF.Functions.ToTsVector("english", e.EventContent) + .Matches(EF.Functions.ToTsQuery("english", tsQuery))); + } + catch + { + // Fallback to simple LIKE search if full-text search fails + return query.Where(e => e.EventContent.ToLower().Contains(normalizedSearchTerm.ToLower())); + } + } + + /// + /// Converts a search term to PostgreSQL tsquery format + /// + private static string ConvertToTsQuery(string searchTerm) + { + // Split terms and join with AND operator + var terms = searchTerm.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(term => term.Replace("'", "''")) // Escape single quotes + .Where(term => !string.IsNullOrWhiteSpace(term)) + .Select(term => $"'{term}'") + .ToArray(); + + return string.Join(" & ", terms); + } } } diff --git a/src/Netstr/Messaging/Events/Validators/Nip05Validator.cs b/src/Netstr/Messaging/Events/Validators/Nip05Validator.cs new file mode 100644 index 0000000..e1de786 --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/Nip05Validator.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Netstr.Messaging.Models; +using Netstr.Services; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Validator for NIP-05 DNS-based identity verification in metadata events + /// Note: This validator doesn't reject events, it just performs verification for monitoring + /// + public class Nip05Validator : IEventValidator + { + private readonly INip05VerificationService _nip05Service; + private readonly ILogger _logger; + + public Nip05Validator( + INip05VerificationService nip05Service, + ILogger logger) + { + this._nip05Service = nip05Service; + this._logger = logger; + } + + public string? Validate(Event e, ClientContext context) + { + // Only validate kind 0 (metadata) events + if (e.Kind != 0) + return null; // Success - no validation error + + // NIP-05 validation is async, so we'll do it in a background task + // to avoid blocking event processing + _ = Task.Run(async () => await ValidateNip05Async(e)); + + // Never reject events based on NIP-05 validation + // This is for verification tracking only + return null; // Success - always allow events to pass through + } + + private async Task ValidateNip05Async(Event e) + { + try + { + if (string.IsNullOrWhiteSpace(e.Content)) + return; + + var metadata = JsonSerializer.Deserialize(e.Content); + if (metadata?.Nip05 == null) + return; + + this._logger.LogDebug($"Validating NIP-05 identifier '{metadata.Nip05}' for pubkey {e.PublicKey}"); + + var result = await this._nip05Service.VerifyIdentifierAsync(metadata.Nip05, e.PublicKey); + + if (result.IsValid) + { + this._logger.LogInformation($"NIP-05 verification successful: {metadata.Nip05} -> {e.PublicKey}"); + } + else + { + this._logger.LogWarning($"NIP-05 verification failed for {e.PublicKey} claiming '{metadata.Nip05}': {result.Error}"); + } + } + catch (JsonException ex) + { + this._logger.LogWarning($"Failed to parse metadata JSON for NIP-05 validation in event {e.Id}: {ex.Message}"); + } + catch (Exception ex) + { + this._logger.LogError(ex, $"Unexpected error during NIP-05 validation for event {e.Id}"); + } + } + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/MessageHandlers/FilterMessageHandlerBase.cs b/src/Netstr/Messaging/MessageHandlers/FilterMessageHandlerBase.cs index 233555c..b553bce 100644 --- a/src/Netstr/Messaging/MessageHandlers/FilterMessageHandlerBase.cs +++ b/src/Netstr/Messaging/MessageHandlers/FilterMessageHandlerBase.cs @@ -154,6 +154,7 @@ private SubscriptionFilter GetSubscriptionFilter(string subscriptionId, JsonDocu r.Since, r.Until, r.Limit, + r.Search, orTags, andTags); } diff --git a/src/Netstr/Messaging/Models/EventKind.cs b/src/Netstr/Messaging/Models/EventKind.cs index 7606044..699d49a 100644 --- a/src/Netstr/Messaging/Models/EventKind.cs +++ b/src/Netstr/Messaging/Models/EventKind.cs @@ -42,7 +42,10 @@ public enum EventKind InterestSets = 30015, EmojiSets = 30030, ReleaseArtifactSets = 30063, - AppCurationSets = 30267 + AppCurationSets = 30267, + + // NIP-78 Application-specific Data + ApplicationSpecificData = 30078 } /// diff --git a/src/Netstr/Messaging/Models/Nip05/Nip05Response.cs b/src/Netstr/Messaging/Models/Nip05/Nip05Response.cs new file mode 100644 index 0000000..54abfe7 --- /dev/null +++ b/src/Netstr/Messaging/Models/Nip05/Nip05Response.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Netstr.Messaging.Models.Nip05 +{ + /// + /// Response format for NIP-05 DNS-based identity verification + /// from /.well-known/nostr.json endpoints + /// + public class Nip05Response + { + /// + /// Mapping of names to public keys + /// + [JsonPropertyName("names")] + public Dictionary? Names { get; set; } + + /// + /// Optional mapping of public keys to relay URLs + /// + [JsonPropertyName("relays")] + public Dictionary? Relays { get; set; } + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/Models/Nip05/Nip05Result.cs b/src/Netstr/Messaging/Models/Nip05/Nip05Result.cs new file mode 100644 index 0000000..f0a97e6 --- /dev/null +++ b/src/Netstr/Messaging/Models/Nip05/Nip05Result.cs @@ -0,0 +1,22 @@ +namespace Netstr.Messaging.Models.Nip05 +{ + /// + /// Result of NIP-05 verification attempt + /// + public class Nip05Result + { + public bool IsValid { get; } + public string? Error { get; } + public DateTime VerifiedAt { get; } + + private Nip05Result(bool isValid, string? error = null) + { + IsValid = isValid; + Error = error; + VerifiedAt = DateTime.UtcNow; + } + + public static Nip05Result Valid() => new(true); + public static Nip05Result Invalid(string error) => new(false, error); + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/Models/SubscriptionFilter.cs b/src/Netstr/Messaging/Models/SubscriptionFilter.cs index 444198c..b3a501e 100644 --- a/src/Netstr/Messaging/Models/SubscriptionFilter.cs +++ b/src/Netstr/Messaging/Models/SubscriptionFilter.cs @@ -26,6 +26,9 @@ public record SubscriptionFilterRequest [JsonPropertyName("limit")] public int? Limit { get; init; } + [JsonPropertyName("search")] + public string? Search { get; init; } + [JsonExtensionData] public Dictionary? AdditionalData { get; set; } } @@ -37,11 +40,12 @@ public record SubscriptionFilter( DateTimeOffset? Since, DateTimeOffset? Until, int? Limit, + string? Search, Dictionary OrTags, Dictionary AndTags) { public SubscriptionFilter() - : this([], [], [], null, null, null, [], []) + : this([], [], [], null, null, null, null, [], []) { } } diff --git a/src/Netstr/Messaging/Models/UserMetadata.cs b/src/Netstr/Messaging/Models/UserMetadata.cs new file mode 100644 index 0000000..6c1e3d3 --- /dev/null +++ b/src/Netstr/Messaging/Models/UserMetadata.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace Netstr.Messaging.Models +{ + /// + /// User metadata structure for kind 0 events + /// + public class UserMetadata + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("about")] + public string? About { get; set; } + + [JsonPropertyName("picture")] + public string? Picture { get; set; } + + [JsonPropertyName("banner")] + public string? Banner { get; set; } + + [JsonPropertyName("nip05")] + public string? Nip05 { get; set; } + + [JsonPropertyName("lud06")] + public string? Lud06 { get; set; } + + [JsonPropertyName("lud16")] + public string? Lud16 { get; set; } + + [JsonPropertyName("website")] + public string? Website { get; set; } + + [JsonPropertyName("display_name")] + public string? DisplayName { get; set; } + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs index 496b6af..07998b6 100644 --- a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs +++ b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Netstr.Data; +using Netstr.Messaging.Events; using Netstr.Messaging.Models; namespace Netstr.Messaging.Subscriptions @@ -33,6 +34,7 @@ public static IQueryable WhereAnyFilterMatches( (filter.Kinds.Contains(x.EventKind) || !filter.Kinds.Any()) && (filter.Since <= x.EventCreatedAt || !filter.Since.HasValue) && (filter.Until >= x.EventCreatedAt || !filter.Until.HasValue)) + .WhereMatchesSearch(filter.Search) .WhereOrTags(filter.OrTags) .WhereAndTags(filter.AndTags) .Where(x => !protectedKinds.Contains(x.EventKind) || x.EventPublicKey == authenticatedPublicKey || x.Tags.Any(tag => tag.Name == EventTag.PublicKey && tag.Value == authenticatedPublicKey)) diff --git a/src/Netstr/Messaging/Subscriptions/SearchMatcher.cs b/src/Netstr/Messaging/Subscriptions/SearchMatcher.cs new file mode 100644 index 0000000..b7b21fe --- /dev/null +++ b/src/Netstr/Messaging/Subscriptions/SearchMatcher.cs @@ -0,0 +1,156 @@ +using Netstr.Messaging.Models; + +namespace Netstr.Messaging.Subscriptions +{ + /// + /// Utility class for matching events against search terms (NIP-50) + /// + public static class SearchMatcher + { + /// + /// Checks if an event matches the given search term + /// + /// The event to match + /// The search term to match against + /// True if the event matches the search term + public static bool MatchesSearch(Event eventItem, string? searchTerm) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + return true; + + if (string.IsNullOrWhiteSpace(eventItem.Content)) + return false; + + var content = eventItem.Content.ToLowerInvariant(); + var normalizedSearchTerm = searchTerm.ToLowerInvariant().Trim(); + + // Check for advanced search extensions + if (normalizedSearchTerm.Contains(':')) + { + return MatchesAdvancedSearch(eventItem, normalizedSearchTerm); + } + + // Basic text search - split on spaces and require all terms + var terms = normalizedSearchTerm.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return terms.All(term => content.Contains(term)); + } + + /// + /// Handles advanced search with extensions like "include:spam", "domain:example.com" + /// + private static bool MatchesAdvancedSearch(Event eventItem, string searchTerm) + { + var parts = ParseSearchTerms(searchTerm); + var content = eventItem.Content.ToLowerInvariant(); + + foreach (var (extension, value) in parts.Extensions) + { + if (!ApplySearchExtension(eventItem, extension, value)) + return false; + } + + // Apply basic text search if there are remaining terms + if (!string.IsNullOrEmpty(parts.BasicSearch)) + { + var terms = parts.BasicSearch.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (!terms.All(term => content.Contains(term))) + return false; + } + + return true; + } + + /// + /// Parses search terms into extensions and basic text search + /// + private static (string BasicSearch, List<(string Extension, string Value)> Extensions) ParseSearchTerms(string searchTerm) + { + var extensions = new List<(string, string)>(); + var basicTerms = new List(); + + var terms = searchTerm.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + foreach (var term in terms) + { + if (term.Contains(':') && !term.StartsWith("http")) + { + var colonIndex = term.IndexOf(':'); + var extension = term[..colonIndex]; + var value = term[(colonIndex + 1)..]; + extensions.Add((extension, value)); + } + else + { + basicTerms.Add(term); + } + } + + return (string.Join(' ', basicTerms), extensions); + } + + /// + /// Applies a search extension filter + /// + private static bool ApplySearchExtension(Event eventItem, string extension, string value) + { + return extension.ToLowerInvariant() switch + { + "include" => ApplyIncludeFilter(eventItem, value), + "domain" => ApplyDomainFilter(eventItem, value), + "kind" => ApplyKindFilter(eventItem, value), + "since" => ApplySinceFilter(eventItem, value), + "until" => ApplyUntilFilter(eventItem, value), + _ => true // Unknown extensions are ignored + }; + } + + private static bool ApplyIncludeFilter(Event eventItem, string value) + { + // Include filter for specific content types + var content = eventItem.Content.ToLowerInvariant(); + return value.ToLowerInvariant() switch + { + "spam" => false, // Could integrate with spam detection + "replies" => eventItem.Tags.Any(tag => tag.Length > 1 && tag[0] == "e"), + "mentions" => eventItem.Tags.Any(tag => tag.Length > 1 && tag[0] == "p"), + _ => content.Contains(value.ToLowerInvariant()) + }; + } + + private static bool ApplyDomainFilter(Event eventItem, string domain) + { + // Filter by domain mentioned in content + var content = eventItem.Content.ToLowerInvariant(); + return content.Contains(domain.ToLowerInvariant()); + } + + private static bool ApplyKindFilter(Event eventItem, string kindValue) + { + if (long.TryParse(kindValue, out var kind)) + { + return eventItem.Kind == kind; + } + return false; + } + + private static bool ApplySinceFilter(Event eventItem, string sinceValue) + { + if (long.TryParse(sinceValue, out var sinceTimestamp)) + { + var sinceDate = DateTimeOffset.FromUnixTimeSeconds(sinceTimestamp); + return eventItem.CreatedAt >= sinceDate; + } + return false; + } + + private static bool ApplyUntilFilter(Event eventItem, string untilValue) + { + if (long.TryParse(untilValue, out var untilTimestamp)) + { + var untilDate = DateTimeOffset.FromUnixTimeSeconds(untilTimestamp); + return eventItem.CreatedAt <= untilDate; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/Subscriptions/SubscriptionFilterMatcher.cs b/src/Netstr/Messaging/Subscriptions/SubscriptionFilterMatcher.cs index e4260cb..d564791 100644 --- a/src/Netstr/Messaging/Subscriptions/SubscriptionFilterMatcher.cs +++ b/src/Netstr/Messaging/Subscriptions/SubscriptionFilterMatcher.cs @@ -16,6 +16,7 @@ public static bool IsMatch(SubscriptionFilter filter, Event e) () => filter.Kinds.EmptyOrAny(x => x == e.Kind), () => !filter.Since.HasValue || filter.Since <= e.CreatedAt, () => !filter.Until.HasValue || filter.Until >= e.CreatedAt, + () => SearchMatcher.MatchesSearch(e, filter.Search), () => filter.OrTags.All(tag => e.Tags.Any(x => tag.Key == x[0] && tag.Value.Contains(x[1]))), () => filter.AndTags.All(tag => tag.Value.All(tagValue => e.Tags.Any(eTag => eTag[0] == tag.Key && eTag[1] == tagValue))) ]; diff --git a/src/Netstr/Options/Limits/SearchLimits.cs b/src/Netstr/Options/Limits/SearchLimits.cs new file mode 100644 index 0000000..a021d57 --- /dev/null +++ b/src/Netstr/Options/Limits/SearchLimits.cs @@ -0,0 +1,33 @@ +namespace Netstr.Options.Limits +{ + /// + /// Configuration limits for NIP-50 search functionality + /// + public class SearchLimits + { + /// + /// Maximum length of search terms + /// + public int MaxSearchTermLength { get; set; } = 100; + + /// + /// Maximum number of search results returned + /// + public int MaxSearchResults { get; set; } = 1000; + + /// + /// Enable advanced search extensions (include:, domain:, etc.) + /// + public bool EnableAdvancedSearch { get; set; } = true; + + /// + /// Enable PostgreSQL full-text search for better performance + /// + public bool EnableFullTextSearch { get; set; } = true; + + /// + /// Minimum search term length required + /// + public int MinSearchTermLength { get; set; } = 2; + } +} \ No newline at end of file diff --git a/src/Netstr/Options/LimitsOptions.cs b/src/Netstr/Options/LimitsOptions.cs index 62866f5..82b0d9d 100644 --- a/src/Netstr/Options/LimitsOptions.cs +++ b/src/Netstr/Options/LimitsOptions.cs @@ -9,6 +9,7 @@ public LimitsOptions() Subscriptions = new(); Events = new(); Negentropy = new(); + Search = new(); } public int MaxPayloadSize { get; init; } @@ -18,5 +19,7 @@ public LimitsOptions() public required EventLimits Events { get; init; } public required NegentropyLimits Negentropy { get; init; } + + public required SearchLimits Search { get; init; } } } diff --git a/src/Netstr/Services/Nip05VerificationService.cs b/src/Netstr/Services/Nip05VerificationService.cs new file mode 100644 index 0000000..844113e --- /dev/null +++ b/src/Netstr/Services/Nip05VerificationService.cs @@ -0,0 +1,199 @@ +using Microsoft.Extensions.Caching.Memory; +using System.Text.Json; +using Netstr.Messaging.Models.Nip05; + +namespace Netstr.Services +{ + /// + /// Service for verifying NIP-05 DNS-based identities + /// + public interface INip05VerificationService + { + Task VerifyIdentifierAsync(string identifier, string pubkey); + Task GetVerifiedIdentifierAsync(string pubkey); + Task IsIdentifierVerifiedAsync(string identifier, string pubkey); + } + + public class Nip05VerificationService : INip05VerificationService + { + private readonly HttpClient _httpClient; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + + // Cache keys + private const string CACHE_KEY_PREFIX = "nip05"; + private const string VERIFIED_CACHE_PREFIX = "nip05_verified"; + + // Cache expiration times + private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromHours(1); + private static readonly TimeSpan FAILED_CACHE_DURATION = TimeSpan.FromMinutes(15); + + public Nip05VerificationService( + HttpClient httpClient, + IMemoryCache cache, + ILogger logger) + { + _httpClient = httpClient; + _cache = cache; + _logger = logger; + + // Configure HttpClient for NIP-05 requests + _httpClient.Timeout = TimeSpan.FromSeconds(10); + _httpClient.DefaultRequestHeaders.Add("User-Agent", "Netstr/2.0 (NIP-05)"); + } + + public async Task VerifyIdentifierAsync(string identifier, string pubkey) + { + try + { + if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(pubkey)) + { + return Nip05Result.Invalid("Invalid identifier or pubkey"); + } + + // Parse identifier (user@domain.com or _@domain.com) + var parts = identifier.Split('@'); + if (parts.Length != 2) + { + return Nip05Result.Invalid("Invalid identifier format - must be user@domain"); + } + + var (user, domain) = (parts[0], parts[1]); + + // Validate domain format + if (string.IsNullOrWhiteSpace(domain) || domain.Contains(' ')) + { + return Nip05Result.Invalid("Invalid domain format"); + } + + // Check cache first + var cacheKey = $"{CACHE_KEY_PREFIX}:{domain}:{user}"; + if (_cache.TryGetValue(cacheKey, out Nip05CacheEntry? cached) && cached?.Response != null) + { + _logger.LogDebug($"NIP-05 cache hit for {identifier}"); + return ValidateResponse(cached.Response, user, pubkey); + } + + // Fetch .well-known/nostr.json + var url = $"https://{domain}/.well-known/nostr.json?name={user}"; + _logger.LogDebug($"Fetching NIP-05 verification from {url}"); + + try + { + var response = await _httpClient.GetStringAsync(url); + var nostrJson = JsonSerializer.Deserialize(response); + + if (nostrJson == null) + { + var result = Nip05Result.Invalid("Invalid response format"); + CacheFailedResult(cacheKey); + return result; + } + + // Cache successful response + var cacheEntry = new Nip05CacheEntry { Response = nostrJson, FetchedAt = DateTime.UtcNow }; + _cache.Set(cacheKey, cacheEntry, CACHE_DURATION); + + var validationResult = ValidateResponse(nostrJson, user, pubkey); + + // Cache verified status if successful + if (validationResult.IsValid) + { + var verifiedCacheKey = $"{VERIFIED_CACHE_PREFIX}:{pubkey}"; + _cache.Set(verifiedCacheKey, identifier, CACHE_DURATION); + } + + return validationResult; + } + catch (HttpRequestException ex) + { + _logger.LogWarning($"HTTP error fetching NIP-05 for {identifier}: {ex.Message}"); + var result = Nip05Result.Invalid($"Failed to fetch verification: {ex.Message}"); + CacheFailedResult(cacheKey); + return result; + } + catch (TaskCanceledException ex) + { + _logger.LogWarning($"Timeout fetching NIP-05 for {identifier}: {ex.Message}"); + var result = Nip05Result.Invalid("Request timeout"); + CacheFailedResult(cacheKey); + return result; + } + catch (JsonException ex) + { + _logger.LogWarning($"JSON parsing error for NIP-05 {identifier}: {ex.Message}"); + var result = Nip05Result.Invalid("Invalid JSON response"); + CacheFailedResult(cacheKey); + return result; + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Unexpected error verifying NIP-05 for {identifier}"); + return Nip05Result.Invalid($"Verification failed: {ex.Message}"); + } + } + + public Task GetVerifiedIdentifierAsync(string pubkey) + { + if (string.IsNullOrWhiteSpace(pubkey)) + return Task.FromResult(null); + + var cacheKey = $"{VERIFIED_CACHE_PREFIX}:{pubkey}"; + if (_cache.TryGetValue(cacheKey, out string? cachedIdentifier)) + { + return Task.FromResult(cachedIdentifier); + } + + return Task.FromResult(null); + } + + public async Task IsIdentifierVerifiedAsync(string identifier, string pubkey) + { + var result = await VerifyIdentifierAsync(identifier, pubkey); + return result.IsValid; + } + + private Nip05Result ValidateResponse(Nip05Response response, string user, string pubkey) + { + if (response?.Names == null) + { + return Nip05Result.Invalid("No names found in response"); + } + + if (response.Names.TryGetValue(user, out var storedPubkey)) + { + if (string.Equals(storedPubkey, pubkey, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation($"NIP-05 verification successful for {user} -> {pubkey}"); + return Nip05Result.Valid(); + } + else + { + _logger.LogWarning($"NIP-05 pubkey mismatch for {user}: expected {pubkey}, got {storedPubkey}"); + return Nip05Result.Invalid("Public key mismatch"); + } + } + + _logger.LogWarning($"NIP-05 name {user} not found in response"); + return Nip05Result.Invalid("Name not found in verification response"); + } + + private void CacheFailedResult(string cacheKey) + { + // Cache failed results for shorter duration to prevent repeated failed requests + var failedEntry = new Nip05CacheEntry + { + Response = null, + FetchedAt = DateTime.UtcNow + }; + _cache.Set(cacheKey, failedEntry, FAILED_CACHE_DURATION); + } + + private class Nip05CacheEntry + { + public Nip05Response? Response { get; set; } + public DateTime FetchedAt { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 86a0794..746ea15 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -54,6 +54,13 @@ "MaxSubscriptionAgeSeconds": 300, "StaleSubscriptionPeriodSeconds": 60, "FrameSizeLimit": 524288 + }, + "Search": { + "MaxSearchTermLength": 100, + "MaxSearchResults": 1000, + "EnableAdvancedSearch": true, + "EnableFullTextSearch": true, + "MinSearchTermLength": 2 } }, "Cleanup": { @@ -77,7 +84,7 @@ "Description": "A nostr relay", "PublicKey": "NA", "Contact": "NA", - "SupportedNips": [ 1, 2, 4, 9, 11, 13, 17, 40, 42, 45, 51, 57, 62, 65, 70, 77, 119 ], + "SupportedNips": [ 1, 2, 4, 5, 9, 11, 13, 17, 40, 42, 45, 50, 51, 57, 59, 62, 65, 70, 77, 78, 119 ], "Version": "v2.0.1" }, "Whitelist": { diff --git a/test/Netstr.Tests/NIPs/01.feature.cs b/test/Netstr.Tests/NIPs/01.feature.cs index 9a840ba..320e079 100644 --- a/test/Netstr.Tests/NIPs/01.feature.cs +++ b/test/Netstr.Tests/NIPs/01.feature.cs @@ -124,8 +124,8 @@ public void InvalidMessagesAreDiscardedValidOnesAccepted() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Invalid messages are discarded, valid ones accepted", "\tRelay shouldn\'t broadcast messages with invalid Id or Signnature. It should also" + - " reply with OK false.\r\n\tThis also covers correct validation of events with speci" + - "al characters", tagsOfScenario, argumentsOfScenario, featureTags); + " reply with OK false.\n\tThis also covers correct validation of events with specia" + + "l characters", tagsOfScenario, argumentsOfScenario, featureTags); #line 16 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -338,8 +338,8 @@ public void ClosedSubscriptionsShouldNoLongerReceiveEvents() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Closed subscriptions should no longer receive events", "\tAfter a subscription is closed the relay should no longer forward events for tha" + - "t subscription\r\n\tHowever it should still forward them for other existing subscri" + - "ptions", tagsOfScenario, argumentsOfScenario, featureTags); + "t subscription\n\tHowever it should still forward them for other existing subscrip" + + "tions", tagsOfScenario, argumentsOfScenario, featureTags); #line 65 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/04.feature.cs b/test/Netstr.Tests/NIPs/04.feature.cs index 4a0a92f..778a5a5 100644 --- a/test/Netstr.Tests/NIPs/04.feature.cs +++ b/test/Netstr.Tests/NIPs/04.feature.cs @@ -114,8 +114,8 @@ public void NotAuthenticatedClientTriesToFetchKind4Events() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Not authenticated client tries to fetch kind 4 events", "\tAlice can\'t fetch kind 4 events when she isn\'t authenticated\r\n\tThis should be tr" + - "ue even when multiple filters are used", tagsOfScenario, argumentsOfScenario, featureTags); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Not authenticated client tries to fetch kind 4 events", "\tAlice can\'t fetch kind 4 events when she isn\'t authenticated\n\tThis should be tru" + + "e even when multiple filters are used", tagsOfScenario, argumentsOfScenario, featureTags); #line 13 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/05.feature b/test/Netstr.Tests/NIPs/05.feature new file mode 100644 index 0000000..2551761 --- /dev/null +++ b/test/Netstr.Tests/NIPs/05.feature @@ -0,0 +1,28 @@ +Feature: NIP-05 DNS-based Identities + Tests for NIP-05 DNS-based identity verification implementation + + Background: + Given a relay at "wss://localhost:5001" + And a user Alice + And Alice is connected to the relay + + Scenario: Accept metadata event with valid NIP-05 identifier + When Alice publishes a metadata event with NIP-05 identifier "alice@example.com" + Then the relay accepts the event + And the event is stored in the database + + Scenario: Accept metadata event with invalid NIP-05 identifier + When Alice publishes a metadata event with NIP-05 identifier "invalid-format" + Then the relay accepts the event + And the event is stored in the database + # Note: NIP-05 validation doesn't reject events, only logs verification results + + Scenario: Accept metadata event without NIP-05 identifier + When Alice publishes a metadata event without NIP-05 identifier + Then the relay accepts the event + And the event is stored in the database + + Scenario: Handle metadata event with empty NIP-05 identifier + When Alice publishes a metadata event with empty NIP-05 identifier + Then the relay accepts the event + And the event is stored in the database \ No newline at end of file diff --git a/test/Netstr.Tests/NIPs/05.feature.cs b/test/Netstr.Tests/NIPs/05.feature.cs new file mode 100644 index 0000000..a99e60c --- /dev/null +++ b/test/Netstr.Tests/NIPs/05.feature.cs @@ -0,0 +1,252 @@ +// ------------------------------------------------------------------------------ +// +// 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_05DNS_BasedIdentitiesFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "05.feature" +#line hidden + + public NIP_05DNS_BasedIdentitiesFeature(NIP_05DNS_BasedIdentitiesFeature.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-05 DNS-based Identities", " Tests for NIP-05 DNS-based identity verification implementation", 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 4 + #line hidden +#line 5 + testRunner.Given("a relay at \"wss://localhost:5001\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 6 + testRunner.And("a user Alice", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 7 + testRunner.And("Alice is connected to the relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with valid NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] + [Xunit.TraitAttribute("Description", "Accept metadata event with valid NIP-05 identifier")] + public void AcceptMetadataEventWithValidNIP_05Identifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with valid NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 9 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 10 + testRunner.When("Alice publishes a metadata event with NIP-05 identifier \"alice@example.com\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 11 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 12 + testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with invalid NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] + [Xunit.TraitAttribute("Description", "Accept metadata event with invalid NIP-05 identifier")] + public void AcceptMetadataEventWithInvalidNIP_05Identifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with invalid NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 14 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 15 + testRunner.When("Alice publishes a metadata event with NIP-05 identifier \"invalid-format\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 16 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 17 + testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event without NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] + [Xunit.TraitAttribute("Description", "Accept metadata event without NIP-05 identifier")] + public void AcceptMetadataEventWithoutNIP_05Identifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event without NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 20 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 21 + testRunner.When("Alice publishes a metadata event without NIP-05 identifier", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 22 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 23 + testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Handle metadata event with empty NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] + [Xunit.TraitAttribute("Description", "Handle metadata event with empty NIP-05 identifier")] + public void HandleMetadataEventWithEmptyNIP_05Identifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Handle metadata event with empty NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 25 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 26 + testRunner.When("Alice publishes a metadata event with empty NIP-05 identifier", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 27 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 28 + testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#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_05DNS_BasedIdentitiesFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_05DNS_BasedIdentitiesFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/09.feature.cs b/test/Netstr.Tests/NIPs/09.feature.cs index 4f59b54..bf498ee 100644 --- a/test/Netstr.Tests/NIPs/09.feature.cs +++ b/test/Netstr.Tests/NIPs/09.feature.cs @@ -41,8 +41,8 @@ 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-09", "\tA special event with kind 5, meaning \"deletion\" is defined as having a list of o" + - "ne or more e or a tags, \r\n\teach referencing an event the author is requesting to" + - " be deleted.", ProgrammingLanguage.CSharp, featureTags); + "ne or more e or a tags, \n\teach referencing an event the author is requesting to " + + "be deleted.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } diff --git a/test/Netstr.Tests/NIPs/11.feature.cs b/test/Netstr.Tests/NIPs/11.feature.cs index 0694a28..72794e4 100644 --- a/test/Netstr.Tests/NIPs/11.feature.cs +++ b/test/Netstr.Tests/NIPs/11.feature.cs @@ -41,8 +41,8 @@ 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-11", "\tRelays may provide server metadata to clients to inform them of capabilities, ad" + - "ministrative contacts, and various server attributes.\r\n\tThis is made available a" + - "s a JSON document over HTTP, on the same URI as the relay\'s websocket.", ProgrammingLanguage.CSharp, featureTags); + "ministrative contacts, and various server attributes.\n\tThis is made available as" + + " a JSON document over HTTP, on the same URI as the relay\'s websocket.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -108,7 +108,7 @@ public void RelaySendsAnInformationDocument() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Relay sends an information document", "\tGET HTTP request to the websockets endpoint with a application/nostr+json Accept" + - " header should\r\n\tproduce a json Relay Information Document", tagsOfScenario, argumentsOfScenario, featureTags); + " header should\n\tproduce a json Relay Information Document", tagsOfScenario, argumentsOfScenario, featureTags); #line 11 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/13.feature.cs b/test/Netstr.Tests/NIPs/13.feature.cs index 0043d3c..26ee4b5 100644 --- a/test/Netstr.Tests/NIPs/13.feature.cs +++ b/test/Netstr.Tests/NIPs/13.feature.cs @@ -40,8 +40,8 @@ public NIP_13Feature(NIP_13Feature.FixtureData fixtureData, Netstr_Tests_XUnitAs 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-13", "\t Proof of Work (PoW) is a way to add a proof of computational work to a note.\r\n\t" + - " This proof can be used as a means of spam deterrence.", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-13", "\t Proof of Work (PoW) is a way to add a proof of computational work to a note.\n\t " + + "This proof can be used as a means of spam deterrence.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -115,8 +115,8 @@ public void MessagesWithLowDifficultyAndThoseOffTargetAreRejectedThoseWithHighAn string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Messages with low difficulty and those off target are rejected, those with high a" + - "nd on target difficulty accepted", "\t1) Low diff\r\n\t2) High diff but doesn\'t match target\r\n\t3) High diff\r\n\t4) High dif" + - "f matching target", tagsOfScenario, argumentsOfScenario, featureTags); + "nd on target difficulty accepted", "\t1) Low diff\n\t2) High diff but doesn\'t match target\n\t3) High diff\n\t4) High diff m" + + "atching target", tagsOfScenario, argumentsOfScenario, featureTags); #line 13 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/40.feature.cs b/test/Netstr.Tests/NIPs/40.feature.cs index 155056e..5238c92 100644 --- a/test/Netstr.Tests/NIPs/40.feature.cs +++ b/test/Netstr.Tests/NIPs/40.feature.cs @@ -221,8 +221,8 @@ public void ExpiredEventAlreadySavedInARelayIsOmittedFromSubResponse() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Expired event already saved in a relay is omitted from sub response", "\tWe need to save an already expired event in the relay, that would be hard using " + - "the publishing step (relay would reject it)\r\n\tSo just introduce a new step for t" + - "his NIP which bypasses publishing and inserts directly into DB", tagsOfScenario, argumentsOfScenario, featureTags); + "the publishing step (relay would reject it)\n\tSo just introduce a new step for th" + + "is NIP which bypasses publishing and inserts directly into DB", tagsOfScenario, argumentsOfScenario, featureTags); #line 31 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/45.feature.cs b/test/Netstr.Tests/NIPs/45.feature.cs index eb4a0d2..8ecc18c 100644 --- a/test/Netstr.Tests/NIPs/45.feature.cs +++ b/test/Netstr.Tests/NIPs/45.feature.cs @@ -250,9 +250,9 @@ public void CountingSomeoneElsesDMsReturnsOnlyThoseFromMe() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Counting someone elses DMs returns only those from me", "\tBob sends a DM to Charlie\r\n\tAlice sends a DM to Charlie\r\n\tAlice tries to count a" + - "ll Charlie\'s DMs but only those from her are counted\r\n\tCharlie counts his own DM" + - "s which should return count of all", tagsOfScenario, argumentsOfScenario, featureTags); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Counting someone elses DMs returns only those from me", "\tBob sends a DM to Charlie\n\tAlice sends a DM to Charlie\n\tAlice tries to count all" + + " Charlie\'s DMs but only those from her are counted\n\tCharlie counts his own DMs w" + + "hich should return count of all", tagsOfScenario, argumentsOfScenario, featureTags); #line 41 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/51.feature b/test/Netstr.Tests/NIPs/51.feature index eca3e6b..fb29494 100644 --- a/test/Netstr.Tests/NIPs/51.feature +++ b/test/Netstr.Tests/NIPs/51.feature @@ -41,8 +41,8 @@ Feature: NIP-51 Lists When Alice publishes an event with kind 30030 and tags: | d | custom-emojis | | name | My Custom Emojis | - | emoji | happy | https://example.com/happy.png | - | emoji | sad | https://example.com/sad.png | + | emoji | happy,https://example.com/happy.png | + | emoji | sad,https://example.com/sad.png | Then the relay accepts the event When Alice subscribes to events with kind 30030 Then Alice receives 1 event diff --git a/test/Netstr.Tests/NIPs/51.feature.cs b/test/Netstr.Tests/NIPs/51.feature.cs new file mode 100644 index 0000000..f25b1a7 --- /dev/null +++ b/test/Netstr.Tests/NIPs/51.feature.cs @@ -0,0 +1,425 @@ +// ------------------------------------------------------------------------------ +// +// 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_51ListsFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "51.feature" +#line hidden + + public NIP_51ListsFeature(NIP_51ListsFeature.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-51 Lists", " Tests for NIP-51 Lists implementation", 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 4 + #line hidden +#line 5 + testRunner.Given("a relay at \"wss://localhost:5001\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 6 + testRunner.And("a user Alice", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 7 + testRunner.And("Alice is connected to the relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a public mute list")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve a public mute list")] + public void CreateAndRetrieveAPublicMuteList() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a public mute list", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 9 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + "p", + "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); + table127.AddRow(new string[] { + "p", + "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"}); +#line 10 + testRunner.When("Alice publishes an event with kind 10000 and tags:", ((string)(null)), table127, "When "); +#line hidden +#line 13 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 14 + testRunner.When("Alice subscribes to events with kind 10000", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 15 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 16 + testRunner.And("the event has 2 \"p\" tags", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a private mute list")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve a private mute list")] + public void CreateAndRetrieveAPrivateMuteList() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a private mute list", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 18 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { + "p", + "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); +#line 19 + testRunner.When("Alice publishes an event with kind 10000 and encrypted content and tags:", ((string)(null)), table128, "When "); +#line hidden +#line 21 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 22 + testRunner.When("Alice subscribes to events with kind 10000", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 23 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 24 + testRunner.And("the event has encrypted content", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 25 + testRunner.And("the event has 1 \"p\" tag", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a bookmark set")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve a bookmark set")] + public void CreateAndRetrieveABookmarkSet() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a bookmark set", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 27 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { + "d", + "my-bookmarks"}); + table129.AddRow(new string[] { + "name", + "Programming Resources"}); + table129.AddRow(new string[] { + "about", + "Collection of useful programming articles and tutorials"}); + table129.AddRow(new string[] { + "e", + "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"}); + table129.AddRow(new string[] { + "a", + "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"}); +#line 28 + testRunner.When("Alice publishes an event with kind 30003 and tags:", ((string)(null)), table129, "When "); +#line hidden +#line 34 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 35 + testRunner.When("Alice subscribes to events with kind 30003", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 36 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 37 + testRunner.And("the event has tag \"d\" with value \"my-bookmarks\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 38 + testRunner.And("the event has tag \"name\" with value \"Programming Resources\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve an emoji set")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve an emoji set")] + public void CreateAndRetrieveAnEmojiSet() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve an emoji set", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 40 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { + "d", + "custom-emojis"}); + table130.AddRow(new string[] { + "name", + "My Custom Emojis"}); + table130.AddRow(new string[] { + "emoji", + "happy,https://example.com/happy.png"}); + table130.AddRow(new string[] { + "emoji", + "sad,https://example.com/sad.png"}); +#line 41 + testRunner.When("Alice publishes an event with kind 30030 and tags:", ((string)(null)), table130, "When "); +#line hidden +#line 46 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 47 + testRunner.When("Alice subscribes to events with kind 30030", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 48 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 49 + testRunner.And("the event has 2 \"emoji\" tags", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a relay set")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve a relay set")] + public void CreateAndRetrieveARelaySet() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a relay set", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 51 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { + "d", + "my-relays"}); + table131.AddRow(new string[] { + "name", + "Primary Relays"}); + table131.AddRow(new string[] { + "about", + "My main relay connections"}); + table131.AddRow(new string[] { + "relay", + "wss://relay1.example.com"}); + table131.AddRow(new string[] { + "relay", + "wss://relay2.example.com"}); +#line 52 + testRunner.When("Alice publishes an event with kind 30002 and tags:", ((string)(null)), table131, "When "); +#line hidden +#line 58 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 59 + testRunner.When("Alice subscribes to events with kind 30002", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 60 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 61 + testRunner.And("the event has 2 \"relay\" tags", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a kind mute set")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-51 Lists")] + [Xunit.TraitAttribute("Description", "Create and retrieve a kind mute set")] + public void CreateAndRetrieveAKindMuteSet() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a kind mute set", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 63 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { + "d", + "1"}); + table132.AddRow(new string[] { + "p", + "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); + table132.AddRow(new string[] { + "p", + "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"}); +#line 64 + testRunner.When("Alice publishes an event with kind 30007 and tags:", ((string)(null)), table132, "When "); +#line hidden +#line 68 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 69 + testRunner.When("Alice subscribes to events with kind 30007", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 70 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 71 + testRunner.And("the event has tag \"d\" with value \"1\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 72 + testRunner.And("the event has 2 \"p\" tags", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#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_51ListsFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_51ListsFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/57.feature b/test/Netstr.Tests/NIPs/57.feature index 713d474..7ae9f40 100644 --- a/test/Netstr.Tests/NIPs/57.feature +++ b/test/Netstr.Tests/NIPs/57.feature @@ -8,7 +8,7 @@ Feature: NIP-57 Lightning Zaps Scenario: Create and retrieve a zap request When Alice publishes an event with kind 9734 and tags: - | relays | wss://relay1.example.com | wss://relay2.example.com | + | relays | wss://relay1.example.com,wss://relay2.example.com | | amount | 21000 | | lnurl | lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp | | p | 04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9 | diff --git a/test/Netstr.Tests/NIPs/57.feature.cs b/test/Netstr.Tests/NIPs/57.feature.cs new file mode 100644 index 0000000..f886287 --- /dev/null +++ b/test/Netstr.Tests/NIPs/57.feature.cs @@ -0,0 +1,222 @@ +// ------------------------------------------------------------------------------ +// +// 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_57LightningZapsFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "57.feature" +#line hidden + + public NIP_57LightningZapsFeature(NIP_57LightningZapsFeature.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-57 Lightning Zaps", " Tests for NIP-57 Lightning Zaps implementation", 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 4 + #line hidden +#line 5 + testRunner.Given("a relay at \"wss://localhost:5001\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 6 + testRunner.And("a user Alice", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 7 + testRunner.And("Alice is connected to the relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a zap request")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-57 Lightning Zaps")] + [Xunit.TraitAttribute("Description", "Create and retrieve a zap request")] + public void CreateAndRetrieveAZapRequest() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a zap request", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 9 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { + "relays", + "wss://relay1.example.com,wss://relay2.example.com"}); + table133.AddRow(new string[] { + "amount", + "21000"}); + table133.AddRow(new string[] { + "lnurl", + "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x" + + "9xp"}); + table133.AddRow(new string[] { + "p", + "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"}); +#line 10 + testRunner.When("Alice publishes an event with kind 9734 and tags:", ((string)(null)), table133, "When "); +#line hidden +#line 15 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 16 + testRunner.When("Alice subscribes to events with kind 9734", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 17 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 18 + testRunner.And("the event has tag \"p\" with value \"04c915daefee38317fa734444acee390a8269fe5810b224" + + "1e5e6dd343dfbecc9\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Create and retrieve a zap receipt")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-57 Lightning Zaps")] + [Xunit.TraitAttribute("Description", "Create and retrieve a zap receipt")] + public void CreateAndRetrieveAZapReceipt() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create and retrieve a zap receipt", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 20 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { + "p", + "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"}); + table134.AddRow(new string[] { + "bolt11", + @"lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"}); + table134.AddRow(new string[] { + "description", + @"{""pubkey"":""32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"",""content"":"""",""id"":""d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d"",""created_at"":1674164539,""sig"":""77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d"",""kind"":9734,""tags"":[[""e"",""3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8""],[""p"",""32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245""],[""relays"",""wss://relay.damus.io"",""wss://nostr-relay.wlvs.space"",""wss://nostr.fmt.wiz.biz"",""wss://relay.nostr.bg"",""wss://nostr.oxtr.dev"",""wss://nostr.v0l.io"",""wss://brb.io"",""wss://nostr.bitcoiner.social"",""ws://monad.jb55.com:8080"",""wss://relay.snort.social""]]}"}); + table134.AddRow(new string[] { + "preimage", + "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"}); +#line 21 + testRunner.When("Alice publishes an event with kind 9735 and tags:", ((string)(null)), table134, "When "); +#line hidden +#line 26 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 27 + testRunner.When("Alice subscribes to events with kind 9735", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 28 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 29 + testRunner.And(@"the event has tag ""bolt11"" with value starting with ""lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0""", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#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_57LightningZapsFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_57LightningZapsFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/62.feature.cs b/test/Netstr.Tests/NIPs/62.feature.cs index f9e2923..b128602 100644 --- a/test/Netstr.Tests/NIPs/62.feature.cs +++ b/test/Netstr.Tests/NIPs/62.feature.cs @@ -41,8 +41,8 @@ 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); + "\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); } @@ -84,32 +84,32 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table127.AddRow(new string[] { + table135.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table127, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table135, "And "); #line hidden - TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table128.AddRow(new string[] { + table136.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 10 - testRunner.And("Bob is connected to relay", ((string)(null)), table128, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table136, "And "); #line hidden - TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table129.AddRow(new string[] { + table137.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 13 - testRunner.And("Charlie is connected to relay", ((string)(null)), table129, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table137, "And "); #line hidden } @@ -125,9 +125,9 @@ 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); + 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\n\tOnl" + + "y events from before the request\'s createdAt timestamp is deleted\n\tNo-one else\'s" + + " events are deleted", tagsOfScenario, argumentsOfScenario, featureTags); #line 17 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -141,104 +141,104 @@ public void RequestToVanishDeletesUsersData() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table130.AddRow(new string[] { + table138.AddRow(new string[] { "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643", "Hello", "1", "", "1728905459"}); - table130.AddRow(new string[] { + table138.AddRow(new string[] { "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957", "Hello Later", "1", "", "1728905481"}); - table130.AddRow(new string[] { + table138.AddRow(new string[] { "5c19b5808ee4ad3d31e4129cc112679147e28f3d88e24683a3afa327ba0a2ee8", "DM", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1728905459"}); - table130.AddRow(new string[] { + table138.AddRow(new string[] { "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f", "DM Late", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1728905480"}); #line 21 - testRunner.When("Bob publishes events", ((string)(null)), table130, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table138, "When "); #line hidden - TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table131.AddRow(new string[] { + table139.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table131.AddRow(new string[] { + table139.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table131.AddRow(new string[] { + table139.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); #line 27 - testRunner.When("Alice publishes events", ((string)(null)), table131, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table139, "When "); #line hidden - TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table132.AddRow(new string[] { + table140.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 32 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table132, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table140, "And "); #line hidden - TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EVENT", "abcd", "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EVENT", "abcd", "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EVENT", "abcd", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EVENT", "abcd", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EVENT", "abcd", "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643"}); - table133.AddRow(new string[] { + table141.AddRow(new string[] { "EOSE", "abcd", ""}); #line 35 - testRunner.Then("Charlie receives messages", ((string)(null)), table133, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table141, "Then "); #line hidden } this.ScenarioCleanup(); @@ -266,61 +266,61 @@ public void OldEventsPublishedAfterRequestToVanishAreRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table134.AddRow(new string[] { + table142.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table134.AddRow(new string[] { + table142.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table134.AddRow(new string[] { + table142.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table134.AddRow(new string[] { + table142.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); #line 46 - testRunner.When("Alice publishes events", ((string)(null)), table134, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table142, "When "); #line hidden - TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table143 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table135.AddRow(new string[] { + table143.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "true"}); - table135.AddRow(new string[] { + table143.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table135.AddRow(new string[] { + table143.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "false"}); - table135.AddRow(new string[] { + table143.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "true"}); #line 52 - testRunner.Then("Alice receives messages", ((string)(null)), table135, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table143, "Then "); #line hidden } this.ScenarioCleanup(); @@ -334,8 +334,8 @@ 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); + "effect. \n\tClients and relays are not obliged to support \"unrequest vanish\" funct" + + "ionality.", tagsOfScenario, argumentsOfScenario, featureTags); #line 59 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -349,41 +349,41 @@ public void DeletingRequestToVanishIsRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table144 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table136.AddRow(new string[] { + table144.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table136.AddRow(new string[] { + table144.AddRow(new string[] { "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", "", "5", "[[\"e\", \"9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e\"]]", "1728905471"}); #line 62 - testRunner.When("Alice publishes events", ((string)(null)), table136, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table144, "When "); #line hidden - TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table145 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table137.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table137.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", "false"}); #line 66 - testRunner.Then("Alice receives messages", ((string)(null)), table137, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table145, "Then "); #line hidden } this.ScenarioCleanup(); @@ -396,9 +396,9 @@ 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); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Older Request to Vanish does nothing, newer deletes newer events", "\tFirst vanish request works as expected. \n\tSecond (older) one should be ignored a" + + "nd old events should still be rejetected.\n\tThird (newer) is accepted and its Cre" + + "atedAt is used to reject old events.\n\tNewer events are still accepted.", tagsOfScenario, argumentsOfScenario, featureTags); #line 71 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -412,101 +412,101 @@ public void OlderRequestToVanishDoesNothingNewerDeletesNewerEvents() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table146 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", "I\'m outta here sooner", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905460"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", "Hello", "1", "", "1728905465"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", "I\'m outta here later", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905490"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table138.AddRow(new string[] { + table146.AddRow(new string[] { "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", "Hello", "1", "", "1728905495"}); #line 76 - testRunner.When("Alice publishes events", ((string)(null)), table138, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table146, "When "); #line hidden - TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table147 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "true"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "true"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", "false"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", "false"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", "true"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "false"}); - table139.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", "true"}); #line 86 - testRunner.Then("Alice receives messages", ((string)(null)), table139, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table147, "Then "); #line hidden } this.ScenarioCleanup(); @@ -519,9 +519,9 @@ 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); + 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.\n\tCorrect one assumes the c" + + "onnection is on ws://localhost/. Relay should be able to normalize its own URL a" + + "nd the one in tag (e.g. trim ws:// or wss://, trailing / etc)", tagsOfScenario, argumentsOfScenario, featureTags); #line 97 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -535,51 +535,51 @@ public void RequestToVanishIsIgnoredWhenRelayTagDoesntMatchCurrentRelay() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table148 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table140.AddRow(new string[] { + table148.AddRow(new string[] { "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", "I\'m outta here", "62", "", "1728905470"}); - table140.AddRow(new string[] { + table148.AddRow(new string[] { "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", "I\'m outta here", "62", "[[\"relay\",\"blabla\"]]", "1728905470"}); - table140.AddRow(new string[] { + table148.AddRow(new string[] { "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", "I\'m outta here", "62", "[[\"relay\",\"ws://localhost/\"]]", "1728905470"}); #line 100 - testRunner.When("Alice publishes events", ((string)(null)), table140, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table148, "When "); #line hidden - TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table149 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table141.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", "false"}); - table141.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", "false"}); - table141.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", "true"}); #line 105 - testRunner.Then("Alice receives messages", ((string)(null)), table141, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table149, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/65.feature.cs b/test/Netstr.Tests/NIPs/65.feature.cs index b9f60cd..950ae94 100644 --- a/test/Netstr.Tests/NIPs/65.feature.cs +++ b/test/Netstr.Tests/NIPs/65.feature.cs @@ -40,8 +40,8 @@ public NIP_65RelayListMetadataFeature(NIP_65RelayListMetadataFeature.FixtureData 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-65 Relay List Metadata", " As a NOSTR client\r\n I want to publish and retrieve my relay preferences\r\n " + - " So that other clients know which relays I use", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-65 Relay List Metadata", " As a NOSTR client\n I want to publish and retrieve my relay preferences\n " + + " So that other clients know which relays I use", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -114,23 +114,23 @@ public void PublishingValidRelayList() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read", "write"}); - table142.AddRow(new string[] { + table150.AddRow(new string[] { "r", "wss://relay2.com", "read", ""}); - table142.AddRow(new string[] { + table150.AddRow(new string[] { "r", "wss://relay3.com", "write", ""}); #line 11 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table142, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table150, "When "); #line hidden #line 15 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -166,16 +166,16 @@ public void UpdatingExistingRelayList() #line 19 testRunner.Given("I have published relay configurations", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table143 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read"}); - table143.AddRow(new string[] { + table151.AddRow(new string[] { "r", "wss://relay4.com", "write"}); #line 20 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table143, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table151, "When "); #line hidden #line 23 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -243,13 +243,13 @@ public void PublishingInvalidRelayURL() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table144 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { "r", "invalid-url", "read", "write"}); #line 32 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table144, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table152, "When "); #line hidden #line 34 testRunner.Then("I should receive an error message containing \"Invalid relay URL format\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -279,12 +279,12 @@ public void PublishingInvalidPermissionMarker() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table145 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "invalid"}); #line 37 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table145, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table153, "When "); #line hidden #line 39 testRunner.Then("I should receive an error message containing \"Invalid relay permission marker\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); diff --git a/test/Netstr.Tests/NIPs/70.feature.cs b/test/Netstr.Tests/NIPs/70.feature.cs index e6d13c8..3b15845 100644 --- a/test/Netstr.Tests/NIPs/70.feature.cs +++ b/test/Netstr.Tests/NIPs/70.feature.cs @@ -40,8 +40,8 @@ public NIP_70Feature(NIP_70Feature.FixtureData fixtureData, Netstr_Tests_XUnitAs 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-70", "\tWhen the \"-\" tag is present, that means the event is \"protected\".\r\n\tA protected " + - "event is an event that can only be published to relays by its author.", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-70", "\tWhen the \"-\" tag is present, that means the event is \"protected\".\n\tA protected e" + + "vent is an event that can only be published to relays by its author.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -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 table146 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table154 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table146.AddRow(new string[] { + table154.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table146, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table154, "And "); #line hidden } @@ -120,35 +120,35 @@ public void NotAuthenticatedClientTriesToPublishProtectedEvent() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table147 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table155 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table147.AddRow(new string[] { + table155.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 13 - testRunner.When("Alice publishes an event", ((string)(null)), table147, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table155, "When "); #line hidden - TechTalk.SpecFlow.Table table148 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table148.AddRow(new string[] { + table156.AddRow(new string[] { "AUTH", "*", ""}); - table148.AddRow(new string[] { + table156.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "false"}); #line 16 - testRunner.Then("Alice receives messages", ((string)(null)), table148, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table156, "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 table149 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table157 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table149.AddRow(new string[] { + table157.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 24 - testRunner.When("Alice publishes an event", ((string)(null)), table149, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table157, "When "); #line hidden - TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table158 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table150.AddRow(new string[] { + table158.AddRow(new string[] { "AUTH", "*", ""}); - table150.AddRow(new string[] { + table158.AddRow(new string[] { "OK", "*", "true"}); - table150.AddRow(new string[] { + table158.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "true"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table150, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table158, "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 table151 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table159 = new TechTalk.SpecFlow.Table(new string[] { "Id", "PublicKey", "Content", "Kind", "Tags", "CreatedAt"}); - table151.AddRow(new string[] { + table159.AddRow(new string[] { "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "Protected", @@ -255,26 +255,26 @@ public void AuthenticatedClientTriesToPublishSomeoneElsesProtectedEvent() "[[ \"-\" ]]", "1722337837"}); #line 36 - testRunner.When("Alice publishes an event", ((string)(null)), table151, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table159, "When "); #line hidden - TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table160 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table152.AddRow(new string[] { + table160.AddRow(new string[] { "AUTH", "*", ""}); - table152.AddRow(new string[] { + table160.AddRow(new string[] { "OK", "*", "true"}); - table152.AddRow(new string[] { + table160.AddRow(new string[] { "OK", "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "false"}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table152, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table160, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/Steps/05.cs b/test/Netstr.Tests/NIPs/Steps/05.cs new file mode 100644 index 0000000..8eefe39 --- /dev/null +++ b/test/Netstr.Tests/NIPs/Steps/05.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using Netstr.Messaging.Models; +using StackExchange.Redis; +using TechTalk.SpecFlow; + +namespace Netstr.Tests.NIPs.Steps +{ + public partial class Steps + { + + [When(@"(.*) publishes a metadata event with NIP-05 identifier ""(.*)""")] + public async Task WhenUserPublishesMetadataEventWithNip05Identifier(string user, string nip05Identifier) + { + var metadata = new UserMetadata + { + Name = user, + Nip05 = nip05Identifier, + About = "Test user with NIP-05 identifier" + }; + + var content = JsonSerializer.Serialize(metadata); + await WhenUserPublishesEvent(user, "0", content, Array.Empty()); + } + + [When(@"(.*) publishes a metadata event without NIP-05 identifier")] + public async Task WhenUserPublishesMetadataEventWithoutNip05Identifier(string user) + { + var metadata = new UserMetadata + { + Name = user, + About = "Test user without NIP-05 identifier" + }; + + var content = JsonSerializer.Serialize(metadata); + await WhenUserPublishesEvent(user, "0", content, Array.Empty()); + } + + [When(@"(.*) publishes a metadata event with empty NIP-05 identifier")] + public async Task WhenUserPublishesMetadataEventWithEmptyNip05Identifier(string user) + { + var metadata = new UserMetadata + { + Name = user, + Nip05 = "", + About = "Test user with empty NIP-05 identifier" + }; + + var content = JsonSerializer.Serialize(metadata); + await WhenUserPublishesEvent(user, "0", content, Array.Empty()); + } + + private async Task WhenUserPublishesEvent(string user, string kind, string content, string[][] tags) + { + var c = this.scenarioContext.Get()[user]; + + var e = new Event + { + Id = "", + Signature = "", + Content = content, + CreatedAt = DateTimeOffset.UtcNow, + PublicKey = c.Keys.PublicKey, + Tags = tags, + Kind = long.Parse(kind) + }; + + e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); + + await c.WebSocket.SendEventAsync(e); + + var start = DateTimeOffset.UtcNow; + await c.WaitForMessageAsync(start, ["OK", e.Id]); + } + } +} \ No newline at end of file diff --git a/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs b/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs index f9fcfec..0226333 100644 --- a/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs +++ b/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs @@ -11,32 +11,33 @@ namespace Netstr.Tests.NIPs.Steps { - [Binding] - public class RelayListSteps : StepsBase + public partial class Steps { - private readonly ScenarioContext context; - - public RelayListSteps(ScenarioContext context, TestContext testContext) - : base(testContext) - { - this.context = context; - } [Given("I have published relay configurations")] public async Task GivenIHavePublishedRelayConfigurations() { - var tags = new[] + var c = this.scenarioContext.Get()["Alice"]; + + var tags = new string[][] { new[] { "r", "wss://relay1.com", "read", "write" }, new[] { "r", "wss://relay2.com", "read" } }; - await this.PublishEvent(new Event + var e = new Event { - Kind = (int)EventKind.RelayList, - Tags = tags.ToList(), - Content = string.Empty - }); + Id = "", + Signature = "", + Content = "", + CreatedAt = DateTimeOffset.UtcNow, + PublicKey = c.Keys.PublicKey, + Tags = tags, + Kind = (long)EventKind.RelayList + }; + + e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); + await c.WebSocket.SendEventAsync(e); await Task.Delay(100); // Allow time for processing } @@ -44,42 +45,61 @@ await this.PublishEvent(new Event [When(@"I publish an event with kind 10002 and tags:")] public async Task WhenIPublishAnEventWithKindAndTags(Table table) { - var tags = table.Rows.Select(row => row.Values.ToArray()).ToList(); + var c = this.scenarioContext.Get()["Alice"]; + var tags = table.Rows.Select(row => row.Values.ToArray()).ToArray(); - await this.PublishEvent(new Event + var e = new Event { - Kind = (int)EventKind.RelayList, + Id = "", + Signature = "", + Content = "", + CreatedAt = DateTimeOffset.UtcNow, + PublicKey = c.Keys.PublicKey, Tags = tags, - Content = string.Empty - }); + Kind = (long)EventKind.RelayList + }; + + e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); + await c.WebSocket.SendEventAsync(e); } [When(@"I publish an event with kind 10002 and no tags")] public async Task WhenIPublishAnEventWithKindAndNoTags() { - await this.PublishEvent(new Event + var c = this.scenarioContext.Get()["Alice"]; + + var e = new Event { - Kind = (int)EventKind.RelayList, - Tags = new List(), - Content = string.Empty - }); + Id = "", + Signature = "", + Content = "", + CreatedAt = DateTimeOffset.UtcNow, + PublicKey = c.Keys.PublicKey, + Tags = Array.Empty(), + Kind = (long)EventKind.RelayList + }; + + e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); + await c.WebSocket.SendEventAsync(e); } [When(@"I request relay configurations for my public key")] public async Task WhenIRequestRelayConfigurationsForMyPublicKey() { - var response = await this.Client.GetAsync($"/api/relay/{this.Alice.PublicKey}"); - context.Set(response); + var c = this.scenarioContext.Get()["Alice"]; + var response = await c.HttpClient.GetAsync($"/api/relay/{c.Keys.PublicKey}"); + this.scenarioContext.Set(response); } [Then(@"the relay configurations should be stored for my public key")] public async Task ThenTheRelayConfigurationsShouldBeStoredForMyPublicKey() { - using var scope = this.Factory.Services.CreateScope(); + var c = this.scenarioContext.Get()["Alice"]; + using var scope = this.factory.Services.CreateScope(); using var db = scope.ServiceProvider.GetRequiredService(); var configs = await db.RelayConfigs - .Where(r => r.PubKey == this.Alice.PublicKey) + .Where(r => r.PubKey == c.Keys.PublicKey) .ToListAsync(); configs.Should().NotBeEmpty(); @@ -91,11 +111,12 @@ public async Task ThenTheRelayConfigurationsShouldBeStoredForMyPublicKey() [Then(@"my old relay configurations should be replaced")] public async Task ThenMyOldRelayConfigurationsShouldBeReplaced() { - using var scope = this.Factory.Services.CreateScope(); + var c = this.scenarioContext.Get()["Alice"]; + using var scope = this.factory.Services.CreateScope(); using var db = scope.ServiceProvider.GetRequiredService(); var configs = await db.RelayConfigs - .Where(r => r.PubKey == this.Alice.PublicKey) + .Where(r => r.PubKey == c.Keys.PublicKey) .ToListAsync(); configs.Should().NotContain(c => c.RelayUrl == "wss://relay2.com"); @@ -104,11 +125,12 @@ public async Task ThenMyOldRelayConfigurationsShouldBeReplaced() [Then(@"the new relay configurations should be stored")] public async Task ThenTheNewRelayConfigurationsShouldBeStored() { - using var scope = this.Factory.Services.CreateScope(); + var c = this.scenarioContext.Get()["Alice"]; + using var scope = this.factory.Services.CreateScope(); using var db = scope.ServiceProvider.GetRequiredService(); var configs = await db.RelayConfigs - .Where(r => r.PubKey == this.Alice.PublicKey) + .Where(r => r.PubKey == c.Keys.PublicKey) .ToListAsync(); configs.Should().NotBeEmpty(); @@ -119,7 +141,7 @@ public async Task ThenTheNewRelayConfigurationsShouldBeStored() [Then(@"I should receive my relay configurations")] public async Task ThenIShouldReceiveMyRelayConfigurations() { - var response = context.Get(); + var response = this.scenarioContext.Get(); response.StatusCode.Should().Be(HttpStatusCode.OK); var content = await response.Content.ReadAsStringAsync(); From be6ae325620fb375266d4e8240c61cacc94174b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:21:16 -0400 Subject: [PATCH 13/49] test: align generated NIP specs and relay list test suite - refresh generated feature snapshots and test project references - update relay-list and whitelist validator tests for recent behavior changes - adjust test host wiring for current auth and event handler composition --- .claude/settings.local.json | 5 +- .../Handlers/TestRelayListEventHandler.cs | 12 ++-- test/Netstr.Tests/AuthTests.cs | 4 +- .../Netstr.Tests/Events/EventHandlersTests.cs | 3 +- .../Events/FilterEventMatchingTests.cs | 17 ++--- .../Events/ListEventValidatorTests.cs | 6 +- .../Events/WhitelistValidatorTests.cs | 14 ++-- test/Netstr.Tests/NIPs/01.feature.cs | 8 +-- test/Netstr.Tests/NIPs/04.feature.cs | 4 +- test/Netstr.Tests/NIPs/09.feature.cs | 4 +- test/Netstr.Tests/NIPs/11.feature.cs | 6 +- test/Netstr.Tests/NIPs/13.feature.cs | 8 +-- test/Netstr.Tests/NIPs/40.feature.cs | 4 +- test/Netstr.Tests/NIPs/45.feature.cs | 6 +- test/Netstr.Tests/NIPs/62.feature.cs | 26 +++---- test/Netstr.Tests/NIPs/65.feature.cs | 4 +- test/Netstr.Tests/NIPs/70.feature.cs | 4 +- test/Netstr.Tests/NIPs/Steps/42.cs | 4 +- .../Netstr.Tests/NIPs/Steps/RelayListSteps.cs | 2 +- test/Netstr.Tests/Netstr.Tests.csproj | 1 + .../WhitelistSubscriptionValidatorTests.cs | 14 ++-- test/Netstr.Tests/WebApplicationFactory.cs | 2 + test/Netstr.Tests/WhitelistTests.cs | 72 +++++++------------ 23 files changed, 112 insertions(+), 118 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c25d101..fea4326 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,10 @@ "Bash(dotnet test:*)", "Bash(export PATH=\"$PATH:$HOME/.dotnet\")", "Bash(dotnet --version)", - "Bash(dotnet build:*)" + "Bash(dotnet build:*)", + "Bash(grep:*)", + "Bash(rg:*)", + "Bash(find:*)" ], "deny": [] } diff --git a/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs index 440a90b..667a89e 100644 --- a/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs @@ -16,15 +16,15 @@ public TestRelayListEventHandler( ILogger logger, IDbContextFactory dbFactory) { - _logger = logger; - _dbFactory = dbFactory; + this._logger = logger; + this._dbFactory = dbFactory; } public bool CanHandleEvent(Event e) => e.Kind == (long)EventKind.RelayList; public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) { - _logger.LogInformation( + this._logger.LogInformation( "Test Relay List Event Received:\nFull Event:\n{@Event}\nTags:\n{@Tags}\nContent:\n{Content}", e, e.Tags, @@ -33,18 +33,18 @@ public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) try { - using var context = _dbFactory.CreateDbContext(); + using var context = this._dbFactory.CreateDbContext(); // Store the event directly in the Events table // The event and its tags will be automatically saved through the normal event processing pipeline // No need to update RelayConfigs table as we're using events as source of truth - _logger.LogInformation("Successfully processed relay list event {EventId} for user {PubKey}", e.Id, e.PublicKey); + this._logger.LogInformation("Successfully processed relay list event {EventId} for user {PubKey}", e.Id, e.PublicKey); sender.SendOk(e.Id); } catch (Exception error) { - _logger.LogError(error, "Failed to process relay list event {EventId} for user {PubKey}", e.Id, e.PublicKey); + this._logger.LogError(error, "Failed to process relay list event {EventId} for user {PubKey}", e.Id, e.PublicKey); sender.SendNotOk(e.Id, "Failed to process relay list event"); } } diff --git a/test/Netstr.Tests/AuthTests.cs b/test/Netstr.Tests/AuthTests.cs index 3cf1eb6..73b7bdc 100644 --- a/test/Netstr.Tests/AuthTests.cs +++ b/test/Netstr.Tests/AuthTests.cs @@ -56,7 +56,7 @@ public async Task PublishAuthModeTest() ["relay", "ws://localhost"], ["challenge", auth[1].ToString()] ], - Kind = EventKind.Auth + Kind = (long)EventKind.Auth }; e = Helpers.FinalizeEvent(e, Alice.PrivateKey); @@ -97,7 +97,7 @@ public async Task WrongAuthEventKindTest() ["relay", "ws://localhost"], ["challenge", auth[1].ToString()] ], - Kind = EventKind.Auth + 1 + Kind = (long)EventKind.Auth + 1 }; e = Helpers.FinalizeEvent(e, Alice.PrivateKey); diff --git a/test/Netstr.Tests/Events/EventHandlersTests.cs b/test/Netstr.Tests/Events/EventHandlersTests.cs index 20c03c7..f5e5cfb 100644 --- a/test/Netstr.Tests/Events/EventHandlersTests.cs +++ b/test/Netstr.Tests/Events/EventHandlersTests.cs @@ -52,7 +52,8 @@ public EventHandlersTests() MaxPendingEvents = 10 }, Subscriptions = new Options.Limits.SubscriptionLimits(), - Negentropy = new Options.Limits.NegentropyLimits() + Negentropy = new Options.Limits.NegentropyLimits(), + Search = new Options.Limits.SearchLimits() }); // receiver is a client with 2 registered subscriptions diff --git a/test/Netstr.Tests/Events/FilterEventMatchingTests.cs b/test/Netstr.Tests/Events/FilterEventMatchingTests.cs index aa5d027..b7b4ce0 100644 --- a/test/Netstr.Tests/Events/FilterEventMatchingTests.cs +++ b/test/Netstr.Tests/Events/FilterEventMatchingTests.cs @@ -44,7 +44,7 @@ public void TrueForEmptyFilter() [InlineData("6b3cdd0302ded8068a", false)] public void IdsFilterTests(string id, bool expectation) { - var filter = new SubscriptionFilter([id], [], [], null, null, 0, [], []); + var filter = new SubscriptionFilter([id], [], [], null, null, 0, null, [], []); var result = SubscriptionFilterMatcher.IsMatch(filter, this.e); Assert.Equal(expectation, result); @@ -56,7 +56,7 @@ public void IdsFilterTests(string id, bool expectation) [InlineData("22e804d26ed16b68db52", false)] public void AuthorsFilterTests(string author, bool expectation) { - var filter = new SubscriptionFilter([], [author], [], null, null, 0, [], []); + var filter = new SubscriptionFilter([], [author], [], null, null, 0, null, [], []); var result = SubscriptionFilterMatcher.IsMatch(filter, this.e); Assert.Equal(expectation, result); @@ -67,7 +67,7 @@ public void AuthorsFilterTests(string author, bool expectation) [InlineData(1, true)] public void KindsFilterTests(int kind, bool expecation) { - var filter = new SubscriptionFilter([], [], [kind], null, null, 0, [], []); + var filter = new SubscriptionFilter([], [], [kind], null, null, 0, null, [], []); var result = SubscriptionFilterMatcher.IsMatch(filter, this.e); Assert.Equal(expecation, result); @@ -79,7 +79,7 @@ public void KindsFilterTests(int kind, bool expecation) [InlineData(1648351381, false)] public void SinceFilterTests(int since, bool expecation) { - var filter = new SubscriptionFilter([], [], [], DateTimeOffset.FromUnixTimeSeconds(since), null, 0, [], []); + var filter = new SubscriptionFilter([], [], [], DateTimeOffset.FromUnixTimeSeconds(since), null, 0, null, [], []); var result = SubscriptionFilterMatcher.IsMatch(filter, this.e); Assert.Equal(expecation, result); @@ -91,7 +91,7 @@ public void SinceFilterTests(int since, bool expecation) [InlineData(1648351381, true)] public void UntilFilterTests(int until, bool expecation) { - var filter = new SubscriptionFilter([], [], [], null, DateTimeOffset.FromUnixTimeSeconds(until), 0, [], []); + var filter = new SubscriptionFilter([], [], [], null, DateTimeOffset.FromUnixTimeSeconds(until), 0, null, [], []); var result = SubscriptionFilterMatcher.IsMatch(filter, this.e); Assert.Equal(expecation, result); @@ -112,6 +112,7 @@ public void MultipleFiltersTest(string ids, string authors, int kind, int since, DateTimeOffset.FromUnixTimeSeconds(since), DateTimeOffset.FromUnixTimeSeconds(until), 0, + null, [], []); @@ -127,7 +128,7 @@ public void SingleTagsMatchTest() [], [], [], - null, null, 0, + null, null, 0, null, new() { ["e"] = ["7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96"] @@ -145,7 +146,7 @@ public void MultipleTagsMatchTest() [], [], [], - null, null, 0, + null, null, 0, null, new() { ["e"] = ["7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96"], @@ -164,7 +165,7 @@ public void SomeTagsDoNotMatchTest() [], [], [], - null, null, 0, + null, null, 0, null, new() { ["e"] = ["abcd"], diff --git a/test/Netstr.Tests/Events/ListEventValidatorTests.cs b/test/Netstr.Tests/Events/ListEventValidatorTests.cs index 5a8b918..54d77e9 100644 --- a/test/Netstr.Tests/Events/ListEventValidatorTests.cs +++ b/test/Netstr.Tests/Events/ListEventValidatorTests.cs @@ -11,7 +11,7 @@ public void ValidateListType_ShouldReturnNull_ForUnknownEventKind() { // Arrange var validator = new ListEventValidator(); - var unknownEvent = new Event { Kind = 99999 }; // Unknown kind + var unknownEvent = new Event { Kind = 99999, Content = string.Empty, CreatedAt = DateTimeOffset.UtcNow, Id = "test", PublicKey = "test", Signature = "test", Tags = [] }; // Unknown kind // Act var result = validator.Validate(unknownEvent, null); @@ -25,7 +25,7 @@ public void ValidateListType_ShouldValidateMuteList() { // Arrange var validator = new ListEventValidator(); - var muteListEvent = new Event { Kind = (int)EventKind.MuteList, Tags = new[] { new[] { "p" } } }; + var muteListEvent = new Event { Kind = (int)EventKind.MuteList, Tags = new[] { new[] { "p" } }, Content = string.Empty, CreatedAt = DateTimeOffset.UtcNow, Id = "test", PublicKey = "test", Signature = "test" }; // Act var result = validator.Validate(muteListEvent, null); @@ -39,7 +39,7 @@ public void ValidateListType_ShouldReturnInvalidListTags_ForInvalidMuteList() { // Arrange var validator = new ListEventValidator(); - var invalidMuteListEvent = new Event { Kind = (int)EventKind.MuteList, Tags = new[] { new[] { "invalid" } } }; + var invalidMuteListEvent = new Event { Kind = (int)EventKind.MuteList, Tags = new[] { new[] { "invalid" } }, Content = string.Empty, CreatedAt = DateTimeOffset.UtcNow, Id = "test", PublicKey = "test", Signature = "test" }; // Act var result = validator.Validate(invalidMuteListEvent, null); diff --git a/test/Netstr.Tests/Events/WhitelistValidatorTests.cs b/test/Netstr.Tests/Events/WhitelistValidatorTests.cs index b22f269..8777973 100644 --- a/test/Netstr.Tests/Events/WhitelistValidatorTests.cs +++ b/test/Netstr.Tests/Events/WhitelistValidatorTests.cs @@ -17,14 +17,14 @@ namespace Netstr.Tests.Events public class WhitelistValidatorTests { private readonly Mock> loggerMock; - private readonly Mock> optionsMock; - private readonly WhitelistOptions options; + private readonly Mock> optionsMock; + private WhitelistOptions options; private readonly WhitelistValidator validator; public WhitelistValidatorTests() { loggerMock = new Mock>(); - optionsMock = new Mock>(); + optionsMock = new Mock>(); options = new WhitelistOptions { Enabled = true, @@ -32,7 +32,7 @@ public WhitelistValidatorTests() RestrictPublishing = true, RestrictSubscribing = true }; - optionsMock.Setup(x => x.Value).Returns(options); + optionsMock.Setup(x => x.CurrentValue).Returns(options); validator = new WhitelistValidator(loggerMock.Object, optionsMock.Object); } @@ -40,7 +40,8 @@ public WhitelistValidatorTests() public void Validate_WhitelistDisabled_ReturnsNull() { // Arrange - options.Enabled = false; + options = new WhitelistOptions { Enabled = false }; + optionsMock.Setup(x => x.CurrentValue).Returns(options); var e = CreateEvent("not_allowed_pubkey"); var context = new ClientContext("client1", "127.0.0.1"); @@ -55,7 +56,8 @@ public void Validate_WhitelistDisabled_ReturnsNull() public void Validate_RestrictPublishingDisabled_ReturnsNull() { // Arrange - options.RestrictPublishing = false; + options = new WhitelistOptions { RestrictPublishing = false }; + optionsMock.Setup(x => x.CurrentValue).Returns(options); var e = CreateEvent("not_allowed_pubkey"); var context = new ClientContext("client1", "127.0.0.1"); diff --git a/test/Netstr.Tests/NIPs/01.feature.cs b/test/Netstr.Tests/NIPs/01.feature.cs index 320e079..9a840ba 100644 --- a/test/Netstr.Tests/NIPs/01.feature.cs +++ b/test/Netstr.Tests/NIPs/01.feature.cs @@ -124,8 +124,8 @@ public void InvalidMessagesAreDiscardedValidOnesAccepted() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Invalid messages are discarded, valid ones accepted", "\tRelay shouldn\'t broadcast messages with invalid Id or Signnature. It should also" + - " reply with OK false.\n\tThis also covers correct validation of events with specia" + - "l characters", tagsOfScenario, argumentsOfScenario, featureTags); + " reply with OK false.\r\n\tThis also covers correct validation of events with speci" + + "al characters", tagsOfScenario, argumentsOfScenario, featureTags); #line 16 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -338,8 +338,8 @@ public void ClosedSubscriptionsShouldNoLongerReceiveEvents() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Closed subscriptions should no longer receive events", "\tAfter a subscription is closed the relay should no longer forward events for tha" + - "t subscription\n\tHowever it should still forward them for other existing subscrip" + - "tions", tagsOfScenario, argumentsOfScenario, featureTags); + "t subscription\r\n\tHowever it should still forward them for other existing subscri" + + "ptions", tagsOfScenario, argumentsOfScenario, featureTags); #line 65 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/04.feature.cs b/test/Netstr.Tests/NIPs/04.feature.cs index 778a5a5..4a0a92f 100644 --- a/test/Netstr.Tests/NIPs/04.feature.cs +++ b/test/Netstr.Tests/NIPs/04.feature.cs @@ -114,8 +114,8 @@ public void NotAuthenticatedClientTriesToFetchKind4Events() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Not authenticated client tries to fetch kind 4 events", "\tAlice can\'t fetch kind 4 events when she isn\'t authenticated\n\tThis should be tru" + - "e even when multiple filters are used", tagsOfScenario, argumentsOfScenario, featureTags); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Not authenticated client tries to fetch kind 4 events", "\tAlice can\'t fetch kind 4 events when she isn\'t authenticated\r\n\tThis should be tr" + + "ue even when multiple filters are used", tagsOfScenario, argumentsOfScenario, featureTags); #line 13 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/09.feature.cs b/test/Netstr.Tests/NIPs/09.feature.cs index bf498ee..4f59b54 100644 --- a/test/Netstr.Tests/NIPs/09.feature.cs +++ b/test/Netstr.Tests/NIPs/09.feature.cs @@ -41,8 +41,8 @@ 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-09", "\tA special event with kind 5, meaning \"deletion\" is defined as having a list of o" + - "ne or more e or a tags, \n\teach referencing an event the author is requesting to " + - "be deleted.", ProgrammingLanguage.CSharp, featureTags); + "ne or more e or a tags, \r\n\teach referencing an event the author is requesting to" + + " be deleted.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } diff --git a/test/Netstr.Tests/NIPs/11.feature.cs b/test/Netstr.Tests/NIPs/11.feature.cs index 72794e4..0694a28 100644 --- a/test/Netstr.Tests/NIPs/11.feature.cs +++ b/test/Netstr.Tests/NIPs/11.feature.cs @@ -41,8 +41,8 @@ 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-11", "\tRelays may provide server metadata to clients to inform them of capabilities, ad" + - "ministrative contacts, and various server attributes.\n\tThis is made available as" + - " a JSON document over HTTP, on the same URI as the relay\'s websocket.", ProgrammingLanguage.CSharp, featureTags); + "ministrative contacts, and various server attributes.\r\n\tThis is made available a" + + "s a JSON document over HTTP, on the same URI as the relay\'s websocket.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -108,7 +108,7 @@ public void RelaySendsAnInformationDocument() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Relay sends an information document", "\tGET HTTP request to the websockets endpoint with a application/nostr+json Accept" + - " header should\n\tproduce a json Relay Information Document", tagsOfScenario, argumentsOfScenario, featureTags); + " header should\r\n\tproduce a json Relay Information Document", tagsOfScenario, argumentsOfScenario, featureTags); #line 11 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/13.feature.cs b/test/Netstr.Tests/NIPs/13.feature.cs index 26ee4b5..0043d3c 100644 --- a/test/Netstr.Tests/NIPs/13.feature.cs +++ b/test/Netstr.Tests/NIPs/13.feature.cs @@ -40,8 +40,8 @@ public NIP_13Feature(NIP_13Feature.FixtureData fixtureData, Netstr_Tests_XUnitAs 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-13", "\t Proof of Work (PoW) is a way to add a proof of computational work to a note.\n\t " + - "This proof can be used as a means of spam deterrence.", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-13", "\t Proof of Work (PoW) is a way to add a proof of computational work to a note.\r\n\t" + + " This proof can be used as a means of spam deterrence.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -115,8 +115,8 @@ public void MessagesWithLowDifficultyAndThoseOffTargetAreRejectedThoseWithHighAn string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Messages with low difficulty and those off target are rejected, those with high a" + - "nd on target difficulty accepted", "\t1) Low diff\n\t2) High diff but doesn\'t match target\n\t3) High diff\n\t4) High diff m" + - "atching target", tagsOfScenario, argumentsOfScenario, featureTags); + "nd on target difficulty accepted", "\t1) Low diff\r\n\t2) High diff but doesn\'t match target\r\n\t3) High diff\r\n\t4) High dif" + + "f matching target", tagsOfScenario, argumentsOfScenario, featureTags); #line 13 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/40.feature.cs b/test/Netstr.Tests/NIPs/40.feature.cs index 5238c92..155056e 100644 --- a/test/Netstr.Tests/NIPs/40.feature.cs +++ b/test/Netstr.Tests/NIPs/40.feature.cs @@ -221,8 +221,8 @@ public void ExpiredEventAlreadySavedInARelayIsOmittedFromSubResponse() string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Expired event already saved in a relay is omitted from sub response", "\tWe need to save an already expired event in the relay, that would be hard using " + - "the publishing step (relay would reject it)\n\tSo just introduce a new step for th" + - "is NIP which bypasses publishing and inserts directly into DB", tagsOfScenario, argumentsOfScenario, featureTags); + "the publishing step (relay would reject it)\r\n\tSo just introduce a new step for t" + + "his NIP which bypasses publishing and inserts directly into DB", tagsOfScenario, argumentsOfScenario, featureTags); #line 31 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/45.feature.cs b/test/Netstr.Tests/NIPs/45.feature.cs index 8ecc18c..eb4a0d2 100644 --- a/test/Netstr.Tests/NIPs/45.feature.cs +++ b/test/Netstr.Tests/NIPs/45.feature.cs @@ -250,9 +250,9 @@ public void CountingSomeoneElsesDMsReturnsOnlyThoseFromMe() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Counting someone elses DMs returns only those from me", "\tBob sends a DM to Charlie\n\tAlice sends a DM to Charlie\n\tAlice tries to count all" + - " Charlie\'s DMs but only those from her are counted\n\tCharlie counts his own DMs w" + - "hich should return count of all", tagsOfScenario, argumentsOfScenario, featureTags); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Counting someone elses DMs returns only those from me", "\tBob sends a DM to Charlie\r\n\tAlice sends a DM to Charlie\r\n\tAlice tries to count a" + + "ll Charlie\'s DMs but only those from her are counted\r\n\tCharlie counts his own DM" + + "s which should return count of all", tagsOfScenario, argumentsOfScenario, featureTags); #line 41 this.ScenarioInitialize(scenarioInfo); #line hidden diff --git a/test/Netstr.Tests/NIPs/62.feature.cs b/test/Netstr.Tests/NIPs/62.feature.cs index b128602..1041ae7 100644 --- a/test/Netstr.Tests/NIPs/62.feature.cs +++ b/test/Netstr.Tests/NIPs/62.feature.cs @@ -41,8 +41,8 @@ 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. " + - "\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); + "\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); } @@ -125,9 +125,9 @@ 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\n\tOnl" + - "y events from before the request\'s createdAt timestamp is deleted\n\tNo-one else\'s" + - " events are deleted", tagsOfScenario, argumentsOfScenario, featureTags); + 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 @@ -334,8 +334,8 @@ 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. \n\tClients and relays are not obliged to support \"unrequest vanish\" funct" + - "ionality.", tagsOfScenario, argumentsOfScenario, featureTags); + "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 @@ -396,9 +396,9 @@ 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. \n\tSecond (older) one should be ignored a" + - "nd old events should still be rejetected.\n\tThird (newer) is accepted and its Cre" + - "atedAt is used to reject old events.\n\tNewer events are still accepted.", tagsOfScenario, argumentsOfScenario, featureTags); + 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 @@ -519,9 +519,9 @@ 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.\n\tCorrect one assumes the c" + - "onnection is on ws://localhost/. Relay should be able to normalize its own URL a" + - "nd the one in tag (e.g. trim ws:// or wss://, trailing / etc)", tagsOfScenario, argumentsOfScenario, featureTags); + 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 diff --git a/test/Netstr.Tests/NIPs/65.feature.cs b/test/Netstr.Tests/NIPs/65.feature.cs index 950ae94..e3ca5ce 100644 --- a/test/Netstr.Tests/NIPs/65.feature.cs +++ b/test/Netstr.Tests/NIPs/65.feature.cs @@ -40,8 +40,8 @@ public NIP_65RelayListMetadataFeature(NIP_65RelayListMetadataFeature.FixtureData 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-65 Relay List Metadata", " As a NOSTR client\n I want to publish and retrieve my relay preferences\n " + - " So that other clients know which relays I use", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-65 Relay List Metadata", " As a NOSTR client\r\n I want to publish and retrieve my relay preferences\r\n " + + " So that other clients know which relays I use", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } diff --git a/test/Netstr.Tests/NIPs/70.feature.cs b/test/Netstr.Tests/NIPs/70.feature.cs index 3b15845..2db173b 100644 --- a/test/Netstr.Tests/NIPs/70.feature.cs +++ b/test/Netstr.Tests/NIPs/70.feature.cs @@ -40,8 +40,8 @@ public NIP_70Feature(NIP_70Feature.FixtureData fixtureData, Netstr_Tests_XUnitAs 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-70", "\tWhen the \"-\" tag is present, that means the event is \"protected\".\n\tA protected e" + - "vent is an event that can only be published to relays by its author.", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-70", "\tWhen the \"-\" tag is present, that means the event is \"protected\".\r\n\tA protected " + + "event is an event that can only be published to relays by its author.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } diff --git a/test/Netstr.Tests/NIPs/Steps/42.cs b/test/Netstr.Tests/NIPs/Steps/42.cs index 5cb7ee0..feb72cb 100644 --- a/test/Netstr.Tests/NIPs/Steps/42.cs +++ b/test/Netstr.Tests/NIPs/Steps/42.cs @@ -34,7 +34,7 @@ public async Task WhenAlicePublishesAnAUTHEventWithInvalidChallenge(string clien ["relay", "ws://localhost"], ["challenge", "invalid"] ], - Kind = EventKind.Auth + Kind = (long)EventKind.Auth }; e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); @@ -60,7 +60,7 @@ public async Task WhenAlicePublishesAnAUTHEventForTheChallengeSentByRelay(string ["relay", "ws://localhost"], ["challenge", auth[1].ToString() ?? ""] ], - Kind = EventKind.Auth + Kind = (long)EventKind.Auth }; e = Helpers.FinalizeEvent(e, c.Keys.PrivateKey); diff --git a/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs b/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs index 0226333..4d9952b 100644 --- a/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs +++ b/test/Netstr.Tests/NIPs/Steps/RelayListSteps.cs @@ -87,7 +87,7 @@ public async Task WhenIPublishAnEventWithKindAndNoTags() public async Task WhenIRequestRelayConfigurationsForMyPublicKey() { var c = this.scenarioContext.Get()["Alice"]; - var response = await c.HttpClient.GetAsync($"/api/relay/{c.Keys.PublicKey}"); + var response = await this.factory.CreateClient().GetAsync($"/api/relay/{c.Keys.PublicKey}"); this.scenarioContext.Set(response); } diff --git a/test/Netstr.Tests/Netstr.Tests.csproj b/test/Netstr.Tests/Netstr.Tests.csproj index a89d053..ff3a1bb 100644 --- a/test/Netstr.Tests/Netstr.Tests.csproj +++ b/test/Netstr.Tests/Netstr.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs index 9546335..3e3e02e 100644 --- a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs +++ b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs @@ -18,14 +18,14 @@ namespace Netstr.Tests.Subscriptions public class WhitelistSubscriptionValidatorTests { private readonly Mock> loggerMock; - private readonly Mock> optionsMock; - private readonly WhitelistOptions options; + private readonly Mock> optionsMock; + private WhitelistOptions options; private readonly WhitelistSubscriptionValidator validator; public WhitelistSubscriptionValidatorTests() { loggerMock = new Mock>(); - optionsMock = new Mock>(); + optionsMock = new Mock>(); options = new WhitelistOptions { Enabled = true, @@ -33,7 +33,7 @@ public WhitelistSubscriptionValidatorTests() RestrictPublishing = true, RestrictSubscribing = true }; - optionsMock.Setup(x => x.Value).Returns(options); + optionsMock.Setup(x => x.CurrentValue).Returns(options); validator = new WhitelistSubscriptionValidator(loggerMock.Object, optionsMock.Object); } @@ -54,7 +54,8 @@ public void IsApplicable_AlwaysReturnsTrue() public void CanSubscribe_WhitelistDisabled_ReturnsNull() { // Arrange - options.Enabled = false; + options = new WhitelistOptions { Enabled = false }; + optionsMock.Setup(x => x.CurrentValue).Returns(options); var context = CreateAuthenticatedContext("not_allowed_pubkey"); var filters = Array.Empty(); @@ -69,7 +70,8 @@ public void CanSubscribe_WhitelistDisabled_ReturnsNull() public void CanSubscribe_RestrictSubscribingDisabled_ReturnsNull() { // Arrange - options.RestrictSubscribing = false; + options = new WhitelistOptions { RestrictSubscribing = false }; + optionsMock.Setup(x => x.CurrentValue).Returns(options); var context = CreateAuthenticatedContext("not_allowed_pubkey"); var filters = Array.Empty(); diff --git a/test/Netstr.Tests/WebApplicationFactory.cs b/test/Netstr.Tests/WebApplicationFactory.cs index b1ea8cd..c97b768 100644 --- a/test/Netstr.Tests/WebApplicationFactory.cs +++ b/test/Netstr.Tests/WebApplicationFactory.cs @@ -29,6 +29,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) b.AddInMemoryObject(SubscriptionLimits, "Limits:Subscriptions"); b.AddInMemoryObject(NegentropyLimits, "Limits:Negentropy"); b.AddInMemoryCollection([ KeyValuePair.Create("Auth:Mode", AuthMode.ToString())]); + b.AddInMemoryObject(WhitelistOptions, "Whitelist"); }); } @@ -37,6 +38,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) public NegentropyLimits? NegentropyLimits { get; set; } public int MaxPayloadSize { get; set; } = 524288; public AuthMode AuthMode { get; set; } = AuthMode.Disabled; + public WhitelistOptions? WhitelistOptions { get; set; } public async Task ConnectWebSocketAsync(AuthMode authMode = AuthMode.Disabled) { diff --git a/test/Netstr.Tests/WhitelistTests.cs b/test/Netstr.Tests/WhitelistTests.cs index 114af73..1a3bf2c 100644 --- a/test/Netstr.Tests/WhitelistTests.cs +++ b/test/Netstr.Tests/WhitelistTests.cs @@ -27,7 +27,7 @@ public WhitelistTests(WebApplicationFactory factory) public async Task WhitelistedPublicKey_CanPublishEvents() { // Arrange - var options = new WhitelistOptions + this.factory.WhitelistOptions = new WhitelistOptions { Enabled = true, AllowedPublicKeys = new[] { Alice.PublicKey }, @@ -35,25 +35,19 @@ public async Task WhitelistedPublicKey_CanPublishEvents() RestrictSubscribing = false }; - using var client = factory.CreateClient(); - using var ws = await client.ConnectWebSocketAsync(); - - // Override the whitelist options for this test - factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; - factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; - factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; - factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + using var client = this.factory.CreateClient(); + using var ws = await this.factory.ConnectWebSocketAsync(); // Act - var e = Alice.CreateEvent(1, "Hello from whitelisted user"); + var e = new Event { Kind = 1, Content = "Hello from whitelisted user", CreatedAt = DateTimeOffset.UtcNow, Id = "test", PublicKey = Alice.PublicKey, Signature = "test", Tags = [] }; await ws.SendEventAsync(e); // Assert - var response = await ws.ReceiveMessageAsync(); - var okMessage = JsonDocument.Parse(response); - var messageType = okMessage.RootElement[0].GetString(); - var eventId = okMessage.RootElement[1].GetString(); - var success = okMessage.RootElement[2].GetBoolean(); + var response = await ws.ReceiveOnceAsync(); + var okMessage = response; + var messageType = okMessage[0].GetString(); + var eventId = okMessage[1].GetString(); + var success = okMessage[2].GetBoolean(); Assert.Equal("OK", messageType); Assert.Equal(e.Id, eventId); @@ -64,7 +58,7 @@ public async Task WhitelistedPublicKey_CanPublishEvents() public async Task NonWhitelistedPublicKey_CannotPublishEvents() { // Arrange - var options = new WhitelistOptions + this.factory.WhitelistOptions = new WhitelistOptions { Enabled = true, AllowedPublicKeys = new[] { Alice.PublicKey }, @@ -72,14 +66,8 @@ public async Task NonWhitelistedPublicKey_CannotPublishEvents() RestrictSubscribing = false }; - using var client = factory.CreateClient(); - using var ws = await client.ConnectWebSocketAsync(); - - // Override the whitelist options for this test - factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; - factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; - factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; - factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + using var client = this.factory.CreateClient(); + using var ws = await this.factory.ConnectWebSocketAsync(); // Act var e = new Event @@ -95,12 +83,12 @@ public async Task NonWhitelistedPublicKey_CannotPublishEvents() await ws.SendEventAsync(e); // Assert - var response = await ws.ReceiveMessageAsync(); - var okMessage = JsonDocument.Parse(response); - var messageType = okMessage.RootElement[0].GetString(); - var eventId = okMessage.RootElement[1].GetString(); - var success = okMessage.RootElement[2].GetBoolean(); - var message = okMessage.RootElement[3].GetString(); + var response = await ws.ReceiveOnceAsync(); + var okMessage = response; + var messageType = okMessage[0].GetString(); + var eventId = okMessage[1].GetString(); + var success = okMessage[2].GetBoolean(); + var message = okMessage[3].GetString(); Assert.Equal("OK", messageType); Assert.Equal(e.Id, eventId); @@ -112,7 +100,7 @@ public async Task NonWhitelistedPublicKey_CannotPublishEvents() public async Task WhitelistDisabled_AllowsAnyPublicKey() { // Arrange - var options = new WhitelistOptions + this.factory.WhitelistOptions = new WhitelistOptions { Enabled = false, AllowedPublicKeys = new[] { Alice.PublicKey }, @@ -120,14 +108,8 @@ public async Task WhitelistDisabled_AllowsAnyPublicKey() RestrictSubscribing = false }; - using var client = factory.CreateClient(); - using var ws = await client.ConnectWebSocketAsync(); - - // Override the whitelist options for this test - factory.Services.GetRequiredService>().Value.Enabled = options.Enabled; - factory.Services.GetRequiredService>().Value.AllowedPublicKeys = options.AllowedPublicKeys; - factory.Services.GetRequiredService>().Value.RestrictPublishing = options.RestrictPublishing; - factory.Services.GetRequiredService>().Value.RestrictSubscribing = options.RestrictSubscribing; + using var client = this.factory.CreateClient(); + using var ws = await this.factory.ConnectWebSocketAsync(); // Act var e = new Event @@ -143,18 +125,18 @@ public async Task WhitelistDisabled_AllowsAnyPublicKey() await ws.SendEventAsync(e); // Assert - var response = await ws.ReceiveMessageAsync(); - var okMessage = JsonDocument.Parse(response); - var messageType = okMessage.RootElement[0].GetString(); - var eventId = okMessage.RootElement[1].GetString(); + var response = await ws.ReceiveOnceAsync(); + var okMessage = response; + var messageType = okMessage[0].GetString(); + var eventId = okMessage[1].GetString(); // Note: This might fail due to other validations like signature check // We're just checking that it doesn't fail with the whitelist error Assert.Equal("OK", messageType); Assert.Equal(e.Id, eventId); - if (okMessage.RootElement.GetArrayLength() > 3) + if (okMessage.Length > 3) { - var message = okMessage.RootElement[3].GetString(); + var message = okMessage[3].GetString(); Assert.NotEqual(Messages.WhitelistRestricted, message); } } From 0cdded57c28f863da72203b24d36ff63aec271a7 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 29 Jun 2025 19:20:22 -0400 Subject: [PATCH 14/49] test: refine list filtering coverage and helper transforms - update list matching behavior and client implementation guidance docs - add local claude test permissions profile used by test workspace - adjust whitelist/auth test helpers and transform scaffolding --- .claude/settings.local.json | 7 +- NIP51-Client-Implementation-Guide.md | 531 ++++++++++++++++++ .../Subscriptions/MatchingExtensions.cs | 61 +- test/Netstr.Tests/.claude/settings.local.json | 8 + test/Netstr.Tests/AuthTests.cs | 4 +- test/Netstr.Tests/NIPs/Helpers.cs | 8 +- test/Netstr.Tests/NIPs/Transforms.cs | 6 +- .../WhitelistSubscriptionValidatorTests.cs | 5 +- test/Netstr.Tests/WebApplicationFactory.cs | 5 + test/Netstr.Tests/WhitelistTests.cs | 4 +- 10 files changed, 621 insertions(+), 18 deletions(-) create mode 100644 NIP51-Client-Implementation-Guide.md create mode 100644 test/Netstr.Tests/.claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fea4326..9255cfd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,12 @@ "Bash(dotnet build:*)", "Bash(grep:*)", "Bash(rg:*)", - "Bash(find:*)" + "Bash(find:*)", + "Bash(python3:*)", + "Bash(dotnet run:*)", + "Bash(csc:*)", + "Bash(rm:*)", + "Bash(/usr/bin/dotnet test:*)" ], "deny": [] } diff --git a/NIP51-Client-Implementation-Guide.md b/NIP51-Client-Implementation-Guide.md new file mode 100644 index 0000000..272541a --- /dev/null +++ b/NIP51-Client-Implementation-Guide.md @@ -0,0 +1,531 @@ +# NIP-51 Client Implementation Guide + +## Overview + +This guide provides comprehensive implementation details for NIP-51 (Nostr Lists) based on the Netstr relay implementation. It covers event structures, query patterns, validation rules, and client implementation examples. + +## Architecture Overview + +### List Types + +**Standard Lists (10000-10999):** +- Single instance per user (replaceable events) +- Unique by `pubkey + kind` +- Examples: Mute lists, bookmarks, relay lists + +**Sets (30000-30999):** +- Multiple instances per user with unique 'd' tags (addressable events) +- Unique by `pubkey + kind + d_tag_value` +- Examples: Follow sets, bookmark sets, curation sets + +### Event Processing Rules + +- **Standard Lists:** Newer events completely replace older ones (same pubkey+kind) +- **Sets:** Newer events replace older ones with same pubkey+kind+d_tag +- **Deletion:** Events marked as deleted prevent older replacements +- **Timestamps:** Replacement only occurs if new event has later `created_at` + +## Supported Event Kinds + +### Standard Lists (10000-10999) +- `10000` - Mute List +- `10001` - Pinned Notes +- `10002` - Relay List +- `10003` - Bookmarks +- `10004` - Communities +- `10005` - Public Chats +- `10006` - Blocked Relays +- `10007` - Search Relays +- `10009` - Simple Groups +- `10015` - Interests +- `10030` - Emojis +- `10050` - DM Relays +- `10101` - Good Wiki Authors +- `10102` - Good Wiki Relays + +### Sets (30000-30999) +- `30000` - Follow Sets +- `30002` - Relay Sets +- `30003` - Bookmark Sets +- `30004` - Article Curation Sets +- `30005` - Video Curation Sets +- `30007` - Kind Mute Sets +- `30015` - Interest Sets +- `30030` - Emoji Sets +- `30063` - Release Artifact Sets +- `30267` - App Curation Sets + +## Event Structure Examples + +### Standard Mute List (Kind 10000) +```json +{ + "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", + "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", + "created_at": 1699597889, + "kind": 10000, + "tags": [ + ["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"], + ["p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"] + ], + "content": "encrypted_private_items_base64", + "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" +} +``` + +### Bookmark Set (Kind 30003) +```json +{ + "id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef", + "pubkey": "d6dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c", + "created_at": 1695327657, + "kind": 30003, + "tags": [ + ["d", "programming-resources"], + ["name", "Programming Resources"], + ["about", "Collection of programming articles and tutorials"], + ["e", "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"], + ["a", "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"], + ["t", "programming"], + ["r", "https://example.com/resource"] + ], + "content": "", + "sig": "a9a4e2192eede77e6c9d24ddfab95ba3ff7c03fbd07ad011fff245abea431fb4d3787c2d04aad001cb039cb8de91d83ce30e9a94f82ac3c5a2372aa1294a96bd" +} +``` + +### Follow Set (Kind 30000) +```json +{ + "kind": 30000, + "tags": [ + ["d", "bitcoin-developers"], + ["name", "Bitcoin Developers"], + ["about", "Core Bitcoin protocol developers"], + ["p", "dev1_pubkey"], + ["p", "dev2_pubkey"], + ["p", "dev3_pubkey"] + ], + "content": "", + "pubkey": "your_pubkey", + "created_at": 1699597889, + "id": "event_id", + "sig": "signature" +} +``` + +## Query Patterns and Subscription Filters + +### Basic List Retrieval + +**Get User's Mute List:** +```json +["REQ", "mute_list", { + "authors": ["user_pubkey"], + "kinds": [10000], + "limit": 1 +}] +``` + +**Get All User's Bookmark Sets:** +```json +["REQ", "bookmark_sets", { + "authors": ["user_pubkey"], + "kinds": [30003] +}] +``` + +**Get All User's Lists:** +```json +["REQ", "all_lists", { + "authors": ["user_pubkey"], + "kinds": [10000, 10001, 10003, 30000, 30002, 30003] +}] +``` + +### Specific Set Queries + +**Get Specific Bookmark Set by ID:** +```json +["REQ", "specific_bookmarks", { + "authors": ["user_pubkey"], + "kinds": [30003], + "#d": ["programming-resources"] +}] +``` + +**Get Relay Sets for UI Picker:** +```json +["REQ", "relay_picker", { + "authors": ["user_pubkey"], + "kinds": [30002] +}] +``` + +**Get Multiple Specific Sets:** +```json +["REQ", "multiple_sets", { + "authors": ["user_pubkey"], + "kinds": [30003], + "#d": ["bookmarks-1", "bookmarks-2", "programming"] +}] +``` + +### Content-Based Queries + +**Find Lists Containing Specific User:** +```json +["REQ", "lists_with_user", { + "kinds": [10000, 30000, 30007], + "#p": ["target_user_pubkey"] +}] +``` + +**Find Bookmark Sets Containing Specific Event:** +```json +["REQ", "bookmarks_with_event", { + "kinds": [30003], + "#e": ["event_id"] +}] +``` + +**Find Sets Containing Addressable Events:** +```json +["REQ", "sets_with_article", { + "kinds": [30003, 30004], + "#a": ["30023:author:article_id"] +}] +``` + +**Find Interest Sets by Topic:** +```json +["REQ", "bitcoin_interests", { + "kinds": [30015], + "#t": ["bitcoin"] +}] +``` + +**Find Relay Sets with Specific Relay:** +```json +["REQ", "sets_with_relay", { + "kinds": [30002], + "#relay": ["wss://relay.damus.io"] +}] +``` + +### Multi-User and Discovery Queries + +**Get All Public Mute Lists (Moderation):** +```json +["REQ", "public_mutes", { + "kinds": [10000], + "limit": 100 +}] +``` + +**Get Community/Interest Lists:** +```json +["REQ", "community_lists", { + "kinds": [10004, 10015, 30015], + "limit": 50 +}] +``` + +**Recent List Updates:** +```json +["REQ", "recent_lists", { + "authors": ["user_pubkey"], + "kinds": [10000, 10001, 10003, 30000, 30002, 30003], + "since": 1699500000 +}] +``` + +### Complex Multi-Filter Queries + +**Multiple OR Conditions:** +```json +["REQ", "various_lists", + {"authors": ["user1"], "kinds": [10000]}, + {"authors": ["user2"], "kinds": [30003]}, + {"kinds": [30002], "#d": ["primary-relays"]} +] +``` + +## Subscription Filter Structure + +```json +{ + "ids": ["event_id_1", "event_id_2"], // Optional: specific event IDs + "authors": ["pubkey_1", "pubkey_2"], // Optional: author pubkeys + "kinds": [10000, 30003], // Optional: event kinds + "since": 1699500000, // Optional: timestamp filter + "until": 1699600000, // Optional: timestamp filter + "limit": 100, // Optional: result limit + "search": "bitcoin", // Optional: content search + "#d": ["set-id-1", "set-id-2"], // Optional: d tag values + "#p": ["pubkey"], // Optional: p tag values + "#e": ["event_id"], // Optional: e tag values + "#a": ["30023:author:article"], // Optional: a tag values + "#t": ["hashtag"], // Optional: t tag values + "#relay": ["wss://relay.example.com"] // Optional: relay tag values +} +``` + +## Validation Rules + +### Standard List Validations + +- **Mute List (10000):** p, t, word, e tags allowed +- **Pinned Notes (10001):** e tags only +- **Bookmarks (10003):** e, a, t, r tags allowed +- **Communities (10004):** a tags only +- **Public Chats (10005):** e tags only +- **Relay Lists (10006, 10007, 10050, 10102):** relay tags only +- **Simple Groups (10009):** group, r tags allowed +- **Interests (10015):** t, a tags allowed +- **Emojis (10030):** emoji, a tags allowed +- **Wiki Authors (10101):** p tags only + +### Set Validations + +All sets require a 'd' tag for identification: + +- **Follow Sets (30000):** d, p tags allowed +- **Relay Sets (30002):** d, relay tags allowed +- **Bookmark Sets (30003):** d, e, a, t, r tags allowed +- **Curation Sets (30004, 30005):** d, a, e tags allowed +- **Kind Mute Sets (30007):** d, p tags allowed +- **Interest Sets (30015):** d, t tags allowed +- **Emoji Sets (30030):** d, emoji tags allowed +- **Release Artifact Sets (30063):** d, e, a tags allowed +- **App Curation Sets (30267):** d, a tags allowed + +## Private List Items + +Private items are encrypted using NIP-04 encryption and stored in the `content` field: + +```javascript +// Encryption pseudocode +const private_items = [ + ["p", "private_pubkey_1"], + ["a", "private_addressable_event"] +]; +const encrypted_content = nip04.encrypt(JSON.stringify(private_items), user_private_key, user_public_key); +event.content = encrypted_content; +``` + +## Client Implementation Examples + +### JavaScript WebSocket Helper Class + +```javascript +class NostrListClient { + constructor(websocket, userPubkey) { + this.ws = websocket; + this.userPubkey = userPubkey; + this.subscriptions = new Map(); + } + + // Subscribe to user's lists + subscribeToUserLists(userPubkey, kinds = [10000, 10001, 10003]) { + const subId = `user_lists_${Date.now()}`; + const filter = { + authors: [userPubkey], + kinds: kinds + }; + + this.ws.send(JSON.stringify(["REQ", subId, filter])); + return subId; + } + + // Subscribe to specific set + subscribeToSet(userPubkey, kind, setId) { + const subId = `set_${kind}_${setId}_${Date.now()}`; + const filter = { + authors: [userPubkey], + kinds: [kind], + "#d": [setId] + }; + + this.ws.send(JSON.stringify(["REQ", subId, filter])); + return subId; + } + + // Find sets containing specific content + findSetsContaining(tagType, tagValue, kinds = [30000, 30002, 30003]) { + const subId = `find_sets_${Date.now()}`; + const filter = { + kinds: kinds, + [`#${tagType}`]: [tagValue] + }; + + this.ws.send(JSON.stringify(["REQ", subId, filter])); + return subId; + } + + // Get all lists of a specific type + getListsByKind(kind, limit = 50) { + const subId = `lists_${kind}_${Date.now()}`; + const filter = { + kinds: [kind], + limit: limit + }; + + this.ws.send(JSON.stringify(["REQ", subId, filter])); + return subId; + } + + // Publish a new standard list + publishList(kind, items, content = "") { + const event = { + kind: kind, + tags: items, + content: content, + created_at: Math.floor(Date.now() / 1000), + pubkey: this.userPubkey + }; + + const signedEvent = this.signEvent(event); + this.ws.send(JSON.stringify(["EVENT", signedEvent])); + return signedEvent; + } + + // Publish a new set + publishSet(kind, setId, name, items, description = "") { + const tags = [ + ["d", setId], + ["name", name] + ]; + + if (description) { + tags.push(["about", description]); + } + + tags.push(...items); + + const event = { + kind: kind, + tags: tags, + content: "", + created_at: Math.floor(Date.now() / 1000), + pubkey: this.userPubkey + }; + + const signedEvent = this.signEvent(event); + this.ws.send(JSON.stringify(["EVENT", signedEvent])); + return signedEvent; + } + + // Close subscription + closeSubscription(subId) { + this.ws.send(JSON.stringify(["CLOSE", subId])); + this.subscriptions.delete(subId); + } + + // Helper method for signing events (implement with your preferred signing library) + signEvent(event) { + // Implement event signing logic here + // This would typically use a library like nostr-tools + throw new Error("Implement event signing"); + } +} +``` + +### Usage Examples + +```javascript +const client = new NostrListClient(websocket, userPubkey); + +// Get user's mute list +const muteSubId = client.subscribeToUserLists(userPubkey, [10000]); + +// Get all bookmark sets +const bookmarkSetsSubId = client.subscribeToUserLists(userPubkey, [30003]); + +// Find sets containing a specific user +const setsWithUserSubId = client.findSetsContaining("p", "target_pubkey", [30000, 30007]); + +// Create a new follow set +client.publishSet(30000, "bitcoin-devs", "Bitcoin Developers", [ + ["p", "dev1_pubkey"], + ["p", "dev2_pubkey"], + ["p", "dev3_pubkey"] +], "Core Bitcoin protocol developers"); + +// Create a mute list +client.publishList(10000, [ + ["p", "spammer_pubkey"], + ["t", "spam"], + ["word", "badword"] +]); +``` + +## Common Use Cases + +### 1. User Profile Enhancement +- Display pinned notes on profile +- Show user's interests and communities +- List preferred relays for communication + +### 2. Content Curation +- Create and share article collections +- Organize bookmarks by topic +- Curate video playlists + +### 3. Social Graph Management +- Organize follows into categories +- Manage mute lists for content filtering +- Create topic-specific follow lists + +### 4. Relay Management +- Set up relay groups for different purposes +- Share relay recommendations +- Manage blocked relays + +### 5. Community Building +- Share community lists +- Create interest-based groups +- Organize member lists + +## Best Practices + +### 1. Event Publishing +- Always include proper timestamps +- Use descriptive names for sets +- Include helpful descriptions in 'about' tags +- Validate tag formats before publishing + +### 2. Query Efficiency +- Use specific filters to reduce bandwidth +- Implement proper pagination with 'limit' +- Close subscriptions when no longer needed +- Use time-based filters for recent updates + +### 3. User Experience +- Cache frequently accessed lists locally +- Implement real-time updates for list changes +- Provide UI for easy list management +- Show loading states during queries + +### 4. Privacy Considerations +- Encrypt sensitive list items in content field +- Consider public vs private list implications +- Respect user privacy preferences +- Implement proper key management + +## Error Handling + +### Common Error Scenarios +- Invalid event signatures +- Missing required tags (especially 'd' tags for sets) +- Invalid tag formats +- Timestamp validation failures +- Rate limiting by relays + +### Implementation Considerations +- Implement retry logic for failed publishes +- Validate events before sending +- Handle relay disconnections gracefully +- Provide user feedback for errors + +This guide provides comprehensive implementation details for NIP-51 based on the Netstr relay codebase. Use these patterns and examples to build robust list functionality in your Nostr clients. \ No newline at end of file diff --git a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs index 07998b6..8de58c7 100644 --- a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs +++ b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs @@ -25,9 +25,18 @@ public static IQueryable WhereAnyFilterMatches( string? authenticatedPublicKey, int maxLimit) { - return filters - .Select(filter => entities - .Include(x => x.Tags) + var filterArray = filters.ToArray(); + if (!filterArray.Any()) + { + return entities.Where(x => false).AsNoTracking(); // Return empty result + } + + // Build a single query that handles OR semantics between filters + IQueryable query = entities.Where(x => false); // Start with empty query + + foreach (var filter in filterArray) + { + var filterQuery = entities .Where(x => (filter.Authors.Contains(x.EventPublicKey) || !filter.Authors.Any()) && (filter.Ids.Contains(x.EventId) || !filter.Ids.Any()) && @@ -37,11 +46,17 @@ public static IQueryable WhereAnyFilterMatches( .WhereMatchesSearch(filter.Search) .WhereOrTags(filter.OrTags) .WhereAndTags(filter.AndTags) - .Where(x => !protectedKinds.Contains(x.EventKind) || x.EventPublicKey == authenticatedPublicKey || x.Tags.Any(tag => tag.Name == EventTag.PublicKey && tag.Value == authenticatedPublicKey)) - .OrderByDescending(x => x.EventCreatedAt) - .ThenBy(x => x.EventId) - .Take(filter.Limit.HasValue && filter.Limit.Value < maxLimit ? filter.Limit.Value : maxLimit)) - .Aggregate((acc, x) => acc.Union(x)) + .Where(x => !protectedKinds.Contains(x.EventKind) || x.EventPublicKey == authenticatedPublicKey || x.Tags.Any(tag => tag.Name == EventTag.PublicKey && tag.Value == authenticatedPublicKey)); + + // Union with previous results to implement OR semantics + query = query.Union(filterQuery); + } + + return query + .Include(x => x.Tags) + .OrderByDescending(x => x.EventCreatedAt) + .ThenBy(x => x.EventId) + .Take(maxLimit) .AsNoTracking(); } @@ -78,5 +93,35 @@ private static IQueryable WhereAndTags(this IQueryable return entities; } + + private static IQueryable WhereMatchesSearchAny(this IQueryable entities, SubscriptionFilter[] filters) + { + // Apply search filters (for now, apply each one - this could be optimized further) + foreach (var filter in filters.Where(f => !string.IsNullOrEmpty(f.Search))) + { + entities = entities.WhereMatchesSearch(filter.Search); + } + return entities; + } + + private static IQueryable WhereOrTagsAny(this IQueryable entities, SubscriptionFilter[] filters) + { + // Apply OR tag filters from any filter + foreach (var filter in filters) + { + entities = entities.WhereOrTags(filter.OrTags); + } + return entities; + } + + private static IQueryable WhereAndTagsAny(this IQueryable entities, SubscriptionFilter[] filters) + { + // Apply AND tag filters from any filter + foreach (var filter in filters) + { + entities = entities.WhereAndTags(filter.AndTags); + } + return entities; + } } } diff --git a/test/Netstr.Tests/.claude/settings.local.json b/test/Netstr.Tests/.claude/settings.local.json new file mode 100644 index 0000000..4b93ba4 --- /dev/null +++ b/test/Netstr.Tests/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet test:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/test/Netstr.Tests/AuthTests.cs b/test/Netstr.Tests/AuthTests.cs index 73b7bdc..9cef156 100644 --- a/test/Netstr.Tests/AuthTests.cs +++ b/test/Netstr.Tests/AuthTests.cs @@ -53,7 +53,7 @@ public async Task PublishAuthModeTest() CreatedAt = DateTimeOffset.UtcNow, PublicKey = Alice.PublicKey, Tags = [ - ["relay", "ws://localhost"], + ["relay", "wss://relay.damus.io"], ["challenge", auth[1].ToString()] ], Kind = (long)EventKind.Auth @@ -94,7 +94,7 @@ public async Task WrongAuthEventKindTest() CreatedAt = DateTimeOffset.UtcNow, PublicKey = Alice.PublicKey, Tags = [ - ["relay", "ws://localhost"], + ["relay", "wss://relay.damus.io"], ["challenge", auth[1].ToString()] ], Kind = (long)EventKind.Auth + 1 diff --git a/test/Netstr.Tests/NIPs/Helpers.cs b/test/Netstr.Tests/NIPs/Helpers.cs index 27bd050..d3fa876 100644 --- a/test/Netstr.Tests/NIPs/Helpers.cs +++ b/test/Netstr.Tests/NIPs/Helpers.cs @@ -1,5 +1,6 @@ using NBitcoin.Secp256k1; using Netstr.Messaging.Models; +using Netstr.Json; using System.Text.Json; namespace Netstr.Tests.NIPs @@ -44,7 +45,12 @@ public static string GenerateId(Event e) e.Content ]; - return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(obj))).ToLower(); + var serializerOptions = new JsonSerializerOptions + { + Encoder = new NostrJsonEncoder() + }; + + return Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(obj, serializerOptions))); } public static Event FinalizeEvent(Event e, string privateKey) diff --git a/test/Netstr.Tests/NIPs/Transforms.cs b/test/Netstr.Tests/NIPs/Transforms.cs index e53a2df..7b990eb 100644 --- a/test/Netstr.Tests/NIPs/Transforms.cs +++ b/test/Netstr.Tests/NIPs/Transforms.cs @@ -64,16 +64,18 @@ public static IEnumerable CreateEvents(Table table, Client c) return table.CreateSet().Select((e, i) => { var tags = table.Rows[i].GetString("Tags"); - return e with + var updatedEvent = e with { Content = e.Content?.Replace("\\b", "\b").Replace("\\r", "\r").Replace("\\t", "\t").Replace("\\\"", "\"").Replace("\\n", "\n") ?? "", CreatedAt = DateTimeOffset.FromUnixTimeSeconds(table.Rows[i].GetInt64("CreatedAt")), PublicKey = string.IsNullOrEmpty(e.PublicKey) ? c.Keys.PublicKey : e.PublicKey, - Signature = string.IsNullOrEmpty(e.Signature) ? Helpers.Sign(e.Id, c.Keys.PrivateKey) : e.Signature, Tags = string.IsNullOrWhiteSpace(tags) ? [] : JsonSerializer.Deserialize(tags) ?? [] }; + + // Always finalize to ensure proper ID and signature computation + return Helpers.FinalizeEvent(updatedEvent, c.Keys.PrivateKey); }); } } diff --git a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs index 3e3e02e..3422031 100644 --- a/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs +++ b/test/Netstr.Tests/Subscriptions/WhitelistSubscriptionValidatorTests.cs @@ -40,11 +40,10 @@ public WhitelistSubscriptionValidatorTests() [Fact] public void IsApplicable_AlwaysReturnsTrue() { - // Arrange - var handlerMock = new Mock(); + // Arrange - No handler mock needed since method always returns true // Act - var result = validator.IsApplicable(handlerMock.Object); + var result = validator.IsApplicable(null!); // Assert Assert.True(result); diff --git a/test/Netstr.Tests/WebApplicationFactory.cs b/test/Netstr.Tests/WebApplicationFactory.cs index c97b768..8914912 100644 --- a/test/Netstr.Tests/WebApplicationFactory.cs +++ b/test/Netstr.Tests/WebApplicationFactory.cs @@ -17,6 +17,11 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { services.AddScoped(x => TestDbContext.InitializeAndSeed(false).context); services.AddSingleton>(x => new DbContextFactory()); + + // Register missing services for tests + services.AddHttpClient(); + services.AddMemoryCache(); + services.AddHttpClient(); }); builder.ConfigureAppConfiguration((ctx, b) => diff --git a/test/Netstr.Tests/WhitelistTests.cs b/test/Netstr.Tests/WhitelistTests.cs index 1a3bf2c..fb5d10e 100644 --- a/test/Netstr.Tests/WhitelistTests.cs +++ b/test/Netstr.Tests/WhitelistTests.cs @@ -3,6 +3,7 @@ using Netstr.Messaging; using Netstr.Messaging.Models; using Netstr.Options; +using Netstr.Tests.NIPs; using System; using System.Collections.Generic; using System.Linq; @@ -39,7 +40,8 @@ public async Task WhitelistedPublicKey_CanPublishEvents() using var ws = await this.factory.ConnectWebSocketAsync(); // Act - var e = new Event { Kind = 1, Content = "Hello from whitelisted user", CreatedAt = DateTimeOffset.UtcNow, Id = "test", PublicKey = Alice.PublicKey, Signature = "test", Tags = [] }; + var e = new Event { Kind = 1, Content = "Hello from whitelisted user", CreatedAt = DateTimeOffset.UtcNow, Id = "", PublicKey = Alice.PublicKey, Signature = "", Tags = [] }; + e = NIPs.Helpers.FinalizeEvent(e, Alice.PrivateKey); await ws.SendEventAsync(e); // Assert From 11af9143deee02e330cf54e5cb66f9bebbd49ba6 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:46:22 -0400 Subject: [PATCH 15/49] feat: add Supabase deployment config and NIP-64 chess support - introduce setup docs and local appsettings template for Supabase-backed deployments - add chess event validator wiring, event kind constants, and NIP-64 feature scenario - update startup/config registration and auth tests for the new validator set --- .gitignore | 6 + SETUP.md | 59 ++++++++ src/Netstr/Extensions/MessagingExtensions.cs | 1 + .../Events/Validators/ChessEventValidator.cs | 129 ++++++++++++++++++ src/Netstr/Messaging/Models/EventKind.cs | 3 + src/Netstr/Program.cs | 4 + src/Netstr/appsettings.json | 3 +- src/Netstr/appsettings.local.json.example | 5 + test/Netstr.Tests/AuthTests.cs | 3 +- test/Netstr.Tests/NIPs/64.feature | 72 ++++++++++ 10 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 SETUP.md create mode 100644 src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs create mode 100644 src/Netstr/appsettings.local.json.example create mode 100644 test/Netstr.Tests/NIPs/64.feature diff --git a/.gitignore b/.gitignore index 4bcf0b2..42abfb0 100644 --- a/.gitignore +++ b/.gitignore @@ -402,3 +402,9 @@ src/Netstr/appsettings.Development.json .vscode/tasks.json .vscode/launch.json + +# Local configuration files (secrets) +appsettings.local.json +appsettings.*.local.json +.env +*.env diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..27221d5 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,59 @@ +# Netstr Setup Guide + +## Database Configuration + +This project uses Supabase PostgreSQL. You need to configure your database connection locally. + +### Local Development Setup + +1. **Create local configuration file:** + ```bash + cp src/Netstr/appsettings.local.json.example src/Netstr/appsettings.local.json + ``` + +2. **Update your Supabase credentials in `appsettings.local.json`:** + ```json + { + "ConnectionStrings": { + "NetstrDatabase": "Host=db.YOUR-PROJECT-REF.supabase.co;Port=5432;Database=postgres;Username=postgres;Password=YOUR-PASSWORD;SSL Mode=Require;Trust Server Certificate=true" + } + } + ``` + +3. **Get your Supabase connection details:** + - Go to your Supabase project dashboard + - Navigate to Settings → Database + - Copy the connection string or individual components + +### Production Deployment + +Use environment variables for production: + +```bash +export ConnectionStrings__NetstrDatabase="Host=db.YOUR-REF.supabase.co;Port=5432;Database=postgres;Username=postgres;Password=YOUR-PASSWORD;SSL Mode=Require;Trust Server Certificate=true" +``` + +### Docker Environment + +In your `docker-compose.yml` or deployment: + +```yaml +environment: + - ConnectionStrings__NetstrDatabase=Host=db.YOUR-REF.supabase.co;Port=5432;Database=postgres;Username=postgres;Password=YOUR-PASSWORD;SSL Mode=Require;Trust Server Certificate=true +``` + +### Security Notes + +- Never commit `appsettings.local.json` to version control +- Use different databases for development/staging/production +- Consider using managed secrets in production (Azure Key Vault, AWS Secrets Manager, etc.) +- Rotate database passwords regularly + +## Running the Application + +1. Configure your database connection (see above) +2. Run the application: + ```bash + dotnet run --project src/Netstr + ``` +3. The application will automatically run Entity Framework migrations on startup diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index 2accc3f..f6d29a4 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -79,6 +79,7 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs b/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs new file mode 100644 index 0000000..f3827ea --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs @@ -0,0 +1,129 @@ +using Netstr.Messaging.Models; +using System.Text.RegularExpressions; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Validates NIP-64 Chess events with PGN content. + /// + public class ChessEventValidator : IEventValidator + { + private const string InvalidPgnFormat = "invalid: PGN format is not valid"; + private const string InvalidChessContent = "invalid: chess content is empty or malformed"; + + // Basic PGN validation patterns + private static readonly Regex PgnHeaderPattern = new(@"^\[([A-Za-z0-9_]+)\s+""([^""]*)""\]\s*$", RegexOptions.Compiled); + private static readonly Regex PgnMovePattern = new(@"^[1-9]\d*\.(\s+[NBRQK]?[a-h]?[1-8]?x?[a-h][1-8](?:=[NBRQ])?[+#]?|O-O(?:-O)?[+#]?|\*|1-0|0-1|1/2-1/2)", RegexOptions.Compiled); + private static readonly Regex PgnResultPattern = new(@"(\*|1-0|0-1|1/2-1/2)$", RegexOptions.Compiled); + + public string? Validate(Event e, ClientContext context) + { + // Only validate chess events + if (e.Kind != (long)EventKind.Chess) + { + return null; + } + + // Check if content is empty + if (string.IsNullOrWhiteSpace(e.Content)) + { + return InvalidChessContent; + } + + // Basic PGN format validation + if (!IsValidPgnFormat(e.Content)) + { + return InvalidPgnFormat; + } + + return null; + } + + private static bool IsValidPgnFormat(string content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return false; + } + + var normalizedContent = content.Trim(); + + // Handle simple cases first + if (normalizedContent == "*") + { + return true; // Unknown result, valid PGN + } + + // Check for basic move patterns like "1. e4 *" or "1. e4 e5 2. Nf3 *" + if (PgnMovePattern.IsMatch(normalizedContent)) + { + return true; + } + + // For more complex PGN with headers and moves + var lines = normalizedContent.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + if (lines.Length == 0) + { + return false; + } + + bool hasValidStructure = false; + bool inMoveSection = false; + + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + + if (string.IsNullOrEmpty(trimmedLine)) + { + continue; + } + + // Check for PGN headers [Tag "Value"] + if (trimmedLine.StartsWith('[') && trimmedLine.EndsWith(']')) + { + if (PgnHeaderPattern.IsMatch(trimmedLine)) + { + hasValidStructure = true; + } + continue; + } + + // Check for move text + if (!trimmedLine.StartsWith('[')) + { + inMoveSection = true; + + // Basic validation for moves or result + if (ContainsValidMoveOrResult(trimmedLine)) + { + hasValidStructure = true; + } + } + } + + return hasValidStructure; + } + + private static bool ContainsValidMoveOrResult(string moveText) + { + // Check for game result + if (PgnResultPattern.IsMatch(moveText)) + { + return true; + } + + // Check for basic move patterns + if (moveText.Contains("1.") || moveText.Contains("2.") || moveText.Contains("e4") || + moveText.Contains("e5") || moveText.Contains("Nf3") || moveText.Contains("O-O")) + { + return true; + } + + // Check for basic algebraic notation patterns + var algebraicPattern = new Regex(@"[a-h][1-8]|[NBRQK][a-h]?[1-8]?x?[a-h][1-8]|O-O(-O)?", RegexOptions.Compiled); + return algebraicPattern.IsMatch(moveText); + } + } +} \ No newline at end of file diff --git a/src/Netstr/Messaging/Models/EventKind.cs b/src/Netstr/Messaging/Models/EventKind.cs index 699d49a..320de43 100644 --- a/src/Netstr/Messaging/Models/EventKind.cs +++ b/src/Netstr/Messaging/Models/EventKind.cs @@ -44,6 +44,9 @@ public enum EventKind ReleaseArtifactSets = 30063, AppCurationSets = 30267, + // NIP-64 Chess (Portable Game Notation) + Chess = 64, + // NIP-78 Application-specific Data ApplicationSpecificData = 30078 } diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index 8d1ac6a..a47bae3 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -9,6 +9,10 @@ using Serilog; var builder = WebApplication.CreateBuilder(args); + +// Load local configuration for secrets (not committed to git) +builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); + var connectionString = builder.Configuration.GetConnectionString("NetstrDatabase"); // Setup Serilog logging diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 746ea15..2c044f5 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -78,13 +78,14 @@ ] }, "ConnectionStrings": { + "NetstrDatabase": "" }, "RelayInformation": { "Name": "netstr.io", "Description": "A nostr relay", "PublicKey": "NA", "Contact": "NA", - "SupportedNips": [ 1, 2, 4, 5, 9, 11, 13, 17, 40, 42, 45, 50, 51, 57, 59, 62, 65, 70, 77, 78, 119 ], + "SupportedNips": [ 1, 2, 4, 5, 9, 11, 13, 17, 40, 42, 45, 50, 51, 57, 59, 62, 64, 65, 70, 77, 78, 119 ], "Version": "v2.0.1" }, "Whitelist": { diff --git a/src/Netstr/appsettings.local.json.example b/src/Netstr/appsettings.local.json.example new file mode 100644 index 0000000..c0c7aad --- /dev/null +++ b/src/Netstr/appsettings.local.json.example @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "NetstrDatabase": "Host=db.[YOUR-PROJECT-REF].supabase.co;Port=5432;Database=postgres;Username=postgres;Password=[YOUR-PASSWORD];SSL Mode=Require;Trust Server Certificate=true" + } +} \ No newline at end of file diff --git a/test/Netstr.Tests/AuthTests.cs b/test/Netstr.Tests/AuthTests.cs index 9cef156..ebb4e8d 100644 --- a/test/Netstr.Tests/AuthTests.cs +++ b/test/Netstr.Tests/AuthTests.cs @@ -19,7 +19,8 @@ public AuthTests() this.factory = new WebApplicationFactory(); } - [Fact] + [Fact + ] public async Task PublishAuthModeTest() { using WebSocket ws = await this.factory.ConnectWebSocketAsync(AuthMode.Publishing); diff --git a/test/Netstr.Tests/NIPs/64.feature b/test/Netstr.Tests/NIPs/64.feature new file mode 100644 index 0000000..f173fa4 --- /dev/null +++ b/test/Netstr.Tests/NIPs/64.feature @@ -0,0 +1,72 @@ +Feature: NIP-64 Chess (Portable Game Notation) + Tests for NIP-64 Chess implementation + + Background: + Given a relay at "wss://localhost:5001" + And a user Alice + And Alice is connected to the relay + + Scenario: Publish a simple chess game in progress + When Alice publishes an event with kind 64 and content "1. e4 *" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + And the event content is "1. e4 *" + + Scenario: Publish a chess game with basic moves + When Alice publishes an event with kind 64 and content "1. e4 e5 2. Nf3 Nc6 3. Bb5 *" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + + Scenario: Publish a complete chess game with PGN headers + When Alice publishes an event with kind 64 and content: + """ + [Event "F/S Return Match"] + [Site "Belgrade, Serbia JUG"] + [Date "1992.11.04"] + [Round "29"] + [White "Fischer, Robert J."] + [Black "Spassky, Boris V."] + [Result "1/2-1/2"] + + 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 1/2-1/2 + """ + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + + Scenario: Publish chess game with alt tag for non-supporting clients + When Alice publishes an event with kind 64 and tags: + | alt | Fischer vs. Spassky in Belgrade on 1992-11-04 | + And content "1. e4 e5 2. Nf3 Nc6 3. Bb5 1/2-1/2" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + And the event has tag "alt" with value "Fischer vs. Spassky in Belgrade on 1992-11-04" + + Scenario: Publish unknown result game + When Alice publishes an event with kind 64 and content "*" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + + Scenario: Reject empty chess content + When Alice publishes an event with kind 64 and content "" + Then the relay rejects the event with "invalid: chess content is empty or malformed" + + Scenario: Reject invalid PGN format + When Alice publishes an event with kind 64 and content "invalid chess moves here" + Then the relay rejects the event with "invalid: PGN format is not valid" + + Scenario: Accept castling notation + When Alice publishes an event with kind 64 and content "1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. O-O O-O *" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event + + Scenario: Accept game with result + When Alice publishes an event with kind 64 and content "1. f3 e5 2. g4 Qh4# 0-1" + Then the relay accepts the event + When Alice subscribes to events with kind 64 + Then Alice receives 1 event \ No newline at end of file From a854f5fecf1446674630bde3ab9397d4eccf7951 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:10:16 -0500 Subject: [PATCH 16/49] feat: add deploy workflow and relay runtime updates - add deployment workflow, LAN runtime scripts, and system service assets - update relay validators/settings and test-generated specs - keep appsettings.Development.json untracked --- .env.example | 5 + .github/workflows/deploy.yml | 52 ++ Implementation-Roadmap.md | 467 ++++++++++++++++++ NIP51-Client-Implementation-Guide.md | 316 ++++++++---- docker-compose.yml | 17 + .../Handlers/TestRelayListEventHandler.cs | 4 +- .../Events/Validators/ChessEventValidator.cs | 3 - .../Events/Validators/WhitelistValidator.cs | 2 +- .../SubscribeMessageHandler.cs | 6 + .../WhitelistSubscriptionValidator.cs | 4 +- .../Middleware/CleanupBackgroundService.cs | 2 +- src/Netstr/Services/ConfigurationWriter.cs | 2 +- src/Netstr/appsettings.json | 9 +- src/Netstr/appsettings.local.json.example | 4 +- test/Netstr.Tests/NIPs/64.feature.cs | 454 +++++++++++++++++ test/Netstr.Tests/NIPs/65.feature.cs | 22 +- test/Netstr.Tests/NIPs/70.feature.cs | 52 +- 17 files changed, 1265 insertions(+), 156 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/deploy.yml create mode 100644 Implementation-Roadmap.md create mode 100644 docker-compose.yml create mode 100644 test/Netstr.Tests/NIPs/64.feature.cs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8a4352c --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Environment variables for Docker deployment +# Copy to .env and fill in your actual values + +DATABASE_CONNECTION_STRING=Host=;Port=5432;Database=;Username=;Password=;SSL Mode=Require;Trust Server Certificate=true +CERT_PASSWORD=your_certificate_password diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2b1a809 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Deploy Netstr + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal + env: + ConnectionStrings__NetstrDatabase: ${{ secrets.DATABASE_CONNECTION_STRING }} + + deploy: + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Publish + run: dotnet publish src/Netstr/Netstr.csproj -c Release -o ./publish + + # Add your deployment steps here (Docker, Azure, AWS, etc.) + - name: Deploy to production + run: | + echo "Add your deployment commands here" + echo "Connection string available as: ${{ secrets.DATABASE_CONNECTION_STRING }}" \ No newline at end of file diff --git a/Implementation-Roadmap.md b/Implementation-Roadmap.md new file mode 100644 index 0000000..f702495 --- /dev/null +++ b/Implementation-Roadmap.md @@ -0,0 +1,467 @@ +# Implementation Roadmap: Local Discord-like Messaging System +## Built on Netstr with Context-Engineering Principles + +> _"The secret to getting ahead is getting started." — Mark Twain_ + +## Project Overview + +Transform the Netstr relay into a local LAN-based Discord-like messaging system for seamless communication between all your devices. This roadmap provides a structured 8-week implementation plan using Context-Engineering principles. + +## Executive Summary + +**Goal**: Create a local network messaging system that allows all your devices to communicate in real-time through channels, similar to Discord but completely private and local. + +**Key Features**: +- Real-time messaging between all devices on your LAN +- Channel-based organization (General, Work, Family, etc.) +- File sharing capabilities +- User presence and device detection +- Modern web interface with Vue.js +- Complete privacy - no data leaves your network + +## Phase-by-Phase Implementation + +### Phase 1: Foundation Setup (Week 1-2) +*Establishing the atomic operations and basic infrastructure* + +#### Week 1: Environment Setup +**Days 1-2: Development Environment** +- [ ] Clone and set up Netstr project +- [ ] Set up Docker development environment +- [ ] Configure PostgreSQL database +- [ ] Test basic Netstr functionality + +**Days 3-4: Local Network Configuration** +- [ ] Configure Netstr for local network deployment +- [ ] Set up Docker Compose for local development +- [ ] Configure network settings for LAN access +- [ ] Test WebSocket connections from multiple devices + +**Days 5-7: Basic Frontend Setup** +- [ ] Initialize Vue.js project with TypeScript +- [ ] Set up Tailwind CSS and component library +- [ ] Implement basic WebSocket client +- [ ] Create basic application layout + +**Deliverables**: +- Working Netstr relay on local network +- Basic Vue.js frontend that connects to relay +- Docker Compose setup for easy deployment + +#### Week 2: Core WebSocket Communication +**Days 8-9: Nostr Client Implementation** +- [ ] Implement `useNostrClient` composable +- [ ] Add WebSocket connection management +- [ ] Implement basic event publishing/subscribing +- [ ] Add connection state management + +**Days 10-11: Basic Message Flow** +- [ ] Implement message sending functionality +- [ ] Add message receiving and display +- [ ] Create basic channel subscription system +- [ ] Add error handling and reconnection logic + +**Days 12-14: State Management** +- [ ] Implement Pinia store for chat state +- [ ] Add user management +- [ ] Create channel management +- [ ] Add message persistence + +**Deliverables**: +- Working WebSocket communication +- Basic message sending/receiving +- State management system +- Connection resilience + +### Phase 2: Core Features (Week 3-4) +*Building molecular patterns and structured components* + +#### Week 3: Channel System +**Days 15-16: Channel Creation and Management** +- [ ] Implement channel creation using Nostr events (kind 30001) +- [ ] Add channel membership management +- [ ] Create channel list UI component +- [ ] Add channel join/leave functionality + +**Days 17-18: Message Threading** +- [ ] Implement reply-to functionality +- [ ] Add message threading display +- [ ] Create message context menu +- [ ] Add message reactions system + +**Days 19-21: User Management** +- [ ] Implement user profiles using Nostr metadata events +- [ ] Add user presence indicators +- [ ] Create user status management +- [ ] Add device detection and display + +**Deliverables**: +- Full channel system with creation/management +- Message threading and replies +- User profiles and presence +- Device detection + +#### Week 4: Real-time Features +**Days 22-23: Enhanced UI Components** +- [ ] Create polished chat interface +- [ ] Add typing indicators +- [ ] Implement message status indicators +- [ ] Add notification system + +**Days 24-25: File Sharing Foundation** +- [ ] Implement file upload API endpoint +- [ ] Add file sharing UI components +- [ ] Create file preview system +- [ ] Add file download functionality + +**Days 26-28: Performance Optimization** +- [ ] Implement message pagination +- [ ] Add lazy loading for message history +- [ ] Optimize WebSocket message handling +- [ ] Add caching for frequently accessed data + +**Deliverables**: +- Polished chat interface +- Basic file sharing +- Performance optimizations +- Notification system + +### Phase 3: Advanced Features (Week 5-6) +*Implementing cellular-level state management and organ-level workflows* + +#### Week 5: Advanced Messaging +**Days 29-30: Rich Message Types** +- [ ] Add support for image previews +- [ ] Implement video/audio file handling +- [ ] Create message formatting (markdown support) +- [ ] Add emoji picker and reactions + +**Days 31-32: Search and History** +- [ ] Implement message search functionality +- [ ] Add advanced filtering options +- [ ] Create message history export +- [ ] Add bookmark/favorite messages + +**Days 33-35: Mobile Responsiveness** +- [ ] Optimize UI for mobile devices +- [ ] Add touch-friendly interactions +- [ ] Implement responsive design +- [ ] Add mobile-specific features + +**Deliverables**: +- Rich message types and formatting +- Search and filtering capabilities +- Mobile-responsive design +- Advanced message features + +#### Week 6: System Integration +**Days 36-37: Push Notifications** +- [ ] Implement browser push notifications +- [ ] Add notification preferences +- [ ] Create notification history +- [ ] Add sound notifications + +**Days 38-39: Device Synchronization** +- [ ] Implement cross-device message sync +- [ ] Add device-specific settings +- [ ] Create device management interface +- [ ] Add device authorization system + +**Days 40-42: Security Enhancements** +- [ ] Implement proper key management +- [ ] Add channel permissions system +- [ ] Create admin/moderator roles +- [ ] Add message encryption for private channels + +**Deliverables**: +- Push notification system +- Device synchronization +- Security enhancements +- Permission system + +### Phase 4: Production Polish (Week 7-8) +*Finalizing the neural system with self-optimization and monitoring* + +#### Week 7: Performance and Reliability +**Days 43-44: Performance Monitoring** +- [ ] Implement application performance monitoring +- [ ] Add connection health monitoring +- [ ] Create performance metrics dashboard +- [ ] Add error tracking and reporting + +**Days 45-46: Scalability Improvements** +- [ ] Optimize database queries +- [ ] Implement connection pooling +- [ ] Add message batching +- [ ] Create automatic cleanup routines + +**Days 47-49: Testing and Bug Fixes** +- [ ] Comprehensive testing across devices +- [ ] Fix any remaining bugs +- [ ] Optimize user experience +- [ ] Add automated testing + +**Deliverables**: +- Performance monitoring +- Scalability improvements +- Comprehensive testing +- Bug fixes and optimization + +#### Week 8: Deployment and Documentation +**Days 50-51: Production Deployment** +- [ ] Create production Docker configuration +- [ ] Set up automated backups +- [ ] Configure monitoring and logging +- [ ] Create deployment scripts + +**Days 52-53: Documentation** +- [ ] Write user documentation +- [ ] Create setup and configuration guides +- [ ] Document API and extension points +- [ ] Create troubleshooting guide + +**Days 54-56: Final Polish** +- [ ] Final UI/UX improvements +- [ ] Add any remaining features +- [ ] Performance final optimizations +- [ ] Prepare for launch + +**Deliverables**: +- Production-ready deployment +- Complete documentation +- Final polish and optimization +- Launch preparation + +## Technical Architecture + +### Context-Engineering Implementation + +**Atomic Level** (Basic Operations): +- WebSocket message sending/receiving +- HTTP API endpoints for file sharing +- Database CRUD operations +- User authentication + +**Molecular Level** (Structured Components): +- Message processing pipeline +- Channel management system +- User presence tracking +- File sharing workflow + +**Cellular Level** (Stateful Systems): +- Real-time state synchronization +- Message persistence +- User session management +- Device registry + +**Organ Level** (Complete Workflows): +- End-to-end message delivery +- Channel lifecycle management +- User onboarding flow +- System monitoring + +### Technology Stack + +**Backend (Netstr Foundation)**: +- ASP.NET Core 6.0 +- PostgreSQL database +- WebSocket for real-time communication +- File storage system + +**Frontend (Vue.js Application)**: +- Vue.js 3 with TypeScript +- Pinia for state management +- Tailwind CSS for styling +- WebSocket client for real-time updates + +**DevOps**: +- Docker for containerization +- Docker Compose for orchestration +- Nginx for reverse proxy +- Automated backups + +## Implementation Details + +### Key Components to Build + +1. **WebSocket Client (`useNostrClient`)** + - Connection management + - Message routing + - Event handling + - Reconnection logic + +2. **Chat Store (`chatStore`)** + - Channel management + - Message persistence + - User state + - Real-time updates + +3. **UI Components** + - Chat interface + - Channel list + - User presence + - File sharing + +4. **Backend Extensions** + - File upload API + - Channel management + - User authentication + - Message routing + +### Database Schema Extensions + +```sql +-- Channels table +CREATE TABLE channels ( + id VARCHAR(64) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + type VARCHAR(20) DEFAULT 'public', + created_by VARCHAR(64), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Channel members table +CREATE TABLE channel_members ( + channel_id VARCHAR(64) REFERENCES channels(id), + user_pubkey VARCHAR(64), + joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (channel_id, user_pubkey) +); + +-- Files table +CREATE TABLE files ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255), + content_type VARCHAR(100), + size BIGINT, + uploaded_by VARCHAR(64), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### Configuration Files + +**docker-compose.yml**: +```yaml +version: '3.8' +services: + netstr-relay: + build: . + ports: + - "5000:5000" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__DefaultConnection=Host=postgres;Database=netstr;Username=netstr;Password=netstr123 + depends_on: + - postgres + volumes: + - ./files:/app/files + + postgres: + image: postgres:15 + environment: + - POSTGRES_DB=netstr + - POSTGRES_USER=netstr + - POSTGRES_PASSWORD=netstr123 + volumes: + - postgres_data:/var/lib/postgresql/data + + frontend: + build: ./frontend + ports: + - "3000:3000" + depends_on: + - netstr-relay + +volumes: + postgres_data: +``` + +## Success Metrics + +### Technical Metrics +- **Performance**: <100ms message delivery +- **Reliability**: 99.9% uptime +- **Scalability**: Support for 10+ concurrent devices +- **Security**: End-to-end encryption for private channels + +### User Experience Metrics +- **Ease of Use**: <5 minutes setup time +- **Responsiveness**: Real-time message delivery +- **Compatibility**: Works on all device types +- **Features**: Full Discord-like functionality + +## Risk Mitigation + +### Technical Risks +1. **WebSocket Connection Issues** + - Mitigation: Robust reconnection logic + - Fallback: HTTP polling if WebSocket fails + +2. **Database Performance** + - Mitigation: Proper indexing and query optimization + - Monitoring: Performance metrics and alerts + +3. **Cross-device Compatibility** + - Mitigation: Extensive testing on multiple devices + - Responsive design for various screen sizes + +### Timeline Risks +1. **Feature Scope Creep** + - Mitigation: Strict adherence to planned features + - Defer non-essential features to post-launch + +2. **Technical Complexity** + - Mitigation: Start with simple implementations + - Iterative improvement approach + +## Post-Launch Roadmap + +### Phase 5: Advanced Features (Month 2) +- Voice/video calling +- Screen sharing +- Advanced file management +- Plugin system + +### Phase 6: Ecosystem (Month 3) +- Mobile apps +- Desktop applications +- API for third-party integrations +- Backup and sync solutions + +## Getting Started + +### Prerequisites +- Docker and Docker Compose +- Node.js 18+ +- Git + +### Quick Start +1. Clone the repository +2. Run `docker-compose up` +3. Open `http://localhost:3000` +4. Start chatting between your devices! + +### Development Setup +1. Backend: `cd src/Netstr && dotnet run` +2. Frontend: `cd frontend && npm run dev` +3. Database: `docker run -p 5432:5432 postgres:15` + +## Conclusion + +This roadmap provides a comprehensive plan for building a local Discord-like messaging system using the Netstr foundation. By following Context-Engineering principles and breaking the implementation into manageable phases, you'll have a fully functional local messaging system within 8 weeks. + +The system will provide: +- **Complete Privacy**: All communication stays within your local network +- **Rich Features**: Full Discord-like functionality +- **Easy Setup**: Simple Docker deployment +- **Extensible Architecture**: Easy to add new features +- **Production Ready**: Robust, scalable, and maintainable + +Start with Phase 1 and work through each phase systematically. The modular approach allows for incremental progress and early wins while building toward the complete vision. + +--- + +*This roadmap applies Context-Engineering principles to create a practical, step-by-step implementation plan for building a local messaging system using the Netstr foundation.* \ No newline at end of file diff --git a/NIP51-Client-Implementation-Guide.md b/NIP51-Client-Implementation-Guide.md index 272541a..9767ce7 100644 --- a/NIP51-Client-Implementation-Guide.md +++ b/NIP51-Client-Implementation-Guide.md @@ -9,11 +9,13 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ### List Types **Standard Lists (10000-10999):** + - Single instance per user (replaceable events) - Unique by `pubkey + kind` - Examples: Mute lists, bookmarks, relay lists **Sets (30000-30999):** + - Multiple instances per user with unique 'd' tags (addressable events) - Unique by `pubkey + kind + d_tag_value` - Examples: Follow sets, bookmark sets, curation sets @@ -28,6 +30,7 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ## Supported Event Kinds ### Standard Lists (10000-10999) + - `10000` - Mute List - `10001` - Pinned Notes - `10002` - Relay List @@ -44,6 +47,7 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists - `10102` - Good Wiki Relays ### Sets (30000-30999) + - `30000` - Follow Sets - `30002` - Relay Sets - `30003` - Bookmark Sets @@ -58,6 +62,7 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ## Event Structure Examples ### Standard Mute List (Kind 10000) + ```json { "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", @@ -74,6 +79,7 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ``` ### Bookmark Set (Kind 30003) + ```json { "id": "567b41fc9060c758c4216fe5f8d3df7c57daad7ae757fa4606f0c39d4dd220ef", @@ -85,7 +91,10 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ["name", "Programming Resources"], ["about", "Collection of programming articles and tutorials"], ["e", "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"], - ["a", "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"], + [ + "a", + "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3" + ], ["t", "programming"], ["r", "https://example.com/resource"] ], @@ -95,6 +104,7 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ``` ### Follow Set (Kind 30000) + ```json { "kind": 30000, @@ -119,135 +129,208 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ### Basic List Retrieval **Get User's Mute List:** + ```json -["REQ", "mute_list", { - "authors": ["user_pubkey"], - "kinds": [10000], - "limit": 1 -}] +[ + "REQ", + "mute_list", + { + "authors": ["user_pubkey"], + "kinds": [10000], + "limit": 1 + } +] ``` **Get All User's Bookmark Sets:** + ```json -["REQ", "bookmark_sets", { - "authors": ["user_pubkey"], - "kinds": [30003] -}] +[ + "REQ", + "bookmark_sets", + { + "authors": ["user_pubkey"], + "kinds": [30003] + } +] ``` **Get All User's Lists:** + ```json -["REQ", "all_lists", { - "authors": ["user_pubkey"], - "kinds": [10000, 10001, 10003, 30000, 30002, 30003] -}] +[ + "REQ", + "all_lists", + { + "authors": ["user_pubkey"], + "kinds": [10000, 10001, 10003, 30000, 30002, 30003] + } +] ``` ### Specific Set Queries **Get Specific Bookmark Set by ID:** + ```json -["REQ", "specific_bookmarks", { - "authors": ["user_pubkey"], - "kinds": [30003], - "#d": ["programming-resources"] -}] +[ + "REQ", + "specific_bookmarks", + { + "authors": ["user_pubkey"], + "kinds": [30003], + "#d": ["programming-resources"] + } +] ``` **Get Relay Sets for UI Picker:** + ```json -["REQ", "relay_picker", { - "authors": ["user_pubkey"], - "kinds": [30002] -}] +[ + "REQ", + "relay_picker", + { + "authors": ["user_pubkey"], + "kinds": [30002] + } +] ``` **Get Multiple Specific Sets:** + ```json -["REQ", "multiple_sets", { - "authors": ["user_pubkey"], - "kinds": [30003], - "#d": ["bookmarks-1", "bookmarks-2", "programming"] -}] +[ + "REQ", + "multiple_sets", + { + "authors": ["user_pubkey"], + "kinds": [30003], + "#d": ["bookmarks-1", "bookmarks-2", "programming"] + } +] ``` ### Content-Based Queries **Find Lists Containing Specific User:** + ```json -["REQ", "lists_with_user", { - "kinds": [10000, 30000, 30007], - "#p": ["target_user_pubkey"] -}] +[ + "REQ", + "lists_with_user", + { + "kinds": [10000, 30000, 30007], + "#p": ["target_user_pubkey"] + } +] ``` **Find Bookmark Sets Containing Specific Event:** + ```json -["REQ", "bookmarks_with_event", { - "kinds": [30003], - "#e": ["event_id"] -}] +[ + "REQ", + "bookmarks_with_event", + { + "kinds": [30003], + "#e": ["event_id"] + } +] ``` **Find Sets Containing Addressable Events:** + ```json -["REQ", "sets_with_article", { - "kinds": [30003, 30004], - "#a": ["30023:author:article_id"] -}] +[ + "REQ", + "sets_with_article", + { + "kinds": [30003, 30004], + "#a": ["30023:author:article_id"] + } +] ``` **Find Interest Sets by Topic:** + ```json -["REQ", "bitcoin_interests", { - "kinds": [30015], - "#t": ["bitcoin"] -}] +[ + "REQ", + "bitcoin_interests", + { + "kinds": [30015], + "#t": ["bitcoin"] + } +] ``` **Find Relay Sets with Specific Relay:** + ```json -["REQ", "sets_with_relay", { - "kinds": [30002], - "#relay": ["wss://relay.damus.io"] -}] +[ + "REQ", + "sets_with_relay", + { + "kinds": [30002], + "#relay": ["wss://relay.damus.io"] + } +] ``` ### Multi-User and Discovery Queries **Get All Public Mute Lists (Moderation):** + ```json -["REQ", "public_mutes", { - "kinds": [10000], - "limit": 100 -}] +[ + "REQ", + "public_mutes", + { + "kinds": [10000], + "limit": 100 + } +] ``` **Get Community/Interest Lists:** + ```json -["REQ", "community_lists", { - "kinds": [10004, 10015, 30015], - "limit": 50 -}] +[ + "REQ", + "community_lists", + { + "kinds": [10004, 10015, 30015], + "limit": 50 + } +] ``` **Recent List Updates:** + ```json -["REQ", "recent_lists", { - "authors": ["user_pubkey"], - "kinds": [10000, 10001, 10003, 30000, 30002, 30003], - "since": 1699500000 -}] +[ + "REQ", + "recent_lists", + { + "authors": ["user_pubkey"], + "kinds": [10000, 10001, 10003, 30000, 30002, 30003], + "since": 1699500000 + } +] ``` ### Complex Multi-Filter Queries **Multiple OR Conditions:** + ```json -["REQ", "various_lists", - {"authors": ["user1"], "kinds": [10000]}, - {"authors": ["user2"], "kinds": [30003]}, - {"kinds": [30002], "#d": ["primary-relays"]} +[ + "REQ", + "various_lists", + { "authors": ["user1"], "kinds": [10000] }, + { "authors": ["user2"], "kinds": [30003] }, + { "kinds": [30002], "#d": ["primary-relays"] } ] ``` @@ -255,19 +338,19 @@ This guide provides comprehensive implementation details for NIP-51 (Nostr Lists ```json { - "ids": ["event_id_1", "event_id_2"], // Optional: specific event IDs - "authors": ["pubkey_1", "pubkey_2"], // Optional: author pubkeys - "kinds": [10000, 30003], // Optional: event kinds - "since": 1699500000, // Optional: timestamp filter - "until": 1699600000, // Optional: timestamp filter - "limit": 100, // Optional: result limit - "search": "bitcoin", // Optional: content search - "#d": ["set-id-1", "set-id-2"], // Optional: d tag values - "#p": ["pubkey"], // Optional: p tag values - "#e": ["event_id"], // Optional: e tag values - "#a": ["30023:author:article"], // Optional: a tag values - "#t": ["hashtag"], // Optional: t tag values - "#relay": ["wss://relay.example.com"] // Optional: relay tag values + "ids": ["event_id_1", "event_id_2"], // Optional: specific event IDs + "authors": ["pubkey_1", "pubkey_2"], // Optional: author pubkeys + "kinds": [10000, 30003], // Optional: event kinds + "since": 1699500000, // Optional: timestamp filter + "until": 1699600000, // Optional: timestamp filter + "limit": 100, // Optional: result limit + "search": "bitcoin", // Optional: content search + "#d": ["set-id-1", "set-id-2"], // Optional: d tag values + "#p": ["pubkey"], // Optional: p tag values + "#e": ["event_id"], // Optional: e tag values + "#a": ["30023:author:article"], // Optional: a tag values + "#t": ["hashtag"], // Optional: t tag values + "#relay": ["wss://relay.example.com"] // Optional: relay tag values } ``` @@ -308,9 +391,13 @@ Private items are encrypted using NIP-04 encryption and stored in the `content` // Encryption pseudocode const private_items = [ ["p", "private_pubkey_1"], - ["a", "private_addressable_event"] + ["a", "private_addressable_event"], ]; -const encrypted_content = nip04.encrypt(JSON.stringify(private_items), user_private_key, user_public_key); +const encrypted_content = nip04.encrypt( + JSON.stringify(private_items), + user_private_key, + user_public_key +); event.content = encrypted_content; ``` @@ -331,9 +418,9 @@ class NostrListClient { const subId = `user_lists_${Date.now()}`; const filter = { authors: [userPubkey], - kinds: kinds + kinds: kinds, }; - + this.ws.send(JSON.stringify(["REQ", subId, filter])); return subId; } @@ -344,9 +431,9 @@ class NostrListClient { const filter = { authors: [userPubkey], kinds: [kind], - "#d": [setId] + "#d": [setId], }; - + this.ws.send(JSON.stringify(["REQ", subId, filter])); return subId; } @@ -356,9 +443,9 @@ class NostrListClient { const subId = `find_sets_${Date.now()}`; const filter = { kinds: kinds, - [`#${tagType}`]: [tagValue] + [`#${tagType}`]: [tagValue], }; - + this.ws.send(JSON.stringify(["REQ", subId, filter])); return subId; } @@ -368,9 +455,9 @@ class NostrListClient { const subId = `lists_${kind}_${Date.now()}`; const filter = { kinds: [kind], - limit: limit + limit: limit, }; - + this.ws.send(JSON.stringify(["REQ", subId, filter])); return subId; } @@ -382,9 +469,9 @@ class NostrListClient { tags: items, content: content, created_at: Math.floor(Date.now() / 1000), - pubkey: this.userPubkey + pubkey: this.userPubkey, }; - + const signedEvent = this.signEvent(event); this.ws.send(JSON.stringify(["EVENT", signedEvent])); return signedEvent; @@ -394,23 +481,23 @@ class NostrListClient { publishSet(kind, setId, name, items, description = "") { const tags = [ ["d", setId], - ["name", name] + ["name", name], ]; - + if (description) { tags.push(["about", description]); } - + tags.push(...items); - + const event = { kind: kind, tags: tags, content: "", created_at: Math.floor(Date.now() / 1000), - pubkey: this.userPubkey + pubkey: this.userPubkey, }; - + const signedEvent = this.signEvent(event); this.ws.send(JSON.stringify(["EVENT", signedEvent])); return signedEvent; @@ -443,46 +530,61 @@ const muteSubId = client.subscribeToUserLists(userPubkey, [10000]); const bookmarkSetsSubId = client.subscribeToUserLists(userPubkey, [30003]); // Find sets containing a specific user -const setsWithUserSubId = client.findSetsContaining("p", "target_pubkey", [30000, 30007]); +const setsWithUserSubId = client.findSetsContaining( + "p", + "target_pubkey", + [30000, 30007] +); // Create a new follow set -client.publishSet(30000, "bitcoin-devs", "Bitcoin Developers", [ - ["p", "dev1_pubkey"], - ["p", "dev2_pubkey"], - ["p", "dev3_pubkey"] -], "Core Bitcoin protocol developers"); +client.publishSet( + 30000, + "bitcoin-devs", + "Bitcoin Developers", + [ + ["p", "dev1_pubkey"], + ["p", "dev2_pubkey"], + ["p", "dev3_pubkey"], + ], + "Core Bitcoin protocol developers" +); // Create a mute list client.publishList(10000, [ ["p", "spammer_pubkey"], ["t", "spam"], - ["word", "badword"] + ["word", "badword"], ]); ``` ## Common Use Cases ### 1. User Profile Enhancement + - Display pinned notes on profile - Show user's interests and communities - List preferred relays for communication ### 2. Content Curation + - Create and share article collections - Organize bookmarks by topic - Curate video playlists ### 3. Social Graph Management + - Organize follows into categories - Manage mute lists for content filtering - Create topic-specific follow lists ### 4. Relay Management + - Set up relay groups for different purposes - Share relay recommendations - Manage blocked relays ### 5. Community Building + - Share community lists - Create interest-based groups - Organize member lists @@ -490,24 +592,28 @@ client.publishList(10000, [ ## Best Practices ### 1. Event Publishing + - Always include proper timestamps - Use descriptive names for sets - Include helpful descriptions in 'about' tags - Validate tag formats before publishing ### 2. Query Efficiency + - Use specific filters to reduce bandwidth - Implement proper pagination with 'limit' - Close subscriptions when no longer needed - Use time-based filters for recent updates ### 3. User Experience + - Cache frequently accessed lists locally - Implement real-time updates for list changes - Provide UI for easy list management - Show loading states during queries ### 4. Privacy Considerations + - Encrypt sensitive list items in content field - Consider public vs private list implications - Respect user privacy preferences @@ -516,6 +622,7 @@ client.publishList(10000, [ ## Error Handling ### Common Error Scenarios + - Invalid event signatures - Missing required tags (especially 'd' tags for sets) - Invalid tag formats @@ -523,9 +630,10 @@ client.publishList(10000, [ - Rate limiting by relays ### Implementation Considerations + - Implement retry logic for failed publishes - Validate events before sending - Handle relay disconnections gracefully - Provide user feedback for errors -This guide provides comprehensive implementation details for NIP-51 based on the Netstr relay codebase. Use these patterns and examples to build robust list functionality in your Nostr clients. \ No newline at end of file +This guide provides comprehensive implementation details for NIP-51 based on the Netstr relay codebase. Use these patterns and examples to build robust list functionality in your Nostr clients. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f4ed06c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + netstr: + build: . + ports: + - "5000:8080" + - "5001:8081" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ConnectionStrings__NetstrDatabase=${DATABASE_CONNECTION_STRING} + - ASPNETCORE_URLS=http://+:8080;https://+:8081 + - ASPNETCORE_Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx + volumes: + - ~/.aspnet/https:/https:ro + restart: unless-stopped \ No newline at end of file diff --git a/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs index 667a89e..7735894 100644 --- a/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/TestRelayListEventHandler.cs @@ -22,7 +22,7 @@ public TestRelayListEventHandler( public bool CanHandleEvent(Event e) => e.Kind == (long)EventKind.RelayList; - public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) + public Task HandleEventAsync(IWebSocketAdapter sender, Event e) { this._logger.LogInformation( "Test Relay List Event Received:\nFull Event:\n{@Event}\nTags:\n{@Tags}\nContent:\n{Content}", @@ -47,6 +47,8 @@ public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) this._logger.LogError(error, "Failed to process relay list event {EventId} for user {PubKey}", e.Id, e.PublicKey); sender.SendNotOk(e.Id, "Failed to process relay list event"); } + + return Task.CompletedTask; } } } diff --git a/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs b/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs index f3827ea..e074287 100644 --- a/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs +++ b/src/Netstr/Messaging/Events/Validators/ChessEventValidator.cs @@ -69,7 +69,6 @@ private static bool IsValidPgnFormat(string content) } bool hasValidStructure = false; - bool inMoveSection = false; foreach (var line in lines) { @@ -93,8 +92,6 @@ private static bool IsValidPgnFormat(string content) // Check for move text if (!trimmedLine.StartsWith('[')) { - inMoveSection = true; - // Basic validation for moves or result if (ContainsValidMoveOrResult(trimmedLine)) { diff --git a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs index c98cfcd..12a4498 100644 --- a/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs +++ b/src/Netstr/Messaging/Events/Validators/WhitelistValidator.cs @@ -11,7 +11,7 @@ public class WhitelistValidator : IEventValidator { private readonly ILogger logger; private readonly IOptionsMonitor options; - private HashSet allowedPublicKeys; + private HashSet allowedPublicKeys = null!; public WhitelistValidator( ILogger logger, diff --git a/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs b/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs index f2ac859..b9982d0 100644 --- a/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs +++ b/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs @@ -50,6 +50,12 @@ protected override async Task HandleMessageCoreAsync( var entities = await GetFilteredEvents(context, filters, adapter.Context.PublicKey).ToArrayAsync(); var events = entities.Select(CreateEvent).ToArray(); + this.logger.LogInformation($"Found {entities.Length} stored events for subscription {subscriptionId}"); + if (entities.Length > 0) + { + this.logger.LogInformation($"First event: {entities[0].EventId}, Kind: {entities[0].EventKind}"); + } + // send stored events (also sends EOSE) subscription.SendStoredEvents(events); } diff --git a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs index 68d8493..6c26eb1 100644 --- a/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs +++ b/src/Netstr/Messaging/Subscriptions/Validators/WhitelistSubscriptionValidator.cs @@ -12,7 +12,7 @@ public class WhitelistSubscriptionValidator : ISubscriptionRequestValidator { private readonly ILogger logger; private readonly IOptionsMonitor options; - private HashSet allowedPublicKeys; + private HashSet allowedPublicKeys = null!; public WhitelistSubscriptionValidator( ILogger logger, @@ -58,7 +58,7 @@ public bool IsApplicable(FilterMessageHandlerBase handler) return "auth-required: authentication required for subscription"; } - if (!this.allowedPublicKeys.Contains(context.PublicKey)) + if (!this.allowedPublicKeys.Contains(context.PublicKey!)) { this.logger.LogWarning($"Rejected subscription from non-whitelisted public key: {context.PublicKey}"); return Messages.WhitelistRestricted; diff --git a/src/Netstr/Middleware/CleanupBackgroundService.cs b/src/Netstr/Middleware/CleanupBackgroundService.cs index 2dae4d0..a82d2f3 100644 --- a/src/Netstr/Middleware/CleanupBackgroundService.cs +++ b/src/Netstr/Middleware/CleanupBackgroundService.cs @@ -32,7 +32,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) this.logger.LogInformation("Running cleanup finished"); - await Task.Delay(TimeSpan.FromDays(1), stoppingToken); + await Task.Delay(TimeSpan.FromDays(1), stoppingToken); } } } diff --git a/src/Netstr/Services/ConfigurationWriter.cs b/src/Netstr/Services/ConfigurationWriter.cs index 736d8cc..a740376 100644 --- a/src/Netstr/Services/ConfigurationWriter.cs +++ b/src/Netstr/Services/ConfigurationWriter.cs @@ -95,7 +95,7 @@ private object GetValue(JsonElement element) { return element.ValueKind switch { - JsonValueKind.String => element.GetString(), + JsonValueKind.String => element.GetString() ?? string.Empty, JsonValueKind.Number => element.TryGetInt64(out long l) ? l : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 2c044f5..c4feb5d 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -24,8 +24,8 @@ "WebSocketsPath": "/" }, "Auth": { - "Mode": "Publishing", - "ProtectedKinds": [] + "Mode": "WhenNeeded", + "ProtectedKinds": [ 4, 1059 ] }, "Limits": { "MaxPayloadSize": 524288, @@ -89,10 +89,11 @@ "Version": "v2.0.1" }, "Whitelist": { - "Enabled": false, + "Enabled": true, "AllowedPublicKeys": [], "RestrictPublishing": true, "RestrictSubscribing": false, - "OwnerPublicKey": "" + "OwnerPublicKey": "", + "ExemptKinds": [ 9735 ] } } diff --git a/src/Netstr/appsettings.local.json.example b/src/Netstr/appsettings.local.json.example index c0c7aad..23f608c 100644 --- a/src/Netstr/appsettings.local.json.example +++ b/src/Netstr/appsettings.local.json.example @@ -1,5 +1,5 @@ { "ConnectionStrings": { - "NetstrDatabase": "Host=db.[YOUR-PROJECT-REF].supabase.co;Port=5432;Database=postgres;Username=postgres;Password=[YOUR-PASSWORD];SSL Mode=Require;Trust Server Certificate=true" + "NetstrDatabase": "Host=;Port=5432;Database=;Username=;Password=;SSL Mode=Require;Trust Server Certificate=true" } -} \ No newline at end of file +} diff --git a/test/Netstr.Tests/NIPs/64.feature.cs b/test/Netstr.Tests/NIPs/64.feature.cs new file mode 100644 index 0000000..d170529 --- /dev/null +++ b/test/Netstr.Tests/NIPs/64.feature.cs @@ -0,0 +1,454 @@ +// ------------------------------------------------------------------------------ +// +// 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_64ChessPortableGameNotationFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "64.feature" +#line hidden + + public NIP_64ChessPortableGameNotationFeature(NIP_64ChessPortableGameNotationFeature.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-64 Chess (Portable Game Notation)", " Tests for NIP-64 Chess implementation", 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 4 + #line hidden +#line 5 + testRunner.Given("a relay at \"wss://localhost:5001\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 6 + testRunner.And("a user Alice", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 7 + testRunner.And("Alice is connected to the relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish a simple chess game in progress")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Publish a simple chess game in progress")] + public void PublishASimpleChessGameInProgress() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish a simple chess game in progress", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 9 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 10 + testRunner.When("Alice publishes an event with kind 64 and content \"1. e4 *\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 11 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 12 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 13 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 14 + testRunner.And("the event content is \"1. e4 *\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish a chess game with basic moves")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Publish a chess game with basic moves")] + public void PublishAChessGameWithBasicMoves() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish a chess game with basic moves", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 16 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 17 + testRunner.When("Alice publishes an event with kind 64 and content \"1. e4 e5 2. Nf3 Nc6 3. Bb5 *\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 18 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 19 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 20 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish a complete chess game with PGN headers")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Publish a complete chess game with PGN headers")] + public void PublishACompleteChessGameWithPGNHeaders() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish a complete chess game with PGN headers", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 22 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 23 + testRunner.When("Alice publishes an event with kind 64 and content:", "[Event \"F/S Return Match\"]\r\n[Site \"Belgrade, Serbia JUG\"]\r\n[Date \"1992.11.04\"]\r\n[" + + "Round \"29\"]\r\n[White \"Fischer, Robert J.\"]\r\n[Black \"Spassky, Boris V.\"]\r\n[Result " + + "\"1/2-1/2\"]\r\n\r\n1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. B" + + "b3 d6 1/2-1/2", ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 35 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 36 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 37 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish chess game with alt tag for non-supporting clients")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Publish chess game with alt tag for non-supporting clients")] + public void PublishChessGameWithAltTagForNon_SupportingClients() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish chess game with alt tag for non-supporting clients", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 39 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { + "alt", + "Fischer vs. Spassky in Belgrade on 1992-11-04"}); +#line 40 + testRunner.When("Alice publishes an event with kind 64 and tags:", ((string)(null)), table150, "When "); +#line hidden +#line 42 + testRunner.And("content \"1. e4 e5 2. Nf3 Nc6 3. Bb5 1/2-1/2\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 43 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 44 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 45 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 46 + testRunner.And("the event has tag \"alt\" with value \"Fischer vs. Spassky in Belgrade on 1992-11-04" + + "\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish unknown result game")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Publish unknown result game")] + public void PublishUnknownResultGame() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish unknown result game", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 48 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 49 + testRunner.When("Alice publishes an event with kind 64 and content \"*\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 50 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 51 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 52 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject empty chess content")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Reject empty chess content")] + public void RejectEmptyChessContent() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject empty chess content", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 54 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 55 + testRunner.When("Alice publishes an event with kind 64 and content \"\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 56 + testRunner.Then("the relay rejects the event with \"invalid: chess content is empty or malformed\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject invalid PGN format")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Reject invalid PGN format")] + public void RejectInvalidPGNFormat() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject invalid PGN format", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 58 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 59 + testRunner.When("Alice publishes an event with kind 64 and content \"invalid chess moves here\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 60 + testRunner.Then("the relay rejects the event with \"invalid: PGN format is not valid\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept castling notation")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Accept castling notation")] + public void AcceptCastlingNotation() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept castling notation", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 62 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 63 + testRunner.When("Alice publishes an event with kind 64 and content \"1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5" + + " 4. O-O O-O *\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 64 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 65 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 66 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept game with result")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-64 Chess (Portable Game Notation)")] + [Xunit.TraitAttribute("Description", "Accept game with result")] + public void AcceptGameWithResult() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept game with result", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 68 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 + this.FeatureBackground(); +#line hidden +#line 69 + testRunner.When("Alice publishes an event with kind 64 and content \"1. f3 e5 2. g4 Qh4# 0-1\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 70 + testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden +#line 71 + testRunner.When("Alice subscribes to events with kind 64", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 72 + testRunner.Then("Alice receives 1 event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "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_64ChessPortableGameNotationFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_64ChessPortableGameNotationFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/65.feature.cs b/test/Netstr.Tests/NIPs/65.feature.cs index e3ca5ce..95592d9 100644 --- a/test/Netstr.Tests/NIPs/65.feature.cs +++ b/test/Netstr.Tests/NIPs/65.feature.cs @@ -114,23 +114,23 @@ public void PublishingValidRelayList() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read", "write"}); - table150.AddRow(new string[] { + table151.AddRow(new string[] { "r", "wss://relay2.com", "read", ""}); - table150.AddRow(new string[] { + table151.AddRow(new string[] { "r", "wss://relay3.com", "write", ""}); #line 11 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table150, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table151, "When "); #line hidden #line 15 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -166,16 +166,16 @@ public void UpdatingExistingRelayList() #line 19 testRunner.Given("I have published relay configurations", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read"}); - table151.AddRow(new string[] { + table152.AddRow(new string[] { "r", "wss://relay4.com", "write"}); #line 20 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table151, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table152, "When "); #line hidden #line 23 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -243,13 +243,13 @@ public void PublishingInvalidRelayURL() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { "r", "invalid-url", "read", "write"}); #line 32 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table152, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table153, "When "); #line hidden #line 34 testRunner.Then("I should receive an error message containing \"Invalid relay URL format\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -279,12 +279,12 @@ public void PublishingInvalidPermissionMarker() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table154 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "invalid"}); #line 37 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table153, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table154, "When "); #line hidden #line 39 testRunner.Then("I should receive an error message containing \"Invalid relay permission marker\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); diff --git a/test/Netstr.Tests/NIPs/70.feature.cs b/test/Netstr.Tests/NIPs/70.feature.cs index 2db173b..0a36288 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 table154 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table155 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table154.AddRow(new string[] { + table155.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table154, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table155, "And "); #line hidden } @@ -120,35 +120,35 @@ public void NotAuthenticatedClientTriesToPublishProtectedEvent() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table155 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table155.AddRow(new string[] { + table156.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 13 - testRunner.When("Alice publishes an event", ((string)(null)), table155, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table156, "When "); #line hidden - TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table157 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table156.AddRow(new string[] { + table157.AddRow(new string[] { "AUTH", "*", ""}); - table156.AddRow(new string[] { + table157.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "false"}); #line 16 - testRunner.Then("Alice receives messages", ((string)(null)), table156, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table157, "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 table157 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table158 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table157.AddRow(new string[] { + table158.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 24 - testRunner.When("Alice publishes an event", ((string)(null)), table157, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table158, "When "); #line hidden - TechTalk.SpecFlow.Table table158 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table159 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table158.AddRow(new string[] { + table159.AddRow(new string[] { "AUTH", "*", ""}); - table158.AddRow(new string[] { + table159.AddRow(new string[] { "OK", "*", "true"}); - table158.AddRow(new string[] { + table159.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "true"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table158, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table159, "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 table159 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table160 = new TechTalk.SpecFlow.Table(new string[] { "Id", "PublicKey", "Content", "Kind", "Tags", "CreatedAt"}); - table159.AddRow(new string[] { + table160.AddRow(new string[] { "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "Protected", @@ -255,26 +255,26 @@ public void AuthenticatedClientTriesToPublishSomeoneElsesProtectedEvent() "[[ \"-\" ]]", "1722337837"}); #line 36 - testRunner.When("Alice publishes an event", ((string)(null)), table159, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table160, "When "); #line hidden - TechTalk.SpecFlow.Table table160 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table161 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table160.AddRow(new string[] { + table161.AddRow(new string[] { "AUTH", "*", ""}); - table160.AddRow(new string[] { + table161.AddRow(new string[] { "OK", "*", "true"}); - table160.AddRow(new string[] { + table161.AddRow(new string[] { "OK", "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "false"}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table160, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table161, "Then "); #line hidden } this.ScenarioCleanup(); From 517806a7b5d65be34a8dd9a3f3a8452cc1cd561d Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:10:16 -0500 Subject: [PATCH 17/49] feat: add dummy probe filtering for nostr-tools compatibility --- .../SubscribeMessageHandler.cs | 34 +++++++++++++++++-- src/Netstr/Messaging/Messages.cs | 1 + src/Netstr/Messaging/SenderExtensions.cs | 9 +++++ src/Netstr/appsettings.json | 5 +-- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs b/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs index b9982d0..3905b4b 100644 --- a/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs +++ b/src/Netstr/Messaging/MessageHandlers/SubscribeMessageHandler.cs @@ -6,6 +6,7 @@ using Netstr.Messaging.Subscriptions.Validators; using Netstr.Options; using System.Text.Json; +using System.Text.RegularExpressions; namespace Netstr.Messaging.MessageHandlers { @@ -14,6 +15,7 @@ namespace Netstr.Messaging.MessageHandlers /// public class SubscribeMessageHandler : FilterMessageHandlerBase { + private static readonly Regex DummyIdPattern = new Regex("^a{64}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly IDbContextFactory db; public SubscribeMessageHandler( @@ -30,11 +32,27 @@ public SubscribeMessageHandler( protected override string AcceptedMessageType => MessageType.Req; protected override async Task HandleMessageCoreAsync( - IWebSocketAdapter adapter, - string subscriptionId, + IWebSocketAdapter adapter, + string subscriptionId, IEnumerable filters, IEnumerable remainingParameters) { + // Detect and ignore nostr-tools dummy connectivity probe + // nostr-tools sends REQ with ids: ["aaaa...64 times"] as a connectivity test + if (IsDummyProbe(filters)) + { + this.logger.LogDebug("Ignored dummy subscription {SubscriptionId} from {ClientId} (connectivity probe)", + subscriptionId, adapter.Context.ClientId); + + // Send NOTICE to inform client (optional but helpful) + adapter.SendNotice(Messages.IgnoredDummyProbe); + + // Send EOSE to maintain proper NIP-01 flow + adapter.SendEose(subscriptionId); + + return; // Short-circuit - no DB query or subscription creation + } + var maxSubscriptions = this.limits.Value.Subscriptions.MaxSubscriptions; if (maxSubscriptions > 0 && adapter.Subscriptions.GetAll().Where(x => x.Key != subscriptionId).Count() >= maxSubscriptions) { @@ -70,7 +88,7 @@ private Event CreateEvent(EventEntity e) Kind = e.EventKind, PublicKey = e.EventPublicKey, Signature = e.EventSignature, - Tags = e.Tags.Select(tag => + Tags = e.Tags.Select(tag => { if (tag.Value == null) { @@ -81,5 +99,15 @@ private Event CreateEvent(EventEntity e) }).ToArray() }; } + + private static bool IsDummyProbe(IEnumerable filters) + { + // Check if any filter contains a single id matching the dummy pattern "aaaa...64 times" + return filters.Any(filter => + filter.Ids != null && + filter.Ids.Length > 0 && + filter.Ids.Any(id => !string.IsNullOrEmpty(id) && DummyIdPattern.IsMatch(id)) + ); + } } } diff --git a/src/Netstr/Messaging/Messages.cs b/src/Netstr/Messaging/Messages.cs index a1cc8fe..50f830a 100644 --- a/src/Netstr/Messaging/Messages.cs +++ b/src/Netstr/Messaging/Messages.cs @@ -32,6 +32,7 @@ public static class Messages public const string UnsupportedFilter = "unsupported: filter contains unknown elements"; public const string RateLimited = "rate-limited: slow down there chief"; public const string WhitelistRestricted = "restricted: your public key is not in the whitelist"; + public const string IgnoredDummyProbe = "ignored: dummy subscription filter (connectivity probe)"; public const string CannotParseMessage = "unable to parse the message"; public const string CannotProcessMessageType = "unknown message type"; diff --git a/src/Netstr/Messaging/SenderExtensions.cs b/src/Netstr/Messaging/SenderExtensions.cs index 4d54116..4e11940 100644 --- a/src/Netstr/Messaging/SenderExtensions.cs +++ b/src/Netstr/Messaging/SenderExtensions.cs @@ -70,5 +70,14 @@ public static void SendCount(this IWebSocketAdapter sender, string id, int count } ]); } + + public static void SendEose(this IWebSocketAdapter sender, string subscriptionId) + { + sender.Send( + [ + MessageType.EndOfStoredEvents, + subscriptionId + ]); + } } } diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index c4feb5d..30f70c6 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -35,7 +35,7 @@ "MaxCreatedAtLowerOffset": 31536000, "MaxCreatedAtUpperOffset": 60, "MaxPendingEvents": 1024, - "MaxEventsPerMinute": 300 + "MaxEventsPerMinute": 1000 }, "Subscriptions": { "MaxInitialLimit": 1000, @@ -46,7 +46,7 @@ }, "Negentropy": { "MaxFilters": 20, - "MaxSubscriptionsPerMinute": 5, + "MaxSubscriptionsPerMinute": 100, "MaxSubscriptionIdLength": 128, "MaxInitialLimit": 500000, "MaxSubscriptions": 1, @@ -97,3 +97,4 @@ "ExemptKinds": [ 9735 ] } } + From 52941c26951dabd944c6a5af0a71ce032744adf7 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:44:43 -0500 Subject: [PATCH 18/49] chore: ignore .claude metadata directories --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 42abfb0..9893c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -408,3 +408,7 @@ appsettings.local.json appsettings.*.local.json .env *.env + +.claude/ +src/Netstr/.claude/ +test/Netstr.Tests/.claude/ From cc3cb008419bccdac81f433ead490ff462c95d48 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:03:29 -0500 Subject: [PATCH 19/49] fix: align NIP-42 auth tagging and harden DB cleanup paths - correct auth tag matching and sender extension behavior for NIP-42 flows - add cleanup/data-retention guidance and resiliency-related fixes --- DATABASE_RETENTION.md | 141 ++++++++++ DATA_LOSS_FIXES.md | 250 ++++++++++++++++++ src/Netstr/Messaging/Events/CleanupService.cs | 63 +++-- .../Events/Handlers/DeleteEventHandler.cs | 38 ++- .../Events/Handlers/EventHandlerBase.cs | 16 ++ .../Events/Handlers/RegularEventHandler.cs | 18 +- .../Events/Handlers/VanishEventHandler.cs | 55 ++-- .../MessageHandlers/AuthMessageHandler.cs | 2 +- src/Netstr/Messaging/Messages.cs | 3 + src/Netstr/Messaging/Models/Event.cs | 5 + src/Netstr/Messaging/Models/EventTag.cs | 1 + src/Netstr/Program.cs | 15 +- test/Netstr.Tests/NIPs/01.feature | 13 +- test/Netstr.Tests/NIPs/01.feature.cs | 50 +++- test/Netstr.Tests/NIPs/04.feature.cs | 96 +++---- test/Netstr.Tests/NIPs/09.feature.cs | 210 +++++++-------- test/Netstr.Tests/NIPs/11.feature.cs | 36 +-- test/Netstr.Tests/NIPs/119.feature.cs | 44 +-- test/Netstr.Tests/NIPs/13.feature.cs | 36 +-- test/Netstr.Tests/NIPs/17.feature.cs | 96 +++---- test/Netstr.Tests/NIPs/40.feature.cs | 54 ++-- test/Netstr.Tests/NIPs/42.feature.cs | 76 +++--- test/Netstr.Tests/NIPs/45.feature.cs | 104 ++++---- test/Netstr.Tests/NIPs/51.feature.cs | 52 ++-- test/Netstr.Tests/NIPs/57.feature.cs | 20 +- test/Netstr.Tests/NIPs/62.feature.cs | 162 ++++++------ test/Netstr.Tests/NIPs/64.feature.cs | 4 +- test/Netstr.Tests/NIPs/65.feature.cs | 22 +- test/Netstr.Tests/NIPs/70.feature.cs | 52 ++-- 29 files changed, 1151 insertions(+), 583 deletions(-) create mode 100644 DATABASE_RETENTION.md create mode 100644 DATA_LOSS_FIXES.md diff --git a/DATABASE_RETENTION.md b/DATABASE_RETENTION.md new file mode 100644 index 0000000..0a836b2 --- /dev/null +++ b/DATABASE_RETENTION.md @@ -0,0 +1,141 @@ +# Database Retention and Cleanup Policy + +This document explains the data retention policies configured for the Netstr relay. + +## Automatic Cleanup Service + +The cleanup service runs **daily** (configured in `CleanupBackgroundService.cs`) and removes events based on the following rules: + +### 1. Soft-Deleted Events +**Retention**: 7 days after deletion +**Configuration**: `DeleteDeletedEventsAfterDays: 7` + +When events are deleted via NIP-09 delete events, they are "soft deleted" (marked with `DeletedAt` timestamp). After 7 days, these soft-deleted events are permanently removed from the database. + +**Example**: An event deleted on January 1st will be permanently removed on January 8th. + +### 2. Expired Events +**Retention**: 7 days after expiration +**Configuration**: `DeleteExpiredEventsAfterDays: 7` + +Events with an expiration tag (NIP-40) are automatically removed 7 days after their expiration date. + +**Example**: An event with expiration set to February 1st will be permanently removed on February 8th. + +### 3. Event Kind-Based Cleanup Rules + +#### Kind 17 (Private Direct Messages) +**Retention**: 14 days +**Reason**: Privacy - private messages should not be stored indefinitely + +```json +{ + "Kinds": ["17"], + "DeleteAfterDays": 14 +} +``` + +#### Kind 40000+ (Custom/Experimental Events) +**Retention**: 7 days +**Reason**: These are typically temporary or experimental event types + +```json +{ + "Kinds": ["40000-"], + "DeleteAfterDays": 7 +} +``` + +## Ephemeral Events (Not Stored) + +Events with kinds **20000-29999** are **never stored** to the database per NIP-01 specification. These are broadcast to connected clients but immediately discarded. + +Examples: +- Kind 20000: Typing indicators +- Kind 20001: Presence updates +- Kind 20002: Live activities + +## Adjusting Retention Policies + +To modify retention periods, edit `appsettings.json` or `appsettings.local.json`: + +```json +{ + "Cleanup": { + "DeleteDeletedEventsAfterDays": 30, // Increase to 30 days + "DeleteExpiredEventsAfterDays": 30, // Increase to 30 days + "DeleteEventsRules": [ + { + "Kinds": ["17"], + "DeleteAfterDays": 30 // Keep private messages for 30 days + } + ] + } +} +``` + +### Recommended Settings by Use Case + +**Public Relay (High Traffic)** +- DeleteDeletedEventsAfterDays: 7 +- DeleteExpiredEventsAfterDays: 7 +- Kind 17: 7-14 days + +**Private/Community Relay** +- DeleteDeletedEventsAfterDays: 30-90 +- DeleteExpiredEventsAfterDays: 30-90 +- Kind 17: 30-90 days + +**Archive Relay** +- DeleteDeletedEventsAfterDays: 365+ +- DeleteExpiredEventsAfterDays: 365+ +- Consider removing Kind 17 rule entirely + +## Monitoring Cleanup + +Cleanup metrics are logged at INFO level. Check your logs for: + +``` +[INF] Cleanup: removed 42 soft-deleted events older than 7 days +[INF] Cleanup: removed 15 expired events older than 7 days +[INF] Cleanup: removed 8 events matching kind rule (kinds: 17, 14 days old) +[INF] Cleanup completed in 2.5 seconds: deleted 65 total events +``` + +For slow cleanup operations (>60 seconds), a WARNING is logged: + +``` +[WRN] Cleanup took 125 seconds to delete 50000 events +``` + +## Database Storage Considerations + +### Supabase Free Tier +- 500MB database storage +- Monitor usage at: https://app.supabase.com/project/_/settings/billing + +### Calculating Storage Needs + +Average event size: ~1-2KB (depending on tags and content) + +| Daily Events | Monthly Storage | Recommended Retention | +|--------------|-----------------|----------------------| +| 100 | ~6MB | 90+ days | +| 1,000 | ~60MB | 30-90 days | +| 10,000 | ~600MB | 7-30 days | +| 100,000 | ~6GB | 1-7 days | + +## Best Practices + +1. **Monitor cleanup logs daily** to ensure cleanup is running +2. **Adjust retention based on storage limits** and relay purpose +3. **Consider database backups** before reducing retention periods +4. **Test retention changes** on development environment first +5. **Document custom rules** for your specific relay needs + +## Related NIPs + +- **NIP-09**: Event Deletion +- **NIP-16**: Event Treatment (Ephemeral, Replaceable, etc.) +- **NIP-40**: Event Expiration +- **NIP-62**: Vanish Requests diff --git a/DATA_LOSS_FIXES.md b/DATA_LOSS_FIXES.md new file mode 100644 index 0000000..e3bba69 --- /dev/null +++ b/DATA_LOSS_FIXES.md @@ -0,0 +1,250 @@ +# Data Loss Prevention - Implementation Summary + +This document summarizes the database reliability improvements implemented to prevent data loss in the Netstr relay. + +## Changes Implemented + +### 1. Comprehensive Exception Handling ✅ + +**File**: `src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs` + +Added multi-layered exception handling to catch and log all database errors: + +- **DbUpdateException (Unique violations)**: Already handled - returns duplicate message +- **DbUpdateException (Other DB errors)**: NEW - Logs error details and returns `DatabaseError` message +- **TimeoutException**: NEW - Logs timeout and returns `DatabaseTimeout` message +- **General Exception**: NEW - Logs unexpected errors and returns `InternalServerError` message + +**Impact**: All database errors are now properly logged with event details (ID, Kind, PubKey) and clients receive appropriate error messages instead of silent failures. + +--- + +### 2. Supabase Connection Resilience ✅ + +**File**: `src/Netstr/Program.cs` + +Configured Npgsql with retry logic and optimization for Supabase: + +```csharp +.AddDbContextFactory(x => x.UseNpgsql(connectionString, options => +{ + // Auto-retry on transient failures (network, timeouts, deadlocks) + options.EnableRetryOnFailure( + maxRetryCount: 3, + maxRetryDelay: TimeSpan.FromSeconds(5), + errorCodesToAdd: null); + + // Explicit 30-second timeout + options.CommandTimeout(30); + + // Batch optimization + options.MaxBatchSize(100); +})) +``` + +**Impact**: +- Automatic recovery from temporary network issues +- Up to 3 retries with exponential backoff +- Better performance with batched operations + +--- + +### 3. Database Performance Monitoring ✅ + +Added timing metrics to all database write operations: + +#### RegularEventHandler +- Tracks save time for each event +- Logs WARNING if save takes >1 second +- DEBUG logs show duration for all saves + +#### DeleteEventHandler +- Tracks delete operation time +- Logs WARNING if operation takes >2 seconds +- INFO logs show count and duration + +#### VanishEventHandler +- Tracks vanish operation (can delete many events) +- Logs WARNING if operation takes >5 seconds +- INFO logs show events deleted and duration + +#### CleanupService +- Detailed breakdown of cleanup operations +- Separate counts for: + - Soft-deleted events (>7 days old) + - Expired events (>7 days old) + - Kind-based rules (Kind 17, Kind 40000+) +- Logs WARNING if cleanup takes >60 seconds + +**Impact**: +- Early detection of database performance issues +- Ability to identify slow operations before they cause timeouts +- Historical data for capacity planning + +--- + +### 4. Error Message Constants ✅ + +**File**: `src/Netstr/Messaging/Messages.cs` + +Added new client-facing error messages: + +```csharp +public const string DatabaseError = "error: database operation failed"; +public const string DatabaseTimeout = "error: database timeout"; +public const string InternalServerError = "error: internal server error"; +``` + +**Impact**: Clients receive clear, standardized error messages when database issues occur. + +--- + +### 5. Documentation ✅ + +**File**: `DATABASE_RETENTION.md` + +Comprehensive documentation covering: +- Automatic cleanup service behavior +- Retention policies for all event types +- Ephemeral event handling +- How to adjust retention settings +- Storage capacity planning +- Monitoring and best practices + +--- + +## Testing + +Build completed successfully with only expected warnings: +- ✅ Code compiles without errors +- ✅ All exception handling paths compile +- ✅ Connection configuration is valid +- ⚠️ Could not overwrite running executable (expected - app is running) + +**Note**: The application needs to be restarted to apply the new connection pooling settings. + +--- + +## What Was NOT Changed + +- **Database schema**: No migrations needed +- **Event validation logic**: Unchanged +- **Subscription handling**: Unchanged +- **Nostr protocol compliance**: Unchanged + +--- + +## Potential Data Loss Causes - Status + +| Issue | Status | Solution | +|-------|--------|----------| +| Unhandled database exceptions | ✅ FIXED | Comprehensive exception handling | +| Connection timeouts | ✅ FIXED | Auto-retry with exponential backoff | +| Supabase pooler issues | ✅ MITIGATED | Retry logic + timeout configuration | +| Unknown performance issues | ✅ FIXED | Performance monitoring added | +| Automatic cleanup | ✅ DOCUMENTED | Retention policy documented | +| Ephemeral events "loss" | ℹ️ BY DESIGN | Not a bug - per NIP-01 spec | + +--- + +## Monitoring Your Relay + +After restart, monitor logs for these new messages: + +### Success Indicators +``` +[DBG] Saved event abc123 (Kind: 1) in 45ms +[INF] Deleted 3 events in 125ms +[INF] Cleanup completed in 2.5 seconds: deleted 42 total events +``` + +### Warning Signs +``` +[WRN] Slow database save for event abc123: 1250ms +[WRN] Slow delete operation for event def456: 2500ms, deleted 10 events +[WRN] Cleanup took 125 seconds to delete 50000 events +``` + +### Error Conditions +``` +[ERR] Database update failed for event abc123 (Kind: 1, PubKey: ...) +[ERR] Database timeout while saving event abc123 +[ERR] Unexpected error handling event abc123 (Kind: 1) +``` + +--- + +## Next Steps + +### Immediate Actions +1. **Restart the application** to apply connection pooling changes +2. **Monitor logs** for the next 24 hours for any database errors +3. **Check Supabase dashboard** for connection/query metrics + +### Within 1 Week +1. Review cleanup logs to verify retention policies are working +2. Check database size growth in Supabase dashboard +3. Verify no slow operation warnings + +### Optional Improvements +1. **Add health check endpoint** that tests database connectivity +2. **Implement metrics export** (Prometheus/StatsD) for monitoring tools +3. **Set up alerting** for database errors in production +4. **Consider read replicas** if query load becomes an issue + +--- + +## Database Queries for Verification + +Run these against your Supabase database to verify data integrity: + +```sql +-- Check recent event inserts +SELECT + COUNT(*) as total_events, + MAX("EventCreatedAt") as latest_event, + MIN("EventCreatedAt") as oldest_event +FROM "Events"; + +-- Check for soft-deleted events +SELECT + COUNT(*) as deleted_count, + MAX("DeletedAt") as most_recent_deletion +FROM "Events" +WHERE "DeletedAt" IS NOT NULL; + +-- Check event distribution by kind +SELECT + "EventKind", + COUNT(*) as count +FROM "Events" +GROUP BY "EventKind" +ORDER BY count DESC +LIMIT 20; + +-- Check database size +SELECT + pg_size_pretty(pg_database_size(current_database())) as database_size; +``` + +--- + +## Support + +If you experience data loss after these changes: + +1. **Check logs** for error messages +2. **Run verification queries** above +3. **Review Supabase metrics** at https://app.supabase.com +4. **Check retention policies** in appsettings.json +5. **Open an issue** with log excerpts and error details + +--- + +## Related Files + +- `src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs` - Exception handling +- `src/Netstr/Program.cs` - Connection configuration +- `src/Netstr/Messaging/Events/CleanupService.cs` - Cleanup monitoring +- `src/Netstr/appsettings.json` - Retention configuration +- `DATABASE_RETENTION.md` - Retention policy documentation diff --git a/src/Netstr/Messaging/Events/CleanupService.cs b/src/Netstr/Messaging/Events/CleanupService.cs index c42e57d..5685e01 100644 --- a/src/Netstr/Messaging/Events/CleanupService.cs +++ b/src/Netstr/Messaging/Events/CleanupService.cs @@ -29,6 +29,7 @@ public CleanupService( public async Task RunCleanupAsync() { + var cleanupStart = DateTimeOffset.UtcNow; var options = this.options.Value; var now = DateTimeOffset.UtcNow; var deletedOffset = now.AddDays(-options.DeleteDeletedEventsAfterDays); @@ -36,30 +37,58 @@ public async Task RunCleanupAsync() using var db = this.db.CreateDbContext(); - var tx = await db.Database.BeginTransactionAsync(); - - // old deleted items - await db.Events.Where(x => x.DeletedAt.HasValue && x.DeletedAt < deletedOffset).ExecuteDeleteAsync(); + // Use execution strategy to handle transactions with retry logic + var strategy = db.Database.CreateExecutionStrategy(); + var totalDeleted = await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + var deleted = 0; - // old expires items - await db.Events.Where(x => x.EventExpiration.HasValue && x.EventExpiration < expiredOffset).ExecuteDeleteAsync(); + // old deleted items + var deletedCount = await db.Events.Where(x => x.DeletedAt.HasValue && x.DeletedAt < deletedOffset).ExecuteDeleteAsync(); + deleted += deletedCount; + this.logger.LogInformation("Cleanup: removed {Count} soft-deleted events older than {Days} days", deletedCount, options.DeleteDeletedEventsAfterDays); - // kind ranges rules - foreach (var rule in options.DeleteEventsRules) - { - var offset = now.AddDays(-rule.DeleteAfterDays); + // old expires items + var expiredCount = await db.Events.Where(x => x.EventExpiration.HasValue && x.EventExpiration < expiredOffset).ExecuteDeleteAsync(); + deleted += expiredCount; + this.logger.LogInformation("Cleanup: removed {Count} expired events older than {Days} days", expiredCount, options.DeleteExpiredEventsAfterDays); - foreach (var range in rule.Kinds.Select(KindRange.Parse)) + // kind ranges rules + foreach (var rule in options.DeleteEventsRules) { - await db.Events.Where(x => x.EventKind >= range.MinKind && x.EventKind <= range.MaxKind && x.EventCreatedAt < offset).ExecuteDeleteAsync(); + var offset = now.AddDays(-rule.DeleteAfterDays); + var ruleDeletedCount = 0; + + foreach (var range in rule.Kinds.Select(KindRange.Parse)) + { + var rangeCount = await db.Events.Where(x => x.EventKind >= range.MinKind && x.EventKind <= range.MaxKind && x.EventCreatedAt < offset).ExecuteDeleteAsync(); + ruleDeletedCount += rangeCount; + } + + deleted += ruleDeletedCount; + this.logger.LogInformation("Cleanup: removed {Count} events matching kind rule (kinds: {Kinds}, {Days} days old)", + ruleDeletedCount, string.Join(", ", rule.Kinds), rule.DeleteAfterDays); } - } - var count = await db.SaveChangesAsync(); - - await tx.CommitAsync(); + await db.SaveChangesAsync(); + await tx.CommitAsync(); - this.logger.LogInformation($"Cleanup deleted {count} items"); + return deleted; + }); + + var cleanupTime = DateTimeOffset.UtcNow - cleanupStart; + + if (cleanupTime.TotalSeconds > 60) + { + this.logger.LogWarning("Cleanup took {Duration} seconds to delete {Count} events", + cleanupTime.TotalSeconds, totalDeleted); + } + else + { + this.logger.LogInformation("Cleanup completed in {Duration} seconds: deleted {Count} total events", + cleanupTime.TotalSeconds, totalDeleted); + } } } } diff --git a/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs index b54821b..6796b8b 100644 --- a/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/DeleteEventHandler.cs @@ -34,8 +34,6 @@ public DeleteEventHandler( protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Event e) { using var db = this.db.CreateDbContext(); - using var tx = await db.Database.BeginTransactionAsync(); - var now = DateTimeOffset.UtcNow; // delete events (= mark as deleted) @@ -52,7 +50,7 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve 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 or undeletable event."); @@ -66,15 +64,35 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve .Select(x => x.Id) .ToArray(); - await db.Events - .Where(x => eventsToDelete.Contains(x.Id)) - .ExecuteUpdateAsync(x => x.SetProperty(x => x.DeletedAt, now)); + // Use execution strategy to handle transactions with retry logic + var strategy = db.Database.CreateExecutionStrategy(); + var updateStart = DateTimeOffset.UtcNow; + + await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + + await db.Events + .Where(x => eventsToDelete.Contains(x.Id)) + .ExecuteUpdateAsync(x => x.SetProperty(x => x.DeletedAt, now)); - db.Add(e.ToEntity(now)); + db.Add(e.ToEntity(now)); + + // save + await db.SaveChangesAsync(); + await tx.CommitAsync(); + }); + + var updateTime = DateTimeOffset.UtcNow - updateStart; + + if (updateTime.TotalMilliseconds > 2000) + { + this.logger.LogWarning("Slow delete operation for event {EventId}: {Duration}ms, deleted {Count} events", + e.Id, updateTime.TotalMilliseconds, eventsToDelete.Length); + } - // save - await db.SaveChangesAsync(); - await tx.CommitAsync(); + this.logger.LogInformation("Deleted {Count} events in {Duration}ms", + eventsToDelete.Length, updateTime.TotalMilliseconds); // reply sender.SendOk(e.Id); diff --git a/src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs b/src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs index 10c02bd..0d4238b 100644 --- a/src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs +++ b/src/Netstr/Messaging/Events/Handlers/EventHandlerBase.cs @@ -34,6 +34,22 @@ public async Task HandleEventAsync(IWebSocketAdapter sender, Event e) this.logger.LogInformation($"Event {e.ToStringUnique()} already exists, ignoring"); sender.SendOk(e.Id, Messages.DuplicateEvent); } + catch (DbUpdateException ex) + { + this.logger.LogError(ex, "Database update failed for event {EventId} (Kind: {Kind}, PubKey: {PubKey})", + e.Id, e.Kind, e.PublicKey); + sender.SendNotOk(e.Id, Messages.DatabaseError); + } + catch (TimeoutException ex) + { + this.logger.LogError(ex, "Database timeout while saving event {EventId}", e.Id); + sender.SendNotOk(e.Id, Messages.DatabaseTimeout); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Unexpected error handling event {EventId} (Kind: {Kind})", e.Id, e.Kind); + sender.SendNotOk(e.Id, Messages.InternalServerError); + } } public abstract bool CanHandleEvent(Event e); diff --git a/src/Netstr/Messaging/Events/Handlers/RegularEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/RegularEventHandler.cs index 6f004eb..a820933 100644 --- a/src/Netstr/Messaging/Events/Handlers/RegularEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/RegularEventHandler.cs @@ -37,10 +37,22 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve return; } - db.Add(e.ToEntity(DateTimeOffset.UtcNow)); + var entity = e.ToEntity(DateTimeOffset.UtcNow); + db.Add(entity); - // save - await db.SaveChangesAsync(); + // save with metrics tracking + var saveStart = DateTimeOffset.UtcNow; + var changes = await db.SaveChangesAsync(); + var saveTime = DateTimeOffset.UtcNow - saveStart; + + if (saveTime.TotalMilliseconds > 1000) + { + this.logger.LogWarning("Slow database save for event {EventId}: {Duration}ms", + e.Id, saveTime.TotalMilliseconds); + } + + this.logger.LogDebug("Saved event {EventId} (Kind: {Kind}) in {Duration}ms", + e.Id, e.Kind, saveTime.TotalMilliseconds); // reply sender.SendOk(e.Id); diff --git a/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs index d33225e..4f3bc8c 100644 --- a/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs +++ b/src/Netstr/Messaging/Events/Handlers/VanishEventHandler.cs @@ -46,23 +46,44 @@ protected override async Task HandleEventCoreAsync(IWebSocketAdapter sender, Eve } 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 == (long)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(); + + var vanishStart = DateTimeOffset.UtcNow; + + // Use execution strategy to handle transactions with retry logic + var strategy = db.Database.CreateExecutionStrategy(); + var deletedCount = await strategy.ExecuteAsync(async () => + { + await using var tx = await db.Database.BeginTransactionAsync(); + + // delete all user's events (or tagged GiftWraps) from before the vanish event + var deleted = await db.Events + .Include(x => x.Tags) + .Where(x => + (x.EventPublicKey == e.PublicKey || + (x.EventKind == (long)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(); + + return deleted; + }); + + var vanishTime = DateTimeOffset.UtcNow - vanishStart; + + if (vanishTime.TotalMilliseconds > 5000) + { + this.logger.LogWarning("Slow vanish operation for user {PubKey}: {Duration}ms, deleted {Count} events", + e.PublicKey, vanishTime.TotalMilliseconds, deletedCount); + } + + this.logger.LogInformation("Vanish request processed for user {PubKey}: deleted {Count} events in {Duration}ms", + e.PublicKey, deletedCount, vanishTime.TotalMilliseconds); // set vanished in cache this.userCache.Vanish(e.PublicKey, e.CreatedAt); diff --git a/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs b/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs index 1185623..7c80781 100644 --- a/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs +++ b/src/Netstr/Messaging/MessageHandlers/AuthMessageHandler.cs @@ -64,7 +64,7 @@ private Event ValidateAuthEvent(JsonDocument[] parameters, ClientContext context } var path = ctx.GetNormalizedUrl(); - var relays = e.GetNormalizedRelayValues(); + var relays = e.GetAuthRelayValues(); if (!relays.Any(x => x == path)) { diff --git a/src/Netstr/Messaging/Messages.cs b/src/Netstr/Messaging/Messages.cs index 50f830a..b39af7d 100644 --- a/src/Netstr/Messaging/Messages.cs +++ b/src/Netstr/Messaging/Messages.cs @@ -33,6 +33,9 @@ public static class Messages public const string RateLimited = "rate-limited: slow down there chief"; public const string WhitelistRestricted = "restricted: your public key is not in the whitelist"; public const string IgnoredDummyProbe = "ignored: dummy subscription filter (connectivity probe)"; + public const string DatabaseError = "error: database operation failed"; + public const string DatabaseTimeout = "error: database timeout"; + public const string InternalServerError = "error: internal server error"; public const string CannotParseMessage = "unable to parse the message"; public const string CannotProcessMessageType = "unknown message type"; diff --git a/src/Netstr/Messaging/Models/Event.cs b/src/Netstr/Messaging/Models/Event.cs index 17e03f5..7643f73 100644 --- a/src/Netstr/Messaging/Models/Event.cs +++ b/src/Netstr/Messaging/Models/Event.cs @@ -84,6 +84,11 @@ public IEnumerable GetNormalizedRelayValues() return GetTagValues(EventTag.Relay).Select(x => x.Contains("://") ? x.Split("://")[1].TrimEnd('/') : x); } + public IEnumerable GetAuthRelayValues() + { + return GetTagValues(EventTag.AuthRelay).Select(x => x.Contains("://") ? x.Split("://")[1].TrimEnd('/') : x); + } + public DateTimeOffset? GetExpirationValue() { if (long.TryParse(GetTagValue(EventTag.Expiration), out var exp) && exp > 0) diff --git a/src/Netstr/Messaging/Models/EventTag.cs b/src/Netstr/Messaging/Models/EventTag.cs index 216fe8b..495761b 100644 --- a/src/Netstr/Messaging/Models/EventTag.cs +++ b/src/Netstr/Messaging/Models/EventTag.cs @@ -9,6 +9,7 @@ public static class EventTag public const string Nonce = "nonce"; public const string Challenge = "challenge"; public const string Relay = "r"; + public const string AuthRelay = "relay"; // NIP-42 AUTH events use full "relay" tag public const string Protected = "-"; public const string Expiration = "expiration"; diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index a47bae3..e2354ed 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -28,7 +28,20 @@ .AddHostedService() .AddHostedService() .AddScoped() - .AddDbContextFactory(x => x.UseNpgsql(connectionString)) + .AddDbContextFactory(x => x.UseNpgsql(connectionString, options => + { + // Enable automatic retry on transient failures (network issues, timeouts, deadlocks) + options.EnableRetryOnFailure( + maxRetryCount: 3, + maxRetryDelay: TimeSpan.FromSeconds(5), + errorCodesToAdd: null); + + // Set command timeout to 30 seconds (default is 30, but being explicit) + options.CommandTimeout(30); + + // Enable connection pooling optimization for Supabase + options.MaxBatchSize(100); + })) .AddSingleton(); var app = builder.Build(); diff --git a/test/Netstr.Tests/NIPs/01.feature b/test/Netstr.Tests/NIPs/01.feature index 40b80e9..73c2255 100644 --- a/test/Netstr.Tests/NIPs/01.feature +++ b/test/Netstr.Tests/NIPs/01.feature @@ -178,7 +178,7 @@ Scenario: Relay can handle complex filters | EOSE | abcd | | Scenario: Zero limit returns EOSE and future events - Setting filter's limit to 0 skips + Setting filter's limit to 0 skips When Bob publishes an event | Id | Content | Kind | CreatedAt | | a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346 | Hello 1 | 1 | 1722337838 | @@ -192,3 +192,14 @@ Scenario: Zero limit returns EOSE and future events | Type | Id | EventId | | EOSE | abcd | | | EVENT | abcd | 0f5ba539c8ebb386336bc259ddc5d268a4959b012f56e3a2dcc1f9ea48d3591c | + +Scenario: Dummy connectivity probe is ignored and returns EOSE + nostr-tools sends a dummy REQ with 64 'a' characters as a connectivity probe. + The relay should detect this, log it, send NOTICE+EOSE, and skip DB queries. + When Alice sends a subscription request probe + | Ids | + | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | + Then Alice receives messages + | Type | Id | EventId | + | NOTICE | * | * | + | EOSE | probe | | diff --git a/test/Netstr.Tests/NIPs/01.feature.cs b/test/Netstr.Tests/NIPs/01.feature.cs index 9a840ba..4b33794 100644 --- a/test/Netstr.Tests/NIPs/01.feature.cs +++ b/test/Netstr.Tests/NIPs/01.feature.cs @@ -852,7 +852,7 @@ public void ZeroLimitReturnsEOSEAndFutureEvents() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Zero limit returns EOSE and future events", "\tSetting filter\'s limit to 0 skips ", tagsOfScenario, argumentsOfScenario, featureTags); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Zero limit returns EOSE and future events", "\tSetting filter\'s limit to 0 skips", tagsOfScenario, argumentsOfScenario, featureTags); #line 180 this.ScenarioInitialize(scenarioInfo); #line hidden @@ -920,6 +920,54 @@ public void ZeroLimitReturnsEOSEAndFutureEvents() this.ScenarioCleanup(); } + [Xunit.SkippableFactAttribute(DisplayName="Dummy connectivity probe is ignored and returns EOSE")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-01")] + [Xunit.TraitAttribute("Description", "Dummy connectivity probe is ignored and returns EOSE")] + public void DummyConnectivityProbeIsIgnoredAndReturnsEOSE() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Dummy connectivity probe is ignored and returns EOSE", "\tnostr-tools sends a dummy REQ with 64 \'a\' characters as a connectivity probe.\r\n\t" + + "The relay should detect this, log it, send NOTICE+EOSE, and skip DB queries.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 196 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { + "Ids"}); + table36.AddRow(new string[] { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}); +#line 199 + testRunner.When("Alice sends a subscription request probe", ((string)(null)), table36, "When "); +#line hidden + TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table37.AddRow(new string[] { + "NOTICE", + "*", + "*"}); + table37.AddRow(new string[] { + "EOSE", + "probe", + ""}); +#line 202 + testRunner.Then("Alice receives messages", ((string)(null)), table37, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class FixtureData : System.IDisposable diff --git a/test/Netstr.Tests/NIPs/04.feature.cs b/test/Netstr.Tests/NIPs/04.feature.cs index 4a0a92f..14634aa 100644 --- a/test/Netstr.Tests/NIPs/04.feature.cs +++ b/test/Netstr.Tests/NIPs/04.feature.cs @@ -82,23 +82,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table36 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table36.AddRow(new string[] { + table38.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table36, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table38, "And "); #line hidden - TechTalk.SpecFlow.Table table37 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table37.AddRow(new string[] { + table39.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table37, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table39, "And "); #line hidden } @@ -129,29 +129,29 @@ public void NotAuthenticatedClientTriesToFetchKind4Events() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table38.AddRow(new string[] { + table40.AddRow(new string[] { "", "4,1"}); - table38.AddRow(new string[] { + table40.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", ""}); #line 16 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table38, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table40, "When "); #line hidden - TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table41 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table39.AddRow(new string[] { + table41.AddRow(new string[] { "AUTH", "*"}); - table39.AddRow(new string[] { + table41.AddRow(new string[] { "CLOSED", "abcd"}); #line 20 - testRunner.Then("Alice receives messages", ((string)(null)), table39, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table41, "Then "); #line hidden } this.ScenarioCleanup(); @@ -181,87 +181,87 @@ public void AuthenticatedClientTriesToFetchKind4Events() #line 27 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 table40 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table42 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table40.AddRow(new string[] { + table42.AddRow(new string[] { "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", "Secret", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table40.AddRow(new string[] { + table42.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 28 - testRunner.And("Bob publishes events", ((string)(null)), table40, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table42, "And "); #line hidden - TechTalk.SpecFlow.Table table41 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table41.AddRow(new string[] { + table43.AddRow(new string[] { "4"}); #line 32 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table41, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table43, "When "); #line hidden - TechTalk.SpecFlow.Table table42 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table44 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table42.AddRow(new string[] { + table44.AddRow(new string[] { "3bf5ac066f40e02f2f4b4b8386e11fc7f9a482cc4ba9aee3758efb544471767b", "Secret 2", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table42.AddRow(new string[] { + table44.AddRow(new string[] { "97ded8973cfc285174a5736c44641d6e904d44b2763bef1b14c7f8f6075e581c", "Charlie\'s Secret 2", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 35 - testRunner.And("Bob publishes events", ((string)(null)), table42, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table44, "And "); #line hidden - TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table45 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table43.AddRow(new string[] { + table45.AddRow(new string[] { "AUTH", "*", "", ""}); - table43.AddRow(new string[] { + table45.AddRow(new string[] { "OK", "*", "", "true"}); - table43.AddRow(new string[] { + table45.AddRow(new string[] { "EVENT", "abcd", "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", ""}); - table43.AddRow(new string[] { + table45.AddRow(new string[] { "EOSE", "abcd", "", ""}); - table43.AddRow(new string[] { + table45.AddRow(new string[] { "EVENT", "abcd", "3bf5ac066f40e02f2f4b4b8386e11fc7f9a482cc4ba9aee3758efb544471767b", ""}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table43, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table45, "Then "); #line hidden } this.ScenarioCleanup(); @@ -292,77 +292,77 @@ public void AuthenticatedClientTriesToFetchKind4EventsThroughOtherFilters() #line 49 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 table44 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table46 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table44.AddRow(new string[] { + table46.AddRow(new string[] { "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", "Secret", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table44.AddRow(new string[] { + table46.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 50 - testRunner.And("Bob publishes events", ((string)(null)), table44, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table46, "And "); #line hidden - TechTalk.SpecFlow.Table table45 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table47 = new TechTalk.SpecFlow.Table(new string[] { "Ids", "Authors", "Kinds"}); - table45.AddRow(new string[] { + table47.AddRow(new string[] { "", "", "4"}); - table45.AddRow(new string[] { + table47.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "", ""}); - table45.AddRow(new string[] { + table47.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", ""}); - table45.AddRow(new string[] { + table47.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "4"}); #line 54 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table45, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table47, "When "); #line hidden - TechTalk.SpecFlow.Table table46 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table48 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table46.AddRow(new string[] { + table48.AddRow(new string[] { "AUTH", "*", "", ""}); - table46.AddRow(new string[] { + table48.AddRow(new string[] { "OK", "*", "", "true"}); - table46.AddRow(new string[] { + table48.AddRow(new string[] { "EVENT", "abcd", "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", ""}); - table46.AddRow(new string[] { + table48.AddRow(new string[] { "EOSE", "abcd", "", ""}); #line 60 - testRunner.Then("Alice receives messages", ((string)(null)), table46, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table48, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/09.feature.cs b/test/Netstr.Tests/NIPs/09.feature.cs index 4f59b54..81402c1 100644 --- a/test/Netstr.Tests/NIPs/09.feature.cs +++ b/test/Netstr.Tests/NIPs/09.feature.cs @@ -84,32 +84,32 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table47 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table49 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table47.AddRow(new string[] { + table49.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table47, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table49, "And "); #line hidden - TechTalk.SpecFlow.Table table48 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table50 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table48.AddRow(new string[] { + table50.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 10 - testRunner.And("Bob is connected to relay", ((string)(null)), table48, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table50, "And "); #line hidden - TechTalk.SpecFlow.Table table49 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table51 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table49.AddRow(new string[] { + table51.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 13 - testRunner.And("Charlie is connected to relay", ((string)(null)), table49, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table51, "And "); #line hidden } @@ -140,25 +140,25 @@ public void DeletionRemovesReferencedRegularEventsAndIsItselfBroadcast() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table50 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table52 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table50.AddRow(new string[] { + table52.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table50.AddRow(new string[] { + table52.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table50.AddRow(new string[] { + table52.AddRow(new string[] { "04c4ee3333f6f4c59ee5d476e5c86d77922976ea0134c5e19eae665324f735c7", "", "5", @@ -166,33 +166,33 @@ public void DeletionRemovesReferencedRegularEventsAndIsItselfBroadcast() " \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"]]", "1722337845"}); #line 19 - testRunner.When("Alice publishes events", ((string)(null)), table50, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table52, "When "); #line hidden - TechTalk.SpecFlow.Table table51 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table53 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table51.AddRow(new string[] { + table53.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"}); #line 24 - testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table51, "And "); + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table53, "And "); #line hidden - TechTalk.SpecFlow.Table table52 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table54 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table52.AddRow(new string[] { + table54.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table52.AddRow(new string[] { + table54.AddRow(new string[] { "EVENT", "abcd", "04c4ee3333f6f4c59ee5d476e5c86d77922976ea0134c5e19eae665324f735c7"}); - table52.AddRow(new string[] { + table54.AddRow(new string[] { "EOSE", "abcd", ""}); #line 27 - testRunner.Then("Bob receives messages", ((string)(null)), table52, "Then "); + testRunner.Then("Bob receives messages", ((string)(null)), table54, "Then "); #line hidden } this.ScenarioCleanup(); @@ -222,103 +222,103 @@ but only those which took place before the deletion event. #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table53 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table55 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Kind", "Tags", "CreatedAt"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "af3224801d0ea862ceb45e3d75998373ff8726541f133dd0bc5badc79c832e88", "0", "", "1722337838"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "37b30f773a1a7ba1615f34482194a531eca4b3a353e7c73a8f0e08985f6a09e4", "10000", "", "1722337840"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "a23d28af8e9395478f297bd649d71a80b3d6c6c2af2c1dc1c9036ac4f451263d", "30000", "[[ \"d\", \"a\" ]]", "1722337835"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "8a75f74fe8798771c98c4c17b847f95e7ef28c7822b57e399bca41dc911f8baf", "30000", "[[ \"d\", \"b\" ]]", "1722337840"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "dd593bc09c98e958eab2414912ad097df6efdef8b99768915d2361aac4c4ceac", "5", "[[\"a\", \"0:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:\"]]", "1722337839"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "fa740ac70b991cd3955945d9799d881cd15971f37bf71902f271b00c6aa8f7f7", "5", "[[\"a\", \"10000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:\"]" + "]", "1722337839"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "8f1dbc29af4b5c96c26ee5c8932409017a1af538dbbf5207d1dc6470b488580e", "5", "[[\"a\", \"30000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:a\"" + "]]", "1722337839"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "b74adc27515ad9fa78a86acfbc03375b1ab8fc63822c826cad7564b7d23c8051", "5", "[[\"a\", \"30000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:b\"" + "]]", "1722337839"}); - table53.AddRow(new string[] { + table55.AddRow(new string[] { "4a2a7d1fe9ea53ba1604eab98523f26eaee750a86983aa5fbe86614f9c5a2318", "30000", "[[ \"d\", \"a\" ]]", "1722337836"}); #line 39 - testRunner.When("Bob publishes events", ((string)(null)), table53, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table55, "When "); #line hidden - TechTalk.SpecFlow.Table table54 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table56 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table54.AddRow(new string[] { + table56.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 50 - testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table54, "And "); + testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table56, "And "); #line hidden - TechTalk.SpecFlow.Table table55 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table57 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "37b30f773a1a7ba1615f34482194a531eca4b3a353e7c73a8f0e08985f6a09e4"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "8a75f74fe8798771c98c4c17b847f95e7ef28c7822b57e399bca41dc911f8baf"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "8f1dbc29af4b5c96c26ee5c8932409017a1af538dbbf5207d1dc6470b488580e"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "b74adc27515ad9fa78a86acfbc03375b1ab8fc63822c826cad7564b7d23c8051"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "dd593bc09c98e958eab2414912ad097df6efdef8b99768915d2361aac4c4ceac"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EVENT", "abcd", "fa740ac70b991cd3955945d9799d881cd15971f37bf71902f271b00c6aa8f7f7"}); - table55.AddRow(new string[] { + table57.AddRow(new string[] { "EOSE", "abcd", ""}); #line 53 - testRunner.Then("Alice receives messages", ((string)(null)), table55, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table57, "Then "); #line hidden } this.ScenarioCleanup(); @@ -347,65 +347,65 @@ public void ItsNotAllowedToDeleteSomeoneElsesEvents() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table56 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table58 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table56.AddRow(new string[] { + table58.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table56.AddRow(new string[] { + table58.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table56.AddRow(new string[] { + table58.AddRow(new string[] { "da4e33af3793fd4f9d5487a116ee1a03142599e9b1115af38838e469473a8c6b", "Tags", "30000", "[[\"d\", \"a\"]]", "1722337848"}); #line 67 - testRunner.When("Alice publishes events", ((string)(null)), table56, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table58, "When "); #line hidden - TechTalk.SpecFlow.Table table57 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table59 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table57.AddRow(new string[] { + table59.AddRow(new string[] { "a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346", "Hello 1", "1", "", "1722337838"}); - table57.AddRow(new string[] { + table59.AddRow(new string[] { "3abeb55eb9e6a58acf06269f5e93dabd4c91d1e51d08beeab884917180b9248f", "Tags", "30000", "[[\"d\", \"a\"]]", "1722337848"}); - table57.AddRow(new string[] { + table59.AddRow(new string[] { "06f7797468cf1fde45dc438288d44418f416302e94dba22e31b8ef60b74f44bc", "", "5", "[[\"e\", \"a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346\"],[\"e\", " + "\"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table57.AddRow(new string[] { + table59.AddRow(new string[] { "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1", "", "5", "[[\"e\", \"a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346\"]]", "1722337845"}); - table57.AddRow(new string[] { + table59.AddRow(new string[] { "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb", "", "5", @@ -413,73 +413,73 @@ public void ItsNotAllowedToDeleteSomeoneElsesEvents() "]]", "1722337849"}); #line 72 - testRunner.And("Bob publishes events", ((string)(null)), table57, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table59, "And "); #line hidden - TechTalk.SpecFlow.Table table58 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table60 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table58.AddRow(new string[] { + table60.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 79 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table58, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table60, "And "); #line hidden - TechTalk.SpecFlow.Table table59 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table61 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EVENT", "abcd", "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EVENT", "abcd", "da4e33af3793fd4f9d5487a116ee1a03142599e9b1115af38838e469473a8c6b"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EVENT", "abcd", "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EVENT", "abcd", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5"}); - table59.AddRow(new string[] { + table61.AddRow(new string[] { "EOSE", "abcd", ""}); #line 82 - testRunner.Then("Charlie receives messages", ((string)(null)), table59, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table61, "Then "); #line hidden - TechTalk.SpecFlow.Table table60 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table62 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table60.AddRow(new string[] { + table62.AddRow(new string[] { "OK", "a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346", "true"}); - table60.AddRow(new string[] { + table62.AddRow(new string[] { "OK", "3abeb55eb9e6a58acf06269f5e93dabd4c91d1e51d08beeab884917180b9248f", "true"}); - table60.AddRow(new string[] { + table62.AddRow(new string[] { "OK", "06f7797468cf1fde45dc438288d44418f416302e94dba22e31b8ef60b74f44bc", "false"}); - table60.AddRow(new string[] { + table62.AddRow(new string[] { "OK", "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1", "true"}); - table60.AddRow(new string[] { + table62.AddRow(new string[] { "OK", "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb", "true"}); #line 90 - testRunner.And("Bob receives messages", ((string)(null)), table60, "And "); + testRunner.And("Bob receives messages", ((string)(null)), table62, "And "); #line hidden } this.ScenarioCleanup(); @@ -506,65 +506,65 @@ public void DeletingADeletionHasNoAffect() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table61 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table63 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table61.AddRow(new string[] { + table63.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table61.AddRow(new string[] { + table63.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table61.AddRow(new string[] { + table63.AddRow(new string[] { "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "", "5", "[[\"e\", \"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table61.AddRow(new string[] { + table63.AddRow(new string[] { "254ab6e975fc906256f9f318e50c450cd745745031459bddb027c655124302a7", "", "5", "[[\"e\", \"367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529\"]]", "1722337845"}); #line 100 - testRunner.When("Alice publishes events", ((string)(null)), table61, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table63, "When "); #line hidden - TechTalk.SpecFlow.Table table62 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table64 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table62.AddRow(new string[] { + table64.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 106 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table62, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table64, "And "); #line hidden - TechTalk.SpecFlow.Table table63 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table65 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table63.AddRow(new string[] { + table65.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table63.AddRow(new string[] { + table65.AddRow(new string[] { "EVENT", "abcd", "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529"}); - table63.AddRow(new string[] { + table65.AddRow(new string[] { "EOSE", "abcd", ""}); #line 109 - testRunner.Then("Charlie receives messages", ((string)(null)), table63, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table65, "Then "); #line hidden } this.ScenarioCleanup(); @@ -591,69 +591,69 @@ public void ResubmissionOfDeletedEventIsRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table64 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table66 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table64.AddRow(new string[] { + table66.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table64.AddRow(new string[] { + table66.AddRow(new string[] { "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "", "5", "[[\"e\", \"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table64.AddRow(new string[] { + table66.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 116 - testRunner.When("Alice publishes events", ((string)(null)), table64, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table66, "When "); #line hidden - TechTalk.SpecFlow.Table table65 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table67 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table65.AddRow(new string[] { + table67.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "1"}); #line 121 - testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table65, "And "); + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table67, "And "); #line hidden - TechTalk.SpecFlow.Table table66 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table68 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table66.AddRow(new string[] { + table68.AddRow(new string[] { "EOSE", "abcd"}); #line 124 - testRunner.Then("Bob receives messages", ((string)(null)), table66, "Then "); + testRunner.Then("Bob receives messages", ((string)(null)), table68, "Then "); #line hidden - TechTalk.SpecFlow.Table table67 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table69 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table67.AddRow(new string[] { + table69.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "true"}); - table67.AddRow(new string[] { + table69.AddRow(new string[] { "OK", "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "true"}); - table67.AddRow(new string[] { + table69.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 127 - testRunner.And("Alice receives messages", ((string)(null)), table67, "And "); + testRunner.And("Alice receives messages", ((string)(null)), table69, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/11.feature.cs b/test/Netstr.Tests/NIPs/11.feature.cs index 0694a28..af9a104 100644 --- a/test/Netstr.Tests/NIPs/11.feature.cs +++ b/test/Netstr.Tests/NIPs/11.feature.cs @@ -84,14 +84,14 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table68 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table70 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table68.AddRow(new string[] { + table70.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "nsec12y4pgafw6kpcqjtfyrdyxtcupnddj5kdft768kdl55wzq50ervpqauqnw4"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table68, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table70, "And "); #line hidden } @@ -122,50 +122,50 @@ public void RelaySendsAnInformationDocument() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table69 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table71 = new TechTalk.SpecFlow.Table(new string[] { "Header", "Value"}); - table69.AddRow(new string[] { + table71.AddRow(new string[] { "Accept", "application/nostr+json"}); #line 14 - testRunner.When("Alice sends a GET HTTP request to its websockets endpoint", ((string)(null)), table69, "When "); + testRunner.When("Alice sends a GET HTTP request to its websockets endpoint", ((string)(null)), table71, "When "); #line hidden - TechTalk.SpecFlow.Table table70 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table72 = new TechTalk.SpecFlow.Table(new string[] { "Header", "Value"}); - table70.AddRow(new string[] { + table72.AddRow(new string[] { "Access-Control-Allow-Origin", "*"}); #line 17 - testRunner.Then("Alice receives a response with headers", ((string)(null)), table70, "Then "); + testRunner.Then("Alice receives a response with headers", ((string)(null)), table72, "Then "); #line hidden - TechTalk.SpecFlow.Table table71 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table73 = new TechTalk.SpecFlow.Table(new string[] { "Field", "Type"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "name", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "description", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "contact", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "pubkey", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "software", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "version", "string"}); - table71.AddRow(new string[] { + table73.AddRow(new string[] { "supported_nips", "int[]"}); #line 20 - testRunner.And("Alice receives a response with json content", ((string)(null)), table71, "And "); + testRunner.And("Alice receives a response with json content", ((string)(null)), table73, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/119.feature.cs b/test/Netstr.Tests/NIPs/119.feature.cs index a9d052d..a595417 100644 --- a/test/Netstr.Tests/NIPs/119.feature.cs +++ b/test/Netstr.Tests/NIPs/119.feature.cs @@ -83,23 +83,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table72 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table74 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table72.AddRow(new string[] { + table74.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table72, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table74, "And "); #line hidden - TechTalk.SpecFlow.Table table73 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table75 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table73.AddRow(new string[] { + table75.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table73, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table75, "And "); #line hidden } @@ -130,77 +130,77 @@ public void TagFilterWithIsTreatedAsAND() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table74 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table76 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table74.AddRow(new string[] { + table76.AddRow(new string[] { "828a22e778269e7ba35ae7fa8b23d9506561700f176677f7a8dc7858282f4be3", "Cute cat", "1", "[[\"t\", \"meme\"], [\"t\", \"cat\"], [\"t\", \"black\"]]", "1722337838"}); - table74.AddRow(new string[] { + table76.AddRow(new string[] { "d711c1bdaf9fc9aa9a1b91580d98991531e95d22870817ba122d248b4151fde8", "Cute dog", "1", "[[\"t\", \"meme\"], [\"t\", \"dog\"], [\"t\", \"black\"]]", "1722337838"}); #line 15 - testRunner.When("Bob publishes events", ((string)(null)), table74, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table76, "When "); #line hidden - TechTalk.SpecFlow.Table table75 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table77 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "&t", "#t"}); - table75.AddRow(new string[] { + table77.AddRow(new string[] { "1", "meme,cat", "black,white"}); #line 19 - testRunner.And("Alice sends a subscription request moarcats", ((string)(null)), table75, "And "); + testRunner.And("Alice sends a subscription request moarcats", ((string)(null)), table77, "And "); #line hidden - TechTalk.SpecFlow.Table table76 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table78 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table76.AddRow(new string[] { + table78.AddRow(new string[] { "dad216b3cebb2754fcef13dfd6299879cd2b4cb7988e38e36bc01874c90fab47", "Cute cat", "1", "[[\"t\", \"meme\"], [\"t\", \"cat\"], [\"t\", \"white\"]]", "1722337840"}); - table76.AddRow(new string[] { + table78.AddRow(new string[] { "a88cc99d717189d32aa5361386a0654a7b5a0c99f52e1377821bcf5302f64c76", "Cute dog", "1", "[[\"t\", \"meme\"], [\"t\", \"dog\"], [\"t\", \"white\"]]", "1722337840"}); #line 22 - testRunner.And("Bob publishes an event", ((string)(null)), table76, "And "); + testRunner.And("Bob publishes an event", ((string)(null)), table78, "And "); #line hidden - TechTalk.SpecFlow.Table table77 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table79 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table77.AddRow(new string[] { + table79.AddRow(new string[] { "EVENT", "moarcats", "828a22e778269e7ba35ae7fa8b23d9506561700f176677f7a8dc7858282f4be3"}); - table77.AddRow(new string[] { + table79.AddRow(new string[] { "EOSE", "moarcats", ""}); - table77.AddRow(new string[] { + table79.AddRow(new string[] { "EVENT", "moarcats", "dad216b3cebb2754fcef13dfd6299879cd2b4cb7988e38e36bc01874c90fab47"}); #line 26 - testRunner.Then("Alice receives messages", ((string)(null)), table77, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table79, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/13.feature.cs b/test/Netstr.Tests/NIPs/13.feature.cs index 0043d3c..55efb23 100644 --- a/test/Netstr.Tests/NIPs/13.feature.cs +++ b/test/Netstr.Tests/NIPs/13.feature.cs @@ -80,23 +80,23 @@ public virtual void FeatureBackground() { #line 5 #line hidden - TechTalk.SpecFlow.Table table78 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table80 = new TechTalk.SpecFlow.Table(new string[] { "Key", "Value"}); - table78.AddRow(new string[] { + table80.AddRow(new string[] { "MinPowDifficulty", "20"}); #line 6 - testRunner.Given("a relay is running with options", ((string)(null)), table78, "Given "); + testRunner.Given("a relay is running with options", ((string)(null)), table80, "Given "); #line hidden - TechTalk.SpecFlow.Table table79 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table81 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table79.AddRow(new string[] { + table81.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 9 - testRunner.And("Alice is connected to relay", ((string)(null)), table79, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table81, "And "); #line hidden } @@ -130,61 +130,61 @@ public void MessagesWithLowDifficultyAndThoseOffTargetAreRejectedThoseWithHighAn #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table80 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table82 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table80.AddRow(new string[] { + table82.AddRow(new string[] { "00387d3bb57ceab60effbefffcaecff27614c60c75d7b36b01caa71249e3ca3c", "Hello", "[[\"nonce\", \"cc2e9737-e4f5-48d2-8c55-1461aeca3c87\"]]", "1", "1722337838"}); - table80.AddRow(new string[] { + table82.AddRow(new string[] { "0000017cb9da5d1295c5d9e902055c25280ae95ea6767ad89a02f928742b703d", "Hello", "[[\"nonce\", \"84fe8193-f35e-4d9e-9871-b509caaa6412\", \"5\"]]", "1", "1722337838"}); - table80.AddRow(new string[] { + table82.AddRow(new string[] { "00000ed0cf8d67d9cb4f5b211ad9c8daea5b7bbf7721e345070d98a91cc289ff", "Hello", "[[\"nonce\", \"49c7c782-8f45-4dbb-adac-5ebc71c3363c\"]]", "1", "1722337838"}); - table80.AddRow(new string[] { + table82.AddRow(new string[] { "000005e3b3172e58be368ed6b51b7ecf96a3d32b1107496bf6d786f8084aa17f", "Hello", "[[\"nonce\", \"045b7487-e889-4179-9d52-ce46beffef66\", \"21\"]]", "1", "1722337838"}); #line 18 - testRunner.When("Alice publishes events", ((string)(null)), table80, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table82, "When "); #line hidden - TechTalk.SpecFlow.Table table81 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table83 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table81.AddRow(new string[] { + table83.AddRow(new string[] { "OK", "00387d3bb57ceab60effbefffcaecff27614c60c75d7b36b01caa71249e3ca3c", "false"}); - table81.AddRow(new string[] { + table83.AddRow(new string[] { "OK", "0000017cb9da5d1295c5d9e902055c25280ae95ea6767ad89a02f928742b703d", "false"}); - table81.AddRow(new string[] { + table83.AddRow(new string[] { "OK", "00000ed0cf8d67d9cb4f5b211ad9c8daea5b7bbf7721e345070d98a91cc289ff", "true"}); - table81.AddRow(new string[] { + table83.AddRow(new string[] { "OK", "000005e3b3172e58be368ed6b51b7ecf96a3d32b1107496bf6d786f8084aa17f", "true"}); #line 24 - testRunner.Then("Alice receives messages", ((string)(null)), table81, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table83, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/17.feature.cs b/test/Netstr.Tests/NIPs/17.feature.cs index 000208a..ee3b803 100644 --- a/test/Netstr.Tests/NIPs/17.feature.cs +++ b/test/Netstr.Tests/NIPs/17.feature.cs @@ -83,23 +83,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table82 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table84 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table82.AddRow(new string[] { + table84.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table82, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table84, "And "); #line hidden - TechTalk.SpecFlow.Table table83 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table85 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table83.AddRow(new string[] { + table85.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table83, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table85, "And "); #line hidden } @@ -129,29 +129,29 @@ public void NotAuthenticatedClientTriesToFetchKind1059Events() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table84 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table86 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table84.AddRow(new string[] { + table86.AddRow(new string[] { "", "1,1059"}); - table84.AddRow(new string[] { + table86.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", ""}); #line 15 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table84, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table86, "When "); #line hidden - TechTalk.SpecFlow.Table table85 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table87 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table85.AddRow(new string[] { + table87.AddRow(new string[] { "AUTH", "*"}); - table85.AddRow(new string[] { + table87.AddRow(new string[] { "CLOSED", "abcd"}); #line 19 - testRunner.Then("Alice receives messages", ((string)(null)), table85, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table87, "Then "); #line hidden } this.ScenarioCleanup(); @@ -182,87 +182,87 @@ public void AuthenticatedClientTriesToFetchKind1059Events() #line 26 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 table86 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table88 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table86.AddRow(new string[] { + table88.AddRow(new string[] { "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", "Secret", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table86.AddRow(new string[] { + table88.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "Charlie\'s Secret", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 27 - testRunner.And("Bob publishes events", ((string)(null)), table86, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table88, "And "); #line hidden - TechTalk.SpecFlow.Table table87 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table89 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table87.AddRow(new string[] { + table89.AddRow(new string[] { "1059"}); #line 31 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table87, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table89, "When "); #line hidden - TechTalk.SpecFlow.Table table88 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table90 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table88.AddRow(new string[] { + table90.AddRow(new string[] { "03403b4d4c4fad3ff1f561f030dff80daa256c66a4a195e3eb58bce90b2457bd", "Secret 2", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table88.AddRow(new string[] { + table90.AddRow(new string[] { "0e9391da7663a19e77d11966f57396a89a3a7bef1be1d045475e75be8eca246e", "Charlie\'s Secret 2", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 34 - testRunner.And("Bob publishes events", ((string)(null)), table88, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table90, "And "); #line hidden - TechTalk.SpecFlow.Table table89 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table91 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table89.AddRow(new string[] { + table91.AddRow(new string[] { "AUTH", "*", "", ""}); - table89.AddRow(new string[] { + table91.AddRow(new string[] { "OK", "*", "", "true"}); - table89.AddRow(new string[] { + table91.AddRow(new string[] { "EVENT", "abcd", "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", ""}); - table89.AddRow(new string[] { + table91.AddRow(new string[] { "EOSE", "abcd", "", ""}); - table89.AddRow(new string[] { + table91.AddRow(new string[] { "EVENT", "abcd", "03403b4d4c4fad3ff1f561f030dff80daa256c66a4a195e3eb58bce90b2457bd", ""}); #line 38 - testRunner.Then("Alice receives messages", ((string)(null)), table89, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table91, "Then "); #line hidden } this.ScenarioCleanup(); @@ -293,77 +293,77 @@ public void AuthenticatedClientTriesToFetchKind1059EventsThroughOtherFilters() #line 48 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 table90 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table92 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table90.AddRow(new string[] { + table92.AddRow(new string[] { "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", "Secret", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table90.AddRow(new string[] { + table92.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "Charlie\'s Secret", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 49 - testRunner.And("Bob publishes events", ((string)(null)), table90, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table92, "And "); #line hidden - TechTalk.SpecFlow.Table table91 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table93 = new TechTalk.SpecFlow.Table(new string[] { "Ids", "Authors", "Kinds"}); - table91.AddRow(new string[] { + table93.AddRow(new string[] { "", "", "1059"}); - table91.AddRow(new string[] { + table93.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "", ""}); - table91.AddRow(new string[] { + table93.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f611059", ""}); - table91.AddRow(new string[] { + table93.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f611059", "1059"}); #line 53 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table91, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table93, "When "); #line hidden - TechTalk.SpecFlow.Table table92 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table94 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table92.AddRow(new string[] { + table94.AddRow(new string[] { "AUTH", "*", "", ""}); - table92.AddRow(new string[] { + table94.AddRow(new string[] { "OK", "*", "", "true"}); - table92.AddRow(new string[] { + table94.AddRow(new string[] { "EVENT", "abcd", "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", ""}); - table92.AddRow(new string[] { + table94.AddRow(new string[] { "EOSE", "abcd", "", ""}); #line 59 - testRunner.Then("Alice receives messages", ((string)(null)), table92, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table94, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/40.feature.cs b/test/Netstr.Tests/NIPs/40.feature.cs index 155056e..bdf5ba6 100644 --- a/test/Netstr.Tests/NIPs/40.feature.cs +++ b/test/Netstr.Tests/NIPs/40.feature.cs @@ -84,23 +84,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table93 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table95 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table93.AddRow(new string[] { + table95.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table93, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table95, "And "); #line hidden - TechTalk.SpecFlow.Table table94 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table96 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table94.AddRow(new string[] { + table96.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table94, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table96, "And "); #line hidden } @@ -131,31 +131,31 @@ public void UnparsableExpirationTagIsIgnored() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table95 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table97 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table95.AddRow(new string[] { + table97.AddRow(new string[] { "0921e0c46e637526c0cb2211cbab49a56a69373b0f86c2500ed530f1533df182", "Test", "1", "[[\"expiration\",\"blah\"]]", "1722337838"}); #line 15 - testRunner.When("Alice publishes events", ((string)(null)), table95, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table97, "When "); #line hidden - TechTalk.SpecFlow.Table table96 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table98 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table96.AddRow(new string[] { + table98.AddRow(new string[] { "OK", "0921e0c46e637526c0cb2211cbab49a56a69373b0f86c2500ed530f1533df182", "true"}); #line 18 - testRunner.Then("Alice receives messages", ((string)(null)), table96, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table98, "Then "); #line hidden } this.ScenarioCleanup(); @@ -183,31 +183,31 @@ public void AlreadyExpiredEventIsRejected() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table97 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table99 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table97.AddRow(new string[] { + table99.AddRow(new string[] { "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "Test", "1", "[[\"expiration\",\"1231002905\"]]", "1722337838"}); #line 24 - testRunner.When("Alice publishes events", ((string)(null)), table97, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table99, "When "); #line hidden - TechTalk.SpecFlow.Table table98 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table100 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table98.AddRow(new string[] { + table100.AddRow(new string[] { "OK", "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "false"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table98, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table100, "Then "); #line hidden } this.ScenarioCleanup(); @@ -236,36 +236,36 @@ public void ExpiredEventAlreadySavedInARelayIsOmittedFromSubResponse() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table99 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table101 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table99.AddRow(new string[] { + table101.AddRow(new string[] { "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "Test", "1", "[[\"expiration\",\"1231002905\"]]", "1722337838"}); #line 34 - testRunner.Given("Bob previously published events", ((string)(null)), table99, "Given "); + testRunner.Given("Bob previously published events", ((string)(null)), table101, "Given "); #line hidden - TechTalk.SpecFlow.Table table100 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table102 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table100.AddRow(new string[] { + table102.AddRow(new string[] { "1"}); #line 37 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table100, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table102, "When "); #line hidden - TechTalk.SpecFlow.Table table101 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table103 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table101.AddRow(new string[] { + table103.AddRow(new string[] { "EOSE", "abcd"}); #line 40 - testRunner.Then("Alice receives messages", ((string)(null)), table101, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table103, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/42.feature.cs b/test/Netstr.Tests/NIPs/42.feature.cs index e32b221..9007d9a 100644 --- a/test/Netstr.Tests/NIPs/42.feature.cs +++ b/test/Netstr.Tests/NIPs/42.feature.cs @@ -83,14 +83,14 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH required", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table102 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table104 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table102.AddRow(new string[] { + table104.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table102, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table104, "And "); #line hidden } @@ -120,46 +120,46 @@ public void NotAuthenticatedClientCannotPublishOrSubscribe() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table103 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table105 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table103.AddRow(new string[] { + table105.AddRow(new string[] { "1"}); #line 11 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table103, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table105, "When "); #line hidden - TechTalk.SpecFlow.Table table104 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table106 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table104.AddRow(new string[] { + table106.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 14 - testRunner.And("Alice publishes events", ((string)(null)), table104, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table106, "And "); #line hidden - TechTalk.SpecFlow.Table table105 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table107 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table105.AddRow(new string[] { + table107.AddRow(new string[] { "AUTH", "*", ""}); - table105.AddRow(new string[] { + table107.AddRow(new string[] { "CLOSED", "abcd", ""}); - table105.AddRow(new string[] { + table107.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 17 - testRunner.Then("Alice receives messages", ((string)(null)), table105, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table107, "Then "); #line hidden } this.ScenarioCleanup(); @@ -189,50 +189,50 @@ public void AuthenticatedClientCanPublishAndSubscribe() #line 24 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 table106 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table108 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table106.AddRow(new string[] { + table108.AddRow(new string[] { "2"}); #line 25 - testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table106, "And "); + testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table108, "And "); #line hidden - TechTalk.SpecFlow.Table table107 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table109 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table107.AddRow(new string[] { + table109.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 28 - testRunner.And("Alice publishes events", ((string)(null)), table107, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table109, "And "); #line hidden - TechTalk.SpecFlow.Table table108 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table110 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table108.AddRow(new string[] { + table110.AddRow(new string[] { "AUTH", "*", ""}); - table108.AddRow(new string[] { + table110.AddRow(new string[] { "OK", "*", "true"}); - table108.AddRow(new string[] { + table110.AddRow(new string[] { "EOSE", "abcd", ""}); - table108.AddRow(new string[] { + table110.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "true"}); #line 31 - testRunner.Then("Alice receives messages", ((string)(null)), table108, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table110, "Then "); #line hidden } this.ScenarioCleanup(); @@ -262,50 +262,50 @@ public void ClientStaysUnauthenticatedWhenInvalidChallengeIsUsed() #line 39 testRunner.When("Alice publishes an AUTH event with invalid challenge", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden - TechTalk.SpecFlow.Table table109 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table111 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table109.AddRow(new string[] { + table111.AddRow(new string[] { "1"}); #line 40 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table109, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table111, "When "); #line hidden - TechTalk.SpecFlow.Table table110 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table112 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table110.AddRow(new string[] { + table112.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 43 - testRunner.And("Alice publishes events", ((string)(null)), table110, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table112, "And "); #line hidden - TechTalk.SpecFlow.Table table111 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table113 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table111.AddRow(new string[] { + table113.AddRow(new string[] { "AUTH", "*", ""}); - table111.AddRow(new string[] { + table113.AddRow(new string[] { "OK", "*", "false"}); - table111.AddRow(new string[] { + table113.AddRow(new string[] { "CLOSED", "abcd", ""}); - table111.AddRow(new string[] { + table113.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 46 - testRunner.Then("Alice receives messages", ((string)(null)), table111, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table113, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/45.feature.cs b/test/Netstr.Tests/NIPs/45.feature.cs index eb4a0d2..cf15371 100644 --- a/test/Netstr.Tests/NIPs/45.feature.cs +++ b/test/Netstr.Tests/NIPs/45.feature.cs @@ -83,32 +83,32 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table112 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table114 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table112.AddRow(new string[] { + table114.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table112, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table114, "And "); #line hidden - TechTalk.SpecFlow.Table table113 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table115 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table113.AddRow(new string[] { + table115.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table113, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table115, "And "); #line hidden - TechTalk.SpecFlow.Table table114 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table116 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table114.AddRow(new string[] { + table116.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 12 - testRunner.And("Charlie is connected to relay", ((string)(null)), table114, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table116, "And "); #line hidden } @@ -138,59 +138,59 @@ public void CountingFollowers() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table115 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table117 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table115.AddRow(new string[] { + table117.AddRow(new string[] { "d589498c49776340a9bf83f63cc4cf960a17360cc3d9fd2a2ec2de4f11ba82b4", "", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "3", "1722337838"}); #line 18 - testRunner.When("Bob publishes an event", ((string)(null)), table115, "When "); + testRunner.When("Bob publishes an event", ((string)(null)), table117, "When "); #line hidden - TechTalk.SpecFlow.Table table116 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table118 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table116.AddRow(new string[] { + table118.AddRow(new string[] { "2ef0ecd7341f5fdb5634210a4505d1c4ba25cb6ff4721282fd45412f93842c66", "", "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"]]", "3", "1722337838"}); #line 21 - testRunner.And("Charlie publishes an event", ((string)(null)), table116, "And "); + testRunner.And("Charlie publishes an event", ((string)(null)), table118, "And "); #line hidden - TechTalk.SpecFlow.Table table117 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table119 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table117.AddRow(new string[] { + table119.AddRow(new string[] { "3", "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"}); #line 24 - testRunner.And("Alice sends a count message abcd", ((string)(null)), table117, "And "); + testRunner.And("Alice sends a count message abcd", ((string)(null)), table119, "And "); #line hidden - TechTalk.SpecFlow.Table table118 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table120 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Count"}); - table118.AddRow(new string[] { + table120.AddRow(new string[] { "AUTH", "*", ""}); - table118.AddRow(new string[] { + table120.AddRow(new string[] { "COUNT", "abcd", "1"}); #line 27 - testRunner.Then("Alice receives a message", ((string)(null)), table118, "Then "); + testRunner.Then("Alice receives a message", ((string)(null)), table120, "Then "); #line hidden } this.ScenarioCleanup(); @@ -217,27 +217,27 @@ public void CountingDMsIsRejectedWhenNotAuthenticated() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table119 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table119.AddRow(new string[] { + table121.AddRow(new string[] { "4"}); #line 33 - testRunner.When("Alice sends a count message abcd", ((string)(null)), table119, "When "); + testRunner.When("Alice sends a count message abcd", ((string)(null)), table121, "When "); #line hidden - TechTalk.SpecFlow.Table table120 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Count"}); - table120.AddRow(new string[] { + table122.AddRow(new string[] { "AUTH", "*", ""}); - table120.AddRow(new string[] { + table122.AddRow(new string[] { "CLOSED", "abcd", ""}); #line 36 - testRunner.Then("Alice receives a message", ((string)(null)), table120, "Then "); + testRunner.Then("Alice receives a message", ((string)(null)), table122, "Then "); #line hidden } this.ScenarioCleanup(); @@ -272,104 +272,104 @@ public void CountingSomeoneElsesDMsReturnsOnlyThoseFromMe() #line 47 testRunner.And("Charlie publishes an AUTH event for the challenge sent by relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden - TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table121.AddRow(new string[] { + table123.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 48 - testRunner.And("Bob publishes an event", ((string)(null)), table121, "And "); + testRunner.And("Bob publishes an event", ((string)(null)), table123, "And "); #line hidden - TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table122.AddRow(new string[] { + table124.AddRow(new string[] { "7b0535b94878efb18b7c7a13630db8227e30961aed6f5556823b612423d676af", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 51 - testRunner.And("Alice publishes an event", ((string)(null)), table122, "And "); + testRunner.And("Alice publishes an event", ((string)(null)), table124, "And "); #line hidden - TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table123.AddRow(new string[] { + table125.AddRow(new string[] { "4", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"}); #line 54 - testRunner.And("Alice sends a count message abcd", ((string)(null)), table123, "And "); + testRunner.And("Alice sends a count message abcd", ((string)(null)), table125, "And "); #line hidden - TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table124.AddRow(new string[] { + table126.AddRow(new string[] { "4", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"}); #line 57 - testRunner.And("Charlie sends a count message abcd", ((string)(null)), table124, "And "); + testRunner.And("Charlie sends a count message abcd", ((string)(null)), table126, "And "); #line hidden - TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success", "Count"}); - table125.AddRow(new string[] { + table127.AddRow(new string[] { "AUTH", "*", "", ""}); - table125.AddRow(new string[] { + table127.AddRow(new string[] { "OK", "*", "true", ""}); - table125.AddRow(new string[] { + table127.AddRow(new string[] { "OK", "7b0535b94878efb18b7c7a13630db8227e30961aed6f5556823b612423d676af", "true", ""}); - table125.AddRow(new string[] { + table127.AddRow(new string[] { "COUNT", "abcd", "", "1"}); #line 60 - testRunner.Then("Alice receives messages", ((string)(null)), table125, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table127, "Then "); #line hidden - TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success", "Count"}); - table126.AddRow(new string[] { + table128.AddRow(new string[] { "AUTH", "*", "", ""}); - table126.AddRow(new string[] { + table128.AddRow(new string[] { "OK", "*", "true", ""}); - table126.AddRow(new string[] { + table128.AddRow(new string[] { "COUNT", "abcd", "", "2"}); #line 66 - testRunner.And("Charlie receives messages", ((string)(null)), table126, "And "); + testRunner.And("Charlie receives messages", ((string)(null)), table128, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/51.feature.cs b/test/Netstr.Tests/NIPs/51.feature.cs index f25b1a7..ac8b612 100644 --- a/test/Netstr.Tests/NIPs/51.feature.cs +++ b/test/Netstr.Tests/NIPs/51.feature.cs @@ -116,14 +116,14 @@ public void CreateAndRetrieveAPublicMuteList() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { "p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); - table127.AddRow(new string[] { + table129.AddRow(new string[] { "p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"}); #line 10 - testRunner.When("Alice publishes an event with kind 10000 and tags:", ((string)(null)), table127, "When "); + testRunner.When("Alice publishes an event with kind 10000 and tags:", ((string)(null)), table129, "When "); #line hidden #line 13 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -162,11 +162,11 @@ public void CreateAndRetrieveAPrivateMuteList() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { "p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); #line 19 - testRunner.When("Alice publishes an event with kind 10000 and encrypted content and tags:", ((string)(null)), table128, "When "); + testRunner.When("Alice publishes an event with kind 10000 and encrypted content and tags:", ((string)(null)), table130, "When "); #line hidden #line 21 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -208,23 +208,23 @@ public void CreateAndRetrieveABookmarkSet() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { "d", "my-bookmarks"}); - table129.AddRow(new string[] { + table131.AddRow(new string[] { "name", "Programming Resources"}); - table129.AddRow(new string[] { + table131.AddRow(new string[] { "about", "Collection of useful programming articles and tutorials"}); - table129.AddRow(new string[] { + table131.AddRow(new string[] { "e", "d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"}); - table129.AddRow(new string[] { + table131.AddRow(new string[] { "a", "30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"}); #line 28 - testRunner.When("Alice publishes an event with kind 30003 and tags:", ((string)(null)), table129, "When "); + testRunner.When("Alice publishes an event with kind 30003 and tags:", ((string)(null)), table131, "When "); #line hidden #line 34 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -266,20 +266,20 @@ public void CreateAndRetrieveAnEmojiSet() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { "d", "custom-emojis"}); - table130.AddRow(new string[] { + table132.AddRow(new string[] { "name", "My Custom Emojis"}); - table130.AddRow(new string[] { + table132.AddRow(new string[] { "emoji", "happy,https://example.com/happy.png"}); - table130.AddRow(new string[] { + table132.AddRow(new string[] { "emoji", "sad,https://example.com/sad.png"}); #line 41 - testRunner.When("Alice publishes an event with kind 30030 and tags:", ((string)(null)), table130, "When "); + testRunner.When("Alice publishes an event with kind 30030 and tags:", ((string)(null)), table132, "When "); #line hidden #line 46 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -318,23 +318,23 @@ public void CreateAndRetrieveARelaySet() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { "d", "my-relays"}); - table131.AddRow(new string[] { + table133.AddRow(new string[] { "name", "Primary Relays"}); - table131.AddRow(new string[] { + table133.AddRow(new string[] { "about", "My main relay connections"}); - table131.AddRow(new string[] { + table133.AddRow(new string[] { "relay", "wss://relay1.example.com"}); - table131.AddRow(new string[] { + table133.AddRow(new string[] { "relay", "wss://relay2.example.com"}); #line 52 - testRunner.When("Alice publishes an event with kind 30002 and tags:", ((string)(null)), table131, "When "); + testRunner.When("Alice publishes an event with kind 30002 and tags:", ((string)(null)), table133, "When "); #line hidden #line 58 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -373,17 +373,17 @@ public void CreateAndRetrieveAKindMuteSet() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { "d", "1"}); - table132.AddRow(new string[] { + table134.AddRow(new string[] { "p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"}); - table132.AddRow(new string[] { + table134.AddRow(new string[] { "p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"}); #line 64 - testRunner.When("Alice publishes an event with kind 30007 and tags:", ((string)(null)), table132, "When "); + testRunner.When("Alice publishes an event with kind 30007 and tags:", ((string)(null)), table134, "When "); #line hidden #line 68 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); diff --git a/test/Netstr.Tests/NIPs/57.feature.cs b/test/Netstr.Tests/NIPs/57.feature.cs index f886287..b0d7ed3 100644 --- a/test/Netstr.Tests/NIPs/57.feature.cs +++ b/test/Netstr.Tests/NIPs/57.feature.cs @@ -116,21 +116,21 @@ public void CreateAndRetrieveAZapRequest() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { "relays", "wss://relay1.example.com,wss://relay2.example.com"}); - table133.AddRow(new string[] { + table135.AddRow(new string[] { "amount", "21000"}); - table133.AddRow(new string[] { + table135.AddRow(new string[] { "lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x" + "9xp"}); - table133.AddRow(new string[] { + table135.AddRow(new string[] { "p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"}); #line 10 - testRunner.When("Alice publishes an event with kind 9734 and tags:", ((string)(null)), table133, "When "); + testRunner.When("Alice publishes an event with kind 9734 and tags:", ((string)(null)), table135, "When "); #line hidden #line 15 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -170,20 +170,20 @@ public void CreateAndRetrieveAZapReceipt() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { "p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"}); - table134.AddRow(new string[] { + table136.AddRow(new string[] { "bolt11", @"lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"}); - table134.AddRow(new string[] { + table136.AddRow(new string[] { "description", @"{""pubkey"":""32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"",""content"":"""",""id"":""d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d"",""created_at"":1674164539,""sig"":""77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d"",""kind"":9734,""tags"":[[""e"",""3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8""],[""p"",""32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245""],[""relays"",""wss://relay.damus.io"",""wss://nostr-relay.wlvs.space"",""wss://nostr.fmt.wiz.biz"",""wss://relay.nostr.bg"",""wss://nostr.oxtr.dev"",""wss://nostr.v0l.io"",""wss://brb.io"",""wss://nostr.bitcoiner.social"",""ws://monad.jb55.com:8080"",""wss://relay.snort.social""]]}"}); - table134.AddRow(new string[] { + table136.AddRow(new string[] { "preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"}); #line 21 - testRunner.When("Alice publishes an event with kind 9735 and tags:", ((string)(null)), table134, "When "); + testRunner.When("Alice publishes an event with kind 9735 and tags:", ((string)(null)), table136, "When "); #line hidden #line 26 testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); diff --git a/test/Netstr.Tests/NIPs/62.feature.cs b/test/Netstr.Tests/NIPs/62.feature.cs index 1041ae7..a3f0ca2 100644 --- a/test/Netstr.Tests/NIPs/62.feature.cs +++ b/test/Netstr.Tests/NIPs/62.feature.cs @@ -84,32 +84,32 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table135.AddRow(new string[] { + table137.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table135, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table137, "And "); #line hidden - TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table136.AddRow(new string[] { + table138.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 10 - testRunner.And("Bob is connected to relay", ((string)(null)), table136, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table138, "And "); #line hidden - TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table137.AddRow(new string[] { + table139.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 13 - testRunner.And("Charlie is connected to relay", ((string)(null)), table137, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table139, "And "); #line hidden } @@ -141,104 +141,104 @@ public void RequestToVanishDeletesUsersData() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table138.AddRow(new string[] { + table140.AddRow(new string[] { "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643", "Hello", "1", "", "1728905459"}); - table138.AddRow(new string[] { + table140.AddRow(new string[] { "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957", "Hello Later", "1", "", "1728905481"}); - table138.AddRow(new string[] { + table140.AddRow(new string[] { "5c19b5808ee4ad3d31e4129cc112679147e28f3d88e24683a3afa327ba0a2ee8", "DM", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1728905459"}); - table138.AddRow(new string[] { + table140.AddRow(new string[] { "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f", "DM Late", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1728905480"}); #line 21 - testRunner.When("Bob publishes events", ((string)(null)), table138, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table140, "When "); #line hidden - TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table139.AddRow(new string[] { + table141.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table139.AddRow(new string[] { + table141.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table139.AddRow(new string[] { + table141.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); #line 27 - testRunner.When("Alice publishes events", ((string)(null)), table139, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table141, "When "); #line hidden - TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table140.AddRow(new string[] { + table142.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 32 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table140, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table142, "And "); #line hidden - TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table143 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EVENT", "abcd", "bb5d31b0522faee9582dfede36a042a3209dc297f34c4850f2de3bbef05ad957"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EVENT", "abcd", "78a1df26e6e30633663934dfb6da696184497ee98964aeae87292aae54bf166f"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EVENT", "abcd", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EVENT", "abcd", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EVENT", "abcd", "1e4ef30065360dd8ba6a4b74c99b6d70447946fa17e31e2960f12d3d7a9fb643"}); - table141.AddRow(new string[] { + table143.AddRow(new string[] { "EOSE", "abcd", ""}); #line 35 - testRunner.Then("Charlie receives messages", ((string)(null)), table141, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table143, "Then "); #line hidden } this.ScenarioCleanup(); @@ -266,61 +266,61 @@ public void OldEventsPublishedAfterRequestToVanishAreRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table144 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table142.AddRow(new string[] { + table144.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table142.AddRow(new string[] { + table144.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table142.AddRow(new string[] { + table144.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table142.AddRow(new string[] { + table144.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); #line 46 - testRunner.When("Alice publishes events", ((string)(null)), table142, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table144, "When "); #line hidden - TechTalk.SpecFlow.Table table143 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table145 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table143.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "true"}); - table143.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table143.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "false"}); - table143.AddRow(new string[] { + table145.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "true"}); #line 52 - testRunner.Then("Alice receives messages", ((string)(null)), table143, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table145, "Then "); #line hidden } this.ScenarioCleanup(); @@ -349,41 +349,41 @@ public void DeletingRequestToVanishIsRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table144 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table146 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table144.AddRow(new string[] { + table146.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table144.AddRow(new string[] { + table146.AddRow(new string[] { "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", "", "5", "[[\"e\", \"9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e\"]]", "1728905471"}); #line 62 - testRunner.When("Alice publishes events", ((string)(null)), table144, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table146, "When "); #line hidden - TechTalk.SpecFlow.Table table145 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table147 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table145.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table145.AddRow(new string[] { + table147.AddRow(new string[] { "OK", "bb8db141cc129fd5fbc792f871bca9f14a04cfb80607feacd19698b4a7dd878a", "false"}); #line 66 - testRunner.Then("Alice receives messages", ((string)(null)), table145, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table147, "Then "); #line hidden } this.ScenarioCleanup(); @@ -412,101 +412,101 @@ public void OlderRequestToVanishDoesNothingNewerDeletesNewerEvents() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table146 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table148 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "Hello", "1", "", "1728905459"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "I\'m outta here", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905470"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", "I\'m outta here sooner", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905460"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", "Hello", "1", "", "1728905465"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", "I\'m outta here later", "62", "[[\"relay\",\"ALL_RELAYS\"]]", "1728905490"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "Hello Later", "1", "", "1728905480"}); - table146.AddRow(new string[] { + table148.AddRow(new string[] { "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", "Hello", "1", "", "1728905495"}); #line 76 - testRunner.When("Alice publishes events", ((string)(null)), table146, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table148, "When "); #line hidden - TechTalk.SpecFlow.Table table147 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table149 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "ff1092c354d94060a185f8b5e4349499079872babe27b882fd4632efcdd001c2", "true"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "true"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "9766e0efe45ecd90c508e66a8dd3eee3a7f16be33af87aded9fc779f40237d0e", "true"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "2f965ea6c9d085a2c0a55b90e6b38ba8d3f64cc022bd0117fc529037bce93cc9", "false"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "8ac0adbfb1340ac100e13f756dcd47e1ac23b84264147924c854351b8ddd1173", "false"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "e2ccbd594526fe5c81144dc9d0ed1164757e21da3b6ce82486fa4bba81a86590", "true"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "f45c291b8c4e3a164e68932f251e50b4182f4dfe2eca76081a7ca2d759568dfd", "false"}); - table147.AddRow(new string[] { + table149.AddRow(new string[] { "OK", "e4262ef3899cb75be630c2940897226d8dca15e81cc4588ed812c86e8bcdabbc", "true"}); #line 86 - testRunner.Then("Alice receives messages", ((string)(null)), table147, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table149, "Then "); #line hidden } this.ScenarioCleanup(); @@ -535,51 +535,51 @@ public void RequestToVanishIsIgnoredWhenRelayTagDoesntMatchCurrentRelay() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table148 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table148.AddRow(new string[] { + table150.AddRow(new string[] { "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", "I\'m outta here", "62", "", "1728905470"}); - table148.AddRow(new string[] { + table150.AddRow(new string[] { "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", "I\'m outta here", "62", "[[\"relay\",\"blabla\"]]", "1728905470"}); - table148.AddRow(new string[] { + table150.AddRow(new string[] { "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", "I\'m outta here", "62", "[[\"relay\",\"ws://localhost/\"]]", "1728905470"}); #line 100 - testRunner.When("Alice publishes events", ((string)(null)), table148, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table150, "When "); #line hidden - TechTalk.SpecFlow.Table table149 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { "Type", "EventId", "Success"}); - table149.AddRow(new string[] { + table151.AddRow(new string[] { "OK", "95a19f740a0415634581033596cdc5596e43a41a9a73bf3775d37d32b6734b72", "false"}); - table149.AddRow(new string[] { + table151.AddRow(new string[] { "OK", "7fbc1941a2a9c07931ad62510283464ff69c8b2a386f47c129a6aecc4e350adc", "false"}); - table149.AddRow(new string[] { + table151.AddRow(new string[] { "OK", "845c4d3df838caaf98e45c06578a2dea7c77d384e43bfc27d239b121e6320020", "true"}); #line 105 - testRunner.Then("Alice receives messages", ((string)(null)), table149, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table151, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/64.feature.cs b/test/Netstr.Tests/NIPs/64.feature.cs index d170529..777d33e 100644 --- a/test/Netstr.Tests/NIPs/64.feature.cs +++ b/test/Netstr.Tests/NIPs/64.feature.cs @@ -233,11 +233,11 @@ public void PublishChessGameWithAltTagForNon_SupportingClients() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { "alt", "Fischer vs. Spassky in Belgrade on 1992-11-04"}); #line 40 - testRunner.When("Alice publishes an event with kind 64 and tags:", ((string)(null)), table150, "When "); + testRunner.When("Alice publishes an event with kind 64 and tags:", ((string)(null)), table152, "When "); #line hidden #line 42 testRunner.And("content \"1. e4 e5 2. Nf3 Nc6 3. Bb5 1/2-1/2\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); diff --git a/test/Netstr.Tests/NIPs/65.feature.cs b/test/Netstr.Tests/NIPs/65.feature.cs index 95592d9..b604095 100644 --- a/test/Netstr.Tests/NIPs/65.feature.cs +++ b/test/Netstr.Tests/NIPs/65.feature.cs @@ -114,23 +114,23 @@ public void PublishingValidRelayList() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read", "write"}); - table151.AddRow(new string[] { + table153.AddRow(new string[] { "r", "wss://relay2.com", "read", ""}); - table151.AddRow(new string[] { + table153.AddRow(new string[] { "r", "wss://relay3.com", "write", ""}); #line 11 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table151, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table153, "When "); #line hidden #line 15 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -166,16 +166,16 @@ public void UpdatingExistingRelayList() #line 19 testRunner.Given("I have published relay configurations", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table154 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "read"}); - table152.AddRow(new string[] { + table154.AddRow(new string[] { "r", "wss://relay4.com", "write"}); #line 20 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table152, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table154, "When "); #line hidden #line 23 testRunner.Then("I should receive an \"OK\" message", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -243,13 +243,13 @@ public void PublishingInvalidRelayURL() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table155 = new TechTalk.SpecFlow.Table(new string[] { "r", "invalid-url", "read", "write"}); #line 32 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table153, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table155, "When "); #line hidden #line 34 testRunner.Then("I should receive an error message containing \"Invalid relay URL format\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); @@ -279,12 +279,12 @@ public void PublishingInvalidPermissionMarker() #line 6 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table154 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { "r", "wss://relay1.com", "invalid"}); #line 37 - testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table154, "When "); + testRunner.When("I publish an event with kind 10002 and tags:", ((string)(null)), table156, "When "); #line hidden #line 39 testRunner.Then("I should receive an error message containing \"Invalid relay permission marker\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); diff --git a/test/Netstr.Tests/NIPs/70.feature.cs b/test/Netstr.Tests/NIPs/70.feature.cs index 0a36288..aaf28a3 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 table155 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table157 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table155.AddRow(new string[] { + table157.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table155, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table157, "And "); #line hidden } @@ -120,35 +120,35 @@ public void NotAuthenticatedClientTriesToPublishProtectedEvent() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table158 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table156.AddRow(new string[] { + table158.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 13 - testRunner.When("Alice publishes an event", ((string)(null)), table156, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table158, "When "); #line hidden - TechTalk.SpecFlow.Table table157 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table159 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table157.AddRow(new string[] { + table159.AddRow(new string[] { "AUTH", "*", ""}); - table157.AddRow(new string[] { + table159.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "false"}); #line 16 - testRunner.Then("Alice receives messages", ((string)(null)), table157, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table159, "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 table158 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table160 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table158.AddRow(new string[] { + table160.AddRow(new string[] { "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "Protected", "1", "[[ \"-\" ]]", "1722337837"}); #line 24 - testRunner.When("Alice publishes an event", ((string)(null)), table158, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table160, "When "); #line hidden - TechTalk.SpecFlow.Table table159 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table161 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table159.AddRow(new string[] { + table161.AddRow(new string[] { "AUTH", "*", ""}); - table159.AddRow(new string[] { + table161.AddRow(new string[] { "OK", "*", "true"}); - table159.AddRow(new string[] { + table161.AddRow(new string[] { "OK", "92f3f4bfb1c756108b242dc02169fa96bd53d5ac5331c6ac5e377045637e2cf5", "true"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table159, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table161, "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 table160 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table162 = new TechTalk.SpecFlow.Table(new string[] { "Id", "PublicKey", "Content", "Kind", "Tags", "CreatedAt"}); - table160.AddRow(new string[] { + table162.AddRow(new string[] { "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "Protected", @@ -255,26 +255,26 @@ public void AuthenticatedClientTriesToPublishSomeoneElsesProtectedEvent() "[[ \"-\" ]]", "1722337837"}); #line 36 - testRunner.When("Alice publishes an event", ((string)(null)), table160, "When "); + testRunner.When("Alice publishes an event", ((string)(null)), table162, "When "); #line hidden - TechTalk.SpecFlow.Table table161 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table163 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table161.AddRow(new string[] { + table163.AddRow(new string[] { "AUTH", "*", ""}); - table161.AddRow(new string[] { + table163.AddRow(new string[] { "OK", "*", "true"}); - table161.AddRow(new string[] { + table163.AddRow(new string[] { "OK", "1c982ee8b0f2484815a4befbb26bb02d6b20b4b3a85bfe6568a3712f943aa940", "false"}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table161, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table163, "Then "); #line hidden } this.ScenarioCleanup(); From 08674f24dfc897de1f256bef3974fc2eda1e5308 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:10:16 -0500 Subject: [PATCH 20/49] feat: make HTTPS redirection configurable and fix subscription limits - add connection option for HTTPS redirect toggle and apply in startup pipeline - adjust subscription matching/limit behavior in relay handlers - add NIP-42 auth reference materials for implementation debugging --- .../Subscriptions/MatchingExtensions.cs | 9 +++++++-- src/Netstr/Options/ConnectionOptions.cs | 1 + src/Netstr/Program.cs | 17 +++++++++++++++-- src/Netstr/Properties/launchSettings.json | 4 ++-- src/Netstr/appsettings.json | 4 +++- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs index 8de58c7..9be3529 100644 --- a/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs +++ b/src/Netstr/Messaging/Subscriptions/MatchingExtensions.cs @@ -33,7 +33,7 @@ public static IQueryable WhereAnyFilterMatches( // Build a single query that handles OR semantics between filters IQueryable query = entities.Where(x => false); // Start with empty query - + foreach (var filter in filterArray) { var filterQuery = entities @@ -52,11 +52,16 @@ public static IQueryable WhereAnyFilterMatches( query = query.Union(filterQuery); } + // Calculate effective limit: use the client's requested limit if specified, otherwise fallback to maxLimit + // When multiple filters have limits, use the minimum (most restrictive) + var specifiedLimits = filterArray.Where(f => f.Limit.HasValue).Select(f => f.Limit!.Value); + var effectiveLimit = specifiedLimits.Any() ? specifiedLimits.Min() : maxLimit; + return query .Include(x => x.Tags) .OrderByDescending(x => x.EventCreatedAt) .ThenBy(x => x.EventId) - .Take(maxLimit) + .Take(effectiveLimit) .AsNoTracking(); } diff --git a/src/Netstr/Options/ConnectionOptions.cs b/src/Netstr/Options/ConnectionOptions.cs index 6c279ac..4753b53 100644 --- a/src/Netstr/Options/ConnectionOptions.cs +++ b/src/Netstr/Options/ConnectionOptions.cs @@ -3,5 +3,6 @@ public class ConnectionOptions { public required string WebSocketsPath { get; init; } + public bool UseHttpsRedirection { get; init; } = true; } } diff --git a/src/Netstr/Program.cs b/src/Netstr/Program.cs index e2354ed..64626b1 100644 --- a/src/Netstr/Program.cs +++ b/src/Netstr/Program.cs @@ -47,13 +47,26 @@ var app = builder.Build(); var options = app.Services.GetRequiredService>(); +// Log environment and configuration +var logger = app.Services.GetRequiredService>(); +logger.LogInformation("Environment: {Environment}", app.Environment.EnvironmentName); +logger.LogInformation("HTTPS Redirect Enabled: {Enabled}", options.Value.UseHttpsRedirection); +logger.LogInformation("WebSocket Path: {Path}", options.Value.WebSocketsPath); + // Setup pipeline + init DB app .UseCors() .UseWebSockets() .UseStaticFiles() - .UseRouting() - .UseHttpsRedirection() + .UseRouting(); + +// Conditionally apply HTTPS redirection based on configuration +if (options.Value.UseHttpsRedirection) +{ + app.UseHttpsRedirection(); +} + +app .AcceptWebSocketsConnections() .EnsureDbContextMigrations(); diff --git a/src/Netstr/Properties/launchSettings.json b/src/Netstr/Properties/launchSettings.json index 38963da..3b67abb 100644 --- a/src/Netstr/Properties/launchSettings.json +++ b/src/Netstr/Properties/launchSettings.json @@ -6,7 +6,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:8081;https://localhost:8443" + "applicationUrl": "http://0.0.0.0:8085;https://0.0.0.0:8443" }, "IIS Express": { "commandName": "IISExpress", @@ -26,7 +26,7 @@ "launchUrl": "https://localhost:8443", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_URLS": "http://localhost:8081;https://localhost:8443" + "ASPNETCORE_URLS": "http://0.0.0.0:8085;https://0.0.0.0:8443" }, "distributionName": "" } diff --git a/src/Netstr/appsettings.json b/src/Netstr/appsettings.json index 30f70c6..af5a194 100644 --- a/src/Netstr/appsettings.json +++ b/src/Netstr/appsettings.json @@ -21,7 +21,8 @@ }, "AllowedHosts": "*", "Connection": { - "WebSocketsPath": "/" + "WebSocketsPath": "/", + "UseHttpsRedirection": true }, "Auth": { "Mode": "WhenNeeded", @@ -98,3 +99,4 @@ } } + From 4cb68287eacd8bc6efbe8954335ceff1abfb3371 Mon Sep 17 00:00:00 2001 From: Emmanuel Almonte <35371633+EmmanuelAlmonte@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:44:42 -0500 Subject: [PATCH 21/49] feat: add NIP-02 follow-list validator and broaden NIP coverage - introduce follow-list event validator and register it in messaging pipeline - extend event kind model for follow-list semantics - add and refresh generated feature/spec tests including NIP-02 and NIP-77 --- src/Netstr/Extensions/MessagingExtensions.cs | 10 +- .../Events/Validators/FollowListValidator.cs | 74 ++ src/Netstr/Messaging/Models/EventKind.cs | 2 + test/Netstr.Tests/NIPs/02.feature | 124 +++ test/Netstr.Tests/NIPs/02.feature.cs | 734 ++++++++++++++ test/Netstr.Tests/NIPs/04.feature.cs | 96 +- test/Netstr.Tests/NIPs/05.feature | 112 ++- test/Netstr.Tests/NIPs/05.feature.cs | 412 ++++++-- test/Netstr.Tests/NIPs/09.feature.cs | 210 ++-- test/Netstr.Tests/NIPs/11.feature.cs | 36 +- test/Netstr.Tests/NIPs/119.feature.cs | 44 +- test/Netstr.Tests/NIPs/13.feature.cs | 36 +- test/Netstr.Tests/NIPs/17.feature.cs | 96 +- test/Netstr.Tests/NIPs/40.feature.cs | 54 +- test/Netstr.Tests/NIPs/42.feature.cs | 76 +- test/Netstr.Tests/NIPs/45.feature.cs | 104 +- test/Netstr.Tests/NIPs/51.feature | 226 +++-- test/Netstr.Tests/NIPs/51.feature.cs | 928 ++++++++++++++---- test/Netstr.Tests/NIPs/57.feature | 149 ++- test/Netstr.Tests/NIPs/57.feature.cs | 727 ++++++++++++-- test/Netstr.Tests/NIPs/62.feature.cs | 162 +-- test/Netstr.Tests/NIPs/64.feature.cs | 4 +- test/Netstr.Tests/NIPs/65.feature | 121 ++- test/Netstr.Tests/NIPs/65.feature.cs | 468 ++++++--- test/Netstr.Tests/NIPs/70.feature.cs | 52 +- test/Netstr.Tests/NIPs/77.feature | 31 + test/Netstr.Tests/NIPs/77.feature.cs | 214 ++++ 27 files changed, 4140 insertions(+), 1162 deletions(-) create mode 100644 src/Netstr/Messaging/Events/Validators/FollowListValidator.cs create mode 100644 test/Netstr.Tests/NIPs/02.feature create mode 100644 test/Netstr.Tests/NIPs/02.feature.cs create mode 100644 test/Netstr.Tests/NIPs/77.feature create mode 100644 test/Netstr.Tests/NIPs/77.feature.cs diff --git a/src/Netstr/Extensions/MessagingExtensions.cs b/src/Netstr/Extensions/MessagingExtensions.cs index f6d29a4..5296765 100644 --- a/src/Netstr/Extensions/MessagingExtensions.cs +++ b/src/Netstr/Extensions/MessagingExtensions.cs @@ -24,7 +24,12 @@ public static IServiceCollection AddMessaging(this IServiceCollection services) services.AddTransient(); // NIP-05 verification service - services.AddHttpClient(); + // Per NIP-05 spec: MUST NOT follow HTTP redirects for security + services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AllowAutoRedirect = false + }); // message services.AddSingleton(); @@ -80,6 +85,9 @@ public static IServiceCollection AddEventValidators(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Netstr/Messaging/Events/Validators/FollowListValidator.cs b/src/Netstr/Messaging/Events/Validators/FollowListValidator.cs new file mode 100644 index 0000000..4d1882b --- /dev/null +++ b/src/Netstr/Messaging/Events/Validators/FollowListValidator.cs @@ -0,0 +1,74 @@ +using Netstr.Messaging.Models; +using System.Text.RegularExpressions; + +namespace Netstr.Messaging.Events.Validators +{ + /// + /// Validates NIP-02 Follow List events (kind 3). + /// Follow lists contain "p" tags referencing other users' public keys. + /// Content is not used per spec but may contain data for backwards compatibility. + /// + public class FollowListValidator : IEventValidator + { + private const string InvalidPubkeyFormat = "invalid: follow list contains invalid pubkey format"; + private const string InvalidRelayUrl = "invalid: follow list contains invalid relay URL"; + private const string InvalidTagFormat = "invalid: follow list must only contain 'p' tags"; + + // Regex for validating 64-character hex pubkeys + private static readonly Regex HexPubkeyPattern = new(@"^[0-9a-fA-F]{64}$", RegexOptions.Compiled); + + public string? Validate(Event e, ClientContext context) + { + // Only validate follow list events (kind 3) + if (e.Kind != (long)EventKind.FollowList) + { + return null; + } + + // NIP-02: Content is not used but may contain JSON for backwards compatibility + // We don't validate content - it can be empty or contain relay data + + // Validate tags + foreach (var tag in e.Tags) + { + if (tag.Length == 0) + { + continue; // Skip empty tags + } + + // Follow list should only contain "p" tags + if (tag[0] != EventTag.PublicKey) + { + return InvalidTagFormat; + } + + // "p" tag must have at least the pubkey + if (tag.Length < 2) + { + return InvalidPubkeyFormat; + } + + // Validate pubkey format (64-char hex) + var pubkey = tag[1]; + if (string.IsNullOrEmpty(pubkey) || !HexPubkeyPattern.IsMatch(pubkey)) + { + return InvalidPubkeyFormat; + } + + // If relay URL is provided (optional), validate it + if (tag.Length >= 3 && !string.IsNullOrEmpty(tag[2])) + { + var relayUrl = tag[2]; + if (!Uri.IsWellFormedUriString(relayUrl, UriKind.Absolute)) + { + return InvalidRelayUrl; + } + } + + // Petname (tag[3]) is optional and can be any string, no validation needed + } + + return null; + } + } +} diff --git a/src/Netstr/Messaging/Models/EventKind.cs b/src/Netstr/Messaging/Models/EventKind.cs index 320de43..238e52f 100644 --- a/src/Netstr/Messaging/Models/EventKind.cs +++ b/src/Netstr/Messaging/Models/EventKind.cs @@ -7,6 +7,8 @@ public enum EventKind { // Basic event kinds UserMetadata = 0, + ShortTextNote = 1, + FollowList = 3, Delete = 5, RequestToVanish = 62, GiftWrap = 1059, diff --git a/test/Netstr.Tests/NIPs/02.feature b/test/Netstr.Tests/NIPs/02.feature new file mode 100644 index 0000000..b2c556a --- /dev/null +++ b/test/Netstr.Tests/NIPs/02.feature @@ -0,0 +1,124 @@ +Feature: NIP-02 + Follow list events (kind 3) contain public keys of users the author is following. + Follow list is a replaceable event (only the latest version per author is kept). + +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: Publish valid follow list with multiple p tags + Alice publishes a follow list with multiple public keys and can query it back. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"],["p","fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"]] | 1722337838 | + And Bob sends a subscription request abcd + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 3 | + Then Alice receives a message + | Type | Id | Success | + | OK | 1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c | true | + And Bob receives messages + | Type | Id | EventId | + | EVENT | abcd | 1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c | + | EOSE | abcd | | + +Scenario: Replace existing follow list with newer timestamp + Follow list is a replaceable event, so only the latest version should be stored. + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"]] | 1722337838 | + | b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef | * | 3 | [["p","fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"]] | 1722337848 | + And Bob sends a subscription request abcd + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 3 | + Then Bob receives messages + | Type | Id | EventId | + | EVENT | abcd | b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef | + | EOSE | abcd | | + +Scenario: Follow list with relay hints and petnames + Follow list p tags can include optional relay URL and petname. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | c3d4e5f6789012345678901234567890123456789012345678901234abcdef01 | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627","wss://relay.example.com","bob"],["p","fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614","wss://nostr.example.com","charlie"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | c3d4e5f6789012345678901234567890123456789012345678901234abcdef01 | true | + +Scenario: Empty follow list with no p tags is valid + A follow list with no contacts is valid. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | d4e5f6789012345678901234567890123456789012345678901234abcdef0123 | * | 3 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | d4e5f6789012345678901234567890123456789012345678901234abcdef0123 | true | + +Scenario: Follow list with content is valid for backwards compatibility + NIP-02 says content is not used but some clients store relay info there. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | e5f6789012345678901234567890123456789012345678901234abcdef012345 | {"wss://relay.example.com":{"write":true}} | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | e5f6789012345678901234567890123456789012345678901234abcdef012345 | true | + +Scenario: Reject follow list with invalid pubkey format - wrong length + Public keys must be 64-character hex strings. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | f6789012345678901234567890123456789012345678901234abcdef01234567 | * | 3 | [["p","abc123"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | Message | + | OK | f6789012345678901234567890123456789012345678901234abcdef01234567 | false | invalid: follow list contains invalid pubkey format | + +Scenario: Reject follow list with invalid pubkey format - non-hex characters + Public keys must only contain hexadecimal characters. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 0789012345678901234567890123456789012345678901234abcdef0123456789 | * | 3 | [["p","zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | Message | + | OK | 0789012345678901234567890123456789012345678901234abcdef0123456789 | false | invalid: follow list contains invalid pubkey format | + +Scenario: Reject follow list with invalid relay URL + Relay URLs must be valid absolute URIs. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 1890123456789012345678901234567890123456789012345abcdef012345678a | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627","not-a-valid-url"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | Message | + | OK | 1890123456789012345678901234567890123456789012345abcdef012345678a | false | invalid: follow list contains invalid relay URL | + +Scenario: Reject follow list with non-p tags + Follow list should only contain p tags. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 2901234567890123456789012345678901234567890123456abcdef012345678b | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"],["e","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | Message | + | OK | 2901234567890123456789012345678901234567890123456abcdef012345678b | false | invalid: follow list must only contain 'p' tags | + +Scenario: Query follow list by author pubkey + Bob and Charlie both have follow lists, Alice can query them by author. + When Bob publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 3012345678901234567890123456789012345678901234567abcdef012345678c | * | 3 | [["p","5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"]] | 1722337838 | + And Charlie publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 4123456789012345678901234567890123456789012345678abcdef012345678d | * | 3 | [["p","5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"]] | 1722337838 | + And Alice sends a subscription request follow_sub + | Authors | Kinds | + | 5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627 | 3 | + Then Alice receives messages + | Type | Id | EventId | + | EVENT | follow_sub | 3012345678901234567890123456789012345678901234567abcdef012345678c | + | EOSE | follow_sub | | diff --git a/test/Netstr.Tests/NIPs/02.feature.cs b/test/Netstr.Tests/NIPs/02.feature.cs new file mode 100644 index 0000000..3541768 --- /dev/null +++ b/test/Netstr.Tests/NIPs/02.feature.cs @@ -0,0 +1,734 @@ +// ------------------------------------------------------------------------------ +// +// 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_02Feature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "02.feature" +#line hidden + + public NIP_02Feature(NIP_02Feature.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-02", "\tFollow list events (kind 3) contain public keys of users the author is following" + + ".\r\n\tFollow list is a replaceable event (only the latest version per author is ke" + + "pt).", 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 table38 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table38.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); +#line 7 + testRunner.And("Alice is connected to relay", ((string)(null)), table38, "And "); +#line hidden + TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table39.AddRow(new string[] { + "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", + "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); +#line 10 + testRunner.And("Bob is connected to relay", ((string)(null)), table39, "And "); +#line hidden + TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table40.AddRow(new string[] { + "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", + "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); +#line 13 + testRunner.And("Charlie is connected to relay", ((string)(null)), table40, "And "); +#line hidden + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Publish valid follow list with multiple p tags")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Publish valid follow list with multiple p tags")] + public void PublishValidFollowListWithMultiplePTags() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Publish valid follow list with multiple p tags", "\tAlice publishes a follow list with multiple public keys and can query it back.", 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 table41 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table41.AddRow(new string[] { + "1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"],[\"p\",\"f" + + "e8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", + "1722337838"}); +#line 19 + testRunner.When("Alice publishes an event", ((string)(null)), table41, "When "); +#line hidden + TechTalk.SpecFlow.Table table42 = new TechTalk.SpecFlow.Table(new string[] { + "Authors", + "Kinds"}); + table42.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "3"}); +#line 22 + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table42, "And "); +#line hidden + TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table43.AddRow(new string[] { + "OK", + "1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c", + "true"}); +#line 25 + testRunner.Then("Alice receives a message", ((string)(null)), table43, "Then "); +#line hidden + TechTalk.SpecFlow.Table table44 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table44.AddRow(new string[] { + "EVENT", + "abcd", + "1c5d9f9b8c3e4d6a7f8e9d0c1b2a3948576a5d4c3b2e1f0a9d8c7b6e5a4f3d2c"}); + table44.AddRow(new string[] { + "EOSE", + "abcd", + ""}); +#line 28 + testRunner.And("Bob receives messages", ((string)(null)), table44, "And "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Replace existing follow list with newer timestamp")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Replace existing follow list with newer timestamp")] + public void ReplaceExistingFollowListWithNewerTimestamp() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Replace existing follow list with newer timestamp", "\tFollow list is a replaceable event, so only the latest version should be stored." + + "", tagsOfScenario, argumentsOfScenario, featureTags); +#line 33 +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 table45 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table45.AddRow(new string[] { + "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"]]", + "1722337838"}); + table45.AddRow(new string[] { + "b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef", + "*", + "3", + "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", + "1722337848"}); +#line 35 + testRunner.When("Alice publishes events", ((string)(null)), table45, "When "); +#line hidden + TechTalk.SpecFlow.Table table46 = new TechTalk.SpecFlow.Table(new string[] { + "Authors", + "Kinds"}); + table46.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "3"}); +#line 39 + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table46, "And "); +#line hidden + TechTalk.SpecFlow.Table table47 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table47.AddRow(new string[] { + "EVENT", + "abcd", + "b2c3d4e5f6789012345678901234567890123456789012345678901234abcdef"}); + table47.AddRow(new string[] { + "EOSE", + "abcd", + ""}); +#line 42 + testRunner.Then("Bob receives messages", ((string)(null)), table47, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Follow list with relay hints and petnames")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Follow list with relay hints and petnames")] + public void FollowListWithRelayHintsAndPetnames() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Follow list with relay hints and petnames", "\tFollow list p tags can include optional relay URL and petname.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 47 +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 table48 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table48.AddRow(new string[] { + "c3d4e5f6789012345678901234567890123456789012345678901234abcdef01", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\",\"wss://r" + + "elay.example.com\",\"bob\"],[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5" + + "d2ec9f8f0e2f614\",\"wss://nostr.example.com\",\"charlie\"]]", + "1722337838"}); +#line 49 + testRunner.When("Alice publishes an event", ((string)(null)), table48, "When "); +#line hidden + TechTalk.SpecFlow.Table table49 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table49.AddRow(new string[] { + "OK", + "c3d4e5f6789012345678901234567890123456789012345678901234abcdef01", + "true"}); +#line 52 + testRunner.Then("Alice receives a message", ((string)(null)), table49, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Empty follow list with no p tags is valid")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Empty follow list with no p tags is valid")] + public void EmptyFollowListWithNoPTagsIsValid() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Empty follow list with no p tags is valid", "\tA follow list with no contacts is valid.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 56 +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 table50 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table50.AddRow(new string[] { + "d4e5f6789012345678901234567890123456789012345678901234abcdef0123", + "*", + "3", + "", + "1722337838"}); +#line 58 + testRunner.When("Alice publishes an event", ((string)(null)), table50, "When "); +#line hidden + TechTalk.SpecFlow.Table table51 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table51.AddRow(new string[] { + "OK", + "d4e5f6789012345678901234567890123456789012345678901234abcdef0123", + "true"}); +#line 61 + testRunner.Then("Alice receives a message", ((string)(null)), table51, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Follow list with content is valid for backwards compatibility")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Follow list with content is valid for backwards compatibility")] + public void FollowListWithContentIsValidForBackwardsCompatibility() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Follow list with content is valid for backwards compatibility", "\tNIP-02 says content is not used but some clients store relay info there.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 65 +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 table52 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table52.AddRow(new string[] { + "e5f6789012345678901234567890123456789012345678901234abcdef012345", + "{\"wss://relay.example.com\":{\"write\":true}}", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"]]", + "1722337838"}); +#line 67 + testRunner.When("Alice publishes an event", ((string)(null)), table52, "When "); +#line hidden + TechTalk.SpecFlow.Table table53 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table53.AddRow(new string[] { + "OK", + "e5f6789012345678901234567890123456789012345678901234abcdef012345", + "true"}); +#line 70 + testRunner.Then("Alice receives a message", ((string)(null)), table53, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject follow list with invalid pubkey format - wrong length")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Reject follow list with invalid pubkey format - wrong length")] + public void RejectFollowListWithInvalidPubkeyFormat_WrongLength() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject follow list with invalid pubkey format - wrong length", "\tPublic keys must be 64-character hex strings.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 74 +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 table54 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table54.AddRow(new string[] { + "f6789012345678901234567890123456789012345678901234abcdef01234567", + "*", + "3", + "[[\"p\",\"abc123\"]]", + "1722337838"}); +#line 76 + testRunner.When("Alice publishes an event", ((string)(null)), table54, "When "); +#line hidden + TechTalk.SpecFlow.Table table55 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success", + "Message"}); + table55.AddRow(new string[] { + "OK", + "f6789012345678901234567890123456789012345678901234abcdef01234567", + "false", + "invalid: follow list contains invalid pubkey format"}); +#line 79 + testRunner.Then("Alice receives a message", ((string)(null)), table55, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject follow list with invalid pubkey format - non-hex characters")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Reject follow list with invalid pubkey format - non-hex characters")] + public void RejectFollowListWithInvalidPubkeyFormat_Non_HexCharacters() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject follow list with invalid pubkey format - non-hex characters", "\tPublic keys must only contain hexadecimal characters.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 83 +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 table56 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table56.AddRow(new string[] { + "0789012345678901234567890123456789012345678901234abcdef0123456789", + "*", + "3", + "[[\"p\",\"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\"]]", + "1722337838"}); +#line 85 + testRunner.When("Alice publishes an event", ((string)(null)), table56, "When "); +#line hidden + TechTalk.SpecFlow.Table table57 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success", + "Message"}); + table57.AddRow(new string[] { + "OK", + "0789012345678901234567890123456789012345678901234abcdef0123456789", + "false", + "invalid: follow list contains invalid pubkey format"}); +#line 88 + testRunner.Then("Alice receives a message", ((string)(null)), table57, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject follow list with invalid relay URL")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Reject follow list with invalid relay URL")] + public void RejectFollowListWithInvalidRelayURL() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject follow list with invalid relay URL", "\tRelay URLs must be valid absolute URIs.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 92 +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 table58 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table58.AddRow(new string[] { + "1890123456789012345678901234567890123456789012345abcdef012345678a", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\",\"not-a-v" + + "alid-url\"]]", + "1722337838"}); +#line 94 + testRunner.When("Alice publishes an event", ((string)(null)), table58, "When "); +#line hidden + TechTalk.SpecFlow.Table table59 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success", + "Message"}); + table59.AddRow(new string[] { + "OK", + "1890123456789012345678901234567890123456789012345abcdef012345678a", + "false", + "invalid: follow list contains invalid relay URL"}); +#line 97 + testRunner.Then("Alice receives a message", ((string)(null)), table59, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Reject follow list with non-p tags")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Reject follow list with non-p tags")] + public void RejectFollowListWithNon_PTags() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Reject follow list with non-p tags", "\tFollow list should only contain p tags.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 101 +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 table60 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table60.AddRow(new string[] { + "2901234567890123456789012345678901234567890123456abcdef012345678b", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"],[\"e\",\"a" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]]", + "1722337838"}); +#line 103 + testRunner.When("Alice publishes an event", ((string)(null)), table60, "When "); +#line hidden + TechTalk.SpecFlow.Table table61 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success", + "Message"}); + table61.AddRow(new string[] { + "OK", + "2901234567890123456789012345678901234567890123456abcdef012345678b", + "false", + "invalid: follow list must only contain \'p\' tags"}); +#line 106 + testRunner.Then("Alice receives a message", ((string)(null)), table61, "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Query follow list by author pubkey")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-02")] + [Xunit.TraitAttribute("Description", "Query follow list by author pubkey")] + public void QueryFollowListByAuthorPubkey() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Query follow list by author pubkey", "\tBob and Charlie both have follow lists, Alice can query them by author.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 110 +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 table62 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table62.AddRow(new string[] { + "3012345678901234567890123456789012345678901234567abcdef012345678c", + "*", + "3", + "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", + "1722337838"}); +#line 112 + testRunner.When("Bob publishes an event", ((string)(null)), table62, "When "); +#line hidden + TechTalk.SpecFlow.Table table63 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table63.AddRow(new string[] { + "4123456789012345678901234567890123456789012345678abcdef012345678d", + "*", + "3", + "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"]]", + "1722337838"}); +#line 115 + testRunner.And("Charlie publishes an event", ((string)(null)), table63, "And "); +#line hidden + TechTalk.SpecFlow.Table table64 = new TechTalk.SpecFlow.Table(new string[] { + "Authors", + "Kinds"}); + table64.AddRow(new string[] { + "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", + "3"}); +#line 118 + testRunner.And("Alice sends a subscription request follow_sub", ((string)(null)), table64, "And "); +#line hidden + TechTalk.SpecFlow.Table table65 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table65.AddRow(new string[] { + "EVENT", + "follow_sub", + "3012345678901234567890123456789012345678901234567abcdef012345678c"}); + table65.AddRow(new string[] { + "EOSE", + "follow_sub", + ""}); +#line 121 + testRunner.Then("Alice receives messages", ((string)(null)), table65, "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_02Feature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + NIP_02Feature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/test/Netstr.Tests/NIPs/04.feature.cs b/test/Netstr.Tests/NIPs/04.feature.cs index 14634aa..78fe380 100644 --- a/test/Netstr.Tests/NIPs/04.feature.cs +++ b/test/Netstr.Tests/NIPs/04.feature.cs @@ -82,23 +82,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table38 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table66 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table38.AddRow(new string[] { + table66.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table38, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table66, "And "); #line hidden - TechTalk.SpecFlow.Table table39 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table67 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table39.AddRow(new string[] { + table67.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table39, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table67, "And "); #line hidden } @@ -129,29 +129,29 @@ public void NotAuthenticatedClientTriesToFetchKind4Events() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table40 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table68 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table40.AddRow(new string[] { + table68.AddRow(new string[] { "", "4,1"}); - table40.AddRow(new string[] { + table68.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", ""}); #line 16 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table40, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table68, "When "); #line hidden - TechTalk.SpecFlow.Table table41 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table69 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table41.AddRow(new string[] { + table69.AddRow(new string[] { "AUTH", "*"}); - table41.AddRow(new string[] { + table69.AddRow(new string[] { "CLOSED", "abcd"}); #line 20 - testRunner.Then("Alice receives messages", ((string)(null)), table41, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table69, "Then "); #line hidden } this.ScenarioCleanup(); @@ -181,87 +181,87 @@ public void AuthenticatedClientTriesToFetchKind4Events() #line 27 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 table42 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table70 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table42.AddRow(new string[] { + table70.AddRow(new string[] { "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", "Secret", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table42.AddRow(new string[] { + table70.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 28 - testRunner.And("Bob publishes events", ((string)(null)), table42, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table70, "And "); #line hidden - TechTalk.SpecFlow.Table table43 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table71 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table43.AddRow(new string[] { + table71.AddRow(new string[] { "4"}); #line 32 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table43, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table71, "When "); #line hidden - TechTalk.SpecFlow.Table table44 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table72 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table44.AddRow(new string[] { + table72.AddRow(new string[] { "3bf5ac066f40e02f2f4b4b8386e11fc7f9a482cc4ba9aee3758efb544471767b", "Secret 2", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table44.AddRow(new string[] { + table72.AddRow(new string[] { "97ded8973cfc285174a5736c44641d6e904d44b2763bef1b14c7f8f6075e581c", "Charlie\'s Secret 2", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 35 - testRunner.And("Bob publishes events", ((string)(null)), table44, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table72, "And "); #line hidden - TechTalk.SpecFlow.Table table45 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table73 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table45.AddRow(new string[] { + table73.AddRow(new string[] { "AUTH", "*", "", ""}); - table45.AddRow(new string[] { + table73.AddRow(new string[] { "OK", "*", "", "true"}); - table45.AddRow(new string[] { + table73.AddRow(new string[] { "EVENT", "abcd", "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", ""}); - table45.AddRow(new string[] { + table73.AddRow(new string[] { "EOSE", "abcd", "", ""}); - table45.AddRow(new string[] { + table73.AddRow(new string[] { "EVENT", "abcd", "3bf5ac066f40e02f2f4b4b8386e11fc7f9a482cc4ba9aee3758efb544471767b", ""}); #line 39 - testRunner.Then("Alice receives messages", ((string)(null)), table45, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table73, "Then "); #line hidden } this.ScenarioCleanup(); @@ -292,77 +292,77 @@ public void AuthenticatedClientTriesToFetchKind4EventsThroughOtherFilters() #line 49 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 table46 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table74 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table46.AddRow(new string[] { + table74.AddRow(new string[] { "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", "Secret", "4", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table46.AddRow(new string[] { + table74.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 50 - testRunner.And("Bob publishes events", ((string)(null)), table46, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table74, "And "); #line hidden - TechTalk.SpecFlow.Table table47 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table75 = new TechTalk.SpecFlow.Table(new string[] { "Ids", "Authors", "Kinds"}); - table47.AddRow(new string[] { + table75.AddRow(new string[] { "", "", "4"}); - table47.AddRow(new string[] { + table75.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "", ""}); - table47.AddRow(new string[] { + table75.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", ""}); - table47.AddRow(new string[] { + table75.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "4"}); #line 54 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table47, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table75, "When "); #line hidden - TechTalk.SpecFlow.Table table48 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table76 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table48.AddRow(new string[] { + table76.AddRow(new string[] { "AUTH", "*", "", ""}); - table48.AddRow(new string[] { + table76.AddRow(new string[] { "OK", "*", "", "true"}); - table48.AddRow(new string[] { + table76.AddRow(new string[] { "EVENT", "abcd", "1bb0124244442abc3bf02234bf601e2a6fc6c262a412936182001cd21502d695", ""}); - table48.AddRow(new string[] { + table76.AddRow(new string[] { "EOSE", "abcd", "", ""}); #line 60 - testRunner.Then("Alice receives messages", ((string)(null)), table48, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table76, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/05.feature b/test/Netstr.Tests/NIPs/05.feature index 2551761..8948fe4 100644 --- a/test/Netstr.Tests/NIPs/05.feature +++ b/test/Netstr.Tests/NIPs/05.feature @@ -1,28 +1,84 @@ -Feature: NIP-05 DNS-based Identities - Tests for NIP-05 DNS-based identity verification implementation - - Background: - Given a relay at "wss://localhost:5001" - And a user Alice - And Alice is connected to the relay - - Scenario: Accept metadata event with valid NIP-05 identifier - When Alice publishes a metadata event with NIP-05 identifier "alice@example.com" - Then the relay accepts the event - And the event is stored in the database - - Scenario: Accept metadata event with invalid NIP-05 identifier - When Alice publishes a metadata event with NIP-05 identifier "invalid-format" - Then the relay accepts the event - And the event is stored in the database - # Note: NIP-05 validation doesn't reject events, only logs verification results - - Scenario: Accept metadata event without NIP-05 identifier - When Alice publishes a metadata event without NIP-05 identifier - Then the relay accepts the event - And the event is stored in the database - - Scenario: Handle metadata event with empty NIP-05 identifier - When Alice publishes a metadata event with empty NIP-05 identifier - Then the relay accepts the event - And the event is stored in the database \ No newline at end of file +Feature: NIP-05 + DNS-based identity verification for user metadata (kind 0) events. + NIP-05 identifiers follow the format: local-part@domain + Verification is done asynchronously and never rejects events. + +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 | + +Scenario: Accept metadata event with NIP-05 identifier + NIP-05 validation runs asynchronously and never rejects events. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 1111111111111111111111111111111111111111111111111111111111111111 | {"name":"alice","nip05":"alice@example.com"} | 0 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 1111111111111111111111111111111111111111111111111111111111111111 | true | + +Scenario: Accept metadata event without NIP-05 identifier + Events without NIP-05 field are valid. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 2222222222222222222222222222222222222222222222222222222222222222 | {"name":"alice","about":"test"} | 0 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 2222222222222222222222222222222222222222222222222222222222222222 | true | + +Scenario: Accept metadata event with empty NIP-05 identifier + Empty NIP-05 field should be accepted. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 3333333333333333333333333333333333333333333333333333333333333333 | {"name":"alice","nip05":""} | 0 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 3333333333333333333333333333333333333333333333333333333333333333 | true | + +Scenario: Accept metadata event with root identifier + Root identifier uses underscore: _@domain.com + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 4444444444444444444444444444444444444444444444444444444444444444 | {"name":"example.com","nip05":"_@example.com"} | 0 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 4444444444444444444444444444444444444444444444444444444444444444 | true | + +Scenario: Accept metadata event with invalid NIP-05 format + Invalid NIP-05 format is still accepted, verification just fails silently. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 5555555555555555555555555555555555555555555555555555555555555555 | {"name":"alice","nip05":"invalid-no-at-sign"} | 0 | | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 5555555555555555555555555555555555555555555555555555555555555555 | true | + +Scenario: Query metadata by author + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 6666666666666666666666666666666666666666666666666666666666666666 | {"name":"alice","nip05":"alice@example.com","picture":"https://example.com/pic.jpg"} | 0 | | 1722337838 | + And Bob sends a subscription request metadata_sub + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 0 | + Then Bob receives messages + | Type | Id | EventId | + | EVENT | metadata_sub | 6666666666666666666666666666666666666666666666666666666666666666 | + | EOSE | metadata_sub | | + +Scenario: Metadata event is replaceable + Only the latest metadata event should be stored per author. + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | 7777777777777777777777777777777777777777777777777777777777777777 | {"name":"alice_old"} | 0 | | 1722337838 | + | 8888888888888888888888888888888888888888888888888888888888888888 | {"name":"alice_new"} | 0 | | 1722337848 | + And Bob sends a subscription request metadata_sub + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 0 | + Then Bob receives messages + | Type | Id | EventId | + | EVENT | metadata_sub | 8888888888888888888888888888888888888888888888888888888888888888 | + | EOSE | metadata_sub | | diff --git a/test/Netstr.Tests/NIPs/05.feature.cs b/test/Netstr.Tests/NIPs/05.feature.cs index a99e60c..4e9c07a 100644 --- a/test/Netstr.Tests/NIPs/05.feature.cs +++ b/test/Netstr.Tests/NIPs/05.feature.cs @@ -19,7 +19,7 @@ namespace Netstr.Tests.NIPs [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public partial class NIP_05DNS_BasedIdentitiesFeature : object, Xunit.IClassFixture, System.IDisposable + public partial class NIP_05Feature : object, Xunit.IClassFixture, System.IDisposable { private static TechTalk.SpecFlow.ITestRunner testRunner; @@ -31,7 +31,7 @@ public partial class NIP_05DNS_BasedIdentitiesFeature : object, Xunit.IClassFixt #line 1 "05.feature" #line hidden - public NIP_05DNS_BasedIdentitiesFeature(NIP_05DNS_BasedIdentitiesFeature.FixtureData fixtureData, Netstr_Tests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) + public NIP_05Feature(NIP_05Feature.FixtureData fixtureData, Netstr_Tests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) { this._testOutputHelper = testOutputHelper; this.TestInitialize(); @@ -40,7 +40,9 @@ public NIP_05DNS_BasedIdentitiesFeature(NIP_05DNS_BasedIdentitiesFeature.Fixture 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-05 DNS-based Identities", " Tests for NIP-05 DNS-based identity verification implementation", ProgrammingLanguage.CSharp, featureTags); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NIPs", "NIP-05", "\tDNS-based identity verification for user metadata (kind 0) events.\r\n\tNIP-05 iden" + + "tifiers follow the format: local-part@domain\r\n\tVerification is done asynchronous" + + "ly and never rejects events.", ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -77,16 +79,28 @@ public void ScenarioCleanup() public virtual void FeatureBackground() { -#line 4 - #line hidden -#line 5 - testRunner.Given("a relay at \"wss://localhost:5001\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); -#line hidden #line 6 - testRunner.And("a user Alice", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden #line 7 - testRunner.And("Alice is connected to the relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden + TechTalk.SpecFlow.Table table77 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table77.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); +#line 8 + testRunner.And("Alice is connected to relay", ((string)(null)), table77, "And "); +#line hidden + TechTalk.SpecFlow.Table table78 = new TechTalk.SpecFlow.Table(new string[] { + "PublicKey", + "PrivateKey"}); + table78.AddRow(new string[] { + "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", + "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); +#line 11 + testRunner.And("Bob is connected to relay", ((string)(null)), table78, "And "); #line hidden } @@ -95,16 +109,16 @@ void System.IDisposable.Dispose() this.TestTearDown(); } - [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with valid NIP-05 identifier")] - [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] - [Xunit.TraitAttribute("Description", "Accept metadata event with valid NIP-05 identifier")] - public void AcceptMetadataEventWithValidNIP_05Identifier() + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Accept metadata event with NIP-05 identifier")] + public void AcceptMetadataEventWithNIP_05Identifier() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with valid NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 9 - this.ScenarioInitialize(scenarioInfo); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with NIP-05 identifier", "\tNIP-05 validation runs asynchronously and never rejects events.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 15 +this.ScenarioInitialize(scenarioInfo); #line hidden if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) { @@ -113,32 +127,100 @@ public void AcceptMetadataEventWithValidNIP_05Identifier() else { this.ScenarioStart(); -#line 4 - this.FeatureBackground(); +#line 6 +this.FeatureBackground(); #line hidden -#line 10 - testRunner.When("Alice publishes a metadata event with NIP-05 identifier \"alice@example.com\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); + TechTalk.SpecFlow.Table table79 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table79.AddRow(new string[] { + "1111111111111111111111111111111111111111111111111111111111111111", + "{\"name\":\"alice\",\"nip05\":\"alice@example.com\"}", + "0", + "", + "1722337838"}); +#line 17 + testRunner.When("Alice publishes an event", ((string)(null)), table79, "When "); #line hidden -#line 11 - testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); + TechTalk.SpecFlow.Table table80 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table80.AddRow(new string[] { + "OK", + "1111111111111111111111111111111111111111111111111111111111111111", + "true"}); +#line 20 + testRunner.Then("Alice receives a message", ((string)(null)), table80, "Then "); #line hidden -#line 12 - testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event without NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Accept metadata event without NIP-05 identifier")] + public void AcceptMetadataEventWithoutNIP_05Identifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event without NIP-05 identifier", "\tEvents without NIP-05 field are valid.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 24 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table81 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table81.AddRow(new string[] { + "2222222222222222222222222222222222222222222222222222222222222222", + "{\"name\":\"alice\",\"about\":\"test\"}", + "0", + "", + "1722337838"}); +#line 26 + testRunner.When("Alice publishes an event", ((string)(null)), table81, "When "); +#line hidden + TechTalk.SpecFlow.Table table82 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table82.AddRow(new string[] { + "OK", + "2222222222222222222222222222222222222222222222222222222222222222", + "true"}); +#line 29 + testRunner.Then("Alice receives a message", ((string)(null)), table82, "Then "); #line hidden } this.ScenarioCleanup(); } - [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with invalid NIP-05 identifier")] - [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] - [Xunit.TraitAttribute("Description", "Accept metadata event with invalid NIP-05 identifier")] - public void AcceptMetadataEventWithInvalidNIP_05Identifier() + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with empty NIP-05 identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Accept metadata event with empty NIP-05 identifier")] + public void AcceptMetadataEventWithEmptyNIP_05Identifier() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with invalid NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 14 - this.ScenarioInitialize(scenarioInfo); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with empty NIP-05 identifier", "\tEmpty NIP-05 field should be accepted.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 33 +this.ScenarioInitialize(scenarioInfo); #line hidden if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) { @@ -147,32 +229,100 @@ public void AcceptMetadataEventWithInvalidNIP_05Identifier() else { this.ScenarioStart(); -#line 4 - this.FeatureBackground(); +#line 6 +this.FeatureBackground(); #line hidden -#line 15 - testRunner.When("Alice publishes a metadata event with NIP-05 identifier \"invalid-format\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); + TechTalk.SpecFlow.Table table83 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table83.AddRow(new string[] { + "3333333333333333333333333333333333333333333333333333333333333333", + "{\"name\":\"alice\",\"nip05\":\"\"}", + "0", + "", + "1722337838"}); +#line 35 + testRunner.When("Alice publishes an event", ((string)(null)), table83, "When "); #line hidden -#line 16 - testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); + TechTalk.SpecFlow.Table table84 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table84.AddRow(new string[] { + "OK", + "3333333333333333333333333333333333333333333333333333333333333333", + "true"}); +#line 38 + testRunner.Then("Alice receives a message", ((string)(null)), table84, "Then "); #line hidden -#line 17 - testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with root identifier")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Accept metadata event with root identifier")] + public void AcceptMetadataEventWithRootIdentifier() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with root identifier", "\tRoot identifier uses underscore: _@domain.com", tagsOfScenario, argumentsOfScenario, featureTags); +#line 42 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table85 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table85.AddRow(new string[] { + "4444444444444444444444444444444444444444444444444444444444444444", + "{\"name\":\"example.com\",\"nip05\":\"_@example.com\"}", + "0", + "", + "1722337838"}); +#line 44 + testRunner.When("Alice publishes an event", ((string)(null)), table85, "When "); +#line hidden + TechTalk.SpecFlow.Table table86 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table86.AddRow(new string[] { + "OK", + "4444444444444444444444444444444444444444444444444444444444444444", + "true"}); +#line 47 + testRunner.Then("Alice receives a message", ((string)(null)), table86, "Then "); #line hidden } this.ScenarioCleanup(); } - [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event without NIP-05 identifier")] - [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] - [Xunit.TraitAttribute("Description", "Accept metadata event without NIP-05 identifier")] - public void AcceptMetadataEventWithoutNIP_05Identifier() + [Xunit.SkippableFactAttribute(DisplayName="Accept metadata event with invalid NIP-05 format")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Accept metadata event with invalid NIP-05 format")] + public void AcceptMetadataEventWithInvalidNIP_05Format() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event without NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 20 - this.ScenarioInitialize(scenarioInfo); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Accept metadata event with invalid NIP-05 format", "\tInvalid NIP-05 format is still accepted, verification just fails silently.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 51 +this.ScenarioInitialize(scenarioInfo); #line hidden if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) { @@ -181,32 +331,114 @@ public void AcceptMetadataEventWithoutNIP_05Identifier() else { this.ScenarioStart(); -#line 4 - this.FeatureBackground(); +#line 6 +this.FeatureBackground(); #line hidden -#line 21 - testRunner.When("Alice publishes a metadata event without NIP-05 identifier", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); + TechTalk.SpecFlow.Table table87 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table87.AddRow(new string[] { + "5555555555555555555555555555555555555555555555555555555555555555", + "{\"name\":\"alice\",\"nip05\":\"invalid-no-at-sign\"}", + "0", + "", + "1722337838"}); +#line 53 + testRunner.When("Alice publishes an event", ((string)(null)), table87, "When "); #line hidden -#line 22 - testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); + TechTalk.SpecFlow.Table table88 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "Success"}); + table88.AddRow(new string[] { + "OK", + "5555555555555555555555555555555555555555555555555555555555555555", + "true"}); +#line 56 + testRunner.Then("Alice receives a message", ((string)(null)), table88, "Then "); #line hidden -#line 23 - testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Query metadata by author")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Query metadata by author")] + public void QueryMetadataByAuthor() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Query metadata by author", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 60 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 6 +this.FeatureBackground(); +#line hidden + TechTalk.SpecFlow.Table table89 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table89.AddRow(new string[] { + "6666666666666666666666666666666666666666666666666666666666666666", + "{\"name\":\"alice\",\"nip05\":\"alice@example.com\",\"picture\":\"https://example.com/pic.jp" + + "g\"}", + "0", + "", + "1722337838"}); +#line 61 + testRunner.When("Alice publishes an event", ((string)(null)), table89, "When "); +#line hidden + TechTalk.SpecFlow.Table table90 = new TechTalk.SpecFlow.Table(new string[] { + "Authors", + "Kinds"}); + table90.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "0"}); +#line 64 + testRunner.And("Bob sends a subscription request metadata_sub", ((string)(null)), table90, "And "); +#line hidden + TechTalk.SpecFlow.Table table91 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table91.AddRow(new string[] { + "EVENT", + "metadata_sub", + "6666666666666666666666666666666666666666666666666666666666666666"}); + table91.AddRow(new string[] { + "EOSE", + "metadata_sub", + ""}); +#line 67 + testRunner.Then("Bob receives messages", ((string)(null)), table91, "Then "); #line hidden } this.ScenarioCleanup(); } - [Xunit.SkippableFactAttribute(DisplayName="Handle metadata event with empty NIP-05 identifier")] - [Xunit.TraitAttribute("FeatureTitle", "NIP-05 DNS-based Identities")] - [Xunit.TraitAttribute("Description", "Handle metadata event with empty NIP-05 identifier")] - public void HandleMetadataEventWithEmptyNIP_05Identifier() + [Xunit.SkippableFactAttribute(DisplayName="Metadata event is replaceable")] + [Xunit.TraitAttribute("FeatureTitle", "NIP-05")] + [Xunit.TraitAttribute("Description", "Metadata event is replaceable")] + public void MetadataEventIsReplaceable() { string[] tagsOfScenario = ((string[])(null)); System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Handle metadata event with empty NIP-05 identifier", null, tagsOfScenario, argumentsOfScenario, featureTags); -#line 25 - this.ScenarioInitialize(scenarioInfo); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Metadata event is replaceable", "\tOnly the latest metadata event should be stored per author.", tagsOfScenario, argumentsOfScenario, featureTags); +#line 72 +this.ScenarioInitialize(scenarioInfo); #line hidden if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) { @@ -215,17 +447,53 @@ public void HandleMetadataEventWithEmptyNIP_05Identifier() else { this.ScenarioStart(); -#line 4 - this.FeatureBackground(); +#line 6 +this.FeatureBackground(); #line hidden -#line 26 - testRunner.When("Alice publishes a metadata event with empty NIP-05 identifier", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); + TechTalk.SpecFlow.Table table92 = new TechTalk.SpecFlow.Table(new string[] { + "Id", + "Content", + "Kind", + "Tags", + "CreatedAt"}); + table92.AddRow(new string[] { + "7777777777777777777777777777777777777777777777777777777777777777", + "{\"name\":\"alice_old\"}", + "0", + "", + "1722337838"}); + table92.AddRow(new string[] { + "8888888888888888888888888888888888888888888888888888888888888888", + "{\"name\":\"alice_new\"}", + "0", + "", + "1722337848"}); +#line 74 + testRunner.When("Alice publishes events", ((string)(null)), table92, "When "); #line hidden -#line 27 - testRunner.Then("the relay accepts the event", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); + TechTalk.SpecFlow.Table table93 = new TechTalk.SpecFlow.Table(new string[] { + "Authors", + "Kinds"}); + table93.AddRow(new string[] { + "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", + "0"}); +#line 78 + testRunner.And("Bob sends a subscription request metadata_sub", ((string)(null)), table93, "And "); #line hidden -#line 28 - testRunner.And("the event is stored in the database", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); + TechTalk.SpecFlow.Table table94 = new TechTalk.SpecFlow.Table(new string[] { + "Type", + "Id", + "EventId"}); + table94.AddRow(new string[] { + "EVENT", + "metadata_sub", + "8888888888888888888888888888888888888888888888888888888888888888"}); + table94.AddRow(new string[] { + "EOSE", + "metadata_sub", + ""}); +#line 81 + testRunner.Then("Bob receives messages", ((string)(null)), table94, "Then "); #line hidden } this.ScenarioCleanup(); @@ -238,12 +506,12 @@ public class FixtureData : System.IDisposable public FixtureData() { - NIP_05DNS_BasedIdentitiesFeature.FeatureSetup(); + NIP_05Feature.FeatureSetup(); } void System.IDisposable.Dispose() { - NIP_05DNS_BasedIdentitiesFeature.FeatureTearDown(); + NIP_05Feature.FeatureTearDown(); } } } diff --git a/test/Netstr.Tests/NIPs/09.feature.cs b/test/Netstr.Tests/NIPs/09.feature.cs index 81402c1..6ceaf61 100644 --- a/test/Netstr.Tests/NIPs/09.feature.cs +++ b/test/Netstr.Tests/NIPs/09.feature.cs @@ -84,32 +84,32 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table49 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table95 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table49.AddRow(new string[] { + table95.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table49, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table95, "And "); #line hidden - TechTalk.SpecFlow.Table table50 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table96 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table50.AddRow(new string[] { + table96.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 10 - testRunner.And("Bob is connected to relay", ((string)(null)), table50, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table96, "And "); #line hidden - TechTalk.SpecFlow.Table table51 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table97 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table51.AddRow(new string[] { + table97.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 13 - testRunner.And("Charlie is connected to relay", ((string)(null)), table51, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table97, "And "); #line hidden } @@ -140,25 +140,25 @@ public void DeletionRemovesReferencedRegularEventsAndIsItselfBroadcast() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table52 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table98 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table52.AddRow(new string[] { + table98.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table52.AddRow(new string[] { + table98.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table52.AddRow(new string[] { + table98.AddRow(new string[] { "04c4ee3333f6f4c59ee5d476e5c86d77922976ea0134c5e19eae665324f735c7", "", "5", @@ -166,33 +166,33 @@ public void DeletionRemovesReferencedRegularEventsAndIsItselfBroadcast() " \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"]]", "1722337845"}); #line 19 - testRunner.When("Alice publishes events", ((string)(null)), table52, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table98, "When "); #line hidden - TechTalk.SpecFlow.Table table53 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table99 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table53.AddRow(new string[] { + table99.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"}); #line 24 - testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table53, "And "); + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table99, "And "); #line hidden - TechTalk.SpecFlow.Table table54 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table100 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table54.AddRow(new string[] { + table100.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table54.AddRow(new string[] { + table100.AddRow(new string[] { "EVENT", "abcd", "04c4ee3333f6f4c59ee5d476e5c86d77922976ea0134c5e19eae665324f735c7"}); - table54.AddRow(new string[] { + table100.AddRow(new string[] { "EOSE", "abcd", ""}); #line 27 - testRunner.Then("Bob receives messages", ((string)(null)), table54, "Then "); + testRunner.Then("Bob receives messages", ((string)(null)), table100, "Then "); #line hidden } this.ScenarioCleanup(); @@ -222,103 +222,103 @@ but only those which took place before the deletion event. #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table55 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table101 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Kind", "Tags", "CreatedAt"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "af3224801d0ea862ceb45e3d75998373ff8726541f133dd0bc5badc79c832e88", "0", "", "1722337838"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "37b30f773a1a7ba1615f34482194a531eca4b3a353e7c73a8f0e08985f6a09e4", "10000", "", "1722337840"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "a23d28af8e9395478f297bd649d71a80b3d6c6c2af2c1dc1c9036ac4f451263d", "30000", "[[ \"d\", \"a\" ]]", "1722337835"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "8a75f74fe8798771c98c4c17b847f95e7ef28c7822b57e399bca41dc911f8baf", "30000", "[[ \"d\", \"b\" ]]", "1722337840"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "dd593bc09c98e958eab2414912ad097df6efdef8b99768915d2361aac4c4ceac", "5", "[[\"a\", \"0:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:\"]]", "1722337839"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "fa740ac70b991cd3955945d9799d881cd15971f37bf71902f271b00c6aa8f7f7", "5", "[[\"a\", \"10000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:\"]" + "]", "1722337839"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "8f1dbc29af4b5c96c26ee5c8932409017a1af538dbbf5207d1dc6470b488580e", "5", "[[\"a\", \"30000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:a\"" + "]]", "1722337839"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "b74adc27515ad9fa78a86acfbc03375b1ab8fc63822c826cad7564b7d23c8051", "5", "[[\"a\", \"30000:5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627:b\"" + "]]", "1722337839"}); - table55.AddRow(new string[] { + table101.AddRow(new string[] { "4a2a7d1fe9ea53ba1604eab98523f26eaee750a86983aa5fbe86614f9c5a2318", "30000", "[[ \"d\", \"a\" ]]", "1722337836"}); #line 39 - testRunner.When("Bob publishes events", ((string)(null)), table55, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table101, "When "); #line hidden - TechTalk.SpecFlow.Table table56 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table102 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table56.AddRow(new string[] { + table102.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 50 - testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table56, "And "); + testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table102, "And "); #line hidden - TechTalk.SpecFlow.Table table57 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table103 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "37b30f773a1a7ba1615f34482194a531eca4b3a353e7c73a8f0e08985f6a09e4"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "8a75f74fe8798771c98c4c17b847f95e7ef28c7822b57e399bca41dc911f8baf"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "8f1dbc29af4b5c96c26ee5c8932409017a1af538dbbf5207d1dc6470b488580e"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "b74adc27515ad9fa78a86acfbc03375b1ab8fc63822c826cad7564b7d23c8051"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "dd593bc09c98e958eab2414912ad097df6efdef8b99768915d2361aac4c4ceac"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EVENT", "abcd", "fa740ac70b991cd3955945d9799d881cd15971f37bf71902f271b00c6aa8f7f7"}); - table57.AddRow(new string[] { + table103.AddRow(new string[] { "EOSE", "abcd", ""}); #line 53 - testRunner.Then("Alice receives messages", ((string)(null)), table57, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table103, "Then "); #line hidden } this.ScenarioCleanup(); @@ -347,65 +347,65 @@ public void ItsNotAllowedToDeleteSomeoneElsesEvents() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table58 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table104 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table58.AddRow(new string[] { + table104.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table58.AddRow(new string[] { + table104.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table58.AddRow(new string[] { + table104.AddRow(new string[] { "da4e33af3793fd4f9d5487a116ee1a03142599e9b1115af38838e469473a8c6b", "Tags", "30000", "[[\"d\", \"a\"]]", "1722337848"}); #line 67 - testRunner.When("Alice publishes events", ((string)(null)), table58, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table104, "When "); #line hidden - TechTalk.SpecFlow.Table table59 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table105 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table59.AddRow(new string[] { + table105.AddRow(new string[] { "a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346", "Hello 1", "1", "", "1722337838"}); - table59.AddRow(new string[] { + table105.AddRow(new string[] { "3abeb55eb9e6a58acf06269f5e93dabd4c91d1e51d08beeab884917180b9248f", "Tags", "30000", "[[\"d\", \"a\"]]", "1722337848"}); - table59.AddRow(new string[] { + table105.AddRow(new string[] { "06f7797468cf1fde45dc438288d44418f416302e94dba22e31b8ef60b74f44bc", "", "5", "[[\"e\", \"a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346\"],[\"e\", " + "\"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table59.AddRow(new string[] { + table105.AddRow(new string[] { "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1", "", "5", "[[\"e\", \"a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346\"]]", "1722337845"}); - table59.AddRow(new string[] { + table105.AddRow(new string[] { "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb", "", "5", @@ -413,73 +413,73 @@ public void ItsNotAllowedToDeleteSomeoneElsesEvents() "]]", "1722337849"}); #line 72 - testRunner.And("Bob publishes events", ((string)(null)), table59, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table105, "And "); #line hidden - TechTalk.SpecFlow.Table table60 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table106 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table60.AddRow(new string[] { + table106.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 79 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table60, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table106, "And "); #line hidden - TechTalk.SpecFlow.Table table61 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table107 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EVENT", "abcd", "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EVENT", "abcd", "da4e33af3793fd4f9d5487a116ee1a03142599e9b1115af38838e469473a8c6b"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EVENT", "abcd", "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EVENT", "abcd", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5"}); - table61.AddRow(new string[] { + table107.AddRow(new string[] { "EOSE", "abcd", ""}); #line 82 - testRunner.Then("Charlie receives messages", ((string)(null)), table61, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table107, "Then "); #line hidden - TechTalk.SpecFlow.Table table62 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table108 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table62.AddRow(new string[] { + table108.AddRow(new string[] { "OK", "a6d166e834e78827af0770f31f15b13a772f281ad880f43ce12c24d4e3d0e346", "true"}); - table62.AddRow(new string[] { + table108.AddRow(new string[] { "OK", "3abeb55eb9e6a58acf06269f5e93dabd4c91d1e51d08beeab884917180b9248f", "true"}); - table62.AddRow(new string[] { + table108.AddRow(new string[] { "OK", "06f7797468cf1fde45dc438288d44418f416302e94dba22e31b8ef60b74f44bc", "false"}); - table62.AddRow(new string[] { + table108.AddRow(new string[] { "OK", "b644d0e9b646df95eee0fba09fd7b742df1a6c878ae752112639302ef0aa2da1", "true"}); - table62.AddRow(new string[] { + table108.AddRow(new string[] { "OK", "9b061a1d369cae854f8d518f0cedceb7ea0169cf9736a92e5362b0535dfa96fb", "true"}); #line 90 - testRunner.And("Bob receives messages", ((string)(null)), table62, "And "); + testRunner.And("Bob receives messages", ((string)(null)), table108, "And "); #line hidden } this.ScenarioCleanup(); @@ -506,65 +506,65 @@ public void DeletingADeletionHasNoAffect() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table63 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table109 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table63.AddRow(new string[] { + table109.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table63.AddRow(new string[] { + table109.AddRow(new string[] { "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927", "Later", "1", "", "1722337848"}); - table63.AddRow(new string[] { + table109.AddRow(new string[] { "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "", "5", "[[\"e\", \"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table63.AddRow(new string[] { + table109.AddRow(new string[] { "254ab6e975fc906256f9f318e50c450cd745745031459bddb027c655124302a7", "", "5", "[[\"e\", \"367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529\"]]", "1722337845"}); #line 100 - testRunner.When("Alice publishes events", ((string)(null)), table63, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table109, "When "); #line hidden - TechTalk.SpecFlow.Table table64 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table110 = new TechTalk.SpecFlow.Table(new string[] { "Authors"}); - table64.AddRow(new string[] { + table110.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75,5bc683a5d12133a9" + "6ac5502c15fe1c2287986cff7baf6283600360e6bb01f627"}); #line 106 - testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table64, "And "); + testRunner.And("Charlie sends a subscription request abcd", ((string)(null)), table110, "And "); #line hidden - TechTalk.SpecFlow.Table table65 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table111 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table65.AddRow(new string[] { + table111.AddRow(new string[] { "EVENT", "abcd", "86aa1ac011362326d5fdda20645fffb9de853b5c315143ea3d4df0bcb6dec927"}); - table65.AddRow(new string[] { + table111.AddRow(new string[] { "EVENT", "abcd", "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529"}); - table65.AddRow(new string[] { + table111.AddRow(new string[] { "EOSE", "abcd", ""}); #line 109 - testRunner.Then("Charlie receives messages", ((string)(null)), table65, "Then "); + testRunner.Then("Charlie receives messages", ((string)(null)), table111, "Then "); #line hidden } this.ScenarioCleanup(); @@ -591,69 +591,69 @@ public void ResubmissionOfDeletedEventIsRejected() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table66 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table112 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table66.AddRow(new string[] { + table112.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); - table66.AddRow(new string[] { + table112.AddRow(new string[] { "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "", "5", "[[\"e\", \"8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5\"]]", "1722337845"}); - table66.AddRow(new string[] { + table112.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 116 - testRunner.When("Alice publishes events", ((string)(null)), table66, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table112, "When "); #line hidden - TechTalk.SpecFlow.Table table67 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table113 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table67.AddRow(new string[] { + table113.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "1"}); #line 121 - testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table67, "And "); + testRunner.And("Bob sends a subscription request abcd", ((string)(null)), table113, "And "); #line hidden - TechTalk.SpecFlow.Table table68 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table114 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table68.AddRow(new string[] { + table114.AddRow(new string[] { "EOSE", "abcd"}); #line 124 - testRunner.Then("Bob receives messages", ((string)(null)), table68, "Then "); + testRunner.Then("Bob receives messages", ((string)(null)), table114, "Then "); #line hidden - TechTalk.SpecFlow.Table table69 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table115 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table69.AddRow(new string[] { + table115.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "true"}); - table69.AddRow(new string[] { + table115.AddRow(new string[] { "OK", "367ca4fcb31777b20fffc7057ca10e3f251322022b57fc4c123ecbf423f3b529", "true"}); - table69.AddRow(new string[] { + table115.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 127 - testRunner.And("Alice receives messages", ((string)(null)), table69, "And "); + testRunner.And("Alice receives messages", ((string)(null)), table115, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/11.feature.cs b/test/Netstr.Tests/NIPs/11.feature.cs index af9a104..62baeec 100644 --- a/test/Netstr.Tests/NIPs/11.feature.cs +++ b/test/Netstr.Tests/NIPs/11.feature.cs @@ -84,14 +84,14 @@ public virtual void FeatureBackground() #line 6 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table70 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table116 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table70.AddRow(new string[] { + table116.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "nsec12y4pgafw6kpcqjtfyrdyxtcupnddj5kdft768kdl55wzq50ervpqauqnw4"}); #line 7 - testRunner.And("Alice is connected to relay", ((string)(null)), table70, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table116, "And "); #line hidden } @@ -122,50 +122,50 @@ public void RelaySendsAnInformationDocument() #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table71 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table117 = new TechTalk.SpecFlow.Table(new string[] { "Header", "Value"}); - table71.AddRow(new string[] { + table117.AddRow(new string[] { "Accept", "application/nostr+json"}); #line 14 - testRunner.When("Alice sends a GET HTTP request to its websockets endpoint", ((string)(null)), table71, "When "); + testRunner.When("Alice sends a GET HTTP request to its websockets endpoint", ((string)(null)), table117, "When "); #line hidden - TechTalk.SpecFlow.Table table72 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table118 = new TechTalk.SpecFlow.Table(new string[] { "Header", "Value"}); - table72.AddRow(new string[] { + table118.AddRow(new string[] { "Access-Control-Allow-Origin", "*"}); #line 17 - testRunner.Then("Alice receives a response with headers", ((string)(null)), table72, "Then "); + testRunner.Then("Alice receives a response with headers", ((string)(null)), table118, "Then "); #line hidden - TechTalk.SpecFlow.Table table73 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table119 = new TechTalk.SpecFlow.Table(new string[] { "Field", "Type"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "name", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "description", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "contact", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "pubkey", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "software", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "version", "string"}); - table73.AddRow(new string[] { + table119.AddRow(new string[] { "supported_nips", "int[]"}); #line 20 - testRunner.And("Alice receives a response with json content", ((string)(null)), table73, "And "); + testRunner.And("Alice receives a response with json content", ((string)(null)), table119, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/119.feature.cs b/test/Netstr.Tests/NIPs/119.feature.cs index a595417..236f0a4 100644 --- a/test/Netstr.Tests/NIPs/119.feature.cs +++ b/test/Netstr.Tests/NIPs/119.feature.cs @@ -83,23 +83,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table74 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table120 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table74.AddRow(new string[] { + table120.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table74, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table120, "And "); #line hidden - TechTalk.SpecFlow.Table table75 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table75.AddRow(new string[] { + table121.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table75, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table121, "And "); #line hidden } @@ -130,77 +130,77 @@ public void TagFilterWithIsTreatedAsAND() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table76 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table76.AddRow(new string[] { + table122.AddRow(new string[] { "828a22e778269e7ba35ae7fa8b23d9506561700f176677f7a8dc7858282f4be3", "Cute cat", "1", "[[\"t\", \"meme\"], [\"t\", \"cat\"], [\"t\", \"black\"]]", "1722337838"}); - table76.AddRow(new string[] { + table122.AddRow(new string[] { "d711c1bdaf9fc9aa9a1b91580d98991531e95d22870817ba122d248b4151fde8", "Cute dog", "1", "[[\"t\", \"meme\"], [\"t\", \"dog\"], [\"t\", \"black\"]]", "1722337838"}); #line 15 - testRunner.When("Bob publishes events", ((string)(null)), table76, "When "); + testRunner.When("Bob publishes events", ((string)(null)), table122, "When "); #line hidden - TechTalk.SpecFlow.Table table77 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "&t", "#t"}); - table77.AddRow(new string[] { + table123.AddRow(new string[] { "1", "meme,cat", "black,white"}); #line 19 - testRunner.And("Alice sends a subscription request moarcats", ((string)(null)), table77, "And "); + testRunner.And("Alice sends a subscription request moarcats", ((string)(null)), table123, "And "); #line hidden - TechTalk.SpecFlow.Table table78 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table78.AddRow(new string[] { + table124.AddRow(new string[] { "dad216b3cebb2754fcef13dfd6299879cd2b4cb7988e38e36bc01874c90fab47", "Cute cat", "1", "[[\"t\", \"meme\"], [\"t\", \"cat\"], [\"t\", \"white\"]]", "1722337840"}); - table78.AddRow(new string[] { + table124.AddRow(new string[] { "a88cc99d717189d32aa5361386a0654a7b5a0c99f52e1377821bcf5302f64c76", "Cute dog", "1", "[[\"t\", \"meme\"], [\"t\", \"dog\"], [\"t\", \"white\"]]", "1722337840"}); #line 22 - testRunner.And("Bob publishes an event", ((string)(null)), table78, "And "); + testRunner.And("Bob publishes an event", ((string)(null)), table124, "And "); #line hidden - TechTalk.SpecFlow.Table table79 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId"}); - table79.AddRow(new string[] { + table125.AddRow(new string[] { "EVENT", "moarcats", "828a22e778269e7ba35ae7fa8b23d9506561700f176677f7a8dc7858282f4be3"}); - table79.AddRow(new string[] { + table125.AddRow(new string[] { "EOSE", "moarcats", ""}); - table79.AddRow(new string[] { + table125.AddRow(new string[] { "EVENT", "moarcats", "dad216b3cebb2754fcef13dfd6299879cd2b4cb7988e38e36bc01874c90fab47"}); #line 26 - testRunner.Then("Alice receives messages", ((string)(null)), table79, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table125, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/13.feature.cs b/test/Netstr.Tests/NIPs/13.feature.cs index 55efb23..29890f9 100644 --- a/test/Netstr.Tests/NIPs/13.feature.cs +++ b/test/Netstr.Tests/NIPs/13.feature.cs @@ -80,23 +80,23 @@ public virtual void FeatureBackground() { #line 5 #line hidden - TechTalk.SpecFlow.Table table80 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { "Key", "Value"}); - table80.AddRow(new string[] { + table126.AddRow(new string[] { "MinPowDifficulty", "20"}); #line 6 - testRunner.Given("a relay is running with options", ((string)(null)), table80, "Given "); + testRunner.Given("a relay is running with options", ((string)(null)), table126, "Given "); #line hidden - TechTalk.SpecFlow.Table table81 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table81.AddRow(new string[] { + table127.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 9 - testRunner.And("Alice is connected to relay", ((string)(null)), table81, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table127, "And "); #line hidden } @@ -130,61 +130,61 @@ public void MessagesWithLowDifficultyAndThoseOffTargetAreRejectedThoseWithHighAn #line 5 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table82 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table82.AddRow(new string[] { + table128.AddRow(new string[] { "00387d3bb57ceab60effbefffcaecff27614c60c75d7b36b01caa71249e3ca3c", "Hello", "[[\"nonce\", \"cc2e9737-e4f5-48d2-8c55-1461aeca3c87\"]]", "1", "1722337838"}); - table82.AddRow(new string[] { + table128.AddRow(new string[] { "0000017cb9da5d1295c5d9e902055c25280ae95ea6767ad89a02f928742b703d", "Hello", "[[\"nonce\", \"84fe8193-f35e-4d9e-9871-b509caaa6412\", \"5\"]]", "1", "1722337838"}); - table82.AddRow(new string[] { + table128.AddRow(new string[] { "00000ed0cf8d67d9cb4f5b211ad9c8daea5b7bbf7721e345070d98a91cc289ff", "Hello", "[[\"nonce\", \"49c7c782-8f45-4dbb-adac-5ebc71c3363c\"]]", "1", "1722337838"}); - table82.AddRow(new string[] { + table128.AddRow(new string[] { "000005e3b3172e58be368ed6b51b7ecf96a3d32b1107496bf6d786f8084aa17f", "Hello", "[[\"nonce\", \"045b7487-e889-4179-9d52-ce46beffef66\", \"21\"]]", "1", "1722337838"}); #line 18 - testRunner.When("Alice publishes events", ((string)(null)), table82, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table128, "When "); #line hidden - TechTalk.SpecFlow.Table table83 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table129 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table83.AddRow(new string[] { + table129.AddRow(new string[] { "OK", "00387d3bb57ceab60effbefffcaecff27614c60c75d7b36b01caa71249e3ca3c", "false"}); - table83.AddRow(new string[] { + table129.AddRow(new string[] { "OK", "0000017cb9da5d1295c5d9e902055c25280ae95ea6767ad89a02f928742b703d", "false"}); - table83.AddRow(new string[] { + table129.AddRow(new string[] { "OK", "00000ed0cf8d67d9cb4f5b211ad9c8daea5b7bbf7721e345070d98a91cc289ff", "true"}); - table83.AddRow(new string[] { + table129.AddRow(new string[] { "OK", "000005e3b3172e58be368ed6b51b7ecf96a3d32b1107496bf6d786f8084aa17f", "true"}); #line 24 - testRunner.Then("Alice receives messages", ((string)(null)), table83, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table129, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/17.feature.cs b/test/Netstr.Tests/NIPs/17.feature.cs index ee3b803..49a11fc 100644 --- a/test/Netstr.Tests/NIPs/17.feature.cs +++ b/test/Netstr.Tests/NIPs/17.feature.cs @@ -83,23 +83,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table84 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table130 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table84.AddRow(new string[] { + table130.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table84, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table130, "And "); #line hidden - TechTalk.SpecFlow.Table table85 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table131 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table85.AddRow(new string[] { + table131.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table85, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table131, "And "); #line hidden } @@ -129,29 +129,29 @@ public void NotAuthenticatedClientTriesToFetchKind1059Events() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table86 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table132 = new TechTalk.SpecFlow.Table(new string[] { "Authors", "Kinds"}); - table86.AddRow(new string[] { + table132.AddRow(new string[] { "", "1,1059"}); - table86.AddRow(new string[] { + table132.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", ""}); #line 15 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table86, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table132, "When "); #line hidden - TechTalk.SpecFlow.Table table87 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table133 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table87.AddRow(new string[] { + table133.AddRow(new string[] { "AUTH", "*"}); - table87.AddRow(new string[] { + table133.AddRow(new string[] { "CLOSED", "abcd"}); #line 19 - testRunner.Then("Alice receives messages", ((string)(null)), table87, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table133, "Then "); #line hidden } this.ScenarioCleanup(); @@ -182,87 +182,87 @@ public void AuthenticatedClientTriesToFetchKind1059Events() #line 26 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 table88 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table134 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table88.AddRow(new string[] { + table134.AddRow(new string[] { "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", "Secret", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table88.AddRow(new string[] { + table134.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "Charlie\'s Secret", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 27 - testRunner.And("Bob publishes events", ((string)(null)), table88, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table134, "And "); #line hidden - TechTalk.SpecFlow.Table table89 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table135 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table89.AddRow(new string[] { + table135.AddRow(new string[] { "1059"}); #line 31 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table89, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table135, "When "); #line hidden - TechTalk.SpecFlow.Table table90 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table136 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table90.AddRow(new string[] { + table136.AddRow(new string[] { "03403b4d4c4fad3ff1f561f030dff80daa256c66a4a195e3eb58bce90b2457bd", "Secret 2", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table90.AddRow(new string[] { + table136.AddRow(new string[] { "0e9391da7663a19e77d11966f57396a89a3a7bef1be1d045475e75be8eca246e", "Charlie\'s Secret 2", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 34 - testRunner.And("Bob publishes events", ((string)(null)), table90, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table136, "And "); #line hidden - TechTalk.SpecFlow.Table table91 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table137 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table91.AddRow(new string[] { + table137.AddRow(new string[] { "AUTH", "*", "", ""}); - table91.AddRow(new string[] { + table137.AddRow(new string[] { "OK", "*", "", "true"}); - table91.AddRow(new string[] { + table137.AddRow(new string[] { "EVENT", "abcd", "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", ""}); - table91.AddRow(new string[] { + table137.AddRow(new string[] { "EOSE", "abcd", "", ""}); - table91.AddRow(new string[] { + table137.AddRow(new string[] { "EVENT", "abcd", "03403b4d4c4fad3ff1f561f030dff80daa256c66a4a195e3eb58bce90b2457bd", ""}); #line 38 - testRunner.Then("Alice receives messages", ((string)(null)), table91, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table137, "Then "); #line hidden } this.ScenarioCleanup(); @@ -293,77 +293,77 @@ public void AuthenticatedClientTriesToFetchKind1059EventsThroughOtherFilters() #line 48 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 table92 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table138 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table92.AddRow(new string[] { + table138.AddRow(new string[] { "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", "Secret", "1059", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "1722337838"}); - table92.AddRow(new string[] { + table138.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "Charlie\'s Secret", "1059", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 49 - testRunner.And("Bob publishes events", ((string)(null)), table92, "And "); + testRunner.And("Bob publishes events", ((string)(null)), table138, "And "); #line hidden - TechTalk.SpecFlow.Table table93 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table139 = new TechTalk.SpecFlow.Table(new string[] { "Ids", "Authors", "Kinds"}); - table93.AddRow(new string[] { + table139.AddRow(new string[] { "", "", "1059"}); - table93.AddRow(new string[] { + table139.AddRow(new string[] { "fb90964eba126b74bc71bf31e9e198dc4fbdd79e3de4d4f02dacddbe8a6ac71c", "", ""}); - table93.AddRow(new string[] { + table139.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f611059", ""}); - table93.AddRow(new string[] { + table139.AddRow(new string[] { "", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f611059", "1059"}); #line 53 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table93, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table139, "When "); #line hidden - TechTalk.SpecFlow.Table table94 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table140 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "EventId", "Success"}); - table94.AddRow(new string[] { + table140.AddRow(new string[] { "AUTH", "*", "", ""}); - table94.AddRow(new string[] { + table140.AddRow(new string[] { "OK", "*", "", "true"}); - table94.AddRow(new string[] { + table140.AddRow(new string[] { "EVENT", "abcd", "ff526515d15975c3839f027cd301ba49afca237fa0d84f53765e9c320a269d90", ""}); - table94.AddRow(new string[] { + table140.AddRow(new string[] { "EOSE", "abcd", "", ""}); #line 59 - testRunner.Then("Alice receives messages", ((string)(null)), table94, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table140, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/40.feature.cs b/test/Netstr.Tests/NIPs/40.feature.cs index bdf5ba6..f2a93aa 100644 --- a/test/Netstr.Tests/NIPs/40.feature.cs +++ b/test/Netstr.Tests/NIPs/40.feature.cs @@ -84,23 +84,23 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table95 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table141 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table95.AddRow(new string[] { + table141.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table95, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table141, "And "); #line hidden - TechTalk.SpecFlow.Table table96 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table142 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table96.AddRow(new string[] { + table142.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table96, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table142, "And "); #line hidden } @@ -131,31 +131,31 @@ public void UnparsableExpirationTagIsIgnored() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table97 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table143 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table97.AddRow(new string[] { + table143.AddRow(new string[] { "0921e0c46e637526c0cb2211cbab49a56a69373b0f86c2500ed530f1533df182", "Test", "1", "[[\"expiration\",\"blah\"]]", "1722337838"}); #line 15 - testRunner.When("Alice publishes events", ((string)(null)), table97, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table143, "When "); #line hidden - TechTalk.SpecFlow.Table table98 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table144 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table98.AddRow(new string[] { + table144.AddRow(new string[] { "OK", "0921e0c46e637526c0cb2211cbab49a56a69373b0f86c2500ed530f1533df182", "true"}); #line 18 - testRunner.Then("Alice receives messages", ((string)(null)), table98, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table144, "Then "); #line hidden } this.ScenarioCleanup(); @@ -183,31 +183,31 @@ public void AlreadyExpiredEventIsRejected() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table99 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table145 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table99.AddRow(new string[] { + table145.AddRow(new string[] { "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "Test", "1", "[[\"expiration\",\"1231002905\"]]", "1722337838"}); #line 24 - testRunner.When("Alice publishes events", ((string)(null)), table99, "When "); + testRunner.When("Alice publishes events", ((string)(null)), table145, "When "); #line hidden - TechTalk.SpecFlow.Table table100 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table146 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table100.AddRow(new string[] { + table146.AddRow(new string[] { "OK", "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "false"}); #line 27 - testRunner.Then("Alice receives messages", ((string)(null)), table100, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table146, "Then "); #line hidden } this.ScenarioCleanup(); @@ -236,36 +236,36 @@ public void ExpiredEventAlreadySavedInARelayIsOmittedFromSubResponse() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table101 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table147 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table101.AddRow(new string[] { + table147.AddRow(new string[] { "4239479a101dbeb8f189dacd6e4638a11013b5a2fc0733901f83c9e84e611778", "Test", "1", "[[\"expiration\",\"1231002905\"]]", "1722337838"}); #line 34 - testRunner.Given("Bob previously published events", ((string)(null)), table101, "Given "); + testRunner.Given("Bob previously published events", ((string)(null)), table147, "Given "); #line hidden - TechTalk.SpecFlow.Table table102 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table148 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table102.AddRow(new string[] { + table148.AddRow(new string[] { "1"}); #line 37 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table102, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table148, "When "); #line hidden - TechTalk.SpecFlow.Table table103 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table149 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id"}); - table103.AddRow(new string[] { + table149.AddRow(new string[] { "EOSE", "abcd"}); #line 40 - testRunner.Then("Alice receives messages", ((string)(null)), table103, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table149, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/42.feature.cs b/test/Netstr.Tests/NIPs/42.feature.cs index 9007d9a..a130c9c 100644 --- a/test/Netstr.Tests/NIPs/42.feature.cs +++ b/test/Netstr.Tests/NIPs/42.feature.cs @@ -83,14 +83,14 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH required", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table104 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table150 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table104.AddRow(new string[] { + table150.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table104, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table150, "And "); #line hidden } @@ -120,46 +120,46 @@ public void NotAuthenticatedClientCannotPublishOrSubscribe() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table105 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table151 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table105.AddRow(new string[] { + table151.AddRow(new string[] { "1"}); #line 11 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table105, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table151, "When "); #line hidden - TechTalk.SpecFlow.Table table106 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table152 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table106.AddRow(new string[] { + table152.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 14 - testRunner.And("Alice publishes events", ((string)(null)), table106, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table152, "And "); #line hidden - TechTalk.SpecFlow.Table table107 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table153 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table107.AddRow(new string[] { + table153.AddRow(new string[] { "AUTH", "*", ""}); - table107.AddRow(new string[] { + table153.AddRow(new string[] { "CLOSED", "abcd", ""}); - table107.AddRow(new string[] { + table153.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 17 - testRunner.Then("Alice receives messages", ((string)(null)), table107, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table153, "Then "); #line hidden } this.ScenarioCleanup(); @@ -189,50 +189,50 @@ public void AuthenticatedClientCanPublishAndSubscribe() #line 24 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 table108 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table154 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table108.AddRow(new string[] { + table154.AddRow(new string[] { "2"}); #line 25 - testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table108, "And "); + testRunner.And("Alice sends a subscription request abcd", ((string)(null)), table154, "And "); #line hidden - TechTalk.SpecFlow.Table table109 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table155 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table109.AddRow(new string[] { + table155.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 28 - testRunner.And("Alice publishes events", ((string)(null)), table109, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table155, "And "); #line hidden - TechTalk.SpecFlow.Table table110 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table156 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table110.AddRow(new string[] { + table156.AddRow(new string[] { "AUTH", "*", ""}); - table110.AddRow(new string[] { + table156.AddRow(new string[] { "OK", "*", "true"}); - table110.AddRow(new string[] { + table156.AddRow(new string[] { "EOSE", "abcd", ""}); - table110.AddRow(new string[] { + table156.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "true"}); #line 31 - testRunner.Then("Alice receives messages", ((string)(null)), table110, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table156, "Then "); #line hidden } this.ScenarioCleanup(); @@ -262,50 +262,50 @@ public void ClientStaysUnauthenticatedWhenInvalidChallengeIsUsed() #line 39 testRunner.When("Alice publishes an AUTH event with invalid challenge", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line hidden - TechTalk.SpecFlow.Table table111 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table157 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table111.AddRow(new string[] { + table157.AddRow(new string[] { "1"}); #line 40 - testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table111, "When "); + testRunner.When("Alice sends a subscription request abcd", ((string)(null)), table157, "When "); #line hidden - TechTalk.SpecFlow.Table table112 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table158 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table112.AddRow(new string[] { + table158.AddRow(new string[] { "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "Hello", "1", "", "1722337838"}); #line 43 - testRunner.And("Alice publishes events", ((string)(null)), table112, "And "); + testRunner.And("Alice publishes events", ((string)(null)), table158, "And "); #line hidden - TechTalk.SpecFlow.Table table113 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table159 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success"}); - table113.AddRow(new string[] { + table159.AddRow(new string[] { "AUTH", "*", ""}); - table113.AddRow(new string[] { + table159.AddRow(new string[] { "OK", "*", "false"}); - table113.AddRow(new string[] { + table159.AddRow(new string[] { "CLOSED", "abcd", ""}); - table113.AddRow(new string[] { + table159.AddRow(new string[] { "OK", "8ed8cc390eaf6db9e0ae8f3bf720a80d81ae49f95f953a9a4e26a72dc7f4a2c5", "false"}); #line 46 - testRunner.Then("Alice receives messages", ((string)(null)), table113, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table159, "Then "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/45.feature.cs b/test/Netstr.Tests/NIPs/45.feature.cs index cf15371..9a7f686 100644 --- a/test/Netstr.Tests/NIPs/45.feature.cs +++ b/test/Netstr.Tests/NIPs/45.feature.cs @@ -83,32 +83,32 @@ public virtual void FeatureBackground() #line 5 testRunner.Given("a relay is running with AUTH enabled", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line hidden - TechTalk.SpecFlow.Table table114 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table160 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table114.AddRow(new string[] { + table160.AddRow(new string[] { "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75", "512a14752ed58380496920da432f1c0cdad952cd4afda3d9bfa51c2051f91b02"}); #line 6 - testRunner.And("Alice is connected to relay", ((string)(null)), table114, "And "); + testRunner.And("Alice is connected to relay", ((string)(null)), table160, "And "); #line hidden - TechTalk.SpecFlow.Table table115 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table161 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table115.AddRow(new string[] { + table161.AddRow(new string[] { "5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627", "3551fc7617f76632e4542992c0bc01fecb224de639c4b6a1e0956946e8bb8a29"}); #line 9 - testRunner.And("Bob is connected to relay", ((string)(null)), table115, "And "); + testRunner.And("Bob is connected to relay", ((string)(null)), table161, "And "); #line hidden - TechTalk.SpecFlow.Table table116 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table162 = new TechTalk.SpecFlow.Table(new string[] { "PublicKey", "PrivateKey"}); - table116.AddRow(new string[] { + table162.AddRow(new string[] { "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614", "f77f81a6a223eb15f81fee569161a4f729401a9cbc31bb69fef6a949b9d3c23a"}); #line 12 - testRunner.And("Charlie is connected to relay", ((string)(null)), table116, "And "); + testRunner.And("Charlie is connected to relay", ((string)(null)), table162, "And "); #line hidden } @@ -138,59 +138,59 @@ public void CountingFollowers() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table117 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table163 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table117.AddRow(new string[] { + table163.AddRow(new string[] { "d589498c49776340a9bf83f63cc4cf960a17360cc3d9fd2a2ec2de4f11ba82b4", "", "[[\"p\",\"5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75\"]]", "3", "1722337838"}); #line 18 - testRunner.When("Bob publishes an event", ((string)(null)), table117, "When "); + testRunner.When("Bob publishes an event", ((string)(null)), table163, "When "); #line hidden - TechTalk.SpecFlow.Table table118 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table164 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Tags", "Kind", "CreatedAt"}); - table118.AddRow(new string[] { + table164.AddRow(new string[] { "2ef0ecd7341f5fdb5634210a4505d1c4ba25cb6ff4721282fd45412f93842c66", "", "[[\"p\",\"5bc683a5d12133a96ac5502c15fe1c2287986cff7baf6283600360e6bb01f627\"]]", "3", "1722337838"}); #line 21 - testRunner.And("Charlie publishes an event", ((string)(null)), table118, "And "); + testRunner.And("Charlie publishes an event", ((string)(null)), table164, "And "); #line hidden - TechTalk.SpecFlow.Table table119 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table165 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table119.AddRow(new string[] { + table165.AddRow(new string[] { "3", "5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75"}); #line 24 - testRunner.And("Alice sends a count message abcd", ((string)(null)), table119, "And "); + testRunner.And("Alice sends a count message abcd", ((string)(null)), table165, "And "); #line hidden - TechTalk.SpecFlow.Table table120 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table166 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Count"}); - table120.AddRow(new string[] { + table166.AddRow(new string[] { "AUTH", "*", ""}); - table120.AddRow(new string[] { + table166.AddRow(new string[] { "COUNT", "abcd", "1"}); #line 27 - testRunner.Then("Alice receives a message", ((string)(null)), table120, "Then "); + testRunner.Then("Alice receives a message", ((string)(null)), table166, "Then "); #line hidden } this.ScenarioCleanup(); @@ -217,27 +217,27 @@ public void CountingDMsIsRejectedWhenNotAuthenticated() #line 4 this.FeatureBackground(); #line hidden - TechTalk.SpecFlow.Table table121 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table167 = new TechTalk.SpecFlow.Table(new string[] { "Kinds"}); - table121.AddRow(new string[] { + table167.AddRow(new string[] { "4"}); #line 33 - testRunner.When("Alice sends a count message abcd", ((string)(null)), table121, "When "); + testRunner.When("Alice sends a count message abcd", ((string)(null)), table167, "When "); #line hidden - TechTalk.SpecFlow.Table table122 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table168 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Count"}); - table122.AddRow(new string[] { + table168.AddRow(new string[] { "AUTH", "*", ""}); - table122.AddRow(new string[] { + table168.AddRow(new string[] { "CLOSED", "abcd", ""}); #line 36 - testRunner.Then("Alice receives a message", ((string)(null)), table122, "Then "); + testRunner.Then("Alice receives a message", ((string)(null)), table168, "Then "); #line hidden } this.ScenarioCleanup(); @@ -272,104 +272,104 @@ public void CountingSomeoneElsesDMsReturnsOnlyThoseFromMe() #line 47 testRunner.And("Charlie publishes an AUTH event for the challenge sent by relay", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); #line hidden - TechTalk.SpecFlow.Table table123 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table169 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table123.AddRow(new string[] { + table169.AddRow(new string[] { "a8b0f9d313888642257af20fc4dbe4a3d71d3c3a72bcfc06c540a235172b7f37", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 48 - testRunner.And("Bob publishes an event", ((string)(null)), table123, "And "); + testRunner.And("Bob publishes an event", ((string)(null)), table169, "And "); #line hidden - TechTalk.SpecFlow.Table table124 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table170 = new TechTalk.SpecFlow.Table(new string[] { "Id", "Content", "Kind", "Tags", "CreatedAt"}); - table124.AddRow(new string[] { + table170.AddRow(new string[] { "7b0535b94878efb18b7c7a13630db8227e30961aed6f5556823b612423d676af", "Charlie\'s Secret", "4", "[[\"p\",\"fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614\"]]", "1722337838"}); #line 51 - testRunner.And("Alice publishes an event", ((string)(null)), table124, "And "); + testRunner.And("Alice publishes an event", ((string)(null)), table170, "And "); #line hidden - TechTalk.SpecFlow.Table table125 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table171 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table125.AddRow(new string[] { + table171.AddRow(new string[] { "4", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"}); #line 54 - testRunner.And("Alice sends a count message abcd", ((string)(null)), table125, "And "); + testRunner.And("Alice sends a count message abcd", ((string)(null)), table171, "And "); #line hidden - TechTalk.SpecFlow.Table table126 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table172 = new TechTalk.SpecFlow.Table(new string[] { "Kinds", "#p"}); - table126.AddRow(new string[] { + table172.AddRow(new string[] { "4", "fe8d7a5726ea97ce6140f9fb06b1fe7d3259bcbf8de42c2a5d2ec9f8f0e2f614"}); #line 57 - testRunner.And("Charlie sends a count message abcd", ((string)(null)), table126, "And "); + testRunner.And("Charlie sends a count message abcd", ((string)(null)), table172, "And "); #line hidden - TechTalk.SpecFlow.Table table127 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table173 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success", "Count"}); - table127.AddRow(new string[] { + table173.AddRow(new string[] { "AUTH", "*", "", ""}); - table127.AddRow(new string[] { + table173.AddRow(new string[] { "OK", "*", "true", ""}); - table127.AddRow(new string[] { + table173.AddRow(new string[] { "OK", "7b0535b94878efb18b7c7a13630db8227e30961aed6f5556823b612423d676af", "true", ""}); - table127.AddRow(new string[] { + table173.AddRow(new string[] { "COUNT", "abcd", "", "1"}); #line 60 - testRunner.Then("Alice receives messages", ((string)(null)), table127, "Then "); + testRunner.Then("Alice receives messages", ((string)(null)), table173, "Then "); #line hidden - TechTalk.SpecFlow.Table table128 = new TechTalk.SpecFlow.Table(new string[] { + TechTalk.SpecFlow.Table table174 = new TechTalk.SpecFlow.Table(new string[] { "Type", "Id", "Success", "Count"}); - table128.AddRow(new string[] { + table174.AddRow(new string[] { "AUTH", "*", "", ""}); - table128.AddRow(new string[] { + table174.AddRow(new string[] { "OK", "*", "true", ""}); - table128.AddRow(new string[] { + table174.AddRow(new string[] { "COUNT", "abcd", "", "2"}); #line 66 - testRunner.And("Charlie receives messages", ((string)(null)), table128, "And "); + testRunner.And("Charlie receives messages", ((string)(null)), table174, "And "); #line hidden } this.ScenarioCleanup(); diff --git a/test/Netstr.Tests/NIPs/51.feature b/test/Netstr.Tests/NIPs/51.feature index fb29494..d4511da 100644 --- a/test/Netstr.Tests/NIPs/51.feature +++ b/test/Netstr.Tests/NIPs/51.feature @@ -1,72 +1,154 @@ -Feature: NIP-51 Lists - Tests for NIP-51 Lists implementation - - Background: - Given a relay at "wss://localhost:5001" - And a user Alice - And Alice is connected to the relay - - Scenario: Create and retrieve a public mute list - When Alice publishes an event with kind 10000 and tags: - | p | 07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9 | - | p | a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4 | - Then the relay accepts the event - When Alice subscribes to events with kind 10000 - Then Alice receives 1 event - And the event has 2 "p" tags - - Scenario: Create and retrieve a private mute list - When Alice publishes an event with kind 10000 and encrypted content and tags: - | p | 07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9 | - Then the relay accepts the event - When Alice subscribes to events with kind 10000 - Then Alice receives 1 event - And the event has encrypted content - And the event has 1 "p" tag - - Scenario: Create and retrieve a bookmark set - When Alice publishes an event with kind 30003 and tags: - | d | my-bookmarks | - | name | Programming Resources | - | about | Collection of useful programming articles and tutorials | - | e | d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e | - | a | 30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3 | - Then the relay accepts the event - When Alice subscribes to events with kind 30003 - Then Alice receives 1 event - And the event has tag "d" with value "my-bookmarks" - And the event has tag "name" with value "Programming Resources" - - Scenario: Create and retrieve an emoji set - When Alice publishes an event with kind 30030 and tags: - | d | custom-emojis | - | name | My Custom Emojis | - | emoji | happy,https://example.com/happy.png | - | emoji | sad,https://example.com/sad.png | - Then the relay accepts the event - When Alice subscribes to events with kind 30030 - Then Alice receives 1 event - And the event has 2 "emoji" tags - - Scenario: Create and retrieve a relay set - When Alice publishes an event with kind 30002 and tags: - | d | my-relays | - | name | Primary Relays | - | about | My main relay connections | - | relay | wss://relay1.example.com | - | relay | wss://relay2.example.com | - Then the relay accepts the event - When Alice subscribes to events with kind 30002 - Then Alice receives 1 event - And the event has 2 "relay" tags - - Scenario: Create and retrieve a kind mute set - When Alice publishes an event with kind 30007 and tags: - | d | 1 | - | p | 07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9 | - | p | a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4 | - Then the relay accepts the event - When Alice subscribes to events with kind 30007 - Then Alice receives 1 event - And the event has tag "d" with value "1" - And the event has 2 "p" tags +Feature: NIP-51 + Standard lists (kinds 10000-10999) are replaceable per author. + Sets (kinds 30000-30999) are addressable and require a "d" tag identifier. + +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 | + +# Mute List (10000) +Scenario: Create public mute list with p tags + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 1111111111111111111111111111111111111111111111111111111111111111 | * | 10000 | [["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],["p","a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 1111111111111111111111111111111111111111111111111111111111111111 | true | + +Scenario: Create mute list with hashtag and word tags + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 2222222222222222222222222222222222222222222222222222222222222222 | * | 10000 | [["t","spam"],["word","scam"],["word","rugpull"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 2222222222222222222222222222222222222222222222222222222222222222 | true | + +Scenario: Query mute list by author + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 3333333333333333333333333333333333333333333333333333333333333333 | * | 10000 | [["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"]] | 1722337838 | + And Bob sends a subscription request mute_sub + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 10000 | + Then Bob receives messages + | Type | Id | EventId | + | EVENT | mute_sub | 3333333333333333333333333333333333333333333333333333333333333333 | + | EOSE | mute_sub | | + +# Bookmarks (10003) +Scenario: Create bookmarks with event and article references + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 4444444444444444444444444444444444444444444444444444444444444444 | * | 10003 | [["e","d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"],["a","30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 4444444444444444444444444444444444444444444444444444444444444444 | true | + +# Blocked Relays (10006) +Scenario: Create blocked relays list + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 5555555555555555555555555555555555555555555555555555555555555555 | * | 10006 | [["relay","wss://badrelay1.com"],["relay","wss://badrelay2.com"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 5555555555555555555555555555555555555555555555555555555555555555 | true | + +# Interests (10015) +Scenario: Create interests list + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 6666666666666666666666666666666666666666666666666666666666666666 | * | 10015 | [["t","bitcoin"],["t","nostr"],["t","programming"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 6666666666666666666666666666666666666666666666666666666666666666 | true | + +# Emoji list (10030) +Scenario: Create emoji list with emoji tags + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 7777777777777777777777777777777777777777777777777777777777777777 | * | 10030 | [["emoji","happy","https://example.com/happy.png"],["emoji","sad","https://example.com/sad.png"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 7777777777777777777777777777777777777777777777777777777777777777 | true | + +# Follow Sets (30000) - Addressable, requires d tag +Scenario: Create follow set with d tag + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 8888888888888888888888888888888888888888888888888888888888888888 | * | 30000 | [["d","friends"],["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],["p","a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | 8888888888888888888888888888888888888888888888888888888888888888 | true | + +Scenario: Reject follow set without d tag + Sets require a d tag identifier. + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | 9999999999999999999999999999999999999999999999999999999999999999 | * | 30000 | [["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | Message | + | OK | 9999999999999999999999999999999999999999999999999999999999999999 | false | * | + +# Relay Sets (30002) +Scenario: Create relay set with d tag + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | * | 30002 | [["d","my-relays"],["relay","wss://relay1.example.com"],["relay","wss://relay2.example.com"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | true | + +# Bookmark Sets (30003) +Scenario: Create bookmark set with d tag + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | * | 30003 | [["d","programming"],["e","d78ba0d5dce22bfff9db0a9e996c9ef27e2c91051de0c4e1da340e0326b4941e"],["a","30023:26dc95542e18b8b7aec2f14610f55c335abebec76f3db9e58c254661d0593a0c:95ODQzw3"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | true | + +# Kind Mute Sets (30007) +Scenario: Create kind mute set with d tag as kind number + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc | * | 30007 | [["d","1"],["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],["p","a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc | true | + +# Interest Sets (30015) +Scenario: Create interest set with d tag + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd | * | 30015 | [["d","tech"],["t","bitcoin"],["t","programming"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd | true | + +# Emoji Sets (30030) +Scenario: Create emoji set with d tag + When Alice publishes an event + | Id | Content | Kind | Tags | CreatedAt | + | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | * | 30030 | [["d","reactions"],["emoji","thumbsup","https://example.com/thumbsup.png"],["emoji","fire","https://example.com/fire.png"]] | 1722337838 | + Then Alice receives a message + | Type | Id | Success | + | OK | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | true | + +# Addressable events are replaced by d tag +Scenario: Update addressable list replaces previous with same d tag + When Alice publishes events + | Id | Content | Kind | Tags | CreatedAt | + | f111111111111111111111111111111111111111111111111111111111111111 | * | 30000 | [["d","friends"],["p","07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"]] | 1722337838 | + | f222222222222222222222222222222222222222222222222222222222222222 | * | 30000 | [["d","friends"],["p","a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"]] | 1722337848 | + And Bob sends a subscription request set_sub + | Authors | Kinds | + | 5758137ec7f38f3d6c3ef103e28cd9312652285dab3497fe5e5f6c5c0ef45e75 | 30000 | + Then Bob receives messages + | Type | Id | EventId | + | EVENT | set_sub | f222222222222222222222222222222222222222222222222222222222222222 | + | EOSE | set_sub | | diff --git a/test/Netstr.Tests/NIPs/51.feature.cs b/test/Netstr.Tests/NIPs/51.feature.cs index ac8b612..2c12dc3 100644 --- a/test/Netstr.Tests/NIPs/51.feature.cs +++ b/test/Netstr.Tests/NIPs/51.feature.cs @@ -19,7 +19,7 @@ namespace Netstr.Tests.NIPs [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public partial class NIP_51ListsFeature : object, Xunit.IClassFixture, System.IDisposable + public partial class NIP_51Feature : object, Xunit.IClassFixture, System.IDisposable { private static TechTalk.SpecFlow.ITestRunner testRunner; @@ -31,7 +31,7 @@ public partial class NIP_51ListsFeature : object, Xunit.IClassFixture