tinyrelayfile
tinyrelay/tinygit/metadata_admission_test.go/

tinygit/metadata_admission_test.go raw
1package tinygit
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/FelineStateMachine/tinyrelay/internal/event"
11 "github.com/FelineStateMachine/tinyrelay/internal/storage"
12)
13
14func 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 := 0
22 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 denied
26 }})
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 = 0
52 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, even
56 // though the metadata shape still parses.
57 calls = 0
58 tampered := e
59 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}
70
71func TestMetadataShapePrecedesRepositoryLookup(t *testing.T) {
72 // No store or host is needed to reject shape. In particular, HEAD must
73 // 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}
82
83func 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 := 0
91 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 denied
95 }})
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 = 0
116 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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs