Skip to content

Commit f9872bb

Browse files
committed
Add more tests
1 parent 7f92a21 commit f9872bb

5 files changed

Lines changed: 244 additions & 16 deletions

File tree

README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ RTask is a secure task runner with API key management that exposes system comman
2424

2525
```bash
2626
git clone <repository-url>
27-
cd rtask.go
27+
cd rtask
2828
go build
2929
```
3030

@@ -61,12 +61,8 @@ executionTimeoutSeconds = 300
6161
command = ["/usr/local/bin/process-webhook.sh"]
6262
async = true
6363
passRequestHeaders = ["X-GitHub-Event", "X-Hub-Signature"]
64-
65-
[tasks.webhook-handler.webhookSecrets]
66-
github = "your-webhook-secret-hash"
67-
68-
[tasks.webhook-handler.webhookSecretFiles]
69-
gitlab = "/etc/rtask/gitlab-webhook-secret"
64+
webhookSecrets.github = "your-webhook-secret-hash"
65+
webhookSecretFiles.gitlab = "/etc/rtask/gitlab-webhook-secret"
7066

7167
[tasks.webhook-handler.environment]
7268
LOG_LEVEL = "info"

handler.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -218,17 +218,18 @@ func (tm *TaskManager) handleRunTask(w http.ResponseWriter, r *http.Request) {
218218
}
219219
}
220220

