diff --git a/README.md b/README.md index 973a5c91..2a407744 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,8 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser - `--allowed-host ` - Set an egress allowlist at creation: the only destinations the browser may reach through Kernel-managed egress (repeatable or comma-separated, max 100). Other destinations are refused with a 403 whose `X-Kernel-Proxy-Error` header is `network_policy_denied`. Filters Kernel-managed egress only, not all browser VM traffic. Accepts exact hostnames (`example.com`), a leading wildcard that matches subdomains but not the domain itself (`*.example.com`), public IPs (`8.8.8.8`, `[2001:4860:4860::8888]`), and public CIDRs (`8.8.4.0/24`); no ports, paths, or schemes. `--start-url` must be allowed. Omit for unfiltered egress; an empty list is invalid. Requires proxy v3; cannot be combined with `--pool-id` or `--pool-name`, even with `--yes` (set the allowlist on the pool with `kernel browser-pools create/update --allowed-host`). An existing list can be replaced or removed later with `browsers update --allowed-host` / `--clear-allowed-hosts`, but cannot be added later if omitted at creation or removed. - `--proxy-route '[,...]='` - Route matching browser requests through a selected proxy (repeatable, max 10 routes with 1–50 hosts each). Example: `--proxy-route 'api.ipify.org,*.ipify.org=name:my-dc-proxy'`. The proxy is an ID by default; use `id:` or `name:` explicitly. Exact hostnames beat wildcards; longer wildcard suffixes beat shorter ones. `*.example.com` matches subdomains, not `example.com`. Matching ignores case and ports. Unmatched hosts use `--proxy-*` or default egress. Routes apply from the start of the session, including to `--start-url`. Routes are create-only and cannot be combined with `--pool-id`/`--pool-name`; configure them on the pool instead. - `--start-url ` - Initial page to open on launch + - `--timezone ` - Session timezone, e.g. `Europe/Berlin`. Defaults to the browser region's timezone + - `--locale ` - Session locale used for languages and date and number formatting, e.g. `de-DE`. Defaults to the browser region's locale - `--proxy-id ` / `--proxy-name ` - Use that proxy for the session regardless of stealth (mutually exclusive with each other and with `--proxy-mode`) - `--proxy-mode direct|default` - Egress mode instead of a selected proxy: `direct` for no proxy regardless of stealth, `default` for the stealth-derived default (Kernel's stealth proxy with `--stealth`, direct egress otherwise). Omit all proxy flags to get the default. - `--name ` - Optional unique name for the session (used to find it later by name; can be changed with `browsers update --name`) @@ -297,6 +299,10 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser - `--clear-name` - Clear the session name - `--tag ` - Set a tag, repeatable; up to 50 pairs. Replaces the entire tag set (not merged); mutually exclusive with `--clear-tags` - `--clear-tags` - Remove all tags from the session + - `--timezone ` - Set the session timezone, e.g. `America/Los_Angeles`. Applies without restarting the browser; mutually exclusive with `--clear-timezone` + - `--clear-timezone` - Clear the timezone override and use the browser region's default + - `--locale ` - Set the session locale, e.g. `de-DE`. Applies without restarting the browser; mutually exclusive with `--clear-locale` + - `--clear-locale` - Clear the locale override and use the browser region's default - `--telemetry=all` - Enable telemetry for all categories - `--telemetry=off` - Disable telemetry - `--telemetry=` - Per-category config, e.g. `--telemetry=network=on,page=off` diff --git a/cmd/browsers.go b/cmd/browsers.go index 47f2e98d..97da389f 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -356,6 +356,28 @@ func formatPrivateHosts(network kernel.BrowserNetworkConfig) string { return strings.Join(network.PrivateHosts, ", ") } +// browserLocationRows shows the effective timezone and locale with whether each +// is the session's override or the browser region's default. +func browserLocationRows(location kernel.BrowserLocation) [][]string { + if location.Timezone == "" && location.Locale == "" { + return nil + } + return [][]string{ + {"Timezone", formatBrowserLocationField(location.Timezone, location.TimezoneSource)}, + {"Locale", formatBrowserLocationField(location.Locale, location.LocaleSource)}, + } +} + +func formatBrowserLocationField(value string, source kernel.BrowserLocationSource) string { + if value == "" { + return "-" + } + if source == "" { + return value + } + return fmt.Sprintf("%s (%s)", value, source) +} + // formatVaultReferences renders the vaults linked to a session for table output, // preferring each vault's name and falling back to its ID. It returns an empty // string when no vaults are linked, so the row can be omitted entirely. @@ -508,6 +530,8 @@ type BrowsersCreateInput struct { AllowedHostsProvided bool ProxyRoutes []string StartURL string + Timezone string + Locale string Extensions []string Vaults []string Viewport string @@ -558,6 +582,14 @@ type BrowsersUpdateInput struct { TagsProvided bool ClearTags bool StartURL string + // Timezone and Locale set the session's location overrides when SetTimezone + // and SetLocale are set; ClearTimezone and ClearLocale remove them. + Timezone string + SetTimezone bool + ClearTimezone bool + Locale string + SetLocale bool + ClearLocale bool // AllowedHosts replaces the session's egress allowlist when // AllowedHostsProvided is set; ClearAllowedHosts removes it. AllowedHosts []string @@ -774,6 +806,12 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { if in.StartURL != "" { params.StartURL = kernel.Opt(in.StartURL) } + if in.Timezone != "" { + params.Location.Timezone = kernel.Opt(in.Timezone) + } + if in.Locale != "" { + params.Location.Locale = kernel.Opt(in.Locale) + } region, err := parseRegionFlag(in.Region) if err != nil { @@ -881,6 +919,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { tableData := buildBrowserTableData(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags) tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)}, []string{"Allowed Hosts", formatAllowedHosts(browser.Network)}, []string{"Proxy Routes", formatProxyRoutes(browser.Network)}) + tableData = append(tableData, browserLocationRows(browser.Location)...) PrintTableNoPad(tableData, true) if len(browser.Vaults) > 0 { rows := pterm.TableData{{"Attached vault ID", "Name"}} @@ -1006,6 +1045,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error { // Append additional detailed fields tableData = append(tableData, []string{"Created At", util.FormatLocal(browser.CreatedAt)}) tableData = append(tableData, []string{"Region", util.OrDash(string(browser.Region))}) + tableData = append(tableData, browserLocationRows(browser.Location)...) tableData = append(tableData, []string{"Timeout (seconds)", fmt.Sprintf("%d", browser.TimeoutSeconds)}) tableData = append(tableData, []string{"Headless", fmt.Sprintf("%t", browser.Headless)}) tableData = append(tableData, []string{"Stealth", fmt.Sprintf("%t", browser.Stealth)}) @@ -1124,6 +1164,19 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { return fmt.Errorf("--name requires a non-empty value; use --clear-name to clear the name") } + if in.SetTimezone && in.ClearTimezone { + return fmt.Errorf("cannot specify both --timezone and --clear-timezone") + } + if in.SetTimezone && in.Timezone == "" { + return fmt.Errorf("--timezone requires a non-empty value; use --clear-timezone to go back to the browser region's default") + } + if in.SetLocale && in.ClearLocale { + return fmt.Errorf("cannot specify both --locale and --clear-locale") + } + if in.SetLocale && in.Locale == "" { + return fmt.Errorf("--locale requires a non-empty value; use --clear-locale to go back to the browser region's default") + } + hasProxyChange := proxySel.set() hasProfileChange := in.ProfileID != "" || in.ProfileName != "" hasViewportChange := in.Viewport != "" @@ -1131,6 +1184,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { hasNameChange := in.SetName || in.ClearName hasTagsChange := len(in.Tags) > 0 || in.ClearTags hasStartURLChange := in.StartURL != "" + hasLocationChange := in.SetTimezone || in.ClearTimezone || in.SetLocale || in.ClearLocale // The API replaces the allowlist with the given entries, or removes it when // sent null. An empty list is rejected, so require at least one entry. @@ -1154,8 +1208,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } // Validate that at least one update option is provided - if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange && !hasStartURLChange && !hasAllowedHostsChange { - return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, --clear-tags, --start-url, --allowed-host, or --clear-allowed-hosts") + if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange && !hasStartURLChange && !hasLocationChange && !hasAllowedHostsChange { + return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, --clear-tags, --start-url, --timezone, --clear-timezone, --locale, --clear-locale, --allowed-host, or --clear-allowed-hosts") } params := kernel.BrowserUpdateParams{} @@ -1181,6 +1235,19 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { params.StartURL = kernel.String(in.StartURL) } + // Handle location changes. Null clears an override so the browser uses its + // region's default. + if in.ClearTimezone { + params.Location.Timezone = param.Null[string]() + } else if in.SetTimezone { + params.Location.Timezone = kernel.Opt(in.Timezone) + } + if in.ClearLocale { + params.Location.Locale = param.Null[string]() + } else if in.SetLocale { + params.Location.Locale = kernel.Opt(in.Locale) + } + // Handle egress allowlist changes. Null removes the allowlist. if in.ClearAllowedHosts { params.Network.AllowedHosts = param.NullSlice[[]string]() @@ -1265,6 +1332,10 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { if hasStartURLChange { pterm.Info.Printf("Start URL: %s\n", util.OrDash(browser.StartURL)) } + if hasLocationChange { + pterm.Info.Printf("Timezone: %s\n", formatBrowserLocationField(browser.Location.Timezone, browser.Location.TimezoneSource)) + pterm.Info.Printf("Locale: %s\n", formatBrowserLocationField(browser.Location.Locale, browser.Location.LocaleSource)) + } if hasAllowedHostsChange { pterm.Info.Printf("Allowed Hosts: %s\n", formatAllowedHosts(browser.Network)) } @@ -2937,6 +3008,7 @@ Supported operations: - Rename or clear the session name (--name or --clear-name) - Replace or clear the session tags (--tag or --clear-tags) - Navigate the session to a URL (--start-url) + - Set or clear the session timezone and locale (--timezone, --locale, --clear-timezone, --clear-locale) - Replace or remove the egress allowlist (--allowed-host or --clear-allowed-hosts) Notes: @@ -2944,6 +3016,9 @@ Notes: - --start-url navigation is best-effort: the update succeeds even if the page fails to load. - --start-url combined with --profile-id/--profile-name overrides the profile's restored tabs. - --tag replaces the entire tag set (it is not merged with existing tags). + - --timezone and --locale apply without restarting the browser. --clear-timezone and + --clear-locale go back to the browser region's default. Omit a flag to leave that field unchanged. + --start-url in the same update opens once the new location applies. - --allowed-host replaces an existing allowlist only; it cannot add one to a browser created without one. Omit to leave the list unchanged; an empty list is invalid. - --clear-allowed-hosts removes the allowlist and returns to unfiltered egress. Once removed, @@ -3006,6 +3081,10 @@ func init() { browsersUpdateCmd.Flags().Bool("clear-tags", false, "Remove all tags from the browser session") browsersUpdateCmd.Flags().StringSlice("allowed-host", nil, "Replace an existing egress allowlist (repeat or comma-separated, max 100), using the same entry rules as 'browsers create --allowed-host'. Cannot add one to a browser created without one or after removal. Omit to leave unchanged; an empty list is invalid. Applies without restarting the browser; --start-url must be allowed by the new list. Requires proxy v3; on leased pooled browsers the pool's allowlist is restored on release (mutually exclusive with --clear-allowed-hosts). See notes for propagation timing") browsersUpdateCmd.Flags().Bool("clear-allowed-hosts", false, "Remove the egress allowlist and return to unfiltered egress; an allowlist cannot be added back to this browser") + browsersUpdateCmd.Flags().String("timezone", "", "Set the session timezone (IANA, e.g. America/Los_Angeles) (mutually exclusive with --clear-timezone)") + browsersUpdateCmd.Flags().Bool("clear-timezone", false, "Clear the session timezone override and use the browser region's default") + browsersUpdateCmd.Flags().String("locale", "", "Set the session locale used for languages and date and number formatting (BCP 47, e.g. de-DE) (mutually exclusive with --clear-locale)") + browsersUpdateCmd.Flags().Bool("clear-locale", false, "Clear the session locale override and use the browser region's default") browsersUpdateCmd.Flags().String("start-url", "", "Navigate the browser to this URL after applying the update. Overrides the restored tabs when a profile is loaded in the same update. Navigation is best-effort, so failures do not fail the update") browsersCmd.AddCommand(browsersListCmd) @@ -3302,6 +3381,8 @@ unrestricted code execution inside the browser VM and is not sandboxed.`, browsersCreateCmd.Flags().StringSlice("allowed-host", nil, "Set an egress allowlist at creation: the only destinations the browser may reach through Kernel-managed egress (repeat or comma-separated, max 100); anything else is refused with a 403 (network_policy_denied). Filters Kernel-managed egress only, not all browser VM traffic. Accepts exact hostnames ('example.com'), a leading wildcard matching subdomains only ('*.example.com'), public IPs ('8.8.8.8', '[2001:4860:4860::8888]'), and public CIDRs ('8.8.4.0/24'). No ports, paths, or schemes. --start-url must be allowed. Omit for unfiltered egress; an empty list is invalid. Requires proxy v3; cannot be combined with --pool-id or --pool-name, even with --yes. An existing list can be replaced or removed with 'browsers update'; it cannot be added later if omitted at creation or removed") browsersCreateCmd.Flags().StringArray("proxy-route", nil, "Route HOST[,HOST...]=PROXY through a proxy (repeatable, max 10 routes and 50 hosts per route). PROXY is an ID by default; use id:ID or name:NAME explicitly. Exact hosts beat wildcards (longer suffixes win); *.example.com excludes example.com. Unmatched hosts use --proxy-* or default egress. Routes also apply to start_url and other setup traffic. Create-only") browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch") + browsersCreateCmd.Flags().String("timezone", "", "Session timezone (IANA, e.g. Europe/Berlin); defaults to the browser region's timezone") + browsersCreateCmd.Flags().String("locale", "", "Session locale used for languages and date and number formatting (BCP 47, e.g. de-DE); defaults to the browser region's locale") browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)") browsersCreateCmd.Flags().String("viewport", "", "Browser viewport size (e.g., 1920x1080@25). Supported: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60, 1280x800@60") browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list") @@ -3443,6 +3524,8 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { allowedHosts, _ := cmd.Flags().GetStringSlice("allowed-host") proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route") startURL, _ := cmd.Flags().GetString("start-url") + timezone, _ := cmd.Flags().GetString("timezone") + locale, _ := cmd.Flags().GetString("locale") extensions, _ := cmd.Flags().GetStringSlice("extension") vaults, _ := cmd.Flags().GetStringArray("vault") viewport, _ := cmd.Flags().GetString("viewport") @@ -3601,6 +3684,8 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { AllowedHostsProvided: cmd.Flags().Changed("allowed-host"), ProxyRoutes: proxyRoutes, StartURL: startURL, + Timezone: timezone, + Locale: locale, Extensions: extensions, Vaults: vaults, Viewport: viewport, @@ -3680,6 +3765,10 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { tags, tagsProvided := tagsFromFlag(cmd, "tag") clearTags, _ := cmd.Flags().GetBool("clear-tags") startURL, _ := cmd.Flags().GetString("start-url") + timezone, _ := cmd.Flags().GetString("timezone") + clearTimezone, _ := cmd.Flags().GetBool("clear-timezone") + locale, _ := cmd.Flags().GetString("locale") + clearLocale, _ := cmd.Flags().GetBool("clear-locale") allowedHosts, _ := cmd.Flags().GetStringSlice("allowed-host") clearAllowedHosts, _ := cmd.Flags().GetBool("clear-allowed-hosts") @@ -3706,6 +3795,12 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { TagsProvided: tagsProvided, ClearTags: clearTags, StartURL: startURL, + Timezone: timezone, + SetTimezone: cmd.Flags().Changed("timezone"), + ClearTimezone: clearTimezone, + Locale: locale, + SetLocale: cmd.Flags().Changed("locale"), + ClearLocale: clearLocale, AllowedHosts: allowedHosts, AllowedHostsProvided: cmd.Flags().Changed("allowed-host"), ClearAllowedHosts: clearAllowedHosts, diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index a3def713..f3f3ef4d 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -2527,6 +2527,164 @@ func TestBrowsersCreate_RejectsStartURLFlagToken(t *testing.T) { assert.False(t, called) } +func TestBrowsersCreate_WithLocation(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserNewParams + fake := &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) { + captured = body + var resp kernel.BrowserNewResponse + require.NoError(t, json.Unmarshal([]byte(`{"session_id":"session123","cdp_ws_url":"ws://example","location":{"timezone":"Europe/Berlin","locale":"de-DE","timezone_source":"override","locale_source":"override"}}`), &resp)) + return &resp, nil + }} + b := BrowsersCmd{browsers: fake} + + require.NoError(t, b.Create(context.Background(), BrowsersCreateInput{Timezone: "Europe/Berlin", Locale: "de-DE"})) + + raw, err := json.Marshal(captured) + require.NoError(t, err) + assert.Contains(t, string(raw), `"location":{"locale":"de-DE","timezone":"Europe/Berlin"}`) + assert.Contains(t, outBuf.String(), "Europe/Berlin (override)") + assert.Contains(t, outBuf.String(), "de-DE (override)") +} + +func TestBrowsersCreate_WithoutLocation_NotSent(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserNewParams + fake := &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) { + captured = body + return &kernel.BrowserNewResponse{SessionID: "session123", CdpWsURL: "ws://example"}, nil + }} + b := BrowsersCmd{browsers: fake} + + require.NoError(t, b.Create(context.Background(), BrowsersCreateInput{})) + + raw, err := json.Marshal(captured) + require.NoError(t, err) + assert.NotContains(t, string(raw), `"location"`) + assert.NotContains(t, outBuf.String(), "Timezone") +} + +// A clear flag sends null for that override; an omitted flag leaves it unchanged. +func TestBrowsersUpdate_Location(t *testing.T) { + tests := []struct { + name string + in BrowsersUpdateInput + want string + }{ + {"sets timezone only", BrowsersUpdateInput{Timezone: "America/Los_Angeles", SetTimezone: true}, `{"timezone":"America/Los_Angeles"}`}, + {"sets locale only", BrowsersUpdateInput{Locale: "ja-JP", SetLocale: true}, `{"locale":"ja-JP"}`}, + {"clears timezone", BrowsersUpdateInput{ClearTimezone: true}, `{"timezone":null}`}, + {"clears both", BrowsersUpdateInput{ClearTimezone: true, ClearLocale: true}, `{"locale":null,"timezone":null}`}, + {"clears one and sets the other", BrowsersUpdateInput{ClearTimezone: true, Locale: "fr-FR", SetLocale: true}, `{"locale":"fr-FR","timezone":null}`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupStdoutCapture(t) + fake, captured := captureUpdateParams(t) + b := BrowsersCmd{browsers: fake} + + tt.in.Identifier = "session123" + require.NoError(t, b.Update(context.Background(), tt.in)) + + raw, err := json.Marshal(*captured) + require.NoError(t, err) + assert.Contains(t, string(raw), `"location":`+tt.want) + }) + } +} + +func TestBrowsersUpdate_LocationFlagErrors(t *testing.T) { + tests := []struct { + name string + in BrowsersUpdateInput + wantErr string + }{ + {"empty timezone", BrowsersUpdateInput{SetTimezone: true}, "--timezone requires a non-empty value; use --clear-timezone to go back to the browser region's default"}, + {"empty locale", BrowsersUpdateInput{SetLocale: true}, "--locale requires a non-empty value; use --clear-locale to go back to the browser region's default"}, + {"timezone and clear", BrowsersUpdateInput{Timezone: "Europe/Berlin", SetTimezone: true, ClearTimezone: true}, "cannot specify both --timezone and --clear-timezone"}, + {"locale and clear", BrowsersUpdateInput{Locale: "de-DE", SetLocale: true, ClearLocale: true}, "cannot specify both --locale and --clear-locale"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fake := &FakeBrowsersService{UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserUpdateParams, opts ...option.RequestOption) (*kernel.BrowserUpdateResponse, error) { + t.Fatal("Update should not be called") + return nil, nil + }} + tt.in.Identifier = "session123" + assert.EqualError(t, (BrowsersCmd{browsers: fake}).Update(context.Background(), tt.in), tt.wantErr) + }) + } +} + +func TestBrowsersUpdate_OmitLocation_NotSent(t *testing.T) { + setupStdoutCapture(t) + fake, captured := captureUpdateParams(t) + b := BrowsersCmd{browsers: fake} + + require.NoError(t, b.Update(context.Background(), BrowsersUpdateInput{Identifier: "session123", Name: "renamed", SetName: true})) + + raw, err := json.Marshal(*captured) + require.NoError(t, err) + assert.NotContains(t, string(raw), `"location"`) +} + +// Exercise the flag wiring through the run function and check what reaches the API. +func TestBrowsersUpdateCommand_ClearTimezoneAndSetLocale(t *testing.T) { + setupStdoutCapture(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method) + assert.Equal(t, "/browsers/sess-1", r.URL.Path) + payload, _ := io.ReadAll(r.Body) + assert.JSONEq(t, `{"location":{"timezone":null,"locale":"de-DE"}}`, string(payload)) + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"session_id":"sess-1","location":{"timezone":"America/New_York","locale":"de-DE","timezone_source":"default","locale_source":"override"}}`) + })) + t.Cleanup(server.Close) + client := kernel.NewClient(option.WithBaseURL(server.URL), option.WithAPIKey("test")) + cmd := &cobra.Command{Use: "update"} + cmd.SetContext(context.WithValue(context.Background(), util.KernelClientKey, client)) + cmd.Flags().String("timezone", "", "") + cmd.Flags().Bool("clear-timezone", false, "") + cmd.Flags().String("locale", "", "") + cmd.Flags().Bool("clear-locale", false, "") + addJSONOutputFlag(cmd) + require.NoError(t, cmd.ParseFlags([]string{"--clear-timezone", "--locale", "de-DE"})) + + require.NoError(t, runBrowsersUpdate(cmd, []string{"sess-1"})) + assert.Contains(t, outBuf.String(), "Timezone: America/New_York (default)") + assert.Contains(t, outBuf.String(), "Locale: de-DE (override)") +} + +func TestLocationFlagsRegistered(t *testing.T) { + for _, path := range [][]string{{"browsers", "create"}, {"browsers", "update"}} { + cmd, _, err := rootCmd.Find(path) + require.NoError(t, err) + assert.NotNil(t, cmd.Flags().Lookup("timezone")) + assert.NotNil(t, cmd.Flags().Lookup("locale")) + } + update, _, err := rootCmd.Find([]string{"browsers", "update"}) + require.NoError(t, err) + assert.NotNil(t, update.Flags().Lookup("clear-timezone")) + assert.NotNil(t, update.Flags().Lookup("clear-locale")) +} + +func TestBrowsersGet_ShowsLocation(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeBrowsersService{ + GetFunc: func(_ context.Context, _ string, _ kernel.BrowserGetParams, _ ...option.RequestOption) (*kernel.BrowserGetResponse, error) { + var resp kernel.BrowserGetResponse + err := json.Unmarshal([]byte(`{"session_id":"sess-1","location":{"timezone":"Europe/Berlin","locale":"en-US","timezone_source":"override","locale_source":"default"}}`), &resp) + require.NoError(t, err) + return &resp, nil + }, + } + b := BrowsersCmd{browsers: fake} + + require.NoError(t, b.Get(context.Background(), BrowsersGetInput{Identifier: "sess-1"})) + assert.Contains(t, outBuf.String(), "Europe/Berlin (override)") + assert.Contains(t, outBuf.String(), "en-US (default)") +} + func TestBrowsersCreate_WithTelemetry(t *testing.T) { setupStdoutCapture(t) var captured kernel.BrowserNewParams diff --git a/go.mod b/go.mod index 899b3c29..9e8741d2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.126.0 + github.com/kernel/kernel-go-sdk v0.129.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index a8ea4ec3..0269be1b 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.0 h1:uv3jdi4DooDGwZ9oF8WrS0YcakKv3Op/k6B8AlgUgXM= -github.com/kernel/kernel-go-sdk v0.126.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.129.0 h1:kU67s3LnMm1g9gbXZsA9FsAeeUYo04yHeqLzqGCjGOQ= +github.com/kernel/kernel-go-sdk v0.129.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=