mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-24 00:22:29 +02:00
731 lines
24 KiB
Go
731 lines
24 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
testAPIPort string
|
|
testAPIBaseURL string
|
|
testAPIBaseDir string
|
|
)
|
|
|
|
// startRedisForAPI starts the Redis container for API tests
|
|
func startRedisForAPI(t *testing.T, log *TestLogger) func() {
|
|
t.Helper()
|
|
composePath := getDockerComposePath(t)
|
|
|
|
log.Info("Starting Redis container for API tests")
|
|
|
|
cmd := exec.Command("docker-compose", "-f", composePath, "up", "-d")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to start Redis container: %v\nOutput: %s", err, output)
|
|
}
|
|
|
|
// Wait for Redis to be ready
|
|
waitForRedisAPI(t, log, testRedisPort, 15*time.Second)
|
|
|
|
return func() {
|
|
log.Info("Stopping Redis container")
|
|
cmd := exec.Command("docker-compose", "-f", composePath, "down", "-v")
|
|
_ = cmd.Run()
|
|
}
|
|
}
|
|
|
|
// waitForRedisAPI waits for Redis to accept connections
|
|
func waitForRedisAPI(t *testing.T, log *TestLogger, port string, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
log.Info("Waiting for Redis on port %s...", port)
|
|
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
log.Success("Redis is ready")
|
|
return
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
t.Fatalf("Redis not ready after %v", timeout)
|
|
}
|
|
|
|
// cleanAndSeedDatabase cleans and seeds the database using CLI
|
|
func cleanAndSeedDatabase(t *testing.T, log *TestLogger) {
|
|
t.Helper()
|
|
binary := getBinaryPath(t)
|
|
|
|
log.Info("Cleaning database...")
|
|
cmd := exec.Command(binary, "--base-folder", testAPIBaseDir, "db", "clean", "--force")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Warn("Database clean failed (may be empty): %s", string(output))
|
|
}
|
|
|
|
log.Info("Seeding database with sample data...")
|
|
cmd = exec.Command(binary, "--base-folder", testAPIBaseDir, "db", "seed")
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to seed database: %v\nOutput: %s", err, output)
|
|
}
|
|
log.Success("Database seeded successfully")
|
|
}
|
|
|
|
// startAPIServer starts the API server for testing
|
|
func startAPIServer(t *testing.T, log *TestLogger, ctx context.Context) (*exec.Cmd, func()) {
|
|
t.Helper()
|
|
binary := getBinaryPath(t)
|
|
workflowPath := getTestdataPath(t)
|
|
|
|
log.Info("Starting API server on port %s", testAPIPort)
|
|
|
|
cmd := exec.CommandContext(ctx, binary, "serve",
|
|
"--master",
|
|
"--port", testAPIPort,
|
|
"--redis-url", testRedisURL,
|
|
"-A", // Disable auth for testing
|
|
"--base-folder", testAPIBaseDir,
|
|
"-F", workflowPath,
|
|
)
|
|
|
|
err := cmd.Start()
|
|
require.NoError(t, err, "Failed to start API server")
|
|
|
|
// Wait for server to be ready
|
|
waitForAPIServer(t, log, testAPIPort, 15*time.Second)
|
|
|
|
return cmd, func() {
|
|
log.Info("Stopping API server")
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
_ = cmd.Wait()
|
|
}
|
|
}
|
|
}
|
|
|
|
// waitForAPIServer waits for the API server to respond
|
|
func waitForAPIServer(t *testing.T, log *TestLogger, port string, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
url := "http://localhost:" + port + "/health"
|
|
|
|
log.Info("Waiting for API server at %s...", url)
|
|
|
|
for time.Now().Before(deadline) {
|
|
resp, err := http.Get(url)
|
|
if err == nil {
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode == 200 {
|
|
log.Success("API server is ready")
|
|
return
|
|
}
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
t.Fatalf("API server not ready after %v", timeout)
|
|
}
|
|
|
|
// apiGet performs a GET request to the API
|
|
func apiGet(t *testing.T, path string) *http.Response {
|
|
t.Helper()
|
|
resp, err := http.Get(testAPIBaseURL + path)
|
|
require.NoError(t, err, "GET %s failed", path)
|
|
return resp
|
|
}
|
|
|
|
// apiPost performs a POST request to the API
|
|
func apiPost(t *testing.T, path string, body interface{}) *http.Response {
|
|
t.Helper()
|
|
var reader io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
require.NoError(t, err, "Failed to marshal request body")
|
|
reader = bytes.NewReader(jsonBody)
|
|
}
|
|
|
|
resp, err := http.Post(testAPIBaseURL+path, "application/json", reader)
|
|
require.NoError(t, err, "POST %s failed", path)
|
|
return resp
|
|
}
|
|
|
|
// apiPut performs a PUT request to the API
|
|
func apiPut(t *testing.T, path string, body interface{}) *http.Response {
|
|
t.Helper()
|
|
var reader io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
require.NoError(t, err, "Failed to marshal request body")
|
|
reader = bytes.NewReader(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPut, testAPIBaseURL+path, reader)
|
|
require.NoError(t, err, "Failed to create PUT request")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err, "PUT %s failed", path)
|
|
return resp
|
|
}
|
|
|
|
// apiDelete performs a DELETE request to the API
|
|
func apiDelete(t *testing.T, path string) *http.Response {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodDelete, testAPIBaseURL+path, nil)
|
|
require.NoError(t, err, "Failed to create DELETE request")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err, "DELETE %s failed", path)
|
|
return resp
|
|
}
|
|
|
|
// parseJSONResponse parses the JSON response body
|
|
func parseJSONResponse(t *testing.T, resp *http.Response) map[string]interface{} {
|
|
t.Helper()
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err, "Failed to read response body")
|
|
|
|
var result map[string]interface{}
|
|
err = json.Unmarshal(body, &result)
|
|
require.NoError(t, err, "Failed to parse JSON response: %s", string(body))
|
|
|
|
return result
|
|
}
|
|
|
|
// TestAPI_AllEndpoints tests all API endpoints with a shared setup
|
|
func TestAPI_AllEndpoints(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping API e2e test in short mode")
|
|
}
|
|
|
|
log := NewTestLogger(t)
|
|
log.Step("Running API E2E tests")
|
|
|
|
testAPIPort = getFreePort(t)
|
|
testAPIBaseURL = "http://localhost:" + testAPIPort
|
|
testAPIBaseDir = t.TempDir()
|
|
|
|
// Step 1: Start Redis
|
|
log.Step("Step 1: Starting Redis")
|
|
cleanupRedis := startRedisForAPI(t, log)
|
|
defer cleanupRedis()
|
|
|
|
// Step 2: Clean and seed database
|
|
log.Step("Step 2: Cleaning and seeding database")
|
|
cleanAndSeedDatabase(t, log)
|
|
|
|
// Step 3: Start API server
|
|
log.Step("Step 3: Starting API server")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
_, cleanupServer := startAPIServer(t, log, ctx)
|
|
defer cleanupServer()
|
|
|
|
// Step 4: Run subtests
|
|
log.Step("Step 4: Running API endpoint tests")
|
|
|
|
t.Run("Health", func(t *testing.T) {
|
|
testHealthEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Info", func(t *testing.T) {
|
|
testInfoEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Workflows", func(t *testing.T) {
|
|
testWorkflowEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Workspaces", func(t *testing.T) {
|
|
testWorkspaceEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Assets", func(t *testing.T) {
|
|
testAssetEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Stats", func(t *testing.T) {
|
|
testStatsEndpoint(t, log)
|
|
})
|
|
|
|
t.Run("Schedules", func(t *testing.T) {
|
|
testScheduleEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Registry", func(t *testing.T) {
|
|
testRegistryEndpoint(t, log)
|
|
})
|
|
|
|
t.Run("Distributed", func(t *testing.T) {
|
|
testDistributedEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Vulnerabilities", func(t *testing.T) {
|
|
testVulnerabilityEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Runs", func(t *testing.T) {
|
|
testRunEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("EventLogs", func(t *testing.T) {
|
|
testEventLogEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Functions", func(t *testing.T) {
|
|
testFunctionEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("Settings", func(t *testing.T) {
|
|
testSettingsEndpoints(t, log)
|
|
})
|
|
|
|
t.Run("WorkflowExtras", func(t *testing.T) {
|
|
testWorkflowExtras(t, log)
|
|
})
|
|
|
|
log.Success("All API E2E tests passed!")
|
|
}
|
|
|
|
func getFreePort(t *testing.T) string {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err, "Failed to get free port")
|
|
defer func() { _ = ln.Close() }()
|
|
_, port, err := net.SplitHostPort(ln.Addr().String())
|
|
require.NoError(t, err, "Failed to parse free port")
|
|
return port
|
|
}
|
|
|
|
// testHealthEndpoints tests health check endpoints
|
|
func testHealthEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing health endpoints")
|
|
|
|
// GET /health
|
|
resp := apiGet(t, "/health")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /health should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Equal(t, "ok", body["status"], "Health status should be 'ok'")
|
|
|
|
// GET /health/ready
|
|
resp = apiGet(t, "/health/ready")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /health/ready should return 200")
|
|
|
|
log.Success("Health endpoints OK")
|
|
}
|
|
|
|
// testInfoEndpoints tests server info endpoints
|
|
func testInfoEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing info endpoints")
|
|
|
|
// Note: GET / serves the embedded UI (HTML), not JSON API
|
|
// We only test the /server-info JSON endpoint here
|
|
|
|
// GET /server-info
|
|
resp := apiGet(t, "/server-info")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /server-info should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "version", "Server-info should contain version")
|
|
|
|
log.Success("Info endpoints OK")
|
|
}
|
|
|
|
// testWorkflowEndpoints tests workflow endpoints
|
|
func testWorkflowEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing workflow endpoints")
|
|
|
|
// GET /osm/api/workflows (DB-based listing returns pagination)
|
|
resp := apiGet(t, "/osm/api/workflows")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/workflows should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
// Note: DB-based listing returns "pagination" with "total", not "count"
|
|
assert.Contains(t, body, "pagination", "Should contain pagination")
|
|
|
|
// GET /osm/api/workflows/test-bash?json=true (need json=true to get JSON, otherwise returns YAML)
|
|
resp = apiGet(t, "/osm/api/workflows/test-bash?json=true")
|
|
// May return 200 (workflow found) or 404 (workflow not found)
|
|
assert.True(t, resp.StatusCode == 200 || resp.StatusCode == 404,
|
|
"GET /osm/api/workflows/test-bash should return 200 or 404")
|
|
if resp.StatusCode == 200 {
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "name", "Should contain workflow name")
|
|
}
|
|
|
|
log.Success("Workflow endpoints OK")
|
|
}
|
|
|
|
// testWorkspaceEndpoints tests workspace endpoints
|
|
func testWorkspaceEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing workspace endpoints")
|
|
|
|
// GET /osm/api/workspaces (seeded data)
|
|
resp := apiGet(t, "/osm/api/workspaces")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/workspaces should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
// Seeded data should have workspaces
|
|
data, ok := body["data"].([]interface{})
|
|
assert.True(t, ok, "Data should be an array")
|
|
assert.NotEmpty(t, data, "Should have seeded workspaces")
|
|
|
|
log.Success("Workspace endpoints OK")
|
|
}
|
|
|
|
// testAssetEndpoints tests asset endpoints
|
|
func testAssetEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing asset endpoints")
|
|
|
|
// GET /osm/api/assets (seeded data)
|
|
resp := apiGet(t, "/osm/api/assets")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/assets should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
// Seeded data should have 10 assets
|
|
data, ok := body["data"].([]interface{})
|
|
assert.True(t, ok, "Data should be an array")
|
|
assert.NotEmpty(t, data, "Should have seeded assets")
|
|
|
|
log.Success("Asset endpoints OK")
|
|
}
|
|
|
|
// testStatsEndpoint tests stats endpoint
|
|
func testStatsEndpoint(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing stats endpoint")
|
|
|
|
// GET /osm/api/stats
|
|
resp := apiGet(t, "/osm/api/stats")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/stats should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "workflows", "Should contain workflows count")
|
|
assert.Contains(t, body, "assets", "Should contain assets count")
|
|
|
|
log.Success("Stats endpoint OK")
|
|
}
|
|
|
|
// testScheduleEndpoints tests schedule CRUD endpoints
|
|
func testScheduleEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing schedule endpoints")
|
|
|
|
// GET /osm/api/schedules (seeded data)
|
|
resp := apiGet(t, "/osm/api/schedules")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/schedules should return 200")
|
|
|
|
// POST /osm/api/schedules - Create new schedule
|
|
schedule := map[string]interface{}{
|
|
"name": "test-api-schedule",
|
|
"workflow_name": "test-bash",
|
|
"workflow_kind": "module",
|
|
"target": "api-test.com",
|
|
"schedule": "0 0 * * *",
|
|
"enabled": true,
|
|
}
|
|
resp = apiPost(t, "/osm/api/schedules", schedule)
|
|
assert.Equal(t, 201, resp.StatusCode, "POST /osm/api/schedules should return 201")
|
|
body := parseJSONResponse(t, resp)
|
|
// Schedule ID is in the "data" object returned by the API
|
|
data, ok := body["data"].(map[string]interface{})
|
|
require.True(t, ok, "Response should contain data object")
|
|
scheduleID, ok := data["id"].(string)
|
|
require.True(t, ok, "Data should contain schedule id (lowercase)")
|
|
require.NotEmpty(t, scheduleID, "Schedule ID should not be empty")
|
|
|
|
// GET /osm/api/schedules/:id
|
|
resp = apiGet(t, "/osm/api/schedules/"+scheduleID)
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/schedules/:id should return 200")
|
|
|
|
// PUT /osm/api/schedules/:id
|
|
update := map[string]interface{}{"name": "updated-api-schedule"}
|
|
resp = apiPut(t, "/osm/api/schedules/"+scheduleID, update)
|
|
assert.Equal(t, 200, resp.StatusCode, "PUT /osm/api/schedules/:id should return 200")
|
|
|
|
// POST /osm/api/schedules/:id/disable
|
|
resp = apiPost(t, "/osm/api/schedules/"+scheduleID+"/disable", nil)
|
|
assert.Equal(t, 200, resp.StatusCode, "POST /osm/api/schedules/:id/disable should return 200")
|
|
|
|
// POST /osm/api/schedules/:id/enable
|
|
resp = apiPost(t, "/osm/api/schedules/"+scheduleID+"/enable", nil)
|
|
assert.Equal(t, 200, resp.StatusCode, "POST /osm/api/schedules/:id/enable should return 200")
|
|
|
|
// DELETE /osm/api/schedules/:id
|
|
resp = apiDelete(t, "/osm/api/schedules/"+scheduleID)
|
|
assert.Equal(t, 200, resp.StatusCode, "DELETE /osm/api/schedules/:id should return 200")
|
|
|
|
log.Success("Schedule endpoints OK")
|
|
}
|
|
|
|
// testRegistryEndpoint tests metadata registry endpoint
|
|
func testRegistryEndpoint(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing registry endpoint")
|
|
|
|
resp := apiGet(t, "/osm/api/registry-info")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/registry-info should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "registry_url", "Should contain registry_url")
|
|
assert.Contains(t, body, "binaries", "Should contain binaries")
|
|
|
|
log.Success("Registry endpoint OK")
|
|
}
|
|
|
|
// testDistributedEndpoints tests distributed mode endpoints
|
|
func testDistributedEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing distributed endpoints")
|
|
|
|
// GET /osm/api/workers (master mode enabled)
|
|
resp := apiGet(t, "/osm/api/workers")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/workers should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "count", "Should contain worker count")
|
|
|
|
// GET /osm/api/tasks
|
|
resp = apiGet(t, "/osm/api/tasks")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/tasks should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "running", "Should contain running tasks")
|
|
assert.Contains(t, body, "completed", "Should contain completed tasks")
|
|
|
|
log.Success("Distributed endpoints OK")
|
|
}
|
|
|
|
// testVulnerabilityEndpoints tests vulnerability CRUD endpoints
|
|
func testVulnerabilityEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing vulnerability endpoints")
|
|
|
|
// GET /osm/api/vulnerabilities (seeded data)
|
|
resp := apiGet(t, "/osm/api/vulnerabilities")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/vulnerabilities should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
assert.Contains(t, body, "pagination", "Should contain pagination")
|
|
data, ok := body["data"].([]interface{})
|
|
assert.True(t, ok, "Data should be an array")
|
|
assert.NotEmpty(t, data, "Should have seeded vulnerabilities")
|
|
|
|
// GET /osm/api/vulnerabilities/summary
|
|
resp = apiGet(t, "/osm/api/vulnerabilities/summary")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/vulnerabilities/summary should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data")
|
|
summaryData, ok := body["data"].(map[string]interface{})
|
|
assert.True(t, ok, "Data should be an object")
|
|
assert.Contains(t, summaryData, "by_severity", "Should contain severity breakdown")
|
|
assert.Contains(t, summaryData, "total", "Should contain total count")
|
|
|
|
// POST /osm/api/vulnerabilities - Create new vulnerability
|
|
vuln := map[string]interface{}{
|
|
"workspace": "test-workspace",
|
|
"vuln_title": "Test XSS Vulnerability",
|
|
"vuln_info": "XSS in search parameter",
|
|
"vuln_desc": "A reflected XSS vulnerability was found",
|
|
"severity": "high",
|
|
"asset_type": "endpoint",
|
|
"asset_value": "test.example.com",
|
|
"tags": []string{"xss", "test"},
|
|
}
|
|
resp = apiPost(t, "/osm/api/vulnerabilities", vuln)
|
|
assert.Equal(t, 201, resp.StatusCode, "POST /osm/api/vulnerabilities should return 201")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain created data")
|
|
createdData, ok := body["data"].(map[string]interface{})
|
|
require.True(t, ok, "Data should be an object")
|
|
vulnID, ok := createdData["id"].(float64)
|
|
require.True(t, ok, "Should have numeric id")
|
|
require.NotZero(t, vulnID, "Vulnerability ID should not be zero")
|
|
|
|
// GET /osm/api/vulnerabilities/:id
|
|
vulnIDStr := fmt.Sprintf("%.0f", vulnID)
|
|
resp = apiGet(t, "/osm/api/vulnerabilities/"+vulnIDStr)
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/vulnerabilities/:id should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data")
|
|
|
|
// DELETE /osm/api/vulnerabilities/:id
|
|
resp = apiDelete(t, "/osm/api/vulnerabilities/"+vulnIDStr)
|
|
assert.Equal(t, 200, resp.StatusCode, "DELETE /osm/api/vulnerabilities/:id should return 200")
|
|
|
|
// Verify deletion
|
|
resp = apiGet(t, "/osm/api/vulnerabilities/"+vulnIDStr)
|
|
assert.Equal(t, 404, resp.StatusCode, "GET deleted vulnerability should return 404")
|
|
|
|
log.Success("Vulnerability endpoints OK")
|
|
}
|
|
|
|
// testRunEndpoints tests run management endpoints
|
|
func testRunEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing run endpoints")
|
|
|
|
// GET /osm/api/runs
|
|
// Note: Current implementation is a stub that returns empty data
|
|
resp := apiGet(t, "/osm/api/runs")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/runs should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
data, ok := body["data"].([]interface{})
|
|
assert.True(t, ok, "Data should be an array")
|
|
|
|
// Use a test run ID for endpoint testing (handlers are stubs)
|
|
testRunID := "test-run-123"
|
|
|
|
// If we have seeded runs, use the first one
|
|
if len(data) > 0 {
|
|
if firstRun, ok := data[0].(map[string]interface{}); ok {
|
|
if id, ok := firstRun["id"].(string); ok {
|
|
testRunID = id
|
|
}
|
|
}
|
|
}
|
|
|
|
// GET /osm/api/runs/:id
|
|
resp = apiGet(t, "/osm/api/runs/"+testRunID)
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/runs/:id should return 200")
|
|
|
|
// GET /osm/api/runs/:id/steps
|
|
resp = apiGet(t, "/osm/api/runs/"+testRunID+"/steps")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/runs/:id/steps should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain steps data")
|
|
|
|
// GET /osm/api/runs/:id/artifacts
|
|
resp = apiGet(t, "/osm/api/runs/"+testRunID+"/artifacts")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/runs/:id/artifacts should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain artifacts data")
|
|
|
|
// POST /osm/api/runs - Create new run (dry-run mode)
|
|
newRun := map[string]interface{}{
|
|
"workflow_name": "test-bash",
|
|
"target": "test-run.example.com",
|
|
"dry_run": true,
|
|
}
|
|
resp = apiPost(t, "/osm/api/runs", newRun)
|
|
// May return 201 (created) or 202 (accepted) or 400 (if workflow not found)
|
|
// Accept 201, 202, or 400 as valid responses
|
|
assert.True(t, resp.StatusCode == 201 || resp.StatusCode == 202 || resp.StatusCode == 400,
|
|
"POST /osm/api/runs should return 201, 202, or 400 (workflow may not exist)")
|
|
|
|
// DELETE /osm/api/runs/:id (cancel) - test with a test run ID
|
|
resp = apiDelete(t, "/osm/api/runs/"+testRunID)
|
|
// May return 200 (cancelled) or 400 (already completed/failed)
|
|
assert.True(t, resp.StatusCode == 200 || resp.StatusCode == 400,
|
|
"DELETE /osm/api/runs/:id should return 200 or 400")
|
|
|
|
log.Success("Run endpoints OK")
|
|
}
|
|
|
|
// testEventLogEndpoints tests event log endpoints
|
|
func testEventLogEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing event log endpoints")
|
|
|
|
// GET /osm/api/event-logs
|
|
resp := apiGet(t, "/osm/api/event-logs")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/event-logs should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
assert.Contains(t, body, "pagination", "Should contain pagination")
|
|
|
|
// GET /osm/api/event-logs?workspace=example.com (filter)
|
|
resp = apiGet(t, "/osm/api/event-logs?workspace=example.com")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/event-logs with filter should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
|
|
// GET /osm/api/event-logs with limit
|
|
resp = apiGet(t, "/osm/api/event-logs?limit=5")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/event-logs with limit should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "data", "Should contain data array")
|
|
|
|
log.Success("Event log endpoints OK")
|
|
}
|
|
|
|
// testFunctionEndpoints tests function evaluation endpoints
|
|
func testFunctionEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing function endpoints")
|
|
|
|
// GET /osm/api/functions/list
|
|
resp := apiGet(t, "/osm/api/functions/list")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/functions/list should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "functions", "Should contain functions list")
|
|
functions, ok := body["functions"].([]interface{})
|
|
assert.True(t, ok, "Functions should be an array")
|
|
assert.NotEmpty(t, functions, "Should have available functions")
|
|
|
|
// POST /osm/api/functions/eval - Evaluate a simple script
|
|
// Note: Uses "script" field, not "expression"
|
|
evalReq := map[string]interface{}{
|
|
"script": "1 + 1",
|
|
}
|
|
resp = apiPost(t, "/osm/api/functions/eval", evalReq)
|
|
assert.Equal(t, 200, resp.StatusCode, "POST /osm/api/functions/eval should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "result", "Should contain result")
|
|
|
|
// POST /osm/api/functions/eval - Evaluate with target variable
|
|
evalReq = map[string]interface{}{
|
|
"script": "'hello ' + target",
|
|
"target": "world",
|
|
}
|
|
resp = apiPost(t, "/osm/api/functions/eval", evalReq)
|
|
assert.Equal(t, 200, resp.StatusCode, "POST /osm/api/functions/eval with target should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "result", "Should contain result")
|
|
|
|
log.Success("Function endpoints OK")
|
|
}
|
|
|
|
// testSettingsEndpoints tests settings management endpoints
|
|
func testSettingsEndpoints(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing settings endpoints")
|
|
|
|
// GET /osm/api/settings/yaml - returns raw YAML text, not JSON
|
|
resp := apiGet(t, "/osm/api/settings/yaml")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/settings/yaml should return 200")
|
|
// Read raw body - endpoint returns YAML text, not JSON
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err, "Should be able to read response body")
|
|
yamlContent := string(bodyBytes)
|
|
assert.NotEmpty(t, yamlContent, "Settings YAML should not be empty")
|
|
// Verify it looks like YAML (contains common config keys)
|
|
assert.Contains(t, yamlContent, "server:", "Should contain server config")
|
|
|
|
log.Success("Settings endpoints OK")
|
|
}
|
|
|
|
// testWorkflowExtras tests additional workflow endpoints
|
|
func testWorkflowExtras(t *testing.T, log *TestLogger) {
|
|
log.Info("Testing workflow extra endpoints")
|
|
|
|
// GET /osm/api/workflows/tags
|
|
resp := apiGet(t, "/osm/api/workflows/tags")
|
|
assert.Equal(t, 200, resp.StatusCode, "GET /osm/api/workflows/tags should return 200")
|
|
body := parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "tags", "Should contain tags array")
|
|
_, ok := body["tags"].([]interface{})
|
|
assert.True(t, ok, "Tags should be an array")
|
|
// May be empty if no workflows have tags, but should still be an array
|
|
|
|
// POST /osm/api/workflows/refresh - Refresh workflow index
|
|
resp = apiPost(t, "/osm/api/workflows/refresh", nil)
|
|
assert.Equal(t, 200, resp.StatusCode, "POST /osm/api/workflows/refresh should return 200")
|
|
body = parseJSONResponse(t, resp)
|
|
assert.Contains(t, body, "message", "Should contain message")
|
|
|
|
log.Success("Workflow extra endpoints OK")
|
|
}
|