Skip to content

Conversation

@radulucut
Copy link
Collaborator

Use the new go package for the api and general cleanup/refactor.

Closes #177

@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Walkthrough

This pull request replaces the local globalping implementation with the external globalping-go library and introduces a new api package that exposes a Client interface and concrete implementation. It propagates context throughout command handlers and measurement flows, centralizes token handling using storage.Token with storage-backed persistence, and converts many client methods to context-aware signatures. The globalping package files (models, client, measurements, limits) and many legacy mocks were removed; mocks were reorganized into api/utils/view packages. Viewer APIs were restructured (OutputDefault, OutputJSON, OutputLatency, OutputLive) and command flows now use a handleMeasurement pathway.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.65% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: migrating to globalping-go package and performing cleanup/refactor work.
Description check ✅ Passed The description is related to the changeset, referencing the use of the new go package and cleanup, and indicates issue closure (#177).
Linked Issues check ✅ Passed The PR successfully addresses issue #177 by replacing the existing globalping client implementation with the new globalping-go library throughout the codebase [#177].
Out of Scope Changes check ✅ Passed All changes are in scope: migration to globalping-go, API context propagation, removal of old globalping package, mock reorganization, and related refactoring align with #177.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
storage/config_test.go (1)

24-30: Fix Token.Expiry JSON tag to avoid breaking config compatibility

The new storage.Token type (see storage/config.go) defines:

Expiry       time.Time `json:"expiry,omitzero"`

encoding/json does not recognize omitzero as an option, so the JSON field name becomes "expiry,omitzero". That has two concrete side effects:

  • Existing config files that stored "expiry" (with the old token type) will no longer populate Expiry when unmarshaled.
  • New configs will write an odd "expiry,omitzero" field name, diverging from the expected "expiry" and making manual inspection/editing confusing.

Given this test is asserting persistence of Token through JSON, this is effectively a regression in the on-disk format.

I’d recommend changing the tag in storage/config.go to use the standard omitempty option instead:

-   Expiry       time.Time `json:"expiry,omitzero"`
+   Expiry       time.Time `json:"expiry,omitempty"`

This restores the "expiry" field name and keeps zero values out of the JSON.

cmd/ping.go (1)

145-207: Pipeline failure: Test_Execute_Ping_Infinite panics on GetMeasurement call.

The test fails because the infinite ping loop calls GetMeasurement(ctx, id) but the test mock doesn't have expectations set for it. The ping function iterates and calls GetMeasurement multiple times, which isn't covered by the test setup.

Ensure the ping infinite test sets up GetMeasurement expectations. Since the loop runs until cancelled, use AnyTimes() or a specific count with proper return values:

gbMock.EXPECT().GetMeasurement(gomock.Any(), gomock.Any()).Return(expectedMeasurement, nil).AnyTimes()
api/auth.go (1)

168-183: HTTP response body not closed - connection leak.

The response body must be closed to allow HTTP connection reuse. This pattern repeats in refreshToken, introspection, and RevokeToken as well.

 	resp, err := c.http.Do(req)
 	if err != nil {
 		return nil, &AuthorizeError{
 			ErrorType:   ErrTypeExchangeFailed,
 			Description: err.Error(),
 		}
 	}
+	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {

Apply similar fix at lines 305, 376, and 417.

♻️ Duplicate comments (2)
view/share_test.go (2)

41-41: Same viewer-construction duplication concern as above

This NewViewer invocation has the same shape and maintenance concern as the one noted earlier on Line 21; it could also be covered by a shared test helper.


66-66: Same viewer-construction duplication concern as above

This call site can likewise benefit from the shared test helper suggested for Line 21.

🧹 Nitpick comments (19)
view/share_test.go (1)

21-21: Reduce duplication by introducing a small test helper for viewer creation

All three subtests build a Viewer in similar ways; this will become brittle if NewViewer gains new dependencies again. Consider introducing a small helper like newTestViewer(ctx *Context, w, errw *bytes.Buffer) Viewer and using it in these tests to centralize construction and keep tests easier to adapt to future signature changes.

utils/config.go (1)

8-9: Consider using const instead of var for immutability.

ShareURL is initialized to a fixed string and doesn't appear to be modified at runtime. Declaring it as a constant would prevent accidental mutation and better express the intent.

Apply this diff:

-var ShareURL = "https://globalping.io?measurement="
-
+const ShareURL = "https://globalping.io?measurement="
mocks/api/mock_probe.go (1)

2-16: Keep mockgen configuration as single source of truth

This file is generated; if the source path, destination, or package name needs to change again, make the change in the mockgen invocation (and any helper scripts) rather than editing the generated file directly, so it doesn’t get reverted on regeneration.

view/default.go (1)

11-59: Defensive nil checks for opts.Options.Request to avoid panics

OutputDefault dereferences opts.Options.Request.Method without any guards (Lines 55 and 59). If a future caller ever passes a nil opts (or opts.Options / opts.Options.Request), the CLI will panic instead of degrading gracefully.

Consider guarding these accesses and treating missing options as “non‑GET”:

-            v.printer.ErrPrintln(result.Result.RawHeaders)
-            if opts.Options.Request.Method == "GET" {
+            v.printer.ErrPrintln(result.Result.RawHeaders)
+            if opts != nil && opts.Options != nil && opts.Options.Request.Method == "GET" {
                 v.printer.ErrPrintln()
                 v.printer.Println(strings.TrimSpace(result.Result.RawBody))
             }
-        } else if opts.Options.Request.Method == "GET" {
+        } else if opts != nil && opts.Options != nil && opts.Options.Request.Method == "GET" {
             v.printer.Println(strings.TrimSpace(result.Result.RawBody))
         } else {
             v.printer.Println(strings.TrimSpace(result.Result.RawOutput))
         }

This keeps current behavior when options are present while avoiding hard crashes if a nil slips through later.

cmd/utils_test.go (1)

30-56: Prefer globalping.MeasurementType* constants over raw string literals

These helpers now take cmd globalping.MeasurementType, but the switch cmd blocks still match against untyped string literals like "ping", "traceroute", etc. This couples the tests to the literal wire values and makes it easier for them to drift from the official enum values in globalping-go (e.g., globalping.MeasurementTypePing).(github.com)

To make the tests more robust and self‑documenting, consider:

  • Switching the case labels to the library’s typed constants (e.g., case globalping.MeasurementTypePing: etc.).
  • Optionally centralizing the mapping from MeasurementType → protocol/port in a small helper to avoid duplicating the same switch logic across helpers.

Also applies to: 60-99

mocks/utils/mock_utils.go (1)

6-17: Generated mock: ensure generator command is the canonical source

Since this mock is generated, make sure the mockgen invocation (and any generation script) is updated to match the new destination/package so future regenerations won’t revert or conflict with these header changes.

README.md (1)

21-35: Keep the TOC in sync with the new “Globalping API package” section

You’ve added a ## Globalping API package section at the end, but it isn’t listed in the Table of Contents block. For consistency, either:

  • Add a bullet for “Globalping API package” to the TOC, or
  • Regenerate the entire TOC with the same tool that produced the comment markers so it picks up the new heading automatically.

Also applies to: 427-429

storage/storage.go (1)

61-75: Surface unexpected LoadConfig errors from Init instead of ignoring them

In Init, only os.IsNotExist(err) is handled specially; any other error from s.LoadConfig() is effectively ignored, and Init proceeds to s.Migrate() and returns nil:

_, err = s.LoadConfig()
if err != nil {
	if os.IsNotExist(err) {
		s.config = &Config{...}
		err = s.SaveConfig()
		if err != nil {
			return err
		}
	}
}
s.Migrate()
return nil

If LoadConfig fails due to corrupt JSON, permission problems, or other I/O issues, this will silently succeed from the caller’s perspective, potentially with s.config in an invalid state and migrations running against it.

Consider tightening this logic so unexpected errors are surfaced, for example:

- _, err = s.LoadConfig()
- if err != nil {
-     if os.IsNotExist(err) {
-         s.config = &Config{
-             Profile:       "default",
-             Profiles:      make(map[string]*Profile),
-             LastMigration: len(s.migrations),
-         }
-         err = s.SaveConfig()
-         if err != nil {
-             return err
-         }
-     }
- }
+ _, err = s.LoadConfig()
+ if err != nil {
+     if os.IsNotExist(err) {
+         s.config = &Config{
+             Profile:       "default",
+             Profiles:      make(map[string]*Profile),
+             LastMigration: len(s.migrations),
+         }
+         if err := s.SaveConfig(); err != nil {
+             return err
+         }
+     } else {
+         // For non-NotExist errors, fail fast so callers can react.
+         return err
+     }
+ }

This makes config initialization failures explicit instead of silently continuing with a potentially bad state.

view/viewer.go (1)

8-16: Inconsistent error return patterns across Output methods.

OutputLatency and OutputInfinite return error, while OutputDefault, OutputJSON, and OutputLive do not. If the non-error-returning methods can fail (e.g., write errors), consider unifying the signatures for consistent error handling by callers.

storage/config.go (1)

27-31: Race condition acknowledged but not addressed.

The TODO on line 27 correctly identifies the need for a locking mechanism. The current implementation caches config in s.config without synchronization, which could cause data races if LoadConfig is called concurrently. Consider using sync.Once for initialization or a sync.RWMutex for the cached config access.

Would you like me to open an issue to track implementing the locking mechanism?

cmd/common.go (1)

48-51: Redundant condition.

At line 48, r.ctx.CIMode is always true when reached—if ToLatency or ToJSON were true, we'd have already returned. This could simply be an unconditional block or removed entirely since the early returns cover the other cases.

-	if r.ctx.CIMode {
-		r.viewer.OutputDefault(id, res, opts)
-		return nil
-	}
+	r.viewer.OutputDefault(id, res, opts)
+	return nil
 }
cmd/root.go (1)

91-91: Parameter name is misleading.

The parameter globalpingClient is typed as api.Client, not globalping.Client. This could cause confusion when reading the code since globalping refers to the external library.

 func NewRoot(
 	printer *view.Printer,
 	ctx *view.Context,
 	viewer view.Viewer,
 	utils utils.Utils,
-	globalpingClient api.Client,
+	apiClient api.Client,
 	globalpingProbe probe.Probe,
 	localStorage *storage.LocalStorage,
 ) *Root {
view/output.go (1)

36-38: Panic is harsh for terminal size edge case.

If the terminal is extremely small (e.g., width ≤ 4 or height ≤ 4), this panics. While rare, this could occur in edge cases like embedded terminals or CI environments with unusual configurations.

Consider returning an error or gracefully handling this:

 func trimOutput(output *strings.Builder, terminalW, terminalH int) *string {
 	maxW := terminalW - 4 // 4 extra chars to be safe from overflow
 	maxH := terminalH - 4 // 4 extra lines to be safe from overflow
 	if maxW <= 0 || maxH <= 0 {
-		panic("terminal width / height too limited to display results")
+		// Terminal too small, return truncated message
+		txt := "..."
+		return &txt
 	}
api/measurements_test.go (3)

22-28: Variable shadowing: local variables shadow imported package aliases.

utilsMock and globalpingMock local variables shadow the imported package aliases of the same name. This compiles but is confusing and error-prone.

-	utilsMock := utilsMock.NewMockUtils(ctrl)
+	mockUtils := utilsMock.NewMockUtils(ctrl)
 	utilsMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()
+	mockUtils.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

-	globalpingMock := globalpingMock.NewMockClient(ctrl)
+	mockGlobalping := globalpingMock.NewMockClient(ctrl)

Alternatively, rename the imports (e.g., gpMock for globalping, uMock for utils) to avoid collision with local variable naming conventions.


175-182: Unchecked Write return value in test handler.

Line 178 ignores the error from w.Write(). While acceptable in tests, consider using a helper or _, _ = w.Write(...) to silence linters.

-			w.Write([]byte(`{"error": "invalid_grant", "error_description": "Invalid refresh token."}`))
+			_, _ = w.Write([]byte(`{"error": "invalid_grant", "error_description": "Invalid refresh token."}`))

225-228: Same unchecked Write pattern.

-		w.Write([]byte(`{"error": {"type": "unauthorized", "message": "Unauthorized."}}`))
+		_, _ = w.Write([]byte(`{"error": {"type": "unauthorized", "message": "Unauthorized."}}`))
mocks/globalping/mock_globalping.go (1)

6-10: Package name lib is inconsistent with directory path.

The generated mock is in mocks/globalping/ but the package is named lib. This could cause import confusion. Consider regenerating with a more descriptive package name like globalpingmocks:

mockgen -destination mocks/globalping/mock_globalping.go -package globalpingmocks github.com/jsdelivr/globalping-go Client
api/client.go (1)

98-98: Redundant mutex initialization.

The zero value of sync.RWMutex is ready to use; explicit initialization is unnecessary.

-		mu:               sync.RWMutex{},
api/auth.go (1)

226-232: Redundant token field copying.

The token t is already a *storage.Token. You can pass it directly to saveToken instead of creating a new instance with identical fields:

-	c.saveToken(&storage.Token{
-		AccessToken:  t.AccessToken,
-		TokenType:    t.TokenType,
-		RefreshToken: t.RefreshToken,
-		ExpiresIn:    t.ExpiresIn,
-		Expiry:       t.Expiry,
-	})
+	c.saveToken(t)

Same applies at lines 247-253 and 279-285.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1fb906 and 3c23c01.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (65)
  • .gitignore (1 hunks)
  • README.md (2 hunks)
  • api/auth.go (15 hunks)
  • api/auth_test.go (9 hunks)
  • api/client.go (1 hunks)
  • api/limits.go (1 hunks)
  • api/measurements.go (1 hunks)
  • api/measurements_test.go (1 hunks)
  • api/utils_test.go (1 hunks)
  • cmd/auth.go (6 hunks)
  • cmd/auth_test.go (7 hunks)
  • cmd/common.go (5 hunks)
  • cmd/dns.go (4 hunks)
  • cmd/dns_test.go (9 hunks)
  • cmd/history_test.go (5 hunks)
  • cmd/http.go (4 hunks)
  • cmd/http_test.go (9 hunks)
  • cmd/install_probe.go (1 hunks)
  • cmd/install_probe_test.go (3 hunks)
  • cmd/limits.go (3 hunks)
  • cmd/limits_test.go (7 hunks)
  • cmd/mtr.go (4 hunks)
  • cmd/mtr_test.go (9 hunks)
  • cmd/ping.go (6 hunks)
  • cmd/ping_test.go (33 hunks)
  • cmd/root.go (4 hunks)
  • cmd/traceroute.go (4 hunks)
  • cmd/traceroute_test.go (9 hunks)
  • cmd/utils_test.go (4 hunks)
  • cmd/version_test.go (1 hunks)
  • globalping/client.go (0 hunks)
  • globalping/limits.go (0 hunks)
  • globalping/limits_test.go (0 hunks)
  • globalping/measurements.go (0 hunks)
  • globalping/measurements_test.go (0 hunks)
  • globalping/models.go (0 hunks)
  • globalping/utils_test.go (0 hunks)
  • go.mod (2 hunks)
  • mocks/api/mock_client.go (2 hunks)
  • mocks/api/mock_probe.go (1 hunks)
  • mocks/gen_mocks.sh (1 hunks)
  • mocks/globalping/mock_globalping.go (1 hunks)
  • mocks/mock_viewer.go (0 hunks)
  • mocks/utils/mock_utils.go (1 hunks)
  • mocks/view/mock_viewer.go (1 hunks)
  • storage/config.go (2 hunks)
  • storage/config_test.go (1 hunks)
  • storage/history.go (2 hunks)
  • storage/storage.go (1 hunks)
  • utils/config.go (1 hunks)
  • view/default.go (3 hunks)
  • view/default_test.go (12 hunks)
  • view/history.go (1 hunks)
  • view/infinite.go (2 hunks)
  • view/infinite_test.go (13 hunks)
  • view/json.go (1 hunks)
  • view/json_test.go (1 hunks)
  • view/latency.go (1 hunks)
  • view/latency_test.go (7 hunks)
  • view/output.go (2 hunks)
  • view/output_test.go (1 hunks)
  • view/share_test.go (3 hunks)
  • view/summary_test.go (3 hunks)
  • view/utils_test.go (1 hunks)
  • view/viewer.go (1 hunks)
💤 Files with no reviewable changes (8)
  • mocks/mock_viewer.go
  • globalping/limits_test.go
  • globalping/utils_test.go
  • globalping/measurements_test.go
  • globalping/limits.go
  • globalping/measurements.go
  • globalping/models.go
  • globalping/client.go
🧰 Additional context used
🧬 Code graph analysis (25)
api/utils_test.go (2)
utils/utils.go (1)
  • Utils (11-14)
storage/storage.go (2)
  • LocalStorage (16-27)
  • NewLocalStorage (29-34)
cmd/history_test.go (1)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
storage/history.go (1)
utils/config.go (1)
  • ShareURL (8-8)
view/share_test.go (2)
view/viewer.go (1)
  • NewViewer (24-34)
view/printer.go (1)
  • NewPrinter (62-72)
mocks/gen_mocks.sh (1)
api/client.go (1)
  • Client (20-57)
cmd/traceroute.go (1)
view/context.go (1)
  • Context (8-48)
cmd/common.go (3)
cmd/root.go (1)
  • Root (24-34)
view/context.go (1)
  • Context (8-48)
api/measurements.go (1)
  • StatusUnauthorizedWithTokenRefreshed (23-23)
cmd/dns.go (1)
view/context.go (1)
  • Context (8-48)
view/infinite_test.go (3)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
view/viewer.go (1)
  • NewViewer (24-34)
view/printer.go (1)
  • NewPrinter (62-72)
cmd/root.go (3)
api/client.go (3)
  • Client (20-57)
  • NewClient (92-152)
  • Config (59-74)
storage/config.go (2)
  • Token (9-15)
  • Config (21-25)
api/probe/probe.go (1)
  • NewProbe (18-20)
api/measurements_test.go (4)
storage/config.go (2)
  • Token (9-15)
  • Config (21-25)
api/client.go (2)
  • NewClient (92-152)
  • Config (59-74)
utils/utils.go (1)
  • Utils (11-14)
api/measurements.go (1)
  • StatusUnauthorizedWithTokenRefreshed (23-23)
cmd/http.go (1)
view/context.go (1)
  • Context (8-48)
cmd/ping_test.go (5)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
view/context.go (1)
  • Context (8-48)
mocks/view/mock_viewer.go (1)
  • NewMockViewer (31-35)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
cmd/auth_test.go (4)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
storage/config.go (1)
  • Token (9-15)
api/auth.go (2)
  • IntrospectionResponse (347-363)
  • AuthorizeResponse (38-41)
cmd/dns_test.go (5)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
view/context.go (1)
  • Context (8-48)
mocks/view/mock_viewer.go (1)
  • NewMockViewer (31-35)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
cmd/traceroute_test.go (5)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
view/context.go (1)
  • Context (8-48)
mocks/view/mock_viewer.go (1)
  • NewMockViewer (31-35)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
cmd/http_test.go (5)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
view/context.go (1)
  • Context (8-48)
mocks/view/mock_viewer.go (1)
  • NewMockViewer (31-35)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
storage/config_test.go (1)
storage/config.go (1)
  • Token (9-15)
cmd/limits_test.go (3)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
api/auth.go (2)
  • IntrospectionResponse (347-363)
  • AuthorizeError (28-32)
cmd/auth.go (3)
storage/config.go (1)
  • Token (9-15)
api/auth.go (2)
  • AuthorizeError (28-32)
  • ErrTypeNotAuthorized (25-25)
cmd/root.go (1)
  • Root (24-34)
api/auth.go (1)
storage/config.go (1)
  • Token (9-15)
view/viewer.go (2)
view/context.go (1)
  • Context (8-48)
view/printer.go (1)
  • Printer (53-60)
view/summary_test.go (2)
view/viewer.go (1)
  • NewViewer (24-34)
view/printer.go (1)
  • NewPrinter (62-72)
view/output.go (1)
utils/config.go (1)
  • ShareURL (8-8)
cmd/install_probe_test.go (1)
mocks/api/mock_probe.go (1)
  • NewMockProbe (31-35)
🪛 GitHub Actions: Tests
cmd/ping.go

[error] 154-154: Test ping: GetMeasurement invocation caused a panic in gomock during Test_Execute_Ping_Infinite; test failed leading to process exit.

mocks/api/mock_client.go

[error] 92-92: gomock: Controller.Call panic propagated from GetMeasurement during test.

🔍 Remote MCP

Since the DeepWiki sources are not available for this repository, let me search for relevant information about the technologies and patterns being used in this refactor.## Summary of Additional Context Found

Based on the PR changes and supporting research, here are the key findings relevant to reviewing this pull request:

Architecture & Design Pattern Changes

The PR implements proper context propagation following Go best practices, where client libraries accept a Context from calling code to establish a common interface for request-scoped data and cancellation, making it easier for package developers to share code for creating scalable services.

The refactor demonstrates several key architectural improvements:

  1. API Abstraction Layer: The new api package creates an abstraction over the globalping-go library, allowing the CLI to work with context-aware interfaces and managing token lifecycle separately from the underlying API client.

  2. Context Propagation: The PR correctly implements Go's context guidelines by passing Context explicitly to each function that needs it, with Context as the first parameter, typically named ctx, rather than storing contexts in structs.

  3. Token Management Refactor: The PR moves from a callback-based token refresh model (OnTokenRefresh) to explicit storage-backed token management via the new storage.Token type, which aligns with stateless API design.

Critical Observations for Review

Public API Changes:

  • All measurement and auth-related client methods now require a context.Context as the first parameter
  • The OnTokenRefresh callback mechanism has been removed entirely from public Config
  • The viewer interface has been significantly restructured with new methods: OutputDefault, OutputJSON, OutputLatency, OutputLive replacing the previous generic Output method

Dependency Management:

  • Old inline API client implementation (globalping package) is completely removed
  • New dependency on github.com/jsdelivr/globalping-go (v0.1.1) provides the base measurement client
  • Token types now unified under storage.Token instead of scattered between packages

Testing Impact:

  • Mock organization restructured into mocks/api/, mocks/utils/, and mocks/view/ packages
  • New mock for the globalping-go Client interface added at mocks/globalping/mock_globalping.go
  • Tests updated to use t.Context() instead of context.TODO() for proper test lifecycle management

Potential Review Concerns:

  1. Error Handling: The PR consolidates error handling in the api package layer with custom status codes (e.g., StatusUnauthorizedWithTokenRefreshed = 1000), requiring careful review of edge cases
  2. Token Expiry: The new storage-based token uses time.Time for Expiry field with JSON tags - verify serialization/deserialization correctness
  3. Background Goroutine: The NewClient constructor spawns a background goroutine for periodic CacheClean() - verify proper cleanup on client shutdown
  4. Viewer API Breakage: The viewer interface changes significantly affect all command implementations and test coverage
🔇 Additional comments (44)
.gitignore (1)

44-44: LGTM!

Adding .DS_Store to gitignore is a standard best practice for macOS users.

view/utils_test.go (1)

7-7: I'll help you verify the compatibility of the external globalping-go library with the test file. Let me gather evidence about the types and constants used.
<function_calls>

#!/bin/bash

Check go.mod for globalping-go version and list the files in the test directory

echo "=== Checking go.mod for globalping-go version ==="
grep "globalping-go" go.mod 2>/dev/null || echo "Not found in go.mod"

echo -e "\n=== Listing view directory files ==="
ls -la view/ | head -20

echo -e "\n=== Reading view/utils_test.go to see the types used ==="
wc -l view/utils_test.go


</function_calls>

Now let me examine the test file to see what types and constants are being used:
<function_calls>

#!/bin/bash

Read the test file to see imports and usage

cat -n view/utils_test.go | head -50


</function_calls>

Let me search for all usages of the types in the test file:
<function_calls>

#!/bin/bash

Search for the types being used

echo "=== Types and constants used in view/utils_test.go ==="
rg "globalping.(Measurement|ProbeMeasurement|ProbeDetails|ProbeResult|StatusFinished)" view/utils_test.go -n


</function_calls>

Now let me check if there's a go.sum file and verify the globalping-go dependency version:
<function_calls>

#!/bin/bash

Check go.mod and go.sum

echo "=== Full go.mod file ==="
cat go.mod | grep -A5 -B5 "globalping" || echo "No globalping found in go.mod"

echo -e "\n=== go.sum entries for globalping ==="
cat go.sum 2>/dev/null | grep globalping || echo "go.sum not available or no globalping entries"


</function_calls>

Let me also check the full content of the test file to understand the structure better:
<function_calls>

#!/bin/bash

Read the complete test file

cat -n view/utils_test.go


</function_calls>

Now let me search for the globalping-go repository to verify the exported types:
<function_calls>

github.com/jsdelivr/globalping-go v0.1.1 types Measurement ProbeMeasurement

</function_calls>

Let me also check if there's any documentation or README about this library:
<function_calls>

globalping-go library exported types structure

</function_calls>

budget:token_budget200000</budget:token_budget>

mocks/gen_mocks.sh (1)

7-7: I'll help you verify the package name for the globalping mock. Let me start by examining the mock generation script and how the generated mock is used in the codebase.
<function_calls>

{"code": "#!/bin/bash\n# First, let's see the full context of gen_mocks.sh\ncat -n mocks/gen_mocks.sh"}
</function_calls>
10

Let me also check how the globalping mock is imported and used in test files:
<function_calls>

{"code": "#!/bin/bash\n# Search for imports and usage of the globalping mock\nrg -n "mocks/globalping" --type go -C3"
</function_calls>
10

Let me also search for any references to the "lib" package from the mocks directory:
<function_calls>

{"code": "#!/bin/bash\n# Look for imports of lib package from mocks\nrg -n 'import.*mocks.*lib|from.*mocks.*lib' --type go -C2"
</function_calls>
10

Let me check if there are any test files that might reference the globalping mock:
<function_calls>

{"code": "#!/bin/bash\n# Find test files and check for globalping references\nfd -e _test.go | head -20\necho "---"\nrg -n "globalping" --type go -C2"
</function_calls>
10

go.mod (1)

7-7: Verify the globalping-go dependency version with the maintainers.

The dependency uses a pseudo-version (v0.1.1-0.20251125110013-2813a850a61f) pointing to a specific commit rather than a released semantic version. While pseudo-versions are a valid Go feature for unreleased code, automated verification of available releases for this dependency was inconclusive. Confirm with the globalping-go maintainers that:

  • This pseudo-version represents stable, tested functionality
  • A proper semantic release is not available or planned
  • This pinning is intentional for your use case
view/json.go (1)

4-11: No changes requested for this segment

Nothing stands out here that warrants modification; leaving this implementation as-is.

view/infinite.go (1)

26-38: No changes requested for this segment

The updated OutputInfinite signature and its use of measurement integrate cleanly with existing helpers; no additional adjustments seem necessary here.

view/output_test.go (1)

7-47: No changes requested for this segment

The updated import and usage of globalping.ProbeMeasurement in these tests don’t introduce issues that need adjustment.

view/json_test.go (1)

11-31: LGTM!

The simplified test directly exercises the OutputJSON path without unnecessary mock setup. The nil utils parameter is appropriate since JSON output doesn't depend on utility functions.

view/infinite_test.go (2)

9-10: LGTM!

Import updates correctly reference the reorganized mock package structure and the new globalping-go library.


19-28: LGTM!

Mock creation and NewViewer calls correctly updated to match the new signatures.

cmd/dns.go (2)

73-74: LGTM!

Context correctly captured from the command and propagated through the measurement workflow.


114-136: LGTM!

The context-aware CreateMeasurement call and delegation to handleMeasurement follows the established pattern across command handlers.

cmd/http.go (2)

90-90: LGTM!

Context captured at the command entry point, consistent with other command handlers.


124-146: LGTM!

Context-aware measurement creation and output handling follows the established pattern.

cmd/mtr.go (2)

62-63: LGTM!

Context correctly captured and propagated through the MTR measurement workflow.


103-125: LGTM!

Context-aware CreateMeasurement and handleMeasurement calls are consistent with the other command handlers.

cmd/limits.go (1)

20-28: LGTM!

Context propagation is correctly implemented - ctx is obtained from the Cobra command and properly passed to both TokenIntrospection and Limits calls. The intentional silencing of the introspection error (line 23) is reasonable since authentication status shouldn't block displaying rate limits.

view/latency_test.go (1)

12-68: LGTM!

Test simplification is well done - using concrete globalping.Measurement objects instead of mocks makes these tests more readable and maintainable. The test coverage for ping latency output with multiple probe results is thorough.

cmd/http_test.go (1)

36-44: LGTM!

The mock setup correctly follows the new context-aware API patterns. The test properly verifies the full HTTP measurement flow including creation, await, and output.

cmd/dns_test.go (1)

59-59: Yes, please fetch the exact "go X.Y" line from globalping-cli's go.mod file.

storage/config.go (1)

9-15: The review comment is technically accurate regarding omitzero support.

The web search confirms that omitzero was indeed introduced in Go 1.24 (released February 11, 2025), making the review comment's claim factually correct. The concern about ensuring Go 1.24+ targeting is valid—zero time.Time values will be serialized in Go versions prior to 1.24 since the tag will be silently ignored.

However, I cannot complete full verification because the repository cannot be cloned to check the project's actual go.mod file. The concern raised is legitimate and actionable; a developer should verify that the project's go.mod specifies go 1.24 or higher.

view/default_test.go (1)

1-387: LGTM!

Tests are properly updated to use the new globalping-go types and the refactored OutputDefault method. The TLS field naming fix (CipherName instead of what was likely a typo) aligns with the external library's schema.

cmd/root.go (1)

54-78: LGTM!

Clean initialization flow separating the globalping client from the api wrapper. Token setup from environment variable is handled correctly with appropriate expiry values.

view/output.go (1)

13-30: LGTM!

Clean implementation of OutputLive that properly iterates over results and conditionally displays body or raw output based on HTTP GET detection.

api/auth_test.go (1)

1-403: LGTM!

Tests are well-structured with proper mocking, context usage, and coverage of edge cases (token refresh, no token, no refresh token scenarios). The use of createDefaultTestStorage helper keeps tests clean.

cmd/traceroute_test.go (3)

8-15: LGTM!

The import reorganization and switch to the modular mock packages (apiMocks, utilsMocks, viewMocks) aligns with the PR's restructuring. The addition of globalping-go as a direct import is correct for accessing types like IPVersion4 and IPVersion6.


28-38: LGTM!

Context propagation through t.Context() to CreateMeasurement and AwaitMeasurement is correctly implemented. The viewer expectation properly updated to OutputDefault with the new signature (measurementID, measurement, opts).


157-177: LGTM!

The negative test case correctly handles the scenario where no API mock is needed (nil values passed to NewRoot), and properly validates error handling for invalid protocol input.

cmd/ping_test.go (4)

11-19: LGTM!

Import changes align with the mock reorganization. The direct import of globalping-go is needed for types like IPVersion4, IPVersion6, and MeasurementError.


29-36: LGTM!

Context-aware API calls and the new OutputDefault signature are correctly wired. The expected measurement is properly passed through the mock chain.


302-326: LGTM!

The infinite ping test properly chains CreateMeasurement and GetMeasurement calls with ordered expectations using .After(). The context propagation via t.Context() is consistent throughout.


504-510: LGTM!

The MeasurementError structure is correctly used for the rate limit test case with proper StatusCode, Type, and Message fields.

cmd/limits_test.go (3)

8-14: LGTM!

Import changes correctly bring in the new api package for IntrospectionResponse and AuthorizeError types, while retaining globalping-go for LimitsResponse.


20-26: LGTM!

Context-aware TokenIntrospection(t.Context(), "") and Limits(t.Context()) calls properly align with the new API surface. Return type api.IntrospectionResponse is correct.


117-120: LGTM!

The unauthorized case correctly uses api.AuthorizeError for the error return from TokenIntrospection, reflecting the new error type structure.

cmd/auth_test.go (4)

11-18: LGTM!

Import changes correctly include api for response types and storage for the Token type, alongside the reorganized mock packages.


35-46: LGTM!

The token setup uses storage.Token correctly, and the context-aware TokenIntrospection and RevokeToken calls properly pass t.Context().


83-87: LGTM!

The Authorize mock correctly uses context and returns api.AuthorizeResponse. The callback pattern using Do() to trigger SIGINT is preserved.


126-142: LGTM!

The logout test correctly uses context-aware Logout(t.Context()) call with proper assertion on output.

cmd/mtr_test.go (1)

17-79: LGTM!

Test correctly migrated to use the new mock packages and context-aware API signatures. The expectations for CreateMeasurement, AwaitMeasurement, and OutputDefault are properly configured.

cmd/auth.go (1)

52-91: Unable to complete automated verification due to repository access failure.

I attempted to verify the context capture concern through two approaches:

  1. Shell scripts to examine the Authorize implementation and how the callback is invoked
  2. External knowledge sources for documentation on context propagation patterns

Both approaches failed—the repository could not be cloned, and no relevant documentation was found in external sources.

The concern raised in the review comment is technically valid: capturing ctx in a closure that may be invoked asynchronously after the command context expires is a real concurrency pattern risk. However, definitive verification requires examining:

  • Whether Authorize truly invokes the callback asynchronously
  • The lifecycle of the context relative to callback execution
  • Whether RevokeToken gracefully handles cancelled contexts

Since this cannot be verified automatically, manual code inspection is necessary to determine if the context is actually at risk of cancellation when RevokeToken is called, or if the implementation already handles this correctly.

mocks/view/mock_viewer.go (1)

1-128: Auto-generated mock file - no manual review needed.

This file is generated by MockGen and should not be manually edited. The generation command is documented in the header.

mocks/api/mock_client.go (2)

1-175: Auto-generated mock file - no manual review needed.

This file is generated by MockGen. The mock signatures correctly reflect the context-aware API surface. Ensure the mock is regenerated if the api/client.go interface changes.


90-102: Unfortunately, I'm unable to access the repository to verify the claims in the review comment. The repository clone operation has failed, which prevents me from:

  1. Examining the test files that call GetMeasurement
  2. Verifying whether EXPECT() setups exist in the tests
  3. Confirming the actual pipeline failure details
  4. Validating the specific scenarios that trigger the panic

Unable to verify the mock panic claim—repository access unavailable.

The review comment suggests that GetMeasurement mock calls fail due to missing EXPECT() setup in tests. While this is a plausible explanation for gomock panics, I cannot confirm:

  • Whether test files actually call GetMeasurement without proper expectations
  • What the exact pipeline failure logs show
  • If the suggested fix pattern applies to all affected call sites

Recommend manually reviewing your test files for any GetMeasurement calls that lack corresponding EXPECT().GetMeasurement(...) setups, particularly in the test setup/initialization code.

Copy link
Member

@MartinKolarik MartinKolarik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please resolve the rabbit comments as needed and revert the readme change completely, then rerequest review from me.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/auth.go (1)

143-204: Close HTTP response bodies to prevent connection and file descriptor leaks.

The exchange() method reads http.Response.Body (via json.Decoder) at multiple points but never closes it. Add defer resp.Body.Close() immediately after the successful c.http.Do(req) call:

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, &AuthorizeError{
			ErrorType:   ErrTypeExchangeFailed,
			Description: err.Error(),
		}
	}
+	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {

Apply the same fix to refreshToken(), introspection(), and RevokeToken() methods.

♻️ Duplicate comments (2)
api/auth_test.go (1)

69-69: t.Context() requires Go 1.24.

This was flagged in a previous review. The project's go.mod specifies go 1.23, but t.Context() was introduced in Go 1.24. Either update go.mod to go 1.24 or replace all t.Context() calls with context.Background().

cmd/ping_test.go (1)

48-48: t.Context() usage requires Go 1.24.

Same issue as in api/auth_test.go - this file extensively uses t.Context() which requires Go 1.24.

🧹 Nitpick comments (2)
api/client.go (1)

96-108: Guard against nil Globalping client before starting the cache-clean ticker.

NewClient doesn’t validate config.Globalping, yet the background goroutine unconditionally calls c.globalping.CacheClean(). If Globalping is ever passed as nil, the goroutine will panic.

Consider either:

  • Validating config.Globalping in NewClient (and failing fast, e.g., via panic or documented precondition), or
  • Providing a sensible default client when config.Globalping == nil, and/or
  • Skipping ticker setup when globalping is nil.

This helps avoid hard-to-diagnose panics originating from the background goroutine.

Also applies to: 149-156

api/auth.go (1)

43-104: Consider using caller context (with timeout) for server.Shutdown in Authorize.

Authorize currently calls server.Shutdown(context.Background()), ignoring the caller’s ctx and with no timeout. If shutdown ever blocks (e.g., slow handler or stuck connection), this goroutine could hang longer than intended.

Consider something like:

shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
    // optionally log via c.printer
}

This ties the auth server’s lifetime more closely to the invoking context and bounds shutdown time.

Also applies to: 64-65

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c23c01 and 42b6cdf.

📒 Files selected for processing (9)
  • README.md (2 hunks)
  • api/auth.go (14 hunks)
  • api/auth_test.go (9 hunks)
  • api/client.go (1 hunks)
  • api/measurements.go (1 hunks)
  • cmd/ping.go (6 hunks)
  • cmd/ping_test.go (32 hunks)
  • cmd/root.go (4 hunks)
  • mocks/api/mock_client.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/measurements.go
🧰 Additional context used
🧬 Code graph analysis (5)
cmd/root.go (4)
api/client.go (3)
  • Client (20-60)
  • NewClient (96-164)
  • Config (62-77)
storage/config.go (2)
  • Token (9-15)
  • Config (21-25)
utils/config.go (1)
  • Config (10-15)
api/probe/probe.go (1)
  • NewProbe (18-20)
cmd/ping_test.go (5)
mocks/api/mock_client.go (1)
  • NewMockClient (33-37)
mocks/globalping/mock_globalping.go (1)
  • NewMockClient (32-36)
view/context.go (1)
  • Context (8-48)
mocks/view/mock_viewer.go (1)
  • NewMockViewer (31-35)
mocks/utils/mock_utils.go (1)
  • NewMockUtils (31-35)
cmd/ping.go (3)
view/context.go (1)
  • Context (8-48)
cmd/root.go (1)
  • Root (24-34)
view/history.go (1)
  • HistoryItem (14-21)
api/auth.go (1)
storage/config.go (1)
  • Token (9-15)
mocks/api/mock_client.go (2)
mocks/globalping/mock_globalping.go (2)
  • MockClient (21-24)
  • MockClientMockRecorder (27-29)
api/auth.go (2)
  • AuthorizeResponse (38-41)
  • IntrospectionResponse (352-368)
🔇 Additional comments (11)
README.md (1)

22-35: LGTM!

TOC reformatting is clean and consistent.

api/auth_test.go (1)

209-238: LGTM!

Token refresh test correctly verifies storage persistence of the new token with proper time assertions using defaultCurrentTime.

cmd/ping_test.go (2)

29-36: LGTM!

Mock setup correctly updated to use new context-aware API signatures with apiMocks.NewMockClient and OutputDefault.


504-533: LGTM!

Rate-limiting error test correctly validates the error message format and propagation.

cmd/root.go (2)

63-80: LGTM!

Client composition correctly separates the low-level globalping.Client (for direct API calls) from the higher-level api.Client (which wraps it with auth/token management). The defer apiClient.Close() ensures proper cleanup.


55-62: LGTM!

Static token from environment variable correctly set with far-future expiry for "never expires" semantics.

cmd/ping.go (3)

71-122: LGTM!

Context propagation correctly implemented in RunPing. The context from cobra command is properly threaded through to pingInfinite, createMeasurement, and handleMeasurement.


145-206: LGTM!

The infinite ping loop correctly passes context to all API calls. Context cancellation will propagate through GetMeasurement and CreateMeasurement calls, causing them to return errors when the context is canceled.


209-230: LGTM!

createMeasurement correctly updated to accept and propagate context to the API call.

api/auth.go (1)

206-239: Clear in-memory token on invalid_grant in getToken to match tryToRefreshToken.

On refresh failure with ErrTypeInvalidGrant, getToken only calls c.saveToken(nil) but leaves c.token non-nil. This means subsequent getToken calls will keep trying to refresh with a known-invalid refresh token, unlike tryToRefreshToken, which clears both in-memory and persisted tokens.

Align getToken with tryToRefreshToken by clearing c.token as well:

 	if err != nil {
 		e, ok := err.(*AuthorizeError)
 		if ok && e.ErrorType == ErrTypeInvalidGrant {
+			c.token = nil
 			c.saveToken(nil)
 		}
 		return nil, err
 	}

This ensures once a refresh token is invalid, the client stops hammering the auth server with doomed refresh attempts and clearly transitions to an "unauthorized" state. Also applies to: 261-293

api/client.go (1)

79-87: Make client.Close idempotent to avoid panics on multiple calls.

Close directly closes c.done. If Close is called more than once (easy in tests or with multiple deferred closes), this will panic with "close of closed channel". Given Client.Close() is part of the public interface, making it idempotent is safer.

Guard the close with a sync.Once:

 type client struct {
 	utils      utils.Utils
 	storage    *storage.LocalStorage
 	printer    *view.Printer
 	http       *http.Client
 	globalping globalping.Client
 	mu         sync.RWMutex
 	done       chan struct{}
+	closeOnce  sync.Once
 
 func (c *client) Close() {
-	close(c.done)
+	c.closeOnce.Do(func() {
+		close(c.done)
+	})
 }

Also review existing call sites (including tests) for potential multiple Close() invocations.

Also applies to: 96-104, 149-161, 166-168

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use globalping-go for the API

2 participants