icebergConfig = config.toIcebergConfig();
+ Catalog backendCatalog =
+ org.apache.iceberg.CatalogUtil.buildIcebergCatalog("backend", icebergConfig, null);
+
+ // Start ICE REST catalog server
+ server = createServer("localhost", 8080, backendCatalog, config, icebergConfig);
+ server.start();
+
+ // Wait for server to be ready
+ while (!server.isStarted()) {
+ Thread.sleep(100);
+ }
+
+ // Server is ready for CLI commands
+ }
+
+ @AfterClass
+ public void tearDown() {
+
+ // Stop the REST catalog server
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ logger.error("Error stopping server: {}", e.getMessage(), e);
+ }
+ }
+
+ // Stop minio container
+ if (minio != null && minio.isRunning()) {
+ minio.stop();
+ }
+ }
+
+ /** Helper method to create a temporary CLI config file */
+ protected File createTempCliConfig() throws Exception {
+ File tempConfigFile = File.createTempFile("ice-rest-cli-", ".yaml");
+ tempConfigFile.deleteOnExit();
+
+ String configContent = "uri: http://localhost:8080\n";
+ Files.write(tempConfigFile.toPath(), configContent.getBytes());
+
+ return tempConfigFile;
+ }
+
+ /** Get the MinIO endpoint URL */
+ protected String getMinioEndpoint() {
+ return "http://" + minio.getHost() + ":" + minio.getMappedPort(9000);
+ }
+
+ /** Get the REST catalog URI */
+ protected String getCatalogUri() {
+ return "http://localhost:8080";
+ }
+}
diff --git a/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioBasedIT.java b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioBasedIT.java
new file mode 100644
index 0000000..151e555
--- /dev/null
+++ b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioBasedIT.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.altinity.ice.rest.catalog;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Scenario-based integration tests for ICE REST Catalog.
+ *
+ * This test class automatically discovers and executes all test scenarios from the
+ * test/resources/scenarios directory. Each scenario is run as a separate test case.
+ */
+public class ScenarioBasedIT extends RESTCatalogTestBase {
+
+ /**
+ * Data provider that discovers all test scenarios.
+ *
+ * @return Array of scenario names to be used as test parameters
+ * @throws Exception If there's an error discovering scenarios
+ */
+ @DataProvider(name = "scenarios")
+ public Object[][] scenarioProvider() throws Exception {
+ Path scenariosDir = getScenariosDirectory();
+ ScenarioTestRunner runner = createScenarioRunner();
+
+ List scenarios = runner.discoverScenarios();
+
+ if (scenarios.isEmpty()) {
+ logger.warn("No test scenarios found in: {}", scenariosDir);
+ return new Object[0][0];
+ }
+
+ logger.info("Discovered {} test scenario(s): {}", scenarios.size(), scenarios);
+
+ // Convert to Object[][] for TestNG data provider
+ Object[][] data = new Object[scenarios.size()][1];
+ for (int i = 0; i < scenarios.size(); i++) {
+ data[i][0] = scenarios.get(i);
+ }
+ return data;
+ }
+
+ /**
+ * Parameterized test that executes a single scenario.
+ *
+ * @param scenarioName Name of the scenario to execute
+ * @throws Exception If the scenario execution fails
+ */
+ @Test(dataProvider = "scenarios")
+ public void testScenario(String scenarioName) throws Exception {
+ logger.info("====== Starting scenario test: {} ======", scenarioName);
+
+ ScenarioTestRunner runner = createScenarioRunner();
+ ScenarioTestRunner.ScenarioResult result = runner.executeScenario(scenarioName);
+
+ // Log results
+ if (result.getRunScriptResult() != null) {
+ logger.info("Run script exit code: {}", result.getRunScriptResult().getExitCode());
+ }
+
+ if (result.getVerifyScriptResult() != null) {
+ logger.info("Verify script exit code: {}", result.getVerifyScriptResult().getExitCode());
+ }
+
+ // Assert success
+ if (!result.isSuccess()) {
+ StringBuilder errorMessage = new StringBuilder();
+ errorMessage.append("Scenario '").append(scenarioName).append("' failed:\n");
+
+ if (result.getRunScriptResult() != null && result.getRunScriptResult().getExitCode() != 0) {
+ errorMessage.append("\nRun script failed with exit code: ");
+ errorMessage.append(result.getRunScriptResult().getExitCode());
+ errorMessage.append("\nStdout:\n");
+ errorMessage.append(result.getRunScriptResult().getStdout());
+ errorMessage.append("\nStderr:\n");
+ errorMessage.append(result.getRunScriptResult().getStderr());
+ }
+
+ if (result.getVerifyScriptResult() != null
+ && result.getVerifyScriptResult().getExitCode() != 0) {
+ errorMessage.append("\nVerify script failed with exit code: ");
+ errorMessage.append(result.getVerifyScriptResult().getExitCode());
+ errorMessage.append("\nStdout:\n");
+ errorMessage.append(result.getVerifyScriptResult().getStdout());
+ errorMessage.append("\nStderr:\n");
+ errorMessage.append(result.getVerifyScriptResult().getStderr());
+ }
+
+ throw new AssertionError(errorMessage.toString());
+ }
+
+ logger.info("====== Scenario test passed: {} ======", scenarioName);
+ }
+
+ /**
+ * Create a ScenarioTestRunner with the appropriate template variables.
+ *
+ * @return Configured ScenarioTestRunner
+ * @throws Exception If there's an error creating the runner
+ */
+ private ScenarioTestRunner createScenarioRunner() throws Exception {
+ Path scenariosDir = getScenariosDirectory();
+
+ // Create CLI config file
+ File cliConfig = createTempCliConfig();
+
+ // Build template variables
+ Map templateVars = new HashMap<>();
+ templateVars.put("CLI_CONFIG", cliConfig.getAbsolutePath());
+ templateVars.put("MINIO_ENDPOINT", getMinioEndpoint());
+ templateVars.put("CATALOG_URI", getCatalogUri());
+
+ // Try to find ice-jar in the build
+ String projectRoot = Paths.get("").toAbsolutePath().getParent().toString();
+ String iceJar = projectRoot + "/ice/target/ice-jar";
+ File iceJarFile = new File(iceJar);
+
+ if (iceJarFile.exists() && iceJarFile.canExecute()) {
+ // Use pre-built ice-jar if available
+ templateVars.put("ICE_CLI", iceJar);
+ logger.info("Using ice-jar from: {}", iceJar);
+ } else {
+ // Fall back to using local-ice wrapper script
+ String localIce = projectRoot + "/.bin/local-ice";
+ templateVars.put("ICE_CLI", localIce);
+ logger.info("Using local-ice script from: {}", localIce);
+ }
+
+ return new ScenarioTestRunner(scenariosDir, templateVars);
+ }
+
+ /**
+ * Get the path to the scenarios directory.
+ *
+ * @return Path to scenarios directory
+ * @throws URISyntaxException If the resource URL cannot be converted to a path
+ */
+ private Path getScenariosDirectory() throws URISyntaxException {
+ // Get the scenarios directory from test resources
+ URL scenariosUrl = getClass().getClassLoader().getResource("scenarios");
+
+ if (scenariosUrl == null) {
+ // If not found in resources, try relative to project
+ return Paths.get("src/test/resources/scenarios");
+ }
+
+ return Paths.get(scenariosUrl.toURI());
+ }
+}
diff --git a/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioConfig.java b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioConfig.java
new file mode 100644
index 0000000..3bfac6e
--- /dev/null
+++ b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioConfig.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.altinity.ice.rest.catalog;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Configuration class representing a test scenario loaded from scenario.yaml.
+ *
+ * This class uses Jackson/SnakeYAML annotations for YAML deserialization.
+ */
+public class ScenarioConfig {
+
+ private String name;
+ private String description;
+ private CatalogConfig catalogConfig;
+ private Map env;
+ private CloudResources cloudResources;
+ private List phases;
+
+ public static class CatalogConfig {
+ private String warehouse;
+ private String name;
+ private String uri;
+
+ public String getWarehouse() {
+ return warehouse;
+ }
+
+ public void setWarehouse(String warehouse) {
+ this.warehouse = warehouse;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+ }
+
+ public static class CloudResources {
+ private S3Resources s3;
+ private SqsResources sqs;
+
+ public S3Resources getS3() {
+ return s3;
+ }
+
+ public void setS3(S3Resources s3) {
+ this.s3 = s3;
+ }
+
+ public SqsResources getSqs() {
+ return sqs;
+ }
+
+ public void setSqs(SqsResources sqs) {
+ this.sqs = sqs;
+ }
+ }
+
+ public static class S3Resources {
+ private List buckets;
+
+ public List getBuckets() {
+ return buckets;
+ }
+
+ public void setBuckets(List buckets) {
+ this.buckets = buckets;
+ }
+ }
+
+ public static class SqsResources {
+ private List queues;
+
+ public List getQueues() {
+ return queues;
+ }
+
+ public void setQueues(List queues) {
+ this.queues = queues;
+ }
+ }
+
+ public static class Phase {
+ private String name;
+ private String description;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+ }
+
+ // Getters and setters
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public CatalogConfig getCatalogConfig() {
+ return catalogConfig;
+ }
+
+ public void setCatalogConfig(CatalogConfig catalogConfig) {
+ this.catalogConfig = catalogConfig;
+ }
+
+ public Map getEnv() {
+ return env;
+ }
+
+ public void setEnv(Map env) {
+ this.env = env;
+ }
+
+ public CloudResources getCloudResources() {
+ return cloudResources;
+ }
+
+ public void setCloudResources(CloudResources cloudResources) {
+ this.cloudResources = cloudResources;
+ }
+
+ public List getPhases() {
+ return phases;
+ }
+
+ public void setPhases(List phases) {
+ this.phases = phases;
+ }
+}
+
+
diff --git a/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioTestRunner.java b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioTestRunner.java
new file mode 100644
index 0000000..3f3b24f
--- /dev/null
+++ b/ice-rest-catalog/src/test/java/com/altinity/ice/rest/catalog/ScenarioTestRunner.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2025 Altinity Inc and/or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.altinity.ice.rest.catalog;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test runner for scenario-based integration tests.
+ *
+ * This class discovers scenario directories, loads their configuration, processes script
+ * templates, and executes them in a controlled test environment.
+ */
+public class ScenarioTestRunner {
+
+ private static final Logger logger = LoggerFactory.getLogger(ScenarioTestRunner.class);
+ private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
+
+ private final Path scenariosDir;
+ private final Map globalTemplateVars;
+
+ /**
+ * Create a new scenario test runner.
+ *
+ * @param scenariosDir Path to the scenarios directory
+ * @param globalTemplateVars Global template variables available to all scenarios
+ */
+ public ScenarioTestRunner(Path scenariosDir, Map globalTemplateVars) {
+ this.scenariosDir = scenariosDir;
+ this.globalTemplateVars = new HashMap<>(globalTemplateVars);
+ }
+
+ /**
+ * Discover all scenario directories.
+ *
+ * @return List of scenario names (directory names)
+ * @throws IOException If there's an error reading the scenarios directory
+ */
+ public List discoverScenarios() throws IOException {
+ if (!Files.exists(scenariosDir) || !Files.isDirectory(scenariosDir)) {
+ logger.warn("Scenarios directory does not exist: {}", scenariosDir);
+ return new ArrayList<>();
+ }
+
+ return Files.list(scenariosDir)
+ .filter(Files::isDirectory)
+ .map(path -> path.getFileName().toString())
+ .filter(name -> !name.startsWith(".")) // Ignore hidden directories
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Load a scenario configuration from its directory.
+ *
+ * @param scenarioName Name of the scenario (directory name)
+ * @return ScenarioConfig object
+ * @throws IOException If there's an error reading the scenario configuration
+ */
+ public ScenarioConfig loadScenarioConfig(String scenarioName) throws IOException {
+ Path scenarioDir = scenariosDir.resolve(scenarioName);
+ Path configPath = scenarioDir.resolve("scenario.yaml");
+
+ if (!Files.exists(configPath)) {
+ throw new IOException("scenario.yaml not found in " + scenarioDir);
+ }
+
+ return yamlMapper.readValue(configPath.toFile(), ScenarioConfig.class);
+ }
+
+ /**
+ * Execute a scenario's scripts.
+ *
+ * @param scenarioName Name of the scenario to execute
+ * @return ScenarioResult containing execution results
+ * @throws Exception If there's an error executing the scenario
+ */
+ public ScenarioResult executeScenario(String scenarioName) throws Exception {
+ logger.info("Executing scenario: {}", scenarioName);
+
+ Path scenarioDir = scenariosDir.resolve(scenarioName);
+ ScenarioConfig config = loadScenarioConfig(scenarioName);
+
+ // Build template variables map
+ Map templateVars = new HashMap<>(globalTemplateVars);
+ templateVars.put("SCENARIO_DIR", scenarioDir.toAbsolutePath().toString());
+
+ // Add environment variables from scenario config
+ if (config.getEnv() != null) {
+ templateVars.putAll(config.getEnv());
+ }
+
+ ScenarioResult result = new ScenarioResult(scenarioName);
+
+ // Execute run.sh.tmpl
+ Path runScriptTemplate = scenarioDir.resolve("run.sh.tmpl");
+ if (Files.exists(runScriptTemplate)) {
+ logger.info("Executing run script for scenario: {}", scenarioName);
+ ScriptExecutionResult runResult = executeScript(runScriptTemplate, templateVars);
+ result.setRunScriptResult(runResult);
+
+ if (runResult.getExitCode() != 0) {
+ logger.error("Run script failed for scenario: {}", scenarioName);
+ logger.error("Exit code: {}", runResult.getExitCode());
+ logger.error("stdout:\n{}", runResult.getStdout());
+ logger.error("stderr:\n{}", runResult.getStderr());
+ return result;
+ }
+ } else {
+ logger.warn("No run.sh.tmpl found for scenario: {}", scenarioName);
+ }
+
+ // Execute verify.sh.tmpl (optional)
+ Path verifyScriptTemplate = scenarioDir.resolve("verify.sh.tmpl");
+ if (Files.exists(verifyScriptTemplate)) {
+ logger.info("Executing verify script for scenario: {}", scenarioName);
+ ScriptExecutionResult verifyResult = executeScript(verifyScriptTemplate, templateVars);
+ result.setVerifyScriptResult(verifyResult);
+
+ if (verifyResult.getExitCode() != 0) {
+ logger.error("Verify script failed for scenario: {}", scenarioName);
+ logger.error("Exit code: {}", verifyResult.getExitCode());
+ logger.error("stdout:\n{}", verifyResult.getStdout());
+ logger.error("stderr:\n{}", verifyResult.getStderr());
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Execute a script template with the given template variables.
+ *
+ * @param scriptTemplatePath Path to the script template
+ * @param templateVars Map of template variables to substitute
+ * @return ScriptExecutionResult containing execution results
+ * @throws IOException If there's an error reading or executing the script
+ */
+ private ScriptExecutionResult executeScript(
+ Path scriptTemplatePath, Map templateVars) throws IOException {
+ // Read the script template
+ String scriptContent = Files.readString(scriptTemplatePath);
+
+ // Process template variables
+ String processedScript = processTemplate(scriptContent, templateVars);
+
+ // Create a temporary executable script file
+ Path tempScript = Files.createTempFile("scenario-script-", ".sh");
+ try {
+ Files.writeString(tempScript, processedScript);
+ tempScript.toFile().setExecutable(true);
+
+ // Execute the script
+ ProcessBuilder processBuilder = new ProcessBuilder("/bin/bash", tempScript.toString());
+
+ // Set environment variables from template vars
+ Map env = processBuilder.environment();
+ env.putAll(templateVars);
+
+ Process process = processBuilder.start();
+
+ // Capture output
+ StringBuilder stdout = new StringBuilder();
+ StringBuilder stderr = new StringBuilder();
+
+ Thread stdoutReader =
+ new Thread(
+ () -> {
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ stdout.append(line).append("\n");
+ logger.info("[script] {}", line);
+ }
+ } catch (IOException e) {
+ logger.error("Error reading stdout", e);
+ }
+ });
+
+ Thread stderrReader =
+ new Thread(
+ () -> {
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ stderr.append(line).append("\n");
+ logger.warn("[script] {}", line);
+ }
+ } catch (IOException e) {
+ logger.error("Error reading stderr", e);
+ }
+ });
+
+ stdoutReader.start();
+ stderrReader.start();
+
+ // Wait for the process to complete
+ int exitCode = process.waitFor();
+
+ // Wait for output readers to finish
+ stdoutReader.join();
+ stderrReader.join();
+
+ return new ScriptExecutionResult(exitCode, stdout.toString(), stderr.toString());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Script execution interrupted", e);
+ } finally {
+ // Clean up temporary script file
+ try {
+ Files.deleteIfExists(tempScript);
+ } catch (IOException e) {
+ logger.warn("Failed to delete temporary script file: {}", tempScript, e);
+ }
+ }
+ }
+
+ /**
+ * Process template variables in a script.
+ *
+ * Replaces {{VARIABLE_NAME}} with the corresponding value from templateVars.
+ *
+ * @param template Template string
+ * @param templateVars Map of variable names to values
+ * @return Processed string with variables substituted
+ */
+ private String processTemplate(String template, Map templateVars) {
+ String result = template;
+ for (Map.Entry entry : templateVars.entrySet()) {
+ String placeholder = "{{" + entry.getKey() + "}}";
+ result = result.replace(placeholder, entry.getValue());
+ }
+ return result;
+ }
+
+ /** Result of executing a scenario. */
+ public static class ScenarioResult {
+ private final String scenarioName;
+ private ScriptExecutionResult runScriptResult;
+ private ScriptExecutionResult verifyScriptResult;
+
+ public ScenarioResult(String scenarioName) {
+ this.scenarioName = scenarioName;
+ }
+
+ public String getScenarioName() {
+ return scenarioName;
+ }
+
+ public ScriptExecutionResult getRunScriptResult() {
+ return runScriptResult;
+ }
+
+ public void setRunScriptResult(ScriptExecutionResult runScriptResult) {
+ this.runScriptResult = runScriptResult;
+ }
+
+ public ScriptExecutionResult getVerifyScriptResult() {
+ return verifyScriptResult;
+ }
+
+ public void setVerifyScriptResult(ScriptExecutionResult verifyScriptResult) {
+ this.verifyScriptResult = verifyScriptResult;
+ }
+
+ public boolean isSuccess() {
+ boolean runSuccess = runScriptResult == null || runScriptResult.getExitCode() == 0;
+ boolean verifySuccess = verifyScriptResult == null || verifyScriptResult.getExitCode() == 0;
+ return runSuccess && verifySuccess;
+ }
+ }
+
+ /** Result of executing a single script. */
+ public static class ScriptExecutionResult {
+ private final int exitCode;
+ private final String stdout;
+ private final String stderr;
+
+ public ScriptExecutionResult(int exitCode, String stdout, String stderr) {
+ this.exitCode = exitCode;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ }
+
+ public int getExitCode() {
+ return exitCode;
+ }
+
+ public String getStdout() {
+ return stdout;
+ }
+
+ public String getStderr() {
+ return stderr;
+ }
+ }
+}
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/QUICK_START.md b/ice-rest-catalog/src/test/resources/scenarios/QUICK_START.md
new file mode 100644
index 0000000..fef4c9b
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/QUICK_START.md
@@ -0,0 +1,249 @@
+# Quick Start: Scenario-Based Testing
+
+## Running Your First Scenario Test
+
+### 1. Build the project (if not already built)
+```bash
+cd /path/to/ice
+mvn clean install -DskipTests
+```
+
+### 2. Run all scenario tests
+```bash
+cd ice-rest-catalog
+mvn test -Dtest=ScenarioBasedIT
+```
+
+### 3. Run a specific scenario
+```bash
+mvn test -Dtest=ScenarioBasedIT#testScenario[basic-operations]
+```
+
+## Understanding Test Output
+
+### Successful Test
+```
+[INFO] Running com.altinity.ice.rest.catalog.ScenarioBasedIT
+[INFO] ====== Starting scenario test: basic-operations ======
+[INFO] [script] Running basic operations test...
+[INFO] [script] ✓ Created namespace: test_ns
+[INFO] [script] ✓ Listed namespaces
+[INFO] [script] ✓ Deleted namespace: test_ns
+[INFO] [script] Basic operations test completed successfully
+[INFO] Run script exit code: 0
+[INFO] ====== Scenario test passed: basic-operations ======
+[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
+```
+
+### Failed Test
+```
+[ERROR] Scenario 'insert-scan' failed:
+[ERROR] Run script failed with exit code: 1
+[ERROR] Stdout:
+Running insert and scan test...
+✓ Created namespace: test_scan
+✗ Scan output does not contain expected column 'sepal'
+```
+
+## Creating Your First Scenario
+
+### 1. Create the directory structure
+```bash
+cd ice-rest-catalog/src/test/resources/scenarios
+mkdir my-first-test
+cd my-first-test
+```
+
+### 2. Create scenario.yaml
+```yaml
+name: "My First Test"
+description: "A simple test that creates and deletes a namespace"
+
+env:
+ MY_NAMESPACE: "my_test_namespace"
+
+cloudResources:
+ s3:
+ buckets:
+ - "test-bucket"
+```
+
+### 3. Create run.sh.tmpl
+```bash
+#!/bin/bash
+set -e
+
+echo "Running my first test..."
+
+# Create namespace
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${MY_NAMESPACE}
+echo "✓ Created namespace: ${MY_NAMESPACE}"
+
+# List namespaces to verify it exists
+{{ICE_CLI}} --config {{CLI_CONFIG}} list-namespaces | grep ${MY_NAMESPACE}
+echo "✓ Verified namespace exists"
+
+# Delete namespace
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${MY_NAMESPACE}
+echo "✓ Deleted namespace: ${MY_NAMESPACE}"
+
+echo "Test completed successfully!"
+```
+
+### 4. Run your scenario
+```bash
+cd ../../.. # back to ice-rest-catalog
+mvn test -Dtest=ScenarioBasedIT#testScenario[my-first-test]
+```
+
+## Template Variables Reference
+
+All scripts have access to these template variables:
+
+| Variable | Example Value | Description |
+|----------|---------------|-------------|
+| `{{ICE_CLI}}` | `/path/to/ice-jar` | Path to ICE CLI executable |
+| `{{CLI_CONFIG}}` | `/tmp/ice-cli-123.yaml` | Path to CLI config file |
+| `{{MINIO_ENDPOINT}}` | `http://localhost:9000` | MinIO endpoint URL |
+| `{{CATALOG_URI}}` | `http://localhost:8080` | REST catalog URI |
+| `{{SCENARIO_DIR}}` | `/path/to/scenarios/my-test` | Absolute path to scenario |
+
+Plus all environment variables from `scenario.yaml` env section.
+
+## Common Patterns
+
+### Pattern 1: Create, Use, Cleanup
+```bash
+#!/bin/bash
+set -e
+
+# Create resources
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE}
+
+# Use resources
+{{ICE_CLI}} --config {{CLI_CONFIG}} insert --create-table ${TABLE} ${INPUT_FILE}
+{{ICE_CLI}} --config {{CLI_CONFIG}} scan ${TABLE}
+
+# Cleanup (always cleanup!)
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE}
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${NAMESPACE}
+```
+
+### Pattern 2: Verify Output
+```bash
+#!/bin/bash
+set -e
+
+# Run command and capture output
+{{ICE_CLI}} --config {{CLI_CONFIG}} scan ${TABLE} > /tmp/output.txt
+
+# Verify output contains expected data
+if ! grep -q "expected_value" /tmp/output.txt; then
+ echo "✗ Expected value not found in output"
+ cat /tmp/output.txt
+ exit 1
+fi
+
+echo "✓ Verification passed"
+```
+
+### Pattern 3: Use Test Data
+```bash
+#!/bin/bash
+set -e
+
+# Reference files relative to scenario directory
+SCENARIO_DIR="{{SCENARIO_DIR}}"
+INPUT_FILE="${SCENARIO_DIR}/input.parquet"
+
+# Use the file
+{{ICE_CLI}} --config {{CLI_CONFIG}} insert --create-table ${TABLE} ${INPUT_FILE}
+```
+
+### Pattern 4: Multi-file Insert
+```yaml
+# scenario.yaml
+env:
+ NAMESPACE: "multi_file_test"
+ TABLE: "multi_file_test.my_table"
+```
+
+```bash
+# run.sh.tmpl
+#!/bin/bash
+set -e
+
+SCENARIO_DIR="{{SCENARIO_DIR}}"
+
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE}
+
+# Insert multiple files
+for file in ${SCENARIO_DIR}/input*.parquet; do
+ echo "Inserting ${file}..."
+ {{ICE_CLI}} --config {{CLI_CONFIG}} insert ${TABLE} ${file}
+done
+
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE}
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${NAMESPACE}
+```
+
+## Tips and Best Practices
+
+### ✅ Do This
+- Always set `set -e` at the top of scripts (exit on error)
+- Always cleanup resources (namespaces, tables)
+- Use descriptive scenario names (e.g., `insert-scan-delete`, not `test1`)
+- Add echo statements to show progress
+- Use `{{SCENARIO_DIR}}` for file paths
+- Test your scenario independently before committing
+
+### ❌ Avoid This
+- Don't hardcode paths (use template variables)
+- Don't leave resources dangling (always cleanup)
+- Don't use generic names like "test" or "temp"
+- Don't assume resources exist (create them in your scenario)
+- Don't share state between scenarios (each should be independent)
+
+## Debugging Failed Scenarios
+
+### 1. Check the test output
+The test logs show script stdout and stderr:
+```bash
+mvn test -Dtest=ScenarioBasedIT#testScenario[my-test] 2>&1 | grep -A 50 "Starting scenario"
+```
+
+### 2. Run the script manually
+```bash
+# Extract the processed script from test logs, or process it manually
+cd ice-rest-catalog/src/test/resources/scenarios/my-test
+cat run.sh.tmpl | sed 's|{{ICE_CLI}}|../../../../../../.bin/local-ice|g' | bash
+```
+
+### 3. Check ICE CLI directly
+```bash
+# Test ICE CLI works
+.bin/local-ice --help
+
+# Test with your config
+.bin/local-ice --config /path/to/config.yaml list-namespaces
+```
+
+### 4. Enable debug logging
+Add to your run.sh.tmpl:
+```bash
+set -x # Enable bash debug mode
+```
+
+## Next Steps
+
+1. Review existing scenarios in `src/test/resources/scenarios/`
+2. Read the [Scenarios README](README.md) for detailed documentation
+3. Check [SCENARIO_TESTING.md](../../SCENARIO_TESTING.md) for architecture details
+4. Start migrating your hardcoded tests to scenarios!
+
+## Questions?
+
+See the [Scenarios README](README.md) for more detailed documentation.
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/README.md b/ice-rest-catalog/src/test/resources/scenarios/README.md
new file mode 100644
index 0000000..4afbe2c
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/README.md
@@ -0,0 +1,144 @@
+# ICE REST Catalog Test Scenarios
+
+This directory contains scenario-based integration tests for the ICE REST Catalog. Each scenario is self-contained with its own configuration, input data, and execution scripts.
+
+## Directory Structure
+
+```
+scenarios/
+ /
+ scenario.yaml # Scenario configuration and metadata
+ run.sh.tmpl # Main test execution script (templated)
+ verify.sh.tmpl # Optional verification script (templated)
+ input.parquet # Input data files (0 or more)
+ input2.parquet
+ ...
+```
+
+## Scenario Configuration (scenario.yaml)
+
+Each scenario must have a `scenario.yaml` file that defines:
+
+```yaml
+name: "Human-readable test name"
+description: "Description of what this scenario tests"
+
+# Optional: Override catalog configuration
+catalogConfig:
+ warehouse: "s3://test-bucket/warehouse"
+
+# Environment variables available to scripts
+env:
+ NAMESPACE_NAME: "test_ns"
+ TABLE_NAME: "test_ns.table1"
+ INPUT_FILE: "input.parquet"
+
+# Optional: Cloud resources needed (for future provisioning)
+cloudResources:
+ s3:
+ buckets:
+ - "test-bucket"
+ sqs:
+ queues:
+ - "test-queue"
+
+# Optional: Test execution phases
+phases:
+ - name: "setup"
+ description: "Initialize resources"
+ - name: "run"
+ description: "Execute main test logic"
+ - name: "verify"
+ description: "Verify results"
+ - name: "cleanup"
+ description: "Clean up resources"
+```
+
+## Script Templates
+
+### run.sh.tmpl
+
+The main test execution script. Template variables are replaced at runtime:
+
+- `{{CLI_CONFIG}}` - Path to temporary ICE CLI config file
+- `{{MINIO_ENDPOINT}}` - MinIO endpoint URL
+- `{{CATALOG_URI}}` - REST catalog URI (e.g., http://localhost:8080)
+- `{{SCENARIO_DIR}}` - Absolute path to the scenario directory
+- All environment variables from `scenario.yaml` env section
+
+Example:
+```bash
+#!/bin/bash
+set -e
+
+# Environment variables from scenario.yaml are available
+echo "Testing namespace: ${NAMESPACE_NAME}"
+
+# Use template variables
+ice --config {{CLI_CONFIG}} create-namespace ${NAMESPACE_NAME}
+
+# Reference input files relative to scenario directory
+INPUT_PATH="{{SCENARIO_DIR}}/${INPUT_FILE}"
+ice --config {{CLI_CONFIG}} insert --create-table ${TABLE_NAME} ${INPUT_PATH}
+```
+
+### verify.sh.tmpl (Optional)
+
+Additional verification logic. Same template variables are available.
+
+Example:
+```bash
+#!/bin/bash
+set -e
+
+# Verify expected output files exist
+if [ ! -f /tmp/test_output.txt ]; then
+ echo "Expected output not found"
+ exit 1
+fi
+
+echo "Verification passed"
+exit 0
+```
+
+## Adding New Scenarios
+
+1. Create a new directory under `scenarios/`
+2. Add `scenario.yaml` with configuration
+3. Add `run.sh.tmpl` with test logic
+4. (Optional) Add `verify.sh.tmpl` for additional verification
+5. Add any input data files (`.parquet`, etc.)
+6. The test framework will automatically discover and run the new scenario
+
+## Running Scenarios
+
+Scenarios are discovered and executed automatically by the `ScenarioBasedIT` test class:
+
+```bash
+mvn test -Dtest=ScenarioBasedIT
+```
+
+To run a specific scenario:
+
+```bash
+mvn test -Dtest=ScenarioBasedIT#testScenario[basic-operations]
+```
+
+## Best Practices
+
+1. **Keep scenarios focused**: Each scenario should test one specific feature or workflow
+2. **Make scripts idempotent**: Scripts should be able to run multiple times
+3. **Clean up resources**: Always clean up namespaces, tables, etc. in the run script
+4. **Use descriptive names**: Scenario directory names should clearly indicate what is being tested
+5. **Document assumptions**: Add comments in scripts about data format expectations
+6. **Avoid hardcoded paths**: Use template variables and environment variables
+7. **Test both success and failure cases**: Create separate scenarios for error conditions
+
+## Example Scenarios
+
+- `basic-operations/` - Tests namespace creation and deletion
+- `insert-scan/` - Tests data insertion and scanning
+- `insert-partitioned/` - Tests partitioned table creation
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl
new file mode 100644
index 0000000..1f6fc47
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/run.sh.tmpl
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+# Template variables will be replaced at runtime:
+# {{ICE_CLI}} - path to ice CLI executable
+# {{CLI_CONFIG}} - path to temporary ICE CLI config file
+# {{MINIO_ENDPOINT}} - minio endpoint URL
+# {{CATALOG_URI}} - REST catalog URI
+# Environment variables from scenario.yaml are also available
+
+echo "Running basic operations test..."
+
+# Create namespace via CLI
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE_NAME}
+echo "✓ Created namespace: ${NAMESPACE_NAME}"
+
+# List namespaces to verify
+{{ICE_CLI}} --config {{CLI_CONFIG}} list-namespaces
+echo "✓ Listed namespaces"
+
+# Delete the namespace via CLI
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${NAMESPACE_NAME}
+echo "✓ Deleted namespace: ${NAMESPACE_NAME}"
+
+echo "Basic operations test completed successfully"
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/scenario.yaml b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/scenario.yaml
new file mode 100644
index 0000000..9a0298f
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/scenario.yaml
@@ -0,0 +1,30 @@
+name: "Basic Catalog Operations"
+description: "Tests fundamental catalog operations like namespace and table management"
+
+# Configuration for ICE REST catalog server (optional overrides from defaults)
+catalogConfig:
+ warehouse: "s3://test-bucket/warehouse"
+
+# Environment variables that will be available to scripts
+env:
+ NAMESPACE_NAME: "test_ns"
+
+# Cloud resources needed for this scenario (future use for provisioning)
+cloudResources:
+ s3:
+ buckets:
+ - "test-bucket"
+
+# Test phases
+phases:
+ - name: "setup"
+ description: "Create namespace"
+ - name: "verify"
+ description: "Verify namespace exists and can be deleted"
+ - name: "cleanup"
+ description: "Delete namespace"
+
+
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl
new file mode 100644
index 0000000..551af86
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/basic-operations/verify.sh.tmpl
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e
+
+# Verification script - checks that the test completed successfully
+# Exit code 0 = success, non-zero = failure
+
+echo "Verifying basic operations test..."
+
+# For this simple test, the run.sh itself does the verification
+# by successfully creating and deleting the namespace
+# More complex scenarios can add additional verification here
+
+echo "✓ Verification passed"
+exit 0
+
+
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/input.parquet b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/input.parquet
new file mode 100644
index 0000000..028c64c
Binary files /dev/null and b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/input.parquet differ
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl
new file mode 100644
index 0000000..f99feb2
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/run.sh.tmpl
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -e
+
+echo "Running insert with partitioning test..."
+
+# Create namespace
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE_NAME}
+echo "✓ Created namespace: ${NAMESPACE_NAME}"
+
+# Get the full path to the input file
+SCENARIO_DIR="{{SCENARIO_DIR}}"
+INPUT_PATH="${SCENARIO_DIR}/${INPUT_FILE}"
+
+# Create table with partitioning and insert data
+{{ICE_CLI}} --config {{CLI_CONFIG}} insert --create-table ${TABLE_NAME} ${INPUT_PATH} --partition="${PARTITION_SPEC}"
+echo "✓ Inserted data with partitioning into table ${TABLE_NAME}"
+
+# Describe the table to verify partitioning (if describe command exists)
+# {{ICE_CLI}} --config {{CLI_CONFIG}} describe-table ${TABLE_NAME}
+
+# Cleanup
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_NAME}
+echo "✓ Deleted table: ${TABLE_NAME}"
+
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${NAMESPACE_NAME}
+echo "✓ Deleted namespace: ${NAMESPACE_NAME}"
+
+echo "Insert with partitioning test completed successfully"
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/scenario.yaml b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/scenario.yaml
new file mode 100644
index 0000000..43c2157
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/scenario.yaml
@@ -0,0 +1,31 @@
+name: "Insert with Partitioning"
+description: "Tests inserting data with partitioning specifications"
+
+catalogConfig:
+ warehouse: "s3://test-bucket/warehouse"
+
+env:
+ NAMESPACE_NAME: "test_insert_partitioned"
+ TABLE_NAME: "test_insert_partitioned.iris_partitioned"
+ INPUT_FILE: "input.parquet"
+ PARTITION_SPEC: '[{"column":"variety","transform":"identity"}]'
+
+cloudResources:
+ s3:
+ buckets:
+ - "test-bucket"
+
+phases:
+ - name: "setup"
+ description: "Create namespace"
+ - name: "run"
+ description: "Create partitioned table and insert data"
+ - name: "verify"
+ description: "Verify table was created with correct partitioning"
+ - name: "cleanup"
+ description: "Delete table and namespace"
+
+
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/verify.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/verify.sh.tmpl
new file mode 100644
index 0000000..43475a6
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-partitioned/verify.sh.tmpl
@@ -0,0 +1,15 @@
+#!/bin/bash
+set -e
+
+echo "Verifying insert with partitioning test..."
+
+# For now, the run.sh itself verifies by successfully completing
+# Future: could add table metadata checks here
+
+echo "✓ Verification passed"
+exit 0
+
+
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-scan/input.parquet b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/input.parquet
new file mode 100644
index 0000000..028c64c
Binary files /dev/null and b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/input.parquet differ
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-scan/run.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/run.sh.tmpl
new file mode 100644
index 0000000..8a3a7ab
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/run.sh.tmpl
@@ -0,0 +1,45 @@
+#!/bin/bash
+set -e
+
+echo "Running insert and scan test..."
+
+# Create namespace
+{{ICE_CLI}} --config {{CLI_CONFIG}} create-namespace ${NAMESPACE_NAME}
+echo "✓ Created namespace: ${NAMESPACE_NAME}"
+
+# Get the full path to the input file (relative to scenario directory)
+SCENARIO_DIR="{{SCENARIO_DIR}}"
+INPUT_PATH="${SCENARIO_DIR}/${INPUT_FILE}"
+
+# Create table and insert data
+{{ICE_CLI}} --config {{CLI_CONFIG}} insert --create-table ${TABLE_NAME} ${INPUT_PATH}
+echo "✓ Inserted data from ${INPUT_FILE} into table ${TABLE_NAME}"
+
+# Scan the table to verify data was inserted
+{{ICE_CLI}} --config {{CLI_CONFIG}} scan ${TABLE_NAME} > /tmp/scan_output.txt
+echo "✓ Scanned table ${TABLE_NAME}"
+
+# Check that scan output contains expected data
+if ! grep -q "sepal" /tmp/scan_output.txt; then
+ echo "✗ Scan output does not contain expected column 'sepal'"
+ cat /tmp/scan_output.txt
+ exit 1
+fi
+
+if ! grep -q "variety" /tmp/scan_output.txt; then
+ echo "✗ Scan output does not contain expected column 'variety'"
+ cat /tmp/scan_output.txt
+ exit 1
+fi
+
+echo "✓ Scan output contains expected columns"
+
+# Cleanup
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-table ${TABLE_NAME}
+echo "✓ Deleted table: ${TABLE_NAME}"
+
+{{ICE_CLI}} --config {{CLI_CONFIG}} delete-namespace ${NAMESPACE_NAME}
+echo "✓ Deleted namespace: ${NAMESPACE_NAME}"
+
+echo "Insert and scan test completed successfully"
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-scan/scenario.yaml b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/scenario.yaml
new file mode 100644
index 0000000..d060db8
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/scenario.yaml
@@ -0,0 +1,28 @@
+name: "Insert and Scan Operations"
+description: "Tests inserting data from parquet files and scanning tables"
+
+catalogConfig:
+ warehouse: "s3://test-bucket/warehouse"
+
+env:
+ NAMESPACE_NAME: "test_scan"
+ TABLE_NAME: "test_scan.users"
+ INPUT_FILE: "input.parquet"
+
+cloudResources:
+ s3:
+ buckets:
+ - "test-bucket"
+
+phases:
+ - name: "setup"
+ description: "Create namespace and table with data"
+ - name: "verify"
+ description: "Scan table and verify data"
+ - name: "cleanup"
+ description: "Delete table and namespace"
+
+
+
+
+
diff --git a/ice-rest-catalog/src/test/resources/scenarios/insert-scan/verify.sh.tmpl b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/verify.sh.tmpl
new file mode 100644
index 0000000..f205999
--- /dev/null
+++ b/ice-rest-catalog/src/test/resources/scenarios/insert-scan/verify.sh.tmpl
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+echo "Verifying insert and scan test..."
+
+# Check that scan output file was created and contains data
+if [ ! -f /tmp/scan_output.txt ]; then
+ echo "✗ Scan output file not found"
+ exit 1
+fi
+
+if [ ! -s /tmp/scan_output.txt ]; then
+ echo "✗ Scan output file is empty"
+ exit 1
+fi
+
+echo "✓ Scan output file exists and contains data"
+
+# Cleanup temp file
+rm -f /tmp/scan_output.txt
+
+echo "✓ Verification passed"
+exit 0
+
+
+
diff --git a/ice/src/test/resources/ice-rest-catalog.yaml b/ice/src/test/resources/ice-rest-catalog.yaml
new file mode 100644
index 0000000..6c27d06
--- /dev/null
+++ b/ice/src/test/resources/ice-rest-catalog.yaml
@@ -0,0 +1,12 @@
+uri: jdbc:sqlite:file:data/ice-rest-catalog/db.sqlite?journal_mode=WAL&synchronous=OFF&journal_size_limit=500
+
+
+s3:
+ endpoint: http://minio:9000
+ pathStyleAccess: true
+ accessKeyID: minioadmin
+ secretAccessKey: minioadmin
+ region: minio
+
+bearerTokens:
+ - value: foo