diff --git a/schema/schema.go b/schema/schema.go index 0b8a476..691f162 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -3,7 +3,10 @@ package schema import ( _ "embed" "fmt" + "io" + "net/http" "net/url" + "os" "slices" "strconv" "strings" @@ -15,8 +18,26 @@ import ( "gopkg.in/yaml.v3" ) -//go:embed schema.yaml -var schemaFile []byte +const DefaultSchemaURL = "https://raw.githubusercontent.com/nostr-protocol/registry-of-kinds/refs/heads/master/schema.yaml" + +func fetchSchemaFromURL(schemaURL string) (string, error) { + resp, err := http.Get(schemaURL) + if err != nil { + return "", fmt.Errorf("failed to fetch schema from URL: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch schema: HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read schema response: %w", err) + } + + return string(body), nil +} type Schema map[string]KindSchema @@ -44,17 +65,32 @@ type Validator struct { FailOnUnknown bool } -func NewValidator(schemaData string) Validator { +func NewValidatorFromBytes(schemaData []byte) (Validator, error) { schema := make(Schema) - if err := yaml.Unmarshal([]byte(schemaData), &schema); err != nil { - panic(fmt.Errorf("failed to parse schema.yaml: %w", err)) + if err := yaml.Unmarshal(schemaData, &schema); err != nil { + return Validator{}, fmt.Errorf("failed to parse schema: %w", err) } - - return Validator{Schema: schema} + return Validator{Schema: schema}, nil } -func NewDefaultValidator() Validator { - return NewValidator(string(schemaFile)) +func NewValidatorFromSchema(sch Schema) Validator { + return Validator{Schema: sch} +} + +func NewValidatorFromFile(filename string) (Validator, error) { + data, err := os.ReadFile(filename) + if err != nil { + return Validator{}, fmt.Errorf("failed to read schema file: %w", err) + } + return NewValidatorFromBytes(data) +} + +func NewValidatorFromURL(schemaURL string) (Validator, error) { + schemaData, err := fetchSchemaFromURL(schemaURL) + if err != nil { + return Validator{}, err + } + return NewValidatorFromBytes([]byte(schemaData)) } var ( diff --git a/schema/schema.yaml b/schema/schema.yaml deleted file mode 100644 index eff7ff1..0000000 --- a/schema/schema.yaml +++ /dev/null @@ -1,1593 +0,0 @@ -_profile: &profile - type: pubkey - required: true - next: - type: relay - -_event: &event - type: id - required: true - next: - type: relay - next: - type: pubkey - -_addr: &addr - type: addr - required: true - next: - type: relay - -_kind: &kind - type: kind - required: true - -_dtag: &dtag - name: d - next: - type: free - required: true - -_atag: &atag - name: a - next: *addr - -_ptag: &ptag - name: p - next: *profile - -_etag: &etag - name: e - next: *event - -0: - description: User Metadata - content: json - -1: - description: Short Text Note - content: free - tags: - - - name: e - next: - type: id - required: true - next: - type: relay - next: - type: constrained - either: - - reply - - root - next: - type: pubkey - - - name: q - next: - type: id - required: true - next: - type: relay - next: - type: pubkey - - - name: q - next: - type: addr - required: true - next: - type: relay - - - name: p - next: *profile - - - name: a - next: *addr - - - name: subject - next: - type: free - required: true - - - name: L - next: - type: free - required: true - - - name: l - next: - type: free - required: true - next: - type: free - -2: - description: Recommend Relay - content: free - deprecated: true - -3: - description: Follows - content: json - tags: - - *ptag - -4: - description: Encrypted Direct Messages - content: free - tags: - - *ptag - -5: - description: Event Deletion Request - content: empty - tags: - - *etag - -6: - description: Repost - content: empty - tags: - - *etag - - *ptag - -7: - content: free - tags: - - *etag - - *ptag - -8: - description: Badge Award - content: empty - tags: - - *atag - - *ptag - -9: - description: Chat Message - content: free - tags: - - - name: e - next: - type: id - required: true - next: - type: relay - next: - type: constrained - either: - - root - next: - type: pubkey - - *ptag - -13: - description: Seal - content: free - -14: - description: Direct Message - content: free - tags: - - *ptag - - *etag - - - name: subject - next: - type: free - required: true - -15: - description: File Message - content: free - tags: - - *ptag - - - name: e - next: - type: id - required: true - next: - type: relay - next: - type: constrained - either: - - reply - - - name: subject - next: - type: free - required: true - - - name: file-type - next: - type: free - required: true - - - name: encryption-algorithm - next: - type: free - required: true - - - name: decryption-key - next: - type: free - required: true - - - name: decryption-nonce - next: - type: free - required: true - - - name: x - next: - type: free - required: true - -16: - description: Generic Repost - content: empty - tags: - - *etag - - *ptag - - - name: k - next: *kind - -17: - description: Reaction to a website - content: free - tags: - - - name: r - next: - type: url - required: true - -40: - description: Channel Creation - content: json - -41: - description: Channel Metadata - content: json - tags: - - - name: e - next: - type: id - required: true - next: - type: relay - next: - type: constrained - either: - - root - - - name: t - next: - type: free - required: true - -42: - description: Channel Message - content: free - tags: - - - name: e - next: - type: id - required: true - next: - type: relay - next: - type: constrained - either: - - root - - reply - - *ptag - -43: - description: Channel Hide Message - content: json - tags: - - - name: e - next: - type: id - required: true - -44: - description: Channel Mute User - content: json - tags: - - *ptag - -64: - description: Chess (PGN) - content: free - tags: - - - name: alt - next: - type: free - required: true - -818: - description: Merge Requests - content: free - tags: - - *atag - - *ptag - -1018: - description: Poll Response - content: empty - tags: - - - name: e - next: - type: id - required: true - - - name: response - next: - type: free - required: true - -1021: - description: Bid - content: free - tags: - - - name: e - next: - type: id - required: true - -1022: - description: Bid confirmation - content: json - tags: - - - name: e - next: - type: id - required: true - -1040: - description: OpenTimestamps - content: free - -1222: - description: Voice Message - content: free - -1244: - description: Voice Message Comment - content: free - -1311: - description: Live Chat Message - content: free - -1337: - description: Code Snippet - content: free - -1622: - description: Git Replies (deprecated) - content: free - deprecated: true - -1971: - description: Problem Tracker - content: free - -1986: - description: Relay reviews - content: free - -1987: - description: AI Embeddings / Vector lists - content: free - -2003: - description: Torrent - content: free - -2004: - description: Torrent Comment - content: free - -2022: - description: Coinjoin Pool - content: free - -4550: - description: Community Post Approval - content: free - -7374: - description: Reserved Cashu Wallet Tokens - content: free - -7375: - description: Cashu Wallet Tokens - content: free - -7376: - description: Cashu Wallet History - content: free - -7516: - description: Geocache log - content: free - -7517: - description: Geocache proof of find - content: free - -9321: - description: Nutzap - content: free - -9467: - description: Tidal login - content: free - -1059: - description: Gift Wrap - content: free - tags: - - *ptag - -1063: - description: File Metadata - content: free - tags: - - - name: url - next: - type: url - required: true - - - name: m - next: - type: free - required: true - - - name: x - next: - type: free - required: true - - - name: ox - next: - type: free - - - name: size - next: - type: free - - - name: dim - next: - type: free - - - name: magnet - next: - type: url - - - name: i - next: - type: free - - - name: blurhash - next: - type: free - - - name: thumb - next: - type: url - next: - type: free - - - name: image - next: - type: url - next: - type: free - - - name: summary - next: - type: free - - - name: alt - next: - type: free - - - name: fallback - next: - type: url - - - name: service - next: - type: free - -1068: - description: Poll - content: free - tags: - - - name: option - next: - type: free - required: true - next: - type: free - required: true - - - name: relay - next: - type: relay - required: true - - - name: polltype - next: - type: constrained - either: - - singlechoice - - multiplechoice - - - name: endsAt - next: - type: free - required: true - -1621: - description: Issues - content: free - tags: - - *atag - - *ptag - - - name: subject - next: - type: free - required: true - - - name: t - next: - type: free - required: true - -1984: - description: Reporting - content: free - tags: - - *ptag - - *etag - - *atag - - - name: L - next: - type: free - required: true - - - name: l - next: - type: free - required: true - next: - type: free - -1985: - description: Label - content: free - tags: - - *ptag - - *etag - - *atag - - - name: L - next: - type: free - required: true - - - name: l - next: - type: free - required: true - next: - type: free - -9041: - description: Zap Goal - content: free - tags: - - - name: amount - next: - type: free - required: true - - - name: relays - next: - type: relay - variadic: true - required: true - - - name: closed_at - next: - type: free - - - name: image - next: - type: url - - - name: summary - next: - type: free - - - name: r - next: - type: url - - *atag - - - name: zap - next: - type: pubkey - required: true - next: - type: relay - next: - type: free - -9734: - description: Zap Request - content: free - tags: - - - name: relays - next: - type: relay - variadic: true - required: true - - - name: amount - next: - type: free - - - name: lnurl - next: - type: free - - *ptag - - *etag - - *atag - - - name: k - next: *kind - -9735: - description: Zap - content: empty - tags: - - *ptag - - - name: P - next: - type: pubkey - required: true - - *etag - - - name: k - next: *kind - - - name: bolt11 - next: - type: free - required: true - - - name: description - next: - type: free - required: true - - - name: preimage - next: - type: free - -1111: - description: Comment - content: free - tags: - - - name: A - next: *addr - - - name: a - next: *addr - - - name: E - next: *event - - - name: e - next: *event - - - name: I - next: &external - type: free - required: true - next: - type: url - - - name: i - next: *external - - - name: K - next: *kind - - - name: K - next: - type: free - required: true - - - name: k - next: *kind - - - name: P - next: *profile - - - name: p - next: *profile - -10002: - description: Relay List Metadata - content: empty - tags: - - - name: r - next: - type: relay - required: true - next: - type: constrained - either: - - read - - write - -9802: - description: Highlights - content: free - tags: - - - name: p - next: *profile - - *etag - - - name: a - next: *addr - - - name: r - next: - type: url - required: true - - - name: context - next: - type: free - - - name: comment - next: - type: free - -10050: - description: Relay list to receive DMs - content: empty - tags: - - - name: relay - next: - type: relay - required: true - -27235: - description: HTTP Auth - content: empty - tags: - - - name: u - next: - type: url - required: true - - - name: method - next: - type: constrained - either: - - GET - - POST - - PUT - - DELETE - required: true - -10000: - description: Mute list - content: empty - -10001: - description: Pin list - content: empty - -10003: - description: Bookmark list - content: empty - -10004: - description: Communities list - content: empty - -10005: - description: Public chats list - content: empty - -10006: - description: Blocked relays list - content: empty - -10007: - description: Search relays list - content: empty - -10009: - description: User groups - content: empty - -10012: - description: Favorite relays list - content: empty - -10013: - description: Private event relay list - content: empty - -10015: - description: Interests list - content: empty - -10019: - description: Nutzap Mint Recommendation - content: empty - -10020: - description: Media follows - content: empty - -10030: - description: User emoji list - content: empty - -10063: - description: User server list - content: empty - -10096: - description: File storage server list - content: empty - -10166: - description: Relay Monitor Announcement - content: empty - -10312: - description: Room Presence - content: empty - -10377: - description: Proxy Announcement - content: empty - -11111: - description: Transport Method Announcement - content: empty - -13194: - description: Wallet Info - content: free - -17375: - description: Cashu Wallet Event - content: free - -21000: - description: Lightning Pub RPC - content: free - -22242: - description: Client Authentication - content: free - -23194: - description: Wallet Request - content: free - -23195: - description: Wallet Response - content: free - -24133: - description: Nostr Connect - content: free - -24242: - description: Blobs stored on mediaservers - content: free - -30008: - description: Profile Badges - content: empty - tags: - - *dtag - - *atag - - *etag - -30009: - description: Badge Definition - content: free - tags: - - *dtag - - - name: name - next: - type: free - required: true - - - name: description - next: - type: free - - - name: image - next: - type: url - next: - type: free - - - name: thumb - next: - type: url - next: - type: free - -30017: - description: Create or update a stall - content: json - tags: - - *dtag - -30018: - description: Create or update a product - content: json - tags: - - *dtag - - - name: t - next: - type: free - -30020: - description: Product sold as an auction - content: json - tags: - - *dtag - -30023: - description: Long-form Content - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: image - next: - type: url - - - name: summary - next: - type: free - - - name: published_at - next: - type: free - - - name: t - next: - type: free - - *etag - - *atag - -30024: - description: Draft Long-form Content - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: image - next: - type: url - - - name: summary - next: - type: free - - - name: published_at - next: - type: free - - - name: t - next: - type: free - - *etag - - *atag - -30078: - description: Application-specific Data - content: free - tags: - - *dtag - -30315: - description: User Statuses - content: free - tags: - - *dtag - - - name: r - next: - type: url - - *ptag - - *etag - - *atag - - - name: expiration - next: - type: free - -30402: - description: Classified Listing - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: summary - next: - type: free - - - name: published_at - next: - type: free - - - name: location - next: - type: free - - - name: price - next: - type: free - required: true - next: - type: free - required: true - next: - type: free - - - name: status - next: - type: constrained - either: - - active - - sold - - - name: t - next: - type: free - - - name: image - next: - type: url - next: - type: free - - - name: g - next: - type: free - - *etag - - *atag - -30403: - description: Draft Classified Listing - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: summary - next: - type: free - - - name: published_at - next: - type: free - - - name: location - next: - type: free - - - name: price - next: - type: free - required: true - next: - type: free - required: true - next: - type: free - - - name: status - next: - type: constrained - either: - - active - - sold - - - name: t - next: - type: free - - - name: image - next: - type: url - next: - type: free - - - name: g - next: - type: free - - *etag - - *atag - -31922: - description: Date-Based Calendar Event - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: summary - next: - type: free - - - name: image - next: - type: url - - - name: location - next: - type: free - - - name: g - next: - type: free - - *ptag - - - name: t - next: - type: free - - - name: r - next: - type: url - - *atag - - - name: start - next: - type: free - required: true - - - name: end - next: - type: free - -31923: - description: Time-Based Calendar Event - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - - name: summary - next: - type: free - - - name: image - next: - type: url - - - name: location - next: - type: free - - - name: g - next: - type: free - - *ptag - - - name: t - next: - type: free - - - name: r - next: - type: url - - *atag - - - name: start - next: - type: free - required: true - - - name: end - next: - type: free - - - name: start_tzid - next: - type: free - - - name: end_tzid - next: - type: free - -31924: - description: Calendar - content: free - tags: - - *dtag - - - name: title - next: - type: free - required: true - - *atag - -31925: - description: Calendar Event RSVP - content: free - tags: - - *dtag - - *etag - - *atag - - - name: status - next: - type: constrained - either: - - accepted - - declined - - tentative - required: true - - - name: fb - next: - type: constrained - either: - - free - - busy - - *ptag - -31989: - description: Handler recommendation - content: empty - tags: - - *dtag - - *atag - -31990: - description: Handler information - content: json - tags: - - *dtag - - - name: k - next: *kind - - - name: web - next: - type: url - required: true - next: - type: constrained - either: - - nevent - - nprofile - - - name: ios - next: - type: free - required: true - -30617: - description: Repository announcements - content: empty - tags: - - *dtag - - - name: name - next: - type: free - required: true - - - name: description - next: - type: free - required: true - - - name: web - next: - type: url - required: true - - - name: clone - next: - type: giturl - required: true - - - name: relays - next: - type: relay - variadic: true - - - name: r - next: - type: gitcommit - required: true - next: - type: constrained - either: - - euc - required: true - - - name: maintainers - next: - type: pubkey - variadic: true - -30618: - description: Repository state announcements - content: empty - tags: - - *dtag - - - prefix: "refs/" - next: - type: gitcommit - required: true - - - name: HEAD - next: - type: free - -1617: - description: Patches - content: free - tags: - - *atag - - - name: r - next: - type: gitcommit - required: true - - *ptag - - - name: t - next: - type: constrained - either: - - root - - root-revision - required: true - - - name: commit - next: - type: gitcommit - required: true - - - name: r - next: - type: gitcommit - required: true - - - name: parent-commit - next: - type: gitcommit - required: true - - - name: commit-pgp-sig - next: - type: free - required: true - - - name: committer - next: - type: free - required: true - next: - type: free - required: true - next: - type: free - required: true - next: - type: free - required: true - -30000: - description: Follow sets - content: empty - -30001: - description: Generic lists - content: empty - deprecated: true - -30002: - description: Relay sets - content: empty - -30003: - description: Bookmark sets - content: empty - -30004: - description: Curation sets - content: empty - -30005: - description: Video sets - content: empty - -30007: - description: Kind mute sets - content: empty - -30015: - description: Interest sets - content: empty - -30019: - description: Marketplace UI/UX - content: json - -30030: - description: Emoji sets - content: empty - -30040: - description: Curated Publication Index - content: free - -30041: - description: Curated Publication Content - content: free - -30063: - description: Release artifact sets - content: empty - -30166: - description: Relay Discovery - content: empty - -30267: - description: App curation sets - content: empty - -30311: - description: Live Event - content: free - -30312: - description: Interactive Room - content: free - -30313: - description: Conference Event - content: free - -30388: - description: Slide Set - content: free - -30818: - description: Wiki article - content: free - -30819: - description: Redirects - content: free - -31234: - description: Draft Event - content: free - -31388: - description: Link Set - content: free - -31890: - description: Feed - content: free - -32267: - description: Software Application - content: free - -34550: - description: Community Definition - content: free - -37516: - description: Geocache listing - content: free - -38172: - description: Cashu Mint Announcement - content: free - -38173: - description: Fedimint Announcement - content: free - -38383: - description: Peer-to-peer Order events - content: free - -39089: - description: Starter packs - content: empty - -39092: - description: Media starter packs - content: empty - -39701: - description: Web bookmarks - content: free diff --git a/schema/schema_test.go b/schema/schema_test.go index 52c5845..8f5f4b4 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -8,8 +8,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestNewValidator(t *testing.T) { - v := NewValidator(string(schemaFile)) +func TestNewValidatorFromURL(t *testing.T) { + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) require.NotNil(t, v.Schema) require.False(t, v.FailOnUnknown) // default value @@ -22,14 +23,14 @@ func TestNewValidator(t *testing.T) { require.True(t, hasKind1111) } -func TestNewValidatorWithInvalidYAML(t *testing.T) { - require.Panics(t, func() { - NewValidator("invalid yaml content: [[[") - }) +func TestNewValidatorFromBytesWithInvalidYAML(t *testing.T) { + _, err := NewValidatorFromBytes([]byte("invalid yaml content: [[[")) + require.Error(t, err) } func TestValidateEvent_BasicSuccess(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // kind 1 with free content and valid p tag evt := nostr.Event{ @@ -40,30 +41,32 @@ func TestValidateEvent_BasicSuccess(t *testing.T) { }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) } func TestValidateEvent_Kind0_JSONContent(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // valid JSON content evt := nostr.Event{ Kind: 0, Content: `{"name":"test","about":"description"}`, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) // invalid JSON content evt.Content = "not-json-content" err = v.ValidateEvent(evt) require.Error(t, err) - require.Equal(t, ErrInvalidContentJson, err) + require.Equal(t, ContentError{Err: ErrInvalidJson}, err) } func TestValidateEvent_UnknownKind(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) evt := nostr.Event{ Kind: nostr.Kind(39999), @@ -71,7 +74,7 @@ func TestValidateEvent_UnknownKind(t *testing.T) { } // should not fail when FailOnUnknown is false (default) - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) // should fail when FailOnUnknown is true @@ -82,22 +85,25 @@ func TestValidateEvent_UnknownKind(t *testing.T) { } func TestValidateEvent_EmptyTag(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) evt := nostr.Event{ - Kind: 1, + Kind: 1, + Content: "test", Tags: nostr.Tags{ nostr.Tag{}, // empty tag }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.Error(t, err) require.Equal(t, ErrEmptyTag, err) } func TestValidateNext_ID(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -116,7 +122,7 @@ func TestValidateNext_ID(t *testing.T) { }, { name: "invalid id - not hex", - tag: nostr.Tag{"e", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, + tag: nostr.Tag{"e", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, valid: false, }, } @@ -124,7 +130,7 @@ func TestValidateNext_ID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "id", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -135,7 +141,8 @@ func TestValidateNext_ID(t *testing.T) { } func TestValidateNext_PubKey(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -162,7 +169,7 @@ func TestValidateNext_PubKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "pubkey", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -173,7 +180,8 @@ func TestValidateNext_PubKey(t *testing.T) { } func TestValidateNext_Relay(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -210,7 +218,7 @@ func TestValidateNext_Relay(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "relay", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -221,7 +229,8 @@ func TestValidateNext_Relay(t *testing.T) { } func TestValidateNext_Kind(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -258,7 +267,7 @@ func TestValidateNext_Kind(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "kind", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -269,7 +278,8 @@ func TestValidateNext_Kind(t *testing.T) { } func TestValidateNext_Constrained(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -294,7 +304,7 @@ func TestValidateNext_Constrained(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "constrained", Required: true, Either: tt.allowed} - err := v.validateNext(tt.tag, 3, next) + _, err := v.validateNext(tt.tag, 3, next) if tt.valid { require.NoError(t, err) } else { @@ -305,7 +315,8 @@ func TestValidateNext_Constrained(t *testing.T) { } func TestValidateNext_GitCommit(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -337,7 +348,7 @@ func TestValidateNext_GitCommit(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "gitcommit", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -348,7 +359,8 @@ func TestValidateNext_GitCommit(t *testing.T) { } func TestValidateNext_Addr(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -370,7 +382,7 @@ func TestValidateNext_Addr(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { next := &nextSpec{Type: "addr", Required: true} - err := v.validateNext(tt.tag, 1, next) + _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) } else { @@ -381,71 +393,76 @@ func TestValidateNext_Addr(t *testing.T) { } func TestValidateNext_Free(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // free type should accept anything tag := nostr.Tag{"test", "any value here", "even", "multiple", "values"} next := &nextSpec{Type: "free", Required: true} - err := v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.NoError(t, err) } func TestValidateNext_UnknownType(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tag := nostr.Tag{"test", "value"} next := &nextSpec{Type: "unknown-type", Required: true} // should not fail when FailOnUnknown is false (default) - err := v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.NoError(t, err) // should fail when FailOnUnknown is true v.FailOnUnknown = true - err = v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.Error(t, err) require.Equal(t, ErrUnknownTagType, err) } func TestValidateNext_RequiredField(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // test missing required field tag := nostr.Tag{"test"} // only name, missing required value next := &nextSpec{Type: "free", Required: true} - err := v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.Error(t, err) require.Contains(t, err.Error(), "missing index 1") // test optional field next = &nextSpec{Type: "free", Required: false} - err = v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.NoError(t, err) } func TestValidateNext_Variadic(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // test variadic field with multiple values tag := nostr.Tag{"test", "value1", "value2", "value3"} next := &nextSpec{Type: "free", Variadic: true} - err := v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.NoError(t, err) // test variadic field with single value tag = nostr.Tag{"test", "only-one-value"} - err = v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.NoError(t, err) // test variadic field with no values (should fail if required) tag = nostr.Tag{"test"} next = &nextSpec{Type: "free", Variadic: true, Required: true} - err = v.validateNext(tag, 1, next) + _, err = v.validateNext(tag, 1, next) require.Error(t, err) } func TestValidateEvent_Kind10002(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // kind 10002 (relay list metadata) with valid r tags evt := nostr.Event{ @@ -457,7 +474,7 @@ func TestValidateEvent_Kind10002(t *testing.T) { }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) // test with invalid relay marker @@ -474,7 +491,8 @@ func TestValidateEvent_Kind10002(t *testing.T) { } func TestValidateEvent_Kind1_ETag(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // kind 1 with e tag (reply/root marker) evt := nostr.Event{ @@ -485,7 +503,7 @@ func TestValidateEvent_Kind1_ETag(t *testing.T) { }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) // test with invalid marker @@ -497,7 +515,8 @@ func TestValidateEvent_Kind1_ETag(t *testing.T) { } func TestValidateEvent_Kind30617_RepositoryAnnouncement(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // kind 30617 (repository announcement) with required tags evt := nostr.Event{ @@ -515,12 +534,13 @@ func TestValidateEvent_Kind30617_RepositoryAnnouncement(t *testing.T) { }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) } func TestValidateEvent_MultiplePossibleTagSpecs(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) // test event with tags that could match multiple specs // kind 1 has both "e" and "q" tags that can take different forms @@ -533,7 +553,7 @@ func TestValidateEvent_MultiplePossibleTagSpecs(t *testing.T) { }, } - err := v.ValidateEvent(evt) + err = v.ValidateEvent(evt) require.NoError(t, err) // test q tag with id format (alternative spec) @@ -545,7 +565,8 @@ func TestValidateEvent_MultiplePossibleTagSpecs(t *testing.T) { } func TestSchema_ErrorMessages(t *testing.T) { - v := NewValidator(string(schemaFile)) + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) tests := []struct { name string @@ -555,8 +576,9 @@ func TestSchema_ErrorMessages(t *testing.T) { { name: "empty tag error", event: nostr.Event{ - Kind: 1, - Tags: nostr.Tags{nostr.Tag{}}, + Kind: 1, + Content: "test", + Tags: nostr.Tags{nostr.Tag{}}, }, expError: ErrEmptyTag, },