stash/pkg/sqlite/studio_test.go
gitgiggety 7164bb28ac
Filter studio hierarchy (#1397)
* Add basic support for hierarchical filters

Add a new `hierarchicalMultiCriterionHandlerBuilder` filter type which
can / will be used for filtering hierarchical things like the
parent/child relation of the studios.
On the frontend side a new IHierarchicalLabeledIdCriterion criterion
type has been added to accompany this new filter type.

* Refactor movieQueryBuilder to use filterBuilder

Refactor the movieQueryBuilder to use the filterBuilder just as scene,
image and gallery as well.

* Support specifying depth for studios filter

Add an optional depth field to the studios filter for scenes, images,
galleries and movies. When specified that number of included
(grant)children are shown as well. In other words: this adds support for
showing scenes set to child studios when searching on the parent studio.

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-06-03 20:52:19 +10:00

592 lines
15 KiB
Go

// +build integration
package sqlite_test
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"testing"
"github.com/stashapp/stash/pkg/models"
"github.com/stretchr/testify/assert"
)
func TestStudioFindByName(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
name := studioNames[studioIdxWithScene] // find a studio by name
studio, err := sqb.FindByName(name, false)
if err != nil {
t.Errorf("Error finding studios: %s", err.Error())
}
assert.Equal(t, studioNames[studioIdxWithScene], studio.Name.String)
name = studioNames[studioIdxWithDupName] // find a studio by name nocase
studio, err = sqb.FindByName(name, true)
if err != nil {
t.Errorf("Error finding studios: %s", err.Error())
}
// studioIdxWithDupName and studioIdxWithScene should have similar names ( only diff should be Name vs NaMe)
//studio.Name should match with studioIdxWithScene since its ID is before studioIdxWithDupName
assert.Equal(t, studioNames[studioIdxWithScene], studio.Name.String)
//studio.Name should match with studioIdxWithDupName if the check is not case sensitive
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithDupName]), strings.ToLower(studio.Name.String))
return nil
})
}
func TestStudioQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
tqb := r.Studio()
name := studioNames[studioIdxWithScene] // find a studio by name
studios, err := tqb.QueryForAutoTag([]string{name})
if err != nil {
t.Errorf("Error finding studios: %s", err.Error())
}
assert.Len(t, studios, 2)
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[0].Name.String))
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[1].Name.String))
return nil
})
}
func TestStudioQueryParent(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioCriterion := models.MultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithChildStudio]),
},
Modifier: models.CriterionModifierIncludes,
}
studioFilter := models.StudioFilterType{
Parents: &studioCriterion,
}
studios, _, err := sqb.Query(&studioFilter, nil)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
assert.Len(t, studios, 1)
// ensure id is correct
assert.Equal(t, sceneIDs[studioIdxWithParentStudio], studios[0].ID)
studioCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithChildStudio]),
},
Modifier: models.CriterionModifierExcludes,
}
q := getStudioStringValue(studioIdxWithParentStudio, titleField)
findFilter := models.FindFilterType{
Q: &q,
}
studios, _, err = sqb.Query(&studioFilter, &findFilter)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
assert.Len(t, studios, 0)
return nil
})
}
func TestStudioDestroyParent(t *testing.T) {
const parentName = "parent"
const childName = "child"
// create parent and child studios
if err := withTxn(func(r models.Repository) error {
createdParent, err := createStudio(r.Studio(), parentName, nil)
if err != nil {
return fmt.Errorf("Error creating parent studio: %s", err.Error())
}
parentID := int64(createdParent.ID)
createdChild, err := createStudio(r.Studio(), childName, &parentID)
if err != nil {
return fmt.Errorf("Error creating child studio: %s", err.Error())
}
sqb := r.Studio()
// destroy the parent
err = sqb.Destroy(createdParent.ID)
if err != nil {
return fmt.Errorf("Error destroying parent studio: %s", err.Error())
}
// destroy the child
err = sqb.Destroy(createdChild.ID)
if err != nil {
return fmt.Errorf("Error destroying child studio: %s", err.Error())
}
return nil
}); err != nil {
t.Error(err.Error())
}
}
func TestStudioFindChildren(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studios, err := sqb.FindChildren(studioIDs[studioIdxWithChildStudio])
if err != nil {
t.Errorf("error calling FindChildren: %s", err.Error())
}
assert.Len(t, studios, 1)
assert.Equal(t, studioIDs[studioIdxWithParentStudio], studios[0].ID)
studios, err = sqb.FindChildren(0)
if err != nil {
t.Errorf("error calling FindChildren: %s", err.Error())
}
assert.Len(t, studios, 0)
return nil
})
}
func TestStudioUpdateClearParent(t *testing.T) {
const parentName = "clearParent_parent"
const childName = "clearParent_child"
// create parent and child studios
if err := withTxn(func(r models.Repository) error {
createdParent, err := createStudio(r.Studio(), parentName, nil)
if err != nil {
return fmt.Errorf("Error creating parent studio: %s", err.Error())
}
parentID := int64(createdParent.ID)
createdChild, err := createStudio(r.Studio(), childName, &parentID)
if err != nil {
return fmt.Errorf("Error creating child studio: %s", err.Error())
}
sqb := r.Studio()
// clear the parent id from the child
updatePartial := models.StudioPartial{
ID: createdChild.ID,
ParentID: &sql.NullInt64{Valid: false},
}
updatedStudio, err := sqb.Update(updatePartial)
if err != nil {
return fmt.Errorf("Error updated studio: %s", err.Error())
}
if updatedStudio.ParentID.Valid {
return errors.New("updated studio has parent ID set")
}
return nil
}); err != nil {
t.Error(err.Error())
}
}
func TestStudioUpdateStudioImage(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
qb := r.Studio()
// create performer to test against
const name = "TestStudioUpdateStudioImage"
created, err := createStudio(r.Studio(), name, nil)
if err != nil {
return fmt.Errorf("Error creating studio: %s", err.Error())
}
image := []byte("image")
err = qb.UpdateImage(created.ID, image)
if err != nil {
return fmt.Errorf("Error updating studio image: %s", err.Error())
}
// ensure image set
storedImage, err := qb.GetImage(created.ID)
if err != nil {
return fmt.Errorf("Error getting image: %s", err.Error())
}
assert.Equal(t, storedImage, image)
// set nil image
err = qb.UpdateImage(created.ID, nil)
if err == nil {
return fmt.Errorf("Expected error setting nil image")
}
return nil
}); err != nil {
t.Error(err.Error())
}
}
func TestStudioDestroyStudioImage(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
qb := r.Studio()
// create performer to test against
const name = "TestStudioDestroyStudioImage"
created, err := createStudio(r.Studio(), name, nil)
if err != nil {
return fmt.Errorf("Error creating studio: %s", err.Error())
}
image := []byte("image")
err = qb.UpdateImage(created.ID, image)
if err != nil {
return fmt.Errorf("Error updating studio image: %s", err.Error())
}
err = qb.DestroyImage(created.ID)
if err != nil {
return fmt.Errorf("Error destroying studio image: %s", err.Error())
}
// image should be nil
storedImage, err := qb.GetImage(created.ID)
if err != nil {
return fmt.Errorf("Error getting image: %s", err.Error())
}
assert.Nil(t, storedImage)
return nil
}); err != nil {
t.Error(err.Error())
}
}
func TestStudioQuerySceneCount(t *testing.T) {
const sceneCount = 1
sceneCountCriterion := models.IntCriterionInput{
Value: sceneCount,
Modifier: models.CriterionModifierEquals,
}
verifyStudiosSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudiosSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyStudiosSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierLessThan
verifyStudiosSceneCount(t, sceneCountCriterion)
}
func verifyStudiosSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioFilter := models.StudioFilterType{
SceneCount: &sceneCountCriterion,
}
studios := queryStudio(t, sqb, &studioFilter, nil)
assert.Greater(t, len(studios), 0)
for _, studio := range studios {
sceneCount, err := r.Scene().CountByStudioID(studio.ID)
if err != nil {
return err
}
verifyInt(t, sceneCount, sceneCountCriterion)
}
return nil
})
}
func TestStudioQueryImageCount(t *testing.T) {
const imageCount = 1
imageCountCriterion := models.IntCriterionInput{
Value: imageCount,
Modifier: models.CriterionModifierEquals,
}
verifyStudiosImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudiosImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyStudiosImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierLessThan
verifyStudiosImageCount(t, imageCountCriterion)
}
func verifyStudiosImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioFilter := models.StudioFilterType{
ImageCount: &imageCountCriterion,
}
studios := queryStudio(t, sqb, &studioFilter, nil)
assert.Greater(t, len(studios), 0)
for _, studio := range studios {
pp := 0
_, count, err := r.Image().Query(&models.ImageFilterType{
Studios: &models.HierarchicalMultiCriterionInput{
Value: []string{strconv.Itoa(studio.ID)},
Modifier: models.CriterionModifierIncludes,
Depth: 0,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, imageCountCriterion)
}
return nil
})
}
func TestStudioQueryGalleryCount(t *testing.T) {
const galleryCount = 1
galleryCountCriterion := models.IntCriterionInput{
Value: galleryCount,
Modifier: models.CriterionModifierEquals,
}
verifyStudiosGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudiosGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyStudiosGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierLessThan
verifyStudiosGalleryCount(t, galleryCountCriterion)
}
func verifyStudiosGalleryCount(t *testing.T, galleryCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioFilter := models.StudioFilterType{
GalleryCount: &galleryCountCriterion,
}
studios := queryStudio(t, sqb, &studioFilter, nil)
assert.Greater(t, len(studios), 0)
for _, studio := range studios {
pp := 0
_, count, err := r.Gallery().Query(&models.GalleryFilterType{
Studios: &models.HierarchicalMultiCriterionInput{
Value: []string{strconv.Itoa(studio.ID)},
Modifier: models.CriterionModifierIncludes,
Depth: 0,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, galleryCountCriterion)
}
return nil
})
}
func TestStudioStashIDs(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
qb := r.Studio()
// create studio to test against
const name = "TestStudioStashIDs"
created, err := createStudio(r.Studio(), name, nil)
if err != nil {
return fmt.Errorf("Error creating studio: %s", err.Error())
}
testStashIDReaderWriter(t, qb, created.ID)
return nil
}); err != nil {
t.Error(err.Error())
}
}
func TestStudioQueryURL(t *testing.T) {
const sceneIdx = 1
studioURL := getStudioStringValue(sceneIdx, urlField)
urlCriterion := models.StringCriterionInput{
Value: studioURL,
Modifier: models.CriterionModifierEquals,
}
filter := models.StudioFilterType{
URL: &urlCriterion,
}
verifyFn := func(g *models.Studio) {
t.Helper()
verifyNullString(t, g.URL, urlCriterion)
}
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierMatchesRegex
urlCriterion.Value = "studio_.*1_URL"
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierIsNull
urlCriterion.Value = ""
verifyStudioQuery(t, filter, verifyFn)
urlCriterion.Modifier = models.CriterionModifierNotNull
verifyStudioQuery(t, filter, verifyFn)
}
func TestStudioQueryRating(t *testing.T) {
const rating = 3
ratingCriterion := models.IntCriterionInput{
Value: rating,
Modifier: models.CriterionModifierEquals,
}
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotEquals
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierLessThan
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierIsNull
verifyStudiosRating(t, ratingCriterion)
ratingCriterion.Modifier = models.CriterionModifierNotNull
verifyStudiosRating(t, ratingCriterion)
}
func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(s *models.Studio)) {
withTxn(func(r models.Repository) error {
t.Helper()
sqb := r.Studio()
studios := queryStudio(t, sqb, &filter, nil)
// assume it should find at least one
assert.Greater(t, len(studios), 0)
for _, studio := range studios {
verifyFn(studio)
}
return nil
})
}
func verifyStudiosRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
studioFilter := models.StudioFilterType{
Rating: &ratingCriterion,
}
studios, _, err := sqb.Query(&studioFilter, nil)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
for _, studio := range studios {
verifyInt64(t, studio.Rating, ratingCriterion)
}
return nil
})
}
func TestStudioQueryIsMissingRating(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Studio()
isMissing := "rating"
studioFilter := models.StudioFilterType{
IsMissing: &isMissing,
}
studios, _, err := sqb.Query(&studioFilter, nil)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
assert.True(t, len(studios) > 0)
for _, studio := range studios {
assert.True(t, !studio.Rating.Valid)
}
return nil
})
}
func queryStudio(t *testing.T, sqb models.StudioReader, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) []*models.Studio {
studios, _, err := sqb.Query(studioFilter, findFilter)
if err != nil {
t.Errorf("Error querying studio: %s", err.Error())
}
return studios
}
// TODO Create
// TODO Update
// TODO Destroy
// TODO Find
// TODO FindBySceneID
// TODO Count
// TODO All
// TODO AllSlim
// TODO Query