//go:build integration // +build integration package sqlite_test import ( "context" "fmt" "math" "path/filepath" "reflect" "regexp" "strconv" "testing" "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stretchr/testify/assert" ) func loadSceneRelationships(ctx context.Context, expected models.Scene, actual *models.Scene) error { if expected.URLs.Loaded() { if err := actual.LoadURLs(ctx, db.Scene); err != nil { return err } } if expected.GalleryIDs.Loaded() { if err := actual.LoadGalleryIDs(ctx, db.Scene); err != nil { return err } } if expected.TagIDs.Loaded() { if err := actual.LoadTagIDs(ctx, db.Scene); err != nil { return err } } if expected.PerformerIDs.Loaded() { if err := actual.LoadPerformerIDs(ctx, db.Scene); err != nil { return err } } if expected.Groups.Loaded() { if err := actual.LoadGroups(ctx, db.Scene); err != nil { return err } } if expected.StashIDs.Loaded() { if err := actual.LoadStashIDs(ctx, db.Scene); err != nil { return err } } if expected.Files.Loaded() { if err := actual.LoadFiles(ctx, db.Scene); err != nil { return err } } // clear Path, Checksum, PrimaryFileID if expected.Path == "" { actual.Path = "" } if expected.Checksum == "" { actual.Checksum = "" } if expected.OSHash == "" { actual.OSHash = "" } if expected.PrimaryFileID == nil { actual.PrimaryFileID = nil } return nil } func Test_sceneQueryBuilder_Create(t *testing.T) { var ( title = "title" code = "1337" details = "details" director = "director" url = "url" rating = 60 resumeTime = 10.0 playDuration = 34.0 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) sceneIndex = 123 sceneIndex2 = 234 endpoint1 = "endpoint1" endpoint2 = "endpoint2" stashID1 = "stashid1" stashID2 = "stashid2" date, _ = models.ParseDate("2003-02-01") videoFile = makeFileWithID(fileIdxStartVideoFiles) ) tests := []struct { name string newObject models.Scene wantErr bool }{ { "full", models.Scene{ Title: title, Code: code, Details: details, Director: director, URLs: models.NewRelatedStrings([]string{url}), Date: &date, Rating: &rating, Organized: true, StudioID: &studioIDs[studioIdxWithScene], CreatedAt: createdAt, UpdatedAt: updatedAt, GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, }), ResumeTime: float64(resumeTime), PlayDuration: playDuration, }, false, }, { "with file", models.Scene{ Title: title, Code: code, Details: details, Director: director, URLs: models.NewRelatedStrings([]string{url}), Date: &date, Rating: &rating, Organized: true, StudioID: &studioIDs[studioIdxWithScene], Files: models.NewRelatedVideoFiles([]*models.VideoFile{ videoFile.(*models.VideoFile), }), CreatedAt: createdAt, UpdatedAt: updatedAt, GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, }), ResumeTime: resumeTime, PlayDuration: playDuration, }, false, }, { "invalid studio id", models.Scene{ StudioID: &invalidID, }, true, }, { "invalid gallery id", models.Scene{ GalleryIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid tag id", models.Scene{ TagIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid performer id", models.Scene{ PerformerIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid group id", models.Scene{ Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: invalidID, SceneIndex: &sceneIndex, }, }), }, true, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) var fileIDs []models.FileID if tt.newObject.Files.Loaded() { for _, f := range tt.newObject.Files.List() { fileIDs = append(fileIDs, f.ID) } } s := tt.newObject if err := qb.Create(ctx, &s, fileIDs); (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.Create() error = %v, wantErr = %v", err, tt.wantErr) } if tt.wantErr { assert.Zero(s.ID) return } assert.NotZero(s.ID) copy := tt.newObject copy.ID = s.ID // load relationships if err := loadSceneRelationships(ctx, copy, &s); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(copy, s) // ensure can find the scene found, err := qb.Find(ctx, s.ID) if err != nil { t.Errorf("sceneQueryBuilder.Find() error = %v", err) } if !assert.NotNil(found) { return } // load relationships if err := loadSceneRelationships(ctx, copy, found); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(copy, *found) return }) } } func clearSceneFileIDs(scene *models.Scene) { if scene.Files.Loaded() { for _, f := range scene.Files.List() { f.Base().ID = 0 } } } func makeSceneFileWithID(i int) *models.VideoFile { ret := makeSceneFile(i) ret.ID = sceneFileIDs[i] return ret } func Test_sceneQueryBuilder_Update(t *testing.T) { var ( title = "title" code = "1337" details = "details" director = "director" url = "url" rating = 60 resumeTime = 10.0 playDuration = 34.0 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) sceneIndex = 123 sceneIndex2 = 234 endpoint1 = "endpoint1" endpoint2 = "endpoint2" stashID1 = "stashid1" stashID2 = "stashid2" date, _ = models.ParseDate("2003-02-01") ) tests := []struct { name string updatedObject *models.Scene wantErr bool }{ { "full", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], Title: title, Code: code, Details: details, Director: director, URLs: models.NewRelatedStrings([]string{url}), Date: &date, Rating: &rating, Organized: true, StudioID: &studioIDs[studioIdxWithScene], CreatedAt: createdAt, UpdatedAt: updatedAt, GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, }), ResumeTime: resumeTime, PlayDuration: playDuration, }, false, }, { "clear nullables", &models.Scene{ ID: sceneIDs[sceneIdxWithSpacedName], GalleryIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}), Groups: models.NewRelatedGroups([]models.GroupsScenes{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, false, }, { "clear gallery ids", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], GalleryIDs: models.NewRelatedIDs([]int{}), }, false, }, { "clear tag ids", &models.Scene{ ID: sceneIDs[sceneIdxWithTag], TagIDs: models.NewRelatedIDs([]int{}), }, false, }, { "clear performer ids", &models.Scene{ ID: sceneIDs[sceneIdxWithPerformer], PerformerIDs: models.NewRelatedIDs([]int{}), }, false, }, { "clear groups", &models.Scene{ ID: sceneIDs[sceneIdxWithGroup], Groups: models.NewRelatedGroups([]models.GroupsScenes{}), }, false, }, { "invalid studio id", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], StudioID: &invalidID, }, true, }, { "invalid gallery id", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], GalleryIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid tag id", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], TagIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid performer id", &models.Scene{ ID: sceneIDs[sceneIdxWithGallery], PerformerIDs: models.NewRelatedIDs([]int{invalidID}), }, true, }, { "invalid group id", &models.Scene{ ID: sceneIDs[sceneIdxWithSpacedName], Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: invalidID, SceneIndex: &sceneIndex, }, }), }, true, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) copy := *tt.updatedObject if err := qb.Update(ctx, tt.updatedObject); (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.Update() error = %v, wantErr %v", err, tt.wantErr) } if tt.wantErr { return } s, err := qb.Find(ctx, tt.updatedObject.ID) if err != nil { t.Errorf("sceneQueryBuilder.Find() error = %v", err) } // load relationships if err := loadSceneRelationships(ctx, copy, s); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(copy, *s) }) } } func clearScenePartial() models.ScenePartial { // leave mandatory fields return models.ScenePartial{ Title: models.OptionalString{Set: true, Null: true}, Code: models.OptionalString{Set: true, Null: true}, Details: models.OptionalString{Set: true, Null: true}, Director: models.OptionalString{Set: true, Null: true}, URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, Date: models.OptionalDate{Set: true, Null: true}, Rating: models.OptionalInt{Set: true, Null: true}, StudioID: models.OptionalInt{Set: true, Null: true}, GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, PerformerIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, } } func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) { var ( title = "title" code = "1337" details = "details" director = "director" url = "url" rating = 60 resumeTime = 10.0 playDuration = 34.0 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) sceneIndex = 123 sceneIndex2 = 234 endpoint1 = "endpoint1" endpoint2 = "endpoint2" stashID1 = "stashid1" stashID2 = "stashid2" date, _ = models.ParseDate("2003-02-01") ) tests := []struct { name string id int partial models.ScenePartial want models.Scene wantErr bool }{ { "full", sceneIDs[sceneIdxWithSpacedName], models.ScenePartial{ Title: models.NewOptionalString(title), Code: models.NewOptionalString(code), Details: models.NewOptionalString(details), Director: models.NewOptionalString(director), URLs: &models.UpdateStrings{ Values: []string{url}, Mode: models.RelationshipUpdateModeSet, }, Date: models.NewOptionalDate(date), Rating: models.NewOptionalInt(rating), Organized: models.NewOptionalBool(true), StudioID: models.NewOptionalInt(studioIDs[studioIdxWithScene]), CreatedAt: models.NewOptionalTime(createdAt), UpdatedAt: models.NewOptionalTime(updatedAt), GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdxWithScene]}, Mode: models.RelationshipUpdateModeSet, }, TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}, Mode: models.RelationshipUpdateModeSet, }, PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}, Mode: models.RelationshipUpdateModeSet, }, GroupIDs: &models.UpdateGroupIDs{ Groups: []models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }, Mode: models.RelationshipUpdateModeSet, }, StashIDs: &models.UpdateStashIDs{ StashIDs: []models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, }, Mode: models.RelationshipUpdateModeSet, }, ResumeTime: models.NewOptionalFloat64(resumeTime), PlayDuration: models.NewOptionalFloat64(playDuration), }, models.Scene{ ID: sceneIDs[sceneIdxWithSpacedName], Files: models.NewRelatedVideoFiles([]*models.VideoFile{ makeSceneFile(sceneIdxWithSpacedName), }), Title: title, Code: code, Details: details, Director: director, URLs: models.NewRelatedStrings([]string{url}), Date: &date, Rating: &rating, Organized: true, StudioID: &studioIDs[studioIdxWithScene], CreatedAt: createdAt, UpdatedAt: updatedAt, GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }), StashIDs: models.NewRelatedStashIDs([]models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, }), ResumeTime: resumeTime, PlayDuration: playDuration, }, false, }, { "clear all", sceneIDs[sceneIdxWithSpacedName], clearScenePartial(), models.Scene{ ID: sceneIDs[sceneIdxWithSpacedName], Files: models.NewRelatedVideoFiles([]*models.VideoFile{ makeSceneFile(sceneIdxWithSpacedName), }), GalleryIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}), Groups: models.NewRelatedGroups([]models.GroupsScenes{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}), PlayDuration: getScenePlayDuration(sceneIdxWithSpacedName), ResumeTime: getSceneResumeTime(sceneIdxWithSpacedName), }, false, }, { "invalid id", invalidID, models.ScenePartial{}, models.Scene{}, true, }, } for _, tt := range tests { qb := db.Scene runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.UpdatePartial(ctx, tt.id, tt.partial) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.UpdatePartial() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { return } // load relationships if err := loadSceneRelationships(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } // ignore file ids clearSceneFileIDs(got) assert.Equal(tt.want, *got) s, err := qb.Find(ctx, tt.id) if err != nil { t.Errorf("sceneQueryBuilder.Find() error = %v", err) } // load relationships if err := loadSceneRelationships(ctx, tt.want, s); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } // ignore file ids clearSceneFileIDs(s) assert.Equal(tt.want, *s) }) } } func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) { var ( sceneIndex = 123 sceneIndex2 = 234 endpoint1 = "endpoint1" endpoint2 = "endpoint2" stashID1 = "stashid1" stashID2 = "stashid2" groupScenes = []models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithDupName], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, } stashIDs = []models.StashID{ { StashID: stashID1, Endpoint: endpoint1, }, { StashID: stashID2, Endpoint: endpoint2, }, } ) tests := []struct { name string id int partial models.ScenePartial want models.Scene wantErr bool }{ { "add galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdx1WithImage], galleryIDs[galleryIdx1WithPerformer]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]), galleryIDs[galleryIdx1WithImage], galleryIDs[galleryIdx1WithPerformer], )), }, false, }, { "add identical galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdx1WithImage], galleryIDs[galleryIdx1WithImage]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]), galleryIDs[galleryIdx1WithImage], )), }, false, }, { "add tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithGallery]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ TagIDs: models.NewRelatedIDs(append( []int{ tagIDs[tagIdx1WithGallery], tagIDs[tagIdx1WithDupName], }, indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags])..., )), }, false, }, { "add identical tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithDupName]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ TagIDs: models.NewRelatedIDs(append( []int{ tagIDs[tagIdx1WithDupName], }, indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags])..., )), }, false, }, { "add performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithDupName], performerIDs[performerIdx1WithGallery]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]), performerIDs[performerIdx1WithDupName], performerIDs[performerIdx1WithGallery], )), }, false, }, { "add identical performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithDupName], performerIDs[performerIdx1WithDupName]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]), performerIDs[performerIdx1WithDupName], )), }, false, }, { "add groups", sceneIDs[sceneIdxWithGroup], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: groupScenes, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ Groups: models.NewRelatedGroups(append([]models.GroupsScenes{ { GroupID: indexesToIDs(groupIDs, sceneGroups[sceneIdxWithGroup])[0], }, }, groupScenes...)), }, false, }, { "add groups to empty", sceneIDs[sceneIdx1WithPerformer], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: groupScenes, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithDupName], SceneIndex: &sceneIndex, }, { GroupID: groupIDs[groupIdxWithStudio], SceneIndex: &sceneIndex2, }, }), }, false, }, { "add stash ids", sceneIDs[sceneIdxWithSpacedName], models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: stashIDs, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ StashIDs: models.NewRelatedStashIDs(append([]models.StashID{sceneStashID(sceneIdxWithSpacedName)}, stashIDs...)), }, false, }, { "add duplicate galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdxWithScene], galleryIDs[galleryIdx1WithPerformer]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]), galleryIDs[galleryIdx1WithPerformer], )), }, false, }, { "add duplicate tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithGallery]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ TagIDs: models.NewRelatedIDs(append( []int{tagIDs[tagIdx1WithGallery]}, indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags])..., )), }, false, }, { "add duplicate performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithGallery]}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]), performerIDs[performerIdx1WithGallery], )), }, false, }, { "add duplicate groups", sceneIDs[sceneIdxWithGroup], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: append([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], SceneIndex: &sceneIndex, }, }, groupScenes..., ), Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ Groups: models.NewRelatedGroups(append([]models.GroupsScenes{ { GroupID: indexesToIDs(groupIDs, sceneGroups[sceneIdxWithGroup])[0], }, }, groupScenes...)), }, false, }, { "add duplicate stash ids", sceneIDs[sceneIdxWithSpacedName], models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: []models.StashID{ sceneStashID(sceneIdxWithSpacedName), }, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{ StashIDs: models.NewRelatedStashIDs([]models.StashID{sceneStashID(sceneIdxWithSpacedName)}), }, false, }, { "add invalid galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{invalidID}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{}, true, }, { "add invalid tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{invalidID}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{}, true, }, { "add invalid performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{invalidID}, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{}, true, }, { "add invalid groups", sceneIDs[sceneIdxWithGroup], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: []models.GroupsScenes{ { GroupID: invalidID, }, }, Mode: models.RelationshipUpdateModeAdd, }, }, models.Scene{}, true, }, { "remove galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdxWithScene]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ GalleryIDs: models.NewRelatedIDs([]int{}), }, false, }, { "remove tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithScene]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx2WithScene]}), }, false, }, { "remove performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithScene]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx2WithScene]}), }, false, }, { "remove groups", sceneIDs[sceneIdxWithGroup], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: []models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], }, }, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ Groups: models.NewRelatedGroups([]models.GroupsScenes{}), }, false, }, { "remove stash ids", sceneIDs[sceneIdxWithSpacedName], models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: []models.StashID{sceneStashID(sceneIdxWithSpacedName)}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ StashIDs: models.NewRelatedStashIDs([]models.StashID{}), }, false, }, { "remove unrelated galleries", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdx1WithImage]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), }, false, }, { "remove unrelated tags", sceneIDs[sceneIdxWithTwoTags], models.ScenePartial{ TagIDs: &models.UpdateIDs{ IDs: []int{tagIDs[tagIdx1WithPerformer]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ TagIDs: models.NewRelatedIDs(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags])), }, false, }, { "remove unrelated performers", sceneIDs[sceneIdxWithTwoPerformers], models.ScenePartial{ PerformerIDs: &models.UpdateIDs{ IDs: []int{performerIDs[performerIdx1WithDupName]}, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ PerformerIDs: models.NewRelatedIDs(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers])), }, false, }, { "remove unrelated groups", sceneIDs[sceneIdxWithGroup], models.ScenePartial{ GroupIDs: &models.UpdateGroupIDs{ Groups: []models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithDupName], }, }, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: indexesToIDs(groupIDs, sceneGroups[sceneIdxWithGroup])[0], }, }), }, false, }, { "remove unrelated stash ids", sceneIDs[sceneIdxWithGallery], models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: stashIDs, Mode: models.RelationshipUpdateModeRemove, }, }, models.Scene{ StashIDs: models.NewRelatedStashIDs([]models.StashID{sceneStashID(sceneIdxWithGallery)}), }, false, }, } for _, tt := range tests { qb := db.Scene runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.UpdatePartial(ctx, tt.id, tt.partial) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.UpdatePartial() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { return } s, err := qb.Find(ctx, tt.id) if err != nil { t.Errorf("sceneQueryBuilder.Find() error = %v", err) } // load relationships if err := loadSceneRelationships(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } if err := loadSceneRelationships(ctx, tt.want, s); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } // only compare fields that were in the partial if tt.partial.PerformerIDs != nil { assert.ElementsMatch(tt.want.PerformerIDs.List(), got.PerformerIDs.List()) assert.ElementsMatch(tt.want.PerformerIDs.List(), s.PerformerIDs.List()) } if tt.partial.TagIDs != nil { assert.ElementsMatch(tt.want.TagIDs.List(), got.TagIDs.List()) assert.ElementsMatch(tt.want.TagIDs.List(), s.TagIDs.List()) } if tt.partial.GalleryIDs != nil { assert.ElementsMatch(tt.want.GalleryIDs.List(), got.GalleryIDs.List()) assert.ElementsMatch(tt.want.GalleryIDs.List(), s.GalleryIDs.List()) } if tt.partial.GroupIDs != nil { assert.ElementsMatch(tt.want.Groups.List(), got.Groups.List()) assert.ElementsMatch(tt.want.Groups.List(), s.Groups.List()) } if tt.partial.StashIDs != nil { assert.ElementsMatch(tt.want.StashIDs.List(), got.StashIDs.List()) assert.ElementsMatch(tt.want.StashIDs.List(), s.StashIDs.List()) } }) } } func Test_sceneQueryBuilder_AddO(t *testing.T) { tests := []struct { name string id int want int wantErr bool }{ { "increment", sceneIDs[1], 1, false, }, { "invalid", invalidID, 0, true, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { got, err := qb.AddO(ctx, tt.id, nil) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.AddO() error = %v, wantErr %v", err, tt.wantErr) return } if len(got) != tt.want { t.Errorf("sceneQueryBuilder.AddO() = %v, want %v", got, tt.want) } }) } } func Test_sceneQueryBuilder_DeleteO(t *testing.T) { tests := []struct { name string id int want int wantErr bool }{ { "decrement", sceneIDs[2], 0, false, }, { "zero", sceneIDs[0], 0, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { got, err := qb.DeleteO(ctx, tt.id, nil) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.DeleteO() error = %v, wantErr %v", err, tt.wantErr) return } if len(got) != tt.want { t.Errorf("sceneQueryBuilder.DeleteO() = %v, want %v", got, tt.want) } }) } } func Test_sceneQueryBuilder_ResetO(t *testing.T) { tests := []struct { name string id int want int wantErr bool }{ { "decrement", sceneIDs[2], 0, false, }, { "zero", sceneIDs[0], 0, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { got, err := qb.ResetO(ctx, tt.id) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.ResetO() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("sceneQueryBuilder.ResetOCounter() = %v, want %v", got, tt.want) } }) } } func Test_sceneQueryBuilder_ResetWatchCount(t *testing.T) { return } func Test_sceneQueryBuilder_Destroy(t *testing.T) { tests := []struct { name string id int wantErr bool }{ { "valid", sceneIDs[sceneIdxWithGallery], false, }, { "invalid", invalidID, true, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) if err := qb.Destroy(ctx, tt.id); (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.Destroy() error = %v, wantErr %v", err, tt.wantErr) } // ensure cannot be found i, err := qb.Find(ctx, tt.id) assert.Nil(err) assert.Nil(i) }) } } func makeSceneWithID(index int) *models.Scene { ret := makeScene(index) ret.ID = sceneIDs[index] ret.Files = models.NewRelatedVideoFiles([]*models.VideoFile{makeSceneFile(index)}) return ret } func Test_sceneQueryBuilder_Find(t *testing.T) { tests := []struct { name string id int want *models.Scene wantErr bool }{ { "valid", sceneIDs[sceneIdxWithSpacedName], makeSceneWithID(sceneIdxWithSpacedName), false, }, { "invalid", invalidID, nil, false, }, { "with galleries", sceneIDs[sceneIdxWithGallery], makeSceneWithID(sceneIdxWithGallery), false, }, { "with performers", sceneIDs[sceneIdxWithTwoPerformers], makeSceneWithID(sceneIdxWithTwoPerformers), false, }, { "with tags", sceneIDs[sceneIdxWithTwoTags], makeSceneWithID(sceneIdxWithTwoTags), false, }, { "with groups", sceneIDs[sceneIdxWithGroup], makeSceneWithID(sceneIdxWithGroup), false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.Find(ctx, tt.id) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.Find() error = %v, wantErr %v", err, tt.wantErr) return } if got != nil { // load relationships if err := loadSceneRelationships(ctx, *tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } clearSceneFileIDs(got) } assert.Equal(tt.want, got) }) } } func postFindScenes(ctx context.Context, want []*models.Scene, got []*models.Scene) error { for i, s := range got { // load relationships if i < len(want) { if err := loadSceneRelationships(ctx, *want[i], s); err != nil { return err } } clearSceneFileIDs(s) } return nil } func Test_sceneQueryBuilder_FindMany(t *testing.T) { tests := []struct { name string ids []int want []*models.Scene wantErr bool }{ { "valid with relationships", []int{ sceneIDs[sceneIdxWithGallery], sceneIDs[sceneIdxWithTwoPerformers], sceneIDs[sceneIdxWithTwoTags], sceneIDs[sceneIdxWithGroup], }, []*models.Scene{ makeSceneWithID(sceneIdxWithGallery), makeSceneWithID(sceneIdxWithTwoPerformers), makeSceneWithID(sceneIdxWithTwoTags), makeSceneWithID(sceneIdxWithGroup), }, false, }, { "invalid", []int{sceneIDs[sceneIdxWithGallery], sceneIDs[sceneIdxWithTwoPerformers], invalidID}, nil, true, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.FindMany(ctx, tt.ids) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.FindMany() error = %v, wantErr %v", err, tt.wantErr) return } if err := postFindScenes(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(tt.want, got) }) } } func Test_sceneQueryBuilder_FindByChecksum(t *testing.T) { getChecksum := func(index int) string { return getSceneStringValue(index, checksumField) } tests := []struct { name string checksum string want []*models.Scene wantErr bool }{ { "valid", getChecksum(sceneIdxWithSpacedName), []*models.Scene{makeSceneWithID(sceneIdxWithSpacedName)}, false, }, { "invalid", "invalid checksum", nil, false, }, { "with galleries", getChecksum(sceneIdxWithGallery), []*models.Scene{makeSceneWithID(sceneIdxWithGallery)}, false, }, { "with performers", getChecksum(sceneIdxWithTwoPerformers), []*models.Scene{makeSceneWithID(sceneIdxWithTwoPerformers)}, false, }, { "with tags", getChecksum(sceneIdxWithTwoTags), []*models.Scene{makeSceneWithID(sceneIdxWithTwoTags)}, false, }, { "with groups", getChecksum(sceneIdxWithGroup), []*models.Scene{makeSceneWithID(sceneIdxWithGroup)}, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.FindByChecksum(ctx, tt.checksum) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.FindByChecksum() error = %v, wantErr %v", err, tt.wantErr) return } if err := postFindScenes(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(tt.want, got) }) } } func Test_sceneQueryBuilder_FindByOSHash(t *testing.T) { getOSHash := func(index int) string { return getSceneStringValue(index, "oshash") } tests := []struct { name string oshash string want []*models.Scene wantErr bool }{ { "valid", getOSHash(sceneIdxWithSpacedName), []*models.Scene{makeSceneWithID(sceneIdxWithSpacedName)}, false, }, { "invalid", "invalid oshash", nil, false, }, { "with galleries", getOSHash(sceneIdxWithGallery), []*models.Scene{makeSceneWithID(sceneIdxWithGallery)}, false, }, { "with performers", getOSHash(sceneIdxWithTwoPerformers), []*models.Scene{makeSceneWithID(sceneIdxWithTwoPerformers)}, false, }, { "with tags", getOSHash(sceneIdxWithTwoTags), []*models.Scene{makeSceneWithID(sceneIdxWithTwoTags)}, false, }, { "with groups", getOSHash(sceneIdxWithGroup), []*models.Scene{makeSceneWithID(sceneIdxWithGroup)}, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { got, err := qb.FindByOSHash(ctx, tt.oshash) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.FindByOSHash() error = %v, wantErr %v", err, tt.wantErr) return } if err := postFindScenes(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("sceneQueryBuilder.FindByOSHash() = %v, want %v", got, tt.want) } }) } } func Test_sceneQueryBuilder_FindByPath(t *testing.T) { getPath := func(index int) string { return getFilePath(folderIdxWithSceneFiles, getSceneBasename(index)) } tests := []struct { name string path string want []*models.Scene wantErr bool }{ { "valid", getPath(sceneIdxWithSpacedName), []*models.Scene{makeSceneWithID(sceneIdxWithSpacedName)}, false, }, { "invalid", "invalid path", nil, false, }, { "with galleries", getPath(sceneIdxWithGallery), []*models.Scene{makeSceneWithID(sceneIdxWithGallery)}, false, }, { "with performers", getPath(sceneIdxWithTwoPerformers), []*models.Scene{makeSceneWithID(sceneIdxWithTwoPerformers)}, false, }, { "with tags", getPath(sceneIdxWithTwoTags), []*models.Scene{makeSceneWithID(sceneIdxWithTwoTags)}, false, }, { "with groups", getPath(sceneIdxWithGroup), []*models.Scene{makeSceneWithID(sceneIdxWithGroup)}, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.FindByPath(ctx, tt.path) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.FindByPath() error = %v, wantErr %v", err, tt.wantErr) return } if err := postFindScenes(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(tt.want, got) }) } } func Test_sceneQueryBuilder_FindByGalleryID(t *testing.T) { tests := []struct { name string galleryID int want []*models.Scene wantErr bool }{ { "valid", galleryIDs[galleryIdxWithScene], []*models.Scene{makeSceneWithID(sceneIdxWithGallery)}, false, }, { "none", galleryIDs[galleryIdx1WithPerformer], nil, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.FindByGalleryID(ctx, tt.galleryID) if (err != nil) != tt.wantErr { t.Errorf("sceneQueryBuilder.FindByGalleryID() error = %v, wantErr %v", err, tt.wantErr) return } if err := postFindScenes(ctx, tt.want, got); err != nil { t.Errorf("loadSceneRelationships() error = %v", err) return } assert.Equal(tt.want, got) return }) } } func TestSceneCountByPerformerID(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene count, err := sqb.CountByPerformerID(ctx, performerIDs[performerIdxWithScene]) if err != nil { t.Errorf("Error counting scenes: %s", err.Error()) } assert.Equal(t, 1, count) count, err = sqb.CountByPerformerID(ctx, 0) if err != nil { t.Errorf("Error counting scenes: %s", err.Error()) } assert.Equal(t, 0, count) return nil }) } func scenesToIDs(i []*models.Scene) []int { ret := make([]int, len(i)) for i, v := range i { ret[i] = v.ID } return ret } func Test_sceneStore_FindByFileID(t *testing.T) { tests := []struct { name string fileID models.FileID include []int exclude []int }{ { "valid", sceneFileIDs[sceneIdx1WithPerformer], []int{sceneIdx1WithPerformer}, nil, }, { "invalid", invalidFileID, nil, []int{sceneIdx1WithPerformer}, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.FindByFileID(ctx, tt.fileID) if err != nil { t.Errorf("SceneStore.FindByFileID() error = %v", err) return } for _, f := range got { clearSceneFileIDs(f) } ids := scenesToIDs(got) include := indexesToIDs(galleryIDs, tt.include) exclude := indexesToIDs(galleryIDs, tt.exclude) for _, i := range include { assert.Contains(ids, i) } for _, e := range exclude { assert.NotContains(ids, e) } }) } } func Test_sceneStore_CountByFileID(t *testing.T) { tests := []struct { name string fileID models.FileID want int }{ { "valid", sceneFileIDs[sceneIdxWithTwoPerformers], 1, }, { "invalid", invalidFileID, 0, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.CountByFileID(ctx, tt.fileID) if err != nil { t.Errorf("SceneStore.CountByFileID() error = %v", err) return } assert.Equal(tt.want, got) }) } } func Test_sceneStore_CountMissingChecksum(t *testing.T) { tests := []struct { name string want int }{ { "valid", 0, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.CountMissingChecksum(ctx) if err != nil { t.Errorf("SceneStore.CountMissingChecksum() error = %v", err) return } assert.Equal(tt.want, got) }) } } func Test_sceneStore_CountMissingOshash(t *testing.T) { tests := []struct { name string want int }{ { "valid", 0, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.CountMissingOSHash(ctx) if err != nil { t.Errorf("SceneStore.CountMissingOSHash() error = %v", err) return } assert.Equal(tt.want, got) }) } } func TestSceneWall(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene const sceneIdx = 2 wallQuery := getSceneStringValue(sceneIdx, "Details") scenes, err := sqb.Wall(ctx, &wallQuery) if err != nil { t.Errorf("Error finding scenes: %s", err.Error()) return nil } assert.Len(t, scenes, 1) scene := scenes[0] assert.Equal(t, sceneIDs[sceneIdx], scene.ID) scenePath := getFilePath(folderIdxWithSceneFiles, getSceneBasename(sceneIdx)) assert.Equal(t, scenePath, scene.Path) wallQuery = "not exist" scenes, err = sqb.Wall(ctx, &wallQuery) if err != nil { t.Errorf("Error finding scene: %s", err.Error()) return nil } assert.Len(t, scenes, 0) return nil }) } func TestSceneQueryQ(t *testing.T) { const sceneIdx = 2 q := getSceneStringValue(sceneIdx, titleField) withTxn(func(ctx context.Context) error { sqb := db.Scene sceneQueryQ(ctx, t, sqb, q, sceneIdx) return nil }) } func queryScene(ctx context.Context, t *testing.T, sqb models.SceneReader, sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) []*models.Scene { t.Helper() result, err := sqb.Query(ctx, models.SceneQueryOptions{ QueryOptions: models.QueryOptions{ FindFilter: findFilter, Count: true, }, SceneFilter: sceneFilter, TotalDuration: true, TotalSize: true, }) if err != nil { t.Errorf("Error querying scene: %v", err) return nil } scenes, err := result.Resolve(ctx) if err != nil { t.Errorf("Error resolving scenes: %v", err) } return scenes } func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q string, expectedSceneIdx int) { filter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, nil, &filter) if !assert.Len(t, scenes, 1) { return } scene := scenes[0] assert.Equal(t, sceneIDs[expectedSceneIdx], scene.ID) // no Q should return all results filter.Q = nil pp := totalScenes filter.PerPage = &pp scenes = queryScene(ctx, t, sqb, nil, &filter) assert.Len(t, scenes, totalScenes) } func TestSceneQuery(t *testing.T) { var ( endpoint = sceneStashID(sceneIdxWithGallery).Endpoint stashID = sceneStashID(sceneIdxWithGallery).StashID depth = -1 ) tests := []struct { name string findFilter *models.FindFilterType filter *models.SceneFilterType includeIdxs []int excludeIdxs []int wantErr bool }{ { "specific resume time", nil, &models.SceneFilterType{ ResumeTime: &models.IntCriterionInput{ Modifier: models.CriterionModifierEquals, Value: int(getSceneResumeTime(sceneIdxWithGallery)), }, }, []int{sceneIdxWithGallery}, []int{sceneIdxWithGroup}, false, }, { "specific play duration", nil, &models.SceneFilterType{ PlayDuration: &models.IntCriterionInput{ Modifier: models.CriterionModifierEquals, Value: int(getScenePlayDuration(sceneIdxWithGallery)), }, }, []int{sceneIdxWithGallery}, []int{sceneIdxWithGroup}, false, }, // { // "specific play count", // nil, // &models.SceneFilterType{ // PlayCount: &models.IntCriterionInput{ // Modifier: models.CriterionModifierEquals, // Value: getScenePlayCount(sceneIdxWithGallery), // }, // }, // []int{sceneIdxWithGallery}, // []int{sceneIdxWithGroup}, // false, // }, { "stash id with endpoint", nil, &models.SceneFilterType{ StashIDEndpoint: &models.StashIDCriterionInput{ Endpoint: &endpoint, StashID: &stashID, Modifier: models.CriterionModifierEquals, }, }, []int{sceneIdxWithGallery}, nil, false, }, { "exclude stash id with endpoint", nil, &models.SceneFilterType{ StashIDEndpoint: &models.StashIDCriterionInput{ Endpoint: &endpoint, StashID: &stashID, Modifier: models.CriterionModifierNotEquals, }, }, nil, []int{sceneIdxWithGallery}, false, }, { "null stash id with endpoint", nil, &models.SceneFilterType{ StashIDEndpoint: &models.StashIDCriterionInput{ Endpoint: &endpoint, Modifier: models.CriterionModifierIsNull, }, }, nil, []int{sceneIdxWithGallery}, false, }, { "not null stash id with endpoint", nil, &models.SceneFilterType{ StashIDEndpoint: &models.StashIDCriterionInput{ Endpoint: &endpoint, Modifier: models.CriterionModifierNotNull, }, }, []int{sceneIdxWithGallery}, nil, false, }, { "with studio id 0 including child studios", nil, &models.SceneFilterType{ Studios: &models.HierarchicalMultiCriterionInput{ Value: []string{"0"}, Modifier: models.CriterionModifierIncludes, Depth: &depth, }, }, nil, nil, false, }, } for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ SceneFilter: tt.filter, QueryOptions: models.QueryOptions{ FindFilter: tt.findFilter, }, }) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr) return } include := indexesToIDs(sceneIDs, tt.includeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs) for _, i := range include { assert.Contains(results.IDs, i) } for _, e := range exclude { assert.NotContains(results.IDs, e) } }) } } func TestSceneQueryPath(t *testing.T) { const ( sceneIdx = 1 otherSceneIdx = 2 ) folder := folderPaths[folderIdxWithSceneFiles] basename := getSceneBasename(sceneIdx) scenePath := getFilePath(folderIdxWithSceneFiles, getSceneBasename(sceneIdx)) tests := []struct { name string input models.StringCriterionInput mustInclude []int mustExclude []int }{ { "equals full path", models.StringCriterionInput{ Value: scenePath, Modifier: models.CriterionModifierEquals, }, []int{sceneIdx}, []int{otherSceneIdx}, }, { "equals full path wildcard", models.StringCriterionInput{ Value: filepath.Join(folder, "scene_0001_%"), Modifier: models.CriterionModifierEquals, }, []int{sceneIdx}, []int{otherSceneIdx}, }, { "not equals full path", models.StringCriterionInput{ Value: scenePath, Modifier: models.CriterionModifierNotEquals, }, []int{otherSceneIdx}, []int{sceneIdx}, }, { "includes folder name", models.StringCriterionInput{ Value: folder, Modifier: models.CriterionModifierIncludes, }, []int{sceneIdx}, nil, }, { "includes base name", models.StringCriterionInput{ Value: basename, Modifier: models.CriterionModifierIncludes, }, []int{sceneIdx}, nil, }, { "includes full path", models.StringCriterionInput{ Value: scenePath, Modifier: models.CriterionModifierIncludes, }, []int{sceneIdx}, []int{otherSceneIdx}, }, { "matches regex", models.StringCriterionInput{ Value: "scene_.*1_Path", Modifier: models.CriterionModifierMatchesRegex, }, []int{sceneIdx}, nil, }, { "not matches regex", models.StringCriterionInput{ Value: "scene_.*1_Path", Modifier: models.CriterionModifierNotMatchesRegex, }, nil, []int{sceneIdx}, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { got, err := qb.Query(ctx, models.SceneQueryOptions{ SceneFilter: &models.SceneFilterType{ Path: &tt.input, }, }) if err != nil { t.Errorf("sceneQueryBuilder.TestSceneQueryPath() error = %v", err) return } mustInclude := indexesToIDs(sceneIDs, tt.mustInclude) mustExclude := indexesToIDs(sceneIDs, tt.mustExclude) missing := sliceutil.Exclude(mustInclude, got.IDs) if len(missing) > 0 { t.Errorf("SceneStore.TestSceneQueryPath() missing expected IDs: %v", missing) } notExcluded := sliceutil.Intersect(mustExclude, got.IDs) if len(notExcluded) > 0 { t.Errorf("SceneStore.TestSceneQueryPath() expected IDs to be excluded: %v", notExcluded) } }) } } func TestSceneQueryURL(t *testing.T) { const sceneIdx = 1 sceneURL := getSceneStringValue(sceneIdx, urlField) urlCriterion := models.StringCriterionInput{ Value: sceneURL, Modifier: models.CriterionModifierEquals, } filter := models.SceneFilterType{ URL: &urlCriterion, } verifyFn := func(s *models.Scene) { t.Helper() urls := s.URLs.List() var url string if len(urls) > 0 { url = urls[0] } verifyString(t, url, urlCriterion) } verifySceneQuery(t, filter, verifyFn) urlCriterion.Modifier = models.CriterionModifierNotEquals verifySceneQuery(t, filter, verifyFn) urlCriterion.Modifier = models.CriterionModifierMatchesRegex urlCriterion.Value = "scene_.*1_URL" verifySceneQuery(t, filter, verifyFn) urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex verifySceneQuery(t, filter, verifyFn) urlCriterion.Modifier = models.CriterionModifierIsNull urlCriterion.Value = "" verifySceneQuery(t, filter, verifyFn) urlCriterion.Modifier = models.CriterionModifierNotNull verifySceneQuery(t, filter, verifyFn) } func TestSceneQueryPathOr(t *testing.T) { const scene1Idx = 1 const scene2Idx = 2 scene1Path := getFilePath(folderIdxWithSceneFiles, getSceneBasename(scene1Idx)) scene2Path := getFilePath(folderIdxWithSceneFiles, getSceneBasename(scene2Idx)) sceneFilter := models.SceneFilterType{ Path: &models.StringCriterionInput{ Value: scene1Path, Modifier: models.CriterionModifierEquals, }, OperatorFilter: models.OperatorFilter[models.SceneFilterType]{ Or: &models.SceneFilterType{ Path: &models.StringCriterionInput{ Value: scene2Path, Modifier: models.CriterionModifierEquals, }, }, }, } withTxn(func(ctx context.Context) error { sqb := db.Scene scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) if !assert.Len(t, scenes, 2) { return nil } assert.Equal(t, scene1Path, scenes[0].Path) assert.Equal(t, scene2Path, scenes[1].Path) return nil }) } func TestSceneQueryPathAndRating(t *testing.T) { const sceneIdx = 1 scenePath := getFilePath(folderIdxWithSceneFiles, getSceneBasename(sceneIdx)) sceneRating := int(getRating(sceneIdx).Int64) sceneFilter := models.SceneFilterType{ Path: &models.StringCriterionInput{ Value: scenePath, Modifier: models.CriterionModifierEquals, }, OperatorFilter: models.OperatorFilter[models.SceneFilterType]{ And: &models.SceneFilterType{ Rating100: &models.IntCriterionInput{ Value: sceneRating, Modifier: models.CriterionModifierEquals, }, }, }, } withTxn(func(ctx context.Context) error { sqb := db.Scene scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) if !assert.Len(t, scenes, 1) { return nil } assert.Equal(t, scenePath, scenes[0].Path) assert.Equal(t, sceneRating, *scenes[0].Rating) return nil }) } func TestSceneQueryPathNotRating(t *testing.T) { const sceneIdx = 1 sceneRating := getRating(sceneIdx) pathCriterion := models.StringCriterionInput{ Value: "scene_.*1_Path", Modifier: models.CriterionModifierMatchesRegex, } ratingCriterion := models.IntCriterionInput{ Value: int(sceneRating.Int64), Modifier: models.CriterionModifierEquals, } sceneFilter := models.SceneFilterType{ Path: &pathCriterion, OperatorFilter: models.OperatorFilter[models.SceneFilterType]{ Not: &models.SceneFilterType{ Rating100: &ratingCriterion, }, }, } withTxn(func(ctx context.Context) error { sqb := db.Scene scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { verifyString(t, scene.Path, pathCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals verifyIntPtr(t, scene.Rating, ratingCriterion) } return nil }) } func TestSceneIllegalQuery(t *testing.T) { assert := assert.New(t) const sceneIdx = 1 subFilter := models.SceneFilterType{ Path: &models.StringCriterionInput{ Value: getSceneStringValue(sceneIdx, "Path"), Modifier: models.CriterionModifierEquals, }, } sceneFilter := &models.SceneFilterType{ OperatorFilter: models.OperatorFilter[models.SceneFilterType]{ And: &subFilter, Or: &subFilter, }, } withTxn(func(ctx context.Context) error { sqb := db.Scene queryOptions := models.SceneQueryOptions{ SceneFilter: sceneFilter, } _, err := sqb.Query(ctx, queryOptions) assert.NotNil(err) sceneFilter.Or = nil sceneFilter.Not = &subFilter _, err = sqb.Query(ctx, queryOptions) assert.NotNil(err) sceneFilter.And = nil sceneFilter.Or = &subFilter _, err = sqb.Query(ctx, queryOptions) assert.NotNil(err) return nil }) } func verifySceneQuery(t *testing.T, filter models.SceneFilterType, verifyFn func(s *models.Scene)) { t.Helper() withTxn(func(ctx context.Context) error { t.Helper() sqb := db.Scene scenes := queryScene(ctx, t, sqb, &filter, nil) for _, scene := range scenes { if err := scene.LoadRelationships(ctx, sqb); err != nil { t.Errorf("Error loading scene relationships: %v", err) } } // assume it should find at least one assert.Greater(t, len(scenes), 0) for _, scene := range scenes { verifyFn(scene) } return nil }) } func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ Path: &pathCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { verifyString(t, scene.Path, pathCriterion) } return nil }) } func verifyStringPtr(t *testing.T, value *string, criterion models.StringCriterionInput) { t.Helper() assert := assert.New(t) if criterion.Modifier == models.CriterionModifierIsNull { if value != nil && *value == "" { // correct return } assert.Nil(value, "expect is null values to be null") } if criterion.Modifier == models.CriterionModifierNotNull { assert.NotNil(value, "expect is null values to be null") assert.Greater(len(*value), 0) } if criterion.Modifier == models.CriterionModifierEquals { assert.Equal(criterion.Value, *value) } if criterion.Modifier == models.CriterionModifierNotEquals { assert.NotEqual(criterion.Value, *value) } if criterion.Modifier == models.CriterionModifierMatchesRegex { assert.NotNil(value) assert.Regexp(regexp.MustCompile(criterion.Value), *value) } if criterion.Modifier == models.CriterionModifierNotMatchesRegex { if value == nil { // correct return } assert.NotRegexp(regexp.MustCompile(criterion.Value), value) } } func verifyString(t *testing.T, value string, criterion models.StringCriterionInput) { t.Helper() assert := assert.New(t) switch criterion.Modifier { case models.CriterionModifierEquals: assert.Equal(criterion.Value, value) case models.CriterionModifierNotEquals: assert.NotEqual(criterion.Value, value) case models.CriterionModifierMatchesRegex: assert.Regexp(regexp.MustCompile(criterion.Value), value) case models.CriterionModifierNotMatchesRegex: assert.NotRegexp(regexp.MustCompile(criterion.Value), value) case models.CriterionModifierIsNull: assert.Equal("", value) case models.CriterionModifierNotNull: assert.NotEqual("", value) } } func TestSceneQueryRating100(t *testing.T) { const rating = 60 ratingCriterion := models.IntCriterionInput{ Value: rating, Modifier: models.CriterionModifierEquals, } verifyScenesRating100(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals verifyScenesRating100(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierGreaterThan verifyScenesRating100(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierLessThan verifyScenesRating100(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierIsNull verifyScenesRating100(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotNull verifyScenesRating100(t, ratingCriterion) } func verifyScenesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ Rating100: &ratingCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { verifyIntPtr(t, scene.Rating, ratingCriterion) } return nil }) } func verifyIntPtr(t *testing.T, value *int, criterion models.IntCriterionInput) { t.Helper() assert := assert.New(t) if criterion.Modifier == models.CriterionModifierIsNull { assert.Nil(value, "expect is null values to be null") } if criterion.Modifier == models.CriterionModifierNotNull { assert.NotNil(value, "expect is null values to be null") } if criterion.Modifier == models.CriterionModifierEquals { assert.Equal(criterion.Value, *value) } if criterion.Modifier == models.CriterionModifierNotEquals { assert.NotEqual(criterion.Value, *value) } if criterion.Modifier == models.CriterionModifierGreaterThan { assert.True(*value > criterion.Value) } if criterion.Modifier == models.CriterionModifierLessThan { assert.True(*value < criterion.Value) } } func TestSceneQueryOCounter(t *testing.T) { const oCounter = 1 oCounterCriterion := models.IntCriterionInput{ Value: oCounter, Modifier: models.CriterionModifierEquals, } verifyScenesOCounter(t, oCounterCriterion) oCounterCriterion.Modifier = models.CriterionModifierNotEquals verifyScenesOCounter(t, oCounterCriterion) oCounterCriterion.Modifier = models.CriterionModifierGreaterThan verifyScenesOCounter(t, oCounterCriterion) oCounterCriterion.Modifier = models.CriterionModifierLessThan verifyScenesOCounter(t, oCounterCriterion) } func verifyScenesOCounter(t *testing.T, oCounterCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ OCounter: &oCounterCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { count, err := sqb.GetOCount(ctx, scene.ID) if err != nil { t.Errorf("Error getting ocounter: %v", err) } verifyInt(t, count, oCounterCriterion) } return nil }) } func verifyInt(t *testing.T, value int, criterion models.IntCriterionInput) bool { t.Helper() assert := assert.New(t) if criterion.Modifier == models.CriterionModifierEquals { return assert.Equal(criterion.Value, value) } if criterion.Modifier == models.CriterionModifierNotEquals { return assert.NotEqual(criterion.Value, value) } if criterion.Modifier == models.CriterionModifierGreaterThan { return assert.Greater(value, criterion.Value) } if criterion.Modifier == models.CriterionModifierLessThan { return assert.Less(value, criterion.Value) } return true } func TestSceneQueryDuration(t *testing.T) { duration := 200.432 durationCriterion := models.IntCriterionInput{ Value: int(duration), Modifier: models.CriterionModifierEquals, } verifyScenesDuration(t, durationCriterion) durationCriterion.Modifier = models.CriterionModifierNotEquals verifyScenesDuration(t, durationCriterion) durationCriterion.Modifier = models.CriterionModifierGreaterThan verifyScenesDuration(t, durationCriterion) durationCriterion.Modifier = models.CriterionModifierLessThan verifyScenesDuration(t, durationCriterion) durationCriterion.Modifier = models.CriterionModifierIsNull verifyScenesDuration(t, durationCriterion) durationCriterion.Modifier = models.CriterionModifierNotNull verifyScenesDuration(t, durationCriterion) } func verifyScenesDuration(t *testing.T, durationCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ Duration: &durationCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { if err := scene.LoadPrimaryFile(ctx, db.File); err != nil { t.Errorf("Error querying scene files: %v", err) return nil } duration := scene.Files.Primary().Duration if durationCriterion.Modifier == models.CriterionModifierEquals { assert.True(t, duration >= float64(durationCriterion.Value) && duration < float64(durationCriterion.Value+1)) } else if durationCriterion.Modifier == models.CriterionModifierNotEquals { assert.True(t, duration < float64(durationCriterion.Value) || duration >= float64(durationCriterion.Value+1)) } else { verifyFloat64(t, duration, durationCriterion) } } return nil }) } func verifyFloat64(t *testing.T, value float64, criterion models.IntCriterionInput) { assert := assert.New(t) if criterion.Modifier == models.CriterionModifierEquals { assert.Equal(float64(criterion.Value), value) } if criterion.Modifier == models.CriterionModifierNotEquals { assert.NotEqual(float64(criterion.Value), value) } if criterion.Modifier == models.CriterionModifierGreaterThan { assert.True(value > float64(criterion.Value)) } if criterion.Modifier == models.CriterionModifierLessThan { assert.True(value < float64(criterion.Value)) } } func verifyFloat64Ptr(t *testing.T, value *float64, criterion models.IntCriterionInput) { assert := assert.New(t) switch criterion.Modifier { case models.CriterionModifierIsNull: assert.Nil(value, "expect is null values to be null") case models.CriterionModifierNotNull: assert.NotNil(value, "expect is not null values to not be null") case models.CriterionModifierEquals: assert.EqualValues(float64(criterion.Value), value) case models.CriterionModifierNotEquals: assert.NotEqualValues(float64(criterion.Value), value) case models.CriterionModifierGreaterThan: assert.True(value != nil && *value > float64(criterion.Value)) case models.CriterionModifierLessThan: assert.True(value != nil && *value < float64(criterion.Value)) } } func TestSceneQueryResolution(t *testing.T) { verifyScenesResolution(t, models.ResolutionEnumLow) verifyScenesResolution(t, models.ResolutionEnumStandard) verifyScenesResolution(t, models.ResolutionEnumStandardHd) verifyScenesResolution(t, models.ResolutionEnumFullHd) verifyScenesResolution(t, models.ResolutionEnumFourK) verifyScenesResolution(t, models.ResolutionEnum("unknown")) } func verifyScenesResolution(t *testing.T, resolution models.ResolutionEnum) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ Resolution: &models.ResolutionCriterionInput{ Value: resolution, Modifier: models.CriterionModifierEquals, }, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) for _, scene := range scenes { if err := scene.LoadPrimaryFile(ctx, db.File); err != nil { t.Errorf("Error querying scene files: %v", err) return nil } f := scene.Files.Primary() height := 0 if f != nil { height = f.Height } verifySceneResolution(t, &height, resolution) } return nil }) } func verifySceneResolution(t *testing.T, height *int, resolution models.ResolutionEnum) { if !resolution.IsValid() { return } assert := assert.New(t) assert.NotNil(height) if t.Failed() { return } h := *height switch resolution { case models.ResolutionEnumLow: assert.True(h < 480) case models.ResolutionEnumStandard: assert.True(h >= 480 && h < 720) case models.ResolutionEnumStandardHd: assert.True(h >= 720 && h < 1080) case models.ResolutionEnumFullHd: assert.True(h >= 1080 && h < 2160) case models.ResolutionEnumFourK: assert.True(h >= 2160) } } func TestAllResolutionsHaveResolutionRange(t *testing.T) { for _, resolution := range models.AllResolutionEnum { assert.NotZero(t, resolution.GetMinResolution(), "Define resolution range for %s in extension_resolution.go", resolution) assert.NotZero(t, resolution.GetMaxResolution(), "Define resolution range for %s in extension_resolution.go", resolution) } } func TestSceneQueryResolutionModifiers(t *testing.T) { if err := withRollbackTxn(func(ctx context.Context) error { qb := db.Scene sceneNoResolution, _ := createScene(ctx, 0, 0) firstScene540P, _ := createScene(ctx, 960, 540) secondScene540P, _ := createScene(ctx, 1280, 719) firstScene720P, _ := createScene(ctx, 1280, 720) secondScene720P, _ := createScene(ctx, 1280, 721) thirdScene720P, _ := createScene(ctx, 1920, 1079) scene1080P, _ := createScene(ctx, 1920, 1080) scenesEqualTo720P := queryScenes(ctx, t, qb, models.ResolutionEnumStandardHd, models.CriterionModifierEquals) scenesNotEqualTo720P := queryScenes(ctx, t, qb, models.ResolutionEnumStandardHd, models.CriterionModifierNotEquals) scenesGreaterThan720P := queryScenes(ctx, t, qb, models.ResolutionEnumStandardHd, models.CriterionModifierGreaterThan) scenesLessThan720P := queryScenes(ctx, t, qb, models.ResolutionEnumStandardHd, models.CriterionModifierLessThan) assert.Subset(t, scenesEqualTo720P, []*models.Scene{firstScene720P, secondScene720P, thirdScene720P}) assert.NotSubset(t, scenesEqualTo720P, []*models.Scene{sceneNoResolution, firstScene540P, secondScene540P, scene1080P}) assert.Subset(t, scenesNotEqualTo720P, []*models.Scene{sceneNoResolution, firstScene540P, secondScene540P, scene1080P}) assert.NotSubset(t, scenesNotEqualTo720P, []*models.Scene{firstScene720P, secondScene720P, thirdScene720P}) assert.Subset(t, scenesGreaterThan720P, []*models.Scene{scene1080P}) assert.NotSubset(t, scenesGreaterThan720P, []*models.Scene{sceneNoResolution, firstScene540P, secondScene540P, firstScene720P, secondScene720P, thirdScene720P}) assert.Subset(t, scenesLessThan720P, []*models.Scene{sceneNoResolution, firstScene540P, secondScene540P}) assert.NotSubset(t, scenesLessThan720P, []*models.Scene{scene1080P, firstScene720P, secondScene720P, thirdScene720P}) return nil }); err != nil { t.Error(err.Error()) } } func queryScenes(ctx context.Context, t *testing.T, queryBuilder models.SceneReaderWriter, resolution models.ResolutionEnum, modifier models.CriterionModifier) []*models.Scene { sceneFilter := models.SceneFilterType{ Resolution: &models.ResolutionCriterionInput{ Value: resolution, Modifier: modifier, }, } // needed so that we don't hit the default limit of 25 scenes pp := 1000 findFilter := &models.FindFilterType{ PerPage: &pp, } return queryScene(ctx, t, queryBuilder, &sceneFilter, findFilter) } func createScene(ctx context.Context, width int, height int) (*models.Scene, error) { name := fmt.Sprintf("TestSceneQueryResolutionModifiers %d %d", width, height) sceneFile := &models.VideoFile{ BaseFile: &models.BaseFile{ Basename: name, ParentFolderID: folderIDs[folderIdxWithSceneFiles], }, Width: width, Height: height, } if err := db.File.Create(ctx, sceneFile); err != nil { return nil, err } scene := &models.Scene{} if err := db.Scene.Create(ctx, scene, []models.FileID{sceneFile.ID}); err != nil { return nil, err } return scene, nil } func TestSceneQueryHasMarkers(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene hasMarkers := "true" sceneFilter := models.SceneFilterType{ HasMarkers: &hasMarkers, } q := getSceneStringValue(sceneIdxWithMarkers, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 1) assert.Equal(t, sceneIDs[sceneIdxWithMarkers], scenes[0].ID) hasMarkers = "false" scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.NotEqual(t, 0, len(scenes)) // ensure non of the ids equal the one with gallery for _, scene := range scenes { assert.NotEqual(t, sceneIDs[sceneIdxWithMarkers], scene.ID) } return nil }) } func TestSceneQueryIsMissingGallery(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "galleries" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } q := getSceneStringValue(sceneIdxWithGallery, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) // ensure non of the ids equal the one with gallery for _, scene := range scenes { assert.NotEqual(t, sceneIDs[sceneIdxWithGallery], scene.ID) } return nil }) } func TestSceneQueryIsMissingStudio(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "studio" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } q := getSceneStringValue(sceneIdxWithStudio, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) // ensure non of the ids equal the one with studio for _, scene := range scenes { assert.NotEqual(t, sceneIDs[sceneIdxWithStudio], scene.ID) } return nil }) } func TestSceneQueryIsMissingMovies(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "movie" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } q := getSceneStringValue(sceneIdxWithGroup, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) // ensure non of the ids equal the one with movies for _, scene := range scenes { assert.NotEqual(t, sceneIDs[sceneIdxWithGroup], scene.ID) } return nil }) } func TestSceneQueryIsMissingPerformers(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "performers" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } q := getSceneStringValue(sceneIdxWithPerformer, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.True(t, len(scenes) > 0) // ensure non of the ids equal the one with movies for _, scene := range scenes { assert.NotEqual(t, sceneIDs[sceneIdxWithPerformer], scene.ID) } return nil }) } func TestSceneQueryIsMissingDate(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "date" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) // one in four scenes have no date assert.Len(t, scenes, int(math.Ceil(float64(totalScenes)/4))) // ensure date is null for _, scene := range scenes { assert.Nil(t, scene.Date) } return nil }) } func TestSceneQueryIsMissingTags(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "tags" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } q := getSceneStringValue(sceneIdxWithTwoTags, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes := queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) findFilter.Q = nil scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.True(t, len(scenes) > 0) return nil }) } func TestSceneQueryIsMissingRating(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "rating" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.True(t, len(scenes) > 0) // ensure rating is null for _, scene := range scenes { assert.Nil(t, scene.Rating) } return nil }) } func TestSceneQueryIsMissingPhash(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene isMissing := "phash" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) if !assert.Len(t, scenes, 1) { return nil } assert.Equal(t, sceneIDs[sceneIdxMissingPhash], scenes[0].ID) return nil }) } func TestSceneQueryPerformers(t *testing.T) { tests := []struct { name string filter models.MultiCriterionInput includeIdxs []int excludeIdxs []int wantErr bool }{ { "includes", models.MultiCriterionInput{ Value: []string{ strconv.Itoa(performerIDs[performerIdxWithScene]), strconv.Itoa(performerIDs[performerIdx1WithScene]), }, Modifier: models.CriterionModifierIncludes, }, []int{ sceneIdxWithPerformer, sceneIdxWithTwoPerformers, }, []int{ sceneIdxWithGallery, }, false, }, { "includes all", models.MultiCriterionInput{ Value: []string{ strconv.Itoa(performerIDs[performerIdx1WithScene]), strconv.Itoa(performerIDs[performerIdx2WithScene]), }, Modifier: models.CriterionModifierIncludesAll, }, []int{ sceneIdxWithTwoPerformers, }, []int{ sceneIdxWithPerformer, }, false, }, { "excludes", models.MultiCriterionInput{ Modifier: models.CriterionModifierExcludes, Value: []string{strconv.Itoa(tagIDs[performerIdx1WithScene])}, }, nil, []int{sceneIdxWithTwoPerformers}, false, }, { "is null", models.MultiCriterionInput{ Modifier: models.CriterionModifierIsNull, }, []int{sceneIdxWithTag}, []int{ sceneIdxWithPerformer, sceneIdxWithTwoPerformers, sceneIdxWithPerformerTwoTags, }, false, }, { "not null", models.MultiCriterionInput{ Modifier: models.CriterionModifierNotNull, }, []int{ sceneIdxWithPerformer, sceneIdxWithTwoPerformers, sceneIdxWithPerformerTwoTags, }, []int{sceneIdxWithTag}, false, }, { "equals", models.MultiCriterionInput{ Modifier: models.CriterionModifierEquals, Value: []string{ strconv.Itoa(tagIDs[performerIdx1WithScene]), strconv.Itoa(tagIDs[performerIdx2WithScene]), }, }, []int{sceneIdxWithTwoPerformers}, []int{ sceneIdxWithThreePerformers, }, false, }, { "not equals", models.MultiCriterionInput{ Modifier: models.CriterionModifierNotEquals, Value: []string{ strconv.Itoa(tagIDs[performerIdx1WithScene]), strconv.Itoa(tagIDs[performerIdx2WithScene]), }, }, nil, nil, true, }, } for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ SceneFilter: &models.SceneFilterType{ Performers: &tt.filter, }, }) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr) return } include := indexesToIDs(sceneIDs, tt.includeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs) for _, i := range include { assert.Contains(results.IDs, i) } for _, e := range exclude { assert.NotContains(results.IDs, e) } }) } } func TestSceneQueryTags(t *testing.T) { tests := []struct { name string filter models.HierarchicalMultiCriterionInput includeIdxs []int excludeIdxs []int wantErr bool }{ { "includes", models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithScene]), strconv.Itoa(tagIDs[tagIdx1WithScene]), }, Modifier: models.CriterionModifierIncludes, }, []int{ sceneIdxWithTag, sceneIdxWithTwoTags, }, []int{ sceneIdxWithGallery, }, false, }, { "includes all", models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx2WithScene]), }, Modifier: models.CriterionModifierIncludesAll, }, []int{ sceneIdxWithTwoTags, }, []int{ sceneIdxWithTag, }, false, }, { "excludes", models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierExcludes, Value: []string{strconv.Itoa(tagIDs[tagIdx1WithScene])}, }, nil, []int{sceneIdxWithTwoTags}, false, }, { "is null", models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierIsNull, }, []int{sceneIdx1WithPerformer}, []int{ sceneIdxWithTag, sceneIdxWithTwoTags, sceneIdxWithMarkerAndTag, }, false, }, { "not null", models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierNotNull, }, []int{ sceneIdxWithTag, sceneIdxWithTwoTags, sceneIdxWithMarkerAndTag, }, []int{sceneIdx1WithPerformer}, false, }, { "equals", models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierEquals, Value: []string{ strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx2WithScene]), }, }, []int{sceneIdxWithTwoTags}, []int{ sceneIdxWithThreeTags, }, false, }, { "not equals", models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierNotEquals, Value: []string{ strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx2WithScene]), }, }, nil, nil, true, }, } for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ SceneFilter: &models.SceneFilterType{ Tags: &tt.filter, }, }) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr) return } include := indexesToIDs(sceneIDs, tt.includeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs) for _, i := range include { assert.Contains(results.IDs, i) } for _, e := range exclude { assert.NotContains(results.IDs, e) } }) } } func TestSceneQueryPerformerTags(t *testing.T) { allDepth := -1 tests := []struct { name string findFilter *models.FindFilterType filter *models.SceneFilterType includeIdxs []int excludeIdxs []int wantErr bool }{ { "includes", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]), }, Modifier: models.CriterionModifierIncludes, }, }, []int{ sceneIdxWithPerformerTag, sceneIdxWithPerformerTwoTags, sceneIdxWithTwoPerformerTag, }, []int{ sceneIdxWithPerformer, }, false, }, { "includes sub-tags", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithParentAndChild]), }, Depth: &allDepth, Modifier: models.CriterionModifierIncludes, }, }, []int{ sceneIdxWithPerformerParentTag, }, []int{ sceneIdxWithPerformer, sceneIdxWithPerformerTag, sceneIdxWithPerformerTwoTags, sceneIdxWithTwoPerformerTag, }, false, }, { "includes all", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx2WithPerformer]), }, Modifier: models.CriterionModifierIncludesAll, }, }, []int{ sceneIdxWithPerformerTwoTags, }, []int{ sceneIdxWithPerformer, sceneIdxWithPerformerTag, sceneIdxWithTwoPerformerTag, }, false, }, { "excludes performer tag tagIdx2WithPerformer", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierExcludes, Value: []string{strconv.Itoa(tagIDs[tagIdx2WithPerformer])}, }, }, nil, []int{sceneIdxWithTwoPerformerTag}, false, }, { "excludes sub-tags", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(tagIDs[tagIdxWithParentAndChild]), }, Depth: &allDepth, Modifier: models.CriterionModifierExcludes, }, }, []int{ sceneIdxWithPerformer, sceneIdxWithPerformerTag, sceneIdxWithPerformerTwoTags, sceneIdxWithTwoPerformerTag, }, []int{ sceneIdxWithPerformerParentTag, }, false, }, { "is null", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierIsNull, }, }, []int{sceneIdx1WithPerformer}, []int{sceneIdxWithPerformerTag}, false, }, { "not null", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierNotNull, }, }, []int{sceneIdxWithPerformerTag}, []int{sceneIdx1WithPerformer}, false, }, { "equals", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierEquals, Value: []string{ strconv.Itoa(tagIDs[tagIdx2WithPerformer]), }, }, }, nil, nil, true, }, { "not equals", nil, &models.SceneFilterType{ PerformerTags: &models.HierarchicalMultiCriterionInput{ Modifier: models.CriterionModifierNotEquals, Value: []string{ strconv.Itoa(tagIDs[tagIdx2WithPerformer]), }, }, }, nil, nil, true, }, } for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ SceneFilter: tt.filter, QueryOptions: models.QueryOptions{ FindFilter: tt.findFilter, }, }) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr) return } include := indexesToIDs(sceneIDs, tt.includeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs) for _, i := range include { assert.Contains(results.IDs, i) } for _, e := range exclude { assert.NotContains(results.IDs, e) } }) } } func TestSceneQueryStudio(t *testing.T) { tests := []struct { name string q string studioCriterion models.HierarchicalMultiCriterionInput expectedIDs []int wantErr bool }{ { "includes", "", models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithScene]), }, Modifier: models.CriterionModifierIncludes, }, []int{sceneIDs[sceneIdxWithStudio]}, false, }, { "excludes", getSceneStringValue(sceneIdxWithStudio, titleField), models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithScene]), }, Modifier: models.CriterionModifierExcludes, }, []int{}, false, }, { "excludes includes null", getSceneStringValue(sceneIdxWithGallery, titleField), models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithScene]), }, Modifier: models.CriterionModifierExcludes, }, []int{sceneIDs[sceneIdxWithGallery]}, false, }, { "equals", "", models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithScene]), }, Modifier: models.CriterionModifierEquals, }, []int{sceneIDs[sceneIdxWithStudio]}, false, }, { "not equals", getSceneStringValue(sceneIdxWithStudio, titleField), models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithScene]), }, Modifier: models.CriterionModifierNotEquals, }, []int{}, false, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { studioCriterion := tt.studioCriterion sceneFilter := models.SceneFilterType{ Studios: &studioCriterion, } var findFilter *models.FindFilterType if tt.q != "" { findFilter = &models.FindFilterType{ Q: &tt.q, } } scenes := queryScene(ctx, t, qb, &sceneFilter, findFilter) assert.ElementsMatch(t, scenesToIDs(scenes), tt.expectedIDs) }) } } func TestSceneQueryStudioDepth(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene depth := 2 studioCriterion := models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithGrandChild]), }, Modifier: models.CriterionModifierIncludes, Depth: &depth, } sceneFilter := models.SceneFilterType{ Studios: &studioCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Len(t, scenes, 1) depth = 1 scenes = queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Len(t, scenes, 0) studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])} scenes = queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Len(t, scenes, 1) // ensure id is correct assert.Equal(t, sceneIDs[sceneIdxWithGrandChildStudio], scenes[0].ID) depth = 2 studioCriterion = models.HierarchicalMultiCriterionInput{ Value: []string{ strconv.Itoa(studioIDs[studioIdxWithGrandChild]), }, Modifier: models.CriterionModifierExcludes, Depth: &depth, } q := getSceneStringValue(sceneIdxWithGrandChildStudio, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) depth = 1 scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 1) studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])} scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) return nil }) } func TestSceneGroups(t *testing.T) { type criterion struct { valueIdxs []int modifier models.CriterionModifier depth int } tests := []struct { name string c criterion q string includeIdxs []int excludeIdxs []int }{ { "includes", criterion{ []int{groupIdxWithScene}, models.CriterionModifierIncludes, 0, }, "", []int{sceneIdxWithGroup}, nil, }, { "excludes", criterion{ []int{groupIdxWithScene}, models.CriterionModifierExcludes, 0, }, getSceneStringValue(sceneIdxWithGroup, titleField), nil, []int{sceneIdxWithGroup}, }, { "includes (depth = 1)", criterion{ []int{groupIdxWithChildWithScene}, models.CriterionModifierIncludes, 1, }, "", []int{sceneIdxWithGroupWithParent}, nil, }, } for _, tt := range tests { valueIDs := indexesToIDs(groupIDs, tt.c.valueIdxs) runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) sceneFilter := &models.SceneFilterType{ Groups: &models.HierarchicalMultiCriterionInput{ Value: intslice.IntSliceToStringSlice(valueIDs), Modifier: tt.c.modifier, }, } if tt.c.depth != 0 { sceneFilter.Groups.Depth = &tt.c.depth } findFilter := &models.FindFilterType{} if tt.q != "" { findFilter.Q = &tt.q } results, err := db.Scene.Query(ctx, models.SceneQueryOptions{ SceneFilter: sceneFilter, QueryOptions: models.QueryOptions{ FindFilter: findFilter, }, }) if err != nil { t.Errorf("SceneStore.Query() error = %v", err) return } include := indexesToIDs(sceneIDs, tt.includeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs) assert.Subset(results.IDs, include) for _, e := range exclude { assert.NotContains(results.IDs, e) } }) } } func TestSceneQueryMovies(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene movieCriterion := models.MultiCriterionInput{ Value: []string{ strconv.Itoa(groupIDs[groupIdxWithScene]), }, Modifier: models.CriterionModifierIncludes, } sceneFilter := models.SceneFilterType{ Movies: &movieCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Len(t, scenes, 1) // ensure id is correct assert.Equal(t, sceneIDs[sceneIdxWithGroup], scenes[0].ID) movieCriterion = models.MultiCriterionInput{ Value: []string{ strconv.Itoa(groupIDs[groupIdxWithScene]), }, Modifier: models.CriterionModifierExcludes, } q := getSceneStringValue(sceneIdxWithGroup, titleField) findFilter := models.FindFilterType{ Q: &q, } scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) assert.Len(t, scenes, 0) return nil }) } func TestSceneQueryPhashDuplicated(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene duplicated := true phashCriterion := models.PHashDuplicationCriterionInput{ Duplicated: &duplicated, } sceneFilter := models.SceneFilterType{ Duplicated: &phashCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Len(t, scenes, dupeScenePhashes*2) duplicated = false scenes = queryScene(ctx, t, sqb, &sceneFilter, nil) // -1 for missing phash assert.Len(t, scenes, totalScenes-(dupeScenePhashes*2)-1) return nil }) } func TestSceneQuerySorting(t *testing.T) { tests := []struct { name string sortBy string dir models.SortDirectionEnum firstSceneIdx int // -1 to ignore lastSceneIdx int }{ { "bitrate", "bitrate", models.SortDirectionEnumAsc, -1, -1, }, { "duration", "duration", models.SortDirectionEnumDesc, -1, -1, }, { "file mod time", "file_mod_time", models.SortDirectionEnumDesc, -1, -1, }, { "file size", "filesize", models.SortDirectionEnumDesc, -1, -1, }, { "frame rate", "framerate", models.SortDirectionEnumDesc, -1, -1, }, { "path", "path", models.SortDirectionEnumDesc, -1, -1, }, { "perceptual_similarity", "perceptual_similarity", models.SortDirectionEnumDesc, -1, -1, }, { "play_count", "play_count", models.SortDirectionEnumDesc, -1, -1, }, { "last_played_at", "last_played_at", models.SortDirectionEnumDesc, -1, -1, }, { "resume_time", "resume_time", models.SortDirectionEnumDesc, sceneIDs[sceneIdx1WithPerformer], -1, }, { "play_duration", "play_duration", models.SortDirectionEnumDesc, sceneIDs[sceneIdx1WithPerformer], -1, }, } qb := db.Scene for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) got, err := qb.Query(ctx, models.SceneQueryOptions{ QueryOptions: models.QueryOptions{ FindFilter: &models.FindFilterType{ Sort: &tt.sortBy, Direction: &tt.dir, }, }, }) if err != nil { t.Errorf("sceneQueryBuilder.TestSceneQuerySorting() error = %v", err) return } scenes, err := got.Resolve(ctx) if err != nil { t.Errorf("sceneQueryBuilder.TestSceneQuerySorting() error = %v", err) return } if !assert.Greater(len(scenes), 0) { return } // scenes should be in same order as indexes firstScene := scenes[0] lastScene := scenes[len(scenes)-1] if tt.firstSceneIdx != -1 { firstSceneID := sceneIDs[tt.firstSceneIdx] assert.Equal(firstSceneID, firstScene.ID) } if tt.lastSceneIdx != -1 { lastSceneID := sceneIDs[tt.lastSceneIdx] assert.Equal(lastSceneID, lastScene.ID) } }) } } func TestSceneQueryPagination(t *testing.T) { perPage := 1 findFilter := models.FindFilterType{ PerPage: &perPage, } withTxn(func(ctx context.Context) error { sqb := db.Scene scenes := queryScene(ctx, t, sqb, nil, &findFilter) assert.Len(t, scenes, 1) firstID := scenes[0].ID page := 2 findFilter.Page = &page scenes = queryScene(ctx, t, sqb, nil, &findFilter) assert.Len(t, scenes, 1) secondID := scenes[0].ID assert.NotEqual(t, firstID, secondID) perPage = 2 page = 1 scenes = queryScene(ctx, t, sqb, nil, &findFilter) assert.Len(t, scenes, 2) assert.Equal(t, firstID, scenes[0].ID) assert.Equal(t, secondID, scenes[1].ID) return nil }) } func TestSceneQueryTagCount(t *testing.T) { const tagCount = 1 tagCountCriterion := models.IntCriterionInput{ Value: tagCount, Modifier: models.CriterionModifierEquals, } verifyScenesTagCount(t, tagCountCriterion) tagCountCriterion.Modifier = models.CriterionModifierNotEquals verifyScenesTagCount(t, tagCountCriterion) tagCountCriterion.Modifier = models.CriterionModifierGreaterThan verifyScenesTagCount(t, tagCountCriterion) tagCountCriterion.Modifier = models.CriterionModifierLessThan verifyScenesTagCount(t, tagCountCriterion) } func verifyScenesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ TagCount: &tagCountCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Greater(t, len(scenes), 0) for _, scene := range scenes { if err := scene.LoadTagIDs(ctx, sqb); err != nil { t.Errorf("scene.LoadTagIDs() error = %v", err) return nil } verifyInt(t, len(scene.TagIDs.List()), tagCountCriterion) } return nil }) } func TestSceneQueryPerformerCount(t *testing.T) { const performerCount = 1 performerCountCriterion := models.IntCriterionInput{ Value: performerCount, Modifier: models.CriterionModifierEquals, } verifyScenesPerformerCount(t, performerCountCriterion) performerCountCriterion.Modifier = models.CriterionModifierNotEquals verifyScenesPerformerCount(t, performerCountCriterion) performerCountCriterion.Modifier = models.CriterionModifierGreaterThan verifyScenesPerformerCount(t, performerCountCriterion) performerCountCriterion.Modifier = models.CriterionModifierLessThan verifyScenesPerformerCount(t, performerCountCriterion) } func verifyScenesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ PerformerCount: &performerCountCriterion, } scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) assert.Greater(t, len(scenes), 0) for _, scene := range scenes { if err := scene.LoadPerformerIDs(ctx, sqb); err != nil { t.Errorf("scene.LoadPerformerIDs() error = %v", err) return nil } verifyInt(t, len(scene.PerformerIDs.List()), performerCountCriterion) } return nil }) } func TestFindByMovieID(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene scenes, err := sqb.FindByGroupID(ctx, groupIDs[groupIdxWithScene]) if err != nil { t.Errorf("error calling FindByMovieID: %s", err.Error()) } assert.Len(t, scenes, 1) assert.Equal(t, sceneIDs[sceneIdxWithGroup], scenes[0].ID) scenes, err = sqb.FindByGroupID(ctx, 0) if err != nil { t.Errorf("error calling FindByMovieID: %s", err.Error()) } assert.Len(t, scenes, 0) return nil }) } func TestFindByPerformerID(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene scenes, err := sqb.FindByPerformerID(ctx, performerIDs[performerIdxWithScene]) if err != nil { t.Errorf("error calling FindByPerformerID: %s", err.Error()) } assert.Len(t, scenes, 1) assert.Equal(t, sceneIDs[sceneIdxWithPerformer], scenes[0].ID) scenes, err = sqb.FindByPerformerID(ctx, 0) if err != nil { t.Errorf("error calling FindByPerformerID: %s", err.Error()) } assert.Len(t, scenes, 0) return nil }) } func TestSceneUpdateSceneCover(t *testing.T) { if err := withTxn(func(ctx context.Context) error { qb := db.Scene sceneID := sceneIDs[sceneIdxWithGallery] return testUpdateImage(t, ctx, sceneID, qb.UpdateCover, qb.GetCover) }); err != nil { t.Error(err.Error()) } } func TestSceneStashIDs(t *testing.T) { if err := withTxn(func(ctx context.Context) error { qb := db.Scene // create scene to test against const name = "TestSceneStashIDs" scene := &models.Scene{ Title: name, } if err := qb.Create(ctx, scene, nil); err != nil { return fmt.Errorf("Error creating scene: %s", err.Error()) } if err := scene.LoadStashIDs(ctx, qb); err != nil { return err } testSceneStashIDs(ctx, t, scene) return nil }); err != nil { t.Error(err.Error()) } } func testSceneStashIDs(ctx context.Context, t *testing.T, s *models.Scene) { // ensure no stash IDs to begin with assert.Len(t, s.StashIDs.List(), 0) // add stash ids const stashIDStr = "stashID" const endpoint = "endpoint" stashID := models.StashID{ StashID: stashIDStr, Endpoint: endpoint, } qb := db.Scene // update stash ids and ensure was updated var err error s, err = qb.UpdatePartial(ctx, s.ID, models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: []models.StashID{stashID}, Mode: models.RelationshipUpdateModeSet, }, }) if err != nil { t.Error(err.Error()) } if err := s.LoadStashIDs(ctx, qb); err != nil { t.Error(err.Error()) return } assert.Equal(t, []models.StashID{stashID}, s.StashIDs.List()) // remove stash ids and ensure was updated s, err = qb.UpdatePartial(ctx, s.ID, models.ScenePartial{ StashIDs: &models.UpdateStashIDs{ StashIDs: []models.StashID{stashID}, Mode: models.RelationshipUpdateModeRemove, }, }) if err != nil { t.Error(err.Error()) } if err := s.LoadStashIDs(ctx, qb); err != nil { t.Error(err.Error()) return } assert.Len(t, s.StashIDs.List(), 0) } func TestSceneQueryQTrim(t *testing.T) { if err := withTxn(func(ctx context.Context) error { qb := db.Scene expectedID := sceneIDs[sceneIdxWithSpacedName] type test struct { query string id int count int } tests := []test{ {query: " zzz yyy ", id: expectedID, count: 1}, {query: " \"zzz yyy xxx\" ", id: expectedID, count: 1}, {query: "zzz", id: expectedID, count: 1}, {query: "\" zzz yyy \"", count: 0}, {query: "\"zzz yyy\"", count: 0}, {query: "\" zzz yyy\"", count: 0}, {query: "\"zzz yyy \"", count: 0}, } for _, tst := range tests { f := models.FindFilterType{ Q: &tst.query, } scenes := queryScene(ctx, t, qb, nil, &f) assert.Len(t, scenes, tst.count) if len(scenes) > 0 { assert.Equal(t, tst.id, scenes[0].ID) } } findFilter := models.FindFilterType{} scenes := queryScene(ctx, t, qb, nil, &findFilter) assert.NotEqual(t, 0, len(scenes)) return nil }); err != nil { t.Error(err.Error()) } } func TestSceneStore_All(t *testing.T) { qb := db.Scene withRollbackTxn(func(ctx context.Context) error { got, err := qb.All(ctx) if err != nil { t.Errorf("SceneStore.All() error = %v", err) return nil } // it's possible that other tests have created scenes assert.GreaterOrEqual(t, len(got), len(sceneIDs)) return nil }) } func TestSceneStore_FindDuplicates(t *testing.T) { qb := db.Scene withRollbackTxn(func(ctx context.Context) error { distance := 0 durationDiff := -1. got, err := qb.FindDuplicates(ctx, distance, durationDiff) if err != nil { t.Errorf("SceneStore.FindDuplicates() error = %v", err) return nil } assert.Len(t, got, dupeScenePhashes) distance = 1 durationDiff = -1. got, err = qb.FindDuplicates(ctx, distance, durationDiff) if err != nil { t.Errorf("SceneStore.FindDuplicates() error = %v", err) return nil } assert.Len(t, got, dupeScenePhashes) return nil }) } func TestSceneStore_AssignFiles(t *testing.T) { tests := []struct { name string sceneID int fileID models.FileID wantErr bool }{ { "valid", sceneIDs[sceneIdx1WithPerformer], sceneFileIDs[sceneIdx1WithStudio], false, }, { "invalid file id", sceneIDs[sceneIdx1WithPerformer], invalidFileID, true, }, { "invalid scene id", invalidID, sceneFileIDs[sceneIdx1WithStudio], true, }, } qb := db.Scene for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withRollbackTxn(func(ctx context.Context) error { if err := qb.AssignFiles(ctx, tt.sceneID, []models.FileID{tt.fileID}); (err != nil) != tt.wantErr { t.Errorf("SceneStore.AssignFiles() error = %v, wantErr %v", err, tt.wantErr) } return nil }) }) } } func TestSceneStore_AddView(t *testing.T) { tests := []struct { name string sceneID int expectedCount int wantErr bool }{ { "valid", sceneIDs[sceneIdx1WithPerformer], 1, //getScenePlayCount(sceneIdx1WithPerformer) + 1, false, }, { "invalid scene id", invalidID, 0, true, }, } qb := db.Scene for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withRollbackTxn(func(ctx context.Context) error { views, err := qb.AddViews(ctx, tt.sceneID, nil) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.AddView() error = %v, wantErr %v", err, tt.wantErr) } if err != nil { return nil } assert := assert.New(t) assert.Equal(tt.expectedCount, len(views)) // find the scene and check the count count, err := qb.CountViews(ctx, tt.sceneID) if err != nil { t.Errorf("SceneStore.CountViews() error = %v", err) } lastView, err := qb.LastView(ctx, tt.sceneID) if err != nil { t.Errorf("SceneStore.LastView() error = %v", err) } assert.Equal(tt.expectedCount, count) assert.True(lastView.After(time.Now().Add(-1 * time.Minute))) return nil }) }) } } func TestSceneStore_DecrementWatchCount(t *testing.T) { return } func TestSceneStore_SaveActivity(t *testing.T) { var ( resumeTime = 111.2 playDuration = 98.7 ) tests := []struct { name string sceneIdx int resumeTime *float64 playDuration *float64 wantErr bool }{ { "both", sceneIdx1WithPerformer, &resumeTime, &playDuration, false, }, { "resumeTime only", sceneIdx1WithPerformer, &resumeTime, nil, false, }, { "playDuration only", sceneIdx1WithPerformer, nil, &playDuration, false, }, { "none", sceneIdx1WithPerformer, nil, nil, false, }, { "invalid scene id", -1, &resumeTime, &playDuration, true, }, } qb := db.Scene for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { withRollbackTxn(func(ctx context.Context) error { id := -1 if tt.sceneIdx != -1 { id = sceneIDs[tt.sceneIdx] } _, err := qb.SaveActivity(ctx, id, tt.resumeTime, tt.playDuration) if (err != nil) != tt.wantErr { t.Errorf("SceneStore.SaveActivity() error = %v, wantErr %v", err, tt.wantErr) } if err != nil { return nil } assert := assert.New(t) // find the scene and check the values scene, err := qb.Find(ctx, id) if err != nil { t.Errorf("SceneStore.Find() error = %v", err) } expectedResumeTime := getSceneResumeTime(tt.sceneIdx) expectedPlayDuration := getScenePlayDuration(tt.sceneIdx) if tt.resumeTime != nil { expectedResumeTime = *tt.resumeTime } if tt.playDuration != nil { expectedPlayDuration += *tt.playDuration } assert.Equal(expectedResumeTime, scene.ResumeTime) assert.Equal(expectedPlayDuration, scene.PlayDuration) return nil }) }) } } // TODO Count // TODO SizeCount // TODO - this should be in history_test and generalised func TestSceneStore_CountAllViews(t *testing.T) { withRollbackTxn(func(ctx context.Context) error { qb := db.Scene sceneID := sceneIDs[sceneIdx1WithPerformer] // get the current play count currentCount, err := qb.CountAllViews(ctx) if err != nil { t.Errorf("SceneStore.CountAllViews() error = %v", err) return nil } // add a view _, err = qb.AddViews(ctx, sceneID, nil) if err != nil { t.Errorf("SceneStore.AddViews() error = %v", err) return nil } // get the new play count newCount, err := qb.CountAllViews(ctx) if err != nil { t.Errorf("SceneStore.CountAllViews() error = %v", err) return nil } assert.Equal(t, currentCount+1, newCount) return nil }) } func TestSceneStore_CountUniqueViews(t *testing.T) { withRollbackTxn(func(ctx context.Context) error { qb := db.Scene sceneID := sceneIDs[sceneIdx1WithPerformer] // get the current play count currentCount, err := qb.CountUniqueViews(ctx) if err != nil { t.Errorf("SceneStore.CountUniqueViews() error = %v", err) return nil } // add a view _, err = qb.AddViews(ctx, sceneID, nil) if err != nil { t.Errorf("SceneStore.AddViews() error = %v", err) return nil } // add a second view _, err = qb.AddViews(ctx, sceneID, nil) if err != nil { t.Errorf("SceneStore.AddViews() error = %v", err) return nil } // get the new play count newCount, err := qb.CountUniqueViews(ctx) if err != nil { t.Errorf("SceneStore.CountUniqueViews() error = %v", err) return nil } assert.Equal(t, currentCount+1, newCount) return nil }) }