Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions pkg/restheadspec/empty_result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package restheadspec

import (
"encoding/json"
"net/http"
"strings"
"testing"

"github.com/bitechdev/ResolveSpec/pkg/common"
)

// Test that normalizeResultArray returns empty array when no records found without ID
func TestNormalizeResultArray_EmptyArrayWhenNoID(t *testing.T) {
handler := &Handler{}

tests := []struct {
name string
input interface{}
shouldBeEmptyArr bool
}{
{
name: "nil should return empty array",
input: nil,
shouldBeEmptyArr: true,
},
{
name: "empty slice should return empty array",
input: []*EmptyTestModel{},
shouldBeEmptyArr: true,
},
{
name: "single element should return the element",
input: []*EmptyTestModel{{ID: 1, Name: "test"}},
shouldBeEmptyArr: false,
},
{
name: "multiple elements should return the slice",
input: []*EmptyTestModel{
{ID: 1, Name: "test1"},
{ID: 2, Name: "test2"},
},
shouldBeEmptyArr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := handler.normalizeResultArray(tt.input)

// For cases that should return empty array
if tt.shouldBeEmptyArr {
emptyArr, ok := result.([]interface{})
if !ok {
t.Errorf("Expected empty array []interface{}{}, got %T: %v", result, result)
return
}
if len(emptyArr) != 0 {
t.Errorf("Expected empty array with length 0, got length %d", len(emptyArr))
}

// Verify it serializes to [] and not null
jsonBytes, err := json.Marshal(result)
if err != nil {
t.Errorf("Failed to marshal result: %v", err)
return
}
if string(jsonBytes) != "[]" {
t.Errorf("Expected JSON '[]', got '%s'", string(jsonBytes))
}
}
})
}
}

// Test that sendFormattedResponse adds X-No-Data-Found header
func TestSendFormattedResponse_NoDataFoundHeader(t *testing.T) {
handler := &Handler{}

// Mock ResponseWriter
mockWriter := &MockTestResponseWriter{
headers: make(map[string]string),
}

metadata := &common.Metadata{
Total: 0,
Count: 0,
Filtered: 0,
Limit: 10,
Offset: 0,
}

options := ExtendedRequestOptions{
RequestOptions: common.RequestOptions{},
}

// Test with empty data
emptyData := []interface{}{}
handler.sendFormattedResponse(mockWriter, emptyData, metadata, options)

// Check if X-No-Data-Found header was set
if mockWriter.headers["X-No-Data-Found"] != "true" {
t.Errorf("Expected X-No-Data-Found header to be 'true', got '%s'", mockWriter.headers["X-No-Data-Found"])
}

// Verify the body is an empty array
if mockWriter.body == nil {
t.Error("Expected body to be set, got nil")
} else {
bodyBytes, err := json.Marshal(mockWriter.body)
if err != nil {
t.Errorf("Failed to marshal body: %v", err)
}
// The body should be wrapped in a Response object with "data" field
bodyStr := string(bodyBytes)
if !strings.Contains(bodyStr, `"data":[]`) && !strings.Contains(bodyStr, `"result":[]`) {
t.Errorf("Expected body to contain empty array, got: %s", bodyStr)
}
}
}

// Test that sendResponseWithOptions adds X-No-Data-Found header
func TestSendResponseWithOptions_NoDataFoundHeader(t *testing.T) {
handler := &Handler{}

// Mock ResponseWriter
mockWriter := &MockTestResponseWriter{
headers: make(map[string]string),
}

metadata := &common.Metadata{}
options := &ExtendedRequestOptions{}

// Test with nil data
handler.sendResponseWithOptions(mockWriter, nil, metadata, options)

// Check if X-No-Data-Found header was set
if mockWriter.headers["X-No-Data-Found"] != "true" {
t.Errorf("Expected X-No-Data-Found header to be 'true', got '%s'", mockWriter.headers["X-No-Data-Found"])
}

// Check status code is 200
if mockWriter.statusCode != 200 {
t.Errorf("Expected status code 200, got %d", mockWriter.statusCode)
}

// Verify the body is an empty array
if mockWriter.body == nil {
t.Error("Expected body to be set, got nil")
} else {
bodyBytes, err := json.Marshal(mockWriter.body)
if err != nil {
t.Errorf("Failed to marshal body: %v", err)
}
bodyStr := string(bodyBytes)
if bodyStr != "[]" {
t.Errorf("Expected body to be '[]', got: %s", bodyStr)
}
}
}

