All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add POST /sessions/:id/exec endpoint for executing commands in sessions - Add session activity tracking (last_activity_at timestamp) - Add database migration 024 for session activity column - Add comprehensive tests for session handlers and service layer - Add wildcard TLS certificate for preview.threesix.ai subdomain - Add infrastructure mocks for testing preview service - Refactor preview cleanup logic to remove unused methods - Add AIOS core documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
762 lines
22 KiB
Go
762 lines
22 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/service"
|
|
)
|
|
|
|
func setupCheckoutTest() (*CheckoutHandler, *mockCheckoutRepository, *mockProjectRepo) {
|
|
checkoutRepo := newMockCheckoutRepository()
|
|
projectRepo := newMockProjectRepo()
|
|
gitRepo := newMockGitRepository()
|
|
|
|
projectRepo.projects["test-project"] = &domain.Project{
|
|
ID: "test-project",
|
|
Name: "test-project",
|
|
PodName: "test-project-0",
|
|
Status: domain.ProjectStatusRunning,
|
|
}
|
|
|
|
checkoutService := service.NewCheckoutService(
|
|
checkoutRepo, gitRepo, projectRepo,
|
|
service.CheckoutServiceConfig{
|
|
GiteaURL: "https://git.threesix.ai",
|
|
DefaultOwner: "threesix",
|
|
DefaultExpiry: 24 * time.Hour,
|
|
},
|
|
)
|
|
|
|
handler := NewCheckoutHandler(checkoutService)
|
|
return handler, checkoutRepo, projectRepo
|
|
}
|
|
|
|
func TestCheckoutHandler_ListBranches(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
t.Run("list_branches_success", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/checkout/branches", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
branches, ok := data["branches"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected branches array, got %T", data["branches"])
|
|
}
|
|
|
|
if len(branches) != 2 {
|
|
t.Fatalf("got %d branches, want 2", len(branches))
|
|
}
|
|
|
|
names := make(map[string]bool)
|
|
for _, b := range branches {
|
|
bm := b.(map[string]any)
|
|
names[bm["name"].(string)] = true
|
|
}
|
|
if !names["main"] {
|
|
t.Error("expected 'main' branch in response")
|
|
}
|
|
if !names["develop"] {
|
|
t.Error("expected 'develop' branch in response")
|
|
}
|
|
})
|
|
|
|
t.Run("list_branches_project_not_found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/nonexistent/checkout/branches", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckoutHandler_Create(t *testing.T) {
|
|
t.Run("create_existing_branch", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"branch": "develop"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["id"] == nil || data["id"] == "" {
|
|
t.Error("expected non-empty id")
|
|
}
|
|
if data["clone_url"] == nil || data["clone_url"] == "" {
|
|
t.Error("expected non-empty clone_url")
|
|
}
|
|
if data["instructions"] == nil || data["instructions"] == "" {
|
|
t.Error("expected non-empty instructions")
|
|
}
|
|
if data["branch"] != "develop" {
|
|
t.Errorf("expected branch=develop, got %v", data["branch"])
|
|
}
|
|
if data["project_id"] != "test-project" {
|
|
t.Errorf("expected project_id=test-project, got %v", data["project_id"])
|
|
}
|
|
if data["status"] != "active" {
|
|
t.Errorf("expected status=active, got %v", data["status"])
|
|
}
|
|
})
|
|
|
|
t.Run("create_new_branch", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"new_branch": "feature-test"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["branch"] != "feature-test" {
|
|
t.Errorf("expected branch=feature-test, got %v", data["branch"])
|
|
}
|
|
})
|
|
|
|
t.Run("create_branch_conflict", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"branch": "develop", "new_branch": "feature-test"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("create_no_branch", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("create_protected_branch", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"branch": "main"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("create_project_not_found", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"branch": "develop"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/nonexistent/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("create_expiry_7d", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"branch": "develop", "expires_in": "7d"}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
// Verify expiry is approximately 7 days from now.
|
|
expiresAtStr, ok := data["expires_at"].(string)
|
|
if !ok || expiresAtStr == "" {
|
|
t.Fatal("expected non-empty expires_at")
|
|
}
|
|
|
|
expiresAt, err := time.Parse(time.RFC3339, expiresAtStr)
|
|
if err != nil {
|
|
t.Fatalf("parse expires_at: %v", err)
|
|
}
|
|
|
|
expectedExpiry := time.Now().Add(7 * 24 * time.Hour)
|
|
diff := expiresAt.Sub(expectedExpiry)
|
|
if diff < -time.Minute || diff > time.Minute {
|
|
t.Errorf("expires_at off by %v, expected ~7 days from now", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckoutHandler_List(t *testing.T) {
|
|
t.Run("list_empty", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/checkout", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
checkouts, ok := data["checkouts"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected checkouts array, got %T", data["checkouts"])
|
|
}
|
|
|
|
if len(checkouts) != 0 {
|
|
t.Errorf("got %d checkouts, want 0", len(checkouts))
|
|
}
|
|
})
|
|
|
|
t.Run("list_with_results", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
// Seed a checkout for the test project.
|
|
checkoutRepo.checkouts["checkout-list-1"] = &domain.Checkout{
|
|
ID: "checkout-list-1",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/checkout", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
checkouts, ok := data["checkouts"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected checkouts array, got %T", data["checkouts"])
|
|
}
|
|
|
|
if len(checkouts) != 1 {
|
|
t.Errorf("got %d checkouts, want 1", len(checkouts))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckoutHandler_Get(t *testing.T) {
|
|
t.Run("get_success", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
checkoutRepo.checkouts["checkout-123"] = &domain.Checkout{
|
|
ID: "checkout-123",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/checkout/checkout-123", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["id"] != "checkout-123" {
|
|
t.Errorf("expected id=checkout-123, got %v", data["id"])
|
|
}
|
|
if data["branch"] != "develop" {
|
|
t.Errorf("expected branch=develop, got %v", data["branch"])
|
|
}
|
|
if data["project_id"] != "test-project" {
|
|
t.Errorf("expected project_id=test-project, got %v", data["project_id"])
|
|
}
|
|
})
|
|
|
|
t.Run("get_wrong_project", func(t *testing.T) {
|
|
handler, checkoutRepo, projectRepo := setupCheckoutTest()
|
|
|
|
// Add a second project so the URL is valid.
|
|
projectRepo.projects["other-project"] = &domain.Project{
|
|
ID: "other-project",
|
|
Name: "other-project",
|
|
PodName: "other-project-0",
|
|
Status: domain.ProjectStatusRunning,
|
|
}
|
|
|
|
checkoutRepo.checkouts["checkout-123"] = &domain.Checkout{
|
|
ID: "checkout-123",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/other-project/checkout/checkout-123", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("get_not_found", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/checkout/nonexistent", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckoutHandler_Checkin(t *testing.T) {
|
|
t.Run("checkin_with_review", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
checkoutRepo.checkouts["checkout-checkin"] = &domain.Checkout{
|
|
ID: "checkout-checkin",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout/checkout-checkin/checkin", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["checkout_id"] != "checkout-checkin" {
|
|
t.Errorf("expected checkout_id=checkout-checkin, got %v", data["checkout_id"])
|
|
}
|
|
if data["status"] != "checked_in" {
|
|
t.Errorf("expected status=checked_in, got %v", data["status"])
|
|
}
|
|
if data["message"] == nil || data["message"] == "" {
|
|
t.Error("expected non-empty message")
|
|
}
|
|
})
|
|
|
|
t.Run("checkin_skip_review", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
checkoutRepo.checkouts["checkout-skip"] = &domain.Checkout{
|
|
ID: "checkout-skip",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{"skip_review": true}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout/checkout-skip/checkin", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["status"] != "checked_in" {
|
|
t.Errorf("expected status=checked_in, got %v", data["status"])
|
|
}
|
|
// With skip_review=true and no work queue, no review task should be set.
|
|
if data["review_task_id"] != nil && data["review_task_id"] != "" {
|
|
t.Errorf("expected empty review_task_id with skip_review, got %v", data["review_task_id"])
|
|
}
|
|
})
|
|
|
|
t.Run("checkin_not_active", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
now := time.Now()
|
|
checkoutRepo.checkouts["checkout-done"] = &domain.Checkout{
|
|
ID: "checkout-done",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: now.Add(-1 * time.Hour),
|
|
ExpiresAt: now.Add(23 * time.Hour),
|
|
Status: domain.CheckoutStatusCheckedIn,
|
|
CheckedInAt: &now,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout/checkout-done/checkin", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("checkin_not_found", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/checkout/nonexistent/checkin", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("checkin_wrong_project", func(t *testing.T) {
|
|
handler, checkoutRepo, projectRepo := setupCheckoutTest()
|
|
|
|
projectRepo.projects["other-project"] = &domain.Project{
|
|
ID: "other-project",
|
|
Name: "other-project",
|
|
PodName: "other-project-0",
|
|
Status: domain.ProjectStatusRunning,
|
|
}
|
|
|
|
checkoutRepo.checkouts["checkout-wrong"] = &domain.Checkout{
|
|
ID: "checkout-wrong",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
body := `{}`
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/other-project/checkout/checkout-wrong/checkin", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckoutHandler_Revoke(t *testing.T) {
|
|
t.Run("revoke_success", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
checkoutRepo.checkouts["checkout-revoke"] = &domain.Checkout{
|
|
ID: "checkout-revoke",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusActive,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/checkout/checkout-revoke", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
data, ok := resp["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected data map, got %T", resp["data"])
|
|
}
|
|
|
|
if data["status"] != "revoked" {
|
|
t.Errorf("expected status=revoked, got %v", data["status"])
|
|
}
|
|
if data["id"] != "checkout-revoke" {
|
|
t.Errorf("expected id=checkout-revoke, got %v", data["id"])
|
|
}
|
|
})
|
|
|
|
t.Run("revoke_not_active", func(t *testing.T) {
|
|
handler, checkoutRepo, _ := setupCheckoutTest()
|
|
|
|
checkoutRepo.checkouts["checkout-already-revoked"] = &domain.Checkout{
|
|
ID: "checkout-already-revoked",
|
|
ProjectID: "test-project",
|
|
Branch: "develop",
|
|
GiteaTokenID: 12345,
|
|
CloneURL: "https://git.threesix.ai/threesix/test-project.git",
|
|
CheckedOutBy: "test",
|
|
CheckedOutAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
Status: domain.CheckoutStatusRevoked,
|
|
}
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/checkout/checkout-already-revoked", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("revoke_not_found", func(t *testing.T) {
|
|
handler, _, _ := setupCheckoutTest()
|
|
|
|
router := chi.NewRouter()
|
|
router.Use(testAdminAuth)
|
|
handler.Mount(router)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/checkout/nonexistent", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|