mirror of https://github.com/stashapp/stash.git
612 lines
12 KiB
Go
612 lines
12 KiB
Go
package identify
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/models/mocks"
|
|
"github.com/stashapp/stash/pkg/scraper"
|
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
var testCtx = context.Background()
|
|
|
|
type mockSceneScraper struct {
|
|
errIDs []int
|
|
results map[int][]*scraper.ScrapedScene
|
|
}
|
|
|
|
func (s mockSceneScraper) ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
|
if intslice.IntInclude(s.errIDs, sceneID) {
|
|
return nil, errors.New("scrape scene error")
|
|
}
|
|
return s.results[sceneID], nil
|
|
}
|
|
|
|
type mockHookExecutor struct {
|
|
}
|
|
|
|
func (s mockHookExecutor) ExecuteSceneUpdatePostHooks(ctx context.Context, input models.SceneUpdateInput, inputFields []string) {
|
|
}
|
|
|
|
func TestSceneIdentifier_Identify(t *testing.T) {
|
|
const (
|
|
errID1 = iota
|
|
errID2
|
|
missingID
|
|
found1ID
|
|
found2ID
|
|
multiFoundID
|
|
multiFound2ID
|
|
errUpdateID
|
|
)
|
|
|
|
var (
|
|
skipMultipleTagID = 1
|
|
skipMultipleTagIDStr = strconv.Itoa(skipMultipleTagID)
|
|
)
|
|
|
|
var (
|
|
scrapedTitle = "scrapedTitle"
|
|
scrapedTitle2 = "scrapedTitle2"
|
|
|
|
boolFalse = false
|
|
boolTrue = true
|
|
)
|
|
|
|
defaultOptions := &MetadataOptions{
|
|
SetOrganized: &boolFalse,
|
|
SetCoverImage: &boolFalse,
|
|
IncludeMalePerformers: &boolFalse,
|
|
SkipSingleNamePerformers: &boolFalse,
|
|
}
|
|
sources := []ScraperSource{
|
|
{
|
|
Scraper: mockSceneScraper{
|
|
errIDs: []int{errID1},
|
|
results: map[int][]*scraper.ScrapedScene{
|
|
found1ID: {{
|
|
Title: &scrapedTitle,
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Scraper: mockSceneScraper{
|
|
errIDs: []int{errID2},
|
|
results: map[int][]*scraper.ScrapedScene{
|
|
found2ID: {{
|
|
Title: &scrapedTitle,
|
|
}},
|
|
errUpdateID: {{
|
|
Title: &scrapedTitle,
|
|
}},
|
|
multiFoundID: {
|
|
{
|
|
Title: &scrapedTitle,
|
|
},
|
|
{
|
|
Title: &scrapedTitle2,
|
|
},
|
|
},
|
|
multiFound2ID: {
|
|
{
|
|
Title: &scrapedTitle,
|
|
},
|
|
{
|
|
Title: &scrapedTitle2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockSceneReaderWriter := &mocks.SceneReaderWriter{}
|
|
mockSceneReaderWriter.On("GetURLs", mock.Anything, mock.Anything).Return(nil, nil)
|
|
mockSceneReaderWriter.On("UpdatePartial", mock.Anything, mock.MatchedBy(func(id int) bool {
|
|
return id == errUpdateID
|
|
}), mock.Anything).Return(nil, errors.New("update error"))
|
|
mockSceneReaderWriter.On("UpdatePartial", mock.Anything, mock.MatchedBy(func(id int) bool {
|
|
return id != errUpdateID
|
|
}), mock.Anything).Return(nil, nil)
|
|
|
|
mockTagFinderCreator := &mocks.TagReaderWriter{}
|
|
mockTagFinderCreator.On("Find", mock.Anything, skipMultipleTagID).Return(&models.Tag{
|
|
ID: skipMultipleTagID,
|
|
Name: skipMultipleTagIDStr,
|
|
}, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
sceneID int
|
|
options *MetadataOptions
|
|
wantErr bool
|
|
}{
|
|
{
|
|
"error scraping",
|
|
errID1,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"error scraping from second",
|
|
errID2,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"found in first scraper",
|
|
found1ID,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"found in second scraper",
|
|
found2ID,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"not found",
|
|
missingID,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"error modifying",
|
|
errUpdateID,
|
|
nil,
|
|
true,
|
|
},
|
|
{
|
|
"multiple found",
|
|
multiFoundID,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"multiple found - set tag",
|
|
multiFound2ID,
|
|
&MetadataOptions{
|
|
SkipMultipleMatches: &boolTrue,
|
|
SkipMultipleMatchTag: &skipMultipleTagIDStr,
|
|
},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
identifier := SceneIdentifier{
|
|
SceneReaderUpdater: mockSceneReaderWriter,
|
|
TagFinderCreator: mockTagFinderCreator,
|
|
DefaultOptions: defaultOptions,
|
|
Sources: sources,
|
|
SceneUpdatePostHookExecutor: mockHookExecutor{},
|
|
}
|
|
|
|
if tt.options != nil {
|
|
identifier.DefaultOptions = tt.options
|
|
}
|
|
|
|
scene := &models.Scene{
|
|
ID: tt.sceneID,
|
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
|
TagIDs: models.NewRelatedIDs([]int{}),
|
|
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
|
}
|
|
if err := identifier.Identify(testCtx, &mocks.TxnManager{}, scene); (err != nil) != tt.wantErr {
|
|
t.Errorf("SceneIdentifier.Identify() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSceneIdentifier_modifyScene(t *testing.T) {
|
|
repo := models.Repository{
|
|
TxnManager: &mocks.TxnManager{},
|
|
}
|
|
boolFalse := false
|
|
defaultOptions := &MetadataOptions{
|
|
SetOrganized: &boolFalse,
|
|
SetCoverImage: &boolFalse,
|
|
IncludeMalePerformers: &boolFalse,
|
|
SkipSingleNamePerformers: &boolFalse,
|
|
}
|
|
tr := &SceneIdentifier{
|
|
DefaultOptions: defaultOptions,
|
|
}
|
|
|
|
type args struct {
|
|
scene *models.Scene
|
|
result *scrapeResult
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantErr bool
|
|
}{
|
|
{
|
|
"empty update",
|
|
args{
|
|
&models.Scene{
|
|
URLs: models.NewRelatedStrings([]string{}),
|
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
|
TagIDs: models.NewRelatedIDs([]int{}),
|
|
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
|
},
|
|
&scrapeResult{
|
|
result: &scraper.ScrapedScene{},
|
|
source: ScraperSource{
|
|
Options: defaultOptions,
|
|
},
|
|
},
|
|
},
|
|
false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if err := tr.modifyScene(testCtx, repo, tt.args.scene, tt.args.result); (err != nil) != tt.wantErr {
|
|
t.Errorf("SceneIdentifier.modifyScene() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getFieldOptions(t *testing.T) {
|
|
const (
|
|
inFirst = "inFirst"
|
|
inSecond = "inSecond"
|
|
inBoth = "inBoth"
|
|
)
|
|
|
|
type args struct {
|
|
options []MetadataOptions
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want map[string]*FieldOptions
|
|
}{
|
|
{
|
|
"simple",
|
|
args{
|
|
[]MetadataOptions{
|
|
{
|
|
FieldOptions: []*FieldOptions{
|
|
{
|
|
Field: inFirst,
|
|
Strategy: FieldStrategyIgnore,
|
|
},
|
|
{
|
|
Field: inBoth,
|
|
Strategy: FieldStrategyIgnore,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FieldOptions: []*FieldOptions{
|
|
{
|
|
Field: inSecond,
|
|
Strategy: FieldStrategyMerge,
|
|
},
|
|
{
|
|
Field: inBoth,
|
|
Strategy: FieldStrategyMerge,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
map[string]*FieldOptions{
|
|
inFirst: {
|
|
Field: inFirst,
|
|
Strategy: FieldStrategyIgnore,
|
|
},
|
|
inSecond: {
|
|
Field: inSecond,
|
|
Strategy: FieldStrategyMerge,
|
|
},
|
|
inBoth: {
|
|
Field: inBoth,
|
|
Strategy: FieldStrategyIgnore,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := getFieldOptions(tt.args.options); !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("getFieldOptions() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getScenePartial(t *testing.T) {
|
|
var (
|
|
originalTitle = "originalTitle"
|
|
originalDate = "2001-01-01"
|
|
originalDetails = "originalDetails"
|
|
originalURL = "originalURL"
|
|
)
|
|
|
|
var (
|
|
scrapedTitle = "scrapedTitle"
|
|
scrapedDate = "2002-02-02"
|
|
scrapedDetails = "scrapedDetails"
|
|
scrapedURL = "scrapedURL"
|
|
)
|
|
|
|
originalDateObj, _ := models.ParseDate(originalDate)
|
|
scrapedDateObj, _ := models.ParseDate(scrapedDate)
|
|
|
|
originalScene := &models.Scene{
|
|
Title: originalTitle,
|
|
Date: &originalDateObj,
|
|
Details: originalDetails,
|
|
URLs: models.NewRelatedStrings([]string{originalURL}),
|
|
}
|
|
|
|
organisedScene := *originalScene
|
|
organisedScene.Organized = true
|
|
|
|
emptyScene := &models.Scene{
|
|
URLs: models.NewRelatedStrings([]string{}),
|
|
}
|
|
|
|
postPartial := models.ScenePartial{
|
|
Title: models.NewOptionalString(scrapedTitle),
|
|
Date: models.NewOptionalDate(scrapedDateObj),
|
|
Details: models.NewOptionalString(scrapedDetails),
|
|
URLs: &models.UpdateStrings{
|
|
Values: []string{scrapedURL},
|
|
Mode: models.RelationshipUpdateModeSet,
|
|
},
|
|
}
|
|
|
|
postPartialMerge := postPartial
|
|
postPartialMerge.URLs = &models.UpdateStrings{
|
|
Values: []string{scrapedURL},
|
|
Mode: models.RelationshipUpdateModeSet,
|
|
}
|
|
|
|
scrapedScene := &scraper.ScrapedScene{
|
|
Title: &scrapedTitle,
|
|
Date: &scrapedDate,
|
|
Details: &scrapedDetails,
|
|
URLs: []string{scrapedURL},
|
|
}
|
|
|
|
scrapedUnchangedScene := &scraper.ScrapedScene{
|
|
Title: &originalTitle,
|
|
Date: &originalDate,
|
|
Details: &originalDetails,
|
|
URLs: []string{originalURL},
|
|
}
|
|
|
|
makeFieldOptions := func(input *FieldOptions) map[string]*FieldOptions {
|
|
return map[string]*FieldOptions{
|
|
"title": input,
|
|
"date": input,
|
|
"details": input,
|
|
"url": input,
|
|
}
|
|
}
|
|
|
|
overwriteAll := makeFieldOptions(&FieldOptions{
|
|
Strategy: FieldStrategyOverwrite,
|
|
})
|
|
ignoreAll := makeFieldOptions(&FieldOptions{
|
|
Strategy: FieldStrategyIgnore,
|
|
})
|
|
mergeAll := makeFieldOptions(&FieldOptions{
|
|
Strategy: FieldStrategyMerge,
|
|
})
|
|
|
|
setOrganised := true
|
|
|
|
type args struct {
|
|
scene *models.Scene
|
|
scraped *scraper.ScrapedScene
|
|
fieldOptions map[string]*FieldOptions
|
|
setOrganized bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want models.ScenePartial
|
|
}{
|
|
{
|
|
"overwrite all",
|
|
args{
|
|
originalScene,
|
|
scrapedScene,
|
|
overwriteAll,
|
|
false,
|
|
},
|
|
postPartial,
|
|
},
|
|
{
|
|
"ignore all",
|
|
args{
|
|
originalScene,
|
|
scrapedScene,
|
|
ignoreAll,
|
|
false,
|
|
},
|
|
models.ScenePartial{},
|
|
},
|
|
{
|
|
"merge (existing values)",
|
|
args{
|
|
originalScene,
|
|
scrapedScene,
|
|
mergeAll,
|
|
false,
|
|
},
|
|
models.ScenePartial{
|
|
URLs: &models.UpdateStrings{
|
|
Values: []string{originalURL, scrapedURL},
|
|
Mode: models.RelationshipUpdateModeSet,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"merge (empty values)",
|
|
args{
|
|
emptyScene,
|
|
scrapedScene,
|
|
mergeAll,
|
|
false,
|
|
},
|
|
postPartialMerge,
|
|
},
|
|
{
|
|
"unchanged",
|
|
args{
|
|
originalScene,
|
|
scrapedUnchangedScene,
|
|
overwriteAll,
|
|
false,
|
|
},
|
|
models.ScenePartial{},
|
|
},
|
|
{
|
|
"set organized",
|
|
args{
|
|
originalScene,
|
|
scrapedUnchangedScene,
|
|
overwriteAll,
|
|
true,
|
|
},
|
|
models.ScenePartial{
|
|
Organized: models.NewOptionalBool(setOrganised),
|
|
},
|
|
},
|
|
{
|
|
"set organized unchanged",
|
|
args{
|
|
&organisedScene,
|
|
scrapedUnchangedScene,
|
|
overwriteAll,
|
|
true,
|
|
},
|
|
models.ScenePartial{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := getScenePartial(tt.args.scene, tt.args.scraped, tt.args.fieldOptions, tt.args.setOrganized)
|
|
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_shouldSetSingleValueField(t *testing.T) {
|
|
const invalid = "invalid"
|
|
|
|
type args struct {
|
|
strategy *FieldOptions
|
|
hasExistingValue bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
"ignore",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: FieldStrategyIgnore,
|
|
},
|
|
false,
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"merge existing",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: FieldStrategyMerge,
|
|
},
|
|
true,
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"merge absent",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: FieldStrategyMerge,
|
|
},
|
|
false,
|
|
},
|
|
true,
|
|
},
|
|
{
|
|
"overwrite",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: FieldStrategyOverwrite,
|
|
},
|
|
true,
|
|
},
|
|
true,
|
|
},
|
|
{
|
|
"nil (merge) existing",
|
|
args{
|
|
&FieldOptions{},
|
|
true,
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"nil (merge) absent",
|
|
args{
|
|
&FieldOptions{},
|
|
false,
|
|
},
|
|
true,
|
|
},
|
|
{
|
|
"invalid (merge) existing",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: invalid,
|
|
},
|
|
true,
|
|
},
|
|
false,
|
|
},
|
|
{
|
|
"invalid (merge) absent",
|
|
args{
|
|
&FieldOptions{
|
|
Strategy: invalid,
|
|
},
|
|
false,
|
|
},
|
|
true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := shouldSetSingleValueField(tt.args.strategy, tt.args.hasExistingValue); got != tt.want {
|
|
t.Errorf("shouldSetSingleValueField() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|