@@ -154,6 +154,59 @@ var (
154154 }
155155)
156156
157+ // Hardcoded mock api return values for prefix endpoint.
158+ // MockPrefixGetResponse simulates NetBox returning prefixes with object-type
159+ // custom fields as nested objects (the read format from the NetBox REST API).
160+ var (
161+ MockPrefixGetResponse = Response [objects.Prefix ]{
162+ Count : 1 ,
163+ Next : nil ,
164+ Previous : nil ,
165+ Results : []objects.Prefix {
166+ {
167+ NetboxObject : objects.NetboxObject {
168+ ID : 1 ,
169+ Tags : []* objects.Tag {MockDefaultSsotTag },
170+ // NetBox returns object-type custom fields as nested objects.
171+ // This is the read format — write format expects just the ID.
172+ CustomFields : map [string ]interface {}{
173+ "source" : "test" ,
174+ "orphan_last_seen" : nil ,
175+ "site_ref" : map [string ]interface {}{
176+ "id" : float64 (1 ),
177+ "display" : "LCL" ,
178+ "url" : "https://netbox/api/dcim/sites/1/" ,
179+ "name" : "LCL" ,
180+ "slug" : "lcl" ,
181+ },
182+ },
183+ },
184+ Prefix : "10.0.0.0/24" ,
185+ },
186+ },
187+ }
188+ MockPrefixCreateResponse = objects.Prefix {
189+ NetboxObject : objects.NetboxObject {
190+ ID : 2 , //nolint:mnd
191+ },
192+ Prefix : "192.168.1.0/24" ,
193+ }
194+ MockPrefixPatchResponse = objects.Prefix {
195+ NetboxObject : objects.NetboxObject {
196+ ID : 1 ,
197+ Tags : []* objects.Tag {
198+ MockDefaultSsotTag ,
199+ },
200+ CustomFields : map [string ]interface {}{
201+ "source" : "test" ,
202+ "orphan_last_seen" : nil ,
203+ "site_ref" : float64 (1 ),
204+ },
205+ },
206+ Prefix : "10.0.0.0/24" ,
207+ }
208+ )
209+
157210const (
158211 MockVersionResponseJSON = "{\" django-version\" : \" 4.2.10\" }"
159212)
@@ -347,6 +400,58 @@ func CreateMockServer() *httptest.Server {
347400 },
348401 )
349402
403+ // Prefix endpoint — mimics NetBox 4.2.x REST API behavior:
404+ // - GET returns prefixes with object-type custom fields as nested objects
405+ // - PATCH/POST validates that custom_fields don't contain nested objects with "display"
406+ handler .HandleFunc (
407+ string (constants .PrefixesAPIPath ),
408+ func (w http.ResponseWriter , r * http.Request ) {
409+ switch r .Method {
410+ case http .MethodGet :
411+ w .WriteHeader (http .StatusOK )
412+ resp , err := json .Marshal (MockPrefixGetResponse )
413+ if err != nil {
414+ log .Printf ("Error marshaling prefix GET response: %v" , err )
415+ }
416+ _ , _ = w .Write (resp )
417+ case http .MethodPost :
418+ body , _ := io .ReadAll (r .Body )
419+ if err := validateCustomFieldsPayload (body ); err != nil {
420+ w .WriteHeader (http .StatusBadRequest )
421+ errResp := map [string ][]string {"custom_fields" : {err .Error ()}}
422+ resp , _ := json .Marshal (errResp )
423+ _ , _ = w .Write (resp )
424+ return
425+ }
426+ w .WriteHeader (http .StatusCreated )
427+ resp , err := json .Marshal (MockPrefixCreateResponse )
428+ if err != nil {
429+ log .Printf ("Error marshaling prefix create response: %v" , err )
430+ }
431+ _ , _ = w .Write (resp )
432+ case http .MethodPatch :
433+ body , _ := io .ReadAll (r .Body )
434+ if err := validateCustomFieldsPayload (body ); err != nil {
435+ w .WriteHeader (http .StatusBadRequest )
436+ errResp := map [string ][]string {"custom_fields" : {err .Error ()}}
437+ resp , _ := json .Marshal (errResp )
438+ _ , _ = w .Write (resp )
439+ return
440+ }
441+ w .WriteHeader (http .StatusOK )
442+ resp , err := json .Marshal (MockPrefixPatchResponse )
443+ if err != nil {
444+ log .Printf ("Error marshaling prefix patch response: %v" , err )
445+ }
446+ _ , _ = w .Write (resp )
447+ case http .MethodDelete :
448+ w .WriteHeader (http .StatusNoContent )
449+ default :
450+ log .Printf ("Wrong http method: %q" , r .Method ) //nolint:gosec
451+ }
452+ },
453+ )
454+
350455 handler .HandleFunc ("/api/read-error" , func (w http.ResponseWriter , _ * http.Request ) {
351456 w .WriteHeader (http .StatusInternalServerError ) // or any relevant status
352457 //nolint:all
@@ -407,6 +512,35 @@ func (m *FailingHTTPClientRead) RoundTrip(_ *http.Request) (*http.Response, erro
407512 }, nil
408513}
409514
515+ // validateCustomFieldsPayload mimics NetBox 4.2.x REST API validation:
516+ // object-type custom field values must be sent as scalar IDs, not as nested
517+ // objects. If a custom_fields value is a map containing "display", NetBox
518+ // returns 400: "Cannot resolve keyword 'display' into field".
519+ func validateCustomFieldsPayload (body []byte ) error {
520+ var payload map [string ]interface {}
521+ if err := json .Unmarshal (body , & payload ); err != nil {
522+ return nil //nolint:nilerr
523+ }
524+ cf , ok := payload ["custom_fields" ]
525+ if ! ok {
526+ return nil
527+ }
528+ cfMap , ok := cf .(map [string ]interface {})
529+ if ! ok {
530+ return nil
531+ }
532+ for key , val := range cfMap {
533+ if nested , isMap := val .(map [string ]interface {}); isMap {
534+ if _ , hasDisplay := nested ["display" ]; hasDisplay {
535+ return fmt .Errorf (
536+ "cannot resolve keyword 'display' into field for custom_field '%s'" , key , //nolint:perfsprint
537+ )
538+ }
539+ }
540+ }
541+ return nil
542+ }
543+
410544type FaultyReader struct {}
411545
412546func (m * FaultyReader ) Read (_ []byte ) (n int , err error ) {
0 commit comments