cli/internal/cmd/php/coolify_test.go
Snider 03c9188d79
feat: infrastructure packages and lint cleanup (#281)
* ci: consolidate duplicate workflows and merge CodeQL configs

Remove 17 duplicate workflow files that were split copies of the
combined originals. Each family (CI, CodeQL, Coverage, PR Build,
Alpha Release) had the same job duplicated across separate
push/pull_request/schedule/manual trigger files.

Merge codeql.yml and codescan.yml into a single codeql.yml with
a language matrix covering go, javascript-typescript, python,
and actions — matching the previous default setup coverage.

Remaining workflows (one per family):
- ci.yml (push + PR + manual)
- codeql.yml (push + PR + schedule, all languages)
- coverage.yml (push + PR + manual)
- alpha-release.yml (push + manual)
- pr-build.yml (PR + manual)
- release.yml (tag push)
- agent-verify.yml, auto-label.yml, auto-project.yml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add collect, config, crypt, plugin packages and fix all lint issues

Add four new infrastructure packages with CLI commands:
- pkg/config: layered configuration (defaults → file → env → flags)
- pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums)
- pkg/plugin: plugin system with GitHub-based install/update/remove
- pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate)

Fix all golangci-lint issues across the entire codebase (~100 errcheck,
staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that
`core go qa` passes with 0 issues.

Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:34:43 +00:00

502 lines
16 KiB
Go

package php
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCoolifyClient_Good(t *testing.T) {
t.Run("creates client with correct base URL", func(t *testing.T) {
client := NewCoolifyClient("https://coolify.example.com", "token")
assert.Equal(t, "https://coolify.example.com", client.BaseURL)
assert.Equal(t, "token", client.Token)
assert.NotNil(t, client.HTTPClient)
})
t.Run("strips trailing slash from base URL", func(t *testing.T) {
client := NewCoolifyClient("https://coolify.example.com/", "token")
assert.Equal(t, "https://coolify.example.com", client.BaseURL)
})
t.Run("http client has timeout", func(t *testing.T) {
client := NewCoolifyClient("https://coolify.example.com", "token")
assert.Equal(t, 30*time.Second, client.HTTPClient.Timeout)
})
}
func TestCoolifyConfig_Good(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
config := CoolifyConfig{
URL: "https://coolify.example.com",
Token: "secret-token",
AppID: "app-123",
StagingAppID: "staging-456",
}
assert.Equal(t, "https://coolify.example.com", config.URL)
assert.Equal(t, "secret-token", config.Token)
assert.Equal(t, "app-123", config.AppID)
assert.Equal(t, "staging-456", config.StagingAppID)
})
}
func TestCoolifyDeployment_Good(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
now := time.Now()
deployment := CoolifyDeployment{
ID: "dep-123",
Status: "finished",
CommitSHA: "abc123",
CommitMsg: "Test commit",
Branch: "main",
CreatedAt: now,
FinishedAt: now.Add(5 * time.Minute),
Log: "Build successful",
DeployedURL: "https://app.example.com",
}
assert.Equal(t, "dep-123", deployment.ID)
assert.Equal(t, "finished", deployment.Status)
assert.Equal(t, "abc123", deployment.CommitSHA)
assert.Equal(t, "Test commit", deployment.CommitMsg)
assert.Equal(t, "main", deployment.Branch)
})
}
func TestCoolifyApp_Good(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
app := CoolifyApp{
ID: "app-123",
Name: "MyApp",
FQDN: "https://myapp.example.com",
Status: "running",
Repository: "https://github.com/user/repo",
Branch: "main",
Environment: "production",
}
assert.Equal(t, "app-123", app.ID)
assert.Equal(t, "MyApp", app.Name)
assert.Equal(t, "https://myapp.example.com", app.FQDN)
assert.Equal(t, "running", app.Status)
})
}
func TestLoadCoolifyConfigFromFile_Good(t *testing.T) {
t.Run("loads config from .env file", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL=https://coolify.example.com
COOLIFY_TOKEN=secret-token
COOLIFY_APP_ID=app-123
COOLIFY_STAGING_APP_ID=staging-456`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.NoError(t, err)
assert.Equal(t, "https://coolify.example.com", config.URL)
assert.Equal(t, "secret-token", config.Token)
assert.Equal(t, "app-123", config.AppID)
assert.Equal(t, "staging-456", config.StagingAppID)
})
t.Run("handles quoted values", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL="https://coolify.example.com"
COOLIFY_TOKEN='secret-token'`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.NoError(t, err)
assert.Equal(t, "https://coolify.example.com", config.URL)
assert.Equal(t, "secret-token", config.Token)
})
t.Run("ignores comments", func(t *testing.T) {
dir := t.TempDir()
envContent := `# This is a comment
COOLIFY_URL=https://coolify.example.com
# COOLIFY_TOKEN=wrong-token
COOLIFY_TOKEN=correct-token`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.NoError(t, err)
assert.Equal(t, "correct-token", config.Token)
})
t.Run("ignores blank lines", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL=https://coolify.example.com
COOLIFY_TOKEN=secret-token`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
config, err := LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.NoError(t, err)
assert.Equal(t, "https://coolify.example.com", config.URL)
})
}
func TestLoadCoolifyConfigFromFile_Bad(t *testing.T) {
t.Run("fails when COOLIFY_URL missing", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_TOKEN=secret-token`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
_, err = LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "COOLIFY_URL is not set")
})
t.Run("fails when COOLIFY_TOKEN missing", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL=https://coolify.example.com`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
_, err = LoadCoolifyConfigFromFile(filepath.Join(dir, ".env"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "COOLIFY_TOKEN is not set")
})
}
func TestLoadCoolifyConfig_FromDirectory_Good(t *testing.T) {
t.Run("loads from directory", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL=https://coolify.example.com
COOLIFY_TOKEN=secret-token`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
config, err := LoadCoolifyConfig(dir)
assert.NoError(t, err)
assert.Equal(t, "https://coolify.example.com", config.URL)
})
}
func TestValidateCoolifyConfig_Bad(t *testing.T) {
t.Run("returns error for empty URL", func(t *testing.T) {
config := &CoolifyConfig{Token: "token"}
_, err := validateCoolifyConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "COOLIFY_URL is not set")
})
t.Run("returns error for empty token", func(t *testing.T) {
config := &CoolifyConfig{URL: "https://coolify.example.com"}
_, err := validateCoolifyConfig(config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "COOLIFY_TOKEN is not set")
})
}
func TestCoolifyClient_TriggerDeploy_Good(t *testing.T) {
t.Run("triggers deployment successfully", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-123/deploy", r.URL.Path)
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "Bearer secret-token", r.Header.Get("Authorization"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
resp := CoolifyDeployment{
ID: "dep-456",
Status: "queued",
CreatedAt: time.Now(),
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
deployment, err := client.TriggerDeploy(context.Background(), "app-123", false)
assert.NoError(t, err)
assert.Equal(t, "dep-456", deployment.ID)
assert.Equal(t, "queued", deployment.Status)
})
t.Run("triggers deployment with force", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]interface{}
_ = json.NewDecoder(r.Body).Decode(&body)
assert.Equal(t, true, body["force"])
resp := CoolifyDeployment{ID: "dep-456", Status: "queued"}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
_, err := client.TriggerDeploy(context.Background(), "app-123", true)
assert.NoError(t, err)
})
t.Run("handles minimal response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Return an invalid JSON response to trigger the fallback
_, _ = w.Write([]byte("not json"))
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
deployment, err := client.TriggerDeploy(context.Background(), "app-123", false)
assert.NoError(t, err)
// The fallback response should be returned
assert.Equal(t, "queued", deployment.Status)
})
}
func TestCoolifyClient_TriggerDeploy_Bad(t *testing.T) {
t.Run("fails on HTTP error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Internal error"})
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
_, err := client.TriggerDeploy(context.Background(), "app-123", false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "API error")
})
}
func TestCoolifyClient_GetDeployment_Good(t *testing.T) {
t.Run("gets deployment details", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-123/deployments/dep-456", r.URL.Path)
assert.Equal(t, "GET", r.Method)
resp := CoolifyDeployment{
ID: "dep-456",
Status: "finished",
CommitSHA: "abc123",
Branch: "main",
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
deployment, err := client.GetDeployment(context.Background(), "app-123", "dep-456")
assert.NoError(t, err)
assert.Equal(t, "dep-456", deployment.ID)
assert.Equal(t, "finished", deployment.Status)
assert.Equal(t, "abc123", deployment.CommitSHA)
})
}
func TestCoolifyClient_GetDeployment_Bad(t *testing.T) {
t.Run("fails on 404", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "Not found"})
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
_, err := client.GetDeployment(context.Background(), "app-123", "dep-456")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Not found")
})
}
func TestCoolifyClient_ListDeployments_Good(t *testing.T) {
t.Run("lists deployments", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-123/deployments", r.URL.Path)
assert.Equal(t, "10", r.URL.Query().Get("limit"))
resp := []CoolifyDeployment{
{ID: "dep-1", Status: "finished"},
{ID: "dep-2", Status: "failed"},
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
deployments, err := client.ListDeployments(context.Background(), "app-123", 10)
assert.NoError(t, err)
assert.Len(t, deployments, 2)
assert.Equal(t, "dep-1", deployments[0].ID)
assert.Equal(t, "dep-2", deployments[1].ID)
})
t.Run("lists without limit", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "", r.URL.Query().Get("limit"))
_ = json.NewEncoder(w).Encode([]CoolifyDeployment{})
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
_, err := client.ListDeployments(context.Background(), "app-123", 0)
assert.NoError(t, err)
})
}
func TestCoolifyClient_Rollback_Good(t *testing.T) {
t.Run("triggers rollback", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-123/rollback", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
assert.Equal(t, "dep-old", body["deployment_id"])
resp := CoolifyDeployment{
ID: "dep-new",
Status: "rolling_back",
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
deployment, err := client.Rollback(context.Background(), "app-123", "dep-old")
assert.NoError(t, err)
assert.Equal(t, "dep-new", deployment.ID)
assert.Equal(t, "rolling_back", deployment.Status)
})
}
func TestCoolifyClient_GetApp_Good(t *testing.T) {
t.Run("gets app details", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-123", r.URL.Path)
assert.Equal(t, "GET", r.Method)
resp := CoolifyApp{
ID: "app-123",
Name: "MyApp",
FQDN: "https://myapp.example.com",
Status: "running",
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "secret-token")
app, err := client.GetApp(context.Background(), "app-123")
assert.NoError(t, err)
assert.Equal(t, "app-123", app.ID)
assert.Equal(t, "MyApp", app.Name)
assert.Equal(t, "https://myapp.example.com", app.FQDN)
})
}
func TestCoolifyClient_SetHeaders(t *testing.T) {
t.Run("sets all required headers", func(t *testing.T) {
client := NewCoolifyClient("https://coolify.example.com", "my-token")
req, _ := http.NewRequest("GET", "https://coolify.example.com", nil)
client.setHeaders(req)
assert.Equal(t, "Bearer my-token", req.Header.Get("Authorization"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
assert.Equal(t, "application/json", req.Header.Get("Accept"))
})
}
func TestCoolifyClient_ParseError(t *testing.T) {
t.Run("parses message field", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Bad request message"})
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "token")
_, err := client.GetApp(context.Background(), "app-123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Bad request message")
})
t.Run("parses error field", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "Error message"})
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "token")
_, err := client.GetApp(context.Background(), "app-123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Error message")
})
t.Run("returns raw body when no JSON fields", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Raw error message"))
}))
defer server.Close()
client := NewCoolifyClient(server.URL, "token")
_, err := client.GetApp(context.Background(), "app-123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Raw error message")
})
}
func TestEnvironmentVariablePriority(t *testing.T) {
t.Run("env vars take precedence over .env file", func(t *testing.T) {
dir := t.TempDir()
envContent := `COOLIFY_URL=https://from-file.com
COOLIFY_TOKEN=file-token`
err := os.WriteFile(filepath.Join(dir, ".env"), []byte(envContent), 0644)
require.NoError(t, err)
// Set environment variables
origURL := os.Getenv("COOLIFY_URL")
origToken := os.Getenv("COOLIFY_TOKEN")
defer func() {
_ = os.Setenv("COOLIFY_URL", origURL)
_ = os.Setenv("COOLIFY_TOKEN", origToken)
}()
_ = os.Setenv("COOLIFY_URL", "https://from-env.com")
_ = os.Setenv("COOLIFY_TOKEN", "env-token")
config, err := LoadCoolifyConfig(dir)
assert.NoError(t, err)
// Environment variables should take precedence
assert.Equal(t, "https://from-env.com", config.URL)
assert.Equal(t, "env-token", config.Token)
})
}