mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-09-05 20:07:25 +00:00
389 lines
11 KiB
Go
389 lines
11 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.
|
|
|
|
//go:build test
|
|
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.woodpecker-ci.org/woodpecker/v3/server"
|
|
forge_mocks "go.woodpecker-ci.org/woodpecker/v3/server/forge/mocks"
|
|
"go.woodpecker-ci.org/woodpecker/v3/server/model"
|
|
manager_mocks "go.woodpecker-ci.org/woodpecker/v3/server/services/mocks"
|
|
"go.woodpecker-ci.org/woodpecker/v3/server/store"
|
|
)
|
|
|
|
// cronFixture seeds one repo + user into the store. It is created once per
|
|
// top-level test and shared across subtests; subtests use unique cron names so
|
|
// the per-repo unique constraint never collides.
|
|
func cronFixture(t *testing.T, s store.Store) (*model.Repo, *model.User) {
|
|
t.Helper()
|
|
user := &model.User{Login: "owner", ForgeRemoteID: "u1"}
|
|
require.NoError(t, s.CreateUser(user))
|
|
repo := &model.Repo{
|
|
UserID: user.ID,
|
|
ForgeRemoteID: "r1",
|
|
Owner: "owner",
|
|
Name: "repo",
|
|
FullName: "owner/repo",
|
|
IsActive: true,
|
|
}
|
|
require.NoError(t, s.CreateRepo(repo))
|
|
return repo, user
|
|
}
|
|
|
|
// seedCron inserts a cron directly into the store for read/update/delete tests.
|
|
func seedCron(t *testing.T, s store.Store, repoID int64, name string) *model.Cron {
|
|
t.Helper()
|
|
cron := &model.Cron{
|
|
RepoID: repoID,
|
|
Name: name,
|
|
Schedule: "@every 1h",
|
|
Timezone: "UTC",
|
|
Branch: "main",
|
|
Enabled: true,
|
|
}
|
|
require.NoError(t, s.CronCreate(cron))
|
|
return cron
|
|
}
|
|
|
|
// cronForgeManager installs a mock manager returning a fresh mock forge and
|
|
// returns the forge so tests can add expectations (e.g. BranchHead).
|
|
func cronForgeManager(t *testing.T) *forge_mocks.MockForge {
|
|
t.Helper()
|
|
mgr := manager_mocks.NewMockManager(t)
|
|
forge := forge_mocks.NewMockForge(t)
|
|
mgr.On("ForgeFromRepo", mock.Anything).Return(forge, nil)
|
|
server.Config.Services.Manager = mgr
|
|
return forge
|
|
}
|
|
|
|
func TestGetCron(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, _ := cronFixture(t, s)
|
|
|
|
t.Run("happy path returns cron", func(t *testing.T) {
|
|
cron := seedCron(t, s, repo.ID, "nightly")
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", strItoa(cron.ID))(tc)
|
|
|
|
GetCron(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Equal(t, cron.ID, got.ID)
|
|
assert.Equal(t, "nightly", got.Name)
|
|
})
|
|
|
|
t.Run("invalid id returns bad request", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "not-a-number")(tc)
|
|
|
|
GetCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("missing cron returns not found", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "9999")(tc)
|
|
|
|
GetCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusNotFound, tc.Recorder.Code)
|
|
})
|
|
}
|
|
|
|
func TestGetCronList(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, _ := cronFixture(t, s)
|
|
|
|
t.Run("empty repo returns empty list", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
|
|
GetCronList(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got []*model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Empty(t, got)
|
|
})
|
|
|
|
t.Run("returns all crons for repo", func(t *testing.T) {
|
|
seedCron(t, s, repo.ID, "list-a")
|
|
seedCron(t, s, repo.ID, "list-b")
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
|
|
GetCronList(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got []*model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Len(t, got, 2)
|
|
})
|
|
}
|
|
|
|
func TestDeleteCron(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, _ := cronFixture(t, s)
|
|
|
|
t.Run("happy path deletes", func(t *testing.T) {
|
|
cron := seedCron(t, s, repo.ID, "del")
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", strItoa(cron.ID))(tc)
|
|
|
|
DeleteCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusNoContent, tc.Ctx.Writer.Status())
|
|
_, err := s.CronFind(repo, cron.ID)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid id returns bad request", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "abc")(tc)
|
|
|
|
DeleteCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("missing cron returns not found", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "9999")(tc)
|
|
|
|
DeleteCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusNotFound, tc.Recorder.Code)
|
|
})
|
|
}
|
|
|
|
func TestPostCron(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, user := cronFixture(t, s)
|
|
|
|
t.Run("happy path creates cron", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRequest(http.MethodPost, &model.Cron{Name: "build", Schedule: "@every 1h"})(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Equal(t, "build", got.Name)
|
|
assert.Equal(t, "UTC", got.Timezone) // defaulted
|
|
assert.Positive(t, got.NextExec)
|
|
})
|
|
|
|
t.Run("validation fails on empty name", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRequest(http.MethodPost, &model.Cron{Schedule: "@every 1h"})(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusUnprocessableEntity, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("malformed body returns bad request", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRawBody(http.MethodPost, "application/json", []byte("{not json"))(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("unparseable schedule fails validation", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRequest(http.MethodPost, &model.Cron{Name: "x", Schedule: "totally-invalid"})(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusUnprocessableEntity, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("branch checked on forge when set", func(t *testing.T) {
|
|
forge := cronForgeManager(t)
|
|
forge.On("BranchHead", mock.Anything, user, mock.Anything, "feature").
|
|
Return(&model.Commit{SHA: "abc"}, nil)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRequest(http.MethodPost, &model.Cron{Name: "br", Schedule: "@every 1h", Branch: "feature"})(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
forge.AssertCalled(t, "BranchHead", mock.Anything, user, mock.Anything, "feature")
|
|
})
|
|
|
|
t.Run("duplicate cron returns conflict", func(t *testing.T) {
|
|
seedCron(t, s, repo.ID, "dup")
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withRequest(http.MethodPost, &model.Cron{Name: "dup", Schedule: "@every 1h"})(tc)
|
|
|
|
PostCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusConflict, tc.Recorder.Code)
|
|
})
|
|
}
|
|
|
|
func TestRunCron(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, _ := cronFixture(t, s)
|
|
|
|
t.Run("invalid id returns bad request", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "abc")(tc)
|
|
|
|
RunCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("missing cron returns not found", func(t *testing.T) {
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "9999")(tc)
|
|
|
|
RunCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusNotFound, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("pipeline creation failure returns internal error", func(t *testing.T) {
|
|
cron := seedCron(t, s, repo.ID, "run-fail")
|
|
// CreatePipeline resolves repo+user from the store, then asks the forge
|
|
// for the branch head. Make that fail so RunCron hits its 500 branch
|
|
// without exercising the full pipeline.Create machinery.
|
|
forge := cronForgeManager(t)
|
|
forge.On("BranchHead", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
|
Return(nil, assert.AnError)
|
|
|
|
tc := newTestContext(t, s)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", strItoa(cron.ID))(tc)
|
|
|
|
RunCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, tc.Recorder.Code)
|
|
})
|
|
}
|
|
|
|
func TestPatchCron(t *testing.T) {
|
|
s := newTestStore(t)
|
|
repo, user := cronFixture(t, s)
|
|
|
|
t.Run("happy path updates name and schedule", func(t *testing.T) {
|
|
cron := seedCron(t, s, repo.ID, "orig")
|
|
cronForgeManager(t)
|
|
|
|
newName := "renamed"
|
|
newSchedule := "@every 2h"
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", strItoa(cron.ID))(tc)
|
|
withRequest(http.MethodPatch, &model.CronPatch{Name: &newName, Schedule: &newSchedule})(tc)
|
|
|
|
PatchCron(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Equal(t, "renamed", got.Name)
|
|
assert.Equal(t, "@every 2h", got.Schedule)
|
|
})
|
|
|
|
t.Run("invalid id returns bad request", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "nope")(tc)
|
|
withRequest(http.MethodPatch, &model.CronPatch{})(tc)
|
|
|
|
PatchCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("missing cron returns not found", func(t *testing.T) {
|
|
cronForgeManager(t)
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", "9999")(tc)
|
|
withRequest(http.MethodPatch, &model.CronPatch{})(tc)
|
|
|
|
PatchCron(tc.Ctx)
|
|
|
|
assert.Equal(t, http.StatusNotFound, tc.Recorder.Code)
|
|
})
|
|
|
|
t.Run("branch checked on forge when patched", func(t *testing.T) {
|
|
cron := seedCron(t, s, repo.ID, "brpatch")
|
|
forge := cronForgeManager(t)
|
|
forge.On("BranchHead", mock.Anything, user, mock.Anything, "develop").
|
|
Return(&model.Commit{SHA: "abc"}, nil)
|
|
|
|
branch := "develop"
|
|
tc := newTestContext(t, s)
|
|
withUser(user)(tc)
|
|
withRepo(repo, &model.Perm{})(tc)
|
|
withParam("cron", strItoa(cron.ID))(tc)
|
|
withRequest(http.MethodPatch, &model.CronPatch{Branch: &branch})(tc)
|
|
|
|
PatchCron(tc.Ctx)
|
|
|
|
require.Equal(t, http.StatusOK, tc.Recorder.Code)
|
|
var got model.Cron
|
|
tc.decodeJSON(t, &got)
|
|
assert.Equal(t, "develop", got.Branch)
|
|
})
|
|
}
|