summaryrefslogtreecommitdiff
path: root/internal/image/gemini_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/image/gemini_test.go')
-rw-r--r--internal/image/gemini_test.go333
1 files changed, 333 insertions, 0 deletions
diff --git a/internal/image/gemini_test.go b/internal/image/gemini_test.go
new file mode 100644
index 0000000..8c34c6e
--- /dev/null
+++ b/internal/image/gemini_test.go
@@ -0,0 +1,333 @@
+package image
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "google.golang.org/genai"
+)
+
+func TestNewGeminiProvider(t *testing.T) {
+ t.Parallel()
+
+ client := NewGeminiProvider(&GeminiConfig{APIKey: "test-key"})
+ if client == nil {
+ t.Fatal("expected client")
+ }
+ if client.config == nil {
+ t.Fatal("expected normalized config")
+ }
+ if client.config.Model != DefaultGeminiImageModel {
+ t.Fatalf("expected default model %q, got %q", DefaultGeminiImageModel, client.config.Model)
+ }
+ if client.config.TextModel != DefaultGeminiTextModel {
+ t.Fatalf("expected default text model %q, got %q", DefaultGeminiTextModel, client.config.TextModel)
+ }
+ if client.Name() != Gemini {
+ t.Fatalf("Name() = %q, want %q", client.Name(), Gemini)
+ }
+}
+
+func TestGeminiProvider_NoAPIKey(t *testing.T) {
+ client := NewGeminiProvider(&GeminiConfig{})
+
+ _, err := client.Search(context.Background(), DefaultSearchOptions("ябълка"))
+ if err == nil {
+ t.Fatal("expected error for missing API key")
+ }
+
+ searchErr, ok := err.(*SearchError)
+ if !ok {
+ t.Fatalf("expected SearchError, got %T", err)
+ }
+ if searchErr.Code != "NO_API_KEY" {
+ t.Fatalf("expected NO_API_KEY error, got %s", searchErr.Code)
+ }
+}
+
+func TestGeminiProvider_Search_CustomPromptSkipsTextGeneration(t *testing.T) {
+ originalText := geminiGenerateText
+ originalImage := geminiGenerateImage
+ t.Cleanup(func() {
+ geminiGenerateText = originalText
+ geminiGenerateImage = originalImage
+ })
+
+ geminiGenerateText = func(context.Context, *GeminiProvider, string, string, string, float32, int32) (string, error) {
+ t.Fatal("unexpected text generation for custom prompt")
+ return "", nil
+ }
+
+ var gotPrompt string
+ geminiGenerateImage = func(_ context.Context, _ *GeminiProvider, prompt, _ string) ([]byte, string, error) {
+ gotPrompt = prompt
+ return mustJPEGBytes(t), "image/jpeg", nil
+ }
+
+ client := NewGeminiProvider(&GeminiConfig{APIKey: "test-key"})
+ callbackCalled := false
+ client.SetPromptCallback(func(prompt string) {
+ callbackCalled = true
+ if prompt != "custom flashcard prompt" {
+ t.Fatalf("callback prompt = %q, want %q", prompt, "custom flashcard prompt")
+ }
+ })
+
+ results, err := client.Search(context.Background(), &SearchOptions{
+ Query: "ябълка",
+ Translation: "banana",
+ CustomPrompt: " custom flashcard prompt ",
+ })
+ if err != nil {
+ t.Fatalf("Search() unexpected error: %v", err)
+ }
+ if gotPrompt != "custom flashcard prompt" {
+ t.Fatalf("image prompt = %q, want %q", gotPrompt, "custom flashcard prompt")
+ }
+ if !callbackCalled {
+ t.Fatal("expected prompt callback to be called")
+ }
+ if client.LastPrompt() != "custom flashcard prompt" {
+ t.Fatalf("LastPrompt() = %q, want %q", client.LastPrompt(), "custom flashcard prompt")
+ }
+ if len(results) != 1 {
+ t.Fatalf("expected 1 result, got %d", len(results))
+ }
+ if results[0].Source != Gemini {
+ t.Fatalf("Source = %q, want %q", results[0].Source, Gemini)
+ }
+ if !strings.HasPrefix(results[0].URL, geminiDataPrefix) {
+ t.Fatalf("expected PNG data URI, got %q", results[0].URL)
+ }
+}
+
+func TestGeminiProvider_Search_GeneratedPromptFlow(t *testing.T) {
+ originalText := geminiGenerateText
+ originalImage := geminiGenerateImage
+ originalStyles := append([]string(nil), ArtisticStyles...)
+ t.Cleanup(func() {
+ geminiGenerateText = originalText
+ geminiGenerateImage = originalImage
+ ArtisticStyles = originalStyles
+ })
+
+ ArtisticStyles = []string{"Photorealism"}
+
+ var sceneCalls int
+ var gotPrompt string
+ var callbackPrompt string
+ geminiGenerateText = func(_ context.Context, _ *GeminiProvider, _, systemPrompt, userPrompt string, temperature float32, maxOutputTokens int32) (string, error) {
+ if strings.Contains(systemPrompt, "educational flashcards for language learning") {
+ sceneCalls++
+ if temperature != 0.7 || maxOutputTokens != 100 {
+ t.Fatalf("scene params = %v/%d, want 0.7/100", temperature, maxOutputTokens)
+ }
+ if !strings.Contains(userPrompt, "apple") {
+ t.Fatalf("scene prompt = %q, want English translation", userPrompt)
+ }
+ return "A bright apple sits centered on a wooden table.", nil
+ }
+ t.Fatalf("unexpected system prompt: %q", systemPrompt)
+ return "", nil
+ }
+
+ geminiGenerateImage = func(_ context.Context, _ *GeminiProvider, prompt, _ string) ([]byte, string, error) {
+ gotPrompt = prompt
+ return mustJPEGBytes(t), "image/jpeg", nil
+ }
+
+ client := NewGeminiProvider(&GeminiConfig{APIKey: "test-key"})
+ callbackCalled := false
+ client.SetPromptCallback(func(prompt string) {
+ callbackCalled = true
+ callbackPrompt = prompt
+ })
+
+ results, err := client.Search(context.Background(), &SearchOptions{
+ Query: "ябълка",
+ Translation: "apple",
+ })
+ if err != nil {
+ t.Fatalf("Search() unexpected error: %v", err)
+ }
+ if sceneCalls != 1 {
+ t.Fatalf("sceneCalls = %d, want 1", sceneCalls)
+ }
+ if !callbackCalled {
+ t.Fatal("expected prompt callback to be called")
+ }
+ if callbackPrompt != gotPrompt {
+ t.Fatalf("prompt callback = %q, want %q", callbackPrompt, gotPrompt)
+ }
+ if client.LastPrompt() != gotPrompt {
+ t.Fatalf("LastPrompt() = %q, want %q", client.LastPrompt(), gotPrompt)
+ }
+ if len(results) != 1 {
+ t.Fatalf("expected 1 result, got %d", len(results))
+ }
+
+ result := results[0]
+ if result.Source != Gemini {
+ t.Fatalf("Source = %q, want %q", result.Source, Gemini)
+ }
+ if result.Width != 1 || result.Height != 1 {
+ t.Fatalf("Size = %dx%d, want %dx%d", result.Width, result.Height, 1, 1)
+ }
+ if !strings.Contains(result.Description, "apple") {
+ t.Fatalf("Description = %q, want translated word", result.Description)
+ }
+ if !strings.Contains(gotPrompt, "Generate a Photorealism educational flashcard image illustrating \"apple\".") {
+ t.Fatalf("Prompt = %q, want translated subject in generated prompt", gotPrompt)
+ }
+ if !strings.Contains(gotPrompt, "Scene: A bright apple sits centered on a wooden table.") {
+ t.Fatalf("Prompt = %q, want generated scene in prompt", gotPrompt)
+ }
+
+ reader, err := client.Download(context.Background(), result.URL)
+ if err != nil {
+ t.Fatalf("Download() unexpected error: %v", err)
+ }
+ t.Cleanup(func() {
+ _ = reader.Close()
+ })
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatalf("ReadAll() unexpected error: %v", err)
+ }
+ if !bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) {
+ t.Fatalf("Search output was not normalized to PNG")
+ }
+}
+
+func TestGeminiProvider_Search_InvalidOptions(t *testing.T) {
+ t.Parallel()
+
+ client := NewGeminiProvider(&GeminiConfig{APIKey: "test-key"})
+ _, err := client.Search(context.Background(), nil)
+ if err == nil {
+ t.Fatal("expected error for nil options")
+ }
+
+ searchErr, ok := err.(*SearchError)
+ if !ok {
+ t.Fatalf("expected SearchError, got %T", err)
+ }
+ if searchErr.Code != "INVALID_OPTIONS" {
+ t.Fatalf("expected INVALID_OPTIONS, got %s", searchErr.Code)
+ }
+}
+
+func TestGeminiProvider_DownloadDataURI(t *testing.T) {
+ client := &GeminiProvider{}
+ payload := mustPNGBytes(t)
+ url := geminiDataPrefix + base64.StdEncoding.EncodeToString(payload)
+
+ reader, err := client.Download(context.Background(), url)
+ if err != nil {
+ t.Fatalf("Download() unexpected error: %v", err)
+ }
+ t.Cleanup(func() {
+ _ = reader.Close()
+ })
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatalf("ReadAll() unexpected error: %v", err)
+ }
+ if !bytes.Equal(data, payload) {
+ t.Fatalf("Download() = %v, want %v", data, payload)
+ }
+}
+
+func TestGeminiProvider_DownloadHTTPFallback(t *testing.T) {
+ client := &GeminiProvider{}
+ payload := []byte("fallback image bytes")
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ t.Fatalf("request method = %s, want GET", r.Method)
+ }
+ _, _ = w.Write(payload)
+ }))
+ t.Cleanup(server.Close)
+
+ reader, err := client.Download(context.Background(), server.URL)
+ if err != nil {
+ t.Fatalf("Download() unexpected error: %v", err)
+ }
+ t.Cleanup(func() {
+ _ = reader.Close()
+ })
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatalf("ReadAll() unexpected error: %v", err)
+ }
+ if !bytes.Equal(data, payload) {
+ t.Fatalf("Download() = %v, want %v", data, payload)
+ }
+}
+
+func TestExtractGeneratedImage(t *testing.T) {
+ t.Parallel()
+
+ want := mustPNGBytes(t)
+ resp := &genai.GenerateContentResponse{
+ Candidates: []*genai.Candidate{
+ {
+ Content: &genai.Content{
+ Parts: []*genai.Part{
+ {InlineData: &genai.Blob{Data: want, MIMEType: "image/png"}},
+ },
+ },
+ },
+ },
+ }
+
+ got, mimeType, err := extractGeneratedImage(resp)
+ if err != nil {
+ t.Fatalf("extractGeneratedImage() error = %v", err)
+ }
+ if mimeType != "image/png" {
+ t.Fatalf("mimeType = %q, want %q", mimeType, "image/png")
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatalf("extractGeneratedImage() = %v, want %v", got, want)
+ }
+}
+
+func TestEncodeAndNormalizePNG(t *testing.T) {
+ t.Parallel()
+
+ jpegBytes := mustJPEGBytes(t)
+ pngBytes, err := normalizePNG(jpegBytes, "image/jpeg")
+ if err != nil {
+ t.Fatalf("normalizePNG() unexpected error: %v", err)
+ }
+ if !bytes.HasPrefix(pngBytes, []byte("\x89PNG\r\n\x1a\n")) {
+ t.Fatalf("normalizePNG() did not return PNG data")
+ }
+
+ dataURL, err := encodeDataURL(jpegBytes, "image/jpeg")
+ if err != nil {
+ t.Fatalf("encodeDataURL() unexpected error: %v", err)
+ }
+ if !strings.HasPrefix(dataURL, geminiDataPrefix) {
+ t.Fatalf("encodeDataURL() = %q, want PNG data URI", dataURL)
+ }
+}
+
+func TestDecodeDataURIErrors(t *testing.T) {
+ t.Parallel()
+
+ if _, err := decodeDataURL("not-a-data-uri"); err == nil {
+ t.Fatal("expected error for invalid data URI")
+ }
+}