diff --git a/internal/api/client.go b/internal/api/client.go index b007933..a5d73df 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -21,6 +21,12 @@ func New(token string) *Client { return &Client{token: token, http: &http.Client{}, baseURL: BaseURL} } +// Token is what this client sends. The panel builds a client once, at start, +// and has to build another when a member signs in from inside it - a client +// still holding the token it started with sends an empty cookie and the +// platform answers `unauthorized`, which reads exactly like a failed login. +func (c *Client) Token() string { return c.token } + func (c *Client) get(path string) ([]byte, error) { base := c.baseURL if base == "" { diff --git a/internal/tui/config_test.go b/internal/tui/config_test.go index bc46680..d8808f4 100644 --- a/internal/tui/config_test.go +++ b/internal/tui/config_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/nxssie/nan-cli/internal/api" @@ -1170,3 +1171,59 @@ func TestSignInKeyIsNotOneThatAlreadyMoves(t *testing.T) { } } } + +// Signing in from the panel wrote the session and left the API client holding +// the token it was built with at start-up, which on a fresh machine is none. +// So it signed a member in and then answered every tab `unauthorized` - which +// reads exactly like the login having failed, and was reported as such. +func TestSigningInGivesTheClientTheNewToken(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("HERMES_HOME", filepath.Join(home, "h")) + + m := newModel(api.New(""), &session.Session{}) + if m.client.Token() != "" { + t.Fatal("this test starts from a client with no token") + } + + // What the flow leaves on disk before the panel is told about it. + if err := session.Save(&session.Session{Token: "a-fresh-session-token"}); err != nil { + t.Fatal(err) + } + + updated, _ := m.Update(signedInMsg{nil}) + after := updated.(model) + + if after.sess.Token != "a-fresh-session-token" { + t.Errorf("the panel did not pick up the session: %q", after.sess.Token) + } + if got := after.client.Token(); got != "a-fresh-session-token" { + t.Errorf("the client still sends %q, so every tab answers unauthorized", got) + } + if after.loginStage != loginOff { + t.Error("the sign-in is still on screen after it succeeded") + } + if len(after.cache) != 0 { + t.Error("the tabs keep the answers they got before signing in") + } +} + +// `e` is published on Home as the way to set the API key, and Home is not +// Setup. It did nothing at all anywhere but Setup - which is the tab you have +// to already be on to know that. +func TestTheKeyForTheKeyWorksFromWhereItIsAdvertised(t *testing.T) { + m := setupModel(t, &session.Session{Token: "t"}) + m.lay = newLayout(90, 30) + m.active = tabIndex(tabHome) + + updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) + after := updated.(model) + + if !after.editingKey { + t.Error("e from Home does nothing, which is where Home tells you to press it") + } + if after.activeID() != tabSetup { + t.Error("e opened the key editor without moving to the tab that shows it") + } +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index d562764..db9dfc0 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -206,6 +206,16 @@ func (m model) Init() tea.Cmd { func (m model) activeID() tabID { return tabDefs[m.active].id } +// Where a tab sits in the bar, for the keys that jump straight to one. +func tabIndex(id tabID) int { + for i, t := range tabDefs { + if t.id == id { + return i + } + } + return 0 +} + // ── update ──────────────────────────────────────────────────────────────────── func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -256,6 +266,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if sess, err := session.Load(); err == nil { m.sess = sess } + // And a client that carries it. The old one was built at start-up with + // whatever token existed then - none - so leaving it in place signed a + // member in and then answered every tab `unauthorized`, which reads + // exactly like the login having failed. + m.client = api.New(m.sess.Token) m.cancelLogin() m.cache = make(map[tabID]any) m.err = nil @@ -420,7 +435,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "e": - if !m.showHelp && m.activeID() == tabSetup { + // From any tab. It used to do nothing at all anywhere but Setup, + // which is the tab you have to already be on to know that - and + // Home tells everyone to press `e` from Home. + if !m.showHelp && m.loginStage == loginOff { + if m.activeID() != tabSetup { + m.active = tabIndex(tabSetup) + m.scrollY = 0 + } m.editingKey = true m.keyInput.SetValue("") m.keyInput.Focus() @@ -2351,7 +2373,7 @@ func (m model) renderSetup(l layout) string { // ── about renderer ─────────────────────────────────────────────────────────── -const Version = "0.1.10" +const Version = "0.1.11" func renderAbout(l layout) string { var b strings.Builder diff --git a/scripts/install.ps1 b/scripts/install.ps1 index f14940a..2d7186f 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -118,7 +118,7 @@ function Get-LatestVersion { could not work out the latest version from the GitHub API it rate limits unauthenticated requests, so this is usually temporary wait a few minutes, or pick a version yourself: - & ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -Version v0.1.10 + & ([scriptblock]::Create((irm https://nan.builders/install.ps1))) -Version v0.1.11 the releases are at https://github.com/$Repo/releases "@ } diff --git a/scripts/install.sh b/scripts/install.sh index 5937628..461aab2 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -62,7 +62,7 @@ require_version() { err "could not work out the latest version from the GitHub API" err "it rate limits unauthenticated requests, so this is usually temporary" err "wait a few minutes, or pick a version yourself:" - printf " VERSION=v0.1.10 curl -fsSL https://nan.builders/install | bash + printf " VERSION=v0.1.11 curl -fsSL https://nan.builders/install | bash " >&2 err "the releases are at https://github.com/$REPO/releases" exit 1