1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
// Package video_test provides unit tests for the Veo video generator.
// All tests are mock-based — no real API calls are made.
package video
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"google.golang.org/genai"
)
// TestNewVeoGenerator_EmptyKey verifies that an empty API key is rejected.
func TestNewVeoGenerator_EmptyKey(t *testing.T) {
t.Parallel()
_, err := NewVeoGenerator("")
if err == nil {
t.Fatal("expected error for empty API key, got nil")
}
}
// TestNewVeoGenerator_WhitespaceKey verifies that a whitespace-only API key is
// treated the same as an empty key.
func TestNewVeoGenerator_WhitespaceKey(t *testing.T) {
t.Parallel()
_, err := NewVeoGenerator(" ")
if err == nil {
t.Fatal("expected error for whitespace API key, got nil")
}
}
// TestNewVeoGenerator_ClientInitFailure verifies that a genai client
// initialisation error propagates as a wrapped error.
func TestNewVeoGenerator_ClientInitFailure(t *testing.T) {
// Do not use t.Parallel: this test replaces the package-global newGenaiClient hook.
// Temporarily replace the genai client constructor with one that always fails.
orig := newGenaiClient
newGenaiClient = func(_ context.Context, _ *genai.ClientConfig) (*genai.Client, error) {
return nil, errors.New("injected init error")
}
t.Cleanup(func() { newGenaiClient = orig })
_, err := NewVeoGenerator("test-api-key")
if err == nil {
t.Fatal("expected error from client init failure, got nil")
}
if !strings.Contains(err.Error(), "injected init error") {
t.Fatalf("unexpected error text: %v", err)
}
}
// TestLoadGalleryImage_Missing verifies that loadGalleryImage returns an error
// when no matching file exists in the given directory.
func TestLoadGalleryImage_Missing(t *testing.T) {
t.Parallel()
dir := t.TempDir()
_, _, err := loadGalleryImage(dir, 1)
if err == nil {
t.Fatal("expected error for missing gallery image, got nil")
}
}
// TestLoadGalleryImage_Found verifies that loadGalleryImage returns the correct
// path and bytes when the expected file exists.
func TestLoadGalleryImage_Found(t *testing.T) {
t.Parallel()
dir := t.TempDir()
imgFile := filepath.Join(dir, "ябълка_gallery_2.png")
wantBytes := []byte("fake-png-data")
if err := os.WriteFile(imgFile, wantBytes, 0o644); err != nil {
t.Fatalf("setup: write test image: %v", err)
}
gotPath, gotBytes, err := loadGalleryImage(dir, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotPath != imgFile {
t.Errorf("path: got %q, want %q", gotPath, imgFile)
}
if string(gotBytes) != string(wantBytes) {
t.Errorf("bytes: got %q, want %q", gotBytes, wantBytes)
}
}
// TestBuildVeoPrompt verifies that the prompt is non-empty and contains the
// key terms that shape Veo's output style.
func TestBuildVeoPrompt(t *testing.T) {
t.Parallel()
prompt := buildVeoPrompt()
if prompt == "" {
t.Fatal("buildVeoPrompt returned empty string")
}
keywords := []string{"comic", "Bulgarian", "educational", "8-second"}
for _, kw := range keywords {
if !strings.Contains(prompt, kw) {
t.Errorf("expected prompt to contain %q", kw)
}
}
}
// TestSaveMP4_WritesFile verifies that saveMP4 creates the expected MP4 file and
// returns its absolute path.
func TestSaveMP4_WritesFile(t *testing.T) {
t.Parallel()
outDir := t.TempDir()
fakeVideo := []byte{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70} // minimal ftyp box bytes
// Simulate source path like the real gallery image would produce.
srcPath := "/stories/ябълка/ябълка_gallery_3.png"
got, err := saveMP4(fakeVideo, outDir, srcPath, 3)
if err != nil {
t.Fatalf("saveMP4 failed: %v", err)
}
if !strings.HasSuffix(got, ".mp4") {
t.Errorf("expected .mp4 suffix, got %q", got)
}
data, err := os.ReadFile(got)
if err != nil {
t.Fatalf("reading saved MP4: %v", err)
}
if string(data) != string(fakeVideo) {
t.Errorf("file contents mismatch")
}
}
// TestSaveMP4_CreatesOutputDir verifies that saveMP4 creates the output directory
// when it does not already exist.
func TestSaveMP4_CreatesOutputDir(t *testing.T) {
t.Parallel()
base := t.TempDir()
outDir := filepath.Join(base, "nested", "output")
fakeVideo := []byte("video-data")
_, err := saveMP4(fakeVideo, outDir, "word_gallery_1.png", 1)
if err != nil {
t.Fatalf("saveMP4 failed: %v", err)
}
if _, statErr := os.Stat(outDir); os.IsNotExist(statErr) {
t.Error("expected output directory to be created")
}
}
// TestSaveMP4_FallbackName verifies that saveMP4 uses a fallback name when the
// source path lacks a recognisable gallery file name (no .png suffix).
func TestSaveMP4_FallbackName(t *testing.T) {
t.Parallel()
outDir := t.TempDir()
fakeVideo := []byte("video-data")
// srcPath with no .png extension triggers the fallback naming path.
got, err := saveMP4(fakeVideo, outDir, "unusual_source", 5)
if err != nil {
t.Fatalf("saveMP4 failed: %v", err)
}
if !strings.HasSuffix(got, ".mp4") {
t.Errorf("expected .mp4 suffix even for fallback name, got %q", got)
}
}
// ---------------------------------------------------------------------------
// pageNumFromPath
// ---------------------------------------------------------------------------
// TestPageNumFromPath verifies that pageNumFromPath correctly extracts the
// gallery page number from various file name patterns.
func TestPageNumFromPath(t *testing.T) {
t.Parallel()
cases := []struct {
path string
want int
}{
{"/stories/ябълка/ябълка_gallery_1.png", 1},
{"/stories/word/word_gallery_10.png", 10},
// Non-gallery path — should return 0.
{"/stories/word/word_cover.png", 0},
// Missing trailing number — should return 0.
{"/stories/word/word_gallery_.png", 0},
// Page number zero — should return 0 (non-positive).
{"/stories/word/word_gallery_0.png", 0},
// Nested gallery name with multiple "_gallery_" tokens — last one wins.
{"/comics/slug/slug_gallery_3.png", 3},
}
for _, tc := range cases {
got := pageNumFromPath(tc.path)
if got != tc.want {
t.Errorf("pageNumFromPath(%q) = %d, want %d", tc.path, got, tc.want)
}
}
}
// ---------------------------------------------------------------------------
// loadGalleryImage — additional edge cases
// ---------------------------------------------------------------------------
// TestLoadGalleryImage_MultipleMatchesUsesFirst verifies that when several
// gallery files share the same page number, loadGalleryImage returns the
// lexicographically first match without error.
func TestLoadGalleryImage_MultipleMatchesUsesFirst(t *testing.T) {
t.Parallel()
dir := t.TempDir()
// Two files for page 1 — alphabetical order determines which is returned.
files := []string{"aaa_gallery_1.png", "zzz_gallery_1.png"}
for _, name := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0o644); err != nil {
t.Fatalf("setup: %v", err)
}
}
gotPath, gotBytes, err := loadGalleryImage(dir, 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// filepath.Glob returns results in sorted order, so aaa_gallery_1.png comes first.
expectedName := "aaa_gallery_1.png"
if filepath.Base(gotPath) != expectedName {
t.Errorf("expected first match %q, got %q", expectedName, filepath.Base(gotPath))
}
if string(gotBytes) != expectedName {
t.Errorf("bytes mismatch: got %q, want %q", gotBytes, expectedName)
}
}
// ---------------------------------------------------------------------------
// VeoGenerator — constructor with valid mock client
// ---------------------------------------------------------------------------
// TestNewVeoGenerator_WithMockClient verifies that NewVeoGenerator succeeds
// when the genai client factory does not return an error.
func TestNewVeoGenerator_WithMockClient(t *testing.T) {
// Do not use t.Parallel: this test replaces the package-global newGenaiClient hook.
orig := newGenaiClient
newGenaiClient = func(_ context.Context, _ *genai.ClientConfig) (*genai.Client, error) {
// Return a zero-value client pointer — sufficient for construction.
return &genai.Client{}, nil
}
t.Cleanup(func() { newGenaiClient = orig })
gen, err := NewVeoGenerator("valid-api-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gen == nil {
t.Fatal("expected non-nil VeoGenerator")
}
if gen.model != DefaultVeoModel {
t.Errorf("model: got %q, want %q", gen.model, DefaultVeoModel)
}
}
|