package port_test import ( "context" "testing" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // ============================================================================= // Interface Compliance Tests // // These tests verify that mock implementations can satisfy the port interfaces. // They serve as compile-time verification that interfaces are correctly defined // and provide example implementations for testing. // ============================================================================= // Compile-time interface compliance checks var ( _ port.ProjectRepository = (*mockProjectRepository)(nil) _ port.CommandExecutor = (*mockCommandExecutor)(nil) _ port.APIKeyRepository = (*mockAPIKeyRepository)(nil) _ port.StreamPublisher = (*mockStreamPublisher)(nil) ) // ============================================================================= // Mock Implementations // ============================================================================= type mockProjectRepository struct { projects map[domain.ProjectID]*domain.Project } func newMockProjectRepository() *mockProjectRepository { return &mockProjectRepository{ projects: make(map[domain.ProjectID]*domain.Project), } } func (m *mockProjectRepository) List(ctx context.Context) ([]domain.Project, error) { result := make([]domain.Project, 0, len(m.projects)) for _, p := range m.projects { result = append(result, *p) } return result, nil } func (m *mockProjectRepository) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) { if p, ok := m.projects[id]; ok { return p, nil } return nil, domain.ErrProjectNotFound } func (m *mockProjectRepository) Exists(ctx context.Context, id domain.ProjectID) (bool, error) { _, ok := m.projects[id] return ok, nil } func (m *mockProjectRepository) Register(ctx context.Context, project *domain.Project) error { m.projects[project.ID] = project return nil } func (m *mockProjectRepository) Unregister(ctx context.Context, id domain.ProjectID) error { delete(m.projects, id) return nil } func (m *mockProjectRepository) RefreshStatus(ctx context.Context) error { return nil } type mockCommandExecutor struct { executeFunc func(ctx context.Context, cmd *domain.Command, podName string, handler domain.OutputHandler) (*domain.CommandResult, error) } func (m *mockCommandExecutor) Execute(ctx context.Context, cmd *domain.Command, podName string, handler domain.OutputHandler) (*domain.CommandResult, error) { if m.executeFunc != nil { return m.executeFunc(ctx, cmd, podName, handler) } return &domain.CommandResult{ CommandID: cmd.ID, ExitCode: 0, DurationMs: 100, }, nil } func (m *mockCommandExecutor) Cancel(ctx context.Context, cmdID domain.CommandID) error { return nil } func (m *mockCommandExecutor) PodExists(ctx context.Context, podName string) (bool, error) { return true, nil } func (m *mockCommandExecutor) CheckConnection(ctx context.Context) error { return nil } type mockAPIKeyRepository struct { keys map[domain.APIKeyID]*domain.APIKey } func newMockAPIKeyRepository() *mockAPIKeyRepository { return &mockAPIKeyRepository{ keys: make(map[domain.APIKeyID]*domain.APIKey), } } func (m *mockAPIKeyRepository) Create(ctx context.Context, key *domain.APIKey, keyHash string) error { m.keys[key.ID] = key return nil } func (m *mockAPIKeyRepository) GetByHash(ctx context.Context, keyHash string) (*domain.APIKey, error) { // In a real implementation, this would look up by hash return nil, domain.ErrKeyNotFound } func (m *mockAPIKeyRepository) Get(ctx context.Context, id domain.APIKeyID) (*domain.APIKey, error) { if k, ok := m.keys[id]; ok { return k, nil } return nil, domain.ErrKeyNotFound } func (m *mockAPIKeyRepository) List(ctx context.Context) ([]*domain.APIKey, error) { result := make([]*domain.APIKey, 0, len(m.keys)) for _, k := range m.keys { result = append(result, k) } return result, nil } func (m *mockAPIKeyRepository) Revoke(ctx context.Context, id domain.APIKeyID) error { if _, ok := m.keys[id]; !ok { return domain.ErrKeyNotFound } return nil } func (m *mockAPIKeyRepository) UpdateLastUsed(ctx context.Context, id domain.APIKeyID) error { if _, ok := m.keys[id]; !ok { return domain.ErrKeyNotFound } return nil } type mockStreamPublisher struct { subscribers map[string][]chan port.StreamEvent } func newMockStreamPublisher() *mockStreamPublisher { return &mockStreamPublisher{ subscribers: make(map[string][]chan port.StreamEvent), } } func (m *mockStreamPublisher) Subscribe(streamID string) (<-chan port.StreamEvent, func()) { ch := make(chan port.StreamEvent, 10) m.subscribers[streamID] = append(m.subscribers[streamID], ch) cleanup := func() { close(ch) } return ch, cleanup } func (m *mockStreamPublisher) SubscribeFromID(streamID string, lastEventID string) (<-chan port.StreamEvent, func()) { // Simplified: just subscribe without replay return m.Subscribe(streamID) } func (m *mockStreamPublisher) Publish(streamID string, event port.StreamEvent) string { for _, ch := range m.subscribers[streamID] { select { case ch <- event: default: // Channel full, skip } } return event.ID } func (m *mockStreamPublisher) Close(streamID string) { for _, ch := range m.subscribers[streamID] { close(ch) } delete(m.subscribers, streamID) } // ============================================================================= // Mock Usage Tests // ============================================================================= func TestMockProjectRepository_BasicOperations(t *testing.T) { repo := newMockProjectRepository() ctx := context.Background() // Register a project project := &domain.Project{ ID: "test-proj", Name: "Test Project", Status: domain.ProjectStatusRunning, } if err := repo.Register(ctx, project); err != nil { t.Fatalf("Register failed: %v", err) } // Verify it exists exists, err := repo.Exists(ctx, "test-proj") if err != nil { t.Fatalf("Exists failed: %v", err) } if !exists { t.Error("project should exist after registration") } // Get the project got, err := repo.Get(ctx, "test-proj") if err != nil { t.Fatalf("Get failed: %v", err) } if got.Name != "Test Project" { t.Errorf("Got name %q, want %q", got.Name, "Test Project") } // List projects list, err := repo.List(ctx) if err != nil { t.Fatalf("List failed: %v", err) } if len(list) != 1 { t.Errorf("List returned %d projects, want 1", len(list)) } // Unregister if err := repo.Unregister(ctx, "test-proj"); err != nil { t.Fatalf("Unregister failed: %v", err) } // Verify not found _, err = repo.Get(ctx, "test-proj") if err != domain.ErrProjectNotFound { t.Errorf("Get after unregister: got error %v, want %v", err, domain.ErrProjectNotFound) } } func TestMockCommandExecutor_Execute(t *testing.T) { executor := &mockCommandExecutor{} ctx := context.Background() cmd := &domain.Command{ ID: "cmd-1", ProjectID: "proj-1", Type: domain.CommandTypeShell, Args: []string{"echo", "hello"}, } var outputLines []domain.OutputLine handler := func(line domain.OutputLine) { outputLines = append(outputLines, line) } result, err := executor.Execute(ctx, cmd, "test-pod", handler) if err != nil { t.Fatalf("Execute failed: %v", err) } if result.CommandID != "cmd-1" { t.Errorf("CommandID = %q, want %q", result.CommandID, "cmd-1") } if result.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", result.ExitCode) } } func TestMockAPIKeyRepository_CRUD(t *testing.T) { repo := newMockAPIKeyRepository() ctx := context.Background() key := &domain.APIKey{ ID: "key-1", Name: "Test Key", Scopes: []domain.Scope{domain.ScopeProjectsRead}, } // Create if err := repo.Create(ctx, key, "hash123"); err != nil { t.Fatalf("Create failed: %v", err) } // Get got, err := repo.Get(ctx, "key-1") if err != nil { t.Fatalf("Get failed: %v", err) } if got.Name != "Test Key" { t.Errorf("Name = %q, want %q", got.Name, "Test Key") } // List list, err := repo.List(ctx) if err != nil { t.Fatalf("List failed: %v", err) } if len(list) != 1 { t.Errorf("List returned %d keys, want 1", len(list)) } // Revoke if err := repo.Revoke(ctx, "key-1"); err != nil { t.Fatalf("Revoke failed: %v", err) } // UpdateLastUsed if err := repo.UpdateLastUsed(ctx, "key-1"); err != nil { t.Fatalf("UpdateLastUsed failed: %v", err) } } func TestMockStreamPublisher_PubSub(t *testing.T) { pub := newMockStreamPublisher() // Subscribe ch, cleanup := pub.Subscribe("stream-1") defer cleanup() // Publish event := port.StreamEvent{ ID: "evt-1", Type: "output", Data: map[string]any{"line": "hello"}, } eventID := pub.Publish("stream-1", event) if eventID != "evt-1" { t.Errorf("Publish returned ID %q, want %q", eventID, "evt-1") } // Receive select { case received := <-ch: if received.ID != "evt-1" { t.Errorf("Received event ID %q, want %q", received.ID, "evt-1") } if received.Data["line"] != "hello" { t.Errorf("Received data = %v, want line=hello", received.Data) } default: t.Error("expected to receive event") } } // ============================================================================= // StreamEvent Tests // ============================================================================= func TestStreamEvent_CanBeInstantiated(t *testing.T) { event := port.StreamEvent{ ID: "event-123", Type: "command_output", Data: map[string]any{ "stream": "stdout", "line": "test output", }, } if event.ID != "event-123" { t.Errorf("StreamEvent.ID = %q, want %q", event.ID, "event-123") } if event.Type != "command_output" { t.Errorf("StreamEvent.Type = %q, want %q", event.Type, "command_output") } if event.Data["stream"] != "stdout" { t.Errorf("StreamEvent.Data[stream] = %v, want stdout", event.Data["stream"]) } }