Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func NewApp() *cli.Command {
Suggest: true,
Usage: "This is OpenStatus Command Line Interface, the OpenStatus.dev CLI",
Description: "OpenStatus is a command line interface for managing your monitors and triggering your synthetics tests. \n\nPlease report any issues at https://github.com/openstatusHQ/cli/issues/new",
Version: "v1.0.0",
Version: "v1.0.1",
Commands: []*cli.Command{
monitors.MonitorsCmd(),
run.RunCmd(),
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func Test_NewApp(t *testing.T) {
t.Errorf("Expected app name 'openstatus', got %s", app.Name)
}

if app.Version != "v1.0.0" {
t.Errorf("Expected version 'v1.0.0', got %s", app.Version)
if app.Version != "v1.0.1" {
t.Errorf("Expected version 'v1.0.1', got %s", app.Version)
}

if !app.Suggest {
Expand Down
2 changes: 2 additions & 0 deletions internal/config/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Request struct {
Method Method `json:"method,omitempty" ,yaml:"method,omitempty"`
// URL to request
URL string `json:"url,omitempty" ,yaml:"url,omitempty"`
// Whether to follow HTTP redirects (defaults to true when not specified)
FollowRedirects *bool `json:"followRedirects,omitempty" ,yaml:"followRedirects,omitempty"`
// Host to connect to
Host string `json:"host,omitempty" ,yaml:"host,omitempty"`
// Port to connect to
Expand Down
88 changes: 88 additions & 0 deletions internal/config/openstatus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,94 @@ func Test_ReadOpenStatus(t *testing.T) {
})
}

func Test_ReadOpenStatus_FollowRedirects(t *testing.T) {
t.Run("followRedirects false is parsed correctly", func(t *testing.T) {
yaml := `
"redirect-monitor":
active: true
frequency: 10m
kind: http
name: Redirect Monitor
regions:
- iad
request:
method: GET
url: https://example.com
followRedirects: false
`
f, err := os.CreateTemp(".", "openstatus*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(yaml)); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}

out, err := config.ReadOpenStatus(f.Name())
if err != nil {
t.Fatal(err)
}

monitor, exists := out["redirect-monitor"]
if !exists {
t.Fatal("Expected 'redirect-monitor' to exist in output")
}

if monitor.Request.FollowRedirects == nil {
t.Fatal("Expected FollowRedirects to be non-nil")
}
if *monitor.Request.FollowRedirects != false {
t.Errorf("Expected FollowRedirects to be false, got %v", *monitor.Request.FollowRedirects)
}
})

t.Run("followRedirects omitted is nil", func(t *testing.T) {
yaml := `
"no-redirect-field":
active: true
frequency: 10m
kind: http
name: No Redirect Field
regions:
- iad
request:
method: GET
url: https://example.com
`
f, err := os.CreateTemp(".", "openstatus*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(yaml)); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}

out, err := config.ReadOpenStatus(f.Name())
if err != nil {
t.Fatal(err)
}

monitor, exists := out["no-redirect-field"]
if !exists {
t.Fatal("Expected 'no-redirect-field' to exist in output")
}

if monitor.Request.FollowRedirects != nil {
t.Errorf("Expected FollowRedirects to be nil, got %v", *monitor.Request.FollowRedirects)
}
})
}

func Test_ParseConfigMonitorsToMonitor(t *testing.T) {
t.Run("Parse monitors map to slice", func(t *testing.T) {
monitors := config.Monitors{
Expand Down
101 changes: 101 additions & 0 deletions internal/monitors/monitor_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package monitors_test

import (
"bytes"
"encoding/json"
"io"
"net/http"
"testing"
Expand Down Expand Up @@ -209,3 +210,103 @@ func Test_CreateMonitor(t *testing.T) {
}
})
}

func boolPtr(b bool) *bool {
return &b
}

func Test_CreateMonitor_FollowRedirects(t *testing.T) {
t.Parallel()

t.Run("followRedirects false is sent in request body", func(t *testing.T) {
var capturedBody map[string]json.RawMessage

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
bodyBytes, _ := io.ReadAll(req.Body)
json.Unmarshal(bodyBytes, &capturedBody)

respBody := `{"monitor":{"id":"100","name":"Redirect Monitor","url":"https://example.com","periodicity":"PERIODICITY_10M","method":"HTTP_METHOD_GET","regions":["REGION_FLY_IAD"],"active":true,"followRedirects":false}}`
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(respBody))),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "Redirect Monitor",
Active: true,
Frequency: config.The10M,
Kind: config.HTTP,
Regions: []config.Region{config.Iad},
Request: config.Request{
URL: "https://example.com",
Method: config.Get,
FollowRedirects: boolPtr(false),
},
}

_, err := monitors.CreateMonitor(interceptor.GetHTTPClient(), "test-api-key", monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

var monitorPayload map[string]json.RawMessage
json.Unmarshal(capturedBody["monitor"], &monitorPayload)

followRedirectsRaw, exists := monitorPayload["followRedirects"]
if !exists {
t.Fatal("Expected followRedirects to be present in request body")
}

var followRedirects bool
json.Unmarshal(followRedirectsRaw, &followRedirects)
if followRedirects != false {
t.Errorf("Expected followRedirects to be false, got %v", followRedirects)
}
})

t.Run("followRedirects nil is omitted from request body", func(t *testing.T) {
var capturedBody map[string]json.RawMessage

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
bodyBytes, _ := io.ReadAll(req.Body)
json.Unmarshal(bodyBytes, &capturedBody)

respBody := `{"monitor":{"id":"101","name":"Default Monitor","url":"https://example.com","periodicity":"PERIODICITY_10M","method":"HTTP_METHOD_GET","regions":["REGION_FLY_IAD"],"active":true}}`
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(respBody))),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "Default Monitor",
Active: true,
Frequency: config.The10M,
Kind: config.HTTP,
Regions: []config.Region{config.Iad},
Request: config.Request{
URL: "https://example.com",
Method: config.Get,
},
}

_, err := monitors.CreateMonitor(interceptor.GetHTTPClient(), "test-api-key", monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

var monitorPayload map[string]json.RawMessage
json.Unmarshal(capturedBody["monitor"], &monitorPayload)

if _, exists := monitorPayload["followRedirects"]; exists {
t.Error("Expected followRedirects to be absent from request body when nil")
}
})
}
15 changes: 14 additions & 1 deletion internal/monitors/monitor_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ func GetMonitorInfo(httpClient *http.Client, apiKey string, monitorId string) er

monitorConfig := resp.GetMonitor()
var monitor Monitor
var regions []monitorv1.Region
switch {
case monitorConfig.HasHttp():
monitor = httpMonitorToLocal(monitorConfig.GetHttp())
regions = monitorConfig.GetHttp().GetRegions()
case monitorConfig.HasTcp():
monitor = tcpMonitorToLocal(monitorConfig.GetTcp())
regions = monitorConfig.GetTcp().GetRegions()
default:
return fmt.Errorf("unknown monitor type")
}
Expand Down Expand Up @@ -80,7 +83,17 @@ func GetMonitorInfo(httpClient *http.Client, apiKey string, monitorId string) er
}

data = append(data, []string{"Frequency", monitor.Periodicity})
data = append(data, []string{"Locations", strings.Join(monitor.Regions, ",")})

// Group regions by provider and display each provider on its own row
regionGroups := groupRegionsByProvider(regions)
providers := []string{"Fly.io", "Koyeb", "Railway"}
for _, provider := range providers {
codes := regionGroups[provider]
if len(codes) > 0 {
data = append(data, []string{fmt.Sprintf("Locations (%s)", provider), strings.Join(codes, ", ")})
}
}
Comment on lines +87 to +95
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The monitor info output only iterates over a hard-coded provider list (Fly.io/Koyeb/Railway). If groupRegionsByProvider ever returns additional buckets (or if you add an "Other" bucket), those regions will still not be displayed. Consider iterating over the grouped map keys deterministically (e.g., preferred order + "Other" fallback), or adding an explicit fallback row when no provider rows are emitted so locations are never omitted.

Copilot uses AI. Check for mistakes.

data = append(data, []string{"Active", fmt.Sprintf("%t", monitor.Active)})
data = append(data, []string{"Public", fmt.Sprintf("%t", monitor.Public)})

Expand Down
97 changes: 97 additions & 0 deletions internal/monitors/monitor_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package monitors_test

import (
"bytes"
"encoding/json"
"io"
"net/http"
"testing"
Expand Down Expand Up @@ -200,3 +201,99 @@ func Test_UpdateMonitor(t *testing.T) {
}
})
}

