Files
ITWorx-Pulse-Public/internal/service/dependency_repository_integration_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

111 lines
5.5 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"os"
"testing"
"time"
"github.com/itworx/pulse/internal/audit"
"github.com/itworx/pulse/internal/database"
"github.com/jackc/pgx/v5"
)
func TestDependencyRepositoryPostgreSQL(t *testing.T) {
dsn := os.Getenv("PULSE_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("PULSE_TEST_DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
pool, err := database.NewPool(ctx, database.Config{URL: dsn, MaxConns: 8, MinConns: 1})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := database.Migrate(ctx, pool); err != nil {
t.Fatal(err)
}
// This test intentionally uses a unique run-scoped namespace and never issues
// DELETE/TRUNCATE against the configured test database.
run := fmt.Sprintf("%012x", time.Now().UnixNano()&0xffffffffffff)
serviceA := "00000000-0000-0000-0000-" + run
serviceB := fmt.Sprintf("00000000-0000-0000-0001-%012x", (time.Now().UnixNano()+1)&0xffffffffffff)
source := fmt.Sprintf("00000000-0000-0000-0002-%012x", (time.Now().UnixNano()+2)&0xffffffffffff)
dependencyID := fmt.Sprintf("00000000-0000-0000-0003-%012x", (time.Now().UnixNano()+3)&0xffffffffffff)
if _, err := pool.Exec(ctx, `INSERT INTO services (id,name,state,revision) VALUES ($1,$2,'unknown',1),($3,$4,'unknown',1)`, serviceA, "dependency-test-a-"+run, serviceB, "dependency-test-b-"+run); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO data_sources (id,type,name,configuration_ref) VALUES ($1,'test',$2,'none')`, source, "dependency-test-"+run); err != nil {
t.Fatal(err)
}
store := &audit.MemoryStore{}
repo, err := NewDependencyRepository(pool, store)
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC().Truncate(time.Microsecond)
item := Dependency{ID: dependencyID, ServiceID: serviceA, DependsOnServiceID: serviceB, SourceID: source, RelationType: RelationDependsOn, Confidence: .9, FirstSeenAt: now, LastSeenAt: &now}
if err := repo.Upsert(ctx, item, "test-user", "corr-dependency-"+run); err != nil {
t.Fatal(err)
}
if err := repo.PersistDiscovery(ctx, []Dependency{item}, source, "corr-discovery-"+run); err != nil {
t.Fatal(err)
}
items, err := repo.List(ctx, serviceA, 10)
if err != nil || len(items) != 1 || items[0].SourceID != source {
t.Fatalf("unexpected dependency list: %+v, %v", items, err)
}
if len(store.Events) < 2 {
t.Fatalf("expected audit events, got %d", len(store.Events))
}
if err := repo.Upsert(ctx, Dependency{ID: fmt.Sprintf("00000000-0000-0000-0004-%012x", (time.Now().UnixNano()+4)&0xffffffffffff), ServiceID: serviceB, DependsOnServiceID: serviceA, RelationType: RelationDependsOn, Confidence: 1, FirstSeenAt: now}, "test-user", "corr-cycle-"+run); err == nil {
t.Fatal("expected cycle rejection")
}
items, err = repo.List(ctx, serviceA, 10)
if err != nil || len(items) != 1 {
t.Fatalf("cycle rejection changed persisted graph: %+v, %v", items, err)
}
if err := repo.InTx(ctx, func(txctx context.Context, tx pgx.Tx) error {
candidate := Dependency{ID: fmt.Sprintf("00000000-0000-0000-0005-%012x", (time.Now().UnixNano()+5)&0xffffffffffff), ServiceID: serviceB, DependsOnServiceID: serviceA, RelationType: RelationBacks, Confidence: 1, FirstSeenAt: now}
if err := upsertDependency(txctx, tx, candidate); err != nil {
return err
}
return errors.New("force rollback")
}, audit.Event{}); err == nil {
t.Fatal("expected rollback error")
}
items, err = repo.List(ctx, serviceA, 10)
if err != nil || len(items) != 1 {
t.Fatalf("rolled-back dependency persisted: %+v, %v", items, err)
}
if err := repo.Archive(ctx, item, "test-user", "corr-archive-"+run); err != nil {
t.Fatal(err)
}
items, err = repo.List(ctx, serviceA, 10)
if err != nil || len(items) != 0 {
t.Fatalf("archived dependency remained active: %+v, %v", items, err)
}
entityA := fmt.Sprintf("00000000-0000-0000-0007-%012x", (time.Now().UnixNano()+7)&0xffffffffffff)
entityB := fmt.Sprintf("00000000-0000-0000-0008-%012x", (time.Now().UnixNano()+8)&0xffffffffffff)
relationID := fmt.Sprintf("00000000-0000-0000-0009-%012x", (time.Now().UnixNano()+9)&0xffffffffffff)
if _, err := pool.Exec(ctx, `INSERT INTO entities (id,entity_type,canonical_name,display_name,first_seen_at) VALUES ($1,'service',$2,$2,$5),($3,'service',$4,$4,$5)`, entityA, "inventory-service-a-"+run, entityB, "inventory-service-b-"+run, now); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `UPDATE services SET entity_id=CASE id WHEN $1::uuid THEN $2::uuid WHEN $3::uuid THEN $4::uuid END WHERE id IN ($1,$3)`, serviceA, entityA, serviceB, entityB); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO entity_relations (id,source_entity_id,relation_type,target_entity_id,source_id,confidence,confirmed,first_seen_at,last_seen_at) VALUES ($1,$2,'backs',$3,$4,.75,true,$5,$5)`, relationID, entityA, entityB, source, now); err != nil {
t.Fatal(err)
}
items, err = repo.List(ctx, serviceA, 10)
if err != nil || len(items) != 1 || items[0].ID != "inventory:"+relationID || items[0].SourceID != source || items[0].RelationType != RelationBacks || !items[0].Confirmed {
t.Fatalf("inventory relation was not projected with provenance: %+v, %v", items, err)
}
if err := repo.Upsert(ctx, Dependency{ID: fmt.Sprintf("00000000-0000-0000-0006-%012x", (time.Now().UnixNano()+6)&0xffffffffffff), ServiceID: serviceA, DependsOnServiceID: "00000000-0000-0000-0000-0000000000ff", RelationType: RelationDependsOn, Confidence: 1, FirstSeenAt: now}, "test-user", "corr-fk-"+run); err == nil {
t.Fatal("expected missing-service foreign key error")
}
}