diff --git a/khatru/policies/schema_validation.go b/khatru/policies/schema_validation.go index d51a0ff..03631c3 100644 --- a/khatru/policies/schema_validation.go +++ b/khatru/policies/schema_validation.go @@ -7,12 +7,12 @@ import ( "fiatjaf.com/nostr/schema" ) -func ValidateAgainstSchema(ctx context.Context, evt nostr.Event) (bool, string) { - v := schema.NewDefaultValidator() - v.FailOnUnknown = true - err := v.ValidateEvent(evt) - if err != nil { - return true, err.Error() +func ValidateAgainstSchema(v schema.Validator) func(ctx context.Context, evt nostr.Event) (bool, string) { + return func(ctx context.Context, evt nostr.Event) (bool, string) { + err := v.ValidateEvent(evt) + if err != nil { + return true, err.Error() + } + return false, "" } - return false, "" } diff --git a/schema/schema.go b/schema/schema.go index 6ddf243..e5ad62d 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -18,7 +18,7 @@ import ( "gopkg.in/yaml.v3" ) -const DefaultSchemaURL = "https://raw.githubusercontent.com/nostr-protocol/registry-of-kinds/refs/heads/master/schema.yaml" +const DefaultSchemaURL = "https://raw.githubusercontent.com/nostr-protocol/registry-of-kinds/daaa3a2a5573606aa0a4c4a98c25460ea9a1e388/schema.yaml" // this is used by hex.Decode in the "hex" validator -- we don't care about data races var hexdummydecoder = make([]byte, 128) @@ -123,7 +123,7 @@ func NewValidatorFromSchema(sch Schema) Validator { }, "json": func(value string, spec *nextSpec) error { if !json.Valid(unsafe.Slice(unsafe.StringData(value), len(value))) { - return ContentError{ErrInvalidJson} + return ErrInvalidJson } return nil }, @@ -311,7 +311,7 @@ func (v *Validator) ValidateEvent(evt nostr.Event) error { if !tagWasValidated { if tagSpecNext, ok := v.Schema.GenericTags[tag[0]]; ok { if ii, err := v.validateNext(tag, 1, &tagSpecNext); err != nil { - lastErr = TagError{ti, ii, err} + return TagError{ti, ii, err} } } } diff --git a/schema/schema_test.go b/schema/schema_test.go index 8f5f4b4..d8664e1 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -12,22 +12,17 @@ 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 + require.False(t, v.FailOnUnknownKind) // default value // test with some known kinds from schema.yaml - _, hasKind0 := v.Schema["0"] // profile metadata + _, hasKind0 := v.Schema.Kinds["0"] // profile metadata require.True(t, hasKind0) - _, hasKind1 := v.Schema["1"] // text note + _, hasKind1 := v.Schema.Kinds["1"] // text note require.True(t, hasKind1) - _, hasKind1111 := v.Schema["1111"] // comment + _, hasKind1111 := v.Schema.Kinds["1111"] // comment require.True(t, hasKind1111) } -func TestNewValidatorFromBytesWithInvalidYAML(t *testing.T) { - _, err := NewValidatorFromBytes([]byte("invalid yaml content: [[[")) - require.Error(t, err) -} - func TestValidateEvent_BasicSuccess(t *testing.T) { v, err := NewValidatorFromURL(DefaultSchemaURL) require.NoError(t, err) @@ -73,12 +68,12 @@ func TestValidateEvent_UnknownKind(t *testing.T) { Content: "test", } - // should not fail when FailOnUnknown is false (default) + // should not fail when FailOnUnknownKind is false (default) err = v.ValidateEvent(evt) require.NoError(t, err) - // should fail when FailOnUnknown is true - v.FailOnUnknown = true + // should fail when FailOnUnknownKind is true + v.FailOnUnknownKind = true err = v.ValidateEvent(evt) require.Error(t, err) require.Equal(t, ErrUnknownKind, err) @@ -347,7 +342,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} + next := &nextSpec{Type: "hex", Min: 40, Max: 40, Required: true} _, err := v.validateNext(tt.tag, 1, next) if tt.valid { require.NoError(t, err) @@ -410,12 +405,12 @@ func TestValidateNext_UnknownType(t *testing.T) { tag := nostr.Tag{"test", "value"} next := &nextSpec{Type: "unknown-type", Required: true} - // should not fail when FailOnUnknown is false (default) + // should not fail when FailOnUnknownType is false (default) _, err = v.validateNext(tag, 1, next) require.NoError(t, err) - // should fail when FailOnUnknown is true - v.FailOnUnknown = true + // should fail when FailOnUnknownType is true + v.FailOnUnknownType = true _, err = v.validateNext(tag, 1, next) require.Error(t, err) require.Equal(t, ErrUnknownTagType, err) @@ -594,8 +589,8 @@ func TestSchema_ErrorMessages(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.expError == ErrUnknownKind { - v.FailOnUnknown = true - defer func() { v.FailOnUnknown = false }() + v.FailOnUnknownKind = true + defer func() { v.FailOnUnknownKind = false }() } err := v.ValidateEvent(tt.event) @@ -604,3 +599,93 @@ func TestSchema_ErrorMessages(t *testing.T) { }) } } + +func TestValidateEvent_RequiredTags(t *testing.T) { + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) + + // kind 17 (website reaction) requires "r" tag + evt := nostr.Event{ + Kind: 17, + Content: "test reaction", + Tags: nostr.Tags{}, // missing required "r" tag + } + + // should fail due to missing required tag + err = v.ValidateEvent(evt) + require.Error(t, err) + require.IsType(t, RequiredTagError{}, err) + rte := err.(RequiredTagError) + require.Contains(t, rte.Missing, "r") + + // add the required "r" tag + evt.Tags = nostr.Tags{ + nostr.Tag{"r", "https://example.com"}, + } + + // should pass now + err = v.ValidateEvent(evt) + require.NoError(t, err) +} + +func TestValidateEvent_GenericTags(t *testing.T) { + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) + + // kind 1 with invalid "t" tag (must be lowercase) + evt := nostr.Event{ + Kind: 1, + Content: "test content", + Tags: nostr.Tags{ + nostr.Tag{"t", "UPPERCASE"}, // invalid: not lowercase + }, + } + + // should fail due to invalid "t" tag + err = v.ValidateEvent(evt) + require.Error(t, err) + require.Contains(t, err.Error(), "not lowercase") + + // fix the "t" tag to be lowercase + evt.Tags = nostr.Tags{ + nostr.Tag{"t", "lowercase"}, + } + + // should pass now + err = v.ValidateEvent(evt) + require.NoError(t, err) +} + +func TestValidateEvent_DTagPresence(t *testing.T) { + v, err := NewValidatorFromURL(DefaultSchemaURL) + require.NoError(t, err) + + // kind 30617 (repository announcement) is addressable and requires "d" tag + evt := nostr.Event{ + Kind: 30617, + Content: "", + Tags: nostr.Tags{ + nostr.Tag{"name", "My Repo"}, + nostr.Tag{"description", "A test repo"}, + nostr.Tag{"web", "https://github.com/user/repo"}, + nostr.Tag{"clone", "https://github.com/user/repo.git"}, + nostr.Tag{"r", "a1b2c3d4e5f6789012345678901234567890abcd", "euc"}, + nostr.Tag{"maintainers", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}, + // missing "d" tag + }, + } + + // should fail due to missing "d" tag + err = v.ValidateEvent(evt) + require.Error(t, err) + require.IsType(t, RequiredTagError{}, err) + rte := err.(RequiredTagError) + require.Contains(t, rte.Missing, "d") + + // add the required "d" tag + evt.Tags = append(evt.Tags, nostr.Tag{"d", "my-repo"}) + + // should pass now + err = v.ValidateEvent(evt) + require.NoError(t, err) +}