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
5 changes: 5 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"fmt"
"net/http"
"os"
"time"

Expand All @@ -14,6 +15,10 @@ const APIBaseURL = "https://api.openstatus.dev/v1"

const ConnectBaseURL = "https://api.openstatus.dev/rpc"

var DefaultHTTPClient = &http.Client{
Timeout: 30 * time.Second,
}

func NewAuthInterceptor(apiKey string) connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
Expand Down
2 changes: 2 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func ResolveToken(flagValue string) (string, error) {
if token != "" {
return token, nil
}
} else if !os.IsNotExist(readErr) {
return "", fmt.Errorf("failed to read token file %s: %w", tokenPath, readErr)
}
}

Expand Down
11 changes: 10 additions & 1 deletion internal/cli/confirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ import (
"fmt"
"os"
"strings"

"github.com/mattn/go-isatty"
)

var isInteractiveStdin = func() bool {
return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
}

func AskForConfirmation(s string) (bool, error) {
if !isInteractiveStdin() {
return false, fmt.Errorf("confirmation required but stdin is not a terminal (use --auto-accept / -y to skip)")
}
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", s)
fmt.Fprintf(os.Stderr, "%s [y/N]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
return false, fmt.Errorf("failed to read user input: %w", err)
Expand Down
3 changes: 3 additions & 0 deletions internal/cli/confirmation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
)

func Test_AskForConfirmation(t *testing.T) {
restore := cli.SetInteractiveStdin(func() bool { return true })
t.Cleanup(restore)

t.Run("Returns true for 'y' input", func(t *testing.T) {
// Create a pipe to simulate stdin
r, w, err := os.Pipe()
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cli

func SetInteractiveStdin(f func() bool) func() {
old := isInteractiveStdin
isInteractiveStdin = f
return func() { isInteractiveStdin = old }
}
16 changes: 4 additions & 12 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package config
import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"

"github.com/knadh/koanf/v2"
)

var k = koanf.New(".")

type TestsConfig struct {
Ids []int `koanf:"ids"`
}
Expand All @@ -18,22 +15,17 @@ type Config struct {
}

func ReadConfig(path string) (*Config, error) {
k := koanf.New(".")

file := file.Provider(path)

err := k.Load(file, yaml.Parser())

if err != nil {
f := file.Provider(path)
if err := k.Load(f, yaml.Parser()); err != nil {
return nil, err
}

var out Config

err = k.Unmarshal("", &out)
if err != nil {
if err := k.Unmarshal("", &out); err != nil {
return nil, err
}

return &out, nil

}
37 changes: 33 additions & 4 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config_test

import (
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
Expand All @@ -18,11 +19,10 @@ tests:

func Test_ReadConfig(t *testing.T) {
t.Run("Read valid config file", func(t *testing.T) {
f, err := os.CreateTemp(".", "config*.yaml")
f, err := os.CreateTemp(t.TempDir(), "config*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(configFile)); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -55,11 +55,10 @@ func Test_ReadConfig(t *testing.T) {
})

t.Run("Invalid YAML content", func(t *testing.T) {
f, err := os.CreateTemp(".", "invalid*.yaml")
f, err := os.CreateTemp(t.TempDir(), "invalid*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte("invalid: yaml: content: [")); err != nil {
t.Fatal(err)
Expand All @@ -74,3 +73,33 @@ func Test_ReadConfig(t *testing.T) {
}
})
}

func Test_ReadConfig_NoStatePollution(t *testing.T) {
dir := t.TempDir()

file1 := filepath.Join(dir, "config1.yaml")
if err := os.WriteFile(file1, []byte("tests:\n ids:\n - 1\n - 2\n - 3\n"), 0600); err != nil {
t.Fatal(err)
}

file2 := filepath.Join(dir, "config2.yaml")
if err := os.WriteFile(file2, []byte("tests:\n ids:\n - 4\n - 5\n - 6\n"), 0600); err != nil {
t.Fatal(err)
}

out1, err := config.ReadConfig(file1)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(out1.Tests.Ids, []int{1, 2, 3}) {
t.Errorf("First read: expected [1,2,3], got %v", out1.Tests.Ids)
}

out2, err := config.ReadConfig(file2)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(out2.Tests.Ids, []int{4, 5, 6}) {
t.Errorf("Second read: expected [4,5,6], got %v", out2.Tests.Ids)
}
}
12 changes: 12 additions & 0 deletions internal/config/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ const (
Waw Region = "waw"
Yul Region = "yul"
Yyz Region = "yyz"
// Koyeb regions (disambiguated from Fly.io regions with same airport code)
KoyebFra Region = "koyeb_fra"
KoyebPar Region = "koyeb_par"
KoyebSfo Region = "koyeb_sfo"
KoyebSin Region = "koyeb_sin"
KoyebTyo Region = "koyeb_tyo"
KoyebWas Region = "koyeb_was"
// Railway regions
RailwayUsWest2 Region = "railway_us-west2"
RailwayUsEast4 Region = "railway_us-east4-eqdc4a"
RailwayEuropeWest4 Region = "railway_europe-west4-drams3a"
RailwayAsiaSoutheast1 Region = "railway_asia-southeast1-eqsg3a"
)

type Method string
Expand Down
13 changes: 5 additions & 8 deletions internal/config/openstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,21 @@ package config
import (
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)

type Monitors map[string]Monitor

func ReadOpenStatus(path string) (Monitors, error) {
f := file.Provider(path)

err := k.Load(f, yaml.Parser())
k := koanf.New(".")

if err != nil {
f := file.Provider(path)
if err := k.Load(f, yaml.Parser()); err != nil {
return nil, err
}

var out Monitors

err = k.Unmarshal("", &out)

if err != nil {
if err := k.Unmarshal("", &out); err != nil {
return nil, err
}

Expand Down
74 changes: 65 additions & 9 deletions internal/config/openstatus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config_test

import (
"os"
"path/filepath"
"testing"

"github.com/openstatusHQ/cli/internal/config"
Expand Down Expand Up @@ -31,11 +32,10 @@ var openstatusConfig = `

func Test_ReadOpenStatus(t *testing.T) {
t.Run("Read valid openstatus config", func(t *testing.T) {
f, err := os.CreateTemp(".", "openstatus*.yaml")
f, err := os.CreateTemp(t.TempDir(), "openstatus*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(openstatusConfig)); err != nil {
t.Fatal(err)
Expand All @@ -49,9 +49,6 @@ func Test_ReadOpenStatus(t *testing.T) {
t.Fatal(err)
}

// Check that the monitor was read correctly
// Note: We check for the specific monitor because the global koanf instance
// may have accumulated state from previous tests
monitor, exists := out["test-monitor"]
if !exists {
t.Fatal("Expected 'test-monitor' to exist in output")
Expand Down Expand Up @@ -112,11 +109,10 @@ func Test_ReadOpenStatus_FollowRedirects(t *testing.T) {
url: https://example.com
followRedirects: false
`
f, err := os.CreateTemp(".", "openstatus*.yaml")
f, err := os.CreateTemp(t.TempDir(), "openstatus*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(yaml)); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -156,11 +152,10 @@ func Test_ReadOpenStatus_FollowRedirects(t *testing.T) {
method: GET
url: https://example.com
`
f, err := os.CreateTemp(".", "openstatus*.yaml")
f, err := os.CreateTemp(t.TempDir(), "openstatus*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())

if _, err := f.Write([]byte(yaml)); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -261,3 +256,64 @@ func Test_ParseConfigMonitorsToMonitor(t *testing.T) {
}
})
}

func Test_ReadOpenStatus_NoStatePollution(t *testing.T) {
dir := t.TempDir()

file1 := filepath.Join(dir, "config1.yaml")
if err := os.WriteFile(file1, []byte(`
"monitor-a":
active: true
frequency: 10m
kind: http
name: Monitor A
regions:
- iad
request:
method: GET
url: https://a.example.com
`), 0600); err != nil {
t.Fatal(err)
}

file2 := filepath.Join(dir, "config2.yaml")
if err := os.WriteFile(file2, []byte(`
"monitor-b":
active: true
frequency: 5m
kind: http
name: Monitor B
regions:
- ams
request:
method: POST
url: https://b.example.com
`), 0600); err != nil {
t.Fatal(err)
}

out1, err := config.ReadOpenStatus(file1)
if err != nil {
t.Fatal(err)
}
if _, exists := out1["monitor-a"]; !exists {
t.Error("First read: expected 'monitor-a' to exist")
}
if len(out1) != 1 {
t.Errorf("First read: expected 1 monitor, got %d", len(out1))
}

out2, err := config.ReadOpenStatus(file2)
if err != nil {
t.Fatal(err)
}
if _, exists := out2["monitor-b"]; !exists {
t.Error("Second read: expected 'monitor-b' to exist")
}
if _, exists := out2["monitor-a"]; exists {
t.Error("Second read: 'monitor-a' should not be present (state pollution)")
}
if len(out2) != 1 {
t.Errorf("Second read: expected 1 monitor, got %d", len(out2))
}
}
4 changes: 2 additions & 2 deletions internal/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strings"

"github.com/openstatusHQ/cli/internal/api"
"github.com/openstatusHQ/cli/internal/auth"
"github.com/openstatusHQ/cli/internal/whoami"
"github.com/urfave/cli/v3"
Expand Down Expand Up @@ -52,7 +52,7 @@ Get your API token from the OpenStatus dashboard.`,
}

fmt.Fprintln(os.Stderr, "Verifying token...")
err := whoami.GetWhoamiCmd(ctx, http.DefaultClient, token, nil)
err := whoami.GetWhoamiCmd(ctx, api.DefaultHTTPClient, token, nil)
if err != nil {
return cli.Exit("Invalid token. Could not authenticate with OpenStatus API", 1)
}
Expand Down
5 changes: 5 additions & 0 deletions internal/monitors/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package monitors

var RegionToString = regionToString
var StringToRegion = stringToRegion
var ConfigToTCPMonitor = configToTCPMonitor
Loading
Loading