~bigbes/sr-ht-spec

ref: 61515a575dc7e931829c05eed0cd3c3a441df753 sr-ht-spec/hooks/proto_test.go -rw-r--r-- 5.6 KiB
61515a57 — Eugene Blikh chore(beads): file spec-ejq.2, CI publish blocked on missing apk-ci-s3 secret 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package hooks

import (
	"bytes"
	"strings"
	"testing"
)

func validRequest() Request {
	return Request{
		Version:    ProtocolVersion,
		Method:     MethodValidateRef,
		Repo:       "/var/lib/spec/~bigbes/rfcs",
		Push:       "4711",
		Credential: Credential{Kind: PrincipalOwner},
		Updates: []RefUpdate{{
			Ref: "refs/heads/main",
			Old: strings.Repeat("0", 40),
			New: strings.Repeat("a", 40),
		}},
	}
}

func TestRequestRoundTrip(t *testing.T) {
	want := validRequest()
	want.Credential = Credential{Kind: PrincipalAgent, Token: "s3cret", Agent: "claude/spec", Session: "abc"}

	var buf bytes.Buffer
	if err := WriteRequest(&buf, want); err != nil {
		t.Fatalf("WriteRequest: %v", err)
	}
	if !strings.HasSuffix(buf.String(), "\n") {
		t.Error("a request must be newline terminated so a peer can frame it")
	}
	got, err := ReadRequest(&buf)
	if err != nil {
		t.Fatalf("ReadRequest: %v", err)
	}
	if got.Method != want.Method || got.Repo != want.Repo || got.Push != want.Push {
		t.Errorf("round trip lost fields: %+v", got)
	}
	if got.Credential != want.Credential {
		t.Errorf("credential round trip: got %+v want %+v", got.Credential, want.Credential)
	}
	if len(got.Updates) != 1 || got.Updates[0] != want.Updates[0] {
		t.Errorf("updates round trip: got %+v", got.Updates)
	}
}

func TestRequestValidate(t *testing.T) {
	zero := strings.Repeat("0", 40)
	tests := []struct {
		name string
		mut  func(*Request)
		want string
	}{
		{"valid", func(*Request) {}, ""},
		{"wrong version", func(r *Request) { r.Version = 99 }, "different builds"},
		{"unknown method", func(r *Request) { r.Method = "reindex-everything" }, "unknown method"},
		{"no repo", func(r *Request) { r.Repo = "" }, "no repository path"},
		{"relative repo", func(r *Request) { r.Repo = "rfcs" }, "not absolute"},
		{"no push id", func(r *Request) { r.Push = "" }, "no push correlation id"},
		{"no principal", func(r *Request) { r.Credential.Kind = "" }, "unknown principal kind"},
		{"agent with no token", func(r *Request) { r.Credential = Credential{Kind: PrincipalAgent} },
			"must carry a token"},
		{"owner with a token", func(r *Request) { r.Credential.Token = "x" }, "must not carry a token"},
		{"no updates", func(r *Request) { r.Updates = nil }, "no ref updates"},
		{"validate-ref with two updates", func(r *Request) {
			r.Updates = append(r.Updates, RefUpdate{Ref: "refs/heads/x", Old: zero, New: zero})
		}, "want exactly 1"},
		{"empty object name", func(r *Request) { r.Updates[0].Old = "" }, "forty zeroes"},
		{"no ref name", func(r *Request) { r.Updates[0].Ref = "" }, "no ref name"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := validRequest()
			tt.mut(&req)
			err := req.Validate()
			switch {
			case tt.want == "" && err != nil:
				t.Fatalf("Validate: %v", err)
			case tt.want == "":
			case err == nil:
				t.Fatalf("Validate accepted %s", tt.name)
			default:
				mentions(t, "the error", err.Error(), tt.want)
			}
		})
	}
}

// TestResponseValidate covers the answers a hook must not act on. An
// ambiguous response is treated as an unreachable daemon, which is a rejection
// — never as permission to proceed.
func TestResponseValidate(t *testing.T) {
	tests := []struct {
		name string
		resp Response
		ok   bool
	}{
		{"ok", Response{Version: ProtocolVersion, OK: true}, true},
		{"rejected with a message", Response{Version: ProtocolVersion, Rejected: true, Message: "no"}, true},
		{"error", Response{Version: ProtocolVersion, Error: "postgres is down"}, true},
		{"wrong version", Response{Version: 2, OK: true}, false},
		{"ok and rejected", Response{Version: ProtocolVersion, OK: true, Rejected: true, Message: "?"}, false},
		{"ok and errored", Response{Version: ProtocolVersion, OK: true, Error: "?"}, false},
		{"rejected with no message", Response{Version: ProtocolVersion, Rejected: true}, false},
		{"rejected with a blank message", Response{Version: ProtocolVersion, Rejected: true, Message: "  \n"}, false},
		{"neither", Response{Version: ProtocolVersion}, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.resp.Validate()
			if tt.ok && err != nil {
				t.Fatalf("Validate: %v", err)
			}
			if !tt.ok && err == nil {
				t.Fatal("Validate accepted an answer no hook can act on")
			}
		})
	}
}

// TestReadRefusesOversizedMessage proves the socket cannot be used to exhaust
// the daemon's memory: the read is capped, and hitting the cap is an error
// rather than a truncation that would decode as a smaller, different request.
func TestReadRefusesOversizedMessage(t *testing.T) {
	huge := validRequest()
	huge.Credential.Token = strings.Repeat("x", maxMessageBytes)

	var buf bytes.Buffer
	if err := WriteRequest(&buf, huge); err == nil {
		t.Fatal("WriteRequest accepted a message over the limit")
	}

	// A peer that does not use WriteRequest still cannot get past the reader.
	raw := `{"version":1,"method":"validate-ref","repo":"` + strings.Repeat("a", maxMessageBytes) + `"}`
	if _, err := ReadRequest(strings.NewReader(raw)); err == nil {
		t.Fatal("ReadRequest accepted a message over the limit")
	}
}

// TestReadToleratesUnknownFields keeps the version number as the compatibility
// contract: a peer from another build gets the sentence about mismatched
// builds, not a json decoding error.
func TestReadToleratesUnknownFields(t *testing.T) {
	raw := `{"version":99,"method":"validate-ref","repo":"/x","push":"1","future_field":true}` + "\n"
	req, err := ReadRequest(strings.NewReader(raw))
	if err != nil {
		t.Fatalf("ReadRequest: %v", err)
	}
	err = req.Validate()
	if err == nil {
		t.Fatal("Validate accepted version 99")
	}
	mentions(t, "the version mismatch", err.Error(), "different builds")
}