// 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 setup import ( "context" "net" "sync" "testing" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "github.com/urfave/cli/v3" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" "go.woodpecker-ci.org/woodpecker/v3/rpc/proto" "go.woodpecker-ci.org/woodpecker/v3/server" "go.woodpecker-ci.org/woodpecker/v3/server/cache" "go.woodpecker-ci.org/woodpecker/v3/server/forge" forge_mocks "go.woodpecker-ci.org/woodpecker/v3/server/forge/mocks" forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" "go.woodpecker-ci.org/woodpecker/v3/server/logging" "go.woodpecker-ci.org/woodpecker/v3/server/model" "go.woodpecker-ci.org/woodpecker/v3/server/pubsub/memory" "go.woodpecker-ci.org/woodpecker/v3/server/queue" server_rpc "go.woodpecker-ci.org/woodpecker/v3/server/rpc" "go.woodpecker-ci.org/woodpecker/v3/server/scheduler" "go.woodpecker-ci.org/woodpecker/v3/server/services" "go.woodpecker-ci.org/woodpecker/v3/server/services/permissions" "go.woodpecker-ci.org/woodpecker/v3/server/store" ) const ( // TestAgentToken is the shared secret used between the in-process server // and agent. Hard-coded for tests — not a real secret. TestAgentToken = "test-agent-secret-for-integration-tests" // TestJWTSecret is used for signing gRPC auth JWTs. TestJWTSecret = "test-jwt-secret-for-integration-tests" // TestForgeType is the forge type the mock pretends to bee. TestForgeType = model.ForgeTypeGitea ) var configLock = sync.Mutex{} // ServerEnv holds all the pieces of a running test server environment. type ServerEnv struct { GRPCAddr string Store store.Store Queue queue.Queue Fixtures *Fixtures Forge *forge_mocks.MockForge Manager services.Manager } // StartServer wires up the full in-process server stack: // - in-memory sqlite store (fully migrated) with seeded fixtures // - in-memory queue, pubsub, and logging // - MockForge that serves the provided workflow files // - gRPC server on a random TCP port // // files must contain at least one entry. Single-workflow scenarios pass one // file named ".woodpecker.yaml"; multi-workflow scenarios pass multiple files // named ".woodpecker/foo.yaml" etc. The repo's Config path is set accordingly. // // All resources are cleaned up via t.Cleanup. func StartServer(ctx context.Context, t *testing.T, files []*forge_types.FileMeta) *ServerEnv { t.Helper() configLock.Lock() defer configLock.Unlock() memStore := newStore(ctx, t) fixtures := seedFixtures(t, memStore) mockForge := newMockForge(t, files) mgr, err := newTestManager(memStore, mockForge) require.NoError(t, err, "create services manager") memQueue, err := queue.New(ctx, queue.Config{Backend: queue.TypeMemory}) require.NoError(t, err, "create queue") // Save and restore server.Config around the test. server.Config is a // package-level global read by server/pipeline and server/rpc. Tests run // sequentially within a package, but we still need to clean up so the next // subtest starts from a known-zero state rather than the previous test's values. orig := server.Config t.Cleanup(func() { configLock.Lock() defer configLock.Unlock() server.Config = orig }) server.Config.Services.Logs = logging.New() server.Config.Services.Scheduler = scheduler.NewScheduler(memQueue, memory.New()) server.Config.Services.Membership = cache.NewMembershipService(memStore) server.Config.Services.Manager = mgr server.Config.Services.LogStore = memStore server.Config.Server.AgentToken = TestAgentToken server.Config.Server.Host = "http://localhost" server.Config.Server.JWTSecret = TestJWTSecret server.Config.Pipeline.DefaultClonePlugin = "docker.io/woodpeckerci/plugin-git:latest" server.Config.Pipeline.TrustedClonePlugins = []string{"docker.io/woodpeckerci/plugin-git:latest"} server.Config.Pipeline.DefaultApprovalMode = model.RequireApprovalNone server.Config.Pipeline.DefaultTimeout = 60 server.Config.Pipeline.MaxTimeout = 60 server.Config.Permissions.Open = true server.Config.Permissions.Admins = permissions.NewAdmins([]string{}) server.Config.Permissions.Orgs = permissions.NewOrgs([]string{}) server.Config.Permissions.OwnersAllowlist = permissions.NewOwnersAllowlist([]string{}) grpcAddr := startGRPCServer(ctx, t, memStore) return &ServerEnv{ GRPCAddr: grpcAddr, Store: memStore, Queue: memQueue, Fixtures: fixtures, Forge: mockForge, Manager: mgr, } } // newTestManager builds a services.Manager whose SetupForge always returns // the provided MockForge, bypassing real forge instantiation. func newTestManager(s store.Store, mockForge *forge_mocks.MockForge) (services.Manager, error) { cmd := &cli.Command{ Flags: []cli.Flag{ // Config fetch tuning. &cli.DurationFlag{Name: "forge-timeout", Value: defaultTimeout}, &cli.UintFlag{Name: "forge-retry", Value: defaultRetry}, &cli.StringSliceFlag{Name: "environment"}, // Forge flags — gitea=true satisfies setupForgeService's type switch. &cli.BoolFlag{Name: string(TestForgeType), Value: true}, &cli.StringFlag{Name: "forge-url", Value: "https://forge.example.test"}, }, } setupForge := services.SetupForge(func(*model.Forge) (forge.Forge, error) { return mockForge, nil }) return services.NewManager(cmd, s, setupForge) } // startGRPCServer binds to a random TCP port, registers Woodpecker's gRPC // services, and starts serving. Shutdown happens via t.Cleanup. func startGRPCServer(ctx context.Context, t *testing.T, s store.Store) string { t.Helper() lis, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err, "listen on random port for gRPC") addr := lis.Addr().String() jwtManager := server_rpc.NewJWTManager(TestJWTSecret) authorizer := server_rpc.NewAuthorizer(jwtManager) grpcServer := grpc.NewServer( grpc.StreamInterceptor(authorizer.StreamInterceptor), grpc.UnaryInterceptor(authorizer.UnaryInterceptor), grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: shortTimeout, }), ) proto.RegisterWoodpeckerServer(grpcServer, server_rpc.NewTestWoodpeckerServer( server.Config.Services.Scheduler, server.Config.Services.Logs, s, prometheus.NewRegistry(), )) proto.RegisterWoodpeckerAuthServer(grpcServer, server_rpc.NewWoodpeckerAuthServer( jwtManager, TestAgentToken, s, )) stopped := make(chan struct{}) grpcCtx, grpcCancel := context.WithCancelCause(ctx) go func() { <-grpcCtx.Done() grpcServer.GracefulStop() close(stopped) }() go func() { if err := grpcServer.Serve(lis); err != nil { grpcCancel(err) } }() t.Cleanup(func() { grpcCancel(nil) <-stopped }) return addr }