All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add POST /sessions/:id/exec endpoint for executing commands in sessions - Add session activity tracking (last_activity_at timestamp) - Add database migration 024 for session activity column - Add comprehensive tests for session handlers and service layer - Add wildcard TLS certificate for preview.threesix.ai subdomain - Add infrastructure mocks for testing preview service - Refactor preview cleanup logic to remove unused methods - Add AIOS core documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
438 lines
11 KiB
Go
438 lines
11 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
|
|
}
|
|
|
|
// mockExecutor implements port.CommandExecutor for testing.
|
|
type mockExecutor struct {
|
|
result *domain.CommandResult
|
|
err error
|
|
}
|
|
|
|
func newMockExecutor() *mockExecutor {
|
|
return &mockExecutor{
|
|
result: &domain.CommandResult{ExitCode: 0, DurationMs: 100},
|
|
}
|
|
}
|
|
|
|
func (m *mockExecutor) Execute(_ context.Context, _ *domain.Command, _ string, handler domain.OutputHandler) (*domain.CommandResult, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
if handler != nil {
|
|
handler(domain.OutputLine{Stream: "stdout", Line: "mock output", Timestamp: time.Now()})
|
|
}
|
|
return m.result, nil
|
|
}
|
|
|
|
func (m *mockExecutor) Cancel(_ context.Context, _ domain.CommandID) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockExecutor) PodExists(_ context.Context, _ string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (m *mockExecutor) CheckConnection(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// mockStreamPublisher implements port.StreamPublisher for testing.
|
|
type mockStreamPublisher struct {
|
|
events map[string][]port.StreamEvent
|
|
seq int64
|
|
}
|
|
|
|
func newMockStreamPublisher() *mockStreamPublisher {
|
|
return &mockStreamPublisher{events: make(map[string][]port.StreamEvent)}
|
|
}
|
|
|
|
func (m *mockStreamPublisher) Subscribe(streamID string) (<-chan port.StreamEvent, func()) {
|
|
ch := make(chan port.StreamEvent, 100)
|
|
return ch, func() { close(ch) }
|
|
}
|
|
|
|
func (m *mockStreamPublisher) SubscribeFromID(streamID string, _ string) (<-chan port.StreamEvent, func()) {
|
|
return m.Subscribe(streamID)
|
|
}
|
|
|
|
func (m *mockStreamPublisher) Publish(streamID string, event port.StreamEvent) string {
|
|
m.seq++
|
|
event.ID = fmt.Sprintf("%s:%d", streamID, m.seq)
|
|
m.events[streamID] = append(m.events[streamID], event)
|
|
return event.ID
|
|
}
|
|
|
|
func (m *mockStreamPublisher) Close(streamID string) {
|
|
delete(m.events, streamID)
|
|
}
|
|
|
|
// Verify mock interfaces at compile time.
|
|
var _ port.CommandExecutor = (*mockExecutor)(nil)
|
|
var _ port.StreamPublisher = (*mockStreamPublisher)(nil)
|