rdev/internal/port/port_test.go
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

381 lines
9.7 KiB
Go

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"])
}
}