Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add auth support via command #129

Merged
merged 10 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Measurement Commands:
traceroute Run a traceroute test

Additional Commands:
auth Auth commands for the Globalping API
completion Generate the autocompletion script for the specified shell
help Help about any command
history Display the measurement history of your current session
Expand Down
119 changes: 119 additions & 0 deletions cmd/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package cmd

import (
"errors"
"syscall"

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/spf13/cobra"
)

func (r *Root) initAuth() {
authCmd := &cobra.Command{
Use: "auth",
Short: "Authenticate with the Globalping API",
Long: "Authenticate with the Globalping API for higher measurements limits.",
}

loginCmd := &cobra.Command{
RunE: r.RunAuthLogin,
Use: "login",
Short: "Log in to your Globalping account",
Long: `Log in to your Globalping account for higher measurements limits.`,
}
radulucut marked this conversation as resolved.
Show resolved Hide resolved

loginFlags := loginCmd.Flags()
loginFlags.Bool("with-token", false, "authenticate with a token read from stdin instead of the default browser-based flow")

statusCmd := &cobra.Command{
RunE: r.RunAuthStatus,
Use: "status",
Short: "Check the current authentication status",
Long: `Check the current authentication status.`,
}

logoutCmd := &cobra.Command{
RunE: r.RunAuthLogout,
Use: "logout",
Short: "Log out from your Globalping account",
Long: `Log out from your Globalping account.`,
}

authCmd.AddCommand(loginCmd)
authCmd.AddCommand(statusCmd)
authCmd.AddCommand(logoutCmd)

r.Cmd.AddCommand(authCmd)
}

func (r *Root) RunAuthLogin(cmd *cobra.Command, args []string) error {
var err error
withToken := cmd.Flags().Changed("with-token")
if withToken {
return r.loginWithToken()
}
url := r.client.Authorize(func(e error) {
defer func() {
r.cancel <- syscall.SIGINT
}()
if e != nil {
err = e
return
}
r.printer.Println("You are now authenticated")
})
r.printer.Println("Please visit the following URL to authenticate:")
radulucut marked this conversation as resolved.
Show resolved Hide resolved
r.printer.Println(url)
<-r.cancel
return err
}