// MockTestResponseWriter for testing
type MockTestResponseWriter struct {
headers map[string]string
statusCode int
body interface{}
}

func (m *MockTestResponseWriter) SetHeader(key, value string) {
m.headers[key] = value
}

func (m *MockTestResponseWriter) WriteHeader(statusCode int) {
m.statusCode = statusCode
}

func (m *MockTestResponseWriter) Write(data []byte) (int, error) {
return len(data), nil
}

func (m *MockTestResponseWriter) WriteJSON(data interface{}) error {
m.body = data
return nil
}

func (m *MockTestResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
return nil
}

// EmptyTestModel for testing
type EmptyTestModel struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
49 changes: 34 additions & 15 deletions pkg/restheadspec/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2143,12 +2143,22 @@ func (h *Handler) sendResponse(w common.ResponseWriter, data interface{}, metada
// sendResponseWithOptions sends a response with optional formatting
func (h *Handler) sendResponseWithOptions(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options *ExtendedRequestOptions) {
w.SetHeader("Content-Type", "application/json")

// Handle nil data - convert to empty array
if data == nil {
data = map[string]interface{}{}
w.WriteHeader(http.StatusPartialContent)
} else {
w.WriteHeader(http.StatusOK)
data = []interface{}{}
}

// Calculate data length after nil conversion
dataLen := reflection.Len(data)

// Add X-No-Data-Found header when no records were found
if dataLen == 0 {
w.SetHeader("X-No-Data-Found", "true")
}

w.WriteHeader(http.StatusOK)

// Normalize single-record arrays to objects if requested
if options != nil && options.SingleRecordAsObject {
data = h.normalizeResultArray(data)
Expand All @@ -2165,7 +2175,7 @@ func (h *Handler) sendResponseWithOptions(w common.ResponseWriter, data interfac
// Returns the single element if data is a slice/array with exactly one element, otherwise returns data unchanged
func (h *Handler) normalizeResultArray(data interface{}) interface{} {
if data == nil {
return map[string]interface{}{}
return []interface{}{}
}

// Use reflection to check if data is a slice or array
Expand All @@ -2180,15 +2190,15 @@ func (h *Handler) normalizeResultArray(data interface{}) interface{} {
// Return the single element
return dataValue.Index(0).Interface()
} else if dataValue.Len() == 0 {
// Return empty object instead of empty array
return map[string]interface{}{}
// Keep empty array as empty array, don't convert to empty object
return []interface{}{}
}
}

if dataValue.Kind() == reflect.String {
str := dataValue.String()
if str == "" || str == "null" {
return map[string]interface{}{}
return []interface{}{}
}

}
Expand All @@ -2199,16 +2209,25 @@ func (h *Handler) normalizeResultArray(data interface{}) interface{} {
func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options ExtendedRequestOptions) {
// Normalize single-record arrays to objects if requested
httpStatus := http.StatusOK

// Handle nil data - convert to empty array
if data == nil {
data = map[string]interface{}{}
httpStatus = http.StatusPartialContent
} else {
dataLen := reflection.Len(data)
if dataLen == 0 {
httpStatus = http.StatusPartialContent
}
data = []interface{}{}
}

// Calculate data length after nil conversion
// Note: This is done BEFORE normalization because X-No-Data-Found indicates
// whether data was found in the database, not the final response format
dataLen := reflection.Len(data)

// Add X-No-Data-Found header when no records were found
if dataLen == 0 {
w.SetHeader("X-No-Data-Found", "true")
}

// Apply normalization after header is set
// normalizeResultArray may convert single-element arrays to objects,
// but the X-No-Data-Found header reflects the original query result
if options.SingleRecordAsObject {
data = h.normalizeResultArray(data)
}
Expand Down
Loading