Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cmd/engram/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,10 @@ func resolveCloudRuntimeConfig(cfg store.Config) (*cloudConfig, error) {
if cc == nil {
cc = &cloudConfig{}
}
// Legacy persisted tokens in cloud.json are intentionally ignored at runtime.
// Runtime auth must come from ENGRAM_CLOUD_TOKEN.
cc.Token = ""
// ENGRAM_CLOUD_TOKEN overrides any token stored in cloud.json.
// When the env var is absent, the persisted token from cloud.json is used
// as a fallback so that `engram sync --cloud` works without requiring the
// env var to be set in every shell session (fix for issue #343).
if v := strings.TrimSpace(os.Getenv("ENGRAM_CLOUD_SERVER")); v != "" {
cc.ServerURL = v
}
Expand Down
11 changes: 7 additions & 4 deletions cmd/engram/main_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1603,9 +1603,12 @@ func TestResolveCloudRuntimeConfigReturnsErrorWhenPersistedConfigUnreadable(t *t
}
}

func TestResolveCloudRuntimeConfigIgnoresPersistedTokenWithoutEnvOverride(t *testing.T) {
func TestResolveCloudRuntimeConfigUsesPersistedTokenAsFallback(t *testing.T) {
// Issue #343: when ENGRAM_CLOUD_TOKEN is not set, the token stored in
// cloud.json must be used so that `engram sync --cloud` works without
// requiring users to export the env var in every shell session.
cfg := testConfig(t)
if err := saveCloudConfig(cfg, &cloudConfig{ServerURL: "https://cloud.example.test", Token: "legacy-token"}); err != nil {
if err := saveCloudConfig(cfg, &cloudConfig{ServerURL: "https://cloud.example.test", Token: "file-token"}); err != nil {
t.Fatalf("save cloud config: %v", err)
}
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
Expand All @@ -1617,8 +1620,8 @@ func TestResolveCloudRuntimeConfigIgnoresPersistedTokenWithoutEnvOverride(t *tes
if runtimeCfg == nil {
t.Fatal("expected non-nil cloud runtime config")
}
if runtimeCfg.Token != "" {
t.Fatalf("expected persisted legacy token to be ignored, got %q", runtimeCfg.Token)
if runtimeCfg.Token != "file-token" {
t.Fatalf("expected persisted token %q as fallback, got %q", "file-token", runtimeCfg.Token)
}
if runtimeCfg.ServerURL != "https://cloud.example.test" {
t.Fatalf("expected server URL to remain available, got %q", runtimeCfg.ServerURL)
Expand Down
121 changes: 121 additions & 0 deletions cmd/engram/sync_cloud_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/Gentleman-Programming/engram/internal/store"
)

// TestResolveCloudRuntimeConfigFallsBackToFileToken asserts that
// resolveCloudRuntimeConfig uses the token stored in cloud.json when
// ENGRAM_CLOUD_TOKEN is not set in the environment (issue #343).
func TestResolveCloudRuntimeConfigFallsBackToFileToken(t *testing.T) {
cfg := testConfig(t)
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
t.Setenv("ENGRAM_CLOUD_SERVER", "")

const fileToken = "file-token-from-cloud-json"
if err := saveCloudConfig(cfg, &cloudConfig{
ServerURL: "https://cloud.example.test",
Token: fileToken,
}); err != nil {
t.Fatalf("save cloud config: %v", err)
}

cc, err := resolveCloudRuntimeConfig(cfg)
if err != nil {
t.Fatalf("resolveCloudRuntimeConfig: %v", err)
}
if cc.Token != fileToken {
t.Fatalf("expected token %q from cloud.json fallback, got %q (ENGRAM_CLOUD_TOKEN not set)", fileToken, cc.Token)
}
}

// TestResolveCloudRuntimeConfigEnvTokenTakesPrecedence asserts that when both
// ENGRAM_CLOUD_TOKEN and a token in cloud.json are present, the env var wins.
func TestResolveCloudRuntimeConfigEnvTokenTakesPrecedence(t *testing.T) {
cfg := testConfig(t)
const envToken = "env-token"
const fileToken = "file-token"
t.Setenv("ENGRAM_CLOUD_TOKEN", envToken)
t.Setenv("ENGRAM_CLOUD_SERVER", "")

if err := saveCloudConfig(cfg, &cloudConfig{
ServerURL: "https://cloud.example.test",
Token: fileToken,
}); err != nil {
t.Fatalf("save cloud config: %v", err)
}

cc, err := resolveCloudRuntimeConfig(cfg)
if err != nil {
t.Fatalf("resolveCloudRuntimeConfig: %v", err)
}
if cc.Token != envToken {
t.Fatalf("expected env token %q to take precedence over file token %q, got %q", envToken, fileToken, cc.Token)
}
}

// TestSyncCloudSendsAuthorizationHeaderFromFileToken is an end-to-end test that
// verifies sync --cloud attaches the Authorization: Bearer header when the token
// is sourced from cloud.json and ENGRAM_CLOUD_TOKEN is not set (issue #343).
func TestSyncCloudSendsAuthorizationHeaderFromFileToken(t *testing.T) {
stubExitWithPanic(t)
stubRuntimeHooks(t)

const fileToken = "secret-file-token"

var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
switch {
case r.Method == http.MethodGet && r.URL.Path == "/sync/pull":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":1,"chunks":[]}`))
case r.Method == http.MethodPost && r.URL.Path == "/sync/push":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()

cfg := testConfig(t)

// Persist token in cloud.json; do NOT set ENGRAM_CLOUD_TOKEN.
t.Setenv("ENGRAM_CLOUD_TOKEN", "")
t.Setenv("ENGRAM_CLOUD_SERVER", "")

if err := saveCloudConfig(cfg, &cloudConfig{
ServerURL: srv.URL,
Token: fileToken,
}); err != nil {
t.Fatalf("save cloud config: %v", err)
}

s, err := store.New(cfg)
if err != nil {
t.Fatalf("open store: %v", err)
}
if err := s.EnrollProject("demo"); err != nil {
_ = s.Close()
t.Fatalf("enroll project: %v", err)
}
_ = s.Close()

withArgs(t, "engram", "sync", "--cloud", "--project", "demo")
_, _, recovered := captureOutputAndRecover(t, func() { cmdSync(cfg) })

if _, ok := recovered.(exitCode); ok {
t.Fatal("sync --cloud fataled; expected success with file token")
}

wantAuth := "Bearer " + fileToken
if !strings.EqualFold(gotAuth, wantAuth) {
t.Fatalf("expected Authorization header %q, got %q (file token not forwarded)", wantAuth, gotAuth)
}
}
Loading