func (r *Root) RunAuthStatus(cmd *cobra.Command, args []string) error {
res, err := r.client.TokenIntrospection("")
if err != nil {
return err
}
if res.Active {
r.printer.Printf("Logged in as %s.\n", res.Username)
} else {
r.printer.Println("Not logged in.")
}
radulucut marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

func (r *Root) RunAuthLogout(cmd *cobra.Command, args []string) error {
err := r.client.Logout()
if err != nil {
return err
}
r.printer.Println("You are now logged out.")
return nil
}

func (r *Root) loginWithToken() error {
r.printer.Println("Please enter your token:")
token, err := r.printer.ReadPassword()
if err != nil {
return err
}
if token == "" {
return errors.New("empty token")
}
introspection, err := r.client.TokenIntrospection(token)
if err != nil {
return err
}
if !introspection.Active {
return errors.New("invalid token")
}
profile := r.storage.GetProfile()
profile.Token = &globalping.Token{
AccessToken: token,
}
err = r.storage.SaveConfig()
if err != nil {
return errors.New("failed to save token")
}
r.printer.Printf("Logged in as %s.\n", introspection.Username)
return nil
}
134 changes: 134 additions & 0 deletions cmd/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package cmd

import (
"bytes"
"context"
"os"
"syscall"
"testing"

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/jsdelivr/globalping-cli/mocks"
"github.com/jsdelivr/globalping-cli/storage"
"github.com/jsdelivr/globalping-cli/view"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)

func Test_Auth_Login_WithToken(t *testing.T) {
t.Cleanup(sessionCleanup)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

gbMock := mocks.NewMockClient(ctrl)

w := new(bytes.Buffer)
r := new(bytes.Buffer)
r.WriteString("token\n")
printer := view.NewPrinter(r, w, w)
ctx := createDefaultContext("")
_storage := storage.NewLocalStorage(".test_globalping-cli")
defer _storage.Remove()
err := _storage.Init()
if err != nil {
t.Fatal(err)
}
root := NewRoot(printer, ctx, nil, nil, gbMock, nil, _storage)

gbMock.EXPECT().TokenIntrospection("token").Return(&globalping.IntrospectionResponse{
Active: true,
Username: "test",
}, nil)

os.Args = []string{"globalping", "auth", "login", "--with-token"}
err = root.Cmd.ExecuteContext(context.TODO())
assert.NoError(t, err)

assert.Equal(t, `Please enter your token:
Logged in as test.
`, w.String())

profile := _storage.GetProfile()
assert.Equal(t, &storage.Profile{
Token: &globalping.Token{
AccessToken: "token",
},
}, profile)
}

func Test_Auth_Login(t *testing.T) {
t.Cleanup(sessionCleanup)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

gbMock := mocks.NewMockClient(ctrl)

w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("")
root := NewRoot(printer, ctx, nil, nil, gbMock, nil, nil)

gbMock.EXPECT().Authorize(gomock.Any()).Do(func(_ any) {
root.cancel <- syscall.SIGINT
}).Return("http://localhost")

os.Args = []string{"globalping", "auth", "login"}
err := root.Cmd.ExecuteContext(context.TODO())
assert.NoError(t, err)

assert.Equal(t, `Please visit the following URL to authenticate:
http://localhost
`, w.String())
}

func Test_AuthStatus(t *testing.T) {
t.Cleanup(sessionCleanup)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

gbMock := mocks.NewMockClient(ctrl)

w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("")

root := NewRoot(printer, ctx, nil, nil, gbMock, nil, nil)

gbMock.EXPECT().TokenIntrospection("").Return(&globalping.IntrospectionResponse{
Active: true,
Username: "test",
}, nil)

os.Args = []string{"globalping", "auth", "status"}
err := root.Cmd.ExecuteContext(context.TODO())
assert.NoError(t, err)

assert.Equal(t, `Logged in as test.
`, w.String())
}

func Test_Logout(t *testing.T) {
t.Cleanup(sessionCleanup)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

gbMock := mocks.NewMockClient(ctrl)

w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("")

root := NewRoot(printer, ctx, nil, nil, gbMock, nil, nil)

gbMock.EXPECT().Logout().Return(nil)

os.Args = []string{"globalping", "auth", "logout"}
err := root.Cmd.ExecuteContext(context.TODO())
assert.NoError(t, err)

assert.Equal(t, "You are now logged out.\n", w.String())
}
14 changes: 7 additions & 7 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func Test_UpdateContext(t *testing.T) {
func test_updateContext_NoArg(t *testing.T) {
ctx := createDefaultContext("ping")
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("test", []string{"1.1.1.1"})
assert.Equal(t, "test", ctx.Cmd)
Expand All @@ -40,7 +40,7 @@ func test_updateContext_NoArg(t *testing.T) {
func test_updateContext_Country(t *testing.T) {
ctx := createDefaultContext("ping")
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("test", []string{"1.1.1.1", "from", "Germany"})
assert.Equal(t, "test", ctx.Cmd)
Expand All @@ -53,7 +53,7 @@ func test_updateContext_Country(t *testing.T) {
func test_updateContext_CountryWhitespace(t *testing.T) {
ctx := createDefaultContext("ping")
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("test", []string{"1.1.1.1", "from", " Germany, France"})
assert.Equal(t, "test", ctx.Cmd)
Expand All @@ -65,7 +65,7 @@ func test_updateContext_CountryWhitespace(t *testing.T) {
func test_updateContext_NoTarget(t *testing.T) {
ctx := createDefaultContext("ping")
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("test", []string{})
assert.Error(t, err)
Expand All @@ -78,7 +78,7 @@ func test_updateContext_CIEnv(t *testing.T) {

ctx := createDefaultContext("ping")
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("test", []string{"1.1.1.1"})
assert.Equal(t, "test", ctx.Cmd)
Expand All @@ -92,7 +92,7 @@ func test_updateContext_TargetIsNotAHostname(t *testing.T) {
ctx := createDefaultContext("ping")
ctx.Ipv4 = true
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("ping", []string{"1.1.1.1"})
assert.EqualError(t, err, ErrTargetIPVersionNotAllowed.Error())
Expand All @@ -107,7 +107,7 @@ func test_updateContext_ResolverIsNotAHostname(t *testing.T) {
ctx := createDefaultContext("dns")
ctx.Ipv4 = true
printer := view.NewPrinter(nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil)
root := NewRoot(printer, ctx, nil, nil, nil, nil, nil)

err := root.updateContext("dns", []string{"example.com", "@1.1.1.1"})
assert.EqualError(t, err, ErrResolverIPVersionNotAllowed.Error())
Expand Down
6 changes: 3 additions & 3 deletions cmd/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func Test_Execute_DNS_Default(t *testing.T) {
w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("dns")
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil)
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil, nil)

os.Args = []string{"globalping", "dns", "jsdelivr.com",
"from", "Berlin",
Expand Down Expand Up @@ -107,7 +107,7 @@ func Test_Execute_DNS_IPv4(t *testing.T) {
w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("dns")
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil)
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil, nil)

os.Args = []string{"globalping", "dns", "jsdelivr.com",
"from", "Berlin",
Expand Down Expand Up @@ -147,7 +147,7 @@ func Test_Execute_DNS_IPv6(t *testing.T) {
w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
ctx := createDefaultContext("dns")
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil)
root := NewRoot(printer, ctx, viewerMock, timeMock, gbMock, nil, nil)

os.Args = []string{"globalping", "dns", "jsdelivr.com",
"from", "Berlin",
Expand Down
2 changes: 1 addition & 1 deletion cmd/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func Test_Execute_History_Default(t *testing.T) {
ctx := createDefaultContext("ping")
w := new(bytes.Buffer)
printer := view.NewPrinter(nil, w, w)
root := NewRoot(printer, ctx, nil, timeMock, nil, nil)
root := NewRoot(printer, ctx, nil, timeMock, nil, nil, nil)
os.Args = []string{"globalping", "ping", "jsdelivr.com"}

ctx.History.Push(&view.HistoryItem{
Expand Down
Loading