Files
tiny-forge/internal/store/proxy_routes_test.go
T
alexei.dolgolyov d516462750 feat(workload): switch ListProxyRoutes to containers index
The Proxies page consumer (and the secondary callers in
internal/api/health.go and internal/api/settings.go) now read
from the normalized containers index instead of the instances
table. Stage ID is recovered through a (project_id, role=stage_name)
join — uniquely-indexed via the existing UNIQUE(project_id, name)
constraint on stages.

Source field stays "instance" for back-compat with the Proxies
page filter (the frontend keys off the literal string).

Three new tests pin the join shape, verify the npm_proxy_id-only
WHERE branch survives, and check that an orphan-role row falls
out of the join cleanly (catches a regression to LEFT JOIN).
2026-05-09 14:05:19 +03:00

174 lines
4.9 KiB
Go

package store
import "testing"
// TestListProxyRoutesJoinShape verifies the new containers-based join produces
// the same ProxyRoute shape the /api/proxies frontend has consumed since this
// query was instances-based. Without this test, a missing column or a wrong
// join condition would silently break the Proxies page.
func TestListProxyRoutesJoinShape(t *testing.T) {
s := newTestStore(t)
p, err := s.CreateProject(Project{
Name: "wf", Image: "nginx", Port: 80, Env: "{}", Volumes: "{}",
})
if err != nil {
t.Fatalf("CreateProject: %v", err)
}
stage, err := s.CreateStage(Stage{
ProjectID: p.ID, Name: "prod", TagPattern: "*", MaxInstances: 1, EnableProxy: true,
})
if err != nil {
t.Fatalf("CreateStage: %v", err)
}
w, err := s.GetWorkloadByRef(WorkloadKindProject, p.ID)
if err != nil {
t.Fatalf("workload: %v", err)
}
// Container with both subdomain and proxy_route_id populated — the rule
// the WHERE clause filters on.
if _, err := s.CreateContainer(Container{
WorkloadID: w.ID,
WorkloadKind: "project",
Role: stage.Name,
ContainerID: "docker-abc",
ImageTag: "v1",
State: "running",
Port: 8080,
Subdomain: "wf-prod",
ProxyRouteID: "route-1",
}); err != nil {
t.Fatalf("CreateContainer: %v", err)
}
// Container without subdomain — must be filtered OUT.
if _, err := s.CreateContainer(Container{
WorkloadID: w.ID,
WorkloadKind: "project",
Role: stage.Name,
ContainerID: "docker-def",
ImageTag: "v2",
State: "running",
}); err != nil {
t.Fatalf("CreateContainer 2: %v", err)
}
routes, err := s.ListProxyRoutes("example.test")
if err != nil {
t.Fatalf("ListProxyRoutes: %v", err)
}
if len(routes) != 1 {
t.Fatalf("expected 1 route, got %d (filter wrong?)", len(routes))
}
r := routes[0]
if r.Source != "instance" {
t.Errorf("Source: got %q, want 'instance' (back-compat)", r.Source)
}
if r.ProjectID != p.ID {
t.Errorf("ProjectID: got %q, want %q", r.ProjectID, p.ID)
}
if r.ProjectName != "wf" {
t.Errorf("ProjectName: got %q, want 'wf'", r.ProjectName)
}
if r.StageID != stage.ID {
t.Errorf("StageID: got %q, want %q", r.StageID, stage.ID)
}
if r.StageName != "prod" {
t.Errorf("StageName: got %q, want 'prod'", r.StageName)
}
if r.ImageTag != "v1" {
t.Errorf("ImageTag: got %q, want 'v1'", r.ImageTag)
}
if r.Subdomain != "wf-prod" {
t.Errorf("Subdomain: got %q, want 'wf-prod'", r.Subdomain)
}
if r.Domain != "wf-prod.example.test" {
t.Errorf("Domain: got %q, want 'wf-prod.example.test'", r.Domain)
}
if r.ContainerID != "docker-abc" {
t.Errorf("ContainerID: got %q, want 'docker-abc'", r.ContainerID)
}
if r.Port != 8080 {
t.Errorf("Port: got %d, want 8080", r.Port)
}
if r.ProxyRouteID != "route-1" {
t.Errorf("ProxyRouteID: got %q, want 'route-1'", r.ProxyRouteID)
}
if r.Status != "running" {
t.Errorf("Status (state): got %q, want 'running'", r.Status)
}
}
func TestListProxyRoutesNpmOnly(t *testing.T) {
// NPM-only routes (npm_proxy_id > 0, proxy_route_id == "") must still be
// returned — that's the original WHERE-clause OR branch.
s := newTestStore(t)
p, _ := s.CreateProject(Project{
Name: "npm-only", Image: "nginx", Port: 80, Env: "{}", Volumes: "{}",
})
stage, _ := s.CreateStage(Stage{
ProjectID: p.ID, Name: "dev", TagPattern: "*", MaxInstances: 1, EnableProxy: true,
})
w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID)
if _, err := s.CreateContainer(Container{
WorkloadID: w.ID,
WorkloadKind: "project",
Role: stage.Name,
ContainerID: "docker-1",
Subdomain: "npm-only-dev",
NpmProxyID: 42,
}); err != nil {
t.Fatalf("CreateContainer: %v", err)
}
routes, err := s.ListProxyRoutes("")
if err != nil {
t.Fatalf("ListProxyRoutes: %v", err)
}
if len(routes) != 1 {
t.Fatalf("expected 1 npm route, got %d", len(routes))
}
if routes[0].NpmProxyID != 42 {
t.Errorf("NpmProxyID: got %d, want 42", routes[0].NpmProxyID)
}
}
func TestListProxyRoutesIgnoresWrongRole(t *testing.T) {
// Belt-and-suspenders: a container whose role doesn't match a stage name
// would orphan the JOIN. Verify the row falls out cleanly (LEFT JOIN
// would expose a real bug here).
s := newTestStore(t)
p, _ := s.CreateProject(Project{
Name: "wf", Image: "nginx", Port: 80, Env: "{}", Volumes: "{}",
})
_, _ = s.CreateStage(Stage{
ProjectID: p.ID, Name: "prod", TagPattern: "*", MaxInstances: 1,
})
w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID)
if _, err := s.CreateContainer(Container{
WorkloadID: w.ID,
WorkloadKind: "project",
Role: "ghost-stage", // intentionally not a real stage name
ContainerID: "docker-x",
Subdomain: "wf-ghost",
ProxyRouteID: "route-x",
}); err != nil {
t.Fatalf("CreateContainer: %v", err)
}
routes, err := s.ListProxyRoutes("")
if err != nil {
t.Fatalf("ListProxyRoutes: %v", err)
}
if len(routes) != 0 {
t.Fatalf("orphan-role row leaked into result: got %d", len(routes))
}
}