rdev/internal/adapter/memory/project_repository_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

271 lines
6.2 KiB
Go

package memory
import (
"context"
"sync"
"testing"
"github.com/orchard9/rdev/internal/domain"
)
func TestProjectRepository_RegisterAndGet(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
project := &domain.Project{
ID: "test-project",
Name: "Test Project",
Description: "A test project",
PodName: "test-pod-0",
Workspace: "/workspace",
Status: domain.ProjectStatusRunning,
}
// Register
if err := repo.Register(ctx, project); err != nil {
t.Fatalf("Register() error = %v", err)
}
// Get
retrieved, err := repo.Get(ctx, "test-project")
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if retrieved.ID != project.ID {
t.Errorf("ID = %q, want %q", retrieved.ID, project.ID)
}
if retrieved.Name != project.Name {
t.Errorf("Name = %q, want %q", retrieved.Name, project.Name)
}
if retrieved.Status != project.Status {
t.Errorf("Status = %q, want %q", retrieved.Status, project.Status)
}
}
func TestProjectRepository_GetNotFound(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
_, err := repo.Get(ctx, "nonexistent")
if err != domain.ErrProjectNotFound {
t.Errorf("Get() error = %v, want %v", err, domain.ErrProjectNotFound)
}
}
func TestProjectRepository_List(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
// Empty list initially
projects, err := repo.List(ctx)
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(projects) != 0 {
t.Errorf("Initial List() length = %d, want 0", len(projects))
}
// Register some projects
for i := 0; i < 3; i++ {
p := &domain.Project{
ID: domain.ProjectID("project-" + string(rune('a'+i))),
Name: "Project " + string(rune('A'+i)),
}
_ = repo.Register(ctx, p)
}
// List should return all
projects, err = repo.List(ctx)
if err != nil {
t.Fatalf("List() error = %v", err)
}
if len(projects) != 3 {
t.Errorf("List() length = %d, want 3", len(projects))
}
}
func TestProjectRepository_Exists(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
project := &domain.Project{
ID: "existing-project",
Name: "Existing",
}
_ = repo.Register(ctx, project)
tests := []struct {
id domain.ProjectID
want bool
}{
{"existing-project", true},
{"nonexistent", false},
}
for _, tt := range tests {
exists, err := repo.Exists(ctx, tt.id)
if err != nil {
t.Errorf("Exists(%q) error = %v", tt.id, err)
}
if exists != tt.want {
t.Errorf("Exists(%q) = %v, want %v", tt.id, exists, tt.want)
}
}
}
func TestProjectRepository_Unregister(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
project := &domain.Project{
ID: "to-remove",
Name: "To Remove",
}
repo.Register(ctx, project)
// Verify it exists
exists, _ := repo.Exists(ctx, "to-remove")
if !exists {
t.Fatal("Project should exist after register")
}
// Unregister
if err := repo.Unregister(ctx, "to-remove"); err != nil {
t.Fatalf("Unregister() error = %v", err)
}
// Verify it's gone
exists, _ = repo.Exists(ctx, "to-remove")
if exists {
t.Error("Project should not exist after unregister")
}
}
func TestProjectRepository_UnregisterNonexistent(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
// Unregistering non-existent project should not error
if err := repo.Unregister(ctx, "nonexistent"); err != nil {
t.Errorf("Unregister(nonexistent) error = %v, want nil", err)
}
}
func TestProjectRepository_SetStatus(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
project := &domain.Project{
ID: "status-test",
Name: "Status Test",
Status: domain.ProjectStatusPending,
}
repo.Register(ctx, project)
// Change status
repo.SetStatus("status-test", domain.ProjectStatusRunning)
// Verify
retrieved, _ := repo.Get(ctx, "status-test")
if retrieved.Status != domain.ProjectStatusRunning {
t.Errorf("Status = %q, want %q", retrieved.Status, domain.ProjectStatusRunning)
}
// Change to error
repo.SetStatus("status-test", domain.ProjectStatusError)
retrieved, _ = repo.Get(ctx, "status-test")
if retrieved.Status != domain.ProjectStatusError {
t.Errorf("Status = %q, want %q", retrieved.Status, domain.ProjectStatusError)
}
}
func TestProjectRepository_SetStatusNonexistent(t *testing.T) {
repo := NewProjectRepository()
// Should not panic on nonexistent project
repo.SetStatus("nonexistent", domain.ProjectStatusRunning)
// No error expected, just a no-op
}
func TestProjectRepository_RefreshStatus(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
// RefreshStatus is a no-op for memory implementation
if err := repo.RefreshStatus(ctx); err != nil {
t.Errorf("RefreshStatus() error = %v, want nil", err)
}
}
func TestProjectRepository_RegisterOverwrite(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
// Register initial
p1 := &domain.Project{
ID: "overwrite-test",
Name: "Original",
Status: domain.ProjectStatusPending,
}
repo.Register(ctx, p1)
// Register with same ID, different data
p2 := &domain.Project{
ID: "overwrite-test",
Name: "Updated",
Status: domain.ProjectStatusRunning,
}
repo.Register(ctx, p2)
// Should have updated data
retrieved, _ := repo.Get(ctx, "overwrite-test")
if retrieved.Name != "Updated" {
t.Errorf("Name = %q, want %q", retrieved.Name, "Updated")
}
if retrieved.Status != domain.ProjectStatusRunning {
t.Errorf("Status = %q, want %q", retrieved.Status, domain.ProjectStatusRunning)
}
}
func TestProjectRepository_ConcurrentAccess(t *testing.T) {
repo := NewProjectRepository()
ctx := context.Background()
var wg sync.WaitGroup
// Concurrent register
for i := 0; i < 50; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
p := &domain.Project{
ID: domain.ProjectID(string(rune('a' + id%26))),
Name: "Project",
}
repo.Register(ctx, p)
}(i)
}
// Concurrent read
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
repo.List(ctx)
}()
}
// Concurrent exists
for i := 0; i < 50; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
_, _ = repo.Exists(ctx, domain.ProjectID(string(rune('a'+id%26))))
}(i)
}
wg.Wait()
// Test passes if no race/deadlock
}