diff --git a/internal/cli/handlers.go b/internal/cli/handlers.go index efb1e3c..6126697 100644 --- a/internal/cli/handlers.go +++ b/internal/cli/handlers.go @@ -11,6 +11,7 @@ import ( "time" agent "github.com/Octrafic/octrafic-cli/internal/agents" + "github.com/Octrafic/octrafic-cli/internal/core/auth" "github.com/Octrafic/octrafic-cli/internal/core/parser" "github.com/Octrafic/octrafic-cli/internal/core/reporter" "github.com/Octrafic/octrafic-cli/internal/core/tester" @@ -920,6 +921,18 @@ func (m *TestUIModel) handleExportTests(toolCall agent.ToolCall) tea.Msg { authData["key_value"] = m.currentProject.AuthConfig.KeyValue authData["username"] = m.currentProject.AuthConfig.Username authData["password"] = m.currentProject.AuthConfig.Password + } else if m.authProvider != nil { + authType = m.authProvider.Type() + switch p := m.authProvider.(type) { + case *auth.BearerAuth: + authData["token"] = p.Token + case *auth.APIKeyAuth: + authData["key_name"] = p.Key + authData["key_value"] = p.Value + case *auth.BasicAuth: + authData["username"] = p.Username + authData["password"] = p.Password + } } var exportResults []map[string]any diff --git a/internal/core/parser/parser.go b/internal/core/parser/parser.go index 118bb0e..b401bbc 100644 --- a/internal/core/parser/parser.go +++ b/internal/core/parser/parser.go @@ -172,10 +172,15 @@ func parseOpenAPI(content []byte) (*Specification, error) { } } + globalSecurity := hasSecurityRequirement(openapi["security"]) + if paths, ok := openapi["paths"].(map[string]any); ok { for path, methods := range paths { if methodMap, ok := methods.(map[string]any); ok { for method, details := range methodMap { + if method == "parameters" || method == "summary" || method == "description" { + continue + } endpoint := Endpoint{ Method: strings.ToUpper(method), Path: path, @@ -193,6 +198,12 @@ func parseOpenAPI(content []byte) (*Specification, error) { } } + if _, hasSecurity := detailsMap["security"]; hasSecurity { + endpoint.RequiresAuth = hasSecurityRequirement(detailsMap["security"]) + } else { + endpoint.RequiresAuth = globalSecurity + } + if responses, ok := detailsMap["responses"].(map[string]any); ok { for statusCode, respData := range responses { if respMap, ok := respData.(map[string]any); ok { @@ -217,6 +228,21 @@ func parseOpenAPI(content []byte) (*Specification, error) { return spec, nil } +// hasSecurityRequirement returns true if the security value is a non-empty array +// containing at least one non-empty security requirement object. +func hasSecurityRequirement(v any) bool { + reqs, ok := v.([]any) + if !ok || len(reqs) == 0 { + return false + } + for _, req := range reqs { + if m, ok := req.(map[string]any); ok && len(m) > 0 { + return true + } + } + return false +} + // extractResponseSchema pulls the JSON schema from a response object. // Handles both OpenAPI 3.x (content/application/json/schema) and Swagger 2.0 (schema). func extractResponseSchema(response map[string]any, definitions map[string]any) map[string]any { diff --git a/internal/core/reporter/reporter.go b/internal/core/reporter/reporter.go index 6f22587..beed3b4 100644 --- a/internal/core/reporter/reporter.go +++ b/internal/core/reporter/reporter.go @@ -2,6 +2,7 @@ package reporter import ( "bytes" + "context" "fmt" "os" "os/exec" @@ -215,9 +216,13 @@ func GeneratePDF(markdownContent string, outputPath string) (string, error) { } _ = tmpFile.Close() - // Convert HTML to PDF using weasyprint - cmd := exec.Command("weasyprint", tmpFile.Name(), absPath) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "weasyprint", tmpFile.Name(), absPath) if output, err := cmd.CombinedOutput(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("weasyprint timed out after 30s") + } return "", fmt.Errorf("weasyprint failed: %s — %w", string(output), err) } diff --git a/internal/exporter/curl.go b/internal/exporter/curl.go index cf68573..771aeee 100644 --- a/internal/exporter/curl.go +++ b/internal/exporter/curl.go @@ -17,7 +17,21 @@ func (e *CurlExporter) Export(req ExportRequest) error { script.WriteString("#!/bin/bash\n\n") fmt.Fprintf(&script, "# Generated curl commands for %s\n", req.BaseURL) - fmt.Fprintf(&script, "BASE_URL=\"%s\"\n\n", req.BaseURL) + fmt.Fprintf(&script, "BASE_URL=\"%s\"\n", req.BaseURL) + + if req.AuthType != "" { + script.WriteString("\n# Set credentials via environment variables before running\n") + switch req.AuthType { + case "bearer": + script.WriteString("# export AUTH_TOKEN=your_token\n") + case "apikey": + script.WriteString("# export API_KEY_VALUE=your_key\n") + case "basic": + script.WriteString("# export AUTH_USER=your_username\n") + script.WriteString("# export AUTH_PASS=your_password\n") + } + } + script.WriteString("\n") for i, test := range req.Tests { if i > 0 { @@ -42,30 +56,26 @@ func (e *CurlExporter) buildCurlCommand(test TestData, req ExportRequest) string parts = append(parts, "curl") parts = append(parts, fmt.Sprintf("-X %s", test.Method)) - parts = append(parts, "-H 'Content-Type: application/json'") - for key, value := range test.Headers { parts = append(parts, fmt.Sprintf("-H '%s: %s'", key, value)) } + if test.Body != nil { + if bodyStr, ok := test.Body.(string); ok && bodyStr != "" { + parts = append(parts, "-H 'Content-Type: application/json'") + } + } + if test.RequiresAuth && req.AuthType != "" { switch req.AuthType { case "bearer": - if token, ok := req.AuthData["token"]; ok { - parts = append(parts, fmt.Sprintf("-H 'Authorization: Bearer %s'", token)) - } + parts = append(parts, "-H 'Authorization: Bearer ${AUTH_TOKEN}'") case "apikey": if keyName, ok := req.AuthData["key_name"]; ok { - if keyValue, ok := req.AuthData["key_value"]; ok { - parts = append(parts, fmt.Sprintf("-H '%s: %s'", keyName, keyValue)) - } + parts = append(parts, fmt.Sprintf("-H '%s: ${API_KEY_VALUE}'", keyName)) } case "basic": - if username, ok := req.AuthData["username"]; ok { - if password, ok := req.AuthData["password"]; ok { - parts = append(parts, fmt.Sprintf("-u '%s:%s'", username, password)) - } - } + parts = append(parts, "-u '${AUTH_USER}:${AUTH_PASS}'") } } diff --git a/internal/exporter/postman.go b/internal/exporter/postman.go index b4a01e2..bbc15b5 100644 --- a/internal/exporter/postman.go +++ b/internal/exporter/postman.go @@ -109,20 +109,16 @@ func (e *PostmanExporter) buildHeaders(test TestData, req ExportRequest) []map[s if test.RequiresAuth && req.AuthType != "" { switch req.AuthType { case "bearer": - if token, ok := req.AuthData["token"]; ok { - headers = append(headers, map[string]interface{}{ - "key": "Authorization", - "value": "Bearer " + token, - }) - } + headers = append(headers, map[string]interface{}{ + "key": "Authorization", + "value": "Bearer {{AUTH_TOKEN}}", + }) case "apikey": if keyName, ok := req.AuthData["key_name"]; ok { - if keyValue, ok := req.AuthData["key_value"]; ok { - headers = append(headers, map[string]interface{}{ - "key": keyName, - "value": keyValue, - }) - } + headers = append(headers, map[string]interface{}{ + "key": keyName, + "value": "{{API_KEY_VALUE}}", + }) } } } diff --git a/internal/exporter/pytest.go b/internal/exporter/pytest.go index 43404c2..ffebd78 100644 --- a/internal/exporter/pytest.go +++ b/internal/exporter/pytest.go @@ -15,31 +15,24 @@ func (e *PytestExporter) FileExtension() string { func (e *PytestExporter) Export(req ExportRequest) error { var script strings.Builder + script.WriteString("import os\n") script.WriteString("import requests\n") script.WriteString("import pytest\n\n") fmt.Fprintf(&script, "BASE_URL = \"%s\"\n\n", req.BaseURL) if req.AuthType != "" { - script.WriteString("# Authentication configuration\n") + script.WriteString("# Set credentials via environment variables before running\n") switch req.AuthType { case "bearer": - if token, ok := req.AuthData["token"]; ok { - fmt.Fprintf(&script, "AUTH_TOKEN = \"%s\"\n", token) - } + script.WriteString("AUTH_TOKEN = os.environ[\"AUTH_TOKEN\"]\n") case "apikey": if keyName, ok := req.AuthData["key_name"]; ok { - if keyValue, ok := req.AuthData["key_value"]; ok { - fmt.Fprintf(&script, "API_KEY_NAME = \"%s\"\n", keyName) - fmt.Fprintf(&script, "API_KEY_VALUE = \"%s\"\n", keyValue) - } + fmt.Fprintf(&script, "API_KEY_NAME = \"%s\"\n", keyName) + script.WriteString("API_KEY_VALUE = os.environ[\"API_KEY_VALUE\"]\n") } case "basic": - if username, ok := req.AuthData["username"]; ok { - if password, ok := req.AuthData["password"]; ok { - fmt.Fprintf(&script, "AUTH_USER = \"%s\"\n", username) - fmt.Fprintf(&script, "AUTH_PASS = \"%s\"\n", password) - } - } + script.WriteString("AUTH_USER = os.environ[\"AUTH_USER\"]\n") + script.WriteString("AUTH_PASS = os.environ[\"AUTH_PASS\"]\n") } script.WriteString("\n") } @@ -50,9 +43,17 @@ func (e *PytestExporter) Export(req ExportRequest) error { fmt.Fprintf(&script, " \"\"\"%s %s\"\"\"\n", test.Method, test.Endpoint) fmt.Fprintf(&script, " url = BASE_URL + \"%s\"\n", test.Endpoint) - script.WriteString(" headers = {\"Content-Type\": \"application/json\"") + hasBody := test.Body != nil + if bodyStr, ok := test.Body.(string); ok && bodyStr == "" { + hasBody = false + } + if hasBody { + script.WriteString(" headers = {\"Content-Type\": \"application/json\"") + } else { + script.WriteString(" headers = {") + } for key, value := range test.Headers { - fmt.Fprintf(&script, ", \"%s\": \"%s\"", key, value) + fmt.Fprintf(&script, "\"%s\": \"%s\", ", key, value) } script.WriteString("}\n")