mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
5784d907c0
* feat(cli): PAD_TOKEN environment override for stored credentials (#879) Layer 1 of #879: if PAD_TOKEN is set, the CLI uses it as the bearer token and skips the credential-store lookup — gh's GH_TOKEN convention. Reads never write credentials.json, so a read-only override sidesteps the multi-agent identity contention completely; the store is never touched under the override. Per the acceptance grounding notes: - NewClientFromURL resolves PAD_TOKEN before the per-server store lookup (the single token-attachment chokepoint). - whoami no longer lies under the override: it skips the store short-circuit and reports the effective identity via a real /me fetch, with an 'Auth: PAD_TOKEN environment override' line. - auth login/logout print a gh-style stderr notice when the override is active. logout additionally pins its server-side session invalidation to the STORED token — an unpinned Logout() after the constructor change would have invalidated the env token's session — and skips the server call when there is no stored session. - pad init's status line and server info's report disclose the override (env_token_override field; the auth probe uses the token every other command would use). Zero behaviour change when PAD_TOKEN is unset. Token minting stays web-only; a minimal 'pad token' CLI is offered as a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): review round 1 — init fails on a rejected PAD_TOKEN; login shortcut skipped under the override; logout asymmetry documented Per the PR #1160 round-1 review: - Bug 1: pad init's auth step no longer falls back to stored credentials when a set PAD_TOKEN is rejected — it fails with the distinct rejected-token message (mirroring whoami), which also makes the status line's override disclosure truthful. Test drives the real padInitCmd flow and asserts the stored identity is never consulted. - Bug 2: login's 'Already logged in as <stored user>' shortcut is skipped when the override is active — it reads the store, and firing it right after envTokenNotice contradicted the notice. A second test pins the unchanged no-override shortcut behaviour. - Doc ask: the deliberate logout asymmetry (the env token's own session is never invalidated; its lifecycle belongs to the minter, GH_TOKEN posture) is now stated in env_token.go's doc comment and the README PAD_TOKEN section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
107 lines
3.2 KiB
Go
107 lines
3.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// Tests for the PAD_TOKEN environment override (issue #879, layer 1).
|
|
//
|
|
// Environment isolation sets BOTH HOME and USERPROFILE: CredentialsPath
|
|
// resolves through os.UserHomeDir, which reads HOME on Unix and
|
|
// USERPROFILE on Windows — setting only HOME (the older convention in
|
|
// this package) leaves Windows runs pointed at the developer's real
|
|
// ~/.pad.
|
|
|
|
func setTempHome(t *testing.T) string {
|
|
t.Helper()
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("USERPROFILE", home)
|
|
return home
|
|
}
|
|
|
|
// PAD_TOKEN is an explicit per-process override, so it must win exactly
|
|
// when set to something non-blank and be invisible otherwise.
|
|
func TestEnvToken_SetReturnsTrimmed(t *testing.T) {
|
|
t.Setenv("PAD_TOKEN", " pad_abc123 ")
|
|
if got := EnvToken(); got != "pad_abc123" {
|
|
t.Errorf("EnvToken() = %q, want %q", got, "pad_abc123")
|
|
}
|
|
}
|
|
|
|
func TestEnvToken_UnsetReturnsEmpty(t *testing.T) {
|
|
t.Setenv("PAD_TOKEN", "")
|
|
if got := EnvToken(); got != "" {
|
|
t.Errorf("EnvToken() = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestEnvToken_WhitespaceOnlyReturnsEmpty(t *testing.T) {
|
|
t.Setenv("PAD_TOKEN", " ")
|
|
if got := EnvToken(); got != "" {
|
|
t.Errorf("EnvToken() = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func writeStoreForClientTest(t *testing.T) {
|
|
t.Helper()
|
|
home := setTempHome(t)
|
|
dir := filepath.Join(home, ".pad")
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
body := `{"version": 2, "credentials": {"http://127.0.0.1:7777": {
|
|
"server_url": "http://127.0.0.1:7777", "token": "padsess_fromstore",
|
|
"user_id": "u-1", "email": "a@b.c", "name": "A"}}}`
|
|
if err := os.WriteFile(filepath.Join(dir, "credentials.json"), []byte(body), 0600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
}
|
|
|
|
// PAD_TOKEN must beat the stored credential: the env var is the caller's
|
|
// explicit, per-process choice; the file is ambient machine state.
|
|
func TestNewClientFromURL_EnvTokenOverridesStore(t *testing.T) {
|
|
writeStoreForClientTest(t)
|
|
t.Setenv("PAD_TOKEN", "pad_envtoken")
|
|
|
|
c := NewClientFromURL("http://127.0.0.1:7777")
|
|
if c.authToken != "pad_envtoken" {
|
|
t.Errorf("authToken = %q, want env token to win over store", c.authToken)
|
|
}
|
|
}
|
|
|
|
func TestNewClientFromURL_NoEnvFallsBackToStore(t *testing.T) {
|
|
writeStoreForClientTest(t)
|
|
t.Setenv("PAD_TOKEN", "")
|
|
|
|
c := NewClientFromURL("http://127.0.0.1:7777")
|
|
if c.authToken != "padsess_fromstore" {
|
|
t.Errorf("authToken = %q, want stored token when PAD_TOKEN unset", c.authToken)
|
|
}
|
|
}
|
|
|
|
// The override must not touch the store: reads stay side-effect-free
|
|
// (the invariant the v2 store documents), so a process running under
|
|
// PAD_TOKEN never contends over credentials.json.
|
|
func TestNewClientFromURL_EnvTokenLeavesStoreUntouched(t *testing.T) {
|
|
writeStoreForClientTest(t)
|
|
t.Setenv("PAD_TOKEN", "pad_envtoken")
|
|
|
|
home, _ := os.UserHomeDir()
|
|
path := filepath.Join(home, ".pad", "credentials.json")
|
|
before, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read before: %v", err)
|
|
}
|
|
_ = NewClientFromURL("http://127.0.0.1:7777")
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read after: %v", err)
|
|
}
|
|
if string(before) != string(after) {
|
|
t.Error("credentials.json changed during client construction under PAD_TOKEN")
|
|
}
|
|
}
|