|
|
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/kindredsystems/silo/internal/db"
|
|
|
|
|
@@ -319,6 +320,260 @@ func TestHandleDeleteRunner(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Trigger integration tests ---
|
|
|
|
|
|
|
|
|
|
// newTriggerRouter builds a router with items, revisions, BOM, and jobs routes
|
|
|
|
|
// so that HTTP-based actions can fire triggerJobs via goroutine.
|
|
|
|
|
func newTriggerRouter(s *Server) http.Handler {
|
|
|
|
|
r := chi.NewRouter()
|
|
|
|
|
r.Route("/api/items", func(r chi.Router) {
|
|
|
|
|
r.Post("/", s.HandleCreateItem)
|
|
|
|
|
r.Route("/{partNumber}", func(r chi.Router) {
|
|
|
|
|
r.Post("/revisions", s.HandleCreateRevision)
|
|
|
|
|
r.Post("/bom", s.HandleAddBOMEntry)
|
|
|
|
|
r.Put("/bom/{childPartNumber}", s.HandleUpdateBOMEntry)
|
|
|
|
|
r.Delete("/bom/{childPartNumber}", s.HandleDeleteBOMEntry)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
r.Route("/api/jobs", func(r chi.Router) {
|
|
|
|
|
r.Get("/", s.HandleListJobs)
|
|
|
|
|
})
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func waitForJobs(t *testing.T, s *Server, itemID string, wantCount int) []*db.Job {
|
|
|
|
|
t.Helper()
|
|
|
|
|
// triggerJobs runs in a goroutine; poll up to 2 seconds.
|
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
|
jobs, err := s.jobs.ListJobs(context.Background(), "", itemID, 50, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("listing jobs: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(jobs) >= wantCount {
|
|
|
|
|
return jobs
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
jobs, _ := s.jobs.ListJobs(context.Background(), "", itemID, 50, 0)
|
|
|
|
|
return jobs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTriggerJobsOnRevisionCreate(t *testing.T) {
|
|
|
|
|
s := newJobTestServer(t)
|
|
|
|
|
if err := s.modules.SetEnabled("jobs", true); err != nil {
|
|
|
|
|
t.Fatalf("enabling jobs module: %v", err)
|
|
|
|
|
}
|
|
|
|
|
router := newTriggerRouter(s)
|
|
|
|
|
|
|
|
|
|
// Create an item.
|
|
|
|
|
createItemDirect(t, s, "TRIG-REV-001", "trigger test item", nil)
|
|
|
|
|
|
|
|
|
|
// Seed a job definition that triggers on revision_created.
|
|
|
|
|
def := &db.JobDefinitionRecord{
|
|
|
|
|
Name: "rev-trigger-test",
|
|
|
|
|
Version: 1,
|
|
|
|
|
TriggerType: "revision_created",
|
|
|
|
|
ScopeType: "item",
|
|
|
|
|
ComputeType: "validate",
|
|
|
|
|
RunnerTags: []string{"test"},
|
|
|
|
|
TimeoutSeconds: 60,
|
|
|
|
|
MaxRetries: 0,
|
|
|
|
|
Priority: 100,
|
|
|
|
|
Enabled: true,
|
|
|
|
|
}
|
|
|
|
|
if err := s.jobs.UpsertDefinition(context.Background(), def); err != nil {
|
|
|
|
|
t.Fatalf("seeding definition: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a revision via HTTP (fires triggerJobs in goroutine).
|
|
|
|
|
body := `{"properties":{"material":"steel"},"comment":"trigger test"}`
|
|
|
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/TRIG-REV-001/revisions", strings.NewReader(body)))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("create revision: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the item ID to filter jobs.
|
|
|
|
|
item, _ := s.items.GetByPartNumber(context.Background(), "TRIG-REV-001")
|
|
|
|
|
if item == nil {
|
|
|
|
|
t.Fatal("item not found after creation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jobs := waitForJobs(t, s, item.ID, 1)
|
|
|
|
|
if len(jobs) == 0 {
|
|
|
|
|
t.Fatal("expected at least 1 triggered job, got 0")
|
|
|
|
|
}
|
|
|
|
|
if jobs[0].DefinitionName != "rev-trigger-test" {
|
|
|
|
|
t.Errorf("expected definition name rev-trigger-test, got %s", jobs[0].DefinitionName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTriggerJobsOnBOMChange(t *testing.T) {
|
|
|
|
|
s := newJobTestServer(t)
|
|
|
|
|
if err := s.modules.SetEnabled("jobs", true); err != nil {
|
|
|
|
|
t.Fatalf("enabling jobs module: %v", err)
|
|
|
|
|
}
|
|
|
|
|
router := newTriggerRouter(s)
|
|
|
|
|
|
|
|
|
|
// Create parent and child items.
|
|
|
|
|
createItemDirect(t, s, "TRIG-BOM-P", "parent", nil)
|
|
|
|
|
createItemDirect(t, s, "TRIG-BOM-C", "child", nil)
|
|
|
|
|
|
|
|
|
|
// Seed a bom_changed job definition.
|
|
|
|
|
def := &db.JobDefinitionRecord{
|
|
|
|
|
Name: "bom-trigger-test",
|
|
|
|
|
Version: 1,
|
|
|
|
|
TriggerType: "bom_changed",
|
|
|
|
|
ScopeType: "item",
|
|
|
|
|
ComputeType: "validate",
|
|
|
|
|
RunnerTags: []string{"test"},
|
|
|
|
|
TimeoutSeconds: 60,
|
|
|
|
|
MaxRetries: 0,
|
|
|
|
|
Priority: 100,
|
|
|
|
|
Enabled: true,
|
|
|
|
|
}
|
|
|
|
|
if err := s.jobs.UpsertDefinition(context.Background(), def); err != nil {
|
|
|
|
|
t.Fatalf("seeding definition: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a BOM entry via HTTP.
|
|
|
|
|
body := `{"child_part_number":"TRIG-BOM-C","rel_type":"component","quantity":2}`
|
|
|
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/TRIG-BOM-P/bom", strings.NewReader(body)))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("add BOM entry: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the parent item ID.
|
|
|
|
|
parent, _ := s.items.GetByPartNumber(context.Background(), "TRIG-BOM-P")
|
|
|
|
|
if parent == nil {
|
|
|
|
|
t.Fatal("parent item not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jobs := waitForJobs(t, s, parent.ID, 1)
|
|
|
|
|
if len(jobs) == 0 {
|
|
|
|
|
t.Fatal("expected at least 1 triggered job, got 0")
|
|
|
|
|
}
|
|
|
|
|
if jobs[0].DefinitionName != "bom-trigger-test" {
|
|
|
|
|
t.Errorf("expected definition name bom-trigger-test, got %s", jobs[0].DefinitionName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTriggerJobsFilterMismatch(t *testing.T) {
|
|
|
|
|
s := newJobTestServer(t)
|
|
|
|
|
if err := s.modules.SetEnabled("jobs", true); err != nil {
|
|
|
|
|
t.Fatalf("enabling jobs module: %v", err)
|
|
|
|
|
}
|
|
|
|
|
router := newTriggerRouter(s)
|
|
|
|
|
|
|
|
|
|
// Create a "part" type item (not "assembly").
|
|
|
|
|
createItemDirect(t, s, "TRIG-FILT-P", "filter parent", nil)
|
|
|
|
|
createItemDirect(t, s, "TRIG-FILT-C", "filter child", nil)
|
|
|
|
|
|
|
|
|
|
// Seed a definition that only triggers for assembly items.
|
|
|
|
|
def := &db.JobDefinitionRecord{
|
|
|
|
|
Name: "assembly-only-test",
|
|
|
|
|
Version: 1,
|
|
|
|
|
TriggerType: "bom_changed",
|
|
|
|
|
ScopeType: "item",
|
|
|
|
|
ComputeType: "validate",
|
|
|
|
|
RunnerTags: []string{"test"},
|
|
|
|
|
TimeoutSeconds: 60,
|
|
|
|
|
MaxRetries: 0,
|
|
|
|
|
Priority: 100,
|
|
|
|
|
Enabled: true,
|
|
|
|
|
Definition: map[string]any{
|
|
|
|
|
"trigger": map[string]any{
|
|
|
|
|
"filter": map[string]any{
|
|
|
|
|
"item_type": "assembly",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
if err := s.jobs.UpsertDefinition(context.Background(), def); err != nil {
|
|
|
|
|
t.Fatalf("seeding definition: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a BOM entry on a "part" item (should NOT match assembly filter).
|
|
|
|
|
body := `{"child_part_number":"TRIG-FILT-C","rel_type":"component","quantity":1}`
|
|
|
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/TRIG-FILT-P/bom", strings.NewReader(body)))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("add BOM entry: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait briefly, then verify no jobs were created.
|
|
|
|
|
parent, _ := s.items.GetByPartNumber(context.Background(), "TRIG-FILT-P")
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
jobs, err := s.jobs.ListJobs(context.Background(), "", parent.ID, 50, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("listing jobs: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(jobs) != 0 {
|
|
|
|
|
t.Errorf("expected 0 jobs (filter mismatch), got %d", len(jobs))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTriggerJobsModuleDisabled(t *testing.T) {
|
|
|
|
|
s := newJobTestServer(t)
|
|
|
|
|
// Jobs module is disabled by default in NewRegistry().
|
|
|
|
|
router := newTriggerRouter(s)
|
|
|
|
|
|
|
|
|
|
// Create items.
|
|
|
|
|
createItemDirect(t, s, "TRIG-DIS-P", "disabled parent", nil)
|
|
|
|
|
createItemDirect(t, s, "TRIG-DIS-C", "disabled child", nil)
|
|
|
|
|
|
|
|
|
|
// Seed a bom_changed definition (it exists in DB but module is off).
|
|
|
|
|
def := &db.JobDefinitionRecord{
|
|
|
|
|
Name: "disabled-trigger-test",
|
|
|
|
|
Version: 1,
|
|
|
|
|
TriggerType: "bom_changed",
|
|
|
|
|
ScopeType: "item",
|
|
|
|
|
ComputeType: "validate",
|
|
|
|
|
RunnerTags: []string{"test"},
|
|
|
|
|
TimeoutSeconds: 60,
|
|
|
|
|
MaxRetries: 0,
|
|
|
|
|
Priority: 100,
|
|
|
|
|
Enabled: true,
|
|
|
|
|
}
|
|
|
|
|
if err := s.jobs.UpsertDefinition(context.Background(), def); err != nil {
|
|
|
|
|
t.Fatalf("seeding definition: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a BOM entry with jobs module disabled.
|
|
|
|
|
body := `{"child_part_number":"TRIG-DIS-C","rel_type":"component","quantity":1}`
|
|
|
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/TRIG-DIS-P/bom", strings.NewReader(body)))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("add BOM entry: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait briefly, then verify no jobs were created.
|
|
|
|
|
parent, _ := s.items.GetByPartNumber(context.Background(), "TRIG-DIS-P")
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
jobs, err := s.jobs.ListJobs(context.Background(), "", parent.ID, 50, 0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("listing jobs: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(jobs) != 0 {
|
|
|
|
|
t.Errorf("expected 0 jobs (module disabled), got %d", len(jobs))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGenerateRunnerToken(t *testing.T) {
|
|
|
|
|
raw, hash, prefix := generateRunnerToken()
|
|
|
|
|
|
|
|
|
|
|