331 lines
9.8 KiB
Go
331 lines
9.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
|
"github.com/Silo-Server/silo-server/internal/auth"
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
"github.com/Silo-Server/silo-server/internal/scanner"
|
|
"github.com/Silo-Server/silo-server/internal/subtitles"
|
|
)
|
|
|
|
type stubSubtitleMediaResolver struct {
|
|
meta *MediaFileMetadata
|
|
}
|
|
|
|
func (s stubSubtitleMediaResolver) GetMediaFileWithMetadata(context.Context, int) (*MediaFileMetadata, error) {
|
|
return s.meta, nil
|
|
}
|
|
|
|
type stubMediaFileResolver struct {
|
|
file *models.MediaFile
|
|
err error
|
|
}
|
|
|
|
func (s stubMediaFileResolver) GetByID(context.Context, int) (*models.MediaFile, error) {
|
|
return s.file, s.err
|
|
}
|
|
|
|
type stubItemAccessChecker struct {
|
|
err error
|
|
}
|
|
|
|
func (s stubItemAccessChecker) EnsureAccessible(context.Context, string, catalog.AccessFilter) error {
|
|
return s.err
|
|
}
|
|
|
|
type stubEpisodeLookup struct {
|
|
episode *models.Episode
|
|
}
|
|
|
|
func (s stubEpisodeLookup) GetByID(context.Context, string) (*models.Episode, error) {
|
|
return s.episode, nil
|
|
}
|
|
|
|
func newSubtitleAuthRequest(method, path string, body io.Reader) *http.Request {
|
|
req := httptest.NewRequest(method, path, body)
|
|
ctx := apimw.SetClaims(context.Background(), &auth.Claims{
|
|
UserID: 1,
|
|
Role: "user",
|
|
TokenType: auth.TokenTypeAccess,
|
|
})
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
func newSubtitleUploadRequest(t *testing.T, mediaFileID int, language, filename string, content []byte) *http.Request {
|
|
t.Helper()
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
if err := writer.WriteField("media_file_id", strconv.Itoa(mediaFileID)); err != nil {
|
|
t.Fatalf("write media_file_id: %v", err)
|
|
}
|
|
if err := writer.WriteField("language", language); err != nil {
|
|
t.Fatalf("write language: %v", err)
|
|
}
|
|
part, err := writer.CreateFormFile("file", filename)
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(content); err != nil {
|
|
t.Fatalf("write file content: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
req := newSubtitleAuthRequest(http.MethodPost, "/subtitles/upload", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
return req
|
|
}
|
|
|
|
func TestHandleUploadSuccess(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
handler.FileAuthorizer = &MediaFileAuthorizer{
|
|
FileResolver: stubMediaFileResolver{
|
|
file: &models.MediaFile{ID: 42, ContentID: "movie-1"},
|
|
},
|
|
ItemAccess: stubItemAccessChecker{},
|
|
}
|
|
|
|
req := newSubtitleUploadRequest(t, 42, "en", "custom.srt", []byte("1\n00:00:01,000 --> 00:00:02,000\nHi\n"))
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleUpload(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body = %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Subtitle subtitles.DownloadedSubtitle `json:"subtitle"`
|
|
}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if resp.Subtitle.Provider != subtitles.ProviderUpload {
|
|
t.Fatalf("provider = %q, want upload", resp.Subtitle.Provider)
|
|
}
|
|
}
|
|
|
|
func TestHandleUploadUnauthorizedMediaFile(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
handler.FileAuthorizer = &MediaFileAuthorizer{
|
|
FileResolver: stubMediaFileResolver{err: scanner.ErrFileNotFound},
|
|
ItemAccess: stubItemAccessChecker{},
|
|
}
|
|
|
|
req := newSubtitleUploadRequest(t, 99, "en", "custom.srt", []byte("hello"))
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleUpload(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleUploadRejectsBadExtension(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
handler.FileAuthorizer = &MediaFileAuthorizer{
|
|
FileResolver: stubMediaFileResolver{
|
|
file: &models.MediaFile{ID: 42, ContentID: "movie-1"},
|
|
},
|
|
ItemAccess: stubItemAccessChecker{},
|
|
}
|
|
|
|
req := newSubtitleUploadRequest(t, 42, "en", "notes.txt", []byte("hello"))
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleUpload(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleUploadRejectsOversizedBody(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
handler.FileAuthorizer = &MediaFileAuthorizer{
|
|
FileResolver: stubMediaFileResolver{
|
|
file: &models.MediaFile{ID: 42, ContentID: "movie-1"},
|
|
},
|
|
ItemAccess: stubItemAccessChecker{},
|
|
}
|
|
|
|
req := newSubtitleUploadRequest(
|
|
t,
|
|
42,
|
|
"en",
|
|
"huge.srt",
|
|
make([]byte, subtitleUploadMaxBodySize+1),
|
|
)
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleUpload(rr, req)
|
|
|
|
if rr.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want 413", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleDetectLanguageRejectsOversizedBody(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", "huge.srt")
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(make([]byte, subtitleUploadMaxBodySize+1)); err != nil {
|
|
t.Fatalf("write file content: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
req := newSubtitleAuthRequest(http.MethodPost, "/subtitles/detect-language", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleDetectLanguage(rr, req)
|
|
|
|
if rr.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("status = %d, want 413", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleDeleteRequiresAccessToMediaFile(t *testing.T) {
|
|
repo := newMockSubtitleRepoForHandler()
|
|
repo.subtitles[1] = &subtitles.DownloadedSubtitle{
|
|
ID: 1,
|
|
MediaFileID: 42,
|
|
Provider: subtitles.ProviderUpload,
|
|
}
|
|
manager := subtitles.NewManager(repo, newMockS3ClientForHandler(), "test-bucket")
|
|
handler := NewSubtitleSearchHandler(manager, repo, stubSubtitleMediaResolver{})
|
|
handler.FileAuthorizer = &MediaFileAuthorizer{
|
|
FileResolver: stubMediaFileResolver{err: scanner.ErrFileNotFound},
|
|
ItemAccess: stubItemAccessChecker{},
|
|
}
|
|
|
|
req := newSubtitleAuthRequest(http.MethodDelete, "/subtitles/1", nil)
|
|
req = withProfileRouteParam(req, "id", "1")
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleDelete(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", rr.Code)
|
|
}
|
|
}
|
|
|
|
type handlerMockSubtitleRepo struct {
|
|
subtitles map[int]*subtitles.DownloadedSubtitle
|
|
nextID int
|
|
byKey map[string]*subtitles.DownloadedSubtitle
|
|
}
|
|
|
|
func newMockSubtitleRepoForHandler() *handlerMockSubtitleRepo {
|
|
return &handlerMockSubtitleRepo{
|
|
subtitles: make(map[int]*subtitles.DownloadedSubtitle),
|
|
byKey: make(map[string]*subtitles.DownloadedSubtitle),
|
|
}
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) InsertDownloadedSubtitle(_ context.Context, sub *subtitles.DownloadedSubtitle) error {
|
|
m.nextID++
|
|
sub.ID = m.nextID
|
|
m.subtitles[sub.ID] = sub
|
|
m.byKey[sub.S3Key] = sub
|
|
return nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) GetDownloadedSubtitle(_ context.Context, id int) (*subtitles.DownloadedSubtitle, error) {
|
|
if sub, ok := m.subtitles[id]; ok {
|
|
copy := *sub
|
|
return ©, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) ListDownloadedSubtitles(context.Context, int) ([]subtitles.DownloadedSubtitle, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) DeleteDownloadedSubtitle(_ context.Context, id int) (*subtitles.DownloadedSubtitle, error) {
|
|
sub := m.subtitles[id]
|
|
delete(m.subtitles, id)
|
|
return sub, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) GetDownloadedSubtitleByS3Key(_ context.Context, s3Key string) (*subtitles.DownloadedSubtitle, error) {
|
|
if sub, ok := m.byKey[s3Key]; ok {
|
|
copy := *sub
|
|
return ©, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) UpdateDownloadedSubtitle(_ context.Context, id int, update subtitles.SubtitleMetadataUpdate) (*subtitles.DownloadedSubtitle, error) {
|
|
sub, ok := m.subtitles[id]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
sub.Language = update.Language
|
|
sub.ReleaseName = update.ReleaseName
|
|
sub.HearingImpaired = update.HearingImpaired
|
|
if sub.S3Key != update.S3Key {
|
|
delete(m.byKey, sub.S3Key)
|
|
sub.S3Key = update.S3Key
|
|
m.byKey[sub.S3Key] = sub
|
|
}
|
|
copy := *sub
|
|
return ©, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) ListProviderConfigs(context.Context) ([]subtitles.ProviderConfig, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) GetProviderConfig(context.Context, string) (*subtitles.ProviderConfig, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *handlerMockSubtitleRepo) UpsertProviderConfig(context.Context, *subtitles.ProviderConfig) error {
|
|
return nil
|
|
}
|
|
|
|
type handlerMockS3Client struct{}
|
|
|
|
func newMockS3ClientForHandler() *handlerMockS3Client {
|
|
return &handlerMockS3Client{}
|
|
}
|
|
|
|
func (handlerMockS3Client) PutObject(context.Context, string, string, []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (handlerMockS3Client) GetObject(context.Context, string, string) ([]byte, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (handlerMockS3Client) DeleteObject(context.Context, string, string) error {
|
|
return nil
|
|
}
|