Skip to content

Commit

Permalink
uitest: Add system for unit testing widgets (#534)
Browse files Browse the repository at this point in the history
Adds a testscript-based system for testing terminal widgets.
This is a generalized, in-memory, Windows-friendly version of with-term
intended for use in unit tests instead of end-to-end tests.
It uses testscript instead of a the custom commands format.

With this in place, we're able to unit test a bunch of the terminal
interactions that were previously untested,
or became untested following RobotView in #480.
  • Loading branch information
abhinav authored Dec 27, 2024
1 parent 00559e3 commit b33a07a
Show file tree
Hide file tree
Showing 49 changed files with 2,062 additions and 208 deletions.
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ issues:
- linters: [revive]
text: 'empty-block: this block is empty, you can remove it'

- linters: [musttag]
path: _test.go$
99 changes: 22 additions & 77 deletions internal/forge/github/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import (
"net/http/httptest"
"net/url"
"os/exec"
"reflect"
"strings"
"testing"
"time"

"github.com/charmbracelet/log"
"github.com/rogpeppe/go-internal/testscript"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.abhg.dev/gs/internal/forge"
"go.abhg.dev/gs/internal/secret"
"go.abhg.dev/gs/internal/ui"
"go.abhg.dev/gs/internal/ui/uitest"
Expand Down Expand Up @@ -188,90 +189,34 @@ func TestDeviceFlowAuthenticator(t *testing.T) {
}

func TestSelectAuthenticator(t *testing.T) {
view := uitest.NewEmulatorView(&uitest.EmulatorViewOptions{
Rows: 40,
})

type result struct {
auth authenticator
err error
}
resultc := make(chan result, 1)
go func() {
defer close(resultc)
uitest.RunScripts(t, func(t testing.TB, ts *testscript.TestScript, view ui.InteractiveView) {
wantType := strings.TrimSpace(ts.ReadFile("want_type"))

got, err := selectAuthenticator(view, authenticatorOptions{
auth, err := selectAuthenticator(view, authenticatorOptions{
Endpoint: oauth2.Endpoint{},
})
resultc <- result{got, err}
}()

// TODO: Generalize termtest and use that here
require.EventuallyWithT(t, func(t *assert.CollectT) {
assert.Contains(t,
view.Screen(),
"Select an authentication method",
)
}, time.Second, 50*time.Millisecond)

// Go through all options, roll back around to the first, and select it
for range _authenticationMethods {
require.NoError(t, view.FeedKeys("\x1b[B")) // Down arrow
}
require.NoError(t, view.FeedKeys("\r")) // Enter

select {
case res, ok := <-resultc:
require.True(t, ok)
auth, err := res.auth, res.err
require.NoError(t, err)

_, ok = auth.(*DeviceFlowAuthenticator)
require.True(t, ok, "want *github.DeviceFlowAuthenticator, got %T", auth)

case <-time.After(time.Second):
t.Fatal("timed out")
}
assert.Equal(t, wantType, reflect.TypeOf(auth).String())
}, &uitest.RunScriptsOptions{
Update: *UpdateFixtures,
Rows: 80,
}, "testdata/auth/select")
}

func TestPATAuthenticator(t *testing.T) {
view := uitest.NewEmulatorView(nil)
func TestAuthenticationFlow_PAT(t *testing.T) {
uitest.RunScripts(t, func(t testing.TB, ts *testscript.TestScript, view ui.InteractiveView) {
wantToken := strings.TrimSpace(ts.ReadFile("want_token"))

type result struct {
tok forge.AuthenticationToken
err error
}
resultc := make(chan result, 1)
go func() {
defer close(resultc)

got, err := (&PATAuthenticator{}).Authenticate(context.Background(), view)
resultc <- result{got, err}
}()

// TODO: Generalize termtest and use that here
require.EventuallyWithT(t, func(t *assert.CollectT) {
assert.Contains(t,
view.Screen(),
"Enter Personal Access Token",
)
}, time.Second, 50*time.Millisecond)

require.NoError(t, view.FeedKeys("token\r"))

select {
case res, ok := <-resultc:
require.True(t, ok)
tok, err := res.tok, res.err
got, err := new(Forge).AuthenticationFlow(context.Background(), view)
require.NoError(t, err)

ght, ok := tok.(*AuthenticationToken)
require.True(t, ok, "want *github.AuthenticationToken, got %T", tok)
assert.Equal(t, "token", ght.AccessToken)

case <-time.After(time.Second):
t.Fatal("timed out")
}
assert.Equal(t, &AuthenticationToken{
AccessToken: wantToken,
}, got)
}, &uitest.RunScriptsOptions{
Update: *UpdateFixtures,
Rows: 80,
}, "testdata/auth/pat.txt")
}

func TestAuthCLI(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions internal/forge/github/flag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package github

import "flag"

var UpdateFixtures = flag.Bool("update", false, "update test fixtures")
3 changes: 1 addition & 2 deletions internal/forge/github/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"crypto/rand"
"flag"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -33,7 +32,7 @@ import (
// using recorded fixtures.

var (
_update = flag.Bool("update", false, "update test fixtures")
_update = github.UpdateFixtures
_fixtures = fixturetest.Config{Update: _update}
)

Expand Down
64 changes: 64 additions & 0 deletions internal/forge/github/testdata/auth/pat.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
init

await Select an authentication method
feed -r 3 <Down>
snapshot
await
snapshot
cmp stdout select
feed <Enter>

await
snapshot
cmp stdout prompt

feed secret
await
snapshot
cmp stdout filled

feed <Enter>

-- want_token --
secret
-- select --
Select an authentication method:
OAuth
Authorize git-spice to act on your behalf from this device only.
git-spice will get access to all repositories: public and private.
For private repositories, you will need to request installation from a
repository owner.

OAuth: Public repositories only
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to public repositories.

GitHub App
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to repositories where the git-spice GitHub
App is installed explicitly.
Use https://github.com/apps/git-spice to install the App on repositories.
For private repositories, you will need to request installation from a
repository owner.

▶ Personal Access Token
Enter a classic or fine-grained Personal Access Token generated from
https://github.com/settings/tokens.
Classic tokens need at least one of the following scopes: repo or
public_repo.
Fine-grained tokens need read/write access to Repository Contents and Pull
requests.
You can use this method if you do not have the ability to install a GitHub
or OAuth App on your repositories.

GitHub CLI
Re-use an existing GitHub CLI (https://cli.github.com) session.
You must be logged into gh with 'gh auth login' for this to work.
You can use this if you're just experimenting and don't want to set up a
token yet.
-- prompt --
Select an authentication method: Personal Access Token
Enter Personal Access Token:
-- filled --
Select an authentication method: Personal Access Token
Enter Personal Access Token: secret
Empty file.
91 changes: 91 additions & 0 deletions internal/forge/github/testdata/auth/select/oauth.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
init

await Select an authentication method
snapshot
cmp stdout prompt

# go through the list of options and roll back
feed -r 3 <Down>
await
snapshot
cmp stdout down

feed -r 2 <Down>
await
snapshot
cmp stdout prompt

feed <Enter>

-- want_type --
*github.DeviceFlowAuthenticator
-- prompt --
Select an authentication method:
▶ OAuth
Authorize git-spice to act on your behalf from this device only.
git-spice will get access to all repositories: public and private.
For private repositories, you will need to request installation from a
repository owner.

OAuth: Public repositories only
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to public repositories.

GitHub App
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to repositories where the git-spice GitHub
App is installed explicitly.
Use https://github.com/apps/git-spice to install the App on repositories.
For private repositories, you will need to request installation from a
repository owner.

Personal Access Token
Enter a classic or fine-grained Personal Access Token generated from
https://github.com/settings/tokens.
Classic tokens need at least one of the following scopes: repo or
public_repo.
Fine-grained tokens need read/write access to Repository Contents and Pull
requests.
You can use this method if you do not have the ability to install a GitHub
or OAuth App on your repositories.

GitHub CLI
Re-use an existing GitHub CLI (https://cli.github.com) session.
You must be logged into gh with 'gh auth login' for this to work.
You can use this if you're just experimenting and don't want to set up a
token yet.
-- down --
Select an authentication method:
OAuth
Authorize git-spice to act on your behalf from this device only.
git-spice will get access to all repositories: public and private.
For private repositories, you will need to request installation from a
repository owner.

OAuth: Public repositories only
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to public repositories.

GitHub App
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to repositories where the git-spice GitHub
App is installed explicitly.
Use https://github.com/apps/git-spice to install the App on repositories.
For private repositories, you will need to request installation from a
repository owner.

▶ Personal Access Token
Enter a classic or fine-grained Personal Access Token generated from
https://github.com/settings/tokens.
Classic tokens need at least one of the following scopes: repo or
public_repo.
Fine-grained tokens need read/write access to Repository Contents and Pull
requests.
You can use this method if you do not have the ability to install a GitHub
or OAuth App on your repositories.

GitHub CLI
Re-use an existing GitHub CLI (https://cli.github.com) session.
You must be logged into gh with 'gh auth login' for this to work.
You can use this if you're just experimenting and don't want to set up a
token yet.
85 changes: 85 additions & 0 deletions internal/forge/github/testdata/auth/select/oauth_public.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
init

await Select an authentication method
snapshot
cmp stdout prompt

feed <Down>
await
snapshot
cmp stdout select

feed <Enter>

-- want_type --
*github.DeviceFlowAuthenticator
-- prompt --
Select an authentication method:
▶ OAuth
Authorize git-spice to act on your behalf from this device only.
git-spice will get access to all repositories: public and private.
For private repositories, you will need to request installation from a
repository owner.

OAuth: Public repositories only
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to public repositories.

GitHub App
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to repositories where the git-spice GitHub
App is installed explicitly.
Use https://github.com/apps/git-spice to install the App on repositories.
For private repositories, you will need to request installation from a
repository owner.

Personal Access Token
Enter a classic or fine-grained Personal Access Token generated from
https://github.com/settings/tokens.
Classic tokens need at least one of the following scopes: repo or
public_repo.
Fine-grained tokens need read/write access to Repository Contents and Pull
requests.
You can use this method if you do not have the ability to install a GitHub
or OAuth App on your repositories.

GitHub CLI
Re-use an existing GitHub CLI (https://cli.github.com) session.
You must be logged into gh with 'gh auth login' for this to work.
You can use this if you're just experimenting and don't want to set up a
token yet.
-- select --
Select an authentication method:
OAuth
Authorize git-spice to act on your behalf from this device only.
git-spice will get access to all repositories: public and private.
For private repositories, you will need to request installation from a
repository owner.

▶ OAuth: Public repositories only
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to public repositories.

GitHub App
Authorize git-spice to act on your behalf from this device only.
git-spice will only get access to repositories where the git-spice GitHub
App is installed explicitly.
Use https://github.com/apps/git-spice to install the App on repositories.
For private repositories, you will need to request installation from a
repository owner.

Personal Access Token
Enter a classic or fine-grained Personal Access Token generated from
https://github.com/settings/tokens.
Classic tokens need at least one of the following scopes: repo or
public_repo.
Fine-grained tokens need read/write access to Repository Contents and Pull
requests.
You can use this method if you do not have the ability to install a GitHub
or OAuth App on your repositories.

GitHub CLI
Re-use an existing GitHub CLI (https://cli.github.com) session.
You must be logged into gh with 'gh auth login' for this to work.
You can use this if you're just experimenting and don't want to set up a
token yet.
Loading

0 comments on commit b33a07a

Please sign in to comment.