mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-04-15 01:41:56 +00:00
286 lines
8.4 KiB
Go
286 lines
8.4 KiB
Go
// 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 rpc
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"go.woodpecker-ci.org/woodpecker/v3/rpc/proto"
|
||
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
||
store_mocks "go.woodpecker-ci.org/woodpecker/v3/server/store/mocks"
|
||
"go.woodpecker-ci.org/woodpecker/v3/server/store/types"
|
||
)
|
||
|
||
// newAuthServer is a test helper that wires up a WoodpeckerAuthServer with the
|
||
// given master token and a mock store, then returns both so tests can set
|
||
// expectations before calling Auth / getAgent.
|
||
func newAuthServer(t *testing.T, masterToken string, store *store_mocks.MockStore) *WoodpeckerAuthServer {
|
||
t.Helper()
|
||
jwtManager := NewJWTManager("test-secret")
|
||
return NewWoodpeckerAuthServer(jwtManager, masterToken, store)
|
||
}
|
||
|
||
func TestAuth(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
t.Run("master token with agentID=-1 creates new system agent and returns access token", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentCreate", &model.Agent{
|
||
OwnerID: model.IDNotSet,
|
||
OrgID: model.IDNotSet,
|
||
Token: "master-secret",
|
||
Capacity: -1,
|
||
}).Return(nil).Once()
|
||
|
||
srv := newAuthServer(t, "master-secret", store)
|
||
resp, err := srv.Auth(t.Context(), &proto.AuthRequest{
|
||
AgentId: -1,
|
||
AgentToken: "master-secret",
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "ok", resp.Status)
|
||
assert.NotEmpty(t, resp.AccessToken)
|
||
// The newly created agent has ID 0 (zero-value) because AgentCreate
|
||
// doesn't set it in the mock – verify the token at least round-trips.
|
||
claims, verifyErr := NewJWTManager("test-secret").Verify(resp.AccessToken)
|
||
require.NoError(t, verifyErr)
|
||
assert.Equal(t, resp.AgentId, claims.AgentID)
|
||
})
|
||
|
||
t.Run("master token with existing agentID returns access token for that agent", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
existingAgent := &model.Agent{
|
||
ID: 42,
|
||
OrgID: model.IDNotSet, // system agent
|
||
OwnerID: model.IDNotSet,
|
||
}
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFind", int64(42)).Return(existingAgent, nil).Once()
|
||
|
||
srv := newAuthServer(t, "master-secret", store)
|
||
resp, err := srv.Auth(t.Context(), &proto.AuthRequest{
|
||
AgentId: 42,
|
||
AgentToken: "master-secret",
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "ok", resp.Status)
|
||
assert.EqualValues(t, 42, resp.AgentId)
|
||
assert.NotEmpty(t, resp.AccessToken)
|
||
})
|
||
|
||
t.Run("individual agent token authenticates successfully", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
agent := &model.Agent{ID: 7, Token: "individual-token"}
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFindByToken", "individual-token").Return(agent, nil).Once()
|
||
|
||
// no master token configured
|
||
srv := newAuthServer(t, "", store)
|
||
resp, err := srv.Auth(t.Context(), &proto.AuthRequest{
|
||
AgentId: 0,
|
||
AgentToken: "individual-token",
|
||
})
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "ok", resp.Status)
|
||
assert.EqualValues(t, 7, resp.AgentId)
|
||
})
|
||
|
||
t.Run("bad token returns error", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFindByToken", "wrong-token").
|
||
Return(nil, types.ErrRecordNotExist).Once()
|
||
|
||
srv := newAuthServer(t, "", store)
|
||
_, err := srv.Auth(t.Context(), &proto.AuthRequest{
|
||
AgentToken: "wrong-token",
|
||
})
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "agent could not auth")
|
||
})
|
||
}
|
||
|
||
func TestGetAgent(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
t.Run("master token + agentID=-1 creates and returns a new system agent", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentCreate", &model.Agent{
|
||
OwnerID: model.IDNotSet,
|
||
OrgID: model.IDNotSet,
|
||
Token: "master",
|
||
Capacity: -1,
|
||
}).Return(nil).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
agent, err := srv.getAgent(-1, "master")
|
||
|
||
require.NoError(t, err)
|
||
require.NotNil(t, agent)
|
||
assert.Equal(t, "master", agent.Token)
|
||
assert.EqualValues(t, model.IDNotSet, agent.OrgID)
|
||
})
|
||
|
||
t.Run("master token + agentID=-1 propagates AgentCreate error", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentCreate", &model.Agent{
|
||
OwnerID: model.IDNotSet,
|
||
OrgID: model.IDNotSet,
|
||
Token: "master",
|
||
Capacity: -1,
|
||
}).Return(errors.New("db error")).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
_, err := srv.getAgent(-1, "master")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "db error")
|
||
})
|
||
|
||
t.Run("master token + existing agentID returns the stored agent", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
systemAgent := &model.Agent{ID: 99, OrgID: model.IDNotSet, OwnerID: model.IDNotSet}
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFind", int64(99)).Return(systemAgent, nil).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
agent, err := srv.getAgent(99, "master")
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, int64(99), agent.ID)
|
||
})
|
||
|
||
t.Run("master token + agentID not found in database returns error", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFind", int64(404)).Return(nil, types.ErrRecordNotExist).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
_, err := srv.getAgent(404, "master")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "AgentID not found in database")
|
||
})
|
||
|
||
t.Run("master token + agentID store returns unexpected error is propagated", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFind", int64(1)).Return(nil, errors.New("connection reset")).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
_, err := srv.getAgent(1, "master")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "connection reset")
|
||
})
|
||
|
||
t.Run("master token + agentID that is not a system agent returns error", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// An agent with a non-IDNotSet OrgID is not a system agent.
|
||
orgAgent := &model.Agent{ID: 5, OrgID: 100, OwnerID: model.IDNotSet}
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFind", int64(5)).Return(orgAgent, nil).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
_, err := srv.getAgent(5, "master")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "not a system agent")
|
||
})
|
||
|
||
t.Run("individual token auth succeeds when token is found", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
agent := &model.Agent{ID: 3, Token: "ind-token"}
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFindByToken", "ind-token").Return(agent, nil).Once()
|
||
|
||
// No master token set – falls straight to individual auth.
|
||
srv := newAuthServer(t, "", store)
|
||
got, err := srv.getAgent(0, "ind-token")
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, int64(3), got.ID)
|
||
})
|
||
|
||
t.Run("individual token not found returns wrapped error", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFindByToken", "bad-token").
|
||
Return(nil, types.ErrRecordNotExist).Once()
|
||
|
||
srv := newAuthServer(t, "", store)
|
||
_, err := srv.getAgent(0, "bad-token")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "individual agent not found by token")
|
||
})
|
||
|
||
t.Run("individual token store returns unexpected error is propagated", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
store := store_mocks.NewMockStore(t)
|
||
store.On("AgentFindByToken", "token").
|
||
Return(nil, errors.New("timeout")).Once()
|
||
|
||
srv := newAuthServer(t, "", store)
|
||
_, err := srv.getAgent(0, "token")
|
||
|
||
require.Error(t, err)
|
||
assert.Contains(t, err.Error(), "timeout")
|
||
})
|
||
|
||
t.Run("master token configured but wrong token falls through to individual auth", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
agent := &model.Agent{ID: 8, Token: "ind-token"}
|
||
store := store_mocks.NewMockStore(t)
|
||
// master token is "master" but caller sends "ind-token" → individual path
|
||
store.On("AgentFindByToken", "ind-token").Return(agent, nil).Once()
|
||
|
||
srv := newAuthServer(t, "master", store)
|
||
got, err := srv.getAgent(0, "ind-token")
|
||
|
||
require.NoError(t, err)
|
||
assert.Equal(t, int64(8), got.ID)
|
||
})
|
||
}
|