package maintenance import ( "bytes" "context" "crypto/ecdh" "crypto/rand" "os" "strconv" "testing" "time" ) func resultPipe(t *testing.T) (*os.File, *os.File) { t.Helper() r, w, err := os.Pipe() if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = r.Close(); _ = w.Close() }) return r, w } func resultContext(t *testing.T) context.Context { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) t.Cleanup(cancel) return ctx } func TestCaptureResultCompleteBoundaries(t *testing.T) { for _, n := range []int{0, 1, MaxResultBytes} { t.Run(strconv.Itoa(n), func(t *testing.T) { r, w := resultPipe(t) body := bytes.Repeat([]byte{'x'}, n) done := make(chan error, 1) go func() { _, e := w.Write(body); _ = w.Close(); done <- e }() got, err := CaptureResult(resultContext(t), r) if err != nil || !bytes.Equal(got, body) { t.Fatalf("size%d capture failed: %v", n, err) } if err := <-done; err != nil { t.Fatal(err) } if _, err := r.Stat(); err == nil { t.Fatal("capture retained its owned read pipe") } }) } } func TestCaptureResultOverflowWithoutEOF(t *testing.T) { r, w := resultPipe(t) done := make(chan error, 1) go func() { _, err := w.Write(bytes.Repeat([]byte{'x'}, MaxResultBytes+1)); done <- err }() body, err := CaptureResult(resultContext(t), r) if body != nil || err != ErrResultOverflow { t.Fatalf("overflow = %d bytes, %v", len(body), err) } if err := <-done; err != nil { t.Fatal(err) } // The write end is deliberately still open: overflow cannot require EOF. if _, err := w.Stat(); err != nil { t.Fatal("writer closed before overflow observation") } } func TestCaptureResultCancellationDiscardsPartialBytes(t *testing.T) { r, w := resultPipe(t) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() type answer struct { body []byte err error } done := make(chan answer, 1) go func() { b, e := CaptureResult(ctx, r); done <- answer{b, e} }() if _, err := w.Write([]byte("synthetic-private-fragment")); err != nil { t.Fatal(err) } cancel() select { case got := <-done: if got.body != nil || got.err != ErrResult { t.Fatal("cancel returned partial bytes or success") } case <-time.After(time.Second): t.Fatal("cancellation did not unblock the actual pipe") } if _, err := r.Stat(); err == nil { t.Fatal("cancel retained read descriptor") } if _, err := w.Stat(); err != nil { t.Fatal("capture claimed to terminate independent writer") } } func TestCaptureResultRequiresEOFBeforeDeadline(t *testing.T) { r, _ := resultPipe(t) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() done := make(chan error, 1) go func() { b, e := CaptureResult(ctx, r) if b != nil { done <- nil return } done <- e }() select { case err := <-done: if err != ErrResult { t.Fatal("open writer accepted without EOF") } case <-time.After(time.Second): t.Fatal("deadline did not close pipe") } } func TestCaptureResultRefusesUnboundedOrNonpipeInput(t *testing.T) { long, cancelLong := context.WithTimeout(context.Background(), MaxExecutionDuration+time.Minute) defer cancelLong() expired, cancelExpired := context.WithCancel(resultContext(t)) cancelExpired() for _, ctx := range []context.Context{nil, context.Background(), long, expired} { r, _ := resultPipe(t) if b, e := CaptureResult(ctx, r); b != nil || e != ErrResult { t.Fatal("invalid context accepted") } if _, e := r.Stat(); e == nil { t.Fatal("refusal retained read pipe") } } f, e := os.CreateTemp(t.TempDir(), "regular") if e != nil { t.Fatal(e) } if b, e := CaptureResult(resultContext(t), f); b != nil || e != ErrResult { t.Fatal("regular file accepted as private stdout") } if b, e := CaptureResult(resultContext(t), nil); b != nil || e != ErrResult { t.Fatal("nil pipe accepted") } } func TestResultEnvelopePurposeAndLimits(t *testing.T) { key, err := ecdh.P256().GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } fingerprint, err := RecipientFingerprint(key.PublicKey()) if err != nil { t.Fatal(err) } binding := Binding{Version: 1, Direction: DirectionResult, Account: "account", App: "home", Host: "test.example", RequestID: "request", AttemptID: "attempt", Recipient: fingerprint} for _, n := range []int{0, 1, MaxResultBytes} { body := bytes.Repeat([]byte{'r'}, n) sealed, err := SealResult(key.PublicKey(), binding, body) if err != nil { t.Fatal(err) } got, err := OpenResult(key, binding, sealed) if err != nil || !bytes.Equal(body, got) { t.Fatalf("result roundtrip size%d: %v", n, err) } } tooLarge := bytes.Repeat([]byte{'r'}, MaxResultBytes+1) if _, err := SealResult(key.PublicKey(), binding, tooLarge); err != ErrResult { t.Fatal("oversized seal accepted") } // Generic crypto accepts this size. Base64's rounding also fits the result // encoded-length bound, so the separate decoded-length check must reject it. sealed, err := Seal(key.PublicKey(), binding, tooLarge) if err != nil { t.Fatal(err) } if b, err := OpenResult(key, binding, sealed); b != nil || err != ErrResult { t.Fatal("oversized decoded result accepted") } for _, direction := range []Direction{DirectionInput, DirectionEnvironment} { wrong := binding wrong.Direction = direction if _, err := SealResult(key.PublicKey(), wrong, []byte("ok")); err != ErrResult { t.Fatal("wrong seal purpose accepted") } sealed, err := Seal(key.PublicKey(), wrong, []byte("ok")) if err != nil { t.Fatal(err) } if b, err := OpenResult(key, wrong, sealed); b != nil || err != ErrResult { t.Fatal("wrong open purpose accepted") } } }