tests: pin -b - stdin for issue create and pr createUnverified
Closes the last open acceptance criterion of rickub/rickub issue 279 (criterion 3): a test asserts -b - reaches the API as the stdin content for BOTH issue create and pr create, bite-proven by reverting the pr-create half. The shared resolver (cmd/body.go) and its structural guard (TestEveryBodyFlagUsesTheSharedResolver) landed with issue 94s fix, and the comment commands have had stdin-to-API tests since; the two CREATE commands - the exact commands 279 reported, pr create once storing a literal dash and discarding an 8.9 KB description with a success line - had no test asserting the stdin bytes land on the wire. Bite-proven as the issue prescribes: reverting only runPRCreates resolveBody call fails ONLY TestPRCreateBodyDashReadsStdin (posted the literal dash) while TestIssueCreateBodyDashReadsStdin stays green, so each half is independently pinned. Restored, the full cmd suite passes.
507eaf7 parent: a97c085 modified
cmd/body_test.go +74 -0 | @@ -209,6 +209,80 @@ func TestMRCommentAnchoredBlankBodyIsRefused(t *testing.T) { | ||
| 209 | 209 | } |
| 210 | 210 | } |
| 211 | 211 | |
| 212 | +// TestIssueCreateBodyDashReadsStdin and TestPRCreateBodyDashReadsStdin close | |
| 213 | +// rickub/rickub#279's acceptance criterion 3: "-b - reaches the API as the stdin | |
| 214 | +// content for BOTH issue create and pr create". The comment commands above | |
| 215 | +// already pin their halves; these pin the two CREATE halves — `pr create -b -` | |
| 216 | +// is the exact command that once stored a literal "-" and discarded an 8.9 KB | |
| 217 | +// description with a success line. Each test fails on a parser that uses the | |
| 218 | +// flag value verbatim (sent == "-"), which is how the pr-create half regressed | |
| 219 | +// before the shared resolver existed. | |
| 220 | +func TestIssueCreateBodyDashReadsStdin(t *testing.T) { | |
| 221 | + var sent string | |
| 222 | + defer withCreateBodyServer(t, &sent, "/issues")() | |
| 223 | + | |
| 224 | + const text = "## Summary\n\nPins the portal gun back on the shelf.\n\n- one\n- two" | |
| 225 | + if _, _, err := executeStdin(t, text+"\n", "issue", "create", "-R", "rick/portal", | |
| 226 | + "-t", "the gun slips", "-b", "-"); err != nil { | |
| 227 | + t.Fatalf("execute: %v", err) | |
| 228 | + } | |
| 229 | + if sent == "-" { | |
| 230 | + t.Fatal(`issue create -b - posted the literal "-" — rickub/rickub#279 has regressed`) | |
| 231 | + } | |
| 232 | + if sent != text { | |
| 233 | + t.Fatalf("posted body = %q, want the stdin text %q", sent, text) | |
| 234 | + } | |
| 235 | +} | |
| 236 | + | |
| 237 | +func TestPRCreateBodyDashReadsStdin(t *testing.T) { | |
| 238 | + var sent string | |
| 239 | + defer withCreateBodyServer(t, &sent, "/merge-requests")() | |
| 240 | + | |
| 241 | + const text = "## What\n\nA long description piped in from a file, the way MR bodies\nare actually written.\n" | |
| 242 | + if _, _, err := executeStdin(t, text, "pr", "create", "-R", "rick/portal", | |
| 243 | + "--base", "main", "--head", "fix/pin-the-gun", "-t", "fix: pin the gun", "-b", "-"); err != nil { | |
| 244 | + t.Fatalf("execute: %v", err) | |
| 245 | + } | |
| 246 | + if sent == "-" { | |
| 247 | + t.Fatal(`pr create -b - posted the literal "-" — rickub/rickub#279 has regressed`) | |
| 248 | + } | |
| 249 | + if sent != strings.TrimRight(text, "\r\n") { | |
| 250 | + t.Fatalf("posted body = %q, want the stdin text %q", sent, text) | |
| 251 | + } | |
| 252 | +} | |
| 253 | + | |
| 254 | +// withCreateBodyServer stands up a fake that answers a create endpoint (path | |
| 255 | +// suffix) and records the request JSON's body field, wiring the CLI at it. The | |
| 256 | +// suffix keeps the two tests honest about WHICH create they exercised; any | |
| 257 | +// other path fails the test rather than passing as an empty capture. | |
| 258 | +func withCreateBodyServer(t *testing.T, got *string, suffix string) func() { | |
| 259 | + t.Helper() | |
| 260 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 261 | + if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, suffix) { | |
| 262 | + t.Errorf("create request used %s %s, want POST …%s", r.Method, r.URL.Path, suffix) | |
| 263 | + } | |
| 264 | + var body struct { | |
| 265 | + Body string `json:"body"` | |
| 266 | + } | |
| 267 | + b, err := io.ReadAll(r.Body) | |
| 268 | + if err != nil { | |
| 269 | + t.Errorf("reading request body: %v", err) | |
| 270 | + } | |
| 271 | + if err := json.Unmarshal(b, &body); err != nil { | |
| 272 | + t.Errorf("request body %q is not the JSON the CLI should send: %v", b, err) | |
| 273 | + } | |
| 274 | + *got = body.Body | |
| 275 | + w.WriteHeader(http.StatusCreated) | |
| 276 | + _ = json.NewEncoder(w).Encode(map[string]any{ | |
| 277 | + "number": 62, "title": "recorded", "state": "open", | |
| 278 | + }) | |
| 279 | + })) | |
| 280 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 281 | + t.Setenv("RICKUB_HOST", srv.URL) | |
| 282 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | |
| 283 | + return srv.Close | |
| 284 | +} | |
| 285 | + | |
| 212 | 286 | // TestBodyFlagHelpIsIdentical: both commands document the stdin behaviour in the |
| 213 | 287 | // same words, because they read the same constant. Issue #94's criterion 4. |
| 214 | 288 | func TestBodyFlagHelpIsIdentical(t *testing.T) { |
| @@ -209,6 +209,80 @@ func TestMRCommentAnchoredBlankBodyIsRefused(t *testing.T) { | |||
| 209 | } | 209 | } |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | +// TestIssueCreateBodyDashReadsStdin and TestPRCreateBodyDashReadsStdin close | ||
| 213 | +// rickub/rickub#279's acceptance criterion 3: "-b - reaches the API as the stdin | ||
| 214 | +// content for BOTH issue create and pr create". The comment commands above | ||
| 215 | +// already pin their halves; these pin the two CREATE halves — `pr create -b -` | ||
| 216 | +// is the exact command that once stored a literal "-" and discarded an 8.9 KB | ||
| 217 | +// description with a success line. Each test fails on a parser that uses the | ||
| 218 | +// flag value verbatim (sent == "-"), which is how the pr-create half regressed | ||
| 219 | +// before the shared resolver existed. | ||
| 220 | +func TestIssueCreateBodyDashReadsStdin(t *testing.T) { | ||
| 221 | + var sent string | ||
| 222 | + defer withCreateBodyServer(t, &sent, "/issues")() | ||
| 223 | + | ||
| 224 | + const text = "## Summary\n\nPins the portal gun back on the shelf.\n\n- one\n- two" | ||
| 225 | + if _, _, err := executeStdin(t, text+"\n", "issue", "create", "-R", "rick/portal", | ||
| 226 | + "-t", "the gun slips", "-b", "-"); err != nil { | ||
| 227 | + t.Fatalf("execute: %v", err) | ||
| 228 | + } | ||
| 229 | + if sent == "-" { | ||
| 230 | + t.Fatal(`issue create -b - posted the literal "-" — rickub/rickub#279 has regressed`) | ||
| 231 | + } | ||
| 232 | + if sent != text { | ||
| 233 | + t.Fatalf("posted body = %q, want the stdin text %q", sent, text) | ||
| 234 | + } | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +func TestPRCreateBodyDashReadsStdin(t *testing.T) { | ||
| 238 | + var sent string | ||
| 239 | + defer withCreateBodyServer(t, &sent, "/merge-requests")() | ||
| 240 | + | ||
| 241 | + const text = "## What\n\nA long description piped in from a file, the way MR bodies\nare actually written.\n" | ||
| 242 | + if _, _, err := executeStdin(t, text, "pr", "create", "-R", "rick/portal", | ||
| 243 | + "--base", "main", "--head", "fix/pin-the-gun", "-t", "fix: pin the gun", "-b", "-"); err != nil { | ||
| 244 | + t.Fatalf("execute: %v", err) | ||
| 245 | + } | ||
| 246 | + if sent == "-" { | ||
| 247 | + t.Fatal(`pr create -b - posted the literal "-" — rickub/rickub#279 has regressed`) | ||
| 248 | + } | ||
| 249 | + if sent != strings.TrimRight(text, "\r\n") { | ||
| 250 | + t.Fatalf("posted body = %q, want the stdin text %q", sent, text) | ||
| 251 | + } | ||
| 252 | +} | ||
| 253 | + | ||
| 254 | +// withCreateBodyServer stands up a fake that answers a create endpoint (path | ||
| 255 | +// suffix) and records the request JSON's body field, wiring the CLI at it. The | ||
| 256 | +// suffix keeps the two tests honest about WHICH create they exercised; any | ||
| 257 | +// other path fails the test rather than passing as an empty capture. | ||
| 258 | +func withCreateBodyServer(t *testing.T, got *string, suffix string) func() { | ||
| 259 | + t.Helper() | ||
| 260 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 261 | + if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, suffix) { | ||
| 262 | + t.Errorf("create request used %s %s, want POST …%s", r.Method, r.URL.Path, suffix) | ||
| 263 | + } | ||
| 264 | + var body struct { | ||
| 265 | + Body string `json:"body"` | ||
| 266 | + } | ||
| 267 | + b, err := io.ReadAll(r.Body) | ||
| 268 | + if err != nil { | ||
| 269 | + t.Errorf("reading request body: %v", err) | ||
| 270 | + } | ||
| 271 | + if err := json.Unmarshal(b, &body); err != nil { | ||
| 272 | + t.Errorf("request body %q is not the JSON the CLI should send: %v", b, err) | ||
| 273 | + } | ||
| 274 | + *got = body.Body | ||
| 275 | + w.WriteHeader(http.StatusCreated) | ||
| 276 | + _ = json.NewEncoder(w).Encode(map[string]any{ | ||
| 277 | + "number": 62, "title": "recorded", "state": "open", | ||
| 278 | + }) | ||
| 279 | + })) | ||
| 280 | + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | ||
| 281 | + t.Setenv("RICKUB_HOST", srv.URL) | ||
| 282 | + t.Setenv("RICKUB_TOKEN", "rickub_pat_test") | ||
| 283 | + return srv.Close | ||
| 284 | +} | ||
| 285 | + | ||
| 212 | // TestBodyFlagHelpIsIdentical: both commands document the stdin behaviour in the | 286 | // TestBodyFlagHelpIsIdentical: both commands document the stdin behaviour in the |
| 213 | // same words, because they read the same constant. Issue #94's criterion 4. | 287 | // same words, because they read the same constant. Issue #94's criterion 4. |
| 214 | func TestBodyFlagHelpIsIdentical(t *testing.T) { | 288 | func TestBodyFlagHelpIsIdentical(t *testing.T) { |