Skip to content

Commit 8aba3a7

Browse files
authored
fix(apiutil): normalize server URL before appending /api/v1 (#72)
1 parent 94bc484 commit 8aba3a7

4 files changed

Lines changed: 146 additions & 4 deletions

File tree

cmd/apiutil/client.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,23 @@ func RawGet(ctx context.Context, client *api.APIClient, path string, params url.
7676
return body, nil
7777
}
7878

79+
// NormalizeServerURL trims trailing slashes from raw and appends /api/v1 exactly
80+
// once. Returns an empty string when raw is blank or all slashes.
81+
func NormalizeServerURL(raw string) string {
82+
s := strings.TrimRight(raw, "/")
83+
if s == "" {
84+
return ""
85+
}
86+
if !strings.HasSuffix(s, "/api/v1") {
87+
s += "/api/v1"
88+
}
89+
return s
90+
}
91+
7992
// NewAPIClientWithKeyAndTransport is the base constructor used by all other helpers.
8093
func NewAPIClientWithKeyAndTransport(apiKey string, transport http.RoundTripper) *api.APIClient {
8194
configuration := api.NewConfiguration()
82-
serverURL := viper.GetString("seerr.server")
83-
if !strings.HasSuffix(serverURL, "/api/v1") {
84-
serverURL = strings.TrimSuffix(serverURL, "/") + "/api/v1"
85-
}
95+
serverURL := NormalizeServerURL(viper.GetString("seerr.server"))
8696
configuration.Servers = api.ServerConfigurations{{URL: serverURL, Description: "Configured Server"}}
8797
key := apiKey
8898
if key == "" {

cmd/mcp/serve.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"net/http"
88
"strings"
99

10+
"seerr-cli/cmd/apiutil"
11+
1012
"github.com/mark3labs/mcp-go/server"
1113
"github.com/spf13/cobra"
1214
"github.com/spf13/viper"
@@ -75,6 +77,10 @@ func runServe(_ *cobra.Command, args []string) error {
7577
return err
7678
}
7779

80+
if err := ValidateServeConfig(); err != nil {
81+
return err
82+
}
83+
7884
if transport == "http" && authToken == "" && routeToken == "" && !noAuth {
7985
return fmt.Errorf("HTTP transport requires --auth-token, --route-token, or --no-auth (insecure) to be set explicitly")
8086
}
@@ -185,6 +191,16 @@ func runServe(_ *cobra.Command, args []string) error {
185191
}
186192
}
187193

194+
// ValidateServeConfig checks that the Seerr server URL is configured. It is
195+
// exported so that tests can verify the fail-fast behaviour without starting
196+
// the server.
197+
func ValidateServeConfig() error {
198+
if apiutil.NormalizeServerURL(viper.GetString("seerr.server")) == "" {
199+
return fmt.Errorf("seerr.server is not configured; set it with --server <url> or add seerr.server to ~/.seerr-cli.yaml")
200+
}
201+
return nil
202+
}
203+
188204
// HealthCheckHandler responds to GET /health with a JSON status payload.
189205
// It is exported so that it can be tested directly from the tests package.
190206
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {

tests/apiutil_client_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"seerr-cli/cmd/apiutil"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestNormalizeServerURL(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
input string
15+
want string
16+
}{
17+
{
18+
name: "bare host",
19+
input: "https://host",
20+
want: "https://host/api/v1",
21+
},
22+
{
23+
name: "trailing slash",
24+
input: "https://host/",
25+
want: "https://host/api/v1",
26+
},
27+
{
28+
name: "multiple trailing slashes",
29+
input: "https://host///",
30+
want: "https://host/api/v1",
31+
},
32+
{
33+
name: "already has api/v1",
34+
input: "https://host/api/v1",
35+
want: "https://host/api/v1",
36+
},
37+
{
38+
name: "api/v1 with trailing slash",
39+
input: "https://host/api/v1/",
40+
want: "https://host/api/v1",
41+
},
42+
{
43+
name: "empty string",
44+
input: "",
45+
want: "",
46+
},
47+
{
48+
name: "only slashes",
49+
input: "///",
50+
want: "",
51+
},
52+
}
53+
for _, tc := range tests {
54+
t.Run(tc.name, func(t *testing.T) {
55+
assert.Equal(t, tc.want, apiutil.NormalizeServerURL(tc.input))
56+
})
57+
}
58+
}

tests/mcp_serve_validation_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package tests
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/viper"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
cmdmcp "seerr-cli/cmd/mcp"
11+
)
12+
13+
func TestMCPServeFailsFastWithoutSeerrServer(t *testing.T) {
14+
original := viper.GetString("seerr.server")
15+
t.Cleanup(func() { viper.Set("seerr.server", original) })
16+
17+
tests := []struct {
18+
name string
19+
seerrServer string
20+
wantErr bool
21+
errContains string
22+
}{
23+
{
24+
name: "missing server returns error",
25+
seerrServer: "",
26+
wantErr: true,
27+
errContains: "seerr.server",
28+
},
29+
{
30+
name: "only slashes returns error",
31+
seerrServer: "///",
32+
wantErr: true,
33+
errContains: "seerr.server",
34+
},
35+
{
36+
name: "valid server passes validation",
37+
seerrServer: "http://localhost:5055",
38+
wantErr: false,
39+
},
40+
{
41+
name: "valid server with trailing slash passes validation",
42+
seerrServer: "http://localhost:5055/",
43+
wantErr: false,
44+
},
45+
}
46+
for _, tc := range tests {
47+
t.Run(tc.name, func(t *testing.T) {
48+
viper.Set("seerr.server", tc.seerrServer)
49+
err := cmdmcp.ValidateServeConfig()
50+
if tc.wantErr {
51+
require.Error(t, err)
52+
assert.Contains(t, err.Error(), tc.errContains)
53+
} else {
54+
require.NoError(t, err)
55+
}
56+
})
57+
}
58+
}

0 commit comments

Comments
 (0)