-
Notifications
You must be signed in to change notification settings - Fork 0
fix: RemoteStep writes HTTP response to _http_response_writer on StopPipeline #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ff9e71a
c826db9
ba9754f
a7cf89d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ package external | |
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/GoCodeAlone/workflow/module" | ||
|
|
@@ -158,3 +160,118 @@ func TestRemoteStep_Execute_StaticConfigPassthrough(t *testing.T) { | |
| t.Errorf("expected timeout=30, got %v", sent["timeout"]) | ||
| } | ||
| } | ||
|
|
||
| // TestRemoteStep_Execute_WritesHTTPResponseOnStopPipeline verifies that when a | ||
| // remote plugin returns StopPipeline=true with response_status, response_body, | ||
| // and response_headers in the output, the RemoteStep writes those to the | ||
| // _http_response_writer and sets _response_handled=true in pipeline metadata. | ||
| func TestRemoteStep_Execute_WritesHTTPResponseOnStopPipeline(t *testing.T) { | ||
| output, _ := structpb.NewStruct(map[string]any{ | ||
| "response_status": float64(http.StatusForbidden), | ||
| "response_body": `{"error":"forbidden: insufficient permissions"}`, | ||
| "response_headers": map[string]any{ | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }) | ||
| stub := &stubPluginServiceClient{ | ||
| response: &pb.ExecuteStepResponse{ | ||
| Output: output, | ||
| StopPipeline: true, | ||
| }, | ||
| } | ||
| step := NewRemoteStep("authz-step", "handle-authz", stub, nil) | ||
|
|
||
| recorder := httptest.NewRecorder() | ||
| pc := module.NewPipelineContext(nil, nil) | ||
| pc.Metadata["_http_response_writer"] = http.ResponseWriter(recorder) | ||
|
|
||
| result, err := step.Execute(context.Background(), pc) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
|
|
||
| if !result.Stop { | ||
| t.Error("expected Stop=true") | ||
| } | ||
|
|
||
| if recorder.Code != http.StatusForbidden { | ||
| t.Errorf("expected HTTP 403, got %d", recorder.Code) | ||
| } | ||
|
|
||
| body := recorder.Body.String() | ||
| if body != `{"error":"forbidden: insufficient permissions"}` { | ||
| t.Errorf("unexpected body: %q", body) | ||
| } | ||
|
|
||
| if pc.Metadata["_response_handled"] != true { | ||
| t.Error("expected _response_handled=true in metadata") | ||
| } | ||
| } | ||
|
Comment on lines
+164
to
+209
|
||
|
|
||
| // TestRemoteStep_Execute_NoHTTPWriterOnStopPipeline verifies that when there is | ||
| // no _http_response_writer in the pipeline metadata, the step still returns the | ||
| // correct StepResult without panicking. | ||
| func TestRemoteStep_Execute_NoHTTPWriterOnStopPipeline(t *testing.T) { | ||
| output, _ := structpb.NewStruct(map[string]any{ | ||
| "response_status": float64(http.StatusForbidden), | ||
| "response_body": `{"error":"forbidden"}`, | ||
| }) | ||
| stub := &stubPluginServiceClient{ | ||
| response: &pb.ExecuteStepResponse{ | ||
| Output: output, | ||
| StopPipeline: true, | ||
| }, | ||
| } | ||
| step := NewRemoteStep("authz-step", "handle-authz2", stub, nil) | ||
|
|
||
| pc := module.NewPipelineContext(nil, nil) | ||
|
|
||
| result, err := step.Execute(context.Background(), pc) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if !result.Stop { | ||
| t.Error("expected Stop=true") | ||
| } | ||
| if pc.Metadata["_response_handled"] == true { | ||
| t.Error("expected _response_handled to remain unset when no writer present") | ||
| } | ||
| } | ||
|
|
||
| // TestRemoteStep_Execute_StopPipelineWithoutResponseStatus verifies that a | ||
| // StopPipeline=true result without response_status does NOT write to the | ||
| // response writer and does NOT set _response_handled. | ||
| func TestRemoteStep_Execute_StopPipelineWithoutResponseStatus(t *testing.T) { | ||
| output, _ := structpb.NewStruct(map[string]any{ | ||
| "some_output": "value", | ||
| }) | ||
| stub := &stubPluginServiceClient{ | ||
| response: &pb.ExecuteStepResponse{ | ||
| Output: output, | ||
| StopPipeline: true, | ||
| }, | ||
| } | ||
| step := NewRemoteStep("authz-step", "handle-authz3", stub, nil) | ||
|
|
||
| recorder := httptest.NewRecorder() | ||
| pc := module.NewPipelineContext(nil, nil) | ||
| pc.Metadata["_http_response_writer"] = http.ResponseWriter(recorder) | ||
|
|
||
| result, err := step.Execute(context.Background(), pc) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if !result.Stop { | ||
| t.Error("expected Stop=true") | ||
| } | ||
| // No response_status in output → should not write anything | ||
| if recorder.Code != http.StatusOK { // recorder default is 200 until WriteHeader is called | ||
| t.Errorf("expected recorder to be untouched (200), got %d", recorder.Code) | ||
| } | ||
| if recorder.Body.Len() != 0 { | ||
| t.Errorf("expected empty body, got %q", recorder.Body.String()) | ||
| } | ||
| if pc.Metadata["_response_handled"] == true { | ||
| t.Error("expected _response_handled to remain unset") | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
output := structToMap(resp.Output)can be nil when the plugin returns a response withoutputunset. In theStopPipelinebranch the code indexesoutput["response_status"], which will panic on a nil map. Initializeoutputto an empty map (or guardoutput != nil) before readingresponse_status/response_headers/response_body.