Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/cli/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions internal/core/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
9 changes: 7 additions & 2 deletions internal/core/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package reporter

import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -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)
}

Expand Down
38 changes: 24 additions & 14 deletions internal/exporter/curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}'")
}
}

Expand Down
20 changes: 8 additions & 12 deletions internal/exporter/postman.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
})
}
}
}
Expand Down
33 changes: 17 additions & 16 deletions internal/exporter/pytest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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")

Expand Down