221+
// Read and validate input for both async and sync cases
222+
var bb []byte
223+
bb, err = io.ReadAll(ctx.stdin)
224+
if err != nil {
225+
http.Error(w, "Invalid content", http.StatusBadRequest)
226+
tm.counterRejection.WithLabelValues("invalid_content").Inc()
227+
return
228+
}
229+
ctx.stdin = bytes.NewReader(bb)
230+
221231
// == async / synchronous
222232
if tm.config.Async {
223-
var bb []byte
224-
bb, err = io.ReadAll(ctx.stdin)
225-
if err != nil {
226-
http.Error(w, "Invalid content", http.StatusBadRequest)
227-
tm.counterRejection.WithLabelValues("invalid_content").Inc()
228-
return
229-
}
230-
ctx.stdin = bytes.NewReader(bb)
231-
232233
// Copy key name
233234
ctx.ctx = context.WithValue(context.Background(), keyNameContextKey, ctx.ctx.Value(keyNameContextKey))
234235
go func() {

handler_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,3 +781,222 @@ func TestAsyncTask_Cleanup(t *testing.T) {
781781
t.Errorf("Expected 'task not found' error, got '%s'", errorResponse["error"])
782782
}
783783
}
784+
785+
// Test: MergeStderr configuration
786+
func TestTask_MergeStderr(t *testing.T) {
787+
store, apiKey := createTestKeyStore(t)
788+
789+
tasks := map[string]Task{
790+
"merge-stderr-false": {
791+
Command: []string{getTestScriptPath(t, "stderr_test.sh")},
792+
APIKeyNames: []string{"test-key"},
793+
Async: false,
794+
MergeStderr: false, // Keep separate
795+
ExecutionTimeoutSeconds: 5,
796+
},
797+
"merge-stderr-true": {
798+
Command: []string{getTestScriptPath(t, "stderr_test.sh")},
799+
APIKeyNames: []string{"test-key"},
800+
Async: false,
801+
MergeStderr: true, // Merge into stdout
802+
ExecutionTimeoutSeconds: 5,
803+
},
804+
}
805+
806+
router := createTestRouter(t, tasks, store)
807+
808+
// Test with MergeStderr = false (separate streams)
809+
req := httptest.NewRequest("POST", "/tasks/merge-stderr-false", nil)
810+
req.Header.Set("Authorization", "Bearer "+apiKey)
811+
w := httptest.NewRecorder()
812+
router.ServeHTTP(w, req)
813+
814+
var result1 taskExecutionResult
815+
json.NewDecoder(w.Body).Decode(&result1)
816+
817+
if !strings.Contains(result1.StdOut, "This is stdout") {
818+
t.Errorf("Expected stdout to contain 'This is stdout', got '%s'", result1.StdOut)
819+
}
820+
if !strings.Contains(result1.StdErr, "This is stderr") {
821+
t.Errorf("Expected stderr to contain 'This is stderr', got '%s'", result1.StdErr)
822+
}
823+
if strings.Contains(result1.StdOut, "This is stderr") {
824+
t.Errorf("stderr should not be in stdout when MergeStderr=false")
825+
}
826+
827+
// Test with MergeStderr = true (merged into stdout)
828+
req = httptest.NewRequest("POST", "/tasks/merge-stderr-true", nil)
829+
req.Header.Set("Authorization", "Bearer "+apiKey)
830+
w = httptest.NewRecorder()
831+
router.ServeHTTP(w, req)
832+
833+
var result2 taskExecutionResult
834+
json.NewDecoder(w.Body).Decode(&result2)
835+
836+
if !strings.Contains(result2.StdOut, "This is stdout") {
837+
t.Errorf("Expected stdout to contain 'This is stdout', got '%s'", result2.StdOut)
838+
}
839+
if !strings.Contains(result2.StdOut, "This is stderr") {
840+
t.Errorf("Expected stderr to be merged into stdout, got '%s'", result2.StdOut)
841+
}
842+
if result2.StdErr != "" {
843+
t.Errorf("Expected stderr to be empty when MergeStderr=true, got '%s'", result2.StdErr)
844+
}
845+
}
846+
847+
// Test: MaxInputBytes limit
848+
func TestTask_MaxInputBytes(t *testing.T) {
849+
store, apiKey := createTestKeyStore(t)
850+
851+
tasks := map[string]Task{
852+
"limited-input": {
853+
Command: []string{getTestScriptPath(t, "echo_stdin.sh")},
854+
APIKeyNames: []string{"test-key"},
855+
Async: false,
856+
MaxInputBytes: 10, // Only 10 bytes allowed
857+
ExecutionTimeoutSeconds: 5,
858+
},
859+
}
860+
861+
router := createTestRouter(t, tasks, store)
862+
863+
// Test with input within limit
864+
smallInput := "small"
865+
req := httptest.NewRequest("POST", "/tasks/limited-input", strings.NewReader(smallInput))
866+
req.Header.Set("Authorization", "Bearer "+apiKey)
867+
w := httptest.NewRecorder()
868+
router.ServeHTTP(w, req)
869+
870+
if w.Code != http.StatusOK {
871+
t.Errorf("Expected status 200 for small input, got %d", w.Code)
872+
}
873+
874+
// Test with input exceeding limit
875+
largeInput := strings.Repeat("x", 100)
876+
req = httptest.NewRequest("POST", "/tasks/limited-input", strings.NewReader(largeInput))
877+
req.Header.Set("Authorization", "Bearer "+apiKey)
878+
w = httptest.NewRecorder()
879+
router.ServeHTTP(w, req)
880+
881+
if w.Code != http.StatusBadRequest {
882+
t.Errorf("Expected status 400 for large input, got %d", w.Code)
883+
}
884+
}
885+
886+
// Test: MaxOutputBytes limit
887+
func TestTask_MaxOutputBytes(t *testing.T) {
888+
store, apiKey := createTestKeyStore(t)
889+
890+
tasks := map[string]Task{
891+
"limited-output": {
892+
Command: []string{getTestScriptPath(t, "large_output.sh")},
893+
APIKeyNames: []string{"test-key"},
894+
Async: false,
895+
MaxOutputBytes: 50, // Only 50 bytes
896+
ExecutionTimeoutSeconds: 5,
897+
},
898+
}
899+
900+
router := createTestRouter(t, tasks, store)
901+
902+
req := httptest.NewRequest("POST", "/tasks/limited-output", nil)
903+
req.Header.Set("Authorization", "Bearer "+apiKey)
904+
w := httptest.NewRecorder()
905+
router.ServeHTTP(w, req)
906+
907+
if w.Code != http.StatusOK {
908+
t.Errorf("Expected status 200, got %d", w.Code)
909+
}
910+
911+
var result taskExecutionResult
912+
json.NewDecoder(w.Body).Decode(&result)
913+
914+
// Output should be truncated to MaxOutputBytes
915+
if len(result.StdOut) > 50 {
916+
t.Errorf("Expected stdout to be truncated to 50 bytes, got %d bytes", len(result.StdOut))
917+
}
918+
}
919+
920+
// Test: WebhookSecrets
921+
func TestTask_WebhookSecrets(t *testing.T) {
922+
store, _ := createTestKeyStore(t)
923+
924+
webhookHash := "test-webhook-hash-123"
925+
926+
tasks := map[string]Task{
927+
"webhook-task": {
928+
Command: []string{getTestScriptPath(t, "fast_task.sh")},
929+
Async: false,
930+
WebhookSecrets: map[string]string{"webhook1": webhookHash},
931+
ExecutionTimeoutSeconds: 5,
932+
},
933+
}
934+
935+
router := createTestRouter(t, tasks, store)
936+
937+
// Test webhook without API key (should work)
938+
req := httptest.NewRequest("POST", "/wh/"+webhookHash, nil)
939+
w := httptest.NewRecorder()
940+
router.ServeHTTP(w, req)
941+
942+
if w.Code != http.StatusOK {
943+
t.Errorf("Expected status 200 for webhook, got %d", w.Code)
944+
}
945+
946+
var result taskExecutionResult
947+
json.NewDecoder(w.Body).Decode(&result)
948+
949+
if result.Status != "success" {
950+
t.Errorf("Expected status 'success', got '%s'", result.Status)
951+
}
952+
953+
// Test with wrong webhook hash (should 404)
954+
req = httptest.NewRequest("POST", "/wh/wrong-hash", nil)
955+
w = httptest.NewRecorder()
956+
router.ServeHTTP(w, req)
957+
958+
if w.Code != http.StatusNotFound {
959+
t.Errorf("Expected status 404 for wrong webhook hash, got %d", w.Code)
960+
}
961+
}
962+
963+
// Test: WebhookSecretFiles
964+
func TestTask_WebhookSecretFiles(t *testing.T) {
965+
store, _ := createTestKeyStore(t)
966+
967+
// Create a temporary file with webhook hash
968+
tempDir := t.TempDir()
969+
webhookFile := tempDir + "/webhook-secret"
970+
webhookHash := "file-webhook-hash-456"
971+
err := os.WriteFile(webhookFile, []byte(webhookHash+"\n"), 0600)
972+
if err != nil {
973+
t.Fatalf("Failed to create webhook file: %v", err)
974+
}
975+
976+
tasks := map[string]Task{
977+
"webhook-file-task": {
978+
Command: []string{getTestScriptPath(t, "fast_task.sh")},
979+
Async: false,
980+
WebhookSecretFiles: map[string]string{"webhook2": webhookFile},
981+
ExecutionTimeoutSeconds: 5,
982+
},
983+
}
984+
985+
router := createTestRouter(t, tasks, store)
986+
987+
// Test webhook from file (should work)
988+
req := httptest.NewRequest("POST", "/wh/"+webhookHash, nil)
989+
w := httptest.NewRecorder()
990+
router.ServeHTTP(w, req)
991+
992+
if w.Code != http.StatusOK {
993+
t.Errorf("Expected status 200 for webhook from file, got %d", w.Code)
994+
}
995+
996+
var result taskExecutionResult
997+
json.NewDecoder(w.Body).Decode(&result)
998+
999+
if result.Status != "success" {
1000+
t.Errorf("Expected status 'success', got '%s'", result.Status)
1001+
}
1002+
}

testdata/large_output.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
# Task that generates large output
3+
# Generate 100 bytes to stdout
4+
for i in {1..10}; do
5+
echo "0123456789"
6+
done
7+
exit 0

testdata/stderr_test.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
# Task that outputs to both stdout and stderr
3+
echo "This is stdout"
4+
echo "This is stderr" >&2
5+
exit 0

0 commit comments

Comments
 (0)