package shared import ( "errors" "os" "path/filepath" "runtime" "testing" "time" ) func TestTokenPathSimpleAndSlash(t *testing.T) { base := t.TempDir() t.Setenv("XDG_CACHE_HOME", base) cases := map[string]string{ "sydney": filepath.Join(base, appDir, "sydney"), "staging/sydney": filepath.Join(base, appDir, "staging", "sydney"), "a/b/c": filepath.Join(base, appDir, "a", "b", "c"), } for ctx, want := range cases { got, err := TokenPath(ctx) if err != nil { t.Fatalf("TokenPath(%q): %v", ctx, err) } if got != want { t.Errorf("TokenPath(%q) = %q, want %q", ctx, got, want) } } } func TestTokenPathRejectsTraversal(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) bad := []string{"", "/etc/passwd", "../escape", "a/../../b", "a//b", "foo/", "./x"} for _, ctx := range bad { if _, err := TokenPath(ctx); err == nil { t.Errorf("TokenPath(%q) expected error, got nil", ctx) } } } func TestSaveLoadRoundTripSlashContext(t *testing.T) { base := t.TempDir() t.Setenv("XDG_CACHE_HOME", base) tok := &Token{ Context: "staging/sydney", Address: "https://vault-staging.syd1.au.unkin.net", Namespace: "staging", Token: "s.abcdef123456", Accessor: "acc-123", Policies: []string{"default", "kv-read"}, Renewable: true, LeaseDurationSeconds: 3600, IssuedAt: time.Now().UTC().Truncate(time.Second), ExpiresAt: time.Now().UTC().Add(time.Hour).Truncate(time.Second), } if err := SaveToken(tok); err != nil { t.Fatalf("SaveToken: %v", err) } // Parent dir for a slash context must be created 0700, file 0600. path, _ := TokenPath("staging/sydney") if runtime.GOOS != "windows" { fi, err := os.Stat(path) if err != nil { t.Fatal(err) } if perm := fi.Mode().Perm(); perm != 0o600 { t.Errorf("token file perm = %o, want 600", perm) } di, err := os.Stat(filepath.Dir(path)) if err != nil { t.Fatal(err) } if perm := di.Mode().Perm(); perm != 0o700 { t.Errorf("token dir perm = %o, want 700", perm) } } got, err := LoadToken("staging/sydney") if err != nil { t.Fatalf("LoadToken: %v", err) } if got.Token != tok.Token || got.Accessor != tok.Accessor || got.Namespace != tok.Namespace { t.Errorf("round-trip mismatch: %+v vs %+v", got, tok) } if len(got.Policies) != 2 || got.Policies[1] != "kv-read" { t.Errorf("policies mismatch: %v", got.Policies) } if !got.Renewable { t.Error("renewable lost in round-trip") } } func TestLoadTokenMissingIsNotExist(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) _, err := LoadToken("never-logged-in") if err == nil { t.Fatal("expected error for missing token") } if !errors.Is(err, os.ErrNotExist) { t.Errorf("error should wrap os.ErrNotExist, got %v", err) } } func TestDeleteToken(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) tok := &Token{Context: "sydney", Address: "https://v", Token: "s.x"} if err := SaveToken(tok); err != nil { t.Fatal(err) } if err := DeleteToken("sydney"); err != nil { t.Fatalf("DeleteToken: %v", err) } // Deleting again is a no-op (not-exist tolerated). if err := DeleteToken("sydney"); err != nil { t.Errorf("second DeleteToken: %v", err) } }