package http import ( "bytes" "database/sql" "encoding/json" "io" "log/slog" "mime/multipart" "net/http" "net/http/httptest" "os" "path" "path/filepath" "runtime" "testing" "time" ffmpegconv "git.vakhrushev.me/av/transcriber/internal/adapter/converter/ffmpeg" ffmpegmv "git.vakhrushev.me/av/transcriber/internal/adapter/metaviewer/ffmpeg" "git.vakhrushev.me/av/transcriber/internal/adapter/recognizer" "git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite" "git.vakhrushev.me/av/transcriber/internal/entity" "git.vakhrushev.me/av/transcriber/internal/service" "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/sqlite3" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupTestDB(t *testing.T) (*sql.DB, *goqu.Database) { // Создаем временную базу данных в памяти db, err := sql.Open("sqlite3", ":memory:") require.NoError(t, err) gq := goqu.New("sqlite3", db) err = goose.SetDialect("sqlite3") require.NoError(t, err) _, b, _, _ := runtime.Caller(0) migpath, err := filepath.Abs(path.Join(b, "../../../../migrations")) require.NoError(t, err) err = goose.Up(db, migpath) require.NoError(t, err) return db, gq } func setupTestRouter(t *testing.T) (*gin.Engine, *TranscribeHandler) { gin.SetMode(gin.TestMode) db, gq := setupTestDB(t) fileRepo := sqlite.NewFileRepository(db, gq) jobRepo := sqlite.NewTranscriptJobRepository(db, gq) metaviewer := ffmpegmv.NewFfmpegMetaViewer() converter := ffmpegconv.NewFfmpegConverter() recognizer := &recognizer.MemoryAudioRecognizer{} // Создаем тестовый логгер logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelError, // Только ошибки в тестах })) trsService := service.NewTranscribeService(jobRepo, fileRepo, metaviewer, converter, recognizer, "data/files", logger) handler := NewTranscribeHandler(jobRepo, trsService) router := gin.New() router.MaxMultipartMemory = 32 << 20 // 32 MiB api := router.Group("/api") { api.POST("/audio", handler.CreateTranscribeJob) api.GET("/status/:id", handler.GetTranscribeJobStatus) } return router, handler } func createMultipartRequest(t *testing.T, audioFilePath string) (*http.Request, string) { // Открываем тестовый аудио файл file, err := os.Open(audioFilePath) require.NoError(t, err) defer file.Close() // Создаем буфер для multipart формы var buf bytes.Buffer writer := multipart.NewWriter(&buf) // Создаем поле для файла part, err := writer.CreateFormFile("audio", filepath.Base(audioFilePath)) require.NoError(t, err) // Копируем содержимое файла _, err = io.Copy(part, file) require.NoError(t, err) // Закрываем writer err = writer.Close() require.NoError(t, err) // Создаем HTTP запрос req, err := http.NewRequest("POST", "/api/audio", &buf) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) return req, writer.FormDataContentType() } func TestCreateTranscribeJob_Success(t *testing.T) { // Создаем временную директорию для файлов tempDir := t.TempDir() // Создаем структуру директорий для тестов testDataDir := filepath.Join(tempDir, "data", "files") err := os.MkdirAll(testDataDir, 0755) require.NoError(t, err) // Временно меняем рабочую директорию для сохранения файлов originalWd, err := os.Getwd() require.NoError(t, err) defer os.Chdir(originalWd) err = os.Chdir(tempDir) require.NoError(t, err) router, _ := setupTestRouter(t) // Копируем тестовый файл во временную директорию srcFile := filepath.Join(originalWd, "testdata", "sample.m4a") dstFile := "sample.m4a" src, err := os.Open(srcFile) require.NoError(t, err) defer src.Close() dst, err := os.Create(dstFile) require.NoError(t, err) defer dst.Close() _, err = io.Copy(dst, src) require.NoError(t, err) defer os.Remove(dstFile) // Создаем запрос с тестовым аудио файлом req, _ := createMultipartRequest(t, dstFile) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат assert.Equal(t, http.StatusCreated, w.Code) var response CreateTranscribeJobResponse err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) // Проверяем, что возвращается корректный ответ assert.NotEmpty(t, response.JobID) assert.Equal(t, entity.StateCreated, response.State) // Проверяем, что файл был сохранен files, err := filepath.Glob(filepath.Join("data", "files", "*")) require.NoError(t, err) assert.Len(t, files, 1) // Проверяем размер сохраненного файла fileInfo, err := os.Stat(files[0]) require.NoError(t, err) assert.Greater(t, fileInfo.Size(), int64(0)) } func TestCreateTranscribeJob_NoFile(t *testing.T) { router, _ := setupTestRouter(t) // Создаем запрос без файла req, err := http.NewRequest("POST", "/api/audio", nil) require.NoError(t, err) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]string err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "No audio file provided", response["error"]) } func TestCreateTranscribeJob_EmptyFile(t *testing.T) { // Создаем временную директорию для файлов tempDir := t.TempDir() // Создаем структуру директорий для тестов testDataDir := filepath.Join(tempDir, "data", "files") err := os.MkdirAll(testDataDir, 0755) require.NoError(t, err) // Временно меняем рабочую директорию для сохранения файлов originalWd, err := os.Getwd() require.NoError(t, err) defer os.Chdir(originalWd) err = os.Chdir(tempDir) require.NoError(t, err) router, _ := setupTestRouter(t) // Создаем пустой временный файл в текущей директории теста emptyFile := "empty.m4a" f, err := os.Create(emptyFile) require.NoError(t, err) f.Close() defer os.Remove(emptyFile) // Создаем запрос с пустым файлом req, _ := createMultipartRequest(t, emptyFile) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат - даже пустой файл должен быть принят assert.Equal(t, http.StatusCreated, w.Code) var response CreateTranscribeJobResponse err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.NotEmpty(t, response.JobID) assert.Equal(t, entity.StateCreated, response.State) } func TestCreateTranscribeJob_DifferentFileExtensions(t *testing.T) { testCases := []struct { name string filename string expectExt string }{ { name: "m4a file", filename: "test.m4a", expectExt: ".m4a", }, { name: "mp3 file", filename: "test.mp3", expectExt: ".mp3", }, { name: "wav file", filename: "test.wav", expectExt: ".wav", }, { name: "file without extension", filename: "test", expectExt: ".audio", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Создаем временную директорию для файлов tempDir := t.TempDir() // Создаем структуру директорий для тестов testDataDir := filepath.Join(tempDir, "data", "files") err := os.MkdirAll(testDataDir, 0755) require.NoError(t, err) // Временно меняем рабочую директорию для сохранения файлов originalWd, err := os.Getwd() require.NoError(t, err) defer os.Chdir(originalWd) err = os.Chdir(tempDir) require.NoError(t, err) router, _ := setupTestRouter(t) // Создаем временный файл с нужным именем в текущей директории теста testFile := tc.filename f, err := os.Create(testFile) require.NoError(t, err) f.WriteString("test audio content") f.Close() defer os.Remove(testFile) // Создаем запрос req, _ := createMultipartRequest(t, testFile) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат assert.Equal(t, http.StatusCreated, w.Code) // Проверяем, что файл сохранен с правильным расширением files, err := filepath.Glob(filepath.Join("data", "files", "*"+tc.expectExt)) require.NoError(t, err) assert.Len(t, files, 1) }) } } func TestGetTranscribeJobStatus_Success(t *testing.T) { router, handler := setupTestRouter(t) // Создаем тестовую запись в базе данных job := &entity.TranscribeJob{ Id: "test-job-id", State: entity.StateCreated, FileID: nil, IsError: false, CreatedAt: time.Now(), } err := handler.jobRepo.Create(job) require.NoError(t, err) // Создаем запрос req, err := http.NewRequest("GET", "/api/status/test-job-id", nil) require.NoError(t, err) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат assert.Equal(t, http.StatusOK, w.Code) var response GetTranscribeJobResponse err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "test-job-id", response.JobID) assert.Equal(t, entity.StateCreated, response.State) assert.NotZero(t, response.CreatedAt) } func TestGetTranscribeJobStatus_NotFound(t *testing.T) { router, _ := setupTestRouter(t) // Создаем запрос с несуществующим ID req, err := http.NewRequest("GET", "/api/status/non-existent-id", nil) require.NoError(t, err) // Выполняем запрос w := httptest.NewRecorder() router.ServeHTTP(w, req) // Проверяем результат assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]string err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "Job not found", response["error"]) }