cli/internal/cmd/php/services_extended_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

313 lines
7.9 KiB
Go

package php
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBaseService_Name_Good(t *testing.T) {
t.Run("returns service name", func(t *testing.T) {
s := &baseService{name: "TestService"}
assert.Equal(t, "TestService", s.Name())
})
}
func TestBaseService_Status_Good(t *testing.T) {
t.Run("returns status when not running", func(t *testing.T) {
s := &baseService{
name: "TestService",
port: 8080,
running: false,
}
status := s.Status()
assert.Equal(t, "TestService", status.Name)
assert.Equal(t, 8080, status.Port)
assert.False(t, status.Running)
assert.Equal(t, 0, status.PID)
})
t.Run("returns status when running", func(t *testing.T) {
s := &baseService{
name: "TestService",
port: 8080,
running: true,
}
status := s.Status()
assert.True(t, status.Running)
})
t.Run("returns error in status", func(t *testing.T) {
testErr := assert.AnError
s := &baseService{
name: "TestService",
lastError: testErr,
}
status := s.Status()
assert.Equal(t, testErr, status.Error)
})
}
func TestBaseService_Logs_Good(t *testing.T) {
t.Run("returns log file content", func(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "test.log")
err := os.WriteFile(logPath, []byte("test log content"), 0644)
require.NoError(t, err)
s := &baseService{logPath: logPath}
reader, err := s.Logs(false)
assert.NoError(t, err)
assert.NotNil(t, reader)
_ = reader.Close()
})
t.Run("returns tail reader in follow mode", func(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "test.log")
err := os.WriteFile(logPath, []byte("test log content"), 0644)
require.NoError(t, err)
s := &baseService{logPath: logPath}
reader, err := s.Logs(true)
assert.NoError(t, err)
assert.NotNil(t, reader)
// Verify it's a tailReader by checking it implements ReadCloser
_, ok := reader.(*tailReader)
assert.True(t, ok)
_ = reader.Close()
})
}
func TestBaseService_Logs_Bad(t *testing.T) {
t.Run("returns error when no log path", func(t *testing.T) {
s := &baseService{name: "TestService"}
_, err := s.Logs(false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no log file available")
})
t.Run("returns error when log file doesn't exist", func(t *testing.T) {
s := &baseService{logPath: "/nonexistent/path/log.log"}
_, err := s.Logs(false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Failed to open log file")
})
}
func TestTailReader_Good(t *testing.T) {
t.Run("creates new tail reader", func(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "test.log")
err := os.WriteFile(logPath, []byte("content"), 0644)
require.NoError(t, err)
file, err := os.Open(logPath)
require.NoError(t, err)
defer func() { _ = file.Close() }()
reader := newTailReader(file)
assert.NotNil(t, reader)
assert.NotNil(t, reader.file)
assert.NotNil(t, reader.reader)
assert.False(t, reader.closed)
})
t.Run("closes file on Close", func(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "test.log")
err := os.WriteFile(logPath, []byte("content"), 0644)
require.NoError(t, err)
file, err := os.Open(logPath)
require.NoError(t, err)
reader := newTailReader(file)
err = reader.Close()
assert.NoError(t, err)
assert.True(t, reader.closed)
})
t.Run("returns EOF when closed", func(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "test.log")
err := os.WriteFile(logPath, []byte("content"), 0644)
require.NoError(t, err)
file, err := os.Open(logPath)
require.NoError(t, err)
reader := newTailReader(file)
_ = reader.Close()
buf := make([]byte, 100)
n, _ := reader.Read(buf)
// When closed, should return 0 bytes (the closed flag causes early return)
assert.Equal(t, 0, n)
})
}
func TestFrankenPHPService_Extended(t *testing.T) {
t.Run("all options set correctly", func(t *testing.T) {
opts := FrankenPHPOptions{
Port: 9000,
HTTPSPort: 9443,
HTTPS: true,
CertFile: "/path/to/cert.pem",
KeyFile: "/path/to/key.pem",
}
service := NewFrankenPHPService("/project", opts)
assert.Equal(t, "FrankenPHP", service.Name())
assert.Equal(t, 9000, service.port)
assert.Equal(t, 9443, service.httpsPort)
assert.True(t, service.https)
assert.Equal(t, "/path/to/cert.pem", service.certFile)
assert.Equal(t, "/path/to/key.pem", service.keyFile)
assert.Equal(t, "/project", service.dir)
})
}
func TestViteService_Extended(t *testing.T) {
t.Run("auto-detects package manager", func(t *testing.T) {
dir := t.TempDir()
// Create bun.lockb to trigger bun detection
err := os.WriteFile(filepath.Join(dir, "bun.lockb"), []byte(""), 0644)
require.NoError(t, err)
service := NewViteService(dir, ViteOptions{})
assert.Equal(t, "bun", service.packageManager)
})
t.Run("uses provided package manager", func(t *testing.T) {
dir := t.TempDir()
service := NewViteService(dir, ViteOptions{PackageManager: "pnpm"})
assert.Equal(t, "pnpm", service.packageManager)
})
}
func TestHorizonService_Extended(t *testing.T) {
t.Run("has zero port", func(t *testing.T) {
service := NewHorizonService("/project")
assert.Equal(t, 0, service.port)
})
}
func TestReverbService_Extended(t *testing.T) {
t.Run("uses default port 8080", func(t *testing.T) {
service := NewReverbService("/project", ReverbOptions{})
assert.Equal(t, 8080, service.port)
})
t.Run("uses custom port", func(t *testing.T) {
service := NewReverbService("/project", ReverbOptions{Port: 9090})
assert.Equal(t, 9090, service.port)
})
}
func TestRedisService_Extended(t *testing.T) {
t.Run("uses default port 6379", func(t *testing.T) {
service := NewRedisService("/project", RedisOptions{})
assert.Equal(t, 6379, service.port)
})
t.Run("accepts config file", func(t *testing.T) {
service := NewRedisService("/project", RedisOptions{ConfigFile: "/path/to/redis.conf"})
assert.Equal(t, "/path/to/redis.conf", service.configFile)
})
}
func TestServiceStatus_Struct(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
testErr := assert.AnError
status := ServiceStatus{
Name: "TestService",
Running: true,
PID: 12345,
Port: 8080,
Error: testErr,
}
assert.Equal(t, "TestService", status.Name)
assert.True(t, status.Running)
assert.Equal(t, 12345, status.PID)
assert.Equal(t, 8080, status.Port)
assert.Equal(t, testErr, status.Error)
})
}
func TestFrankenPHPOptions_Struct(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
opts := FrankenPHPOptions{
Port: 8000,
HTTPSPort: 443,
HTTPS: true,
CertFile: "cert.pem",
KeyFile: "key.pem",
}
assert.Equal(t, 8000, opts.Port)
assert.Equal(t, 443, opts.HTTPSPort)
assert.True(t, opts.HTTPS)
assert.Equal(t, "cert.pem", opts.CertFile)
assert.Equal(t, "key.pem", opts.KeyFile)
})
}
func TestViteOptions_Struct(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
opts := ViteOptions{
Port: 5173,
PackageManager: "bun",
}
assert.Equal(t, 5173, opts.Port)
assert.Equal(t, "bun", opts.PackageManager)
})
}
func TestReverbOptions_Struct(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
opts := ReverbOptions{Port: 8080}
assert.Equal(t, 8080, opts.Port)
})
}
func TestRedisOptions_Struct(t *testing.T) {
t.Run("all fields accessible", func(t *testing.T) {
opts := RedisOptions{
Port: 6379,
ConfigFile: "redis.conf",
}
assert.Equal(t, 6379, opts.Port)
assert.Equal(t, "redis.conf", opts.ConfigFile)
})
}
func TestBaseService_StopProcess_Good(t *testing.T) {
t.Run("returns nil when not running", func(t *testing.T) {
s := &baseService{running: false}
err := s.stopProcess()
assert.NoError(t, err)
})
t.Run("returns nil when cmd is nil", func(t *testing.T) {
s := &baseService{running: true, cmd: nil}
err := s.stopProcess()
assert.NoError(t, err)
})
}