package console import ( "crypto/ed25519" "encoding/json" "errors" "reflect" "runtime" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/version" ) func TestHostConsoleAdmissionAfterFloorPublication(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") build := version.Build version.Build = "build123" t.Cleanup(func() { version.Build = build }) for _, fault := range []string{"none", "missing", "unsigned", "wrong-signer", "wrong-bucket", "wrong-id", "wrong-enrollment", "wrong-compute", "unready", "stale", "report-enrollment", "guard-error"} { t.Run(fault, func(t *testing.T) { ctx := t.Context() s, d, _ := activationFixture(t) s.cfg.HostConsoleID, s.cfg.ReleasePub, s.cfg.HostClaim = "console-2", d.PublicKey, true signer := release.RootSigner(s.cfg.Key) before, err := d.Read(ctx) if err != nil { t.Fatal(err) } if before.Roster.Publication != "published" { t.Fatal("fixture has no published floor") } c := hostactors.Console{Enrollment: strings.Repeat("c", 32), InstanceID: s.cfg.HostConsoleID, Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket} invited := c bucket := d.Bucket switch fault { case "wrong-bucket": bucket = "other" case "wrong-id": invited.InstanceID = "other" case "wrong-enrollment": invited.Enrollment = "invalid" case "wrong-compute": invited.Compute = &hostactors.ComputeBinding{Box: "unknown", Enrollment: strings.Repeat("a", 32)} } invitation := hostactors.ConsoleInvitation{V: 1, Bucket: bucket, Console: invited} invitationSigner := signer if fault == "wrong-signer" { _, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } invitationSigner = release.RootSigner(key) } wire, err := release.SignRootEnvelope(invitationSigner, invitation) if fault == "unsigned" { wire, err = json.Marshal(invitation) } if err != nil { t.Fatal(err) } if fault != "missing" { if err := release.CreateExact(ctx, d.Store, "control/host-console-invitations/"+c.InstanceID+".json", wire); err != nil { t.Fatal(err) } } platform := runtime.GOOS + "-" + runtime.GOARCH hash := strings.Repeat("d", 64) rel := release.SystemRelease{Version: version.Build, ReleaseTag: "v1.2.3", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version.Build, platform), SHA256: hash}}, ConsoleArtifacts: map[string]release.SystemArtifact{platform: {Key: "bootstrap/console/build123/carlos-" + platform, SHA256: hash}}} if _, err := release.PromoteSystem(ctx, d.Store, signer, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } wire, _, err = d.Store.Get(ctx, release.SystemPointerKey("router", "stable")) if err != nil { t.Fatal(err) } if err := release.PublishSystemIdentity(ctx, d.Store, d.PublicKey, wire); err != nil { t.Fatal(err) } next := hostactors.Clone(before.Roster) next.Consoles[c.InstanceID] = c if _, err := d.Replace(ctx, signer, before, next); err == nil { t.Fatal("invitation admitted console without a report") } called := false report, err := s.hostReport(ctx, func() (hostfloor.Running, error) { called = true if fault == "guard-error" { return hostfloor.Running{}, hostfloor.ErrFloor } return hostfloor.Running{ReleaseTag: rel.ReleaseTag, Hash: hash, Protocol: 1, BootID: "boot", FloorSHA256: before.Roster.FloorSHA256, GuardReady: fault != "unready"}, nil }) refused := fault == "missing" || fault == "unsigned" || fault == "wrong-signer" || fault == "wrong-bucket" || fault == "wrong-id" || fault == "wrong-enrollment" || fault == "wrong-compute" if refused || fault == "guard-error" { if err == nil || (refused && called) { t.Fatalf("invalid invitation or guard accepted: called=%v, err=%v", called, err) } return } if err != nil { t.Fatal("invited console cannot report before admission", err) } if report.Actor != c.InstanceID || report.Enrollment != c.Enrollment || report.Component != "console" || report.ProcessID == "" { t.Fatal("incorrect reporting identity", report) } unchanged, err := d.Read(ctx) if err != nil || unchanged.ETag != before.ETag { t.Fatal("reporting changed membership", err) } switch fault { case "stale": report.GeneratedAt = time.Now().Add(-time.Hour) case "report-enrollment": report.Enrollment = strings.Repeat("b", 32) } raw, err := json.Marshal(report) if err != nil { t.Fatal(err) } if err := release.CreateExact(ctx, d.Store, "status/consoles/"+c.InstanceID+".json", raw); err != nil { t.Fatal(err) } admittedAt := time.Now() after, err := d.Replace(ctx, signer, before, next) if fault != "none" { if err == nil { t.Fatal("invalid report admitted console") } return } if err != nil { t.Fatal("admission after reporting failed", err) } enrolled := after.Roster.Consoles[c.InstanceID] if enrolled.EnrolledAt.Before(admittedAt) || enrolled.EnrolledAt.After(time.Now()) { t.Fatal("admission did not assign enrollment time", enrolled.EnrolledAt) } c.EnrolledAt = enrolled.EnrolledAt if !reflect.DeepEqual(enrolled, c) { t.Fatal("admission changed the invited identity", enrolled) } if err := d.Store.Delete(ctx, "control/host-console-invitations/"+c.InstanceID+".json"); err != nil { t.Fatal(err) } _, err = s.hostReport(ctx, func() (hostfloor.Running, error) { return hostfloor.Running{}, hostfloor.ErrFloor }) if !errors.Is(err, hostfloor.ErrFloor) { t.Fatal("admitted console still requires invitation", err) } }) } }