Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 7 additions & 4 deletions cmd/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"github.com/spf13/cobra"
)

func runAPIKey(cfg *config.Config) (*api.APIKeyResponse, error) {
return api.NewClient(cfg.EndpointURL, cfg.APIKey).GetAPIKey()
}

var apiKeyCmd = &cobra.Command{
Use: "api-key",
Short: "Validate your API key",
Expand All @@ -17,16 +21,15 @@ var apiKeyCmd = &cobra.Command{
return err
}

client := api.NewClient(cfg.EndpointURL, cfg.APIKey)
result, err := client.GetAPIKey()
result, err := runAPIKey(cfg)
if err != nil {
return err
}

if isJSONOutput() {
return printJSON(result)
return printJSON(cmd.OutOrStdout(), result)
}
fmt.Printf("Valid API key for team: %s\n", result.TeamName)
fmt.Fprintf(cmd.OutOrStdout(), "Valid API key for team: %s\n", result.TeamName)
return nil
},
}
Expand Down
31 changes: 31 additions & 0 deletions cmd/api_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cmd

import (
"net/http"
"reflect"
"testing"

"github.com/loops-so/cli/internal/api"
)

func TestRunAPIKey(t *testing.T) {
t.Run("returns team name", func(t *testing.T) {
serveJSON(t, http.StatusOK, `{"teamName":"Acme Corp"}`)
result, err := runAPIKey(cfg(t))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := &api.APIKeyResponse{TeamName: "Acme Corp"}
if !reflect.DeepEqual(result, want) {
t.Errorf("got %+v, want %+v", result, want)
}
})

t.Run("returns error on api failure", func(t *testing.T) {
serveJSON(t, http.StatusUnauthorized, `{"error":"Invalid API key"}`)
_, err := runAPIKey(cfg(t))
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
22 changes: 14 additions & 8 deletions cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,30 @@ var loginCmd = &cobra.Command{
return fmt.Errorf("API key cannot be empty")
}

client := api.NewClient(config.EndpointURL(), apiKey)
result, err := client.GetAPIKey()
result, err := runAuthLogin(apiKey)
if err != nil {
return fmt.Errorf("API key verification failed: %w", err)
}

if err := config.Save(apiKey); err != nil {
return err
}

if isJSONOutput() {
return printJSON(Result{Success: true, Message: fmt.Sprintf("Authenticated as team: %s", result.TeamName)})
return printJSON(cmd.OutOrStdout(), Result{Success: true, Message: fmt.Sprintf("Authenticated as team: %s", result.TeamName)})
}
fmt.Printf("API key saved. Authenticated as team: %s\n", result.TeamName)
fmt.Fprintf(cmd.OutOrStdout(), "API key saved. Authenticated as team: %s\n", result.TeamName)
return nil
},
}

func runAuthLogin(apiKey string) (*api.APIKeyResponse, error) {
result, err := api.NewClient(config.EndpointURL(), apiKey).GetAPIKey()
if err != nil {
return nil, fmt.Errorf("API key verification failed: %w", err)
}
if err := config.Save(apiKey); err != nil {
return nil, err
}
return result, nil
}

func init() {
authCmd.AddCommand(loginCmd)
}
31 changes: 31 additions & 0 deletions cmd/auth_login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cmd

import (
"net/http"
"reflect"
"testing"

"github.com/loops-so/cli/internal/api"
)

func TestRunAuthLogin(t *testing.T) {
t.Run("saves key and returns team name", func(t *testing.T) {
serveJSON(t, http.StatusOK, `{"teamName":"Acme"}`)
result, err := runAuthLogin("test-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := &api.APIKeyResponse{TeamName: "Acme"}
if !reflect.DeepEqual(result, want) {
t.Errorf("got %+v, want %+v", result, want)
}
})

t.Run("returns error on api failure", func(t *testing.T) {
serveJSON(t, http.StatusUnauthorized, `{"error":"Invalid API key"}`)
_, err := runAuthLogin("bad-key")
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
10 changes: 7 additions & 3 deletions cmd/auth_logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Remove stored Loops credentials",
RunE: func(cmd *cobra.Command, args []string) error {
if err := config.Delete(); err != nil {
if err := runAuthLogout(); err != nil {
return err
}
if isJSONOutput() {
return printJSON(Result{Success: true})
return printJSON(cmd.OutOrStdout(), Result{Success: true})
}
fmt.Println("Logged out.")
fmt.Fprintln(cmd.OutOrStdout(), "Logged out.")
return nil
},
}

func runAuthLogout() error {
return config.Delete()
}

func init() {
authCmd.AddCommand(logoutCmd)
}
14 changes: 14 additions & 0 deletions cmd/auth_logout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cmd

import (
"testing"
)

func TestRunAuthLogout(t *testing.T) {
t.Run("succeeds", func(t *testing.T) {
mockKeyring(t)
if err := runAuthLogout(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
12 changes: 8 additions & 4 deletions cmd/auth_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ var statusCmd = &cobra.Command{
Use: "status",
Short: "Print the resolved configuration",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
cfg, err := runAuthStatus()
if err != nil {
return err
}

if isJSONOutput() {
return printJSON(cfg)
return printJSON(cmd.OutOrStdout(), cfg)
}

fmt.Printf("API Key: %s\n", cfg.APIKey)
fmt.Printf("Endpoint: %s\n", cfg.EndpointURL)
fmt.Fprintf(cmd.OutOrStdout(), "API Key: %s\n", cfg.APIKey)
fmt.Fprintf(cmd.OutOrStdout(), "Endpoint: %s\n", cfg.EndpointURL)
return nil
},
}

func runAuthStatus() (*config.Config, error) {
return config.Load()
}

func init() {
authCmd.AddCommand(statusCmd)
}
32 changes: 32 additions & 0 deletions cmd/auth_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cmd

import (
"net/http"
"testing"

"github.com/zalando/go-keyring"
)

func TestRunAuthStatus(t *testing.T) {
t.Run("returns config", func(t *testing.T) {
serveJSON(t, http.StatusOK, `{}`)
cfg, err := runAuthStatus()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.APIKey == "" {
t.Error("expected APIKey to be set")
}
if cfg.EndpointURL == "" {
t.Error("expected EndpointURL to be set")
}
})

t.Run("returns error when no key set", func(t *testing.T) {
keyring.MockInit()
_, err := runAuthStatus()
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
36 changes: 36 additions & 0 deletions cmd/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmd

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/loops-so/cli/internal/config"
"github.com/zalando/go-keyring"
)

func cfg(t *testing.T) *config.Config {
t.Helper()
c, err := config.Load()
if err != nil {
t.Fatalf("config.Load: %v", err)
}
return c
}

func mockKeyring(t *testing.T) {
t.Helper()
keyring.MockInit()
}

func serveJSON(t *testing.T, status int, body string) {
t.Helper()
keyring.MockInit()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
t.Setenv("LOOPS_API_KEY", "test-key")
t.Setenv("LOOPS_ENDPOINT_URL", srv.URL)
}
6 changes: 3 additions & 3 deletions cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"io"
)

type outputFlag string
Expand All @@ -30,8 +30,8 @@ func isJSONOutput() bool {
return outputFormat == "json"
}

func printJSON(v any) error {
enc := json.NewEncoder(os.Stdout)
func printJSON(w io.Writer, v any) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(v)
}
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func Execute() {
err := rootCmd.Execute()
if err != nil {
if isJSONOutput() {
printJSON(Result{Success: false, Message: err.Error()})
printJSON(os.Stderr, Result{Success: false, Message: err.Error()})
} else {
fmt.Fprintln(os.Stderr, "Error:", err)
}
Expand Down
45 changes: 25 additions & 20 deletions cmd/transactional.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ func attachmentFromPath(path string) (api.Attachment, error) {
}, nil
}

func runTransactionalList(cfg *config.Config, params api.PaginationParams) ([]api.TransactionalEmail, error) {
client := api.NewClient(cfg.EndpointURL, cfg.APIKey)
if params.Cursor != "" {
emails, _, err := client.ListTransactional(params)
return emails, err
}
return api.Paginate(func(cursor string) ([]api.TransactionalEmail, *api.Pagination, error) {
return client.ListTransactional(api.PaginationParams{
PerPage: params.PerPage,
Cursor: cursor,
})
})
}

func runTransactionalSend(cfg *config.Config, req api.SendTransactionalRequest) error {
return api.NewClient(cfg.EndpointURL, cfg.APIKey).SendTransactional(req)
}

var transactionalCmd = &cobra.Command{
Use: "transactional",
Short: "Manage transactional emails",
Expand All @@ -57,19 +75,7 @@ var transactionalListCmd = &cobra.Command{
}

params := paginationParams(cmd)
client := api.NewClient(cfg.EndpointURL, cfg.APIKey)

var emails []api.TransactionalEmail
if params.Cursor != "" {
emails, _, err = client.ListTransactional(params)
} else {
emails, err = api.Paginate(func(cursor string) ([]api.TransactionalEmail, *api.Pagination, error) {
return client.ListTransactional(api.PaginationParams{
PerPage: params.PerPage,
Cursor: cursor,
})
})
}
emails, err := runTransactionalList(cfg, params)
if err != nil {
return err
}
Expand All @@ -78,15 +84,15 @@ var transactionalListCmd = &cobra.Command{
if emails == nil {
emails = []api.TransactionalEmail{}
}
return printJSON(emails)
return printJSON(cmd.OutOrStdout(), emails)
}

if len(emails) == 0 {
fmt.Println("No transactional emails found.")
fmt.Fprintln(cmd.OutOrStdout(), "No transactional emails found.")
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tLAST UPDATED\tVARIABLES")
for _, e := range emails {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.ID, e.Name, e.LastUpdated, strings.Join(e.DataVariables, ", "))
Expand Down Expand Up @@ -135,15 +141,14 @@ var transactionalSendCmd = &cobra.Command{
req.Attachments = append(req.Attachments, a)
}

client := api.NewClient(cfg.EndpointURL, cfg.APIKey)
if err := client.SendTransactional(req); err != nil {
if err := runTransactionalSend(cfg, req); err != nil {
return err
}

if isJSONOutput() {
return printJSON(Result{Success: true})
return printJSON(cmd.OutOrStdout(), Result{Success: true})
}
fmt.Println("Sent.")
fmt.Fprintln(cmd.OutOrStdout(), "Sent.")
return nil
},
}
Expand Down
Loading
Loading