clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / tinygit / metadata_admission_test.go
metadata_admission_test.go 4.7K
1package tinygit23import (4 "context"5 "errors"6 "path/filepath"7 "strings"8 "testing"910 "github.com/FelineStateMachine/tinyrelay/internal/event"11 "github.com/FelineStateMachine/tinyrelay/internal/storage"12)1314func TestValidMetadataShapeDoesNotGrantAdmission(t *testing.T) {15 ctx := context.Background()16 store, err := storage.Open(ctx, filepath.Join(t.TempDir(), "events.db"))17 if err != nil {18 t.Fatal(err)19 }20 defer store.Close()21 calls := 022 denied := errors.New("blocked: host does not accept this repository")23 g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(t.TempDir(), "git"), Authorize: func(context.Context, Event, Repository) error {24 calls++25 return denied26 }})27 if err != nil {28 t.Fatal(err)29 }30 secret := strings.Repeat("0", 63) + "1"31 for _, kind := range []int{30617, 30618} {32 e := Event{Kind: kind, CreatedAt: 1, Tags: [][]string{{"d", "demo"}}}33 if err := event.Sign(&e, secret); err != nil {34 t.Fatal(err)35 }36 if _, err := ParseMetadata(e); err != nil {37 t.Fatalf("valid shape rejected: %v", err)38 }39 if kind == 30618 {40 // Pure parsing needs no announcement; admission still does.41 if _, err := g.Validate(ctx, e); err == nil || !strings.Contains(err.Error(), "announcement is missing") {42 t.Fatalf("state without authority admitted: %v", err)43 }44 g.repos[key(e.PubKey, "demo")] = Repository{Owner: e.PubKey, Identifier: "demo", EventID: "announcement"}45 }46 for _, admit := range []func(context.Context, Event) error{47 func(ctx context.Context, e Event) error { _, err := g.Validate(ctx, e); return err },48 func(ctx context.Context, e Event) error { _, err := g.ValidateImported(ctx, e); return err },49 g.Publish,50 } {51 calls = 052 if err := admit(ctx, e); !errors.Is(err, denied) || calls != 1 {53 t.Fatalf("shape bypassed host admission: kind=%d calls=%d err=%v", kind, calls, err)54 }55 // A tampered signature must fail before host authorization, even56 // though the metadata shape still parses.57 calls = 058 tampered := e59 tampered.Content = "tampered"60 if err := admit(ctx, tampered); err == nil || errors.Is(err, denied) || calls != 0 {61 t.Fatalf("signature stage bypassed: calls=%d err=%v", calls, err)62 }63 }64 }65 result, err := store.Query(ctx, event.Filter{}, storage.QueryOptions{})66 if err != nil || len(result.Events) != 0 {67 t.Fatalf("rejected metadata persisted: count=%d err=%v", len(result.Events), err)68 }69}7071func TestMetadataShapePrecedesRepositoryLookup(t *testing.T) {72 // No store or host is needed to reject shape. In particular, HEAD must73 // not be deferred until after resolving the announcement and maintainers.74 g := &GitRelay{}75 for _, tag := range [][]string{{"HEAD", "detached"}, {"refs/heads/bad..name", strings.Repeat("a", 40)}} {76 e := Event{Kind: 30618, Tags: [][]string{{"d", "demo"}, tag}}77 if _, err := g.parseRepository(e); err == nil || !strings.HasPrefix(err.Error(), "invalid:") {78 t.Fatalf("shape error must precede repository lookup: %v", err)79 }80 }81}8283func TestInvalidMetadataShapePrecedesHostAdmission(t *testing.T) {84 ctx := context.Background()85 store, err := storage.Open(ctx, filepath.Join(t.TempDir(), "events.db"))86 if err != nil {87 t.Fatal(err)88 }89 defer store.Close()90 calls := 091 denied := errors.New("host denial")92 g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(t.TempDir(), "git"), Authorize: func(context.Context, Event, Repository) error {93 calls++94 return denied95 }})96 if err != nil {97 t.Fatal(err)98 }99 secret := strings.Repeat("0", 63) + "1"100 owner, err := event.PublicKey(secret)101 if err != nil {102 t.Fatal(err)103 }104 g.repos[key(owner, "demo")] = Repository{Owner: owner, Identifier: "demo", EventID: "announcement"}105 e := Event{Kind: 30618, CreatedAt: 1, Tags: [][]string{{"d", "demo"}, {"refs/heads/bad..name", strings.Repeat("a", 40)}}}106 if err := event.Sign(&e, secret); err != nil {107 t.Fatal(err)108 }109 for name, admit := range map[string]func(context.Context, Event) error{110 "validate": func(ctx context.Context, e Event) error { _, err := g.Validate(ctx, e); return err },111 "import": func(ctx context.Context, e Event) error { _, err := g.ValidateImported(ctx, e); return err },112 "publish": g.Publish,113 } {114 t.Run(name, func(t *testing.T) {115 calls = 0116 if err := admit(ctx, e); err == nil || !strings.HasPrefix(err.Error(), "invalid:") {117 t.Errorf("invalid shape returned %v, want shape error before host denial", err)118 }119 if calls != 0 {120 t.Errorf("host authorizer called %d times for invalid shape", calls)121 }122 result, err := store.Query(ctx, event.Filter{IDs: []string{e.ID}}, storage.QueryOptions{})123 if err != nil || len(result.Events) != 0 {124 t.Fatalf("invalid shape persisted: count=%d err=%v", len(result.Events), err)125 }126 })127 }128}