2024-04-16 14:26:06 +03:00
package test
import (
"fmt"
"io"
"os"
"reflect"
"strings"
"testing"
)
2025-12-29 14:16:28 +03:00
// extractTypeFromSchema extracts type from schema, handling oneOf cases
// When oneOf contains a type and null, it returns the non-null type
func extractTypeFromSchema ( schema map [ string ] interface { } ) string {
// Check for direct type
if paramType , ok := schema [ "type" ] . ( string ) ; ok {
return paramType
}
// Check for oneOf
if oneOf , ok := schema [ "oneOf" ] . ( [ ] interface { } ) ; ok {
for _ , item := range oneOf {
if itemMap , ok := item . ( map [ string ] interface { } ) ; ok {
if itemType , ok := itemMap [ "type" ] . ( string ) ; ok {
// Skip null type, return the first non-null type found
if itemType != "null" {
return itemType
}
}
}
}
}
return ""
}
2024-04-16 14:26:06 +03:00
func getParameters ( input map [ string ] interface { } ) [ ] interface { } {
var emptySlice [ ] interface { }
2025-08-29 12:51:25 +03:00
methods := [ ] string { "get" , "post" , "put" , "delete" , "patch" , "head" , "options" }
var methodData interface { }
found := false
for _ , method := range methods {
if data , ok := input [ method ] ; ok {
methodData = data
found = true
break
}
}
if ! found {
2024-04-16 14:26:06 +03:00
return emptySlice
}
2025-08-29 12:51:25 +03:00
parameters , ok := methodData . ( map [ string ] interface { } )
2024-04-16 14:26:06 +03:00
if ! ok {
return emptySlice
}
2025-12-23 17:39:58 +03:00
var result [ ] interface { }
2024-04-16 14:26:06 +03:00
2025-12-23 17:39:58 +03:00
if requestBody , ok := parameters [ "requestBody" ] . ( map [ string ] interface { } ) ; ok {
if content , ok := requestBody [ "content" ] . ( map [ string ] interface { } ) ; ok {
// Check for application/x-www-form-urlencoded, application/json, or multipart/form-data
var schemaData map [ string ] interface { }
var found bool
if formData , ok := content [ "application/x-www-form-urlencoded" ] . ( map [ string ] interface { } ) ; ok {
if schema , ok := formData [ "schema" ] . ( map [ string ] interface { } ) ; ok {
schemaData = schema
found = true
}
} else if jsonData , ok := content [ "application/json" ] . ( map [ string ] interface { } ) ; ok {
if schema , ok := jsonData [ "schema" ] . ( map [ string ] interface { } ) ; ok {
schemaData = schema
found = true
}
} else if multipartData , ok := content [ "multipart/form-data" ] . ( map [ string ] interface { } ) ; ok {
if schema , ok := multipartData [ "schema" ] . ( map [ string ] interface { } ) ; ok {
schemaData = schema
found = true
}
2025-08-29 12:51:25 +03:00
}
2025-12-23 17:39:58 +03:00
if found && schemaData != nil {
if properties , ok := schemaData [ "properties" ] . ( map [ string ] interface { } ) ; ok {
requiredFields := make ( map [ string ] bool )
if req , ok := schemaData [ "required" ] . ( [ ] interface { } ) ; ok {
for _ , r := range req {
if reqStr , ok := r . ( string ) ; ok {
requiredFields [ reqStr ] = true
}
}
}
for name , prop := range properties {
propMap , ok := prop . ( map [ string ] interface { } )
if ! ok {
continue
}
newParam := make ( map [ string ] interface { } )
newParam [ "name" ] = name
2025-12-29 14:16:28 +03:00
propType := extractTypeFromSchema ( propMap )
if propType != "" {
2025-12-23 17:39:58 +03:00
newParam [ "type" ] = propType
}
newParam [ "required" ] = requiredFields [ name ]
2025-12-29 14:16:28 +03:00
if propType == "array" {
// Try to get items from propMap first
2025-12-23 17:39:58 +03:00
if items , ok := propMap [ "items" ] . ( map [ string ] interface { } ) ; ok {
newParam [ "items" ] = items
2025-12-29 14:16:28 +03:00
} else if oneOf , ok := propMap [ "oneOf" ] . ( [ ] interface { } ) ; ok {
// If items not in propMap, try to get from oneOf array element
for _ , item := range oneOf {
if itemMap , ok := item . ( map [ string ] interface { } ) ; ok {
if itemType , ok := itemMap [ "type" ] . ( string ) ; ok && itemType == "array" {
if items , ok := itemMap [ "items" ] . ( map [ string ] interface { } ) ; ok {
newParam [ "items" ] = items
break
}
}
}
}
2025-12-23 17:39:58 +03:00
}
}
result = append ( result , newParam )
}
}
}
}
}
if params , ok := parameters [ "parameters" ] . ( [ ] interface { } ) ; ok {
for _ , p := range params {
param , ok := p . ( map [ string ] interface { } )
2025-08-29 12:51:25 +03:00
if ! ok {
continue
}
2025-12-23 17:39:58 +03:00
newParam := make ( map [ string ] interface { } )
if name , ok := param [ "name" ] . ( string ) ; ok {
newParam [ "name" ] = name
2025-08-29 12:51:25 +03:00
}
2025-12-23 17:39:58 +03:00
if schema , ok := param [ "schema" ] . ( map [ string ] interface { } ) ; ok {
2025-12-29 14:16:28 +03:00
paramType := extractTypeFromSchema ( schema )
if paramType != "" {
2025-12-23 17:39:58 +03:00
newParam [ "type" ] = paramType
2025-08-29 12:51:25 +03:00
}
2025-12-29 14:16:28 +03:00
if paramType == "array" {
2025-12-23 17:39:58 +03:00
if items , ok := schema [ "items" ] . ( map [ string ] interface { } ) ; ok {
2025-08-29 12:51:25 +03:00
newParam [ "items" ] = items
2025-12-29 14:16:28 +03:00
} else if oneOf , ok := schema [ "oneOf" ] . ( [ ] interface { } ) ; ok {
for _ , item := range oneOf {
if itemMap , ok := item . ( map [ string ] interface { } ) ; ok {
if itemType , ok := itemMap [ "type" ] . ( string ) ; ok && itemType == "array" {
if items , ok := itemMap [ "items" ] . ( map [ string ] interface { } ) ; ok {
newParam [ "items" ] = items
break
}
}
}
}
2025-08-29 12:51:25 +03:00
}
}
}
2025-12-23 17:39:58 +03:00
// Handle required field
if required , ok := param [ "required" ] . ( bool ) ; ok {
newParam [ "required" ] = required
} else {
newParam [ "required" ] = false
}
result = append ( result , newParam )
2025-08-29 12:51:25 +03:00
}
}
2025-12-23 17:39:58 +03:00
if len ( result ) > 0 {
return result
}
return emptySlice
2024-04-16 14:26:06 +03:00
}
func getBytesFromJSON ( fileName string , t * testing . T ) [ ] byte {
jsonFile , err := os . Open ( fileName )
if err != nil {
t . Error ( err )
}
defer jsonFile . Close ( )
bytes , err := io . ReadAll ( jsonFile )
if err != nil {
t . Error ( err )
}
return bytes
}
func getErrorsFromJSON ( bytes [ ] byte , t * testing . T , cloud string ) {
var requests map [ string ] interface { }
switch cloud {
case "cloudapi" :
requests = getRequestsMapCloudAPI ( )
case "cloudbroker" :
requests = getRequestsMapCloudbroker ( )
2025-08-29 12:51:25 +03:00
case "sdn" :
requests = getRequestsMapSDN ( )
2024-04-16 14:26:06 +03:00
default :
2025-08-29 12:51:25 +03:00
t . Fatalf ( "Wrong cloud provided, expected `cloudapi`, `cloudbroker` or `sdn`, got %s" , cloud )
2024-04-16 14:26:06 +03:00
}
2024-05-31 13:35:39 +03:00
var dataLogs [ ] string
2024-04-16 14:26:06 +03:00
paths , err := getMapFromFile ( bytes )
if err != nil {
t . Error ( err )
}
i := 0
for k , v := range paths {
// exclude deprecated urls from analysis
if ! validateUrlFromJson ( k ) {
continue
}
params := getParameters ( v . ( map [ string ] interface { } ) )
structure , ok := requests [ k ]
if ! ok {
continue
}
i ++
typStruct := reflect . TypeOf ( structure )
var errs [ ] string
// empty request case
if len ( params ) == 0 && structure == nil {
continue
}
if len ( params ) != typStruct . NumField ( ) {
errs = append ( errs , fmt . Sprintf ( "Platform (%d) and golang structure (%d) have different amount of fields." , len ( params ) , typStruct . NumField ( ) ) )
}
2026-01-16 16:50:40 +03:00
paramMap := make ( map [ string ] bool )
paramRequiredMap := make ( map [ string ] bool )
for _ , p := range params {
param , ok := p . ( map [ string ] interface { } )
if ! ok {
continue
}
name , ok := param [ "name" ] . ( string )
if ok {
paramMap [ name ] = true
required , ok := param [ "required" ] . ( bool )
if ok {
paramRequiredMap [ name ] = required
} else {
paramRequiredMap [ name ] = false
}
}
}
2024-04-16 14:26:06 +03:00
for _ , p := range params {
param , ok := p . ( map [ string ] interface { } )
if ! ok {
continue
}
2025-08-29 12:51:25 +03:00
name , ok := param [ "name" ] . ( string )
if ! ok {
name = ""
}
2024-11-12 12:51:21 +03:00
required , ok := param [ "required" ] . ( bool )
if ! ok {
required = false
}
2025-08-29 12:51:25 +03:00
typ , ok := p . ( map [ string ] interface { } ) [ "type" ] . ( string )
if ! ok {
typ = ""
}
2024-04-16 14:26:06 +03:00
var items string
if p . ( map [ string ] interface { } ) [ "items" ] != nil {
itemsTemp := p . ( map [ string ] interface { } ) [ "items" ]
if itemsTemp != nil {
itemsType := itemsTemp . ( map [ string ] interface { } ) [ "type" ]
if itemsType != nil {
2025-08-29 12:51:25 +03:00
items , ok = itemsType . ( string )
if ! ok {
items = ""
}
2024-04-16 14:26:06 +03:00
}
}
}
var found bool
for i := 0 ; i < typStruct . NumField ( ) ; i ++ {
jsonTag := typStruct . Field ( i ) . Tag . Get ( "json" )
validation , _ := typStruct . Field ( i ) . Tag . Lookup ( "validate" )
if checkName ( name , jsonTag ) {
if ! checkRequired ( required , validation , typ ) {
errs = append ( errs , fmt . Sprintf ( "Field %s has different required parameters on the platform and in golang structure" , name ) )
}
if ! checkKind ( typ , items , typStruct . Field ( i ) . Type ) {
errs = append ( errs , fmt . Sprintf ( "Field %s has different type parameters on the platform and in golang structure" , name ) )
}
found = true
break
}
}
if ! found {
errs = append ( errs , fmt . Sprintf ( "Platform has field %s that golang structure doesn't" , name ) )
}
}
2026-01-16 16:50:40 +03:00
// Check if required fields in Go structure are missing from platform JSON
// or if they exist but are not required on platform
for i := 0 ; i < typStruct . NumField ( ) ; i ++ {
jsonTag := typStruct . Field ( i ) . Tag . Get ( "json" )
validation , _ := typStruct . Field ( i ) . Tag . Lookup ( "validate" )
fieldName := strings . Split ( jsonTag , "," ) [ 0 ]
if fieldName == "" || fieldName == "-" {
continue
}
if strings . Contains ( validation , "required" ) {
if ! paramMap [ fieldName ] {
errs = append ( errs , fmt . Sprintf ( "Golang structure has required field %s that platform doesn't" , fieldName ) )
} else {
platformRequired := paramRequiredMap [ fieldName ]
if ! platformRequired {
fieldType := typStruct . Field ( i ) . Type
if fieldType . Kind ( ) == reflect . Bool {
errs = append ( errs , fmt . Sprintf ( "Golang structure has required field %s that platform doesn't" , fieldName ) )
}
}
}
}
}
2024-04-16 14:26:06 +03:00
if len ( errs ) > 0 {
2024-05-31 13:35:39 +03:00
msg := fmt . Sprintf ( "Path %s has following errors: %v" , k , errs )
t . Error ( msg )
dataLogs = append ( dataLogs , msg )
2024-04-16 14:26:06 +03:00
}
}
if len ( requests ) != i {
2025-08-29 12:51:25 +03:00
msg := fmt . Sprintf ( "Amount of structure checked (%d) is not the same as amount of platform requests available (%d), please check getRequests func in code." ,
2024-04-16 14:26:06 +03:00
i , len ( requests ) )
2024-05-31 13:35:39 +03:00
t . Error ( msg )
dataLogs = append ( dataLogs , msg )
2024-04-16 14:26:06 +03:00
}
}
// checkName checks if name field from platform has the same value as json tag in golang structure (maybe including omitempty)
func checkName ( name , tag string ) bool {
return strings . Contains ( tag , name )
}
// checkRequired checks if required field from platform has the same value as validate tag in golang structure
func checkRequired ( required bool , validation , fieldType string ) bool {
if required && strings . Contains ( validation , "required" ) {
return true
}
if required && ( ! strings . Contains ( validation , "omitempty" ) && validation != "" ) {
return true
}
if ! required && ( validation == "" || strings . Contains ( validation , "omitempty" ) ) {
return true
}
if fieldType == "boolean" {
return true // otherwise we have issues with setting false/true
}
return false
}
// checkKind checks if type field from platform has the same value as field type in golang structure
func checkKind ( platformType , items string , typ reflect . Type ) bool {
//nolint
switch typ . Kind ( ) {
case reflect . Int , reflect . Int32 , reflect . Int64 , reflect . Int8 , reflect . Int16 , reflect . Uint , reflect . Uint64 , reflect . Uint16 , reflect . Uint32 , reflect . Uint8 :
if platformType == "integer" {
return true
}
return false
case reflect . String :
if platformType == "string" {
return true
}
return false
case reflect . Bool :
if platformType == "boolean" {
return true
}
return false
case reflect . Float32 , reflect . Float64 :
if platformType == "number" {
return true
}
return false
case reflect . Array , reflect . Slice :
if platformType != "array" {
return false
}
elem := typ . Elem ( )
switch elem . Kind ( ) {
case reflect . String :
if items == "string" {
return true
}
return false
case reflect . Int , reflect . Int32 , reflect . Int64 , reflect . Int8 , reflect . Int16 , reflect . Uint , reflect . Uint64 , reflect . Uint16 , reflect . Uint32 , reflect . Uint8 :
if items == "integer" {
return true
}
return false
2024-05-31 13:35:39 +03:00
default : // for cases like dataDisks etc.
return true
2024-04-16 14:26:06 +03:00
}
2024-05-31 13:35:39 +03:00
default : // for cases like drivers etc
return true
}
2024-04-16 14:26:06 +03:00
}