Files
osmedeus/test/e2e/install_test.go

893 lines
27 KiB
Go

package e2e
import (
"archive/zip"
"context"
"database/sql"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestData holds database state for verification
type TestData struct {
Workspaces []WorkspaceRecord
Assets []AssetRecord
Vulns []VulnRecord
}
// WorkspaceRecord represents a workspace in the database
type WorkspaceRecord struct {
ID string
Name string
}
// AssetRecord represents an asset in the database
type AssetRecord struct {
ID string
Workspace string
AssetValue string
}
// VulnRecord represents a vulnerability in the database
type VulnRecord struct {
ID string
Workspace string
VulnTitle string
}
// getExamplesPath returns the path to the public/examples directory
func getExamplesPath(t *testing.T) string {
t.Helper()
_, filename, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("Failed to get caller info")
}
return filepath.Join(filepath.Dir(filename), "..", "..", "public", "examples")
}
// setupTestBase creates an isolated test base folder with configuration
func setupTestBase(t *testing.T) (basePath string, workspacesPath string, cleanup func()) {
t.Helper()
// Create temp directory for test base
tempDir, err := os.MkdirTemp("", "osmedeus-test-base-*")
require.NoError(t, err)
basePath = tempDir
workspacesPath = filepath.Join(tempDir, "workspaces")
// Create necessary directories
dirs := []string{
filepath.Join(basePath, "workflows", "modules"),
filepath.Join(basePath, "workflows", "flows"),
filepath.Join(basePath, "external-binaries"),
filepath.Join(basePath, "external-data"),
filepath.Join(basePath, "external-configs"),
filepath.Join(basePath, "snapshot"),
workspacesPath,
}
for _, dir := range dirs {
require.NoError(t, os.MkdirAll(dir, 0755))
}
// Create minimal osm-settings.yaml
settingsContent := `base_folder: ` + basePath + `
environments:
external_binaries_path: "` + basePath + `/external-binaries"
external_data: "` + basePath + `/external-data"
external_configs: "` + basePath + `/external-configs"
workspaces: "` + workspacesPath + `"
workflows: "` + basePath + `/workflows"
snapshot: "` + basePath + `/snapshot"
database:
db_engine: sqlite
db_path: "` + basePath + `/database-osm.sqlite"
server:
host: "127.0.0.1"
port: 18099
simple_user_map_key:
test: test
jwt:
secret_signing_key: test-secret
expiration_minutes: 60
`
settingsPath := filepath.Join(basePath, "osm-settings.yaml")
require.NoError(t, os.WriteFile(settingsPath, []byte(settingsContent), 0644))
cleanup = func() {
_ = os.RemoveAll(tempDir)
}
return basePath, workspacesPath, cleanup
}
// createZipFromDir creates a zip file from a directory
func createZipFromDir(t *testing.T, sourceDir, destZip string) string {
t.Helper()
zipFile, err := os.Create(destZip)
require.NoError(t, err)
defer func() { _ = zipFile.Close() }()
zipWriter := zip.NewWriter(zipFile)
defer func() { _ = zipWriter.Close() }()
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Get relative path
relPath, err := filepath.Rel(sourceDir, path)
if err != nil {
return err
}
// Skip the root directory
if relPath == "." {
return nil
}
// Create zip header
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
header.Method = zip.Deflate
if info.IsDir() {
header.Name += "/"
_, err = zipWriter.CreateHeader(header)
return err
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
file, err := os.Open(path)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = io.Copy(writer, file)
return err
})
require.NoError(t, err)
return destZip
}
// startZipServer starts an httptest server that serves a zip file
func startZipServer(t *testing.T, zipPath string) *httptest.Server {
t.Helper()
zipData, err := os.ReadFile(zipPath)
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=download.zip")
if _, err := w.Write(zipData); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
return server
}
// createGitRepo creates a local git repository from a source directory
// The repo path ends with .git so it's recognized as a git URL
func createGitRepo(t *testing.T, sourceDir string) (repoPath string, cleanup func()) {
t.Helper()
// Create temp directory for git repo
tempDir, err := os.MkdirTemp("", "osmedeus-test-git-*")
require.NoError(t, err)
// Use a path ending with .git so it's recognized as a git URL
repoPath = filepath.Join(tempDir, "repo.git")
// Copy source to repo directory
err = copyDir(sourceDir, repoPath)
require.NoError(t, err)
// Initialize git repo
cmds := [][]string{
{"git", "init"},
{"git", "config", "user.email", "test@test.com"},
{"git", "config", "user.name", "Test"},
{"git", "add", "."},
{"git", "commit", "-m", "Initial commit"},
}
for _, args := range cmds {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = repoPath
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Git command failed: %s\nOutput: %s", args, output)
}
cleanup = func() {
_ = os.RemoveAll(tempDir)
}
return repoPath, cleanup
}
// copyDir recursively copies a directory
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
destPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(destPath, info.Mode())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(destPath, data, info.Mode())
})
}
// seedTestDatabase seeds the test database with sample data
func seedTestDatabase(t *testing.T, log *TestLogger, dbPath string) TestData {
t.Helper()
log.Info("Seeding test database at %s", dbPath)
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer func() { _ = db.Close() }()
// Create tables if they don't exist
createTablesSQL := `
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
local_path TEXT,
data_source TEXT DEFAULT 'local',
total_assets INTEGER DEFAULT 0,
total_vulns INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assets (
id TEXT PRIMARY KEY,
workspace TEXT NOT NULL,
asset_value TEXT,
url TEXT,
asset_type TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS vulnerabilities (
id TEXT PRIMARY KEY,
workspace TEXT NOT NULL,
vuln_title TEXT,
severity TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workflow_metas (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
kind TEXT,
file_path TEXT,
checksum TEXT,
indexed_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
_, err = db.Exec(createTablesSQL)
require.NoError(t, err)
testData := TestData{
Workspaces: []WorkspaceRecord{
{ID: "ws-001", Name: "test-workspace-1"},
{ID: "ws-002", Name: "test-workspace-2"},
},
Assets: []AssetRecord{
{ID: "asset-001", Workspace: "test-workspace-1", AssetValue: "http://example.com"},
{ID: "asset-002", Workspace: "test-workspace-1", AssetValue: "http://api.example.com"},
{ID: "asset-003", Workspace: "test-workspace-2", AssetValue: "http://test.local"},
},
Vulns: []VulnRecord{
{ID: "vuln-001", Workspace: "test-workspace-1", VulnTitle: "SQL Injection"},
{ID: "vuln-002", Workspace: "test-workspace-1", VulnTitle: "XSS"},
},
}
// Insert workspaces
for _, ws := range testData.Workspaces {
_, err = db.Exec("INSERT OR REPLACE INTO workspaces (id, name) VALUES (?, ?)", ws.ID, ws.Name)
require.NoError(t, err)
}
// Insert assets
for _, asset := range testData.Assets {
_, err = db.Exec("INSERT OR REPLACE INTO assets (id, workspace, asset_value) VALUES (?, ?, ?)",
asset.ID, asset.Workspace, asset.AssetValue)
require.NoError(t, err)
}
// Insert vulns
for _, vuln := range testData.Vulns {
_, err = db.Exec("INSERT OR REPLACE INTO vulnerabilities (id, workspace, vuln_title) VALUES (?, ?, ?)",
vuln.ID, vuln.Workspace, vuln.VulnTitle)
require.NoError(t, err)
}
log.Success("Database seeded with %d workspaces, %d assets, %d vulns",
len(testData.Workspaces), len(testData.Assets), len(testData.Vulns))
return testData
}
// getTestData retrieves current database state
func getTestData(t *testing.T, dbPath string) TestData {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer func() { _ = db.Close() }()
testData := TestData{}
// Get workspaces
rows, err := db.Query("SELECT id, name FROM workspaces")
require.NoError(t, err)
defer func() { _ = rows.Close() }()
for rows.Next() {
var ws WorkspaceRecord
require.NoError(t, rows.Scan(&ws.ID, &ws.Name))
testData.Workspaces = append(testData.Workspaces, ws)
}
// Get assets
rows, err = db.Query("SELECT id, workspace, asset_value FROM assets")
require.NoError(t, err)
defer func() { _ = rows.Close() }()
for rows.Next() {
var asset AssetRecord
require.NoError(t, rows.Scan(&asset.ID, &asset.Workspace, &asset.AssetValue))
testData.Assets = append(testData.Assets, asset)
}
// Get vulns
rows, err = db.Query("SELECT id, workspace, vuln_title FROM vulnerabilities")
require.NoError(t, err)
defer func() { _ = rows.Close() }()
for rows.Next() {
var vuln VulnRecord
require.NoError(t, rows.Scan(&vuln.ID, &vuln.Workspace, &vuln.VulnTitle))
testData.Vulns = append(testData.Vulns, vuln)
}
return testData
}
// verifyDatabasePreserved checks that workspace, asset, and vuln records are unchanged
func verifyDatabasePreserved(t *testing.T, log *TestLogger, dbPath string, expected TestData) {
t.Helper()
log.Info("Verifying database preservation...")
actual := getTestData(t, dbPath)
// Verify workspaces
assert.Equal(t, len(expected.Workspaces), len(actual.Workspaces),
"Workspace count mismatch: expected %d, got %d", len(expected.Workspaces), len(actual.Workspaces))
for _, expectedWs := range expected.Workspaces {
found := false
for _, actualWs := range actual.Workspaces {
if actualWs.ID == expectedWs.ID && actualWs.Name == expectedWs.Name {
found = true
break
}
}
assert.True(t, found, "Workspace not found: %s", expectedWs.Name)
}
// Verify assets
assert.Equal(t, len(expected.Assets), len(actual.Assets),
"Asset count mismatch: expected %d, got %d", len(expected.Assets), len(actual.Assets))
for _, expectedAsset := range expected.Assets {
found := false
for _, actualAsset := range actual.Assets {
if actualAsset.ID == expectedAsset.ID {
found = true
assert.Equal(t, expectedAsset.AssetValue, actualAsset.AssetValue)
break
}
}
assert.True(t, found, "Asset not found: %s", expectedAsset.ID)
}
// Verify vulns
assert.Equal(t, len(expected.Vulns), len(actual.Vulns),
"Vuln count mismatch: expected %d, got %d", len(expected.Vulns), len(actual.Vulns))
for _, expectedVuln := range expected.Vulns {
found := false
for _, actualVuln := range actual.Vulns {
if actualVuln.ID == expectedVuln.ID {
found = true
assert.Equal(t, expectedVuln.VulnTitle, actualVuln.VulnTitle)
break
}
}
assert.True(t, found, "Vuln not found: %s", expectedVuln.ID)
}
log.Success("Database preservation verified!")
}
// runInstallWithBase runs the osmedeus install command with a custom base folder
func runInstallWithBase(t *testing.T, log *TestLogger, basePath string, args ...string) (stdout, stderr string, err error) {
t.Helper()
binary := getBinaryPath(t)
// Prepend base folder flag
fullArgs := append([]string{"--base-folder", basePath}, args...)
log.Command(fullArgs...)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, binary, fullArgs...)
var stdoutBuf, stderrBuf strings.Builder
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err = cmd.Run()
stdout = stdoutBuf.String()
stderr = stderrBuf.String()
log.Result(stdout, stderr)
return stdout, stderr, err
}
// =============================================================================
// Test Cases
// =============================================================================
// TestInstall_WorkflowFromLocalZip tests installing workflows from a local zip file
func TestInstall_WorkflowFromLocalZip(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Workflow from Local Zip")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Create zip from example workflows
examplesPath := getExamplesPath(t)
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
}
zipPath := filepath.Join(basePath, "workflows-test.zip")
createZipFromDir(t, workflowsExamplePath, zipPath)
log.Info("Created test zip at %s", zipPath)
// Run install command
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipPath)
require.NoError(t, err, "Install workflow failed: %s\n%s", stdout, stderr)
// Verify workflows were installed
workflowsPath := filepath.Join(basePath, "workflows")
modulesPath := filepath.Join(workflowsPath, "modules")
flowsPath := filepath.Join(workflowsPath, "flows")
// Check modules exist
expectedModules := []string{"subdomain-enum.yaml", "port-scan.yaml", "http-probe.yaml", "screenshot.yaml", "vuln-scan.yaml"}
for _, module := range expectedModules {
modulePath := filepath.Join(modulesPath, module)
assert.FileExists(t, modulePath, "Module not installed: %s", module)
}
// Check flows exist
expectedFlows := []string{"basic-recon.yaml", "vuln-assessment.yaml", "full-scan.yaml"}
for _, flow := range expectedFlows {
flowPath := filepath.Join(flowsPath, flow)
assert.FileExists(t, flowPath, "Flow not installed: %s", flow)
}
log.Success("Workflow installation from local zip verified!")
}
// TestInstall_WorkflowFromZipURL tests installing workflows from a URL
func TestInstall_WorkflowFromZipURL(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Workflow from Zip URL")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Create zip from example workflows
examplesPath := getExamplesPath(t)
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
}
zipPath := filepath.Join(basePath, "workflows-url-test.zip")
createZipFromDir(t, workflowsExamplePath, zipPath)
// Start httptest server
server := startZipServer(t, zipPath)
defer server.Close()
zipURL := server.URL + "/workflows.zip"
log.Info("Serving zip at %s", zipURL)
// Run install command
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipURL)
require.NoError(t, err, "Install workflow from URL failed: %s\n%s", stdout, stderr)
// Verify workflows were installed
modulesPath := filepath.Join(basePath, "workflows", "modules")
assert.DirExists(t, modulesPath, "Modules directory not created")
files, err := os.ReadDir(modulesPath)
require.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 5, "Expected at least 5 module files")
log.Success("Workflow installation from URL verified!")
}
// TestInstall_WorkflowFromGitURL tests installing workflows from a git repository
func TestInstall_WorkflowFromGitURL(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Workflow from Git URL")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Create git repo from example workflows
examplesPath := getExamplesPath(t)
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
}
repoPath, gitCleanup := createGitRepo(t, workflowsExamplePath)
defer gitCleanup()
log.Info("Created git repo at %s", repoPath)
// Run install command with local git path (ends with .git so it's recognized)
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "workflow", repoPath)
require.NoError(t, err, "Install workflow from git URL failed: %s\n%s", stdout, stderr)
// Verify workflows were installed
modulesPath := filepath.Join(basePath, "workflows", "modules")
assert.DirExists(t, modulesPath, "Modules directory not created")
files, err := os.ReadDir(modulesPath)
require.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 5, "Expected at least 5 module files")
log.Success("Workflow installation from git URL verified!")
}
// TestInstall_BaseFromLocalZip tests installing base folder from a local zip file
func TestInstall_BaseFromLocalZip(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Base from Local Zip")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Seed database with test data
dbPath := filepath.Join(basePath, "database-osm.sqlite")
expectedData := seedTestDatabase(t, log, dbPath)
// Create zip from example base
examplesPath := getExamplesPath(t)
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
t.Skipf("Example base not found at %s", baseExamplePath)
}
zipPath := filepath.Join(os.TempDir(), "base-test.zip")
createZipFromDir(t, baseExamplePath, zipPath)
defer func() { _ = os.Remove(zipPath) }()
log.Info("Created test zip at %s", zipPath)
// Run install base command
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipPath)
require.NoError(t, err, "Install base failed: %s\n%s", stdout, stderr)
// Verify base folder structure
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
assert.DirExists(t, filepath.Join(basePath, "workflows", "flows"))
assert.DirExists(t, filepath.Join(basePath, "external-binaries"))
assert.DirExists(t, filepath.Join(basePath, "external-data"))
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
// CRITICAL: Verify database was preserved
verifyDatabasePreserved(t, log, dbPath, expectedData)
log.Success("Base installation from local zip verified with database preservation!")
}
// TestInstall_BaseFromZipURL tests installing base folder from a URL
func TestInstall_BaseFromZipURL(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Base from Zip URL")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Seed database with test data
dbPath := filepath.Join(basePath, "database-osm.sqlite")
expectedData := seedTestDatabase(t, log, dbPath)
// Create zip from example base
examplesPath := getExamplesPath(t)
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
t.Skipf("Example base not found at %s", baseExamplePath)
}
zipPath := filepath.Join(os.TempDir(), "base-url-test.zip")
createZipFromDir(t, baseExamplePath, zipPath)
defer func() { _ = os.Remove(zipPath) }()
// Start httptest server
server := startZipServer(t, zipPath)
defer server.Close()
zipURL := server.URL + "/base.zip"
log.Info("Serving zip at %s", zipURL)
// Run install base command
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipURL)
require.NoError(t, err, "Install base from URL failed: %s\n%s", stdout, stderr)
// Verify base folder structure
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
// CRITICAL: Verify database was preserved
verifyDatabasePreserved(t, log, dbPath, expectedData)
log.Success("Base installation from URL verified with database preservation!")
}
// TestInstall_BaseFromGitURL tests installing base folder from a git repository
func TestInstall_BaseFromGitURL(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Base from Git URL")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Seed database with test data
dbPath := filepath.Join(basePath, "database-osm.sqlite")
expectedData := seedTestDatabase(t, log, dbPath)
// Create git repo from example base
examplesPath := getExamplesPath(t)
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
t.Skipf("Example base not found at %s", baseExamplePath)
}
repoPath, gitCleanup := createGitRepo(t, baseExamplePath)
defer gitCleanup()
log.Info("Created git repo at %s", repoPath)
// Run install base command with local git path (ends with .git so it's recognized)
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", repoPath)
require.NoError(t, err, "Install base from git URL failed: %s\n%s", stdout, stderr)
// Verify base folder structure
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
// CRITICAL: Verify database was preserved
verifyDatabasePreserved(t, log, dbPath, expectedData)
log.Success("Base installation from git URL verified with database preservation!")
}
func TestInstall_BinaryAutoInitBaseSample(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Install Binary auto-initializes base sample")
basePath := t.TempDir()
_, _, _ = runInstallWithBase(t, log, basePath, "install", "binary", "--name", "nuclei", "--check")
assert.DirExists(t, filepath.Join(basePath, "workflows", "modules"))
}
// TestInstall_DatabasePreservation is a comprehensive test for database preservation
func TestInstall_DatabasePreservation(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Comprehensive Database Preservation Verification")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
dbPath := filepath.Join(basePath, "database-osm.sqlite")
// Step 1: Seed database with specific test data
log.Info("Step 1: Seeding database with test data...")
expectedData := seedTestDatabase(t, log, dbPath)
// Add workflow_metas entries (these should change after install)
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = db.Exec("INSERT INTO workflow_metas (id, name, kind) VALUES ('old-wf-1', 'old-workflow', 'module')")
require.NoError(t, err)
_ = db.Close()
// Step 2: Record initial state
log.Info("Step 2: Recording initial database state...")
initialData := getTestData(t, dbPath)
log.Info("Initial state: %d workspaces, %d assets, %d vulns",
len(initialData.Workspaces), len(initialData.Assets), len(initialData.Vulns))
// Step 3: Create and install new base
log.Info("Step 3: Installing new base folder...")
examplesPath := getExamplesPath(t)
baseExamplePath := filepath.Join(examplesPath, "osmedeus-base.example")
if _, err := os.Stat(baseExamplePath); os.IsNotExist(err) {
t.Skipf("Example base not found at %s", baseExamplePath)
}
zipPath := filepath.Join(os.TempDir(), "base-preservation-test.zip")
createZipFromDir(t, baseExamplePath, zipPath)
defer func() { _ = os.Remove(zipPath) }()
stdout, stderr, err := runInstallWithBase(t, log, basePath, "install", "base", zipPath)
require.NoError(t, err, "Install base failed: %s\n%s", stdout, stderr)
// Step 4: Verify exact same records exist
log.Info("Step 4: Verifying database records...")
verifyDatabasePreserved(t, log, dbPath, expectedData)
// Step 5: Verify workflow table can be updated (new workflows should be indexed)
log.Info("Step 5: Verifying workflow indexing capability...")
// The old workflow_metas entry should have been removed when base was replaced
// New workflows from the installed base should be indexable
// (Actual re-indexing happens via 'osmedeus workflow refresh' or server startup)
// Verify the new osm-settings.yaml exists
assert.FileExists(t, filepath.Join(basePath, "osm-settings.yaml"))
// Step 6: Final verification
finalData := getTestData(t, dbPath)
assert.Equal(t, len(expectedData.Workspaces), len(finalData.Workspaces),
"Workspace count changed after install")
assert.Equal(t, len(expectedData.Assets), len(finalData.Assets),
"Asset count changed after install")
assert.Equal(t, len(expectedData.Vulns), len(finalData.Vulns),
"Vuln count changed after install")
log.Success("Comprehensive database preservation test passed!")
log.Info("Verified: Workspaces, Assets, and Vulnerabilities remain unchanged")
log.Info("Verified: Base folder structure was updated")
log.Info("Verified: WorkflowMeta table can be re-indexed independently")
}
// TestInstall_WorkflowListAfterInstall verifies that workflow list command works after install
func TestInstall_WorkflowListAfterInstall(t *testing.T) {
if testing.Short() {
t.Skip("skipping install e2e test in short mode")
}
log := NewTestLogger(t)
log.Step("Test: Workflow List After Install")
// Setup test base
basePath, _, cleanup := setupTestBase(t)
defer cleanup()
// Create and install workflows
examplesPath := getExamplesPath(t)
workflowsExamplePath := filepath.Join(examplesPath, "osmedeus-base.example", "workflows")
if _, err := os.Stat(workflowsExamplePath); os.IsNotExist(err) {
t.Skipf("Example workflows not found at %s", workflowsExamplePath)
}
zipPath := filepath.Join(basePath, "workflows-list-test.zip")
createZipFromDir(t, workflowsExamplePath, zipPath)
// Install workflows
_, _, err := runInstallWithBase(t, log, basePath, "install", "workflow", zipPath)
require.NoError(t, err)
// Run workflow list command
stdout, stderr, _ := runInstallWithBase(t, log, basePath, "workflow", "list")
// Note: workflow list might return error if no workflows are indexed yet
// but we're checking that it doesn't crash
log.Info("Workflow list output: %s", stdout)
if stderr != "" {
log.Info("Workflow list stderr: %s", stderr)
}
// The command should at least not panic
// Success if we got here without a panic
log.Success("Workflow list command executed successfully after install!")
}