Skip to content

Commit 8a06d4b

Browse files
authored
fix(api): correct add-torrent OpenAPI param names and add missing fields (#1426)
1 parent 2994054 commit 8a06d4b

2 files changed

Lines changed: 161 additions & 2 deletions

File tree

internal/web/swagger/openapi.yaml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,10 +1264,46 @@ paths:
12641264
type: array
12651265
items:
12661266
type: string
1267-
startPaused:
1267+
paused:
12681268
type: boolean
1269-
savePath:
1269+
savepath:
12701270
type: string
1271+
indexer_id:
1272+
type: integer
1273+
description: Indexer ID for downloading torrent from an indexer
1274+
skip_checking:
1275+
type: boolean
1276+
sequentialDownload:
1277+
type: boolean
1278+
firstLastPiecePrio:
1279+
type: boolean
1280+
upLimit:
1281+
type: integer
1282+
description: Upload speed limit in KB/s
1283+
dlLimit:
1284+
type: integer
1285+
description: Download speed limit in KB/s
1286+
ratioLimit:
1287+
type: string
1288+
description: Share ratio limit
1289+
seedingTimeLimit:
1290+
type: string
1291+
description: Seeding time limit in minutes
1292+
contentLayout:
1293+
type: string
1294+
description: Content layout (Original, Subfolder, NoSubfolder)
1295+
rename:
1296+
type: string
1297+
description: Rename torrent
1298+
useDownloadPath:
1299+
type: boolean
1300+
description: Use download path
1301+
downloadPath:
1302+
type: string
1303+
description: Download path
1304+
autoTMM:
1305+
type: boolean
1306+
description: Automatic torrent management
12711307
responses:
12721308
'201':
12731309
description: Torrent added successfully

internal/web/swagger/openapi_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
package swagger
55

66
import (
7+
"go/ast"
8+
"go/parser"
9+
"go/token"
10+
"path/filepath"
11+
"runtime"
712
"testing"
813

914
"gopkg.in/yaml.v3"
@@ -105,3 +110,121 @@ func TestOpenAPISecuritySchemes(t *testing.T) {
105110
}
106111
}
107112
}
113+
114+
// TestAddTorrentFormFieldsDocumented verifies every r.FormValue() parameter in the
115+
// add-torrent handler is documented in the OpenAPI spec's multipart schema, and vice versa.
116+
// This catches mismatches like savePath vs savepath that cause silent API failures.
117+
func TestAddTorrentFormFieldsDocumented(t *testing.T) {
118+
// Locate torrents.go relative to this test file so the test works
119+
// regardless of the working directory used by `go test`.
120+
_, thisFile, _, ok := runtime.Caller(0)
121+
if !ok {
122+
t.Fatal("runtime.Caller failed")
123+
}
124+
handlerPath := filepath.Join(filepath.Dir(thisFile), "..", "..", "api", "handlers", "torrents.go")
125+
126+
// Parse the handler source with go/parser so we only inspect the
127+
// AddTorrent method and extract FormValue string arguments from the AST.
128+
fset := token.NewFileSet()
129+
file, err := parser.ParseFile(fset, handlerPath, nil, 0)
130+
if err != nil {
131+
t.Fatalf("Failed to parse torrents handler: %v", err)
132+
}
133+
134+
handlerFields := make(map[string]bool)
135+
for _, decl := range file.Decls {
136+
fn, ok := decl.(*ast.FuncDecl)
137+
if !ok || fn.Name.Name != "AddTorrent" {
138+
continue
139+
}
140+
// Walk the AST of AddTorrent looking for r.FormValue("...") calls.
141+
ast.Inspect(fn.Body, func(n ast.Node) bool {
142+
call, ok := n.(*ast.CallExpr)
143+
if !ok || len(call.Args) != 1 {
144+
return true
145+
}
146+
sel, ok := call.Fun.(*ast.SelectorExpr)
147+
if !ok || sel.Sel.Name != "FormValue" {
148+
return true
149+
}
150+
arg, ok := call.Args[0].(*ast.BasicLit)
151+
if !ok || arg.Kind != token.STRING {
152+
return true
153+
}
154+
// Strip quotes from the string literal.
155+
field := arg.Value[1 : len(arg.Value)-1]
156+
handlerFields[field] = true
157+
return true
158+
})
159+
}
160+
if len(handlerFields) == 0 {
161+
t.Fatal("No FormValue calls found in AddTorrent handler")
162+
}
163+
164+
// Parse the OpenAPI spec and extract properties from the add-torrent endpoint.
165+
var spec map[string]any
166+
if err := yaml.Unmarshal(openapiYAML, &spec); err != nil {
167+
t.Fatalf("Failed to parse OpenAPI spec: %v", err)
168+
}
169+
170+
// Navigate: paths -> /api/instances/{instanceID}/torrents -> post -> requestBody
171+
// -> content -> multipart/form-data -> schema -> properties
172+
asMap := func(v any, path string) map[string]any {
173+
t.Helper()
174+
m, ok := v.(map[string]any)
175+
if !ok {
176+
t.Fatalf("Expected map at %s, got %T", path, v)
177+
}
178+
return m
179+
}
180+
key := func(m map[string]any, k, path string) any {
181+
t.Helper()
182+
v, ok := m[k]
183+
if !ok {
184+
t.Fatalf("Missing key %q at %s", k, path)
185+
}
186+
return v
187+
}
188+
189+
paths := asMap(spec["paths"], "spec.paths")
190+
torrentsPath := asMap(key(paths, "/api/instances/{instanceID}/torrents", "paths"), "paths[torrents]")
191+
post := asMap(key(torrentsPath, "post", "torrents"), "torrents.post")
192+
reqBody := asMap(key(post, "requestBody", "post"), "post.requestBody")
193+
content := asMap(key(reqBody, "content", "requestBody"), "requestBody.content")
194+
formData := asMap(key(content, "multipart/form-data", "content"), "content[multipart/form-data]")
195+
schema := asMap(key(formData, "schema", "formData"), "formData.schema")
196+
properties := asMap(key(schema, "properties", "schema"), "schema.properties")
197+
198+
specFields := make(map[string]bool)
199+
for name := range properties {
200+
specFields[name] = true
201+
}
202+
203+
// torrentFile is a file upload field, not read via FormValue — exclude it.
204+
// urls is read via FormValue but also present in spec.
205+
skipHandler := map[string]bool{
206+
"torrentFile": true,
207+
}
208+
209+
// Check: every handler field must be in the spec.
210+
for field := range handlerFields {
211+
if skipHandler[field] {
212+
continue
213+
}
214+
if !specFields[field] {
215+
t.Errorf("Handler reads r.FormValue(%q) but OpenAPI spec does not document it", field)
216+
}
217+
}
218+
219+
// Check: every spec field must be in the handler (or be torrentFile).
220+
for field := range specFields {
221+
if skipHandler[field] {
222+
continue
223+
}
224+
if !handlerFields[field] {
225+
t.Errorf("OpenAPI spec documents %q but handler does not read it via r.FormValue", field)
226+
}
227+
}
228+
229+
t.Logf("Handler fields: %d, Spec fields: %d", len(handlerFields), len(specFields))
230+
}

0 commit comments

Comments
 (0)