Files
plugin-drone-jenkins/plugin_test.go
T
Bo-Yi Wu e1985fadc9 refactor: extract repeated string literals into constants
- Add tokenParam const in jenkins.go and reuse across main.go
- Add shared test_helpers_test.go with test constants
- Remove unused //nolint:gosec directive in jenkins.go
- Resolve golangci-lint v2.12 goconst and nolintlint warnings
2026-05-08 22:49:00 +08:00

515 lines
13 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
const (
testJobBuildPath = "/job/test-job/build"
testQueueItemPath = "/queue/item/123/api/json"
testBuildStatusPath = "/job/test-job/456/api/json"
)
// TestValidateConfig tests the validateConfig method
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
plugin Plugin
wantError bool
errorMsg string
}{
{
name: "missing all config",
plugin: Plugin{},
wantError: true,
errorMsg: "jenkins base URL is required",
},
{
name: "missing authentication",
plugin: Plugin{
BaseURL: testExampleURL,
},
wantError: true,
errorMsg: testAuthRequiredErr,
},
{
name: "missing token (only username)",
plugin: Plugin{
BaseURL: testExampleURL,
Username: testUserFoo,
},
wantError: true,
errorMsg: testAuthRequiredErr,
},
{
name: "missing username (only token)",
plugin: Plugin{
BaseURL: testExampleURL,
Token: testUserBar,
},
wantError: true,
errorMsg: testAuthRequiredErr,
},
{
name: "user and token auth",
plugin: Plugin{
BaseURL: testExampleURL,
Username: testUserFoo,
Token: testUserBar,
},
wantError: false,
},
{
name: "remote token auth",
plugin: Plugin{
BaseURL: testExampleURL,
RemoteToken: testRemoteTokenValue,
},
wantError: false,
},
{
name: "both auth methods",
plugin: Plugin{
BaseURL: testExampleURL,
Username: testUserFoo,
Token: testUserBar,
RemoteToken: testRemoteTokenValue,
},
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.plugin.validateConfig()
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
// TestTrimWhitespaceFromSlice tests the trimWhitespaceFromSlice function
func TestTrimWhitespaceFromSlice(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "remove empty and whitespace strings",
input: []string{"1", " ", "3"},
expected: []string{"1", "3"},
},
{
name: "no whitespace strings",
input: []string{"1", "2"},
expected: []string{"1", "2"},
},
{
name: "all whitespace",
input: []string{testWhitespaceVal, "\t", "\n"},
expected: []string{},
},
{
name: "empty slice",
input: []string{},
expected: []string{},
},
{
name: "trim surrounding whitespace",
input: []string{" foo ", " bar ", "baz"},
expected: []string{testUserFoo, testUserBar, "baz"},
},
{
name: "mixed empty and valid",
input: []string{"", testValidStr, "", "also-valid", ""},
expected: []string{testValidStr, "also-valid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := trimWhitespaceFromSlice(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestParseParameters tests the parseParameters function
func TestParseParameters(t *testing.T) {
tests := []struct {
name string
input string
expected url.Values
}{
{
name: "valid parameters",
input: "key1=value1\nkey2=value2",
expected: url.Values{
testParamKey1: []string{testParamValue1},
testParamKey2: []string{testParamValue2},
},
},
{
name: "parameter with multiple equals signs",
input: "key=value=with=equals",
expected: url.Values{
testParamKey: []string{"value=with=equals"},
},
},
{
name: "parameter with spaces in value",
input: "key=value with spaces",
expected: url.Values{
testParamKey: []string{"value with spaces"},
},
},
{
name: "parameter with empty value",
input: "key=",
expected: url.Values{
testParamKey: []string{""},
},
},
{
name: "invalid parameter format (no equals)",
input: "invalid",
expected: url.Values{},
},
{
name: "parameter with empty key",
input: "=value",
expected: url.Values{},
},
{
name: "mixed valid and invalid",
input: "valid=yes\ninvalid\nalso=valid",
expected: url.Values{
testValidStr: []string{"yes"},
"also": []string{testValidStr},
},
},
{
name: "key with surrounding whitespace",
input: " key =value",
expected: url.Values{
testParamKey: []string{"value"},
},
},
{
name: "empty string",
input: "",
expected: url.Values{},
},
{
name: "multiple empty lines",
input: "key1=value1\n\n\nkey2=value2",
expected: url.Values{
testParamKey1: []string{testParamValue1},
testParamKey2: []string{testParamValue2},
},
},
{
name: "lines with whitespace only",
input: "key1=value1\n \n\t\nkey2=value2",
expected: url.Values{
testParamKey1: []string{testParamValue1},
testParamKey2: []string{testParamValue2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseParameters(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestExecMissingConfig tests Exec with missing configuration
func TestExecMissingConfig(t *testing.T) {
var plugin Plugin
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), "jenkins base URL is required")
}
// TestExecMissingJenkinsUsername tests Exec with missing username
func TestExecMissingJenkinsUsername(t *testing.T) {
plugin := Plugin{
BaseURL: testExampleURL,
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), testAuthRequiredErr)
}
// TestExecMissingJenkinsToken tests Exec with missing token
func TestExecMissingJenkinsToken(t *testing.T) {
plugin := Plugin{
BaseURL: testExampleURL,
Username: testUserFoo,
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "configuration error")
assert.Contains(t, err.Error(), testAuthRequiredErr)
}
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
func TestExecMissingJenkinsJob(t *testing.T) {
tests := []struct {
name string
jobs []string
}{
{
name: "no jobs",
jobs: []string{},
},
{
name: "only whitespace jobs",
jobs: []string{testWhitespaceVal, "\t", "\n"},
},
{
name: "nil jobs",
jobs: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Plugin{
BaseURL: testExampleURL,
Username: testUserFoo,
Token: testUserBar,
Job: tt.jobs,
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one Jenkins job name is required")
})
}
}
// TestExecTriggerBuild tests successful job triggering
func TestExecTriggerBuild(t *testing.T) {
// Create a mock Jenkins server
queueID := 1
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", queueID))
w.WriteHeader(http.StatusCreated)
queueID++
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{"drone-jenkins"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
}
// TestExecTriggerMultipleJobs tests triggering multiple jobs
func TestExecTriggerMultipleJobs(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only count POST requests (job triggers), not GET requests (crumb)
if r.Method == "POST" {
jobsTriggered++
}
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{"job1", "job2", "job3"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, 3, jobsTriggered)
}
// TestExecWithParameters tests job triggering with parameters
func TestExecWithParameters(t *testing.T) {
// Create a mock Jenkins server
var receivedQuery url.Values
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedQuery = r.URL.Query()
w.Header().Set("Location", "http://jenkins.example.com/queue/item/1/")
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{"parameterized-job"},
Parameters: "branch=main\nenvironment=production",
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, "main", receivedQuery.Get("branch"))
assert.Equal(t, "production", receivedQuery.Get("environment"))
}
// TestExecWithRemoteToken tests job triggering with remote token
func TestExecWithRemoteToken(t *testing.T) {
// Create a mock Jenkins server
var receivedToken string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedToken = r.URL.Query().Get("token")
w.Header().Set("Location", "http://jenkins.example.com/queue/item/1/")
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
RemoteToken: testRemoteTokenValue,
Job: []string{"secure-job"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
assert.Equal(t, testRemoteTokenValue, receivedToken)
}
// TestExecWithJobsContainingWhitespace tests job list with whitespace
func TestExecWithJobsContainingWhitespace(t *testing.T) {
// Create a mock Jenkins server
jobsTriggered := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only count POST requests (job triggers), not GET requests (crumb)
if r.Method == "POST" {
jobsTriggered++
}
w.Header().
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{" job1 ", "job2", testWhitespaceVal, "job3"},
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
// Should trigger 3 jobs (whitespace-only entry should be filtered out)
assert.Equal(t, 3, jobsTriggered)
}
// TestExecWithWaitSuccess tests job execution with wait for successful completion
func TestExecWithWaitSuccess(t *testing.T) {
// Create a mock Jenkins server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case testJobBuildPath:
// Trigger build
w.Header().Set("Location", "http://jenkins.example.com/queue/item/123/")
w.WriteHeader(http.StatusCreated)
case testQueueItemPath:
// Queue item with build number
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"executable":{"number":456}}`))
case testBuildStatusPath:
// Build completed successfully
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"number":456,"building":false,"result":"SUCCESS"}`))
}
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{testJobName},
Wait: true,
}
err := plugin.Exec(context.Background())
assert.NoError(t, err)
}
// TestExecWithWaitFailure tests job execution with wait for failed build
func TestExecWithWaitFailure(t *testing.T) {
// Create a mock Jenkins server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case testJobBuildPath:
// Trigger build
w.Header().Set("Location", "http://jenkins.example.com/queue/item/123/")
w.WriteHeader(http.StatusCreated)
case testQueueItemPath:
// Queue item with build number
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":123,"executable":{"number":456}}`))
case testBuildStatusPath:
// Build completed with failure
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"number":456,"building":false,"result":"FAILURE"}`))
}
}))
defer server.Close()
plugin := Plugin{
BaseURL: server.URL,
Username: testUserFoo,
Token: testUserBar,
Job: []string{testJobName},
Wait: true,
}
err := plugin.Exec(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed with status: FAILURE")
}