Skip to content

Commit 18b8db7

Browse files
committed
Fix package defaults and timeout handling
1 parent 9562c9f commit 18b8db7

10 files changed

Lines changed: 164 additions & 10 deletions

File tree

cmd/playconsole-cli/commands/apps/apps.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ func runList(cmd *cobra.Command, args []string) error {
7171
}
7272

7373
func fetchApps() ([]AppInfo, error) {
74-
ctx := context.Background()
74+
ctx, cancel := context.WithTimeout(context.Background(), cli.ResolveTimeout(60*time.Second))
75+
defer cancel()
7576

7677
// Get credentials
7778
creds, err := config.GetCredentials()

cmd/playconsole-cli/commands/root.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ Design Philosophy:
6262
// Sync flags to cli package
6363
cli.SetPackageName(packageName)
6464
cli.SetProfile(profile)
65-
cli.SetTimeout(timeout)
65+
if cmd.Flags().Changed("timeout") || os.Getenv("GPC_TIMEOUT") != "" {
66+
cli.SetTimeout(timeout)
67+
} else {
68+
cli.SetTimeout("")
69+
}
6670
cli.SetDryRun(dryRun)
6771

6872
// Initialize config

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/spf13/cobra v1.10.2
77
github.com/spf13/viper v1.21.0
88
golang.org/x/oauth2 v0.35.0
9+
golang.org/x/term v0.39.0
910
google.golang.org/api v0.267.0
1011
gopkg.in/yaml.v3 v3.0.1
1112
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
9292
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
9393
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
9494
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
95+
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
96+
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
9597
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
9698
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
9799
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=

internal/api/client.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"google.golang.org/api/androidpublisher/v3"
1111
"google.golang.org/api/option"
1212

13+
"github.com/AndroidPoet/playconsole-cli/internal/cli"
1314
"github.com/AndroidPoet/playconsole-cli/internal/config"
1415
)
1516

