rdev/internal/handlers/infrastructure_mocks_test.go
jordan 1790afd0ee feat: add path-based ingress management for component lifecycle
Adds AddIngressPath and RemoveIngressPath to the Deployer interface
for managing per-component ingress rules in monorepo projects.

- Implement conflict retry logic for concurrent ingress updates
- Add K8s client interface for testability
- Add comprehensive unit tests for ingress path operations
- Add component deployment and teardown methods to ComponentService
- Update service templates with OpenAPI spec improvements
- Add evolving-app cookbook tree for reference
- Split resources.go into resources_ingress.go for path-based routing
- Split component.go into component_deploy.go for deployment helpers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:31:50 -07:00

298 lines
7.3 KiB
Go

package handlers
import (
"context"
"fmt"
"time"
"github.com/orchard9/rdev/internal/domain"
)
// 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
}
// 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) 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
}