detached 70103da

test: cover version-check failures and revoking a branch-limited all-scope token

token create --branch-prefix refuses when GetVersion is unavailable; the cases
are a struct table with the expected message. Revoking an all-scope token with
branch prefixes does not bump the session epoch. Drops the redundant
HasOwnerRights subtest.
commit
70103dad5b0c89e72e649e4618c52b6483406110
author
brice · · 2026-10-12 16:14 UTC
parent
8d436fd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +20 -18
M internal/app/cli_token_test.go +19 -12
@@ -3,9 +3,12 @@
33 package app
44
55 import (
6+ "errors"
7+ "strings"
68 "testing"
79 "time"
810
11+ "connectrpc.com/connect"
912 "github.com/stretchr/testify/assert"
1013 "github.com/stretchr/testify/require"
1114
@@ -96,22 +99,26 @@ func TestTokenCreateBranchPrefixes(t *testing.T) {
9699 })
97100
98101 t.Run("a server without the feature is asked nothing", func(t *testing.T) {
99- for name, version := range map[string]func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error){
100- "feature missing": func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
102+ for _, tt := range []struct {
103+ name string
104+ version func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error)
105+ wantMsg string
106+ }{
107+ {"feature missing", func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
101108 return &ermite.GetVersionResponse{Version: "v0.3.0", Features: []string{"repo.update", "session"}}, nil
102- },
103- "version unreadable": nil, // the fake answers unimplemented
109+ }, "does not support token.branch_prefixes"},
110+ {"version unreadable", nil, "does not implement this call"}, // the fake answers unimplemented
111+ {"server unavailable", func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
112+ return nil, connect.NewError(connect.CodeUnavailable, errors.New("connection refused"))
113+ }, "cannot reach the server"},
104114 } {
105- f := &fakeAPI{createToken: echo, getVersion: version}
115+ f := &fakeAPI{createToken: echo, getVersion: tt.version}
106116 res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--branch-prefix", "agent/x/")
107- require.Error(t, res.err, name)
108- if name == "feature missing" {
109- assert.Contains(t, res.err.Error(), "token.branch_prefixes", name)
110- assert.Contains(t, res.err.Error(), "nothing was created", name)
111- }
117+ require.Error(t, res.err, tt.name)
118+ assert.Contains(t, strings.ToLower(res.err.Error()), tt.wantMsg, tt.name)
112119 calls, _ := f.snapshot()
113- assert.NotContains(t, calls, "CreateToken", "%s: no token is created", name)
114- assert.Empty(t, res.stdout, name)
120+ assert.NotContains(t, calls, "CreateToken", "%s: no token is created", tt.name)
121+ assert.Empty(t, res.stdout, tt.name)
115122 }
116123 })
117124
@@ -3,9 +3,12 @@
3 package app3 package app
4 4
5 import (5 import (
6+ "errors"
7+ "strings"
6 "testing"8 "testing"
7 "time"9 "time"
8 10
11+ "connectrpc.com/connect"
9 "github.com/stretchr/testify/assert"12 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
11 14
@@ -96,22 +99,26 @@ func TestTokenCreateBranchPrefixes(t *testing.T) {
96 })99 })
97 100
98 t.Run("a server without the feature is asked nothing", func(t *testing.T) {101 t.Run("a server without the feature is asked nothing", func(t *testing.T) {
99- for name, version := range map[string]func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error){102+ for _, tt := range []struct {
100- "feature missing": func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {103+ name string
104+ version func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error)
105+ wantMsg string
106+ }{
107+ {"feature missing", func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
101 return &ermite.GetVersionResponse{Version: "v0.3.0", Features: []string{"repo.update", "session"}}, nil108 return &ermite.GetVersionResponse{Version: "v0.3.0", Features: []string{"repo.update", "session"}}, nil
102- },109+ }, "does not support token.branch_prefixes"},
103- "version unreadable": nil, // the fake answers unimplemented110+ {"version unreadable", nil, "does not implement this call"}, // the fake answers unimplemented
111+ {"server unavailable", func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
112+ return nil, connect.NewError(connect.CodeUnavailable, errors.New("connection refused"))
113+ }, "cannot reach the server"},
104 } {114 } {
105- f := &fakeAPI{createToken: echo, getVersion: version}115+ f := &fakeAPI{createToken: echo, getVersion: tt.version}
106 res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--branch-prefix", "agent/x/")116 res := runCLI(t, envFor(f.serve(t)), "", "token", "create", "x", "--preset", "agent", "--branch-prefix", "agent/x/")
107- require.Error(t, res.err, name)117+ require.Error(t, res.err, tt.name)
108- if name == "feature missing" {118+ assert.Contains(t, strings.ToLower(res.err.Error()), tt.wantMsg, tt.name)
109- assert.Contains(t, res.err.Error(), "token.branch_prefixes", name)
110- assert.Contains(t, res.err.Error(), "nothing was created", name)
111- }
112 calls, _ := f.snapshot()119 calls, _ := f.snapshot()
113- assert.NotContains(t, calls, "CreateToken", "%s: no token is created", name)120+ assert.NotContains(t, calls, "CreateToken", "%s: no token is created", tt.name)
114- assert.Empty(t, res.stdout, name)121+ assert.Empty(t, res.stdout, tt.name)
115 }122 }
116 })123 })
117 124
M internal/usecase/tokens_test.go +1 -6
@@ -307,6 +307,7 @@ func TestTokensRevokeOwnerRightsEndsWebSessions(t *testing.T) {
307307 "every scope, by an owner key": {ownerTok, domain.TokenPrincipal(domain.Token{ID: "adm", Scopes: domain.AllScopes()}), true},
308308 "one scope short": {domain.Token{ID: "p", Scopes: domain.AllScopes()[1:]}, domain.Root(), false},
309309 "repo list": {domain.Token{ID: "l", Scopes: domain.AllScopes(), Repos: names(t, "a")}, domain.Root(), false},
310+ "branch prefixes": {domain.Token{ID: "b", Scopes: domain.AllScopes(), BranchPrefixes: []string{"agent/"}}, domain.Root(), false},
310311 } {
311312 t.Run(name, func(t *testing.T) {
312313 t.Parallel()
@@ -614,12 +615,6 @@ func TestTokensCreateBranchPrefixes(t *testing.T) {
614615 }
615616 })
616617
617- t.Run("a token with every scope but limited branches has no owner rights", func(t *testing.T) {
618- t.Parallel()
619- p := domain.TokenPrincipal(domain.Token{ID: "t", Scopes: domain.AllScopes(), BranchPrefixes: []string{"a/"}})
620- assert.False(t, p.HasOwnerRights())
621- })
622-
623618 t.Run("a corrupt empty creator prefix does not cover every child prefix", func(t *testing.T) {
624619 t.Parallel()
625620 f := newTokenFixture(t) // the store expects no Create
@@ -307,6 +307,7 @@ func TestTokensRevokeOwnerRightsEndsWebSessions(t *testing.T) {
307 "every scope, by an owner key": {ownerTok, domain.TokenPrincipal(domain.Token{ID: "adm", Scopes: domain.AllScopes()}), true},307 "every scope, by an owner key": {ownerTok, domain.TokenPrincipal(domain.Token{ID: "adm", Scopes: domain.AllScopes()}), true},
308 "one scope short": {domain.Token{ID: "p", Scopes: domain.AllScopes()[1:]}, domain.Root(), false},308 "one scope short": {domain.Token{ID: "p", Scopes: domain.AllScopes()[1:]}, domain.Root(), false},
309 "repo list": {domain.Token{ID: "l", Scopes: domain.AllScopes(), Repos: names(t, "a")}, domain.Root(), false},309 "repo list": {domain.Token{ID: "l", Scopes: domain.AllScopes(), Repos: names(t, "a")}, domain.Root(), false},
310+ "branch prefixes": {domain.Token{ID: "b", Scopes: domain.AllScopes(), BranchPrefixes: []string{"agent/"}}, domain.Root(), false},
310 } {311 } {
311 t.Run(name, func(t *testing.T) {312 t.Run(name, func(t *testing.T) {
312 t.Parallel()313 t.Parallel()
@@ -614,12 +615,6 @@ func TestTokensCreateBranchPrefixes(t *testing.T) {
614 }615 }
615 })616 })
616 617
617- t.Run("a token with every scope but limited branches has no owner rights", func(t *testing.T) {
618- t.Parallel()
619- p := domain.TokenPrincipal(domain.Token{ID: "t", Scopes: domain.AllScopes(), BranchPrefixes: []string{"a/"}})
620- assert.False(t, p.HasOwnerRights())
621- })
622-
623 t.Run("a corrupt empty creator prefix does not cover every child prefix", func(t *testing.T) {618 t.Run("a corrupt empty creator prefix does not cover every child prefix", func(t *testing.T) {
624 t.Parallel()619 t.Parallel()
625 f := newTokenFixture(t) // the store expects no Create620 f := newTokenFixture(t) // the store expects no Create

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close