mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
* 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>
592 lines
15 KiB
Go
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
|