diff --git a/internal/error_accumulator_test.go b/internal/error_accumulator_test.go index d48f28177..3fa9d7714 100644 --- a/internal/error_accumulator_test.go +++ b/internal/error_accumulator_test.go @@ -1,41 +1,32 @@ package openai_test import ( - "bytes" - "errors" "testing" - utils "github.com/sashabaranov/go-openai/internal" - "github.com/sashabaranov/go-openai/internal/test" + openai "github.com/sashabaranov/go-openai/internal" + "github.com/sashabaranov/go-openai/internal/test/checks" ) -func TestErrorAccumulatorBytes(t *testing.T) { - accumulator := &utils.DefaultErrorAccumulator{ - Buffer: &bytes.Buffer{}, +func TestDefaultErrorAccumulator_WriteMultiple(t *testing.T) { + ea, ok := openai.NewErrorAccumulator().(*openai.DefaultErrorAccumulator) + if !ok { + t.Fatal("type assertion to *DefaultErrorAccumulator failed") } + checks.NoError(t, ea.Write([]byte("{\"error\": \"test1\"}"))) + checks.NoError(t, ea.Write([]byte("{\"error\": \"test2\"}"))) - errBytes := accumulator.Bytes() - if len(errBytes) != 0 { - t.Fatalf("Did not return nil with empty bytes: %s", string(errBytes)) - } - - err := accumulator.Write([]byte("{}")) - if err != nil { - t.Fatalf("%+v", err) - } - - errBytes = accumulator.Bytes() - if len(errBytes) == 0 { - t.Fatalf("Did not return error bytes when has error: %s", string(errBytes)) + expected := "{\"error\": \"test1\"}{\"error\": \"test2\"}" + if string(ea.Bytes()) != expected { + t.Fatalf("Expected %q, got %q", expected, ea.Bytes()) } } -func TestErrorByteWriteErrors(t *testing.T) { - accumulator := &utils.DefaultErrorAccumulator{ - Buffer: &test.FailingErrorBuffer{}, +func TestDefaultErrorAccumulator_EmptyBuffer(t *testing.T) { + ea, ok := openai.NewErrorAccumulator().(*openai.DefaultErrorAccumulator) + if !ok { + t.Fatal("type assertion to *DefaultErrorAccumulator failed") } - err := accumulator.Write([]byte("{")) - if !errors.Is(err, test.ErrTestErrorAccumulatorWriteFailed) { - t.Fatalf("Did not return error when write failed: %v", err) + if len(ea.Bytes()) != 0 { + t.Fatal("Buffer should be empty initially") } } diff --git a/internal/form_builder.go b/internal/form_builder.go index 2224fad45..c2d18fcb7 100644 --- a/internal/form_builder.go +++ b/internal/form_builder.go @@ -53,6 +53,9 @@ func (fb *DefaultFormBuilder) createFormFile(fieldname string, r io.Reader, file } func (fb *DefaultFormBuilder) WriteField(fieldname, value string) error { + if fieldname == "" { + return fmt.Errorf("fieldname cannot be empty") + } return fb.writer.WriteField(fieldname, value) } diff --git a/internal/form_builder_test.go b/internal/form_builder_test.go index 8df989e3b..12f8911ba 100644 --- a/internal/form_builder_test.go +++ b/internal/form_builder_test.go @@ -1,14 +1,57 @@ package openai //nolint:testpackage // testing private field import ( + "errors" + "strings" + "github.com/sashabaranov/go-openai/internal/test/checks" "bytes" - "errors" "os" "testing" ) +type mockFormBuilder struct { + mockCreateFormFile func(string, *os.File) error + mockWriteField func(string, string) error + mockClose func() error +} + +func (m *mockFormBuilder) CreateFormFile(fieldname string, file *os.File) error { + return m.mockCreateFormFile(fieldname, file) +} + +func (m *mockFormBuilder) WriteField(fieldname, value string) error { + return m.mockWriteField(fieldname, value) +} + +func (m *mockFormBuilder) Close() error { + return m.mockClose() +} + +func (m *mockFormBuilder) FormDataContentType() string { + return "" +} + +func TestCloseMethod(t *testing.T) { + t.Run("NormalClose", func(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + checks.NoError(t, builder.Close(), "正常关闭应成功") + }) + + t.Run("ErrorPropagation", func(t *testing.T) { + errorMock := errors.New("mock close error") + mockBuilder := &mockFormBuilder{ + mockClose: func() error { + return errorMock + }, + } + err := mockBuilder.Close() + checks.ErrorIs(t, err, errorMock, "应传递关闭错误") + }) +} + type failingWriter struct { } @@ -43,3 +86,95 @@ func TestFormBuilderWithClosedFile(t *testing.T) { checks.HasError(t, err, "formbuilder should return error if file is closed") checks.ErrorIs(t, err, os.ErrClosed, "formbuilder should return error if file is closed") } + +func TestMultiPartFormUploads(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + + t.Run("MultipleFiles", func(t *testing.T) { + file1, _ := os.CreateTemp(t.TempDir(), "*.png") + file2, _ := os.CreateTemp(t.TempDir(), "*.jpg") + defer file1.Close() + defer file2.Close() + + checks.NoError(t, builder.CreateFormFile("image1", file1), "PNG file upload failed") + checks.NoError(t, builder.CreateFormFile("image2", file2), "JPG file upload failed") + checks.NoError(t, builder.WriteField("description", "test images"), "Field write failed") + }) + + t.Run("LargeFileConcurrent", func(t *testing.T) { + bigFile, _ := os.CreateTemp(t.TempDir(), "*.bin") + defer bigFile.Close() + _, err := bigFile.Write(make([]byte, 1024*1024*5)) // 5MB test file + checks.NoError(t, err, "Failed to write large file data") + checks.NoError(t, builder.CreateFormFile("bigfile", bigFile), "Large file upload failed") + checks.NoError(t, builder.WriteField("note", "large file test"), "Field write failed") + }) + + t.Run("MixedContentTypes", func(t *testing.T) { + csvFile, _ := os.CreateTemp(t.TempDir(), "*.csv") + textFile, _ := os.CreateTemp(t.TempDir(), "*.txt") + defer csvFile.Close() + defer textFile.Close() + + checks.NoError(t, builder.CreateFormFile("data", csvFile), "CSV file upload failed") + checks.NoError(t, builder.CreateFormFile("text", textFile), "Text file upload failed") + checks.NoError(t, builder.WriteField("format", "mixed"), "Field write failed") + }) +} + +func TestFormDataContentType(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + contentType := builder.FormDataContentType() + if !strings.HasPrefix(contentType, "multipart/form-data") { + t.Fatalf("Content-Type格式错误,期望multipart/form-data开头,实际得到:%s", contentType) + } +} + +func TestCreateFormFileReader(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + + t.Run("SpecialCharacters", func(t *testing.T) { + checks.NoError(t, builder.CreateFormFileReader("field", strings.NewReader("content"), "测 试@file.txt"), "特殊字符文件名应处理成功") + }) + + t.Run("InvalidReader", func(t *testing.T) { + err := builder.CreateFormFileReader("field", &failingReader{}, "valid.txt") + checks.HasError(t, err, "无效reader应返回错误") + }) +} + +type failingReader struct{} + +func (r *failingReader) Read(_ []byte) (int, error) { + return 0, errors.New("mock read error") +} + +func TestWriteFieldEdgeCases(t *testing.T) { + mockErr := errors.New("mock write error") + t.Run("EmptyFieldName", func(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + err := builder.WriteField("", "valid-value") + checks.HasError(t, err, "should return error for empty field name") + }) + + t.Run("EmptyValue", func(t *testing.T) { + body := &bytes.Buffer{} + builder := NewFormBuilder(body) + err := builder.WriteField("valid-field", "") + checks.NoError(t, err, "should allow empty value") + }) + + t.Run("MockWriterFailure", func(t *testing.T) { + mockBuilder := &mockFormBuilder{ + mockWriteField: func(_, _ string) error { + return mockErr + }, + } + err := mockBuilder.WriteField("field", "value") + checks.ErrorIs(t, err, mockErr, "should propagate write error") + }) +} diff --git a/internal/marshaller_test.go b/internal/marshaller_test.go new file mode 100644 index 000000000..70694faed --- /dev/null +++ b/internal/marshaller_test.go @@ -0,0 +1,34 @@ +package openai_test + +import ( + "testing" + + openai "github.com/sashabaranov/go-openai/internal" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestJSONMarshaller_Normal(t *testing.T) { + jm := &openai.JSONMarshaller{} + data := map[string]string{"key": "value"} + + b, err := jm.Marshal(data) + checks.NoError(t, err) + if len(b) == 0 { + t.Fatal("should return non-empty bytes") + } +} + +func TestJSONMarshaller_InvalidInput(t *testing.T) { + jm := &openai.JSONMarshaller{} + _, err := jm.Marshal(make(chan int)) + checks.HasError(t, err, "should return error for unsupported type") +} + +func TestJSONMarshaller_EmptyValue(t *testing.T) { + jm := &openai.JSONMarshaller{} + b, err := jm.Marshal(nil) + checks.NoError(t, err) + if string(b) != "null" { + t.Fatalf("unexpected marshaled value: %s", string(b)) + } +} diff --git a/internal/unmarshaler_test.go b/internal/unmarshaler_test.go new file mode 100644 index 000000000..d63eac779 --- /dev/null +++ b/internal/unmarshaler_test.go @@ -0,0 +1,37 @@ +package openai_test + +import ( + "testing" + + openai "github.com/sashabaranov/go-openai/internal" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestJSONUnmarshaler_Normal(t *testing.T) { + jm := &openai.JSONUnmarshaler{} + data := []byte(`{"key":"value"}`) + var v map[string]string + + err := jm.Unmarshal(data, &v) + checks.NoError(t, err) + if v["key"] != "value" { + t.Fatal("unmarshal result mismatch") + } +} + +func TestJSONUnmarshaler_InvalidJSON(t *testing.T) { + jm := &openai.JSONUnmarshaler{} + data := []byte(`{invalid}`) + var v map[string]interface{} + + err := jm.Unmarshal(data, &v) + checks.HasError(t, err, "should return error for invalid JSON") +} + +func TestJSONUnmarshaler_EmptyInput(t *testing.T) { + jm := &openai.JSONUnmarshaler{} + var v interface{} + + err := jm.Unmarshal(nil, &v) + checks.HasError(t, err, "should return error for nil input") +}