Port all PHP command files from core/cli internal/cmd/php/ into a standalone module. Inlines workspace dependency to avoid cross-module internal imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
502 lines
16 KiB
Go
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)
|
|
})
|
|
}
|