package activator import ( "context" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // Finding 2 on the claim path: when the token and the key are already // present, EnsureClaimAdminToken writes nothing and calls no authorizer, // but it does not answer until the env file and its directory are synced. // A file that is not 0600 goes through the authorized write, which sets it. func TestEnsureClaimAdminTokenSyncsAPresentKeyBeforeItAnswers(t *testing.T) { probe := probeInstanceKeySync(t) const host = "claim-key.example" b := &ExecBackend{DataDir: t.TempDir()} rt := registry.Route{Host: host, Socket: "/run/carlos/claim-key.sock", DBPath: "/data/claim-key.db"} envPath := filepath.Join(b.DataDir, host+".env") body := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("d", 64) + "\n" + InstanceKeyVar + "=" + strings.Repeat("A", 43) + "=\n" if err := os.WriteFile(envPath, []byte(body), 0o600); err != nil { t.Fatal(err) } authorized := 0 authorize := func(_ context.Context, write func() error) error { authorized++; return write() } for _, fail := range []func(error){func(e error) { probe.fileErr = e }, func(e error) { probe.dirErr = e }} { fail(errors.New("injected: EIO")) if err := b.EnsureClaimAdminToken(context.Background(), rt, authorize); err == nil { t.Fatal("the launch source answered while the env file's sync fails") } fail(nil) } before := *probe if err := b.EnsureClaimAdminToken(context.Background(), rt, authorize); err != nil { t.Fatal(err) } if authorized != 0 || probe.file != before.file+1 || probe.dir != before.dir+1 { t.Fatalf("authorized %d, synced file %d dir %d; want 0, 1, 1", authorized, probe.file-before.file, probe.dir-before.dir) } if err := os.Chmod(envPath, 0o644); err != nil { t.Fatal(err) } if err := b.EnsureClaimAdminToken(context.Background(), rt, authorize); err != nil { t.Fatal(err) } if st, _ := os.Stat(envPath); authorized != 1 || st.Mode().Perm() != 0o600 { t.Fatalf("a 0644 file: authorized %d, mode %o; want the guarded path to set 600", authorized, st.Mode().Perm()) } if raw, _ := os.ReadFile(envPath); string(raw) != body { t.Fatal("the claim path rewrote the env file") } } // Pre-flight ruling A: the early return never fails a launch that succeeds // today. With no .env at all and both the token and the key supplied // by the rendered configuration, there is no file to make durable (a // configured key is exempt, ruling 5, and an absent own file is ruling 7's // case), so the launch source answers as it did before: no open failure, no // authorizer, and no file created. func TestEnsureClaimAdminTokenProceedsWithNoOwnEnvFileWhenConfigSuppliesBoth(t *testing.T) { const host = "claim-cfg.example" b := &ExecBackend{DataDir: t.TempDir()} rt := registry.Route{Host: host, Socket: "/run/carlos/claim-cfg.sock", DBPath: "/data/claim-cfg.db"} cfg := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("d", 64) + "\n" + InstanceKeyVar + "=configured\n" if err := os.WriteFile(filepath.Join(b.DataDir, host+".config.env"), []byte(cfg), 0o600); err != nil { t.Fatal(err) } authorized := 0 authorize := func(_ context.Context, write func() error) error { authorized++; return write() } if err := b.EnsureClaimAdminToken(context.Background(), rt, authorize); err != nil { t.Fatalf("a launch that succeeds today failed: %v", err) } if _, err := os.Stat(filepath.Join(b.DataDir, host+".env")); !errors.Is(err, os.ErrNotExist) || authorized != 0 { t.Fatalf("the early return wrote: authorized %d, own env file: %v", authorized, err) } } // The guarded write mints only what the effective env lacks. A token the // rendered configuration supplies is present (the launch delivers it), so // the guarded path mints the key alone and does not put a second token // into the own file above it: only a missing token is minted, as the // function has always promised. func TestEnsureClaimAdminTokenMintsOnlyTheKeyWhenConfigSuppliesTheToken(t *testing.T) { const host = "claim-cfgtok.example" b := &ExecBackend{DataDir: t.TempDir()} rt := registry.Route{Host: host, Socket: "/run/carlos/claim-cfgtok.sock", DBPath: "/data/claim-cfgtok.db"} tok := strings.Repeat("d", 64) if err := os.WriteFile(filepath.Join(b.DataDir, host+".config.env"), []byte("CARLOS_ADMIN_TOKEN="+tok+"\n"), 0o600); err != nil { t.Fatal(err) } authorized := 0 authorize := func(_ context.Context, write func() error) error { authorized++; return write() } if err := b.EnsureClaimAdminToken(context.Background(), rt, authorize); err != nil { t.Fatal(err) } raw, err := os.ReadFile(filepath.Join(b.DataDir, host+".env")) if err != nil { t.Fatalf("the guarded path minted no key: %v", err) } own := parseEnvBytes(raw) if authorized != 1 || !wellFormedInstanceKey(own[InstanceKeyVar]) || own["CARLOS_ADMIN_TOKEN"] != "" { t.Fatalf("authorized %d, own file %d bytes, key well formed %v, token present %v; want one guarded write with a key and no token", authorized, len(raw), wellFormedInstanceKey(own[InstanceKeyVar]), own["CARLOS_ADMIN_TOKEN"] != "") } vars, err := b.instanceEnv(rt) if err != nil || vars["CARLOS_ADMIN_TOKEN"] != tok { t.Fatalf("the launch's token changed: %q, %v", vars["CARLOS_ADMIN_TOKEN"], err) } } // Task 6 ruling (K2): the claim launch source is the one launch path that // returns the key's refusal without a wrapper of its own, so it names the // host there, once. A fenced or conflicting row is refused under the guard, // the error carries "instance key for ", and the file is untouched. func TestEnsureClaimAdminTokenNamesTheHostOnceInAKeyRefusal(t *testing.T) { for _, layout := range []Layout{LayoutFenced, LayoutConflict} { t.Run(string(layout), func(t *testing.T) { b := testExecBackend(t) const host = "shop.example.com" rt := registry.Route{Host: host, Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath(b.DataDir, host), Socket: filepath.Join(provision.SocketDir, host+".sock")} own := filepath.Join(b.DataDir, host+".env") tok := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("d", 64) + "\n" if err := os.WriteFile(own, []byte(tok), 0o600); err != nil { t.Fatal(err) } switch layout { case LayoutFenced: b.LayoutFacts.Journal = func(string) bool { return true } case LayoutConflict: b.LayoutFacts.SameID = func(a, b string) bool { return true } b.LayoutFacts.Peers = func() ([]registry.Route, error) { return []registry.Route{{Host: "other.example.com", Kind: "instance", Hibernate: true, Socket: "/nonexistent/other.sock"}}, nil } } authorized := 0 authorize := func(_ context.Context, write func() error) error { authorized++; return write() } err := b.EnsureClaimAdminToken(context.Background(), rt, authorize) if err == nil || !strings.Contains(err.Error(), "instance key for "+host) || !strings.Contains(err.Error(), "not minted") { t.Fatalf("a %s row: err = %v, want the key's refusal naming the host", layout, err) } if n := strings.Count(err.Error(), host+": "); n != 1 { t.Fatalf("a %s row: the refusal names the host %d times: %v", layout, n, err) } if authorized != 1 { t.Fatalf("authorized %d times, want the one guarded write the refusal came from", authorized) } if raw, _ := os.ReadFile(own); string(raw) != tok { t.Fatalf("a refused %s row's env file was changed", layout) } }) } }