mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-06-29 15:26:02 +00:00
239 lines
7.6 KiB
Go
239 lines
7.6 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"scriberr/internal/database"
|
|
"scriberr/internal/models"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func idempotentJSONRequest(t *testing.T, s *authTestServer, method, path string, body any, token, key string) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
|
|
var payload bytes.Buffer
|
|
require.NoError(t, json.NewEncoder(&payload).Encode(body))
|
|
req, err := http.NewRequest(method, path, &payload)
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Idempotency-Key", key)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
s.router.ServeHTTP(recorder, req)
|
|
|
|
var response map[string]any
|
|
if recorder.Code != http.StatusNoContent {
|
|
require.NoError(t, json.NewDecoder(recorder.Body).Decode(&response))
|
|
}
|
|
return recorder, response
|
|
}
|
|
|
|
func idempotentUploadRequest(t *testing.T, s *authTestServer, token, key string, body []byte, contentType string) (*httptest.ResponseRecorder, map[string]any) {
|
|
t.Helper()
|
|
|
|
req, err := http.NewRequest(http.MethodPost, "/api/v1/files", bytes.NewReader(body))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Idempotency-Key", key)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
s.router.ServeHTTP(recorder, req)
|
|
|
|
var response map[string]any
|
|
require.NoError(t, json.NewDecoder(recorder.Body).Decode(&response))
|
|
return recorder, response
|
|
}
|
|
|
|
func fixedMultipartUpload(t *testing.T, filename string, content []byte, title string) ([]byte, string) {
|
|
t.Helper()
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
require.NoError(t, writer.SetBoundary("scriberr-test-boundary"))
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
require.NoError(t, err)
|
|
_, err = part.Write(content)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer.WriteField("title", title))
|
|
require.NoError(t, writer.Close())
|
|
return body.Bytes(), writer.FormDataContentType()
|
|
}
|
|
|
|
func TestIdempotencyCachesJSONCreateAndRejectsBodyMismatch(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
|
|
resp, body := idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/api-keys", map[string]any{
|
|
"name": "CLI",
|
|
"description": "first",
|
|
}, token, "idem-api-key")
|
|
require.Equal(t, http.StatusCreated, resp.Code)
|
|
firstRawKey := body["key"].(string)
|
|
firstID := body["id"].(string)
|
|
|
|
resp, body = idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/api-keys", map[string]any{
|
|
"name": "CLI",
|
|
"description": "first",
|
|
}, token, "idem-api-key")
|
|
require.Equal(t, http.StatusCreated, resp.Code)
|
|
require.Equal(t, firstRawKey, body["key"])
|
|
require.Equal(t, firstID, body["id"])
|
|
|
|
var count int64
|
|
require.NoError(t, database.DB.Model(&models.APIKey{}).Count(&count).Error)
|
|
require.Equal(t, int64(1), count)
|
|
|
|
resp, body = idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/api-keys", map[string]any{
|
|
"name": "CLI",
|
|
"description": "changed",
|
|
}, token, "idem-api-key")
|
|
require.Equal(t, http.StatusConflict, resp.Code)
|
|
errBody := body["error"].(map[string]any)
|
|
require.Equal(t, "IDEMPOTENCY_CONFLICT", errBody["code"])
|
|
}
|
|
|
|
func TestIdempotencyCoalescesConcurrentCreates(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
|
|
const workers = 8
|
|
start := make(chan struct{})
|
|
type result struct {
|
|
status int
|
|
body map[string]any
|
|
err error
|
|
}
|
|
responses := make(chan result, workers)
|
|
var wg sync.WaitGroup
|
|
for range workers {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
payload, err := json.Marshal(map[string]any{
|
|
"name": "Concurrent CLI",
|
|
"description": "same payload",
|
|
})
|
|
if err != nil {
|
|
responses <- result{err: err}
|
|
return
|
|
}
|
|
req, err := http.NewRequest(http.MethodPost, "/api/v1/api-keys", bytes.NewReader(payload))
|
|
if err != nil {
|
|
responses <- result{err: err}
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Idempotency-Key", "idem-concurrent-api-key")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
s.router.ServeHTTP(recorder, req)
|
|
|
|
var body map[string]any
|
|
if err := json.NewDecoder(recorder.Body).Decode(&body); err != nil {
|
|
responses <- result{status: recorder.Code, err: err}
|
|
return
|
|
}
|
|
responses <- result{status: recorder.Code, body: body}
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
close(responses)
|
|
|
|
var firstID string
|
|
for response := range responses {
|
|
require.NoError(t, response.err)
|
|
require.Equal(t, http.StatusCreated, response.status)
|
|
id := response.body["id"].(string)
|
|
if firstID == "" {
|
|
firstID = id
|
|
}
|
|
require.Equal(t, firstID, id)
|
|
require.NotEmpty(t, response.body["key"])
|
|
}
|
|
|
|
var count int64
|
|
require.NoError(t, database.DB.Model(&models.APIKey{}).Count(&count).Error)
|
|
require.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func TestIdempotencyCachesMultipartUpload(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
body, contentType := fixedMultipartUpload(t, "meeting.wav", []byte("RIFF----WAVEfmt data"), "Meeting")
|
|
|
|
resp, first := idempotentUploadRequest(t, s, token, "idem-upload", body, contentType)
|
|
require.Equal(t, http.StatusCreated, resp.Code)
|
|
|
|
resp, second := idempotentUploadRequest(t, s, token, "idem-upload", body, contentType)
|
|
require.Equal(t, http.StatusCreated, resp.Code)
|
|
require.Equal(t, first["id"], second["id"])
|
|
|
|
var count int64
|
|
require.NoError(t, database.DB.Model(&models.TranscriptionJob{}).Where("source_file_hash IS NULL").Count(&count).Error)
|
|
require.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func TestIdempotencyDoesNotBufferMultipartUpload(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
body, contentType := fixedMultipartUpload(t, "meeting.wav", []byte("RIFF----WAVEfmt data"), "Meeting")
|
|
|
|
req, err := http.NewRequest(http.MethodPost, "/api/v1/files", bytes.NewReader(body))
|
|
require.NoError(t, err)
|
|
req.ContentLength = maxIdempotencyBodyBytes + 1
|
|
req.Header.Set("Content-Type", contentType)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Idempotency-Key", "idem-large-upload")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
s.router.ServeHTTP(recorder, req)
|
|
|
|
require.Equal(t, http.StatusCreated, recorder.Code)
|
|
var response map[string]any
|
|
require.NoError(t, json.NewDecoder(recorder.Body).Decode(&response))
|
|
require.NotEmpty(t, response["id"])
|
|
}
|
|
|
|
func TestIdempotencyCachesTranscriptionCreate(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
fileID, _ := createUploadedFileForTranscription(t, s, token)
|
|
|
|
payload := map[string]any{
|
|
"file_id": fileID,
|
|
"title": "Queued once",
|
|
}
|
|
resp, first := idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/transcriptions", payload, token, "idem-transcription")
|
|
require.Equal(t, http.StatusAccepted, resp.Code)
|
|
|
|
resp, second := idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/transcriptions", payload, token, "idem-transcription")
|
|
require.Equal(t, http.StatusAccepted, resp.Code)
|
|
require.Equal(t, first["id"], second["id"])
|
|
|
|
var count int64
|
|
require.NoError(t, database.DB.Model(&models.TranscriptionJob{}).Where("source_file_hash IS NOT NULL").Count(&count).Error)
|
|
require.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func TestIdempotencyValidation(t *testing.T) {
|
|
s := newAuthTestServer(t)
|
|
token := registerForFileTests(t, s)
|
|
|
|
resp, body := idempotentJSONRequest(t, s, http.MethodPost, "/api/v1/api-keys", map[string]any{"name": "CLI"}, token, "bad key with spaces")
|
|
require.Equal(t, http.StatusUnprocessableEntity, resp.Code)
|
|
errBody := body["error"].(map[string]any)
|
|
require.Equal(t, "Idempotency-Key", errBody["field"])
|
|
}
|