@@ -28,12 +29,19 @@ type debugTransport struct {
2829

2930
func (t *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
3031
fmt.Printf("DEBUG: %s %s\n", req.Method, req.URL)
31-
return t.base.RoundTrip(req)
32+
resp, err := t.base.RoundTrip(req)
33+
if err != nil {
34+
fmt.Printf("DEBUG: request failed: %v\n", err)
35+
return nil, err
36+
}
37+
fmt.Printf("DEBUG: response %s\n", resp.Status)
38+
return resp, nil
3239
}
3340

3441
// NewClient creates a new API client
3542
func NewClient(packageName string, timeout time.Duration) (*Client, error) {
3643
ctx := context.Background()
44+
timeout = cli.ResolveTimeout(timeout)
3745

3846
// Get credentials
3947
creds, err := config.GetCredentials()
@@ -151,10 +159,10 @@ func (c *Client) SystemAPKs() *androidpublisher.SystemapksService {
151159

152160
// Edit represents an active edit session
153161
type Edit struct {
154-
client *Client
155-
editID string
156-
ctx context.Context
157-
cancel context.CancelFunc
162+
client *Client
163+
editID string
164+
ctx context.Context
165+
cancel context.CancelFunc
158166
}
159167

160168
// CreateEdit creates a new edit session

internal/api/enablement.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"regexp"
99
"runtime"
1010
"strings"
11+
12+
"golang.org/x/term"
1113
)
1214

1315
// APIEnablementError represents an error when an API needs to be enabled
@@ -73,6 +75,14 @@ func ParseAPIEnablementError(err error) *APIEnablementError {
7375

7476
// HandleAPIEnablement provides an interactive flow to enable a required API
7577
func HandleAPIEnablement(apiErr *APIEnablementError) error {
78+
if !isInteractiveSession() {
79+
return fmt.Errorf(
80+
"API '%s' must be enabled before retrying. Enable it here: %s",
81+
apiErr.APITitle,
82+
apiErr.ActivationURL,
83+
)
84+
}
85+
7686
fmt.Println()
7787
fmt.Println("╔══════════════════════════════════════════════════════════════════╗")
7888
fmt.Println("║ API ENABLEMENT REQUIRED ║")
@@ -134,6 +144,10 @@ func HandleAPIEnablement(apiErr *APIEnablementError) error {
134144
}
135145
}
136146

147+
func isInteractiveSession() bool {
148+
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
149+
}
150+
137151
func waitForEnablement() error {
138152
fmt.Println()
139153
fmt.Println("After enabling the API in Google Cloud Console:")
@@ -194,8 +208,8 @@ func copyToClipboard(text string) error {
194208

195209
// RequiredAPIs lists all APIs that might be needed by the CLI
196210
var RequiredAPIs = map[string]string{
197-
"androidpublisher.googleapis.com": "Google Play Android Developer API",
198-
"playdeveloperreporting.googleapis.com": "Google Play Developer Reporting API",
211+
"androidpublisher.googleapis.com": "Google Play Android Developer API",
212+
"playdeveloperreporting.googleapis.com": "Google Play Developer Reporting API",
199213
}
200214

201215
// CheckAndEnableAPI wraps an API call and handles enablement if needed

internal/api/enablement_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package api
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestHandleAPIEnablementReturnsURLWhenNonInteractive(t *testing.T) {
9+
apiErr := &APIEnablementError{
10+
APITitle: "Google Play Android Developer API",
11+
ActivationURL: "https://console.cloud.google.com/apis/api/androidpublisher.googleapis.com/overview?project=123456",
12+
}
13+
14+
err := HandleAPIEnablement(apiErr)
15+
if err == nil {
16+
t.Fatal("expected non-interactive enablement error")
17+
}
18+
19+
message := err.Error()
20+
if !strings.Contains(message, apiErr.APITitle) {
21+
t.Fatalf("expected API title in error, got %q", message)
22+
}
23+
if !strings.Contains(message, apiErr.ActivationURL) {
24+
t.Fatalf("expected activation URL in error, got %q", message)
25+
}
26+
}

internal/api/reporting.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"google.golang.org/api/option"
1010
"google.golang.org/api/playdeveloperreporting/v1beta1"
1111

12+
"github.com/AndroidPoet/playconsole-cli/internal/cli"
1213
"github.com/AndroidPoet/playconsole-cli/internal/config"
1314
)
1415

@@ -22,6 +23,7 @@ type ReportingClient struct {
2223
// NewReportingClient creates a new Reporting API client
2324
func NewReportingClient(packageName string, timeout time.Duration) (*ReportingClient, error) {
2425
ctx := context.Background()
26+
timeout = cli.ResolveTimeout(timeout)
2527

2628
// Get credentials
2729
creds, err := config.GetCredentials()

internal/cli/shared.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package cli
22

33
import (
44
"fmt"
5+
"time"
56

67
"github.com/spf13/cobra"
78
"github.com/spf13/viper"
9+
10+
"github.com/AndroidPoet/playconsole-cli/internal/config"
811
)
912

1013
var (
@@ -39,7 +42,13 @@ func GetPackageName() string {
3942
if packageName != "" {
4043
return packageName
4144
}
42-
return viper.GetString("package")
45+
if pkg := viper.GetString("package"); pkg != "" {
46+
return pkg
47+
}
48+
if profile := config.GetProfile(); profile != nil {
49+
return profile.DefaultPackage
50+
}
51+
return ""
4352
}
4453

4554
// GetProfile returns the profile name from flag, env, or config
@@ -66,6 +75,20 @@ func GetTimeout() string {
6675
return t
6776
}
6877

78+
// ResolveTimeout returns an explicit user timeout when present, otherwise fallback.
79+
func ResolveTimeout(fallback time.Duration) time.Duration {
80+
if timeout == "" {
81+
return fallback
82+
}
83+
84+
parsed, err := time.ParseDuration(timeout)
85+
if err != nil || parsed <= 0 {
86+
return fallback
87+
}
88+
89+
return parsed
90+
}
91+
6992
// IsDryRun returns whether dry-run mode is enabled
7093
func IsDryRun() bool {
7194
return dryRun

internal/cli/shared_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
"time"
8+
9+
"github.com/spf13/viper"
10+
11+
"github.com/AndroidPoet/playconsole-cli/internal/config"
12+
)
13+
14+
func TestGetPackageNameFallsBackToProfileDefault(t *testing.T) {
15+
previousPackageName := packageName
16+
previousTimeout := timeout
17+
packageName = ""
18+
timeout = ""
19+
viper.Set("package", "")
20+
t.Cleanup(func() {
21+
packageName = previousPackageName
22+
timeout = previousTimeout
23+
viper.Set("package", "")
24+
})
25+
26+
configFile := filepath.Join(t.TempDir(), "config.json")
27+
configJSON := `{
28+
"default_profile": "default",
29+
"profiles": {
30+
"default": {
31+
"name": "default",
32+
"credentials_path": "/tmp/creds.json",
33+
"default_package": "com.example.profile"
34+
}
35+
}
36+
}`
37+
if err := os.WriteFile(configFile, []byte(configJSON), 0o600); err != nil {
38+
t.Fatalf("WriteFile returned error: %v", err)
39+
}
40+
41+
if err := config.Init(configFile, ""); err != nil {
42+
t.Fatalf("Init returned error: %v", err)
43+
}
44+
45+
if got := GetPackageName(); got != "com.example.profile" {
46+
t.Fatalf("expected default package from profile, got %q", got)
47+
}
48+
}
49+
50+
func TestResolveTimeoutUsesFallbackWithoutExplicitOverride(t *testing.T) {
51+
previousTimeout := timeout
52+
timeout = ""
53+
t.Cleanup(func() {
54+
timeout = previousTimeout
55+
})
56+
57+
fallback := 5 * time.Minute
58+
if got := ResolveTimeout(fallback); got != fallback {
59+
t.Fatalf("expected fallback timeout %s, got %s", fallback, got)
60+
}
61+
}
62+
63+
func TestResolveTimeoutUsesExplicitOverride(t *testing.T) {
64+
previousTimeout := timeout
65+
timeout = "90s"
66+
t.Cleanup(func() {
67+
timeout = previousTimeout
68+
})
69+
70+
if got := ResolveTimeout(5 * time.Minute); got != 90*time.Second {
71+
t.Fatalf("expected explicit timeout override, got %s", got)
72+
}
73+
}

0 commit comments

Comments
 (0)