rdev/internal/handlers/infrastructure_mocks_test.go
jordan 8282d60c69 feat: implement composable monorepo template system with component architecture
Adds the composable monorepo template system that generates project skeletons
with pluggable components (service, worker, app-react, app-astro, cli).

Key changes:
- Monorepo skeleton templates with shared pkg/, scripts/, and git hooks
- Component templates (service, worker, app-react, app-astro, cli) with
  Dockerfiles, CI steps, and component.yaml manifests
- Component domain model with validation and dependency resolution
- Component handler endpoints for CRUD and composition
- Template provider extended with BuildComposableProject and component assembly
- Deployer extended with composable project deployment support
- Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning)
- envutil package for centralized env var reads with defaults
- api.DecodeJSON helper for standardized request body decoding
- Standardized response helpers (WriteBadRequest, WriteNotFound, etc.)
- Replaced fullstack-app cookbook with composable-app cookbook
- Hardened handler timeouts, logging, and error responses across all handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:11:42 -07:00

290 lines
7.1 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
}