diff --git a/cmd/root.go b/cmd/root.go index 281fe50..92d3df7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,10 +61,7 @@ func Execute() { } } globalpingClient := globalping.NewClientWithCacheCleanup(globalping.Config{ - APIURL: config.GlobalpingAPIURL, - AuthURL: config.GlobalpingAuthURL, - DashboardURL: config.GlobalpingDashboardURL, - AuthToken: token, + AuthToken: token, OnTokenRefresh: func(token *globalping.Token) { profile.Token = token err := localStorage.SaveConfig() diff --git a/globalping/client.go b/globalping/client.go index ee3060c..da42208 100644 --- a/globalping/client.go +++ b/globalping/client.go @@ -6,33 +6,51 @@ import ( "time" ) +const ( + GlobalpingAPIURL = "https://api.globalping.io/v1" + GlobalpingAuthURL = "https://auth.globalping.io" + GlobalpingDashboardURL = "https://dash.globalping.io" +) + type Client interface { // Creates a new measurement with parameters set in the request body. The measurement runs asynchronously and you can retrieve its current state at the URL returned in the Location header. // // https://globalping.io/docs/api.globalping.io#post-/v1/measurements CreateMeasurement(measurement *MeasurementCreate) (*MeasurementCreateResponse, error) + // Returns the status and results of an existing measurement. Measurements are typically available for up to 7 days after creation. // // https://globalping.io/docs/api.globalping.io#get-/v1/measurements/-id- GetMeasurement(id string) (*Measurement, error) + + // Waits for the measurement to complete and returns the results. + // + // https://globalping.io/docs/api.globalping.io#get-/v1/measurements/-id- + AwaitMeasurement(id string) (*Measurement, error) + // Returns the status and results of an existing measurement. Measurements are typically available for up to 7 days after creation. // // https://globalping.io/docs/api.globalping.io#get-/v1/measurements/-id- GetMeasurementRaw(id string) ([]byte, error) + // Returns a link to be used for authorization and listens for the authorization callback. // // onTokenRefresh will be called if the authorization is successful. Authorize(callback func(error)) (*AuthorizeResponse, error) + // Returns the introspection response for the token. // // If the token is empty, the client's current token will be used. TokenIntrospection(token string) (*IntrospectionResponse, error) + // Removes the current token from the client. It also revokes the tokens if the refresh token is available. // // onTokenRefresh will be called if the token is successfully removed. Logout() error + // Revokes the token. RevokeToken(token string) error + // Returns the rate limits for the current user or IP address. Limits() (*LimitsResponse, error) } @@ -40,10 +58,10 @@ type Client interface { type Config struct { HTTPClient *http.Client // If set, this client will be used for API requests and authorization - APIURL string - DashboardURL string + APIURL string // optional + DashboardURL string // optional - AuthURL string + AuthURL string // optional AuthClientID string AuthClientSecret string AuthToken *Token @@ -90,6 +108,17 @@ func NewClient(config Config) Client { userAgent: config.UserAgent, cache: map[string]*CacheEntry{}, } + + if config.APIURL == "" { + c.apiURL = GlobalpingAPIURL + } + if config.AuthURL == "" { + c.authURL = GlobalpingAuthURL + } + if config.DashboardURL == "" { + c.dashboardURL = GlobalpingDashboardURL + } + if config.HTTPClient != nil { c.http = config.HTTPClient } else { @@ -97,6 +126,7 @@ func NewClient(config Config) Client { Timeout: 30 * time.Second, } } + if config.AuthToken != nil { c.token = &Token{ AccessToken: config.AuthToken.AccessToken, diff --git a/globalping/measurements.go b/globalping/measurements.go index f79d91c..104022e 100644 --- a/globalping/measurements.go +++ b/globalping/measurements.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "strconv" + "time" "github.com/andybalholm/brotli" "github.com/jsdelivr/globalping-cli/utils" @@ -161,6 +162,34 @@ func (c *client) GetMeasurement(id string) (*Measurement, error) { return m, nil } +func (c *client) AwaitMeasurement(id string) (*Measurement, error) { + respBytes, err := c.GetMeasurementRaw(id) + if err != nil { + return nil, err + } + m := &Measurement{} + err = json.Unmarshal(respBytes, m) + if err != nil { + return nil, &MeasurementError{ + Message: fmt.Sprintf("invalid get measurement format returned: %v %s", err, string(respBytes)), + } + } + for m.Status == StatusInProgress { + time.Sleep(500 * time.Millisecond) + respBytes, err := c.GetMeasurementRaw(id) + if err != nil { + return nil, err + } + err = json.Unmarshal(respBytes, m) + if err != nil { + return nil, &MeasurementError{ + Message: fmt.Sprintf("invalid get measurement format returned: %v %s", err, string(respBytes)), + } + } + } + return m, nil +} + func (c *client) GetMeasurementRaw(id string) ([]byte, error) { req, err := http.NewRequest("GET", c.apiURL+"/measurements/"+id, nil) if err != nil { diff --git a/globalping/measurements_test.go b/globalping/measurements_test.go index 0b855ef..cc5e6d9 100644 --- a/globalping/measurements_test.go +++ b/globalping/measurements_test.go @@ -1048,6 +1048,31 @@ func Test_GetMeasurementRaw_Json(t *testing.T) { assert.Equal(t, `{"id":"abcd"}`, string(res)) } +func Test_AwaitMeasurement(t *testing.T) { + count := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + var err error + if count == 3 { + _, err = w.Write([]byte(`{"id":"abcd", "status": "finished"}`)) + } else { + _, err = w.Write([]byte(`{"id":"abcd", "status": "in-progress"}`)) + } + if err != nil { + panic(err) + } + count++ + })) + defer server.Close() + client := NewClient(Config{APIURL: server.URL}) + res, err := client.AwaitMeasurement("abcd") + if err != nil { + t.Error(err) + } + assert.Equal(t, "abcd", res.ID) + assert.Equal(t, StatusFinished, res.Status) +} + func generateServer(json string, statusCode int) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) diff --git a/mocks/mock_client.go b/mocks/mock_client.go index 3730a87..052f069 100644 --- a/mocks/mock_client.go +++ b/mocks/mock_client.go @@ -54,6 +54,21 @@ func (mr *MockClientMockRecorder) Authorize(callback any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorize", reflect.TypeOf((*MockClient)(nil).Authorize), callback) } +// AwaitMeasurement mocks base method. +func (m *MockClient) AwaitMeasurement(id string) (*globalping.Measurement, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AwaitMeasurement", id) + ret0, _ := ret[0].(*globalping.Measurement) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AwaitMeasurement indicates an expected call of AwaitMeasurement. +func (mr *MockClientMockRecorder) AwaitMeasurement(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AwaitMeasurement", reflect.TypeOf((*MockClient)(nil).AwaitMeasurement), id) +} + // CreateMeasurement mocks base method. func (m *MockClient) CreateMeasurement(measurement *globalping.MeasurementCreate) (*globalping.MeasurementCreateResponse, error) { m.ctrl.T.Helper() diff --git a/utils/config.go b/utils/config.go index e6e3a1d..2d17f96 100644 --- a/utils/config.go +++ b/utils/config.go @@ -7,9 +7,6 @@ import ( type Config struct { GlobalpingToken string - GlobalpingAPIURL string - GlobalpingAuthURL string - GlobalpingDashboardURL string GlobalpingAuthClientID string GlobalpingAuthClientSecret string GlobalpingAPIInterval _time.Duration @@ -17,9 +14,6 @@ type Config struct { func NewConfig() *Config { return &Config{ - GlobalpingAPIURL: "https://api.globalping.io/v1", - GlobalpingAuthURL: "https://auth.globalping.io", - GlobalpingDashboardURL: "https://dash.globalping.io", GlobalpingAuthClientID: "be231712-03f4-45bf-9f15-023506ce0b72", GlobalpingAuthClientSecret: "public", GlobalpingAPIInterval: 500 * _time.Millisecond, diff --git a/view/output.go b/view/output.go index 644b89f..b70fe4e 100644 --- a/view/output.go +++ b/view/output.go @@ -12,6 +12,7 @@ import ( var ShareURL = "https://globalping.io?measurement=" +// TODO: Use globalping.AwaitMeasurement instead of GetMeasurement func (v *viewer) Output(id string, m *globalping.MeasurementCreate) error { // Wait for first result to arrive from a probe before starting display (can be in-progress) data, err := v.globalping.GetMeasurement(id)