//go:build integration package main import ( "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) func TestAConsumerPinnedBackIsNoLongerGrantedByTheProviderItLeft(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory for _, m := range storeManifests() { register(t, open, m) } if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil { t.Fatal(err) } if _, err := assign(ctx, open, "anchor", "store"); err != nil { t.Fatal(err) } if err := inv.HoldSeat(ctx, "mesh-store", catalogue.ScopeMesh, "anchor", "store"); err != nil { t.Fatal(err) } for _, a := range [][2]string{{"laptop", "store"}, {"laptop", "board"}} { if _, err := assign(ctx, open, a[0], a[1]); err != nil { t.Fatalf("assign %s %s: %v", a[0], a[1], err) } } // Bound to the anchor's store, and sent so: the credential from the anchor is made and recorded. if err := inv.PinProvision(ctx, "laptop", "postgres-database", "anchor", "store"); err != nil { t.Fatal(err) } plan, _, err := resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } // Only a send makes a credential; a question makes nothing. if _, err := composeSend(ctx, open, "laptop"); err != nil { t.Fatal(err) } if n := need(t, plan, "board", "postgres-database"); n.From != "anchor" { t.Fatalf("pinned to the anchor and bound to %s", n.From) } if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil { t.Fatal(err) } grants, _, unbound, err := grantsOf(ctx, open, "anchor") if err != nil { t.Fatal(err) } if g, ok := grantOf(grants, "postgres-database", "laptop", "board"); !ok || g.From != "board" || len(unbound) != 0 { t.Fatalf("a consumer bound to the anchor is not granted there: %+v, unbound %+v", grants, unbound) } // Pinned back beside its data, as the operator did. if err := inv.PinProvision(ctx, "laptop", "postgres-database", "laptop", "store"); err != nil { t.Fatal(err) } plan, _, err = resolveNode(ctx, open, "laptop") if err != nil { t.Fatal(err) } if _, err := composeSend(ctx, open, "laptop"); err != nil { t.Fatal(err) } if n := need(t, plan, "board", "postgres-database"); n.From != "laptop" { t.Fatalf("pinned back to the laptop and bound to %s", n.From) } if err := inv.RecordBindings(ctx, "laptop", boundToData(plan, nil)); err != nil { t.Fatal(err) } holders, err := inv.HoldersOf(ctx, "postgres-database", "laptop") if err != nil { t.Fatal(err) } if len(holders) != 2 { t.Fatalf("today's state is two credentials on record, one from each store: %+v", holders) } // The store it left: no longer granted, and said. grants, _, unbound, err = grantsOf(ctx, open, "anchor") if err != nil { t.Fatal(err) } if g, ok := grantOf(grants, "postgres-database", "laptop", "board"); ok && g.From != "" { t.Fatalf("the anchor's store is still granted a consumer bound to the laptop's: %+v", g) } if len(unbound) != 1 || unbound[0].Module != "board" || unbound[0].Provider != "anchor" || strings.Join(unbound[0].BoundTo, ",") != "laptop" { t.Fatalf("the consumer that moved is not the one said: %+v", unbound) } planned, settings, err := resolveNode(ctx, open, "anchor") if err != nil { t.Fatal(err) } declared, err := declarationFor(ctx, open, "anchor", planned, settings) if err != nil { t.Fatal(err) } if got := declared.Received["store"]["postgres-database"]; len(got) != 0 { t.Fatalf("the anchor's store is still told about %+v", got) } said := printed(t, func() error { reportLeftOut("anchor", declared); return nil }) if !strings.Contains(said, "board on laptop is bound to laptop for postgres-database, not to anchor") || !strings.Contains(said, "cleanup delete") { t.Fatalf("the push does not say whom the anchor no longer grants:\n%s", said) } // The store it is bound to: granted. grants, _, unbound, err = grantsOf(ctx, open, "laptop") if err != nil { t.Fatal(err) } if g, ok := grantOf(grants, "postgres-database", "laptop", "board"); !ok || g.From != "board" || len(unbound) != 0 { t.Fatalf("the consumer is not granted by the store it is bound to: %+v, unbound %+v", grants, unbound) } // And the credential from the store it left is kept: the key to the login and data held there. if holders, err = inv.HoldersOf(ctx, "postgres-database", "laptop"); err != nil || len(holders) != 2 { t.Fatalf("a credential was forgotten while its provider still holds the login: %+v, %v", holders, err) } } func TestAConsumerWhoseResolutionCannotBeReadIsNotWithdrawn(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory for _, m := range storeManifests() { register(t, open, m) } for _, a := range [][2]string{{"anchor", "store"}, {"laptop", "board"}} { if _, err := assign(ctx, open, a[0], a[1]); err != nil { t.Fatalf("assign %s %s: %v", a[0], a[1], err) } } if _, err := composeSend(ctx, open, "laptop"); err != nil { t.Fatal(err) } // The laptop's key changes to one nothing can seal to: its resolution cannot be completed, and // that is not its set failing to compose. laptop, err := inv.NodeByName(ctx, "laptop") if err != nil { t.Fatal(err) } if err := inv.RecordSealingKey(ctx, laptop.ID, "not a key"); err != nil { t.Fatal(err) } if _, _, err := resolveNode(ctx, open, "laptop"); err == nil || unresolvable(err) { t.Fatalf("the seam this test relies on moved: %v", err) } grants, _, unbound, err := grantsOf(ctx, open, "anchor") if err == nil { t.Fatalf("an unreadable consumer was answered: grants %+v, unbound %+v", grants, unbound) } // mesh-sdk names the provider's node where the controller's own words named the provision. if !strings.Contains(err.Error(), "what laptop asked of") || !strings.Contains(err.Error(), "cannot be read") { t.Fatalf("the error does not say whose resolution could not be read: %v", err) } }