rdev/internal/handlers/infrastructure_mocks_test.go
jordan 9226454b85
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: label-based undeploy, GC reconciliation, checkout/sessions, pool status
- Add UndeployAll() using label selectors to clean up monorepo components
  on project deletion (replaces name-based Undeploy in DeleteProject and
  the direct undeploy handler)
- Add ResourceGC background worker that periodically finds K8s resources
  whose project label has no matching DB record, deletes after 1h safety
  window
- Widen deployer client type from *kubernetes.Clientset to
  kubernetes.Interface for testability
- UndeployAll accumulates errors via errors.Join instead of failing fast
- Add checkout/checkin sidecar dev flow: temporary git tokens, branch
  checkout, review on checkin with cleanup workers
- Add interactive sessions: pod binding, command execution, SSE streaming,
  ephemeral preview URLs with session cleanup workers
- Add GET /workers/pool endpoint for aggregate capacity and queue depth
- Add sessions:read and sessions:execute auth scopes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:11:28 -07:00

370 lines
9.1 KiB
Go

package handlers
import (
"context"
"fmt"
"time"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
// mockGitRepository implements port.GitRepository for testing.
type mockGitRepository struct {
repos map[string]*domain.Repo
err error
}
func newMockGitRepository() *mockGitRepository {
return &mockGitRepository{repos: make(map[string]*domain.Repo)}
}
func (m *mockGitRepository) CreateRepo(_ context.Context, name, description string, private bool) (*domain.Repo, error) {
if m.err != nil {
return nil, m.err
}
repo := &domain.Repo{
ID: 1,
Owner: "threesix",
Name: name,
FullName: "threesix/" + name,
Description: description,
Private: private,
CloneSSH: fmt.Sprintf("git@git.threesix.ai:threesix/%s.git", name),
CloneHTTP: fmt.Sprintf("https://git.threesix.ai/threesix/%s.git", name),
HTMLURL: fmt.Sprintf("https://git.threesix.ai/threesix/%s", name),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
m.repos[name] = repo
return repo, nil
}
func (m *mockGitRepository) DeleteRepo(_ context.Context, _, name string) error {
if m.err != nil {
return m.err
}
delete(m.repos, name)
return nil
}
func (m *mockGitRepository) ListRepos(_ context.Context, _ string) ([]*domain.Repo, error) {
if m.err != nil {
return nil, m.err
}
var repos []*domain.Repo
for _, r := range m.repos {
repos = append(repos, r)
}
return repos, nil
}
func (m *mockGitRepository) GetRepo(_ context.Context, _, name string) (*domain.Repo, error) {
if m.err != nil {
return nil, m.err
}
r, ok := m.repos[name]
if !ok {
return nil, fmt.Errorf("repo not found: %s", name)
}
return r, nil
}
func (m *mockGitRepository) AddCollaborator(context.Context, string, string, string, string) error {
return m.err
}
func (m *mockGitRepository) RemoveCollaborator(context.Context, string, string, string) error {
return m.err
}
func (m *mockGitRepository) AddDeployKey(context.Context, string, string, string, string, bool) (*domain.DeployKey, error) {
return nil, m.err
}
func (m *mockGitRepository) DeleteDeployKey(context.Context, string, string, int64) error {
return m.err
}
func (m *mockGitRepository) CreateWebhook(context.Context, string, string, string, string, []string) (*domain.RepoWebhook, error) {
return nil, m.err
}
func (m *mockGitRepository) DeleteWebhook(context.Context, string, string, int64) error {
return m.err
}
func (m *mockGitRepository) ListBranches(_ context.Context, _, _ string) ([]*domain.GitBranch, error) {
if m.err != nil {
return nil, m.err
}
return []*domain.GitBranch{
{Name: "main", CommitSHA: "abc123", Protected: true},
{Name: "develop", CommitSHA: "def456", Protected: false},
}, nil
}
func (m *mockGitRepository) CreateBranch(_ context.Context, _, _, branchName, _ string) (*domain.GitBranch, error) {
if m.err != nil {
return nil, m.err
}
return &domain.GitBranch{
Name: branchName,
CommitSHA: "newcommit123",
Protected: false,
}, nil
}
func (m *mockGitRepository) CreateAccessToken(_ context.Context, name string, _ []string, _ *time.Time) (*domain.GitAccessToken, error) {
if m.err != nil {
return nil, m.err
}
return &domain.GitAccessToken{
ID: 12345,
Name: name,
Token: "test-token-12345",
Scopes: []string{"write:repository"},
}, nil
}
func (m *mockGitRepository) DeleteAccessToken(_ context.Context, _ int64) error {
return m.err
}
// mockDNSProvider implements port.DNSProvider for testing.
type mockDNSProvider struct {
records map[string]*domain.DNSRecord
err error
}
func newMockDNSProvider() *mockDNSProvider {
return &mockDNSProvider{records: make(map[string]*domain.DNSRecord)}
}
func (m *mockDNSProvider) CreateRecord(_ context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
record.ID = "rec-" + record.Name
m.records[record.Name] = &record
return &record, nil
}
func (m *mockDNSProvider) UpdateRecord(_ context.Context, recordID string, record domain.DNSRecord) (*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
record.ID = recordID
m.records[recordID] = &record
return &record, nil
}
func (m *mockDNSProvider) DeleteRecord(_ context.Context, recordID string) error {
if m.err != nil {
return m.err
}
delete(m.records, recordID)
return nil
}
func (m *mockDNSProvider) DeleteRecordByName(_ context.Context, _, name string) error {
if m.err != nil {
return m.err
}
delete(m.records, name)
return nil
}
func (m *mockDNSProvider) GetRecord(_ context.Context, recordID string) (*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
r, ok := m.records[recordID]
if !ok {
return nil, fmt.Errorf("record not found")
}
return r, nil
}
func (m *mockDNSProvider) ListRecords(_ context.Context, recordType string) ([]*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
var result []*domain.DNSRecord
for _, r := range m.records {
if recordType == "" || r.Type == recordType {
result = append(result, r)
}
}
return result, nil
}
func (m *mockDNSProvider) FindRecord(_ context.Context, _, name string) (*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
r, ok := m.records[name]
if !ok {
return nil, nil
}
return r, nil
}
func (m *mockDNSProvider) UpsertRecord(ctx context.Context, record domain.DNSRecord) (*domain.DNSRecord, error) {
if m.err != nil {
return nil, m.err
}
// Check if record exists, then update or create
existing, _ := m.FindRecord(ctx, record.Type, record.Name)
if existing != nil {
return m.UpdateRecord(ctx, existing.ID, record)
}
return m.CreateRecord(ctx, record)
}
// mockDeployer implements port.Deployer for testing.
type mockDeployer struct {
deployments map[string]*domain.DeployStatus
logs string
err error
}
func newMockDeployer() *mockDeployer {
return &mockDeployer{deployments: make(map[string]*domain.DeployStatus)}
}
func (m *mockDeployer) Deploy(_ context.Context, spec domain.DeploySpec) error {
if m.err != nil {
return m.err
}
m.deployments[spec.ProjectName] = &domain.DeployStatus{
ProjectName: spec.ProjectName,
Image: spec.Image,
Replicas: spec.Replicas,
ReadyReplicas: 0,
URL: "https://" + spec.Domain,
Status: domain.DeploymentStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return nil
}
func (m *mockDeployer) Undeploy(_ context.Context, projectName string) error {
if m.err != nil {
return m.err
}
delete(m.deployments, projectName)
return nil
}
func (m *mockDeployer) UndeployAll(_ context.Context, projectName string) error {
if m.err != nil {
return m.err
}
delete(m.deployments, projectName)
return nil
}
func (m *mockDeployer) GetStatus(_ context.Context, projectName string) (*domain.DeployStatus, error) {
if m.err != nil {
return nil, m.err
}
s, ok := m.deployments[projectName]
if !ok {
return nil, nil
}
return s, nil
}
func (m *mockDeployer) Restart(_ context.Context, _ string) error {
return m.err
}
func (m *mockDeployer) Scale(_ context.Context, projectName string, replicas int) error {
if m.err != nil {
return m.err
}
if s, ok := m.deployments[projectName]; ok {
s.Replicas = replicas
}
return nil
}
func (m *mockDeployer) GetLogs(_ context.Context, _ string, _ int) (string, error) {
if m.err != nil {
return "", m.err
}
return m.logs, nil
}
func (m *mockDeployer) AddIngressHost(_ context.Context, _, _ string) error {
return m.err
}
func (m *mockDeployer) RemoveIngressHost(_ context.Context, _, _ string) error {
return m.err
}
func (m *mockDeployer) UndeployComponent(_ context.Context, _, _ string) error {
return m.err
}
func (m *mockDeployer) GetComponentStatus(_ context.Context, _, _ string) (*domain.DeployStatus, error) {
if m.err != nil {
return nil, m.err
}
return nil, nil
}
func (m *mockDeployer) ListComponentStatuses(_ context.Context, _ string) (*domain.ProjectDeployStatus, error) {
if m.err != nil {
return nil, m.err
}
return &domain.ProjectDeployStatus{}, nil
}
func (m *mockDeployer) RestartComponent(_ context.Context, _, _ string) error {
return m.err
}
func (m *mockDeployer) ScaleComponent(_ context.Context, _, _ string, _ int) error {
return m.err
}
func (m *mockDeployer) GetComponentLogs(_ context.Context, _, _ string, _ int) (string, error) {
if m.err != nil {
return "", m.err
}
return m.logs, nil
}
func (m *mockDeployer) AddIngressPath(_ context.Context, _, _, _, _ string, _ int) error {
return m.err
}
func (m *mockDeployer) RemoveIngressPath(_ context.Context, _, _, _ string) error {
return m.err
}
// mockPreviewManager implements port.PreviewManager for testing.
type mockPreviewManager struct {
previews map[string]bool
err error
}
func newMockPreviewManager() *mockPreviewManager {
return &mockPreviewManager{previews: make(map[string]bool)}
}
func (m *mockPreviewManager) CreatePreview(_ context.Context, opts port.PreviewOptions) error {
if m.err != nil {
return m.err
}
m.previews[opts.SessionID] = true
return nil
}
func (m *mockPreviewManager) DeletePreview(_ context.Context, sessionID string) error {
if m.err != nil {
return m.err
}
delete(m.previews, sessionID)
return nil
}