func Test_UpdateMonitor_FollowRedirects(t *testing.T) {
t.Parallel()

t.Run("followRedirects false is sent in update request body", func(t *testing.T) {
var capturedBody map[string]json.RawMessage

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
bodyBytes, _ := io.ReadAll(req.Body)
json.Unmarshal(bodyBytes, &capturedBody)

respBody := `{"monitor":{"id":"123","name":"Redirect Monitor","url":"https://example.com","periodicity":"PERIODICITY_10M","method":"HTTP_METHOD_GET","regions":["REGION_FLY_IAD"],"active":true,"followRedirects":false}}`
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(respBody))),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "Redirect Monitor",
Active: true,
Frequency: config.The10M,
Kind: config.HTTP,
Regions: []config.Region{config.Iad},
Request: config.Request{
URL: "https://example.com",
Method: config.Get,
FollowRedirects: boolPtr(false),
},
}

_, err := monitors.UpdateMonitor(interceptor.GetHTTPClient(), "test-api-key", 123, monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

var monitorPayload map[string]json.RawMessage
json.Unmarshal(capturedBody["monitor"], &monitorPayload)

followRedirectsRaw, exists := monitorPayload["followRedirects"]
if !exists {
t.Fatal("Expected followRedirects to be present in update request body")
}

var followRedirects bool
json.Unmarshal(followRedirectsRaw, &followRedirects)
if followRedirects != false {
t.Errorf("Expected followRedirects to be false, got %v", followRedirects)
}
})

t.Run("followRedirects nil is omitted from update request body", func(t *testing.T) {
var capturedBody map[string]json.RawMessage

interceptor := &interceptorHTTPClient{
f: func(req *http.Request) (*http.Response, error) {
bodyBytes, _ := io.ReadAll(req.Body)
json.Unmarshal(bodyBytes, &capturedBody)

respBody := `{"monitor":{"id":"124","name":"Default Monitor","url":"https://example.com","periodicity":"PERIODICITY_10M","method":"HTTP_METHOD_GET","regions":["REGION_FLY_IAD"],"active":true}}`
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(respBody))),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
},
}

monitor := config.Monitor{
Name: "Default Monitor",
Active: true,
Frequency: config.The10M,
Kind: config.HTTP,
Regions: []config.Region{config.Iad},
Request: config.Request{
URL: "https://example.com",
Method: config.Get,
},
}

_, err := monitors.UpdateMonitor(interceptor.GetHTTPClient(), "test-api-key", 124, monitor)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

var monitorPayload map[string]json.RawMessage
json.Unmarshal(capturedBody["monitor"], &monitorPayload)

if _, exists := monitorPayload["followRedirects"]; exists {
t.Error("Expected followRedirects to be absent from update request body when nil")
}
})
}
Loading
Loading