package console import ( "context" "encoding/json" "testing" "time" "github.com/aws/smithy-go" "github.com/carlosframework/platform/internal/awsinv" "github.com/carlosframework/platform/internal/infraview" ) // The pass writes the snapshot where the page reads it, and loadInventory // reads back exactly what was written. func TestInfraPassWritesSnapshot(t *testing.T) { fx := newConsoleFixture(t) fx.srv.inventory = func(context.Context) (infraview.Inventory, error) { return infraview.Inventory{ Provider: "aws", Account: "1234", TakenAt: time.Now().UTC(), Resources: []infraview.Resource{{Provider: "aws", Kind: infraview.KindMachine, ID: "i-1"}}, }, nil } if err := fx.srv.infraPass(context.Background()); err != nil { t.Fatalf("infraPass: %v", err) } body, _, err := fx.rel.Get(context.Background(), infraview.InventoryKey) if err != nil { t.Fatalf("read back: %v", err) } var inv infraview.Inventory if err := json.Unmarshal(body, &inv); err != nil { t.Fatalf("parse: %v", err) } if len(inv.Resources) != 1 || inv.Resources[0].ID != "i-1" { t.Fatalf("stored = %+v", inv) } got, why := fx.srv.loadInventory(context.Background()) if got == nil || why != "" { t.Fatalf("loadInventory = %+v, %q; want the snapshot", got, why) } } // An auth-shaped failure latches the pass off for the process after ONE // attempt: "this deployment has no inventory grant" is a permanent answer, // not something to re-ask AWS every hour forever. // // The fake returns awsinv.ErrNoGrant — the REAL error value // awsinv.Provider.Inventory produces when every read comes back denied and // nothing was discovered (internal/awsinv/awsinv.go) — not a hand-made // smithy.APIError. A prior version of this test used a fake AccessDenied // smithy error, an error shape the real seam never actually emits (the // per-mapping denials are folded into Partial strings well before // assembly decides anything, so no smithy.APIError survives to the // caller); that let the pass pass its tests while the real deny path // never latched at all. Using the real sentinel is what proves the fix. func TestInfraPassLatchesOnAuthShapedFailure(t *testing.T) { fx := newConsoleFixture(t) calls := 0 fx.srv.inventory = func(context.Context) (infraview.Inventory, error) { calls++ return infraview.Inventory{}, awsinv.ErrNoGrant } if err := fx.srv.infraPass(context.Background()); err == nil { t.Fatal("want an error from the denied pass") } if err := fx.srv.infraPass(context.Background()); err == nil { t.Fatal("want an error from the latched pass") } if calls != 1 { t.Fatalf("provider called %d times, want 1 — the latch must stop the second attempt", calls) } if _, why := fx.srv.loadInventory(context.Background()); why != "ungranted" { t.Fatalf("loadInventory reason = %q, want %q", why, "ungranted") } } // A transient failure is NOT the latch: CE throttles under load and a blip // must not silently kill inventory until the next redeploy. Uses a real // smithy.GenericAPIError coded ThrottlingException — the same fake type // cepull_test.go uses in this package — so this proves a genuine AWS // throttle survives errors.As(&smithy.APIError{}) and still fails // awsinv.IsAuthShaped's code check, rather than merely proving a plain // non-API error fails the type assertion outright. func TestInfraPassRetriesTransientFailure(t *testing.T) { fx := newConsoleFixture(t) calls := 0 fx.srv.inventory = func(context.Context) (infraview.Inventory, error) { calls++ return infraview.Inventory{}, &smithy.GenericAPIError{Code: "ThrottlingException"} } _ = fx.srv.infraPass(context.Background()) _ = fx.srv.infraPass(context.Background()) if calls != 2 { t.Fatalf("provider called %d times, want 2 — a throttle must not latch", calls) } } // With no snapshot ever written, the page must be told "never", never // handed an empty inventory that would render as "this account has no // resources". func TestLoadInventoryNeverTaken(t *testing.T) { fx := newConsoleFixture(t) inv, why := fx.srv.loadInventory(context.Background()) if inv != nil || why != "never" { t.Fatalf("loadInventory = %+v, %q; want nil, \"never\"", inv, why) } } // StartInfraPass leaves as soon as the latch is set, not an hour later. The // pass that FLIPS the latch returns its own wrapped error rather than // errInfraDisabled — only the NEXT call hits the top-of-function guard — so // without the post-pass latch check the goroutine parks on a one-hour ticker // with provably nothing left to do. This test would sit for infraPassInterval // if that check were removed, which is exactly the regression it exists to // catch. func TestStartInfraPassStopsWhenLatched(t *testing.T) { fx := newConsoleFixture(t) calls := 0 fx.srv.inventory = func(context.Context) (infraview.Inventory, error) { calls++ return infraview.Inventory{}, awsinv.ErrNoGrant } done := make(chan struct{}) go func() { fx.srv.StartInfraPass(context.Background()) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("StartInfraPass is still ticking after the latch was set") } if calls != 1 { t.Errorf("provider called %d times, want exactly 1 — the latch is permanent for the process", calls) } if !fx.srv.infraDisabled.Load() { t.Error("the latch was not set") } }