| 1 | package tinygit |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 11 | ) |
| 12 | |
| 13 | type maintainerFunc func(context.Context, Repository, string) bool |
| 14 | |
| 15 | func (f maintainerFunc) IsMaintainer(ctx context.Context, r Repository, pubkey string) bool { |
| 16 | return f(ctx, r, pubkey) |
| 17 | } |
| 18 | |
| 19 | // The host's maintainer answer is consulted after the owner and the |
| 20 | // maintainers tag, and it decides whether a state event from another key is |
| 21 | // accepted. |
| 22 | func TestMaintainerSourceDecidesStateAuthority(t *testing.T) { |
| 23 | root := t.TempDir() |
| 24 | store, err := storage.Open(context.Background(), filepath.Join(root, "relay.db" )) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | defer store.Close() |
| 29 | vouched := map[string]bool{} |
| 30 | var asked []string |
| 31 | g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(root, "git" ), Maintainers: maintainerFunc(func(_ context.Context, r Repository, pubkey string) bool { |
| 32 | asked = append(asked, r.Identifier+":" +pubkey) |
| 33 | return vouched[pubkey] |
| 34 | })}) |
| 35 | if err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | ownerSecret, agentSecret := strings.Repeat("0" , 63)+"1" , strings.Repeat("0" , 63)+"2" |
| 39 | owner, _ := event.PublicKey(ownerSecret) |
| 40 | agent, _ := event.PublicKey(agentSecret) |
| 41 | announcement := event.Event{Kind: 30617, CreatedAt: 100, Tags: [][]string{{"d" , "demo" }, {"clone" , "https://relay.example/" + owner + "/demo.git" }}} |
| 42 | if err := event.Sign(&announcement, ownerSecret); err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | if err := g.Publish(context.Background(), announcement); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | state := func(createdAt int64) event.Event { |
| 49 | e := event.Event{Kind: 30618, CreatedAt: createdAt, Tags: [][]string{{"d" , "demo" }, {"HEAD" , "ref: refs/heads/main" }, {"refs/heads/main" , strings.Repeat("a" , 40)}}} |
| 50 | if err := event.Sign(&e, agentSecret); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | return e |
| 54 | } |
| 55 | if _, err := g.Validate(context.Background(), state(101)); err == nil || !strings.Contains(err.Error(), "not an accepted repository maintainer" ) { |
| 56 | t.Fatalf("state from an unvouched key: %v" , err) |
| 57 | } |
| 58 | if len(asked) == 0 || asked[0] != "demo:" +agent { |
| 59 | t.Fatalf("source was not consulted for the repository: %v" , asked) |
| 60 | } |
| 61 | vouched[agent] = true |
| 62 | repo, err := g.Validate(context.Background(), state(102)) |
| 63 | if err != nil { |
| 64 | t.Fatalf("state from a vouched key: %v" , err) |
| 65 | } |
| 66 | if repo.Owner != owner || repo.Refs["refs/heads/main" ] != strings.Repeat("a" , 40) { |
| 67 | t.Fatalf("state bound to the wrong repository: %+v" , repo) |
| 68 | } |
| 69 | if !g.IsMaintainer(context.Background(), repo, owner) || g.IsMaintainer(context.Background(), repo, "" ) { |
| 70 | t.Fatal("owner must count and an empty key must not" ) |
| 71 | } |
| 72 | vouched[agent] = false |
| 73 | if _, err := g.Validate(context.Background(), state(103)); err == nil { |
| 74 | t.Fatal("a withdrawn answer must reject the next state" ) |
| 75 | } |
| 76 | } |