Public source validation / validate (push) Failing after 3m8s
111 lines
5.5 KiB
Go
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")
|
|
}
|
|
}
|