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>
271 lines
6.2 KiB
Go
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
|
|
}
|