Skip to content

Commit ea9ec8c

Browse files
fix(cloud): fall back to cloud.json token when ENGRAM_CLOUD_TOKEN is unset (#430)
resolveCloudRuntimeConfig was zeroing cc.Token unconditionally before reading the env var, so any token persisted in cloud.json was silently discarded. Users running `engram sync --cloud` without ENGRAM_CLOUD_TOKEN exported always received a 401 even when cloud.json held a valid token. Remove the blanket zero assignment so the file token acts as a fallback when the env var is absent. ENGRAM_CLOUD_TOKEN still takes precedence when set. Update the test that locked in the old behaviour and add three new tests that assert the fallback and the end-to-end Authorization header. Closes #343
1 parent 6868287 commit ea9ec8c

3 files changed

Lines changed: 132 additions & 7 deletions

File tree

cmd/engram/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,10 @@ func resolveCloudRuntimeConfig(cfg store.Config) (*cloudConfig, error) {
446446
if cc == nil {
447447
cc = &cloudConfig{}
448448
}
449-
// Legacy persisted tokens in cloud.json are intentionally ignored at runtime.
450-
// Runtime auth must come from ENGRAM_CLOUD_TOKEN.
451-
cc.Token = ""
449+
// ENGRAM_CLOUD_TOKEN overrides any token stored in cloud.json.
450+
// When the env var is absent, the persisted token from cloud.json is used
451+
// as a fallback so that `engram sync --cloud` works without requiring the
452+
// env var to be set in every shell session (fix for issue #343).
452453
if v := strings.TrimSpace(os.Getenv("ENGRAM_CLOUD_SERVER")); v != "" {
453454
cc.ServerURL = v
454455
}

cmd/engram/main_extra_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,9 +1603,12 @@ func TestResolveCloudRuntimeConfigReturnsErrorWhenPersistedConfigUnreadable(t *t
16031603
}
16041604
}
16051605

1606-
func TestResolveCloudRuntimeConfigIgnoresPersistedTokenWithoutEnvOverride(t *testing.T) {
1606+
func TestResolveCloudRuntimeConfigUsesPersistedTokenAsFallback(t *testing.T) {
1607+
// Issue #343: when ENGRAM_CLOUD_TOKEN is not set, the token stored in
1608+
// cloud.json must be used so that `engram sync --cloud` works without
1609+
// requiring users to export the env var in every shell session.
16071610
cfg := testConfig(t)
1608-
if err := saveCloudConfig(cfg, &cloudConfig{ServerURL: "https://cloud.example.test", Token: "legacy-token"}); err != nil {
1611+
if err := saveCloudConfig(cfg, &cloudConfig{ServerURL: "https://cloud.example.test", Token: "file-token"}); err != nil {
16091612
t.Fatalf("save cloud config: %v", err)
16101613
}
16111614
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
@@ -1617,8 +1620,8 @@ func TestResolveCloudRuntimeConfigIgnoresPersistedTokenWithoutEnvOverride(t *tes
16171620
if runtimeCfg == nil {
16181621
t.Fatal("expected non-nil cloud runtime config")
16191622
}
1620-
if runtimeCfg.Token != "" {
1621-
t.Fatalf("expected persisted legacy token to be ignored, got %q", runtimeCfg.Token)
1623+
if runtimeCfg.Token != "file-token" {
1624+
t.Fatalf("expected persisted token %q as fallback, got %q", "file-token", runtimeCfg.Token)
16221625
}
16231626
if runtimeCfg.ServerURL != "https://cloud.example.test" {
16241627
t.Fatalf("expected server URL to remain available, got %q", runtimeCfg.ServerURL)

cmd/engram/sync_cloud_auth_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/Gentleman-Programming/engram/internal/store"
10+
)
11+
12+
// TestResolveCloudRuntimeConfigFallsBackToFileToken asserts that
13+
// resolveCloudRuntimeConfig uses the token stored in cloud.json when
14+
// ENGRAM_CLOUD_TOKEN is not set in the environment (issue #343).
15+
func TestResolveCloudRuntimeConfigFallsBackToFileToken(t *testing.T) {
16+
cfg := testConfig(t)
17+
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
18+
t.Setenv("ENGRAM_CLOUD_SERVER", "")
19+
20+
const fileToken = "file-token-from-cloud-json"
21+
if err := saveCloudConfig(cfg, &cloudConfig{
22+
ServerURL: "https://cloud.example.test",
23+
Token: fileToken,
24+
}); err != nil {
25+
t.Fatalf("save cloud config: %v", err)
26+
}
27+
28+
cc, err := resolveCloudRuntimeConfig(cfg)
29+
if err != nil {
30+
t.Fatalf("resolveCloudRuntimeConfig: %v", err)
31+
}
32+
if cc.Token != fileToken {
33+
t.Fatalf("expected token %q from cloud.json fallback, got %q (ENGRAM_CLOUD_TOKEN not set)", fileToken, cc.Token)
34+
}
35+
}
36+
37+
// TestResolveCloudRuntimeConfigEnvTokenTakesPrecedence asserts that when both
38+
// ENGRAM_CLOUD_TOKEN and a token in cloud.json are present, the env var wins.
39+
func TestResolveCloudRuntimeConfigEnvTokenTakesPrecedence(t *testing.T) {
40+
cfg := testConfig(t)
41+
const envToken = "env-token"
42+
const fileToken = "file-token"
43+
t.Setenv("ENGRAM_CLOUD_TOKEN", envToken)
44+
t.Setenv("ENGRAM_CLOUD_SERVER", "")
45+
46+
if err := saveCloudConfig(cfg, &cloudConfig{
47+
ServerURL: "https://cloud.example.test",
48+
Token: fileToken,
49+
}); err != nil {
50+
t.Fatalf("save cloud config: %v", err)
51+
}
52+
53+
cc, err := resolveCloudRuntimeConfig(cfg)
54+
if err != nil {
55+
t.Fatalf("resolveCloudRuntimeConfig: %v", err)
56+
}
57+
if cc.Token != envToken {
58+
t.Fatalf("expected env token %q to take precedence over file token %q, got %q", envToken, fileToken, cc.Token)
59+
}
60+
}
61+
62+
// TestSyncCloudSendsAuthorizationHeaderFromFileToken is an end-to-end test that
63+
// verifies sync --cloud attaches the Authorization: Bearer header when the token
64+
// is sourced from cloud.json and ENGRAM_CLOUD_TOKEN is not set (issue #343).
65+
func TestSyncCloudSendsAuthorizationHeaderFromFileToken(t *testing.T) {
66+
stubExitWithPanic(t)
67+
stubRuntimeHooks(t)
68+
69+
const fileToken = "secret-file-token"
70+
71+
var gotAuth string
72+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73+
gotAuth = r.Header.Get("Authorization")
74+
switch {
75+
case r.Method == http.MethodGet && r.URL.Path == "/sync/pull":
76+
w.Header().Set("Content-Type", "application/json")
77+
_, _ = w.Write([]byte(`{"version":1,"chunks":[]}`))
78+
case r.Method == http.MethodPost && r.URL.Path == "/sync/push":
79+
w.WriteHeader(http.StatusOK)
80+
_, _ = w.Write([]byte(`{"status":"ok"}`))
81+
default:
82+
http.NotFound(w, r)
83+
}
84+
}))
85+
defer srv.Close()
86+
87+
cfg := testConfig(t)
88+
89+
// Persist token in cloud.json; do NOT set ENGRAM_CLOUD_TOKEN.
90+
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
91+
t.Setenv("ENGRAM_CLOUD_SERVER", "")
92+
93+
if err := saveCloudConfig(cfg, &cloudConfig{
94+
ServerURL: srv.URL,
95+
Token: fileToken,
96+
}); err != nil {
97+
t.Fatalf("save cloud config: %v", err)
98+
}
99+
100+
s, err := store.New(cfg)
101+
if err != nil {
102+
t.Fatalf("open store: %v", err)
103+
}
104+
if err := s.EnrollProject("demo"); err != nil {
105+
_ = s.Close()
106+
t.Fatalf("enroll project: %v", err)
107+
}
108+
_ = s.Close()
109+
110+
withArgs(t, "engram", "sync", "--cloud", "--project", "demo")
111+
_, _, recovered := captureOutputAndRecover(t, func() { cmdSync(cfg) })
112+
113+
if _, ok := recovered.(exitCode); ok {
114+
t.Fatal("sync --cloud fataled; expected success with file token")
115+
}
116+
117+
wantAuth := "Bearer " + fileToken
118+
if !strings.EqualFold(gotAuth, wantAuth) {
119+
t.Fatalf("expected Authorization header %q, got %q (file token not forwarded)", wantAuth, gotAuth)
120+
}
121+
}

0 commit comments

Comments
 (0)