Skip to content

Commit 0d72895

Browse files
committed
Update CIMIS API and complete coverage
1 parent 7fe8d89 commit 0d72895

27 files changed

Lines changed: 3926 additions & 230 deletions

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ cimis profile \
188188

189189
- `-stations string` - CSV list or range (e.g., `2,5,10` or `1-10`)
190190
- `-year int` - Year to fetch
191-
- `-start string` - Start date `MM/DD/YYYY` (overrides year)
192-
- `-end string` - End date `MM/DD/YYYY` (overrides year)
191+
- `-start string` - Start date `YYYY-MM-DD` (overrides year; `MM/DD/YYYY` also accepted)
192+
- `-end string` - End date `YYYY-MM-DD` (overrides year; `MM/DD/YYYY` also accepted)
193193
- `-concurrency int` - Worker pool size (default: 4)
194194
- `-retries int` - Max retries on failure (default: 3)
195195
- `-perf` - Print detailed metrics
@@ -235,7 +235,7 @@ See the [library documentation](https://github.com/dl-alexandre/cimis-tsdb#readm
235235

236236
```bash
237237
# Fetch today's data for station 2
238-
cimis fetch-streaming -stations 2 -start $(date -v-1d +%m/%d/%Y) -end $(date +%m/%d/%Y)
238+
cimis fetch-streaming -stations 2 -start $(date -v-1d +%Y-%m-%d) -end $(date +%Y-%m-%d)
239239

240240
# Query last 7 days
241241
cimis query -station 2 -start $(date -v-7d +%Y-%m-%d) -end $(date +%Y-%m-%d)
@@ -277,5 +277,5 @@ MIT
277277
## Links
278278

279279
- [CIMIS Website](https://cimis.water.ca.gov/)
280-
- [CIMIS API Documentation](https://cimis.water.ca.gov/WSNReportCriteria.aspx)
280+
- [CIMIS API Documentation](https://et.water.ca.gov/)
281281
- [cimis-tsdb Library](https://github.com/dl-alexandre/cimis-tsdb)

cmd/cimis/api_clients.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package main
2+
3+
import "github.com/dl-alexandre/cimis-cli/internal/api"
4+
5+
var (
6+
newAPIClient = api.NewClient
7+
newOptimizedAPIClient = api.NewOptimizedClient
8+
)

cmd/cimis/browser.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ package main
22

33
import (
44
"fmt"
5-
"log"
65
"os/exec"
76
"runtime"
87
)
98

9+
var (
10+
lookPath = exec.LookPath
11+
startBrowserCommand = func(cmd string, args ...string) error {
12+
return exec.Command(cmd, args...).Start()
13+
}
14+
runtimeGOOS = runtime.GOOS
15+
)
16+
1017
type BrowserOpener interface {
1118
Open(url string) error
1219
}
@@ -17,7 +24,7 @@ func (s *systemBrowserOpener) Open(url string) error {
1724
var cmd string
1825
var args []string
1926

20-
switch runtime.GOOS {
27+
switch runtimeGOOS {
2128
case "windows":
2229
cmd = "cmd"
2330
args = []string{"/c", "start", url}
@@ -29,11 +36,11 @@ func (s *systemBrowserOpener) Open(url string) error {
2936
args = []string{url}
3037
}
3138

32-
if _, err := exec.LookPath(cmd); err != nil {
39+
if _, err := lookPath(cmd); err != nil {
3340
return fmt.Errorf("no browser command found: %s not found. Please install a browser or manually visit: %s", cmd, url)
3441
}
3542

36-
if err := exec.Command(cmd, args...).Start(); err != nil {
43+
if err := startBrowserCommand(cmd, args...); err != nil {
3744
return fmt.Errorf("failed to open browser: %w", err)
3845
}
3946
return nil
@@ -52,7 +59,7 @@ func cmdRegister() {
5259
fmt.Printf("URL: %s\n", registerURL)
5360

5461
if err := openBrowser(registerURL); err != nil {
55-
log.Fatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, registerURL)
62+
logFatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, registerURL)
5663
}
5764

5865
fmt.Println("Browser opened successfully!")
@@ -69,7 +76,7 @@ func cmdLogin() {
6976
fmt.Printf("URL: %s\n", loginURL)
7077

7178
if err := openBrowser(loginURL); err != nil {
72-
log.Fatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, loginURL)
79+
logFatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, loginURL)
7380
}
7481

7582
fmt.Println("Browser opened successfully!")
@@ -81,13 +88,13 @@ func cmdLogin() {
8188
}
8289

8390
func cmdAPI() {
84-
const apiURL = "https://cimis.water.ca.gov/WSNReportCriteria.aspx"
91+
const apiURL = "https://et.water.ca.gov/"
8592

8693
fmt.Println("Opening CIMIS API documentation page in your browser...")
8794
fmt.Printf("URL: %s\n", apiURL)
8895

8996
if err := openBrowser(apiURL); err != nil {
90-
log.Fatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, apiURL)
97+
logFatalf("Failed to open browser: %v\nPlease manually visit: %s\n", err, apiURL)
9198
}
9299

93100
fmt.Println("Browser opened successfully!")

cmd/cimis/browser_test.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"os"
6+
"strings"
7+
"testing"
8+
)
9+
10+
type mockBrowserOpener struct {
11+
urls []string
12+
err error
13+
}
14+
15+
func (m *mockBrowserOpener) Open(url string) error {
16+
m.urls = append(m.urls, url)
17+
return m.err
18+
}
19+
20+
func withFatalPanic(t *testing.T) {
21+
t.Helper()
22+
23+
originalFatal := logFatal
24+
originalFatalf := logFatalf
25+
t.Cleanup(func() {
26+
logFatal = originalFatal
27+
logFatalf = originalFatalf
28+
})
29+
30+
logFatal = func(v ...interface{}) {
31+
panic("fatal")
32+
}
33+
logFatalf = func(format string, v ...interface{}) {
34+
panic("fatalf")
35+
}
36+
}
37+
38+
func expectPanic(t *testing.T, fn func()) {
39+
t.Helper()
40+
41+
defer func() {
42+
if recover() == nil {
43+
t.Fatal("expected panic")
44+
}
45+
}()
46+
fn()
47+
}
48+
49+
func TestBrowserCommandsUseConfiguredOpener(t *testing.T) {
50+
original := browserOpener
51+
defer func() { browserOpener = original }()
52+
53+
mock := &mockBrowserOpener{}
54+
browserOpener = mock
55+
56+
tests := []struct {
57+
name string
58+
run func()
59+
wantURL string
60+
wantOutput string
61+
}{
62+
{"register", cmdRegister, "https://cimis.water.ca.gov/Welcome.aspx", "After registering"},
63+
{"login", cmdLogin, "https://cimis.water.ca.gov/Auth/Login.aspx", "After logging in"},
64+
{"api", cmdAPI, "https://et.water.ca.gov/", "CIMIS API access"},
65+
}
66+
67+
for _, tt := range tests {
68+
t.Run(tt.name, func(t *testing.T) {
69+
before := len(mock.urls)
70+
output := captureStdout(t, tt.run)
71+
72+
if len(mock.urls) != before+1 {
73+
t.Fatalf("browser opener calls = %d, want %d", len(mock.urls), before+1)
74+
}
75+
if got := mock.urls[len(mock.urls)-1]; got != tt.wantURL {
76+
t.Fatalf("opened URL = %q, want %q", got, tt.wantURL)
77+
}
78+
if !strings.Contains(output, tt.wantOutput) {
79+
t.Fatalf("output missing %q:\n%s", tt.wantOutput, output)
80+
}
81+
})
82+
}
83+
}
84+
85+
func TestBrowserCommandsFatalOnOpenError(t *testing.T) {
86+
withFatalPanic(t)
87+
88+
original := browserOpener
89+
defer func() { browserOpener = original }()
90+
91+
tests := []struct {
92+
name string
93+
run func()
94+
}{
95+
{"register", cmdRegister},
96+
{"login", cmdLogin},
97+
{"api", cmdAPI},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
browserOpener = &mockBrowserOpener{err: errors.New("open failed")}
103+
expectPanic(t, func() {
104+
captureStdout(t, tt.run)
105+
})
106+
})
107+
}
108+
}
109+
110+
func TestFatalIfErr(t *testing.T) {
111+
fatalIfErr(nil)
112+
113+
withFatalPanic(t)
114+
expectPanic(t, func() {
115+
fatalIfErr(errors.New("boom"))
116+
})
117+
}
118+
119+
func TestSystemBrowserOpenerOpen(t *testing.T) {
120+
originalLookPath := lookPath
121+
originalStart := startBrowserCommand
122+
originalGOOS := runtimeGOOS
123+
defer func() {
124+
lookPath = originalLookPath
125+
startBrowserCommand = originalStart
126+
runtimeGOOS = originalGOOS
127+
}()
128+
129+
tests := []struct {
130+
goos string
131+
wantCmd string
132+
}{
133+
{"darwin", "open"},
134+
{"windows", "cmd"},
135+
{"linux", "xdg-open"},
136+
}
137+
138+
for _, tt := range tests {
139+
t.Run(tt.goos, func(t *testing.T) {
140+
runtimeGOOS = tt.goos
141+
var gotLookPath string
142+
var gotCmd string
143+
var gotArgs []string
144+
lookPath = func(file string) (string, error) {
145+
gotLookPath = file
146+
return "/usr/bin/" + file, nil
147+
}
148+
startBrowserCommand = func(cmd string, args ...string) error {
149+
gotCmd = cmd
150+
gotArgs = append([]string(nil), args...)
151+
return nil
152+
}
153+
154+
err := (&systemBrowserOpener{}).Open("https://example.test")
155+
if err != nil {
156+
t.Fatalf("Open() error = %v", err)
157+
}
158+
159+
if gotLookPath != tt.wantCmd {
160+
t.Fatalf("lookPath command = %q, want %q", gotLookPath, tt.wantCmd)
161+
}
162+
if gotCmd != tt.wantCmd {
163+
t.Fatalf("start command = %q, want %q", gotCmd, tt.wantCmd)
164+
}
165+
if len(gotArgs) == 0 || gotArgs[len(gotArgs)-1] != "https://example.test" {
166+
t.Fatalf("start args = %v, want URL as final arg", gotArgs)
167+
}
168+
})
169+
}
170+
}
171+
172+
func TestSystemBrowserOpenerOpenErrors(t *testing.T) {
173+
originalLookPath := lookPath
174+
originalStart := startBrowserCommand
175+
defer func() {
176+
lookPath = originalLookPath
177+
startBrowserCommand = originalStart
178+
}()
179+
180+
lookPath = func(file string) (string, error) {
181+
return "", errors.New("missing")
182+
}
183+
if err := (&systemBrowserOpener{}).Open("https://example.test"); err == nil {
184+
t.Fatal("expected missing browser command error")
185+
}
186+
187+
lookPath = func(file string) (string, error) {
188+
return "/usr/bin/" + file, nil
189+
}
190+
startBrowserCommand = func(cmd string, args ...string) error {
191+
return errors.New("start failed")
192+
}
193+
if err := (&systemBrowserOpener{}).Open("https://example.test"); err == nil {
194+
t.Fatal("expected start error")
195+
}
196+
}
197+
198+
func TestDefaultStartBrowserCommand(t *testing.T) {
199+
if err := startBrowserCommand(os.Args[0], "-test.run=TestBrowserStartHelperProcess"); err != nil {
200+
t.Fatalf("startBrowserCommand() error = %v", err)
201+
}
202+
}
203+
204+
func TestBrowserStartHelperProcess(t *testing.T) {}
205+
206+
func TestRunDispatchBrowserCommands(t *testing.T) {
207+
withNoAutoUpdate(t)
208+
209+
original := browserOpener
210+
defer func() { browserOpener = original }()
211+
212+
mock := &mockBrowserOpener{}
213+
browserOpener = mock
214+
215+
for _, command := range []string{"register", "login", "api-docs"} {
216+
if code := run([]string{"cimis", command}); code != 0 {
217+
t.Fatalf("%s exit code = %d, want 0", command, code)
218+
}
219+
}
220+
if len(mock.urls) != 3 {
221+
t.Fatalf("browser opener calls = %d, want 3", len(mock.urls))
222+
}
223+
}

cmd/cimis/fatal.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package main
2+
3+
import "log"
4+
5+
var (
6+
logFatal = log.Fatal
7+
logFatalf = log.Fatalf
8+
)
9+
10+
func fatalIfErr(err error) {
11+
if err != nil {
12+
logFatal(err)
13+
}
14+
}

0 commit comments

Comments
 (0)