This commit is contained in:
Anbraten
2026-02-24 16:52:59 +01:00
parent 727f6c4662
commit ac43237b4e
25 changed files with 1324 additions and 649 deletions

View File

@@ -0,0 +1,60 @@
package blocks
import (
"os"
"path"
"testing"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
type gitRepo struct {
folder string
}
func NewGitRepo(t *testing.T) *gitRepo {
return &gitRepo{
folder: t.TempDir(),
}
}
func (r *gitRepo) Clone(t *testing.T, remoteURL string) {
utils.NewCommand("git", "clone", remoteURL, r.folder).RunOrFail(t)
}
func (r *gitRepo) Init(t *testing.T, remoteURL string) {
utils.NewCommand("git", "init").RunOrFail(t)
utils.NewCommand("git", "remote", "add", remoteURL).RunOrFail(t)
utils.NewCommand("git", "branch", "set-upstream-to=origin/main").RunOrFail(t)
}
func (r *gitRepo) InitFromTemplate(t *testing.T, templatePath, remoteURL string) {
utils.NewCommand("cp", "-r", templatePath, r.folder).RunOrFail(t)
r.Init(t, remoteURL)
}
func (r *gitRepo) Add(t *testing.T, filePath string) {
utils.NewCommand("git", "add", filePath).RunOrFail(t)
}
func (r *gitRepo) Commit(t *testing.T, message string) {
utils.NewCommand("git", "commit", "-m", message).RunOrFail(t)
}
func (r *gitRepo) Push(t *testing.T) {
utils.NewCommand("git", "push").RunOrFail(t)
}
func (r *gitRepo) Tag(t *testing.T, name, message string) {
utils.NewCommand("git", "tag", "-a", name, "-m", message).RunOrFail(t)
}
func (r *gitRepo) WriteFile(t *testing.T, filePath string, content []byte) {
// Ensure the directory exists
os.MkdirAll(path.Join(r.folder, path.Dir(filePath)), 0755)
err := os.WriteFile(path.Join(r.folder, filePath), content, 0644)
if err != nil {
t.Fatalf("Failed to write file %s: %v", filePath, err)
}
}

View File

@@ -0,0 +1,26 @@
package blocks
import "testing"
type TestRepo struct {
}
func NewTestRepo() *TestRepo {
return &TestRepo{}
}
func (r *TestRepo) Enable(t *testing.T) {
}
func (r *TestRepo) Repair(t *testing.T) {
}
func (r *TestRepo) Disable(t *testing.T) {
}
func (r *TestRepo) Delete(t *testing.T) {
}

View File

@@ -0,0 +1,23 @@
package blocks
import "testing"
type TestSecret struct {
repo *TestRepo
}
func NewTestSecret(repo *TestRepo) *TestSecret {
return &TestSecret{repo: repo}
}
func (s *TestSecret) Create(t *testing.T, key, value string) {
}
func (s *TestSecret) Update(t *testing.T, value string) {
}
func (s *TestSecret) Delete(t *testing.T) {
}

69
test/integration/env/agent.go vendored Normal file
View File

@@ -0,0 +1,69 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package env
import (
"fmt"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
func (e *TestEnv) StartAgent(serverURL, agentToken string) error {
t := e.t
if e.Agent != nil {
return fmt.Errorf("agent already started")
}
t.Log(" 🤖 Starting Woodpecker Agent with mock backend...")
service := utils.NewService("go", "run", "./cmd/agent/").
WorkDir(e.projectRoot).
// Agent configuration
SetEnv("WOODPECKER_SERVER", serverURL).
SetEnv("WOODPECKER_AGENT_SECRET", agentToken).
// SetEnv("WOODPECKER_MAX_WORKFLOWS", "1").
// SetEnv("WOODPECKER_HEALTHCHECK", "false").
SetEnv("WOODPECKER_BACKEND", "dummy").
// Log level
SetEnv("WOODPECKER_LOG_LEVEL", "debug")
if err := service.Start(); err != nil {
return fmt.Errorf("failed to start agent: %w", err)
}
t.Cleanup(e.StopAgent)
e.Agent = service
// TODO: wait for agent to be ready
// if err := utils.WaitForHTTP("http://localhost:3000", 30*time.Second); err != nil {
// return fmt.Errorf("forge did not become ready: %w", err)
// }
t.Logf(" ✓ Woodpecker Agent started successfully")
return nil
}
func (e *TestEnv) StopAgent() {
t := e.t
if e.Agent != nil {
if err := e.Agent.Stop(); err != nil {
t.Errorf("Warning: Failed to stop agent: %v", err)
} else {
t.Logf("Woodpecker agent stopped successfully")
}
}
}

130
test/integration/env/env.go vendored Normal file
View File

@@ -0,0 +1,130 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package env
import (
"context"
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
// TestEnv represents the complete integration test environment
// with all necessary components (Forge, Woodpecker Server, Woodpecker Agent)
type TestEnv struct {
t *testing.T
ctx context.Context
cancel context.CancelFunc
projectRoot string
// Components
Forge *TestForge
Server *TestServer
Agent *utils.Service
// API Clients
GiteaClient *GiteaClient
WoodpeckerClient *utils.WoodpeckerClient
}
func SetupTestEnv(t *testing.T) *TestEnv {
t.Helper()
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
env := &TestEnv{
t: t,
ctx: ctx,
cancel: cancel,
}
return env
}
func (e *TestEnv) Start() {
t := e.t
t.Helper()
t.Log("🚀 Setting up integration test environment...")
// Step 1: Start Forge (Gitea)
e.Forge = NewTestForge()
err := e.Forge.Start(t)
if err != nil {
e.Stop()
t.Fatalf("Failed to start forge: %v", err)
}
giteaClient := "test-client"
giteaClientSecret := "test-secret"
e.GiteaClient = NewGiteaClient(e.Forge.URL, e.Forge.AdminToken)
// Step 2: Start Woodpecker Server
t.Log(" 🔧 Starting Woodpecker Server...")
e.Server = &TestServer{
URL: "http://localhost:8000",
}
err = e.Server.Start(t, e.Forge.URL, giteaClient, giteaClientSecret)
if err != nil {
e.Stop()
t.Fatalf("Failed to start Woodpecker Server: %v", err)
}
// woodpeckerURL := "http://localhost:8000"
woodpeckerGRPC_URL := "http://localhost:9000"
woodpeckerAgentToken := "woodpecker-agent-token"
// Step 3: Start Woodpecker Agent with dummy backend
err = e.StartAgent(woodpeckerGRPC_URL, woodpeckerAgentToken)
if err != nil {
e.Stop()
t.Fatalf("Failed to start Woodpecker Agent: %v", err)
}
t.Log("✅ Integration test environment setup complete!")
}
func (e *TestEnv) Stop() {
t := e.t
t.Helper()
t.Log("🧹 Cleaning up test environment...")
if e.cancel != nil {
e.cancel()
}
if e.Agent != nil {
e.Agent.Stop()
}
if e.Server != nil {
e.Server.Stop()
}
if e.Forge != nil {
e.Forge.Stop()
}
t.Log("✓ Cleanup complete")
}

133
test/integration/env/forge.go vendored Normal file
View File

@@ -0,0 +1,133 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build test
package env
import (
"fmt"
"net/url"
"path/filepath"
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
type TestForge struct {
URL string
AdminUser string
AdminPassword string
AdminEmail string
AdminToken string
service *utils.Service
}
func NewTestForge() *TestForge {
return &TestForge{
URL: "http://localhost:8000",
AdminUser: "woodpecker",
AdminPassword: "woodpecker123",
AdminEmail: "woodpecker@localhost",
}
}
func (f *TestForge) Start(t *testing.T) error {
if f.service != nil {
return fmt.Errorf("forge already started")
}
projectRoot := "."
t.Log(" 📦 Starting forge (Gitea) ...")
composeFile := filepath.Join(projectRoot, "data", "gitea", "docker-compose.yml")
f.service = utils.NewService("docker", "compose", "-f", composeFile, "up", "-d")
if err := f.service.Start(); err != nil {
return fmt.Errorf("failed to start forge: %w", err)
}
// Wait for Gitea to be ready
if err := utils.WaitForHTTP("http://localhost:3000", 30*time.Second); err != nil {
return fmt.Errorf("forge did not become ready: %w", err)
}
t.Log(" ✓ Forge started successfully")
return nil
}
func (f *TestForge) SetupAdmin(t *testing.T) error {
utils.NewCommand("docker", "compose", "exec", "gitea",
"gitea", "admin", "user", "create",
"--username", f.AdminUser,
"--password", f.AdminPassword,
"--email", f.AdminEmail,
"--admin",
).RunOrFail(t)
adminToken, err := utils.NewCommand("docker", "compose", "exec", "-T", "gitea",
"gitea", "admin", "user", "generate-access-token",
"-u", f.AdminUser,
"--scopes", "write:repository,write:user",
"--raw",
).Run()
if err != nil {
return fmt.Errorf("failed to generate admin token: %w", err)
}
f.AdminToken = adminToken
return nil
}
func (f *TestForge) SetupOAuthApp(t *testing.T, clientName, clientSecret string) error {
appID, err := utils.NewCommand("docker", "compose", "exec", "-T", "gitea",
"gitea", "admin", "oauth2", "add",
"--name", clientName,
"--redirect-uris", "http://localhost:8000/callback",
"--client-secret", clientSecret,
).Run()
if err != nil {
return fmt.Errorf("failed to create OAuth app: %w", err)
}
t.Logf(" ✓ OAuth app created with ID: %s", appID)
return nil
}
func (f *TestForge) GetRepositoryCloneURL(repo string) (string, error) {
u, err := url.Parse(f.URL)
if err != nil {
return "", fmt.Errorf("invalid forge URL: %w", err)
}
u.User = url.UserPassword(f.AdminUser, f.AdminPassword)
return fmt.Sprintf("%s/%s.git", u.String(), repo), nil
}
func (f *TestForge) Stop() error {
if f.service == nil {
return nil
}
if err := f.service.Stop(); err != nil {
return fmt.Errorf("failed to stop forge: %w", err)
}
f.service = nil
return nil
}

182
test/integration/env/gitea_client.go vendored Normal file
View File

@@ -0,0 +1,182 @@
package env
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// GiteaClient provides methods to interact with Gitea API
type GiteaClient struct {
baseURL string
token string
client *http.Client
}
// NewGiteaClient creates a new Gitea API client
func NewGiteaClient(baseURL, token string) *GiteaClient {
return &GiteaClient{
baseURL: baseURL,
token: token,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// doRequest performs an HTTP request to Gitea API
func (c *GiteaClient) doRequest(method, path string, body any) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewBuffer(jsonData)
}
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if c.token != "" {
req.Header.Set("Authorization", "token "+c.token)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return c.client.Do(req)
}
// CreateRepository creates a new repository in Gitea
func (c *GiteaClient) CreateRepository(name, description string) (map[string]any, error) {
body := map[string]any{
"name": name,
"description": description,
"private": false,
"auto_init": true, // Initialize with README
}
resp, err := c.doRequest("POST", "/api/v1/user/repos", body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to create repository: status %d, body: %s", resp.StatusCode, string(respBody))
}
var repo map[string]any
if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return repo, nil
}
// CreateFile creates or updates a file in a repository
func (c *GiteaClient) CreateFile(owner, repo, filepath, content, message string) error {
body := map[string]any{
"content": content, // Should be base64 encoded
"message": message,
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
resp, err := c.doRequest("POST", path, body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to create file: status %d, body: %s", resp.StatusCode, string(respBody))
}
return nil
}
// TriggerWebhook simulates a push webhook from Gitea to Woodpecker
func (c *GiteaClient) TriggerWebhook(webhookURL string, payload map[string]any) error {
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal webhook payload: %w", err)
}
req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create webhook request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Gitea-Event", "push")
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send webhook: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("webhook returned error: status %d, body: %s", resp.StatusCode, string(respBody))
}
return nil
}
// CreateWebhook creates a webhook in a Gitea repository
func (c *GiteaClient) CreateWebhook(owner, repo, webhookURL string) error {
body := map[string]any{
"type": "gitea",
"active": true,
"config": map[string]string{
"url": webhookURL,
"content_type": "json",
},
"events": []string{"push", "pull_request", "create", "delete"},
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner, repo)
resp, err := c.doRequest("POST", path, body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to create webhook: status %d, body: %s", resp.StatusCode, string(respBody))
}
return nil
}
// GetRepository gets repository information
func (c *GiteaClient) GetRepository(owner, repo string) (map[string]any, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo)
resp, err := c.doRequest("GET", path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get repository: status %d, body: %s", resp.StatusCode, string(respBody))
}
var repository map[string]any
if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return repository, nil
}

95
test/integration/env/server.go vendored Normal file
View File

@@ -0,0 +1,95 @@
package env
import (
"fmt"
"net/http"
"path/filepath"
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
type TestServer struct {
URL string
service *utils.Service
}
func (s *TestServer) Start(t *testing.T, giteaURL, giteaClient, giteaClientSecret string) error {
if s.service != nil {
return fmt.Errorf("server already started")
}
projectRoot := "."
t.Log(" 🔧 Starting Woodpecker Server...")
// Prepare web dist directory
utils.NewCommand("mkdir", "-p", filepath.Join(projectRoot, "web/dist")).RunOrFail(t)
utils.NewCommand("sh", "-c", fmt.Sprintf("echo test > %s", filepath.Join(projectRoot, "web/dist/index.html"))).RunOrFail(t)
s.service = utils.NewService("go", "run", "./cmd/server/").
WorkDir(projectRoot).
// Server configuration
SetEnv("WOODPECKER_OPEN", "true").
SetEnv("WOODPECKER_ADMIN", "woodpecker").
SetEnv("WOODPECKER_HOST", "http://localhost:8000").
SetEnv("WOODPECKER_SERVER_ADDR", ":8000").
SetEnv("WOODPECKER_GRPC_ADDR", ":9000").
SetEnv("WOODPECKER_WEBHOOK_HOST", "http://localhost:8000").
SetEnv("WOODPECKER_AGENT_SECRET", "test-secret-123").
// Gitea forge configuration
SetEnv("WOODPECKER_GITEA", "true").
SetEnv("WOODPECKER_GITEA_URL", giteaURL).
SetEnv("WOODPECKER_GITEA_CLIENT", giteaClient).
SetEnv("WOODPECKER_GITEA_SECRET", giteaClientSecret).
// Log level
SetEnv("WOODPECKER_LOG_LEVEL", "debug")
if err := s.service.Start(); err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
// Wait for server to be ready
if err := utils.WaitForHTTP("http://localhost:8000/healthz", 30*time.Second); err != nil {
return fmt.Errorf("server did not become ready: %w", err)
}
t.Logf(" ✓ Woodpecker server started successfully")
return nil
}
// Simulate user login
func (s *TestServer) Login(code, state string) (string, error) {
client := http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://localhost:8000/authorize?code=%s&state=%s", code, state))
if err != nil {
return "", fmt.Errorf("failed to perform login request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login request failed with status: %s", resp.Status)
}
cookies := resp.Cookies()
for _, cookie := range cookies {
if cookie.Name == "user_sess" {
return cookie.Value, nil
}
}
return "", fmt.Errorf("user_sess cookie not found in login response")
}
func (s *TestServer) Stop() error {
if s.service == nil {
return nil
}
if err := s.service.Stop(); err != nil {
return fmt.Errorf("failed to stop server: %w", err)
}
return nil
}

View File

@@ -0,0 +1,183 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline_test
import (
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/env"
)
// TestFlow_CancelPipeline tests the flow of canceling a running pipeline.
//
// Flow:
// 1. Setup test environment
// 2. Create a repository with a long-running pipeline
// 3. Trigger the pipeline
// 4. Wait for pipeline to start executing
// 5. Cancel the pipeline via API
// 6. Verify that the pipeline status changes to "killed" or "cancelled"
// 7. Verify that running steps are stopped
// 8. Verify that no new steps are started after cancellation
func TestFlow_CancelPipeline(t *testing.T) {
// Setup the complete test environment
e := env.SetupTestEnv(t)
e.Start()
// Define a pipeline with long-running steps
// Using the mock backend, we can control step duration with SLEEP env var
// pipelineConfig := `
// when:
// - event: push
// steps:
// - name: long-running-step
// image: alpine:latest
// commands:
// - echo "Starting long-running step"
// - sleep 30 # This will be simulated by mock backend
// - echo "This should not be reached if cancelled"
// - name: second-step
// image: alpine:latest
// commands:
// - echo "This step should not start if pipeline is cancelled"
// `
// TODO: Step 1: Create repository and push pipeline config
t.Log("📝 Setting up repository with long-running pipeline...")
// repo, err := env.CreateTestRepository("test-cancel-pipeline", pipelineConfig)
// if err != nil {
// t.Fatalf("Failed to create test repository: %v", err)
// }
// TODO: Step 2: Activate repository
t.Log("🔗 Activating repository...")
// err = env.WoodpeckerClient.ActivateRepo(repo.Owner, repo.Name)
// TODO: Step 3: Trigger pipeline
t.Log("🚀 Triggering pipeline...")
// pipeline, err := env.WoodpeckerClient.TriggerPipeline(repo.Owner, repo.Name, "main")
// if err != nil {
// t.Fatalf("Failed to trigger pipeline: %v", err)
// }
// pipelineID := int(pipeline["number"].(float64))
// t.Logf("✓ Pipeline #%d triggered", pipelineID)
// TODO: Step 4: Wait for pipeline to start running
t.Log("⏳ Waiting for pipeline to start...")
// var pipelineStatus string
// for i := 0; i < 20; i++ {
// p, err := env.WoodpeckerClient.GetPipeline(repo.Owner, repo.Name, pipelineID)
// if err == nil {
// pipelineStatus = p["status"].(string)
// if pipelineStatus == "running" {
// t.Log("✓ Pipeline is now running")
// break
// }
// }
// time.Sleep(500 * time.Millisecond)
// }
//
// if pipelineStatus != "running" {
// t.Fatalf("Pipeline did not start running, status: %s", pipelineStatus)
// }
// TODO: Step 5: Cancel the pipeline
t.Log("🛑 Cancelling pipeline...")
// Give it a moment to ensure it's really running
time.Sleep(2 * time.Second)
// err = env.WoodpeckerClient.CancelPipeline(repo.Owner, repo.Name, pipelineID)
// if err != nil {
// t.Fatalf("Failed to cancel pipeline: %v", err)
// }
// t.Log("✓ Cancel request sent")
// TODO: Step 6: Wait for pipeline to be stopped
t.Log("⏳ Waiting for pipeline to be cancelled...")
// var finalStatus string
// for i := 0; i < 20; i++ {
// p, err := env.WoodpeckerClient.GetPipeline(repo.Owner, repo.Name, pipelineID)
// if err == nil {
// finalStatus = p["status"].(string)
// if finalStatus == "killed" || finalStatus == "cancelled" {
// t.Logf("✓ Pipeline status: %s", finalStatus)
// break
// }
// }
// time.Sleep(500 * time.Millisecond)
// }
// TODO: Step 7: Verify pipeline status
t.Log("✅ Verifying pipeline was cancelled...")
// if finalStatus != "killed" && finalStatus != "cancelled" {
// t.Errorf("Expected pipeline status to be 'killed' or 'cancelled', got: %s", finalStatus)
// }
// TODO: Step 8: Verify steps were stopped
t.Log("📋 Verifying steps were stopped...")
// steps, err := env.WoodpeckerClient.GetPipelineSteps(repo.Owner, repo.Name, pipelineID)
// if err != nil {
// t.Fatalf("Failed to get pipeline steps: %v", err)
// }
//
// Verify that:
// - First step was killed/cancelled
// - Second step never started or was skipped
// for _, step := range steps {
// stepName := step["name"].(string)
// stepStatus := step["status"].(string)
// t.Logf(" Step '%s': %s", stepName, stepStatus)
// }
t.Log("✅ Cancel pipeline flow test completed!")
t.Log("")
t.Log(" This test verifies that:")
t.Log(" - Running pipelines can be cancelled via API")
t.Log(" - Pipeline status is updated to 'killed' or 'cancelled'")
t.Log(" - Running steps are gracefully stopped")
t.Log(" - Pending steps are not started after cancellation")
t.Log(" - Agent properly handles cancellation signals")
}
// TestFlow_CancelPipeline_MultipleSteps tests cancellation with multiple running steps
func TestFlow_CancelPipeline_MultipleSteps(t *testing.T) {
t.Skip("TODO: Implement test for cancelling with multiple concurrent steps")
// This test should verify:
// - Pipeline with parallel steps can be cancelled
// - All running steps are stopped
// - No new steps start after cancellation
}
// TestFlow_CancelPipeline_EarlyStage tests cancellation during early pipeline stages
func TestFlow_CancelPipeline_EarlyStage(t *testing.T) {
t.Skip("TODO: Implement test for early-stage cancellation")
// This test should verify:
// - Pipeline can be cancelled during setup phase
// - Pipeline can be cancelled before first step starts
// - Resources are properly cleaned up even with early cancellation
}
// TestFlow_CancelPipeline_AlreadyCompleted tests attempting to cancel a completed pipeline
func TestFlow_CancelPipeline_AlreadyCompleted(t *testing.T) {
t.Skip("TODO: Implement test for cancelling completed pipeline")
// This test should verify:
// - Attempting to cancel a completed pipeline returns appropriate error/status
// - Pipeline status remains "success" or "failure" (doesn't change to cancelled)
}

View File

@@ -0,0 +1,177 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline_test
import (
"testing"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/blocks"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/env"
)
// TestFlow_ManualTrigger tests the flow of manually triggering a pipeline via API.
//
// Flow:
// 1. Setup test environment
// 2. Create a repository with a pipeline configuration
// 3. Activate the repository
// 4. Manually trigger a pipeline via Woodpecker API (not via webhook)
// 5. Verify that the pipeline is created and queued
// 6. Verify that the pipeline executes successfully
// 7. Verify pipeline metadata shows it was manually triggered
func TestFlow_ManualTrigger(t *testing.T) {
// Setup the complete test environment
e := env.SetupTestEnv(t)
e.Start()
// Define a simple pipeline for manual triggering
pipelineConfig := `
when:
- event: manual
steps:
- name: manual-pipeline-step
image: alpine:latest
commands:
- echo "This pipeline was manually triggered!"
- echo "No webhook was needed"
- echo "Triggered via API call"
`
// TODO: Step 1: Create repository
t.Log("📝 Creating test repository...")
gitRepo := blocks.NewGitRepo(t)
// gitRepo.Init(t)
gitRepo.WriteFile(t, ".woodpecker.yml", []byte(pipelineConfig))
gitRepo.Add(t, ".woodpecker.yml")
gitRepo.Commit(t, ":tada: init")
gitRepo.Push(t)
t.Log("✓ Repository created and pipeline config pushed")
// TODO: Step 3: Activate repository in Woodpecker
t.Log("🔗 Activating repository in Woodpecker...")
// err = env.WoodpeckerClient.ActivateRepo(repo.Owner, repo.Name)
// if err != nil {
// t.Fatalf("Failed to activate repository: %v", err)
// }
repo := blocks.NewTestRepo()
repo.Enable(t)
t.Log("✓ Repository activated")
// TODO: Step 4: Manually trigger a pipeline
t.Log("🚀 Manually triggering pipeline...")
// Trigger with specific branch
// branch := "main"
// pipeline, err := env.WoodpeckerClient.TriggerPipeline(repo.Owner, repo.Name, branch)
// if err != nil {
// t.Fatalf("Failed to trigger pipeline: %v", err)
// }
// pipelineID := int(pipeline["number"].(float64))
// t.Logf("✓ Manual pipeline #%d triggered", pipelineID)
// TODO: Step 5: Wait for pipeline to complete
t.Log("⏳ Waiting for pipeline to complete...")
// status, err := env.WoodpeckerClient.WaitForPipelineComplete(
// repo.Owner,
// repo.Name,
// pipelineID,
// 60*time.Second,
// )
// if err != nil {
// t.Fatalf("Error waiting for pipeline: %v", err)
// }
// TODO: Step 6: Verify pipeline succeeded
t.Log("✅ Verifying pipeline status...")
// if status != "success" {
// t.Fatalf("Expected pipeline to succeed, got status: %s", status)
// }
// t.Log("✓ Pipeline completed successfully")
// TODO: Step 7: Verify pipeline metadata
t.Log("📋 Verifying pipeline metadata...")
// p, err := env.WoodpeckerClient.GetPipeline(repo.Owner, repo.Name, pipelineID)
// if err != nil {
// t.Fatalf("Failed to get pipeline details: %v", err)
// }
//
// Verify event type is "manual" or "deploy"
// event := p["event"].(string)
// if event != "manual" && event != "deploy" {
// t.Errorf("Expected event to be 'manual' or 'deploy', got: %s", event)
// }
//
// Verify the branch
// pipelineBranch := p["branch"].(string)
// if pipelineBranch != branch {
// t.Errorf("Expected branch to be '%s', got: %s", branch, pipelineBranch)
// }
// TODO: Step 8: Verify logs
t.Log("📋 Verifying pipeline logs...")
// logs, err := env.WoodpeckerClient.GetPipelineLogs(repo.Owner, repo.Name, pipelineID)
// if !strings.Contains(logs, "manually triggered") {
// t.Error("Expected 'manually triggered' in logs")
// }
t.Log("✅ Manual trigger flow test completed successfully!")
t.Log("")
t.Log(" This test verifies that:")
t.Log(" - Pipelines can be triggered manually via API")
t.Log(" - No webhook or forge event is required")
t.Log(" - Pipeline executes with correct branch/event metadata")
t.Log(" - Manual triggers work independently of push events")
}
// TestFlow_ManualTrigger_WithVariables tests manual trigger with custom variables
func TestFlow_ManualTrigger_WithVariables(t *testing.T) {
t.Skip("TODO: Implement test for manual trigger with variables")
// This test should verify:
// - Manual triggers can include custom environment variables
// - Variables are properly passed to pipeline steps
// - Variables override defaults when specified
}
// TestFlow_ManualTrigger_DifferentBranches tests manual triggering on different branches
func TestFlow_ManualTrigger_DifferentBranches(t *testing.T) {
t.Skip("TODO: Implement test for manual trigger on different branches")
// This test should verify:
// - Manual trigger works on non-default branches
// - Correct pipeline configuration is used for the specified branch
// - Branch-specific when conditions are evaluated correctly
}
// TestFlow_ManualTrigger_WhilePipelineRunning tests manual trigger while another is running
func TestFlow_ManualTrigger_WhilePipelineRunning(t *testing.T) {
t.Skip("TODO: Implement test for concurrent manual triggers")
// This test should verify:
// - Multiple manual triggers can be queued
// - Each pipeline runs independently
// - Queue and concurrency limits are respected
}
// TestFlow_ManualTrigger_WithParameters tests manual trigger with pipeline parameters
func TestFlow_ManualTrigger_WithParameters(t *testing.T) {
t.Skip("TODO: Implement test for manual trigger with parameters")
// This test should verify:
// - Manual triggers can pass parameters to workflows
// - Parameters are accessible in pipeline configuration
// - Parameter validation works correctly
}

View File

@@ -0,0 +1,98 @@
// Copyright 2026 Woodpecker Authors
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pipeline_test
import (
"testing"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/blocks"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/env"
)
func TestFlow_TriggerPipelineByPush(t *testing.T) {
// Setup the complete test environment
e := env.SetupTestEnv(t)
e.Start()
// Define a simple pipeline configuration
pipelineConfig := `
when:
- event: push
steps:
- name: greeting
image: alpine:latest
commands:
- echo "Hello from Woodpecker CI!"
- echo "This pipeline was triggered by a push event"
`
t.Log("📝 Creating git repository ...")
gitRepo := blocks.NewGitRepo(t)
cloneURL, err := e.Forge.GetRepositoryCloneURL(t.Name())
if err != nil {
t.Fatalf("Failed to get repository clone URL: %v", err)
}
gitRepo.Init(t, cloneURL)
gitRepo.WriteFile(t, "README.md", []byte(t.Name()))
gitRepo.Add(t, "README.md")
gitRepo.Commit(t, ":tada: initial commit")
gitRepo.Push(t)
t.Log("🔗 Activating repository in Woodpecker...")
t.Log("🚀 Pushing pipeline config to trigger pipeline...")
gitRepo.WriteFile(t, ".woodpecker.yml", []byte(pipelineConfig))
gitRepo.Add(t, ".woodpecker.yml")
gitRepo.Commit(t, ":tada: init")
t.Log("✓ Pipeline config committed, pushing to trigger pipeline...")
// TODO: Step 5: Wait for pipeline to be created
t.Log("⏳ Waiting for pipeline to be created...")
// Poll Woodpecker API for pipeline
// var pipelineID int
// for i := 0; i < 10; i++ {
// pipelines, err := env.WoodpeckerClient.GetPipelines(repo.Owner, repo.Name)
// if err == nil && len(pipelines) > 0 {
// pipelineID = pipelines[0]["number"].(int)
// break
// }
// time.Sleep(1 * time.Second)
// }
// TODO: Step 6: Wait for pipeline to complete
t.Log("⏳ Waiting for pipeline to complete...")
// status, err := env.WoodpeckerClient.WaitForPipelineComplete(
// repo.Owner,
// repo.Name,
// pipelineID,
// 60*time.Second,
// )
// TODO: Step 7: Verify pipeline succeeded
t.Log("✅ Verifying pipeline status...")
// if status != "success" {
// t.Fatalf("Expected pipeline to succeed, got status: %s", status)
// }
// TODO: Step 8: Verify logs contain expected output
t.Log("📋 Verifying pipeline logs...")
// logs, err := env.WoodpeckerClient.GetPipelineLogs(repo.Owner, repo.Name, pipelineID)
// if !strings.Contains(logs, "Hello from Woodpecker CI!") {
// t.Error("Expected log output not found")
// }
t.Log("✅ Push trigger flow test completed successfully!")
}

View File

@@ -1,7 +1,7 @@
package integration
package repo_test
import "testing"
func TestRegistryInjected(t *testing.T) {
func TestFlow_RegistryInjected(t *testing.T) {
// TODO: check if a registry was injected into the pipeline config
}

View File

@@ -1,7 +1,7 @@
package integration
package repo_test
import "testing"
func TestSecretInjected(t *testing.T) {
func TestFlow_SecretInjected(t *testing.T) {
// TODO: check if a secret was injected into the pipeline config
}

View File

@@ -0,0 +1,38 @@
package repo_test
import (
"testing"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/blocks"
)
func TestFlow_EnableRepo(t *testing.T) {
t.Parallel()
repo := blocks.NewTestRepo()
repo.Enable(t)
}
func TestFlow_RepairRepo(t *testing.T) {
t.Parallel()
repo := blocks.NewTestRepo()
repo.Enable(t)
repo.Repair(t)
}
func TestFlow_DisableRepo(t *testing.T) {
t.Parallel()
repo := blocks.NewTestRepo()
repo.Enable(t)
repo.Disable(t)
}
func TestFlow_DeleteRepo(t *testing.T) {
t.Parallel()
repo := blocks.NewTestRepo()
repo.Enable(t)
repo.Delete(t)
}

View File

@@ -1,103 +0,0 @@
package integration_test
import (
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
// TestSimplePipelineExecution demonstrates the full integration test workflow
// This test verifies that a basic pipeline can execute successfully through all components
func TestSimplePipelineExecution(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Step 1: Start Woodpecker server
server, err := utils.StartServer(t)
if err != nil {
t.Fatalf("Could not start server: %s", err)
}
defer func() {
if err := server.Stop(); err != nil {
t.Logf("Warning: failed to stop server: %v", err)
}
}()
// Step 2: Start Woodpecker agent
agent, err := utils.StartAgent(t)
if err != nil {
t.Fatalf("Could not start agent: %s", err)
}
defer func() {
if err := agent.Stop(); err != nil {
t.Logf("Warning: failed to stop agent: %v", err)
}
}()
// Give the system a moment to stabilize
time.Sleep(3 * time.Second)
t.Log("✓ Server and agent started successfully")
// Step 3: Create API client (in production, you'd authenticate with the forge)
// For now, we're using the admin user in OPEN mode
client := utils.NewWoodpeckerClient("http://localhost:8000", "")
// Step 4: Create a test repository with a simple pipeline
config := utils.TestRepoConfig{
Name: "test-repo",
PipelineConfig: utils.SimplePipelineConfig(),
}
repoPath := utils.CreateTestRepo(t, config)
t.Logf("✓ Test repository created at: %s", repoPath)
// TODO: The following steps require forge integration
// Once Gitea is running, these would work:
//
// 5. Push repository to Gitea
// 6. Activate repository in Woodpecker via API
// 7. Trigger a pipeline (webhook or manual)
// 8. Wait for pipeline to complete
// 9. Verify pipeline succeeded
// 10. Check pipeline logs
// For now, just verify the API is accessible
_, err = client.GetRepos()
if err != nil {
t.Logf("Note: Could not fetch repos (expected without forge): %v", err)
}
t.Log("✓ Integration test framework is ready!")
t.Log("Next: Add forge (Gitea) integration to complete the workflow")
}
// TestAgentConnection verifies that the agent can connect to the server
func TestAgentConnection(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Start server
server, err := utils.StartServer(t)
if err != nil {
t.Fatalf("Could not start server: %s", err)
}
defer server.Stop()
// Start agent
agent, err := utils.StartAgent(t)
if err != nil {
t.Fatalf("Could not start agent: %s", err)
}
defer agent.Stop()
// Wait for connection to establish
time.Sleep(3 * time.Second)
// TODO: Add API endpoint check to verify agent is connected
// This could be done via the server's API: GET /api/agents
t.Log("✓ Agent connection test completed")
}

View File

@@ -1,49 +0,0 @@
package integration_test
import (
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
// TestEnvStart verifies that all components can start successfully
func TestEnvStart(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Start server
server, err := utils.StartServer(t)
if err != nil {
t.Fatalf("Could not start server: %s", err)
}
defer func() {
if err := server.Stop(); err != nil {
t.Logf("Warning: failed to stop server: %v", err)
}
}()
// Start agent
agent, err := utils.StartAgent(t)
if err != nil {
t.Fatalf("Could not start agent: %s", err)
}
defer func() {
if err := agent.Stop(); err != nil {
t.Logf("Warning: failed to stop agent: %v", err)
}
}()
// Wait for services to stabilize
time.Sleep(3 * time.Second)
// Verify server health endpoint
if err := utils.WaitForHTTP("http://localhost:8000/healthz", 5*time.Second); err != nil {
t.Fatalf("Server health check failed: %v", err)
}
t.Log("✓ All components started successfully")
t.Log("✓ Server is healthy and responding")
t.Log("✓ Agent is running")
}

View File

@@ -1,54 +0,0 @@
package pipeline_test
import "testing"
func TestPipelineByPush(t *testing.T) {
// TODO: push to forge
// TODO: check pipeline list
// TODO: check if pipeline is push
// TODO: check for correct branch, commit message, ...
}
func TestPipelineByTag(t *testing.T) {
// TODO: push tag to forge
// TODO: check pipeline list
// TODO: check if pipeline is tag
// TODO: check for correct tag, ...
}
func TestPipelineByRelease(t *testing.T) {
// TODO: push tag to forge
// TODO: check pipeline list
// TODO: check if pipeline is release
// TODO: check for correct tag, ...
}
func TestPipelineByManual(t *testing.T) {
// TODO: push tag to forge
// TODO: check pipeline list
// TODO: check if pipeline is manual
}
func TestPipelineExecuted(t *testing.T) {
// TODO: push to forge
// TODO: check pipeline list
// TODO: start agent
// TODO: check logs and thereby if agent worked successfully
}
func TestRestartPipeline(t *testing.T) {
// TODO: push to forge
// TODO: check pipeline list
// TODO: start agent
// TODO: check pipeline finished
// TODO: restart pipeline
// TODO: wait for restarted pipeline to finish
}
func TestApprovePipeline(t *testing.T) {
// TODO
}
func TestDeclinePipeline(t *testing.T) {
// TODO
}

View File

@@ -1,42 +0,0 @@
package integration_test
import (
"testing"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
// TestBasicPipelineExecution tests that a simple pipeline can be executed end-to-end
// This is the foundational integration test that verifies:
// 1. Server starts and is accessible
// 2. Agent connects to server
// 3. A simple "hello world" pipeline can be triggered and executed
func TestBasicPipelineExecution(t *testing.T) {
t.Parallel()
// Start the woodpecker server
server, err := utils.StartServer(t)
if err != nil {
t.Fatalf("Could not start server: %s", err)
}
defer server.Stop()
// Start the woodpecker agent
agent, err := utils.StartAgent(t)
if err != nil {
t.Fatalf("Could not start agent: %s", err)
}
defer agent.Stop()
// TODO: check server api if agent is connected
// TODO: Once forge integration is working:
// 1. Create a test repository with a simple .woodpecker.yml
// 2. Register the repository with Woodpecker
// 3. Trigger a pipeline (e.g., via webhook or manual trigger)
// 4. Wait for pipeline to complete
// 5. Verify pipeline succeeded
t.Log("✓ Server and agent started successfully")
t.Log("Next steps: Add repository creation, pipeline trigger, and result verification")
}

View File

@@ -1,19 +0,0 @@
package integration
import "testing"
func TestEnableRepo(t *testing.T) {
}
func TestRepairRepo(t *testing.T) {
}
func TestDisableRepo(t *testing.T) {
}
func TestDeleteRepo(t *testing.T) {
}

View File

@@ -28,7 +28,7 @@ func NewWoodpeckerClient(baseURL, token string) *WoodpeckerClient {
}
// doRequest performs an HTTP request with authentication
func (c *WoodpeckerClient) doRequest(method, path string, body interface{}) (*http.Response, error) {
func (c *WoodpeckerClient) doRequest(method, path string, body any) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
@@ -54,7 +54,7 @@ func (c *WoodpeckerClient) doRequest(method, path string, body interface{}) (*ht
}
// GetRepos fetches the list of repositories
func (c *WoodpeckerClient) GetRepos() ([]map[string]interface{}, error) {
func (c *WoodpeckerClient) GetRepos() ([]map[string]any, error) {
resp, err := c.doRequest("GET", "/api/repos", nil)
if err != nil {
return nil, err
@@ -66,7 +66,7 @@ func (c *WoodpeckerClient) GetRepos() ([]map[string]interface{}, error) {
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
var repos []map[string]interface{}
var repos []map[string]any
if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
@@ -92,7 +92,7 @@ func (c *WoodpeckerClient) ActivateRepo(owner, name string) error {
}
// GetPipeline fetches a specific pipeline
func (c *WoodpeckerClient) GetPipeline(owner, name string, pipelineID int) (map[string]interface{}, error) {
func (c *WoodpeckerClient) GetPipeline(owner, name string, pipelineID int) (map[string]any, error) {
path := fmt.Sprintf("/api/repos/%s/%s/pipelines/%d", owner, name, pipelineID)
resp, err := c.doRequest("GET", path, nil)
if err != nil {
@@ -105,7 +105,7 @@ func (c *WoodpeckerClient) GetPipeline(owner, name string, pipelineID int) (map[
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
var pipeline map[string]interface{}
var pipeline map[string]any
if err := json.NewDecoder(resp.Body).Decode(&pipeline); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
@@ -114,7 +114,7 @@ func (c *WoodpeckerClient) GetPipeline(owner, name string, pipelineID int) (map[
}
// TriggerPipeline manually triggers a pipeline
func (c *WoodpeckerClient) TriggerPipeline(owner, name, branch string) (map[string]interface{}, error) {
func (c *WoodpeckerClient) TriggerPipeline(owner, name, branch string) (map[string]any, error) {
path := fmt.Sprintf("/api/repos/%s/%s/pipelines", owner, name)
body := map[string]string{
"branch": branch,
@@ -131,7 +131,7 @@ func (c *WoodpeckerClient) TriggerPipeline(owner, name, branch string) (map[stri
return nil, fmt.Errorf("failed to trigger pipeline: status %d, body: %s", resp.StatusCode, string(respBody))
}
var pipeline map[string]interface{}
var pipeline map[string]any
if err := json.NewDecoder(resp.Body).Decode(&pipeline); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
@@ -139,6 +139,103 @@ func (c *WoodpeckerClient) TriggerPipeline(owner, name, branch string) (map[stri
return pipeline, nil
}
// CancelPipeline cancels a running pipeline
func (c *WoodpeckerClient) CancelPipeline(owner, name string, pipelineID int) error {
path := fmt.Sprintf("/api/repos/%s/%s/pipelines/%d/cancel", owner, name, pipelineID)
resp, err := c.doRequest("POST", path, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to cancel pipeline: status %d, body: %s", resp.StatusCode, string(body))
}
return nil
}
// GetPipelines fetches all pipelines for a repository
func (c *WoodpeckerClient) GetPipelines(owner, name string) ([]map[string]any, error) {
path := fmt.Sprintf("/api/repos/%s/%s/pipelines", owner, name)
resp, err := c.doRequest("GET", path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
var pipelines []map[string]any
if err := json.NewDecoder(resp.Body).Decode(&pipelines); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return pipelines, nil
}
// GetPipelineSteps fetches all steps for a pipeline
func (c *WoodpeckerClient) GetPipelineSteps(owner, name string, pipelineID int) ([]map[string]any, error) {
path := fmt.Sprintf("/api/repos/%s/%s/pipelines/%d", owner, name, pipelineID)
resp, err := c.doRequest("GET", path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
}
var pipeline map[string]any
if err := json.NewDecoder(resp.Body).Decode(&pipeline); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
// Extract steps/workflows from pipeline
// TODO: Adjust based on actual API response structure
steps, ok := pipeline["steps"].([]any)
if !ok {
return nil, fmt.Errorf("steps not found in pipeline response")
}
result := make([]map[string]any, len(steps))
for i, step := range steps {
result[i] = step.(map[string]any)
}
return result, nil
}
// GetPipelineLogs fetches logs for a pipeline
func (c *WoodpeckerClient) GetPipelineLogs(owner, name string, pipelineID int) (string, error) {
// TODO: Implement based on actual Woodpecker API
// This might require iterating through workflow steps and fetching logs for each
path := fmt.Sprintf("/api/repos/%s/%s/pipelines/%d/logs", owner, name, pipelineID)
resp, err := c.doRequest("GET", path, nil)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("failed to get logs: status %d, body: %s", resp.StatusCode, string(body))
}
logs, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read logs: %w", err)
}
return string(logs), nil
}
// WaitForPipelineComplete waits for a pipeline to complete (success or failure)
func (c *WoodpeckerClient) WaitForPipelineComplete(owner, name string, pipelineID int, timeout time.Duration) (string, error) {
deadline := time.Now().Add(timeout)

View File

@@ -13,10 +13,11 @@ type Command struct {
env map[string]string
}
func NewTask(cmdName string, args ...string) *Command {
func NewCommand(cmdName string, args ...string) *Command {
cmd := exec.Command(cmdName, args...)
return &Command{
cmd: cmd,
env: make(map[string]string),
}
}

View File

@@ -1,33 +0,0 @@
package utils
import "testing"
type TestRepo struct {
folder string
}
func (r *TestRepo) Clone(t *testing.T, sourcePath, remoteURL string) error {
r.folder = t.TempDir()
NewTask("cp", "-r", sourcePath, r.folder).RunOrFail(t)
NewTask("git", "init").RunOrFail(t)
NewTask("git", "remote", "add", remoteURL).RunOrFail(t)
r.Commit(t, ":tada: init")
r.Push(t)
return nil
}
func (r *TestRepo) Commit(t *testing.T, message string) {
NewTask("git", "commit", "-m", message).RunOrFail(t)
}
func (r *TestRepo) Push(t *testing.T) {
NewTask("git", "push", "-u", "origin", "main").RunOrFail(t)
}
func (r *TestRepo) Tag(t *testing.T, name, message string) {
NewTask("git", "tag", "-a", name, "-m", message).RunOrFail(t)
}

View File

@@ -1,94 +0,0 @@
package utils
import (
"os"
"path/filepath"
"testing"
)
// TestRepoConfig holds configuration for creating a test repository
type TestRepoConfig struct {
Name string
PipelineConfig string
}
// CreateTestRepo creates a test repository with a Woodpecker pipeline configuration
func CreateTestRepo(t *testing.T, config TestRepoConfig) string {
// Create temporary directory for the repo
repoDir := t.TempDir()
// Create .woodpecker.yml with the provided config
woodpeckerYml := filepath.Join(repoDir, ".woodpecker.yml")
if err := os.WriteFile(woodpeckerYml, []byte(config.PipelineConfig), 0644); err != nil {
t.Fatalf("Failed to create .woodpecker.yml: %v", err)
}
// Initialize git repository
NewTask("git", "init").WorkDir(repoDir).RunOrFail(t)
NewTask("git", "config", "user.name", "Test User").WorkDir(repoDir).RunOrFail(t)
NewTask("git", "config", "user.email", "test@example.com").WorkDir(repoDir).RunOrFail(t)
NewTask("git", "add", ".").WorkDir(repoDir).RunOrFail(t)
NewTask("git", "commit", "-m", "Initial commit").WorkDir(repoDir).RunOrFail(t)
t.Logf("Created test repository at: %s", repoDir)
return repoDir
}
// SimplePipelineConfig returns a basic pipeline configuration for testing
func SimplePipelineConfig() string {
return `
when:
- event: push
branch: main
steps:
- name: greeting
image: alpine:latest
commands:
- echo "Hello from Woodpecker!"
- echo "Pipeline is working correctly"
`
}
// MultiStepPipelineConfig returns a pipeline with multiple steps
func MultiStepPipelineConfig() string {
return `
when:
- event: push
branch: main
steps:
- name: step1
image: alpine:latest
commands:
- echo "Step 1: Starting"
- sleep 1
- name: step2
image: alpine:latest
commands:
- echo "Step 2: Running"
- sleep 1
- name: step3
image: alpine:latest
commands:
- echo "Step 3: Completed"
`
}
// FailingPipelineConfig returns a pipeline that will fail
func FailingPipelineConfig() string {
return `
when:
- event: push
branch: main
steps:
- name: will-fail
image: alpine:latest
commands:
- echo "This step will fail"
- exit 1
`
}

View File

@@ -1,97 +0,0 @@
package utils
import (
"fmt"
"path/filepath"
"testing"
"time"
)
func StartForge(t *testing.T) (*Service, error) {
// Start Gitea using docker-compose
// Use the existing docker-compose file
projectRoot := getProjectRoot()
composeFile := filepath.Join(projectRoot, "data", "gitea", "docker-compose.yml")
service := NewService("docker-compose", "-f", composeFile, "up", "-d")
if err := service.Start(); err != nil {
return nil, fmt.Errorf("failed to start forge: %w", err)
}
// Wait for Gitea to be ready
if err := WaitForHTTP("http://localhost:3000", 30*time.Second); err != nil {
return nil, fmt.Errorf("forge did not become ready: %w", err)
}
t.Logf("Forge (Gitea) started successfully")
return service, nil
}
func StartServer(t *testing.T) (*Service, error) {
projectRoot := getProjectRoot()
// Prepare web dist directory
NewTask("mkdir", "-p", filepath.Join(projectRoot, "web/dist")).RunOrFail(t)
NewTask("sh", "-c", fmt.Sprintf("echo test > %s", filepath.Join(projectRoot, "web/dist/index.html"))).RunOrFail(t)
service := NewService("go", "run", "./cmd/server/").
WorkDir(projectRoot).
// Server configuration
SetEnv("WOODPECKER_OPEN", "true").
SetEnv("WOODPECKER_ADMIN", "woodpecker").
SetEnv("WOODPECKER_HOST", "http://localhost:8000").
SetEnv("WOODPECKER_SERVER_ADDR", ":8000").
SetEnv("WOODPECKER_GRPC_ADDR", ":9000").
SetEnv("WOODPECKER_WEBHOOK_HOST", "http://localhost:8000").
SetEnv("WOODPECKER_AGENT_SECRET", "test-secret-123").
// Gitea forge configuration
SetEnv("WOODPECKER_GITEA", "true").
SetEnv("WOODPECKER_GITEA_URL", "http://localhost:3000").
SetEnv("WOODPECKER_GITEA_CLIENT", "test-client").
SetEnv("WOODPECKER_GITEA_SECRET", "test-secret").
// Log level
SetEnv("WOODPECKER_LOG_LEVEL", "debug")
if err := service.Start(); err != nil {
return nil, fmt.Errorf("failed to start server: %w", err)
}
// Wait for server to be ready
if err := WaitForHTTP("http://localhost:8000/healthz", 30*time.Second); err != nil {
return nil, fmt.Errorf("server did not become ready: %w", err)
}
t.Logf("Woodpecker server started successfully")
return service, nil
}
func StartAgent(t *testing.T) (*Service, error) {
projectRoot := getProjectRoot()
service := NewService("go", "run", "./cmd/agent/").
WorkDir(projectRoot).
// Agent configuration
SetEnv("WOODPECKER_SERVER", "localhost:9000").
SetEnv("WOODPECKER_AGENT_SECRET", "test-secret-123").
SetEnv("WOODPECKER_MAX_WORKFLOWS", "1").
SetEnv("WOODPECKER_HEALTHCHECK", "false").
SetEnv("WOODPECKER_BACKEND", "docker").
// Log level
SetEnv("WOODPECKER_LOG_LEVEL", "debug")
if err := service.Start(); err != nil {
return nil, fmt.Errorf("failed to start agent: %w", err)
}
// Give agent time to connect
time.Sleep(2 * time.Second)
t.Logf("Woodpecker agent started successfully")
return service, nil
}
func getProjectRoot() string {
// This assumes tests run from test/integration/
return "../.."
}

View File

@@ -1,146 +0,0 @@
package integration_test
import (
"testing"
"time"
"go.woodpecker-ci.org/woodpecker/v3/test/integration/utils"
)
// TestCompleteWorkflow demonstrates a full end-to-end workflow
// NOTE: This test requires Gitea to be running and properly configured
// Uncomment and use once forge integration is complete
func TestCompleteWorkflow(t *testing.T) {
t.Skip("Skipping until forge integration is complete")
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Step 1: Start all services
t.Log("Starting Gitea forge...")
forge, err := utils.StartForge(t)
if err != nil {
t.Fatalf("Could not start forge: %s", err)
}
defer forge.Stop()
t.Log("Starting Woodpecker server...")
server, err := utils.StartServer(t)
if err != nil {
t.Fatalf("Could not start server: %s", err)
}
defer server.Stop()
t.Log("Starting Woodpecker agent...")
agent, err := utils.StartAgent(t)
if err != nil {
t.Fatalf("Could not start agent: %s", err)
}
defer agent.Stop()
time.Sleep(5 * time.Second)
t.Log("✓ All services started")
// Step 2: Create and configure test repository
config := utils.TestRepoConfig{
Name: "test-pipeline-repo",
PipelineConfig: utils.MultiStepPipelineConfig(),
}
repoPath := utils.CreateTestRepo(t, config)
t.Logf("✓ Created test repository at: %s", repoPath)
// Step 3: Push to Gitea
// TODO: Implement pushing to Gitea
// This would involve:
// - Creating a repository in Gitea via API
// - Adding Gitea as a remote
// - Pushing the repository
t.Log("TODO: Push repository to Gitea")
// Step 4: Activate repository in Woodpecker
client := utils.NewWoodpeckerClient("http://localhost:8000", "your-token-here")
owner := "woodpecker"
repoName := "test-pipeline-repo"
err = client.ActivateRepo(owner, repoName)
if err != nil {
t.Fatalf("Failed to activate repository: %v", err)
}
t.Log("✓ Repository activated in Woodpecker")
// Step 5: Trigger a pipeline
pipeline, err := client.TriggerPipeline(owner, repoName, "main")
if err != nil {
t.Fatalf("Failed to trigger pipeline: %v", err)
}
pipelineID := int(pipeline["id"].(float64))
t.Logf("✓ Pipeline #%d triggered", pipelineID)
// Step 6: Wait for pipeline to complete
status, err := client.WaitForPipelineComplete(owner, repoName, pipelineID, 5*time.Minute)
if err != nil {
t.Fatalf("Pipeline did not complete: %v", err)
}
// Step 7: Verify success
if status != "success" {
t.Fatalf("Pipeline failed with status: %s", status)
}
t.Logf("✓ Pipeline completed successfully!")
t.Log("✓ Complete workflow test passed!")
}
// TestPipelineFailure tests that pipeline failures are handled correctly
func TestPipelineFailure(t *testing.T) {
t.Skip("Skipping until forge integration is complete")
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Start services (abbreviated)
server, _ := utils.StartServer(t)
defer server.Stop()
agent, _ := utils.StartAgent(t)
defer agent.Stop()
time.Sleep(3 * time.Second)
// Create a repository with a failing pipeline
config := utils.TestRepoConfig{
Name: "failing-pipeline",
PipelineConfig: utils.FailingPipelineConfig(),
}
utils.CreateTestRepo(t, config)
// TODO: Complete the test once forge integration is ready
// 1. Push to Gitea
// 2. Activate repo
// 3. Trigger pipeline
// 4. Verify it fails with expected status
// 5. Check error logs
t.Log("TODO: Complete pipeline failure test")
}
// TestMultipleConcurrentPipelines tests handling of multiple pipelines
func TestMultipleConcurrentPipelines(t *testing.T) {
t.Skip("Skipping until basic workflow is stable")
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// This test would verify:
// 1. Multiple repositories can be activated
// 2. Pipelines from different repos can run concurrently
// 3. Agent properly handles workflow queue
// 4. Results are correctly associated with respective repos
t.Log("TODO: Implement concurrent pipeline test")
}