385 lines
9.6 KiB
Go
385 lines
9.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
|
|
)
|
|
|
|
func createTestUser(t *testing.T, s *Store, id string) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
_, err := s.CreateUser(ctx, id, model.DefaultQuota())
|
|
if err != nil {
|
|
t.Fatalf("create test user %q: %v", id, err)
|
|
}
|
|
}
|
|
|
|
func newTestRunner(user, id string) *model.Runner {
|
|
return &model.Runner{
|
|
ID: id,
|
|
User: user,
|
|
RepoURL: "ilia/test-repo",
|
|
Branch: "main",
|
|
Tools: "go",
|
|
Task: "implement feature",
|
|
Status: model.RunnerStatusReceived,
|
|
CPUReq: "2",
|
|
MemReq: "4Gi",
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
}
|
|
|
|
func cleanRunners(t *testing.T) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if _, err := testPool.Exec(ctx, "DELETE FROM runners"); err != nil {
|
|
t.Fatalf("clean runners: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateRunner(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-abc123")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create runner: %v", err)
|
|
}
|
|
|
|
got, err := s.GetRunner(ctx, "runner-abc123")
|
|
if err != nil {
|
|
t.Fatalf("get runner: %v", err)
|
|
}
|
|
if got.ID != "runner-abc123" {
|
|
t.Errorf("got id %q, want %q", got.ID, "runner-abc123")
|
|
}
|
|
if got.User != "alice" {
|
|
t.Errorf("got user %q, want %q", got.User, "alice")
|
|
}
|
|
if got.Status != model.RunnerStatusReceived {
|
|
t.Errorf("got status %q, want %q", got.Status, model.RunnerStatusReceived)
|
|
}
|
|
if got.RepoURL != "ilia/test-repo" {
|
|
t.Errorf("got repo %q, want %q", got.RepoURL, "ilia/test-repo")
|
|
}
|
|
}
|
|
|
|
func TestCreateRunner_Duplicate(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-dup1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("first create: %v", err)
|
|
}
|
|
err := s.CreateRunner(ctx, r)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate runner")
|
|
}
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Errorf("got %v, want ErrDuplicate", err)
|
|
}
|
|
}
|
|
|
|
func TestGetRunner_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := s.GetRunner(ctx, "nonexistent")
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent runner")
|
|
}
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("got %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestListRunners(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
createTestUser(t, s, "bob")
|
|
ctx := context.Background()
|
|
|
|
r1 := newTestRunner("alice", "runner-list1")
|
|
r2 := newTestRunner("alice", "runner-list2")
|
|
r3 := newTestRunner("bob", "runner-list3")
|
|
for _, r := range []*model.Runner{r1, r2, r3} {
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create runner %s: %v", r.ID, err)
|
|
}
|
|
}
|
|
|
|
all, err := s.ListRunners(ctx, "", "")
|
|
if err != nil {
|
|
t.Fatalf("list all: %v", err)
|
|
}
|
|
if len(all) != 3 {
|
|
t.Fatalf("expected 3 runners, got %d", len(all))
|
|
}
|
|
|
|
aliceRunners, err := s.ListRunners(ctx, "alice", "")
|
|
if err != nil {
|
|
t.Fatalf("list alice: %v", err)
|
|
}
|
|
if len(aliceRunners) != 2 {
|
|
t.Fatalf("expected 2 runners for alice, got %d", len(aliceRunners))
|
|
}
|
|
|
|
byStatus, err := s.ListRunners(ctx, "", "received")
|
|
if err != nil {
|
|
t.Fatalf("list by status: %v", err)
|
|
}
|
|
if len(byStatus) != 3 {
|
|
t.Fatalf("expected 3 received runners, got %d", len(byStatus))
|
|
}
|
|
}
|
|
|
|
func TestUpdateRunnerStatus_ValidTransition(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-trans1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
if err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, ""); err != nil {
|
|
t.Fatalf("transition to pod_creating: %v", err)
|
|
}
|
|
|
|
got, _ := s.GetRunner(ctx, r.ID)
|
|
if got.Status != model.RunnerStatusPodCreating {
|
|
t.Errorf("expected pod_creating, got %s", got.Status)
|
|
}
|
|
|
|
if err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "forgejo-42"); err != nil {
|
|
t.Fatalf("transition to runner_registered: %v", err)
|
|
}
|
|
|
|
got, _ = s.GetRunner(ctx, r.ID)
|
|
if got.Status != model.RunnerStatusRunnerRegistered {
|
|
t.Errorf("expected runner_registered, got %s", got.Status)
|
|
}
|
|
if got.ForgejoRunnerID != "forgejo-42" {
|
|
t.Errorf("expected forgejo_runner_id 'forgejo-42', got %q", got.ForgejoRunnerID)
|
|
}
|
|
}
|
|
|
|
func TestUpdateRunnerStatus_InvalidTransition(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-invalid1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusCompleted, "")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid transition received -> completed")
|
|
}
|
|
}
|
|
|
|
func TestUpdateRunnerStatus_SetsClaimedAt(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-claimed1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, "")
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "")
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusJobClaimed, "")
|
|
|
|
got, _ := s.GetRunner(ctx, r.ID)
|
|
if got.ClaimedAt == nil {
|
|
t.Error("expected claimed_at to be set")
|
|
}
|
|
}
|
|
|
|
func TestUpdateRunnerStatus_SetsCompletedAt(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-complete1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, "")
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "")
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusJobClaimed, "")
|
|
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusCompleted, "")
|
|
|
|
got, _ := s.GetRunner(ctx, r.ID)
|
|
if got.CompletedAt == nil {
|
|
t.Error("expected completed_at to be set")
|
|
}
|
|
}
|
|
|
|
func TestDeleteRunner(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r := newTestRunner("alice", "runner-del1")
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
if err := s.DeleteRunner(ctx, r.ID); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
|
|
_, err := s.GetRunner(ctx, r.ID)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound after delete, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteRunner_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
ctx := context.Background()
|
|
|
|
err := s.DeleteRunner(ctx, "nonexistent")
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIsDeliveryProcessed(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
isDupe, err := s.IsDeliveryProcessed(ctx, "delivery-1")
|
|
if err != nil {
|
|
t.Fatalf("check delivery: %v", err)
|
|
}
|
|
if isDupe {
|
|
t.Error("expected delivery-1 to not be processed yet")
|
|
}
|
|
|
|
r := newTestRunner("alice", "runner-dedupe1")
|
|
r.WebhookDeliveryID = "delivery-1"
|
|
if err := s.CreateRunner(ctx, r); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
isDupe, err = s.IsDeliveryProcessed(ctx, "delivery-1")
|
|
if err != nil {
|
|
t.Fatalf("check delivery after create: %v", err)
|
|
}
|
|
if !isDupe {
|
|
t.Error("expected delivery-1 to be processed")
|
|
}
|
|
}
|
|
|
|
func TestIsDeliveryProcessed_EmptyID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
ctx := context.Background()
|
|
|
|
isDupe, err := s.IsDeliveryProcessed(ctx, "")
|
|
if err != nil {
|
|
t.Fatalf("check empty delivery: %v", err)
|
|
}
|
|
if isDupe {
|
|
t.Error("expected empty delivery ID to return false")
|
|
}
|
|
}
|
|
|
|
func TestWebhookDeliveryDedupe_UniqueConstraint(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
r1 := newTestRunner("alice", "runner-uniq1")
|
|
r1.WebhookDeliveryID = "webhook-abc"
|
|
if err := s.CreateRunner(ctx, r1); err != nil {
|
|
t.Fatalf("first insert: %v", err)
|
|
}
|
|
|
|
r2 := newTestRunner("alice", "runner-uniq2")
|
|
r2.WebhookDeliveryID = "webhook-abc"
|
|
err := s.CreateRunner(ctx, r2)
|
|
if err == nil {
|
|
t.Fatal("expected duplicate error for same webhook_delivery_id")
|
|
}
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Errorf("expected ErrDuplicate, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetStaleRunners(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cleanRunners(t)
|
|
createTestUser(t, s, "alice")
|
|
ctx := context.Background()
|
|
|
|
old := newTestRunner("alice", "runner-stale1")
|
|
old.CreatedAt = time.Now().UTC().Add(-3 * time.Hour)
|
|
if err := s.CreateRunner(ctx, old); err != nil {
|
|
t.Fatalf("create old runner: %v", err)
|
|
}
|
|
|
|
fresh := newTestRunner("alice", "runner-fresh1")
|
|
fresh.CreatedAt = time.Now().UTC()
|
|
if err := s.CreateRunner(ctx, fresh); err != nil {
|
|
t.Fatalf("create fresh runner: %v", err)
|
|
}
|
|
|
|
stale, err := s.GetStaleRunners(ctx, 2*time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("get stale: %v", err)
|
|
}
|
|
if len(stale) != 1 {
|
|
t.Fatalf("expected 1 stale runner, got %d", len(stale))
|
|
}
|
|
if stale[0].ID != "runner-stale1" {
|
|
t.Errorf("expected stale runner runner-stale1, got %s", stale[0].ID)
|
|
}
|
|
}
|
|
|
|
func TestGenerateRunnerID(t *testing.T) {
|
|
id1, err := GenerateRunnerID()
|
|
if err != nil {
|
|
t.Fatalf("generate: %v", err)
|
|
}
|
|
if len(id1) == 0 {
|
|
t.Error("expected non-empty ID")
|
|
}
|
|
|
|
id2, _ := GenerateRunnerID()
|
|
if id1 == id2 {
|
|
t.Error("expected unique IDs")
|
|
}
|
|
|
|
if id1[:7] != "runner-" {
|
|
t.Errorf("expected runner- prefix, got %s", id1[:7])
|
|
}
|
|
}
|