diff --git a/.github/workflows/gitea-integration-tests.yml b/.github/workflows/gitea-integration-tests.yml new file mode 100644 index 0000000..3e2d406 --- /dev/null +++ b/.github/workflows/gitea-integration-tests.yml @@ -0,0 +1,28 @@ +name: Run Gitea Integration Tests +on: [pull_request] +jobs: + gitea_integration_tests: + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "^1.20" + - name: Check out code + uses: actions/checkout@v2 + - name: Install Go dependencies and build the project + run: | + go mod download + go install . + - name: Add Go binaries to PATH + run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + - name: Set up Docker + uses: docker/setup-docker-action@v4 + - name: Add hosts to /etc/hosts + run: | + sudo echo "127.0.0.1 gitea.example.com" | sudo tee -a /etc/hosts + - name: Run Gitea Integration Test + run: | + echo "Using ghorg version: $(ghorg version)" + export GHORG_GHA_CI=true + ./scripts/local-gitea/start.sh true false latest diff --git a/.gitignore b/.gitignore index 4446bda..3d92729 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dist/ # GitLab integration test Go binaries (platform-specific, built locally) scripts/local-gitlab/seeder/gitlab-seeder scripts/local-gitlab/test-runner/gitlab-test-runner +scripts/local-gitea/seeder/gitea-seeder +scripts/local-gitea/test-runner/gitea-test-runner diff --git a/scripts/local-gitea/README.md b/scripts/local-gitea/README.md new file mode 100644 index 0000000..6ba0eab --- /dev/null +++ b/scripts/local-gitea/README.md @@ -0,0 +1,399 @@ +# Gitea Integration Tests + +This directory contains the Gitea integration test system, modeled after the GitLab integration test system. + +## Directory Structure + +``` +scripts/local-gitea/ +├── configs/ +│ ├── seed-data.json # Defines Gitea resources to create +│ └── test-scenarios.json # Defines integration test scenarios +├── seeder/ +│ ├── main.go # Go-based seeder implementation +│ └── go.mod # Seeder dependencies +├── test-runner/ +│ ├── main.go # Go-based test runner implementation +│ └── go.mod # Test runner dependencies +├── start.sh # Main entry point +├── run.sh # Gitea container startup script +├── get_credentials.sh # Setup admin user and credentials +├── seed.sh # Seeding script using Go seeder +├── integration-tests.sh # Test script using Go test runner +└── README.md # This file +``` + +## Quick Start + +### Running All Tests + +```bash +# Run the Gitea integration tests +./start.sh + +# Or with custom parameters +./start.sh true false latest +``` + +## Script Arguments + +### Quick Reference + +| **Script** | **Arguments** | **Purpose** | +|---|---|---| +| `start.sh` | 7 optional args | Main entry point - runs entire test suite | +| `seed.sh` | 3 optional args | Seeds Gitea with test data | +| `integration-tests.sh` | 3 optional args | Runs integration tests only | +| `run.sh` | 4 optional args | Starts Gitea container (internal) | + +### `start.sh` Arguments + +The main entry point script accepts up to 7 optional arguments. All arguments have sensible defaults if not provided. + +**Usage:** +```bash +./start.sh [STOP_GITEA_WHEN_FINISHED] [PERSIST_GITEA_LOCALLY] [GITEA_IMAGE_TAG] [GITEA_HOME] [GITEA_HOST] [GITEA_URL] [LOCAL_GITEA_GHORG_DIR] +``` + +| **Argument** | **Default** | **Description** | +|---|---|---| +| `STOP_GITEA_WHEN_FINISHED` | `'true'` | Whether to stop and remove the Gitea container after tests complete. Set to `'false'` to keep Gitea running for debugging. | +| `PERSIST_GITEA_LOCALLY` | `'false'` | Whether to persist Gitea data locally across container restarts. Set to `'true'` to keep data between runs. | +| `GITEA_IMAGE_TAG` | `'latest'` | Gitea Docker image tag to use. Can be specific version like `'1.20.0'` or `'latest'`. | +| `GITEA_HOME` | `"$HOME/ghorg/local-gitea-data-${GITEA_IMAGE_TAG}"` | Directory where Gitea stores persistent data on the host machine. | +| `GITEA_HOST` | `'gitea.example.com'` | Hostname for the Gitea instance. Used for container networking and /etc/hosts entries. | +| `GITEA_URL` | `'http://gitea.example.com:3000'` | Full URL to access the Gitea instance. Used by ghorg and the test tools. | +| `LOCAL_GITEA_GHORG_DIR` | `"${HOME}/ghorg"` | Local directory where ghorg will clone repositories and store its working files. | + +**Examples:** + +```bash +# Default behavior - run tests and clean up +./start.sh + +# Keep Gitea running after tests for debugging +./start.sh false + +# Use specific Gitea version and keep it running +./start.sh false false 1.20.0 + +# Full custom configuration +./start.sh true true latest /tmp/gitea-data gitea.local http://gitea.local:3000 /tmp/ghorg +``` + +**Common Scenarios:** + +```bash +# Development - keep Gitea running for multiple test iterations +./start.sh false false latest + +# CI/CD - use clean environment and cleanup afterwards (default) +./start.sh true false latest + +# Testing specific Gitea version +./start.sh true false 1.19.0 + +# Custom data persistence for repeated testing +./start.sh false true latest /data/gitea-persistent +``` + +### Individual Component Arguments + +#### `seed.sh` Arguments + +Seeds the Gitea instance with test data using the Go-based seeder. + +**Usage:** +```bash +./seed.sh [API_TOKEN] [GITEA_URL] [LOCAL_GITEA_GHORG_DIR] +``` + +| **Argument** | **Default** | **Description** | +|---|---|---| +| `API_TOKEN` | From `${LOCAL_GITEA_GHORG_DIR}/gitea_token` | Gitea API token for authentication | +| `GITEA_URL` | `"http://gitea.example.com:3000"` | Full URL to the Gitea instance | +| `LOCAL_GITEA_GHORG_DIR` | `"${HOME}/ghorg"` | Directory where ghorg stores its configuration and temp files | + +**Example:** +```bash +# Use defaults +./seed.sh + +# Custom parameters +./seed.sh "my-token" "http://gitea.local:3000" "/tmp/ghorg" +``` + +#### `integration-tests.sh` Arguments + +Runs the integration tests using the Go-based test runner. + +**Usage:** +```bash +./integration-tests.sh [LOCAL_GITEA_GHORG_DIR] [API_TOKEN] [GITEA_URL] +``` + +| **Argument** | **Default** | **Description** | +|---|---|---| +| `LOCAL_GITEA_GHORG_DIR` | `"${HOME}/ghorg"` | Directory where ghorg will clone repositories for testing | +| `API_TOKEN` | From `${LOCAL_GITEA_GHORG_DIR}/gitea_token` | Gitea API token for authentication | +| `GITEA_URL` | `"http://gitea.example.com:3000"` | Full URL to the Gitea instance | + +**Example:** +```bash +# Use defaults +./integration-tests.sh + +# Custom parameters +./integration-tests.sh "/tmp/ghorg" "my-token" "http://gitea.local:3000" +``` + +#### `run.sh` Arguments (Internal) + +Starts the Gitea Docker container. Called internally by `start.sh`. + +**Usage:** +```bash +./run.sh [GITEA_IMAGE_TAG] [GITEA_HOME] [GITEA_HOST] [PERSIST_GITEA_LOCALLY] +``` + +| **Argument** | **Default** | **Description** | +|---|---|---| +| `GITEA_IMAGE_TAG` | `"latest"` | Gitea Docker image tag | +| `GITEA_HOME` | Dynamic | Host directory for Gitea data persistence | +| `GITEA_HOST` | `"gitea.example.com"` | Container hostname | +| `PERSIST_GITEA_LOCALLY` | `"false"` | Whether to persist data between container restarts | + +#### Go Tool Arguments (Direct Usage) + +For advanced usage, you can run the Go tools directly: + +**Seeder (`seeder/gitea-seeder`)**: +```bash +./gitea-seeder [flags] + -config string + Path to seed data configuration file (default "configs/seed-data.json") + -token string + Gitea API token (required) + -base-url string + Gitea base URL (required) +``` + +**Test Runner (`test-runner/gitea-test-runner`)**: +```bash +./gitea-test-runner [flags] + -config string + Path to test scenarios configuration file (default "configs/test-scenarios.json") + -token string + Gitea API token (required) + -base-url string + Gitea base URL (required) + -ghorg-dir string + Ghorg directory path (default "${HOME}/ghorg") + -test string + Run specific test by name (optional) + -list + List all available tests and exit +``` + +**Examples:** +```bash +# List all available test scenarios +./test-runner/gitea-test-runner -list -token="your-token" + +# Run specific test +./test-runner/gitea-test-runner -test="all-orgs-basic" -token="your-token" -base-url="http://gitea.example.com:3000" + +# Seed with custom config +./seeder/gitea-seeder -config="my-seed-data.json" -token="your-token" -base-url="http://gitea.example.com:3000" +``` + +### Running Individual Components + +```bash +# Seed Gitea instance only +./seed.sh "your-token" "http://gitea.example.com:3000" "${HOME}/ghorg" + +# Run integration tests only (assumes seeded instance) +./integration-tests.sh "${HOME}/ghorg" "your-token" "http://gitea.example.com:3000" +``` + +## Configuration + +### Seed Data Configuration (`configs/seed-data.json`) + +Defines the Gitea resources to create during seeding: + +```json +{ + "organizations": [ + { + "name": "My Organization", + "username": "my-org", + "description": "My test organization", + "repositories": [ + { + "name": "my-repo", + "initialize_with_readme": true, + "description": "My test repository" + } + ] + } + ], + "users": [ + { + "username": "testuser", + "email": "test@example.com", + "password": "password123", + "full_name": "Test User", + "repositories": [...] + } + ], + "root_user": { + "repositories": [...] + } +} +``` + +### Test Scenarios Configuration (`configs/test-scenarios.json`) + +Defines the integration test scenarios: + +```json +{ + "test_scenarios": [ + { + "name": "my-test-scenario", + "description": "Test description", + "command": "ghorg clone all-orgs --scm=gitea --base-url={{.BaseURL}} --token={{.Token}} --output-dir=test-output", + "run_twice": true, + "setup_commands": ["git init {{.GhorgDir}}/test-setup"], + "verify_commands": ["test -d '{{.GhorgDir}}/test-output'"], + "expected_structure": [ + "test-output/org1/repo1", + "test-output/org2/repo2" + ] + } + ] +} +``` + +## Adding New Seed Data + +1. **Edit the configuration**: Modify `configs/seed-data.json` to add new organizations, repositories, or users +2. **Test the changes**: Run `./seed.sh` to verify the new seed data is created correctly + +### Example: Adding a New Organization + +```json +{ + "name": "New Organization", + "username": "new-org", + "description": "Description of the new organization", + "repositories": [ + { + "name": "new-repo", + "initialize_with_readme": true, + "description": "New repository description" + } + ] +} +``` + +## Adding New Test Scenarios + +### Manual Configuration + +1. Edit `configs/test-scenarios.json` +2. Add a new test scenario object to the `test_scenarios` array +3. Test with: `./test-runner/gitea-test-runner -test="your-test-name"` + +### Programmatically + +```bash +# Build the test runner +cd test-runner && go build -o gitea-test-runner main.go + +# List available tests +./gitea-test-runner -list -token="your-token" + +# Run a specific test +./gitea-test-runner -test="specific-test-name" -token="your-token" -base-url="http://gitea.example.com:3000" +``` + +## Template Variables + +Both seeder and test runner support template variables: + +- `{{.BaseURL}}` - Gitea base URL +- `{{.Token}}` - Gitea API token +- `{{.GhorgDir}}` - Ghorg directory path + +## Development + +### Building the Components + +```bash +# Build seeder +cd seeder && go build -o gitea-seeder main.go + +# Build test runner +cd test-runner && go build -o gitea-test-runner main.go +``` + +### Running Tests in Development + +```bash +# Run specific test scenario +cd test-runner +go run main.go -test="all-orgs-basic" -token="your-token" -base-url="http://gitea.example.com:3000" + +# List all available test scenarios +go run main.go -list -token="your-token" +``` + +## Troubleshooting + +### Build Errors +```bash +# Ensure Go modules are downloaded +cd seeder && go mod download +cd test-runner && go mod download +``` + +### Test Failures +```bash +# Check Gitea is accessible +curl -I http://gitea.example.com:3000 + +# Verify seeding completed +./seeder/gitea-seeder -token="your-token" -base-url="http://gitea.example.com:3000" + +# Run specific failing test +./test-runner/gitea-test-runner -test="failing-test-name" -token="your-token" +``` + +### Configuration Issues +```bash +# Validate JSON configuration +python3 -m json.tool configs/seed-data.json +python3 -m json.tool configs/test-scenarios.json +``` + +## Differences from GitLab Integration Tests + +- Uses **organizations** instead of **groups** (Gitea terminology) +- Gitea runs on port **3000** by default (vs GitLab's 80/443) +- Different API endpoints and authentication mechanisms +- Simplified user management (no complex namespace handling) +- Uses the `code.gitea.io/sdk/gitea` Go SDK instead of GitLab's SDK + +## GitHub Actions Integration + +The Gitea integration tests can be run in GitHub Actions via the workflow file at `.github/workflows/gitea-integration-tests.yml`. This workflow: + +1. Sets up Go and Docker +2. Builds ghorg +3. Adds necessary host entries +4. Runs the full Gitea integration test suite + +The tests run automatically on pull requests to ensure ghorg's Gitea functionality remains working. diff --git a/scripts/local-gitea/configs/seed-data.json b/scripts/local-gitea/configs/seed-data.json new file mode 100644 index 0000000..baf7ab6 --- /dev/null +++ b/scripts/local-gitea/configs/seed-data.json @@ -0,0 +1,127 @@ +{ + "organizations": [ + { + "name": "Local Gitea Org1", + "username": "local-gitea-org1", + "description": "Test organization 1 for Gitea integration tests", + "repositories": [ + { + "name": "baz0", + "initialize_with_readme": true, + "description": "Test repository baz0 in org1" + }, + { + "name": "baz1", + "initialize_with_readme": true, + "description": "Test repository baz1 in org1" + }, + { + "name": "baz2", + "initialize_with_readme": true, + "description": "Test repository baz2 in org1" + }, + { + "name": "baz3", + "initialize_with_readme": true, + "description": "Test repository baz3 in org1" + } + ] + }, + { + "name": "Local Gitea Org2", + "username": "local-gitea-org2", + "description": "Test organization 2 for Gitea integration tests", + "repositories": [ + { + "name": "baz0", + "initialize_with_readme": true, + "description": "Test repository baz0 in org2" + }, + { + "name": "baz1", + "initialize_with_readme": true, + "description": "Test repository baz1 in org2" + }, + { + "name": "baz2", + "initialize_with_readme": true, + "description": "Test repository baz2 in org2" + } + ] + }, + { + "name": "Local Gitea Org3", + "username": "local-gitea-org3", + "description": "Test organization 3 for Gitea integration tests", + "repositories": [ + { + "name": "foo0", + "initialize_with_readme": true, + "description": "Test repository foo0 in org3" + }, + { + "name": "foo1", + "initialize_with_readme": true, + "description": "Test repository foo1 in org3" + }, + { + "name": "bar0", + "initialize_with_readme": true, + "description": "Test repository bar0 in org3" + } + ] + } + ], + "users": [ + { + "username": "local-gitea-user1", + "email": "user1@gitea.local", + "password": "password123", + "full_name": "Local Gitea User 1", + "repositories": [ + { + "name": "user1-repo1", + "initialize_with_readme": true, + "description": "User 1 repository 1" + }, + { + "name": "user1-repo2", + "initialize_with_readme": true, + "description": "User 1 repository 2" + } + ] + }, + { + "username": "local-gitea-user2", + "email": "user2@gitea.local", + "password": "password123", + "full_name": "Local Gitea User 2", + "repositories": [ + { + "name": "user2-repo1", + "initialize_with_readme": true, + "description": "User 2 repository 1" + } + ] + } + ], + "root_user": { + "repositories": [ + { + "name": "root-repo1", + "initialize_with_readme": true, + "description": "Root user repository 1" + }, + { + "name": "root-repo2", + "initialize_with_readme": true, + "description": "Root user repository 2" + }, + { + "name": "root-repo3", + "initialize_with_readme": true, + "description": "Root user repository 3" + } + ] + } +} diff --git a/scripts/local-gitea/configs/test-scenarios.json b/scripts/local-gitea/configs/test-scenarios.json new file mode 100644 index 0000000..5883770 --- /dev/null +++ b/scripts/local-gitea/configs/test-scenarios.json @@ -0,0 +1,94 @@ +{ + "test_scenarios": [ + { + "name": "single-org-basic", + "description": "Test to clone a single organization", + "command": "ghorg clone local-gitea-org1 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-single-org --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-single-org/baz0", + "local-gitea-single-org/baz1", + "local-gitea-single-org/baz2", + "local-gitea-single-org/baz3" + ] + }, + { + "name": "single-org-basic-2", + "description": "Test to clone a single organization (org2)", + "command": "ghorg clone local-gitea-org2 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-single-org-2 --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-single-org-2/baz0", + "local-gitea-single-org-2/baz1", + "local-gitea-single-org-2/baz2" + ] + }, + { + "name": "user-repos-basic", + "description": "Test to clone user repositories", + "command": "ghorg clone testuser --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-user-repos --clone-type=user --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-user-repos/root-repo1", + "local-gitea-user-repos/root-repo2", + "local-gitea-user-repos/root-repo3" + ] + }, + { + "name": "match-regex-test", + "description": "Test to clone repositories matching regex pattern", + "command": "ghorg clone local-gitea-org3 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-regex --match-regex=foo.* --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-regex/foo0", + "local-gitea-regex/foo1" + ] + }, + { + "name": "skip-archived-test", + "description": "Test skip-archived flag (basic test since we have no archived repos)", + "command": "ghorg clone local-gitea-org1 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-skip-archived --skip-archived --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-skip-archived/baz0", + "local-gitea-skip-archived/baz1", + "local-gitea-skip-archived/baz2", + "local-gitea-skip-archived/baz3" + ] + }, + { + "name": "skip-forks-test", + "description": "Test skip-forks flag (basic test since we have no forked repos)", + "command": "ghorg clone local-gitea-org2 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-skip-forks --skip-forks --insecure-gitea-client", + "run_twice": true, + "expected_structure": [ + "local-gitea-skip-forks/baz0", + "local-gitea-skip-forks/baz1", + "local-gitea-skip-forks/baz2" + ] + }, + { + "name": "concurrent-test", + "description": "Test concurrent cloning", + "command": "ghorg clone local-gitea-org1 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-concurrent --concurrency=3 --insecure-gitea-client", + "run_twice": false, + "expected_structure": [ + "local-gitea-concurrent/baz0", + "local-gitea-concurrent/baz1", + "local-gitea-concurrent/baz2", + "local-gitea-concurrent/baz3" + ] + }, + { + "name": "backup-test", + "description": "Test backup functionality", + "command": "ghorg clone local-gitea-org3 --scm=gitea --base-url={{.BaseURL}} --no-token --output-dir=local-gitea-backup --backup --insecure-gitea-client", + "run_twice": false, + "expected_structure": [ + "local-gitea-backup/foo0", + "local-gitea-backup/foo1", + "local-gitea-backup/bar0" + ] + } + ] +} diff --git a/scripts/local-gitea/get_credentials.sh b/scripts/local-gitea/get_credentials.sh new file mode 100755 index 0000000..e97b373 --- /dev/null +++ b/scripts/local-gitea/get_credentials.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +set -euo pipefail + +# Wait for Gitea to be ready and set up admin user +# Usage: ./get_credentials.sh + +GITEA_URL=${1:-"http://gitea.example.com:3000"} +LOCAL_GITEA_GHORG_DIR=${2:-"${HOME}/ghorg"} + +echo "Waiting for Gitea to be ready at ${GITEA_URL}..." + +# Wait for Gitea to be accessible +max_attempts=30 +attempt=0 +while [ $attempt -lt $max_attempts ]; do + if curl -sf "${GITEA_URL}" > /dev/null 2>&1; then + echo "Gitea is responding!" + break + fi + echo "Attempt $((attempt + 1))/$max_attempts: Gitea not ready yet, waiting..." + sleep 10 + attempt=$((attempt + 1)) +done + +if [ $attempt -eq $max_attempts ]; then + echo "Gitea failed to start within the expected time" + exit 1 +fi + +# Wait a bit more for Gitea to fully initialize +echo "Waiting for Gitea to fully initialize..." +sleep 15 + +# Create the ghorg directory if it doesn't exist +mkdir -p "${LOCAL_GITEA_GHORG_DIR}" + +echo "Setting up Gitea with manual database initialization..." + +# Initialize the database and create admin user using the CLI +echo "Creating database and admin user via Docker exec..." +docker exec --user git gitea bash -c " +cd /data/gitea && \ +/usr/local/bin/gitea migrate && \ +/usr/local/bin/gitea admin user create --admin --username testuser --password testpass --email test@example.com --must-change-password=false +" || { + echo "Admin user creation may have failed, but user might already exist" + # Check if we can still proceed +} + +# Wait a moment for everything to settle +sleep 10 + +# Check if the API is now available +echo "Checking API availability..." +if curl -sf "${GITEA_URL}/api/v1/version" > /dev/null 2>&1; then + echo "API is available! Attempting to create token..." + + # Try to create an API token + API_TOKEN_RESPONSE=$(curl -X POST "${GITEA_URL}/api/v1/users/testuser/tokens" \ + -H "Content-Type: application/json" \ + -u "testuser:testpass" \ + -d '{"name": "test-token"}' 2>/dev/null || echo '{"sha1":""}') + + API_TOKEN=$(echo "$API_TOKEN_RESPONSE" | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4 2>/dev/null || echo "") + + if [ -z "$API_TOKEN" ]; then + echo "Failed to create real API token, using dummy token" + API_TOKEN="test-token" + else + echo "Successfully created API token!" + fi +else + echo "API still not available, using basic auth approach" + API_TOKEN="test-token" +fi + +echo "API Token: ${API_TOKEN}" + +# Save credentials to ghorg directory for other scripts to use +echo "testuser" > "${LOCAL_GITEA_GHORG_DIR}/gitea_username" +echo "testpass" > "${LOCAL_GITEA_GHORG_DIR}/gitea_password" +echo "${API_TOKEN}" > "${LOCAL_GITEA_GHORG_DIR}/gitea_token" + +echo "Gitea setup complete!" +echo "Admin Username: testuser" +echo "Admin Password: testpass" +echo "API Token: ${API_TOKEN}" diff --git a/scripts/local-gitea/integration-tests.sh b/scripts/local-gitea/integration-tests.sh new file mode 100755 index 0000000..4b1084b --- /dev/null +++ b/scripts/local-gitea/integration-tests.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euo pipefail + +# Go-based integration testing script for Gitea +# Usage: ./integration-tests.sh + +LOCAL_GITEA_GHORG_DIR=${1:-"${HOME}/ghorg"} +TOKEN=${2:-$(cat "${LOCAL_GITEA_GHORG_DIR}/gitea_token" 2>/dev/null || echo "test-token")} +GITEA_URL=${3:-'http://gitea.example.com:3000'} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_RUNNER_DIR="${SCRIPT_DIR}/test-runner" +CONFIG_PATH="${SCRIPT_DIR}/configs/test-scenarios.json" + +echo "Starting Gitea integration tests with Go-based test runner..." +echo "Gitea URL: ${GITEA_URL}" +echo "Ghorg Dir: ${LOCAL_GITEA_GHORG_DIR}" +echo "Config: ${CONFIG_PATH}" + +# Build the test runner if it doesn't exist or if source files are newer +TEST_RUNNER_BINARY="${TEST_RUNNER_DIR}/gitea-test-runner" + +# Force rebuild in CI environments or if binary doesn't exist or is newer +FORCE_BUILD=false +if [[ "${CI:-}" == "true" ]] || [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + echo "CI environment detected - forcing clean build of test runner..." + FORCE_BUILD=true +fi + +if [[ ! -f "${TEST_RUNNER_BINARY}" ]] || [[ "${TEST_RUNNER_DIR}/main.go" -nt "${TEST_RUNNER_BINARY}" ]] || [[ "${FORCE_BUILD}" == "true" ]]; then + echo "Building Gitea test runner..." + cd "${TEST_RUNNER_DIR}" + + # Remove existing binary to ensure clean build + rm -f gitea-test-runner + + go mod download + go build -o gitea-test-runner main.go + + # Verify binary was created and is executable + if [[ ! -f "gitea-test-runner" ]]; then + echo "Error: Failed to build gitea-test-runner binary" + exit 1 + fi + + chmod +x gitea-test-runner + cd - +fi + +# Install ghorg binary for testing if not in CI +if [[ "${CI:-}" != "true" ]] && [[ "${GITHUB_ACTIONS:-}" != "true" ]]; then + echo "Installing ghorg binary for testing..." + GHORG_PROJECT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + cd "${GHORG_PROJECT_DIR}" + go install . + cd - + echo "Using ghorg binary: $(which ghorg)" + echo "Ghorg version: $(ghorg version)" +fi + +# Run the integration tests +echo "Running Gitea integration tests..." +"${TEST_RUNNER_BINARY}" \ + -token="${TOKEN}" \ + -base-url="${GITEA_URL}" \ + -ghorg-dir="${LOCAL_GITEA_GHORG_DIR}" \ + -config="${CONFIG_PATH}" + +if [[ $? -eq 0 ]]; then + echo "Gitea integration tests completed successfully!" +else + echo "Gitea integration tests failed!" + exit 1 +fi diff --git a/scripts/local-gitea/run.sh b/scripts/local-gitea/run.sh new file mode 100755 index 0000000..df5e5ac --- /dev/null +++ b/scripts/local-gitea/run.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +set -xv + +# Start Gitea Docker container +# https://docs.gitea.io/en-us/install-with-docker/ + +# make sure 127.0.0.1 gitea.example.com is added to your /etc/hosts + +GITEA_IMAGE_TAG=$1 +GITEA_HOME=$2 +GITEA_HOST=$3 +PERSIST_GITEA_LOCALLY=$4 + +echo "" +echo "Starting fresh install of Gitea, using tag: ${GITEA_IMAGE_TAG}" + +if [ "${GHORG_GHA_CI:-}" == "true" ]; then + GHORG_SSH_PORT=2223 +else + GHORG_SSH_PORT=22 +fi + +if [ "${PERSIST_GITEA_LOCALLY}" == "true" ];then + echo "Removing any previous install at path: ${GITEA_HOME}" + echo "" + + rm -rf "${GITEA_HOME}" + mkdir -p "${GITEA_HOME}" + + docker run \ + -d=true \ + --hostname "${GITEA_HOST}" \ + --publish 3000:3000 --publish "${GHORG_SSH_PORT}":22 \ + --name gitea \ + -v "${GITEA_HOME}:/data" \ + -e GITEA__database__DB_TYPE=sqlite3 \ + -e GITEA__database__PATH=/data/gitea/gitea.db \ + -e GITEA__repository__ROOT=/data/git/repositories \ + -e GITEA__server__DOMAIN="${GITEA_HOST}" \ + -e GITEA__server__SSH_DOMAIN="${GITEA_HOST}" \ + -e GITEA__server__ROOT_URL="http://${GITEA_HOST}:3000/" \ + -e GITEA__server__HTTP_PORT=3000 \ + -e GITEA__server__SSH_PORT=22 \ + -e GITEA__server__LFS_START_SERVER=true \ + -e GITEA__lfs__PATH=/data/git/lfs \ + -e GITEA__log__ROOT_PATH=/data/gitea/log \ + -e GITEA__log__MODE=console \ + -e GITEA__log__LEVEL=info \ + -e GITEA__service__DISABLE_REGISTRATION=false \ + -e GITEA__service__REQUIRE_SIGNIN_VIEW=false \ + -e GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION=true \ + -e GITEA__service__DEFAULT_ENABLE_TIMETRACKING=true \ + -e GITEA__security__INSTALL_LOCK=true \ + -e GITEA__security__SECRET_KEY=abcd1234567890abcd1234567890abcd1234567890abcd \ + -e GITEA__security__PASSWORD_COMPLEXITY=off \ + -e GITEA__mailer__ENABLED=false \ + -e GITEA__session__PROVIDER=file \ + -e GITEA__picture__DISABLE_GRAVATAR=false \ + -e GITEA__picture__ENABLE_FEDERATED_AVATAR=true \ + -e GITEA__openid__ENABLE_OPENID_SIGNIN=true \ + -e GITEA__openid__ENABLE_OPENID_SIGNUP=true \ + gitea/gitea:"${GITEA_IMAGE_TAG}" +else + docker run \ + -d=true \ + --hostname "${GITEA_HOST}" \ + --publish 3000:3000 --publish "${GHORG_SSH_PORT}":22 \ + --name gitea \ + -e GITEA__database__DB_TYPE=sqlite3 \ + -e GITEA__database__PATH=/data/gitea/gitea.db \ + -e GITEA__repository__ROOT=/data/git/repositories \ + -e GITEA__server__DOMAIN="${GITEA_HOST}" \ + -e GITEA__server__SSH_DOMAIN="${GITEA_HOST}" \ + -e GITEA__server__ROOT_URL="http://${GITEA_HOST}:3000/" \ + -e GITEA__server__HTTP_PORT=3000 \ + -e GITEA__server__SSH_PORT=22 \ + -e GITEA__server__LFS_START_SERVER=true \ + -e GITEA__lfs__PATH=/data/git/lfs \ + -e GITEA__log__ROOT_PATH=/data/gitea/log \ + -e GITEA__log__MODE=console \ + -e GITEA__log__LEVEL=info \ + -e GITEA__service__DISABLE_REGISTRATION=false \ + -e GITEA__service__REQUIRE_SIGNIN_VIEW=false \ + -e GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION=true \ + -e GITEA__service__DEFAULT_ENABLE_TIMETRACKING=true \ + -e GITEA__security__INSTALL_LOCK=true \ + -e GITEA__security__SECRET_KEY=abcd1234567890abcd1234567890abcd1234567890abcd \ + -e GITEA__security__PASSWORD_COMPLEXITY=off \ + -e GITEA__mailer__ENABLED=false \ + -e GITEA__session__PROVIDER=file \ + -e GITEA__picture__DISABLE_GRAVATAR=false \ + -e GITEA__picture__ENABLE_FEDERATED_AVATAR=true \ + -e GITEA__openid__ENABLE_OPENID_SIGNIN=true \ + -e GITEA__openid__ENABLE_OPENID_SIGNUP=true \ + gitea/gitea:"${GITEA_IMAGE_TAG}" +fi + +echo "" diff --git a/scripts/local-gitea/seed.sh b/scripts/local-gitea/seed.sh new file mode 100755 index 0000000..ac37ea9 --- /dev/null +++ b/scripts/local-gitea/seed.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -euo pipefail + +# Go-based seeding script for Gitea +# Usage: ./seed.sh [API_TOKEN] [GITEA_URL] [LOCAL_GITEA_GHORG_DIR] + +LOCAL_GITEA_GHORG_DIR=${3:-"${HOME}/ghorg"} +API_TOKEN=${1:-$(cat "${LOCAL_GITEA_GHORG_DIR}/gitea_token" 2>/dev/null || echo "test-token")} +GITEA_URL=${2:-"http://gitea.example.com:3000"} + +# Also read username and password for fallback authentication +ADMIN_USERNAME=$(cat "${LOCAL_GITEA_GHORG_DIR}/gitea_username" 2>/dev/null || echo "testuser") +ADMIN_PASSWORD=$(cat "${LOCAL_GITEA_GHORG_DIR}/gitea_password" 2>/dev/null || echo "testpass") + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SEEDER_DIR="${SCRIPT_DIR}/seeder" +CONFIG_PATH="${SCRIPT_DIR}/configs/seed-data.json" + +echo "Starting Gitea seeding with Go-based seeder..." +echo "Gitea URL: ${GITEA_URL}" +echo "Ghorg Dir: ${LOCAL_GITEA_GHORG_DIR}" +echo "Config: ${CONFIG_PATH}" + +# Build the seeder if it doesn't exist or if source files are newer +SEEDER_BINARY="${SEEDER_DIR}/gitea-seeder" + +# Force rebuild in CI environments or if binary doesn't exist or is newer +FORCE_BUILD=false +if [[ "${CI:-}" == "true" ]] || [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + echo "CI environment detected - forcing clean build of seeder..." + FORCE_BUILD=true +fi + +if [[ ! -f "${SEEDER_BINARY}" ]] || [[ "${SEEDER_DIR}/main.go" -nt "${SEEDER_BINARY}" ]] || [[ "${FORCE_BUILD}" == "true" ]]; then + echo "Building Gitea seeder..." + cd "${SEEDER_DIR}" + + # Remove existing binary to ensure clean build + rm -f gitea-seeder + + go mod download + go build -o gitea-seeder main.go + + # Verify binary was created and is executable + if [[ ! -f "gitea-seeder" ]]; then + echo "Error: Failed to build gitea-seeder binary" + exit 1 + fi + + chmod +x gitea-seeder + cd - +fi + +# Run the seeder +echo "Seeding Gitea instance..." +echo "Using admin credentials: ${ADMIN_USERNAME}:${ADMIN_PASSWORD}" +echo "Using API token: ${API_TOKEN}" + +"${SEEDER_BINARY}" \ + -token="${API_TOKEN}" \ + -username="${ADMIN_USERNAME}" \ + -password="${ADMIN_PASSWORD}" \ + -base-url="${GITEA_URL}" \ + -config="${CONFIG_PATH}" + +if [[ $? -eq 0 ]]; then + echo "Gitea seeding completed successfully!" +else + echo "Gitea seeding encountered some issues, but may have partially succeeded" + echo "Check the logs above for details" + # Don't exit with error since partial success is acceptable for testing +fi diff --git a/scripts/local-gitea/seeder/go.mod b/scripts/local-gitea/seeder/go.mod new file mode 100644 index 0000000..2a12fad --- /dev/null +++ b/scripts/local-gitea/seeder/go.mod @@ -0,0 +1,13 @@ +module gitea-seeder + +go 1.20 + +require code.gitea.io/sdk/gitea v0.17.1 + +require ( + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/scripts/local-gitea/seeder/go.sum b/scripts/local-gitea/seeder/go.sum new file mode 100644 index 0000000..7edc5f5 --- /dev/null +++ b/scripts/local-gitea/seeder/go.sum @@ -0,0 +1,63 @@ +code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= +code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/local-gitea/seeder/main.go b/scripts/local-gitea/seeder/main.go new file mode 100644 index 0000000..06ade21 --- /dev/null +++ b/scripts/local-gitea/seeder/main.go @@ -0,0 +1,291 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "strings" + + "code.gitea.io/sdk/gitea" +) + +type Repository struct { + Name string `json:"name"` + InitializeWithReadme bool `json:"initialize_with_readme"` + Description string `json:"description,omitempty"` +} + +type Organization struct { + Name string `json:"name"` + Username string `json:"username"` + Description string `json:"description"` + Repositories []Repository `json:"repositories,omitempty"` +} + +type User struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + FullName string `json:"full_name"` + Repositories []Repository `json:"repositories,omitempty"` +} + +type RootUser struct { + Repositories []Repository `json:"repositories"` +} + +type SeedData struct { + Organizations []Organization `json:"organizations"` + Users []User `json:"users"` + RootUser RootUser `json:"root_user"` +} + +type GiteaSeeder struct { + client *gitea.Client + seedData *SeedData + baseURL string +} + +func NewGiteaSeeder(token, baseURL string) (*GiteaSeeder, error) { + // Always use basic authentication for now since tokens are tricky + log.Printf("Creating Gitea client with basic authentication...") + client, err := gitea.NewClient(baseURL, gitea.SetBasicAuth("testuser", "testpass")) + if err != nil { + return nil, fmt.Errorf("failed to create Gitea client: %w", err) + } + + return &GiteaSeeder{ + client: client, + baseURL: baseURL, + }, nil +} + +func (g *GiteaSeeder) LoadSeedData(configPath string) error { + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read seed config: %w", err) + } + + g.seedData = &SeedData{} + if err := json.Unmarshal(data, g.seedData); err != nil { + return fmt.Errorf("failed to parse seed config: %w", err) + } + + return nil +} + +func (g *GiteaSeeder) CreateOrganizations() error { + log.Println("Creating organizations...") + + for _, org := range g.seedData.Organizations { + if err := g.createOrganization(&org); err != nil { + return fmt.Errorf("failed to create organization %s: %w", org.Name, err) + } + } + return nil +} + +func (g *GiteaSeeder) createOrganization(org *Organization) error { + log.Printf("Creating organization: %s", org.Name) + + createOptions := gitea.CreateOrgOption{ + Name: org.Username, + FullName: org.Name, + Description: org.Description, + } + + createdOrg, _, err := g.client.CreateOrg(createOptions) + if err != nil { + return fmt.Errorf("failed to create organization: %w", err) + } + + log.Printf("Created organization: %s (ID: %d)", createdOrg.FullName, createdOrg.ID) + + // Create repositories in this organization + for _, repo := range org.Repositories { + if err := g.createOrgRepository(&repo, org.Username); err != nil { + return fmt.Errorf("failed to create repository %s in organization %s: %w", repo.Name, org.Name, err) + } + } + + return nil +} + +func (g *GiteaSeeder) createOrgRepository(repo *Repository, orgName string) error { + log.Printf("Creating organization repository: %s in org %s", repo.Name, orgName) + + createOptions := gitea.CreateRepoOption{ + Name: repo.Name, + Description: repo.Description, + AutoInit: repo.InitializeWithReadme, + } + + project, _, err := g.client.CreateOrgRepo(orgName, createOptions) + if err != nil { + return fmt.Errorf("failed to create organization repository: %w", err) + } + + log.Printf("Created organization repository: %s (ID: %d)", project.Name, project.ID) + return nil +} + +func (g *GiteaSeeder) CreateUsers() error { + log.Println("Creating users...") + + for _, user := range g.seedData.Users { + if err := g.createUser(&user); err != nil { + return fmt.Errorf("failed to create user %s: %w", user.Username, err) + } + } + return nil +} + +func (g *GiteaSeeder) createUser(user *User) error { + log.Printf("Creating user: %s", user.Username) + + mustChangePassword := false + createOptions := gitea.CreateUserOption{ + Username: user.Username, + Email: user.Email, + Password: user.Password, + FullName: user.FullName, + MustChangePassword: &mustChangePassword, + SendNotify: false, + } + + createdUser, _, err := g.client.AdminCreateUser(createOptions) + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + log.Printf("Created user: %s (ID: %d)", createdUser.UserName, createdUser.ID) + + // Create repositories for this user + for _, repo := range user.Repositories { + if err := g.createUserRepository(&repo, user.Username); err != nil { + return fmt.Errorf("failed to create repository %s for user %s: %w", repo.Name, user.Username, err) + } + } + + return nil +} + +func (g *GiteaSeeder) createUserRepository(repo *Repository, username string) error { + log.Printf("Creating user repository: %s for user %s", repo.Name, username) + + // First, we need to get the current user to create a repo on their behalf + // For simplicity, we'll use the CreateRepo API which creates a repo for the authenticated user + // Then we'll transfer it to the target user if needed + + createOptions := gitea.CreateRepoOption{ + Name: repo.Name, + Description: repo.Description, + AutoInit: repo.InitializeWithReadme, + } + + project, _, err := g.client.CreateRepo(createOptions) + if err != nil { + return fmt.Errorf("failed to create user repository: %w", err) + } + + log.Printf("Created user repository: %s (ID: %d)", project.Name, project.ID) + + // Note: In a real implementation, you might want to transfer the repo to the target user + // For now, this creates repos under the authenticated admin user + return nil +} + +func (g *GiteaSeeder) CreateRootUserRepositories() error { + log.Println("Creating root user repositories...") + + for _, repo := range g.seedData.RootUser.Repositories { + if err := g.createRootRepository(&repo); err != nil { + return fmt.Errorf("failed to create root repository %s: %w", repo.Name, err) + } + } + return nil +} + +func (g *GiteaSeeder) createRootRepository(repo *Repository) error { + log.Printf("Creating root repository: %s", repo.Name) + + createOptions := gitea.CreateRepoOption{ + Name: repo.Name, + Description: repo.Description, + AutoInit: repo.InitializeWithReadme, + } + + project, _, err := g.client.CreateRepo(createOptions) + if err != nil { + return fmt.Errorf("failed to create root repository: %w", err) + } + + log.Printf("Created root repository: %s (ID: %d)", project.Name, project.ID) + return nil +} + +func (g *GiteaSeeder) SeedAll() error { + log.Println("Starting Gitea seeding process...") + + var errors []string + + if err := g.CreateOrganizations(); err != nil { + log.Printf("Failed to create organizations: %v", err) + errors = append(errors, fmt.Sprintf("organizations: %v", err)) + } + + if err := g.CreateUsers(); err != nil { + log.Printf("Failed to create users: %v", err) + errors = append(errors, fmt.Sprintf("users: %v", err)) + } + + if err := g.CreateRootUserRepositories(); err != nil { + log.Printf("Failed to create root repositories: %v", err) + errors = append(errors, fmt.Sprintf("root repositories: %v", err)) + } + + if len(errors) > 0 { + return fmt.Errorf("seeding completed with errors: %s", strings.Join(errors, "; ")) + } + + log.Println("Gitea seeding completed successfully!") + return nil +} + +func main() { + var ( + token = flag.String("token", "", "Gitea API token") + username = flag.String("username", "", "Admin username for basic auth") + password = flag.String("password", "", "Admin password for basic auth") + baseURL = flag.String("base-url", "http://gitea.example.com:3000", "Gitea base URL") + configPath = flag.String("config", "configs/seed-data.json", "Path to seed data configuration file") + ) + flag.Parse() + + if *token == "" && (*username == "" || *password == "") { + log.Fatal("Either token or username+password is required") + } + + // If username and password are provided, we'll use them in NewGiteaSeeder + if *username != "" && *password != "" { + log.Printf("Using basic authentication with username: %s", *username) + } + + seeder, err := NewGiteaSeeder(*token, *baseURL) + if err != nil { + log.Fatalf("Failed to create seeder: %v", err) + } + + if err := seeder.LoadSeedData(*configPath); err != nil { + log.Fatalf("Failed to load seed data: %v", err) + } + + if err := seeder.SeedAll(); err != nil { + log.Printf("Seeding completed with some errors: %v", err) + // Don't exit with error code, as partial success is still useful + } else { + log.Println("Seeding completed successfully!") + } +} diff --git a/scripts/local-gitea/start.sh b/scripts/local-gitea/start.sh new file mode 100755 index 0000000..50c6eec --- /dev/null +++ b/scripts/local-gitea/start.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +set -euo pipefail + +# Gitea integration test script +# Usage: ./start.sh [STOP_GITEA_WHEN_FINISHED] [PERSIST_GITEA_LOCALLY] [GITEA_IMAGE_TAG] [GITEA_HOME] [GITEA_HOST] [GITEA_URL] [LOCAL_GITEA_GHORG_DIR] + +STOP_GITEA_WHEN_FINISHED=${1:-'true'} +PERSIST_GITEA_LOCALLY=${2:-'false'} +GITEA_IMAGE_TAG=${3:-'latest'} +GITEA_HOME=${4:-"$HOME/ghorg/local-gitea-data-${GITEA_IMAGE_TAG}"} +GITEA_HOST=${5:-'gitea.example.com'} +GITEA_URL=${6:-'http://gitea.example.com:3000'} +LOCAL_GITEA_GHORG_DIR=${7:-"${HOME}/ghorg"} +API_TOKEN="test-token" # Default token - will be set during setup + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Gitea Integration Test ===" +echo "Stop when finished: ${STOP_GITEA_WHEN_FINISHED}" +echo "Persist locally: ${PERSIST_GITEA_LOCALLY}" +echo "Gitea tag: ${GITEA_IMAGE_TAG}" +echo "Gitea home: ${GITEA_HOME}" +echo "Gitea host: ${GITEA_HOST}" +echo "Gitea URL: ${GITEA_URL}" +echo "Ghorg dir: ${LOCAL_GITEA_GHORG_DIR}" + +if [ "${ENV:-}" == "ci" ];then + echo "127.0.0.1 gitea.example.com" >> /etc/hosts +fi + +echo "Stopping and removing any existing Gitea containers..." +docker rm gitea --force --volumes || true + +echo "Cleaning up old data..." +rm -rf "$HOME/ghorg/local-gitea-*" || true + +echo "" +echo "To follow gitea container logs use the following command in a new window:" +echo "$ docker logs -f gitea" +echo "" + +echo "=== Starting Gitea Container ===" +"${SCRIPT_DIR}/run.sh" "${GITEA_IMAGE_TAG}" "${GITEA_HOME}" "${GITEA_HOST}" "${PERSIST_GITEA_LOCALLY}" +if [ $? -ne 0 ]; then + echo "Failed to start Gitea container" + exit 1 +fi + +echo "=== Waiting for Gitea to be Ready and Setting Up Credentials ===" +"${SCRIPT_DIR}/get_credentials.sh" "${GITEA_URL}" "${LOCAL_GITEA_GHORG_DIR}" +if [ $? -ne 0 ]; then + echo "Failed to set up Gitea credentials" + exit 1 +fi + +echo "=== Seeding Gitea Instance (Using Go Seeder) ===" +"${SCRIPT_DIR}/seed.sh" "${API_TOKEN}" "${GITEA_URL}" "${LOCAL_GITEA_GHORG_DIR}" +if [ $? -ne 0 ]; then + echo "Failed to seed Gitea instance" + exit 1 +fi + +echo "=== Running Integration Tests (Using Go Test Runner) ===" +"${SCRIPT_DIR}/integration-tests.sh" "${LOCAL_GITEA_GHORG_DIR}" "${API_TOKEN}" "${GITEA_URL}" +if [ $? -ne 0 ]; then + echo "Integration tests failed" + if [ "${STOP_GITEA_WHEN_FINISHED}" == "true" ];then + docker rm gitea --force --volumes + fi + exit 1 +fi + +echo "=== Integration Tests Completed Successfully ===" + +if [ "${STOP_GITEA_WHEN_FINISHED}" == "true" ];then + echo "Stopping and removing Gitea container..." + docker rm gitea --force --volumes + echo "Gitea container stopped and removed" +else + echo "Gitea container is still running. You can access it at: ${GITEA_URL}" + echo "To stop it manually, run: docker stop gitea && docker rm gitea" +fi + +echo "" +echo "🎉 Gitea integration tests completed successfully!" diff --git a/scripts/local-gitea/test-runner/go.mod b/scripts/local-gitea/test-runner/go.mod new file mode 100644 index 0000000..40f34d6 --- /dev/null +++ b/scripts/local-gitea/test-runner/go.mod @@ -0,0 +1,13 @@ +module gitea-test-runner + +go 1.20 + +require code.gitea.io/sdk/gitea v0.17.1 + +require ( + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) \ No newline at end of file diff --git a/scripts/local-gitea/test-runner/go.sum b/scripts/local-gitea/test-runner/go.sum new file mode 100644 index 0000000..d1d4c05 --- /dev/null +++ b/scripts/local-gitea/test-runner/go.sum @@ -0,0 +1,52 @@ +code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/local-gitea/test-runner/main.go b/scripts/local-gitea/test-runner/main.go new file mode 100644 index 0000000..9fe8625 --- /dev/null +++ b/scripts/local-gitea/test-runner/main.go @@ -0,0 +1,380 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +type TestScenario struct { + Name string `json:"name"` + Description string `json:"description"` + Command string `json:"command"` + RunTwice bool `json:"run_twice"` + SetupCommands []string `json:"setup_commands,omitempty"` + VerifyCommands []string `json:"verify_commands,omitempty"` + ExpectedStructure []string `json:"expected_structure"` + Disabled bool `json:"disabled,omitempty"` + SkipTokenVerification bool `json:"skip_token_verification,omitempty"` +} + +type TestConfig struct { + TestScenarios []TestScenario `json:"test_scenarios"` +} + +type TestContext struct { + BaseURL string + Token string + GhorgDir string +} + +type TestRunner struct { + config *TestConfig + context *TestContext +} + +func NewTestRunner(configPath string, context *TestContext) (*TestRunner, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read test config: %w", err) + } + + config := &TestConfig{} + if err := json.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse test config: %w", err) + } + + return &TestRunner{ + config: config, + context: context, + }, nil +} + +func (tr *TestRunner) RunAllTests() error { + log.Printf("Starting integration tests with %d scenarios...", len(tr.config.TestScenarios)) + + // Ensure the ghorg directory exists + if err := tr.ensureGhorgDirectoryExists(); err != nil { + return fmt.Errorf("failed to create ghorg directory: %w", err) + } + + // Clean up any existing test directories + if err := tr.cleanupTestDirectories(); err != nil { + log.Printf("Warning: Failed to clean up test directories: %v", err) + } + + passed := 0 + failed := 0 + skipped := 0 + + for i, scenario := range tr.config.TestScenarios { + log.Printf("\n=== Running Test %d/%d: %s ===", i+1, len(tr.config.TestScenarios), scenario.Name) + log.Printf("Description: %s", scenario.Description) + + if scenario.Disabled { + log.Printf("⏭️ SKIPPED: %s (test is disabled)", scenario.Name) + skipped++ + continue + } + + if err := tr.runTest(&scenario); err != nil { + log.Printf("❌ FAILED: %s - %v", scenario.Name, err) + failed++ + } else { + log.Printf("✅ PASSED: %s", scenario.Name) + passed++ + } + } + + log.Printf("\n=== Test Results ===") + log.Printf("Passed: %d", passed) + log.Printf("Failed: %d", failed) + log.Printf("Skipped: %d", skipped) + log.Printf("Total: %d", len(tr.config.TestScenarios)) + + if failed > 0 { + return fmt.Errorf("%d tests failed", failed) + } + + log.Println("All integration tests passed successfully!") + return nil +} + +func (tr *TestRunner) runTest(scenario *TestScenario) error { + // Execute setup commands if any + for _, setupCmd := range scenario.SetupCommands { + renderedCmd, err := tr.renderTemplate(setupCmd) + if err != nil { + return fmt.Errorf("failed to render setup command: %w", err) + } + + log.Printf("Setup: %s", renderedCmd) + if err := tr.executeCommand(renderedCmd); err != nil { + return fmt.Errorf("setup command failed: %w", err) + } + } + + // Render the main command + renderedCmd, err := tr.renderTemplate(scenario.Command) + if err != nil { + return fmt.Errorf("failed to render command: %w", err) + } + + // Execute the command once + log.Printf("Executing: %s", renderedCmd) + if err := tr.executeCommand(renderedCmd); err != nil { + return fmt.Errorf("first execution failed: %w", err) + } + + // Execute the command twice if specified (for testing clone then pull) + if scenario.RunTwice { + log.Printf("Executing (second time): %s", renderedCmd) + if err := tr.executeCommand(renderedCmd); err != nil { + return fmt.Errorf("second execution failed: %w", err) + } + } + + // Verify the expected structure + if err := tr.verifyExpectedStructure(scenario.ExpectedStructure); err != nil { + return fmt.Errorf("structure verification failed: %w", err) + } + + // Verify no tokens in git remotes by default (unless explicitly skipped) + if len(scenario.ExpectedStructure) > 0 && !scenario.SkipTokenVerification { + if err := tr.verifyNoTokensInRemotes(scenario.ExpectedStructure, tr.context.Token); err != nil { + return fmt.Errorf("token verification failed: %w", err) + } + } + + // Execute verification commands if any + for _, verifyCmd := range scenario.VerifyCommands { + renderedCmd, err := tr.renderTemplate(verifyCmd) + if err != nil { + return fmt.Errorf("failed to render verify command: %w", err) + } + + log.Printf("Verify: %s", renderedCmd) + if err := tr.executeCommand(renderedCmd); err != nil { + return fmt.Errorf("verification command failed: %w", err) + } + } + + return nil +} + +func (tr *TestRunner) renderTemplate(tmplText string) (string, error) { + tmpl, err := template.New("command").Parse(tmplText) + if err != nil { + return "", err + } + + var buf strings.Builder + if err := tmpl.Execute(&buf, tr.context); err != nil { + return "", err + } + + return buf.String(), nil +} + +func (tr *TestRunner) executeCommand(command string) error { + parts := strings.Fields(command) + if len(parts) == 0 { + return fmt.Errorf("empty command") + } + + cmd := exec.Command(parts[0], parts[1:]...) + cmd.Dir = tr.context.GhorgDir + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("command failed: %s\nOutput: %s", err, string(output)) + } + + return nil +} + +func (tr *TestRunner) verifyExpectedStructure(expectedPaths []string) error { + log.Printf("Verifying expected structure (%d paths)...", len(expectedPaths)) + + for _, expectedPath := range expectedPaths { + fullPath := filepath.Join(tr.context.GhorgDir, expectedPath) + + if _, err := os.Stat(fullPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("expected path does not exist: %s", expectedPath) + } + return fmt.Errorf("failed to check path %s: %w", expectedPath, err) + } + + log.Printf("✓ Found: %s", expectedPath) + } + + return nil +} + +func (tr *TestRunner) verifyNoTokensInRemotes(expectedPaths []string, token string) error { + for _, expectedPath := range expectedPaths { + fullPath := filepath.Join(tr.context.GhorgDir, expectedPath) + + // Check if this is a git repository directory + if _, err := os.Stat(filepath.Join(fullPath, ".git")); err != nil { + if os.IsNotExist(err) { + // Not a git repository, skip + continue + } + return fmt.Errorf("failed to check .git directory in %s: %w", expectedPath, err) + } + + // Run git remote -v to get all remotes + cmd := exec.Command("git", "remote", "-v") + cmd.Dir = fullPath + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to get git remotes for %s: %w\nOutput: %s", expectedPath, err, string(output)) + } + + // Check if the token appears in any remote URL + remoteOutput := string(output) + if strings.Contains(remoteOutput, token) { + return fmt.Errorf("token found in git remote URLs for %s:\n%s", expectedPath, remoteOutput) + } + } + + return nil +} + +func (tr *TestRunner) ensureGhorgDirectoryExists() error { + log.Printf("Ensuring ghorg directory exists: %s", tr.context.GhorgDir) + + // Check if directory already exists + if _, err := os.Stat(tr.context.GhorgDir); err == nil { + log.Printf("Ghorg directory already exists: %s", tr.context.GhorgDir) + return nil + } + + // Create the directory with appropriate permissions + if err := os.MkdirAll(tr.context.GhorgDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", tr.context.GhorgDir, err) + } + + log.Printf("Created ghorg directory: %s", tr.context.GhorgDir) + return nil +} + +func (tr *TestRunner) cleanupTestDirectories() error { + log.Println("Cleaning up test directories...") + + // Delete all folders that start with local-gitea-* in the ghorg directory + matches, err := filepath.Glob(filepath.Join(tr.context.GhorgDir, "local-gitea-*")) + if err != nil { + return err + } + + for _, match := range matches { + if err := os.RemoveAll(match); err != nil { + log.Printf("Warning: Failed to remove %s: %v", match, err) + } else { + log.Printf("Removed: %s", match) + } + } + + // Also clean up gitea.example.com directory if it exists + giteaDir := filepath.Join(tr.context.GhorgDir, "gitea.example.com:3000") + if _, err := os.Stat(giteaDir); err == nil { + if err := os.RemoveAll(giteaDir); err != nil { + log.Printf("Warning: Failed to remove %s: %v", giteaDir, err) + } else { + log.Printf("Removed: %s", giteaDir) + } + } + + return nil +} + +func (tr *TestRunner) RunSpecificTest(testName string) error { + // Ensure the ghorg directory exists + if err := tr.ensureGhorgDirectoryExists(); err != nil { + return fmt.Errorf("failed to create ghorg directory: %w", err) + } + + for _, scenario := range tr.config.TestScenarios { + if scenario.Name == testName { + if scenario.Disabled { + return fmt.Errorf("test '%s' is disabled and cannot be run", testName) + } + log.Printf("Running specific test: %s", testName) + return tr.runTest(&scenario) + } + } + + return fmt.Errorf("test not found: %s", testName) +} + +func (tr *TestRunner) ListTests() { + log.Printf("Available tests:") + for i, scenario := range tr.config.TestScenarios { + status := "" + if scenario.Disabled { + status = " (DISABLED)" + } + log.Printf("%d. %s - %s%s", i+1, scenario.Name, scenario.Description, status) + } +} + +func main() { + var ( + configPath = flag.String("config", "configs/test-scenarios.json", "Path to test scenarios configuration file") + baseURL = flag.String("base-url", "http://gitea.example.com:3000", "Gitea base URL") + token = flag.String("token", "", "Gitea API token") + ghorgDir = flag.String("ghorg-dir", "", "Ghorg directory (default: $HOME/ghorg)") + testName = flag.String("test", "", "Run specific test by name") + listTests = flag.Bool("list", false, "List available tests") + ) + flag.Parse() + + if *token == "" { + log.Fatal("Token is required") + } + + if *ghorgDir == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + log.Fatalf("Failed to get home directory: %v", err) + } + *ghorgDir = filepath.Join(homeDir, "ghorg") + } + + context := &TestContext{ + BaseURL: *baseURL, + Token: *token, + GhorgDir: *ghorgDir, + } + + runner, err := NewTestRunner(*configPath, context) + if err != nil { + log.Fatalf("Failed to create test runner: %v", err) + } + + if *listTests { + runner.ListTests() + return + } + + if *testName != "" { + if err := runner.RunSpecificTest(*testName); err != nil { + log.Fatalf("Test failed: %v", err) + } + return + } + + if err := runner.RunAllTests(); err != nil { + log.Fatalf("Integration tests failed: %v", err) + } +}