From 88a9d705a9c0f240e76876ac2d990c81d3ee40eb Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 22 Apr 2021 21:37:59 +0200 Subject: [PATCH 01/13] Add integration tests for exif parsers --- api/go.mod | 3 ++ api/go.sum | 6 +++ api/scanner/exif/exif.go | 4 +- api/scanner/exif/exif_parser_external.go | 4 +- api/scanner/exif/exif_parser_internal.go | 48 +++++++++--------- api/scanner/exif/exif_test.go | 61 +++++++++++++++++++++++ api/scanner/exif/test_data/bird.jpg | Bin 0 -> 37498 bytes 7 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 api/scanner/exif/exif_test.go create mode 100644 api/scanner/exif/test_data/bird.jpg diff --git a/api/go.mod b/api/go.mod index 493ee735..801265e1 100644 --- a/api/go.mod +++ b/api/go.mod @@ -21,6 +21,8 @@ require ( github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f + github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect github.com/strukturag/libheif v1.11.0 github.com/vektah/gqlparser/v2 v2.1.0 github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 @@ -30,6 +32,7 @@ require ( golang.org/x/text v0.3.6 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/vansante/go-ffprobe.v2 v2.0.2 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gorm.io/driver/mysql v1.0.5 gorm.io/driver/postgres v1.0.8 gorm.io/driver/sqlite v1.1.4 diff --git a/api/go.sum b/api/go.sum index 02e0d65c..08336cc9 100644 --- a/api/go.sum +++ b/api/go.sum @@ -539,6 +539,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -546,6 +548,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/strukturag/libheif v1.11.0 h1:HaWu5re98INSXNq7C8o5AwLcv2qD8+U7a+jVCpGWemI= github.com/strukturag/libheif v1.11.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= @@ -756,6 +760,8 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o= gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI= gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= diff --git a/api/scanner/exif/exif.go b/api/scanner/exif/exif.go index 082f26c4..0f40a50a 100644 --- a/api/scanner/exif/exif.go +++ b/api/scanner/exif/exif.go @@ -11,7 +11,7 @@ import ( ) type exifParser interface { - ParseExif(media *models.Media) (*models.MediaEXIF, error) + ParseExif(media_path string) (*models.MediaEXIF, error) } var use_exiftool bool = false @@ -55,7 +55,7 @@ func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) { parser = &internalExifParser{} } - exif, err := parser.ParseExif(media) + exif, err := parser.ParseExif(media.Path) if err != nil { return nil, errors.Wrap(err, "failed to parse exif data") } diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index b151f1c3..b057d1c9 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -10,7 +10,7 @@ import ( type externalExifParser struct{} -func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { +func (p *externalExifParser) ParseExif(media_path string) (returnExif *models.MediaEXIF, returnErr error) { // ExifTool - No print conversion mode et, err := exiftool.NewExiftool(exiftool.NoPrintConversion()) if err != nil { @@ -19,7 +19,7 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. } defer et.Close() - fileInfos := et.ExtractMetadata(media.Path) + fileInfos := et.ExtractMetadata(media_path) newExif := models.MediaEXIF{} for _, fileInfo := range fileInfos { diff --git a/api/scanner/exif/exif_parser_internal.go b/api/scanner/exif/exif_parser_internal.go index 59523a32..b1d30129 100644 --- a/api/scanner/exif/exif_parser_internal.go +++ b/api/scanner/exif/exif_parser_internal.go @@ -15,8 +15,8 @@ import ( // internalExifParser is an exif parser that parses the media without the use of external tools type internalExifParser struct{} -func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { - photoFile, err := os.Open(media.Path) +func (p *internalExifParser) ParseExif(media_path string) (returnExif *models.MediaEXIF, returnErr error) { + photoFile, err := os.Open(media_path) if err != nil { return nil, err } @@ -38,17 +38,17 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. newExif := models.MediaEXIF{} - model, err := p.readStringTag(exifTags, exif.Model, media) + model, err := p.readStringTag(exifTags, exif.Model, media_path) if err == nil { newExif.Camera = model } - maker, err := p.readStringTag(exifTags, exif.Make, media) + maker, err := p.readStringTag(exifTags, exif.Make, media_path) if err == nil { newExif.Maker = maker } - lens, err := p.readStringTag(exifTags, exif.LensModel, media) + lens, err := p.readStringTag(exifTags, exif.LensModel, media_path) if err == nil { newExif.Lens = lens } @@ -66,7 +66,7 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. } } - apertureRat, err := p.readRationalTag(exifTags, exif.FNumber, media) + apertureRat, err := p.readRationalTag(exifTags, exif.FNumber, media_path) if err == nil { aperture, _ := apertureRat.Float64() newExif.Aperture = &aperture @@ -74,11 +74,11 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. isoTag, err := exifTags.Get(exif.ISOSpeedRatings) if err != nil { - log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %s\n", media.Title) + log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %v\n", media_path) } else { iso, err := isoTag.Int(0) if err != nil { - log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %s\n", media.Title) + log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %v\n", media_path) } else { iso64 := int64(iso) newExif.Iso = &iso64 @@ -99,7 +99,7 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. if err == nil { focalLength, err := focalLengthTag.Int(1) if err != nil { - log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %s\n%s\n", media.Title, err) + log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %v\n%s\n", media_path, err) } else { focalLenFloat := float64(focalLength) newExif.FocalLength = &focalLenFloat @@ -108,19 +108,19 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. } } - flash, err := p.readIntegerTag(exifTags, exif.Flash, media) + flash, err := p.readIntegerTag(exifTags, exif.Flash, media_path) if err == nil { flash64 := int64(*flash) newExif.Flash = &flash64 } - orientation, err := p.readIntegerTag(exifTags, exif.Orientation, media) + orientation, err := p.readIntegerTag(exifTags, exif.Orientation, media_path) if err == nil { orientation64 := int64(*orientation) newExif.Orientation = &orientation64 } - exposureProgram, err := p.readIntegerTag(exifTags, exif.ExposureProgram, media) + exposureProgram, err := p.readIntegerTag(exifTags, exif.ExposureProgram, media_path) if err == nil { exposureProgram64 := int64(*exposureProgram) newExif.ExposureProgram = &exposureProgram64 @@ -136,59 +136,59 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. return } -func (p *internalExifParser) readStringTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*string, error) { +func (p *internalExifParser) readStringTag(tags *exif.Exif, name exif.FieldName, media_path string) (*string, error) { tag, err := tags.Get(name) if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) + return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media_path) } if tag != nil { value, err := tag.StringVal() if err != nil { - return nil, errors.Wrapf(err, "could not parse %s from EXIF as string: %s", name, media.Title) + return nil, errors.Wrapf(err, "could not parse %s from EXIF as string: %s", name, media_path) } return &value, nil } - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media_path) return nil, errors.New("exif tag returned null") } -func (p *internalExifParser) readRationalTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*big.Rat, error) { +func (p *internalExifParser) readRationalTag(tags *exif.Exif, name exif.FieldName, media_path string) (*big.Rat, error) { tag, err := tags.Get(name) if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) + return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media_path) } if tag != nil { value, err := tag.Rat(0) if err != nil { - return nil, errors.Wrapf(err, "could not parse %s from EXIF as rational: %s", name, media.Title) + return nil, errors.Wrapf(err, "could not parse %s from EXIF as rational: %s", name, media_path) } return value, nil } - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media_path) return nil, errors.New("exif tag returned null") } -func (p *internalExifParser) readIntegerTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*int, error) { +func (p *internalExifParser) readIntegerTag(tags *exif.Exif, name exif.FieldName, media_path string) (*int, error) { tag, err := tags.Get(name) if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) + return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media_path) } if tag != nil { value, err := tag.Int(0) if err != nil { - return nil, errors.Wrapf(err, "Could not parse %s from EXIF as integer: %s", name, media.Title) + return nil, errors.Wrapf(err, "Could not parse %s from EXIF as integer: %s", name, media_path) } return &value, nil } - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media_path) return nil, errors.New("exif tag returned null") } diff --git a/api/scanner/exif/exif_test.go b/api/scanner/exif/exif_test.go new file mode 100644 index 00000000..049de693 --- /dev/null +++ b/api/scanner/exif/exif_test.go @@ -0,0 +1,61 @@ +// +build integration + +package exif + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const bird_path = "./test_data/bird.jpg" + +func TestExifParsers(t *testing.T) { + t.Parallel() + + parsers := []struct { + name string + parser exifParser + }{ + { + name: "internal", + parser: &internalExifParser{}, + }, + { + name: "external", + parser: &externalExifParser{}, + }, + } + + for _, p := range parsers { + t.Run(p.name, func(t *testing.T) { + t.Parallel() + + exif, err := p.parser.ParseExif(bird_path) + + if assert.NoError(t, err) { + assert.EqualValues(t, *exif.Camera, "Canon EOS 600D") + assert.EqualValues(t, *exif.Maker, "Canon") + assert.WithinDuration(t, *exif.DateShot, time.Unix(1336318784, 0).UTC(), time.Minute) + assert.EqualValues(t, *exif.Aperture, 6.3) + assert.EqualValues(t, *exif.Iso, 800) + assert.EqualValues(t, *exif.FocalLength, 300) + assert.EqualValues(t, *exif.Flash, 16) + assert.EqualValues(t, *exif.Orientation, 1) + assert.InDelta(t, *exif.GPSLatitude, 65.01681388888889, 0.0001) + assert.InDelta(t, *exif.GPSLongitude, 25.466863888888888, 0.0001) + } + }) + } +} + +// func TestExternalExifParser(t *testing.T) { +// parser := externalExifParser{} + +// exif, err := parser.ParseExif((bird_path)) + +// if assert.NoError(t, err) { +// assert.Equal(t, exif, &bird_exif) +// } +// } diff --git a/api/scanner/exif/test_data/bird.jpg b/api/scanner/exif/test_data/bird.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea631f9ed3f7df68cf62ae26b7d2f03a9f2c3b5f GIT binary patch literal 37498 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Π^r%?uJ!z#IqW-B? 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 literal 0 HcmV?d00001 From dc8325bd4abbe20760dae3a211b773cd7815919f Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 22 Apr 2021 22:46:52 +0200 Subject: [PATCH 02/13] Extend exif tests --- api/scanner/exif/exif_parser_external.go | 18 +++++++ api/scanner/exif/exif_parser_internal.go | 8 +++- api/scanner/exif/exif_test.go | 57 +++++++++++++++++------ api/scanner/exif/test_data/stripped.jpg | Bin 0 -> 11132 bytes 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 api/scanner/exif/test_data/stripped.jpg diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index b057d1c9..61d958ec 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -21,6 +21,7 @@ func (p *externalExifParser) ParseExif(media_path string) (returnExif *models.Me fileInfos := et.ExtractMetadata(media_path) newExif := models.MediaEXIF{} + found_exif := false for _, fileInfo := range fileInfos { if fileInfo.Err != nil { @@ -31,18 +32,21 @@ func (p *externalExifParser) ParseExif(media_path string) (returnExif *models.Me // Get camera model model, err := fileInfo.GetString("Model") if err == nil { + found_exif = true newExif.Camera = &model } // Get Camera make make, err := fileInfo.GetString("Make") if err == nil { + found_exif = true newExif.Maker = &make } // Get lens lens, err := fileInfo.GetString("LensModel") if err == nil { + found_exif = true newExif.Lens = &lens } @@ -52,6 +56,7 @@ func (p *externalExifParser) ParseExif(media_path string) (returnExif *models.Me layout := "2006:01:02 15:04:05" dateTime, err := time.Parse(layout, date) if err == nil { + found_exif = true newExif.DateShot = &dateTime } } @@ -59,58 +64,71 @@ func (p *externalExifParser) ParseExif(media_path string) (returnExif *models.Me // Get exposure time exposureTime, err := fileInfo.GetFloat("ExposureTime") if err == nil { + found_exif = true newExif.Exposure = &exposureTime } // Get aperture aperture, err := fileInfo.GetFloat("Aperture") if err == nil { + found_exif = true newExif.Aperture = &aperture } // Get ISO iso, err := fileInfo.GetInt("ISO") if err == nil { + found_exif = true newExif.Iso = &iso } // Get focal length focalLen, err := fileInfo.GetFloat("FocalLength") if err == nil { + found_exif = true newExif.FocalLength = &focalLen } // Get flash info flash, err := fileInfo.GetInt("Flash") if err == nil { + found_exif = true newExif.Flash = &flash } // Get orientation orientation, err := fileInfo.GetInt("Orientation") if err == nil { + found_exif = true newExif.Orientation = &orientation } // Get exposure program expProgram, err := fileInfo.GetInt("ExposureProgram") if err == nil { + found_exif = true newExif.ExposureProgram = &expProgram } // GPS coordinates - longitude longitudeRaw, err := fileInfo.GetFloat("GPSLongitude") if err == nil { + found_exif = true newExif.GPSLongitude = &longitudeRaw } // GPS coordinates - latitude latitudeRaw, err := fileInfo.GetFloat("GPSLatitude") if err == nil { + found_exif = true newExif.GPSLatitude = &latitudeRaw } } + if !found_exif { + return nil, nil + } + returnExif = &newExif return } diff --git a/api/scanner/exif/exif_parser_internal.go b/api/scanner/exif/exif_parser_internal.go index b1d30129..5897e6e5 100644 --- a/api/scanner/exif/exif_parser_internal.go +++ b/api/scanner/exif/exif_parser_internal.go @@ -5,6 +5,7 @@ import ( "log" "math/big" "os" + "time" "github.com/photoview/photoview/api/graphql/models" "github.com/pkg/errors" @@ -33,7 +34,8 @@ func (p *internalExifParser) ParseExif(media_path string) (returnExif *models.Me exifTags, err := exif.Decode(photoFile) if err != nil { - return nil, errors.Wrap(err, "Could not decode EXIF") + return nil, nil + // return nil, errors.Wrap(err, "Could not decode EXIF") } newExif := models.MediaEXIF{} @@ -55,7 +57,9 @@ func (p *internalExifParser) ParseExif(media_path string) (returnExif *models.Me date, err := exifTags.DateTime() if err == nil { - newExif.DateShot = &date + _, tz := date.Zone() + date_utc := date.Add(time.Duration(tz) * time.Second).UTC() + newExif.DateShot = &date_utc } exposure, err := exifTags.Get(exif.ExposureTime) diff --git a/api/scanner/exif/exif_test.go b/api/scanner/exif/exif_test.go index 049de693..f125be95 100644 --- a/api/scanner/exif/exif_test.go +++ b/api/scanner/exif/exif_test.go @@ -1,18 +1,20 @@ -// +build integration - package exif import ( + "fmt" + "path" "testing" "time" + "github.com/barasher/go-exiftool" + "github.com/photoview/photoview/api/graphql/models" "github.com/stretchr/testify/assert" ) -const bird_path = "./test_data/bird.jpg" - func TestExifParsers(t *testing.T) { - t.Parallel() + if testing.Short() { + t.Skip("skipping integration test") + } parsers := []struct { name string @@ -28,13 +30,14 @@ func TestExifParsers(t *testing.T) { }, } - for _, p := range parsers { - t.Run(p.name, func(t *testing.T) { - t.Parallel() - - exif, err := p.parser.ParseExif(bird_path) - - if assert.NoError(t, err) { + images := []struct { + path string + assert func(t *testing.T, exif *models.MediaEXIF) + }{ + { + path: "./test_data/bird.jpg", + assert: func(t *testing.T, exif *models.MediaEXIF) { + assert.WithinDuration(t, *exif.DateShot, time.Unix(1336318784, 0).UTC(), time.Minute) assert.EqualValues(t, *exif.Camera, "Canon EOS 600D") assert.EqualValues(t, *exif.Maker, "Canon") assert.WithinDuration(t, *exif.DateShot, time.Unix(1336318784, 0).UTC(), time.Minute) @@ -45,8 +48,34 @@ func TestExifParsers(t *testing.T) { assert.EqualValues(t, *exif.Orientation, 1) assert.InDelta(t, *exif.GPSLatitude, 65.01681388888889, 0.0001) assert.InDelta(t, *exif.GPSLongitude, 25.466863888888888, 0.0001) - } - }) + }, + }, + { + path: "./test_data/stripped.jpg", + assert: func(t *testing.T, exif *models.MediaEXIF) { + assert.Nil(t, exif) + }, + }, + } + + for _, p := range parsers { + for _, img := range images { + t.Run(fmt.Sprintf("%s:%s", p.name, path.Base(img.path)), func(t *testing.T) { + + if p.name == "external" { + _, err := exiftool.NewExiftool() + if err != nil { + t.Skip("failed to get exiftool, skipping test") + } + } + + exif, err := p.parser.ParseExif(img.path) + + if assert.NoError(t, err) { + img.assert(t, exif) + } + }) + } } } diff --git a/api/scanner/exif/test_data/stripped.jpg b/api/scanner/exif/test_data/stripped.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a1788e809c9a17658af8369054b22096a50c788 GIT binary patch literal 11132 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 POb%u^c4P&qKmYna*f$6s literal 0 HcmV?d00001 From 60f8a68bf61b6c9415bf27b9cbc40f76d5c7eb0f Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 15:10:23 +0200 Subject: [PATCH 03/13] Implement database integration tests --- .gitignore | 1 + api/database/database.go | 74 +++++++++++++++++----------- api/graphql/models/models_test.go | 12 +++++ api/graphql/models/site_info.go | 49 +++++++++--------- api/graphql/models/site_info_test.go | 21 ++++++++ api/graphql/models/user_test.go | 25 ++++++++++ api/scanner/exif/exif_test.go | 10 ++-- api/scanner/scanner_test.go | 12 +++++ api/test_utils/integration_setup.go | 70 ++++++++++++++++++++++++++ api/test_utils/test_db_manager.go | 60 ++++++++++++++++++++++ 10 files changed, 281 insertions(+), 53 deletions(-) create mode 100644 api/graphql/models/models_test.go create mode 100644 api/graphql/models/site_info_test.go create mode 100644 api/graphql/models/user_test.go create mode 100644 api/scanner/scanner_test.go create mode 100644 api/test_utils/integration_setup.go create mode 100644 api/test_utils/test_db_manager.go diff --git a/.gitignore b/.gitignore index 541190a5..e24ed426 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ photoview.db photoview.db-journal .env +testing.env # docker docker-compose.yml diff --git a/api/database/database.go b/api/database/database.go index 82b8b61f..597e3e44 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -20,8 +20,7 @@ import ( "gorm.io/gorm/logger" ) -func getMysqlAddress() (string, error) { - addressString := utils.EnvMysqlURL.GetValue() +func GetMysqlAddress(addressString string) (string, error) { if addressString == "" { return "", errors.New(fmt.Sprintf("Environment variable %s missing, exiting", utils.EnvMysqlURL.GetName())) } @@ -37,8 +36,7 @@ func getMysqlAddress() (string, error) { return config.FormatDSN(), nil } -func getPostgresAddress() (*url.URL, error) { - addressString := utils.EnvPostgresURL.GetValue() +func GetPostgresAddress(addressString string) (*url.URL, error) { if addressString == "" { return nil, errors.New(fmt.Sprintf("Environment variable %s missing, exiting", utils.EnvPostgresURL.GetName())) } @@ -51,8 +49,7 @@ func getPostgresAddress() (*url.URL, error) { return address, nil } -func getSqliteAddress() (*url.URL, error) { - path := utils.EnvSqlitePath.GetValue() +func GetSqliteAddress(path string) (*url.URL, error) { if path == "" { path = "photoview.db" } @@ -73,11 +70,11 @@ func getSqliteAddress() (*url.URL, error) { return address, nil } -func configureDatabase(config *gorm.Config) (*gorm.DB, error) { +func ConfigureDatabase(config *gorm.Config) (*gorm.DB, error) { var databaseDialect gorm.Dialector switch drivers.DatabaseDriver() { case drivers.DatabaseDriverMysql: - mysqlAddress, err := getMysqlAddress() + mysqlAddress, err := GetMysqlAddress(utils.EnvMysqlURL.GetValue()) if err != nil { return nil, err } @@ -85,7 +82,7 @@ func configureDatabase(config *gorm.Config) (*gorm.DB, error) { databaseDialect = gorm_mysql.Open(mysqlAddress) case drivers.DatabaseDriverSqlite: - sqliteAddress, err := getSqliteAddress() + sqliteAddress, err := GetSqliteAddress(utils.EnvSqlitePath.GetValue()) if err != nil { return nil, err } @@ -93,7 +90,7 @@ func configureDatabase(config *gorm.Config) (*gorm.DB, error) { databaseDialect = sqlite.Open(sqliteAddress.String()) case drivers.DatabaseDriverPostgres: - postgresAddress, err := getPostgresAddress() + postgresAddress, err := GetPostgresAddress(utils.EnvPostgresURL.GetValue()) if err != nil { return nil, err } @@ -126,7 +123,7 @@ func SetupDatabase() (*gorm.DB, error) { for retryCount := 1; retryCount <= 5; retryCount++ { var err error - db, err = configureDatabase(&config) + db, err = ConfigureDatabase(&config) if err == nil { sqlDB, dbErr := db.DB() if dbErr != nil { @@ -151,24 +148,26 @@ func SetupDatabase() (*gorm.DB, error) { return db, nil } -func MigrateDatabase(db *gorm.DB) error { - err := db.AutoMigrate( - &models.User{}, - &models.AccessToken{}, - &models.SiteInfo{}, - &models.Media{}, - &models.MediaURL{}, - &models.Album{}, - &models.MediaEXIF{}, - &models.VideoMetadata{}, - &models.ShareToken{}, - &models.UserMediaData{}, - &models.UserPreferences{}, +var database_models []interface{} = []interface{}{ + &models.User{}, + &models.AccessToken{}, + &models.SiteInfo{}, + &models.Media{}, + &models.MediaURL{}, + &models.Album{}, + &models.MediaEXIF{}, + &models.VideoMetadata{}, + &models.ShareToken{}, + &models.UserMediaData{}, + &models.UserPreferences{}, - // Face detection - &models.FaceGroup{}, - &models.ImageFace{}, - ) + // Face detection + &models.FaceGroup{}, + &models.ImageFace{}, +} + +func MigrateDatabase(db *gorm.DB) error { + err := db.AutoMigrate(database_models...) if err != nil { log.Printf("Auto migration failed: %v\n", err) @@ -188,3 +187,22 @@ func MigrateDatabase(db *gorm.DB) error { return nil } + +func ClearDatabase(db *gorm.DB) error { + err := db.Transaction(func(tx *gorm.DB) error { + sess := tx.Session(&gorm.Session{AllowGlobalUpdate: true}) + for _, model := range database_models { + if err := sess.Delete(model).Error; err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} diff --git a/api/graphql/models/models_test.go b/api/graphql/models/models_test.go new file mode 100644 index 00000000..615307e2 --- /dev/null +++ b/api/graphql/models/models_test.go @@ -0,0 +1,12 @@ +package models_test + +import ( + "os" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + os.Exit(test_utils.IntegrationTestRun(m)) +} diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go index 76d2dd7e..593f4fa7 100644 --- a/api/graphql/models/site_info.go +++ b/api/graphql/models/site_info.go @@ -16,32 +16,37 @@ func (SiteInfo) TableName() string { return "site_info" } +func DefaultSiteInfo() SiteInfo { + defaultConcurrentWorkers := 3 + if db_drivers.DatabaseDriver() == db_drivers.DatabaseDriverSqlite { + defaultConcurrentWorkers = 1 + } + + return SiteInfo{ + InitialSetup: true, + PeriodicScanInterval: 0, + ConcurrentWorkers: defaultConcurrentWorkers, + } +} + // GetSiteInfo gets the site info row from the database, and creates it if it does not exist func GetSiteInfo(db *gorm.DB) (*SiteInfo, error) { - var siteInfo SiteInfo + var siteInfo []*SiteInfo - if err := db.First(&siteInfo).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - defaultConcurrentWorkers := 3 - if db_drivers.DatabaseDriver() == db_drivers.DatabaseDriverSqlite { - defaultConcurrentWorkers = 1 - } - - siteInfo = SiteInfo{ - InitialSetup: true, - PeriodicScanInterval: 0, - ConcurrentWorkers: defaultConcurrentWorkers, - } - - if err := db.Create(&siteInfo).Error; err != nil { - return nil, errors.Wrap(err, "initialize site_info") - } - } else { - return nil, errors.Wrap(err, "get site info from database") - } + if err := db.Limit(1).Find(&siteInfo).Error; err != nil { + return nil, errors.Wrap(err, "get site info from database") } - return &siteInfo, nil + if len(siteInfo) == 0 { + newSiteInfo := DefaultSiteInfo() + + if err := db.Create(&newSiteInfo).Error; err != nil { + return nil, errors.Wrap(err, "initialize site_info") + } + + return &newSiteInfo, nil + } else { + return siteInfo[0], nil + } } diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go new file mode 100644 index 00000000..a947dce2 --- /dev/null +++ b/api/graphql/models/site_info_test.go @@ -0,0 +1,21 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestSiteInfo(t *testing.T) { + db := test_utils.DatabaseTest(t) + + site_info, err := models.GetSiteInfo(db) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, models.DefaultSiteInfo(), *site_info) +} diff --git a/api/graphql/models/user_test.go b/api/graphql/models/user_test.go new file mode 100644 index 00000000..a8232ffc --- /dev/null +++ b/api/graphql/models/user_test.go @@ -0,0 +1,25 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestUserRegistrationAuthorization(t *testing.T) { + db := test_utils.DatabaseTest(t) + + password := "1234" + user, err := models.RegisterUser(db, "admin", &password, true) + + if !assert.NoError(t, err) { + return + } + + assert.EqualValues(t, "admin", user.Username) + assert.NotNil(t, user.Password) + assert.NotEqualValues(t, "1234", user.Password) // should be hashed + assert.True(t, user.Admin) +} diff --git a/api/scanner/exif/exif_test.go b/api/scanner/exif/exif_test.go index f125be95..956b2aa8 100644 --- a/api/scanner/exif/exif_test.go +++ b/api/scanner/exif/exif_test.go @@ -2,19 +2,23 @@ package exif import ( "fmt" + "os" "path" "testing" "time" "github.com/barasher/go-exiftool" "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" "github.com/stretchr/testify/assert" ) +func TestMain(m *testing.M) { + os.Exit(test_utils.IntegrationTestRun(m)) +} + func TestExifParsers(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } + test_utils.FilesystemTest(t) parsers := []struct { name string diff --git a/api/scanner/scanner_test.go b/api/scanner/scanner_test.go new file mode 100644 index 00000000..9f76d568 --- /dev/null +++ b/api/scanner/scanner_test.go @@ -0,0 +1,12 @@ +package scanner_test + +import ( + "os" + "testing" + + "github.com/photoview/photoview/api/test_utils" +) + +func TestMain(m *testing.M) { + os.Exit(test_utils.UnitTestRun(m)) +} diff --git a/api/test_utils/integration_setup.go b/api/test_utils/integration_setup.go new file mode 100644 index 00000000..edf231ab --- /dev/null +++ b/api/test_utils/integration_setup.go @@ -0,0 +1,70 @@ +package test_utils + +import ( + "flag" + "log" + "path" + "runtime" + "testing" + + "github.com/joho/godotenv" + "gorm.io/gorm" +) + +type integration_options struct { + Database *bool + Filesystem *bool +} + +var integration_flags integration_options = integration_options{ + Database: flag.Bool("database", false, "run database integration tests"), + Filesystem: flag.Bool("filesystem", false, "run filesystem integration tests"), +} + +var test_dbm TestDBManager = TestDBManager{} + +func UnitTestRun(m *testing.M) int { + flag.Parse() + return m.Run() +} + +func IntegrationTestRun(m *testing.M) int { + flag.Parse() + + if *integration_flags.Database { + _, file, _, ok := runtime.Caller(0) + if !ok { + log.Fatal("could not get runtime file path") + } + + envPath := path.Join(path.Dir(file), "..", "testing.env") + + if err := godotenv.Load(envPath); err != nil { + log.Println("No testing.env file found") + } + } + + result := m.Run() + + test_dbm.Close() + + return result +} + +func FilesystemTest(t *testing.T) { + if !*integration_flags.Filesystem { + t.Skip("Filesystem integration tests disabled") + } +} + +func DatabaseTest(t *testing.T) *gorm.DB { + if !*integration_flags.Database { + t.Skip("Database integration tests disabled") + } + + if err := test_dbm.SetupOrReset(); err != nil { + t.Fatalf("failed to setup or reset test database: %v", err) + } + + return test_dbm.DB +} diff --git a/api/test_utils/test_db_manager.go b/api/test_utils/test_db_manager.go new file mode 100644 index 00000000..03028d48 --- /dev/null +++ b/api/test_utils/test_db_manager.go @@ -0,0 +1,60 @@ +package test_utils + +import ( + "github.com/photoview/photoview/api/database" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type TestDBManager struct { + DB *gorm.DB +} + +func (dbm *TestDBManager) SetupOrReset() error { + if dbm.DB == nil { + return dbm.setup() + } else { + return dbm.reset() + } +} + +func (dbm *TestDBManager) Close() error { + if dbm.DB == nil { + return nil + } + + sqlDB, err := dbm.DB.DB() + if err != nil { + return errors.Wrap(err, "get db instance when closing test database") + } + + sqlDB.Close() + dbm.DB = nil + + return nil +} + +func (dbm *TestDBManager) setup() error { + config := gorm.Config{} + db, err := database.ConfigureDatabase(&config) + if err != nil { + return errors.Wrap(err, "configure test database") + } + + if err := database.MigrateDatabase(db); err != nil { + return errors.Wrap(err, "migrate test database") + } + + dbm.DB = db + + return nil +} + +func (dbm *TestDBManager) reset() error { + + if err := database.ClearDatabase(dbm.DB); err != nil { + return errors.Wrap(err, "reset test database") + } + + return nil +} From 5ca4bd43bad317093d49f8e4ded6c70ed95132f4 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 20:52:55 +0200 Subject: [PATCH 04/13] Setup databases for github actions tests --- .github/mysql.testing.env | 2 ++ .github/postgres.testing.env | 2 ++ .github/sqlite.testing.env | 2 ++ .github/workflows/tests.yml | 51 +++++++++++++++++++++++++++++++++--- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 .github/mysql.testing.env create mode 100644 .github/postgres.testing.env create mode 100644 .github/sqlite.testing.env diff --git a/.github/mysql.testing.env b/.github/mysql.testing.env new file mode 100644 index 00000000..4b66e67e --- /dev/null +++ b/.github/mysql.testing.env @@ -0,0 +1,2 @@ +PHOTOVIEW_DATABASE_DRIVER=mysql +PHOTOVIEW_MYSQL_URL='photoview:photosecret@tcp(mariadb)/photoview_test' diff --git a/.github/postgres.testing.env b/.github/postgres.testing.env new file mode 100644 index 00000000..f8bdbe23 --- /dev/null +++ b/.github/postgres.testing.env @@ -0,0 +1,2 @@ +PHOTOVIEW_DATABASE_DRIVER=postgres +PHOTOVIEW_POSTGRES_URL='photoview:photosecret@tcp(postgres)/photoview_test' diff --git a/.github/sqlite.testing.env b/.github/sqlite.testing.env new file mode 100644 index 00000000..a24802ed --- /dev/null +++ b/.github/sqlite.testing.env @@ -0,0 +1,2 @@ +PHOTOVIEW_DATABASE_DRIVER=sqlite +PHOTOVIEW_SQLITE_PATH=photoview_test.db diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58528670..83edc15f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,15 +11,44 @@ jobs: name: Test API runs-on: ubuntu-20.04 + strategy: + matrix: + database: ['mysql', 'postgres', 'sqlite'] + + services: + mariadb: + image: mariadb:10.5 + env: + MYSQL_DATABASE: photoview_test + MYSQL_USER: photoview + MYSQL_PASSWORD: photosecret + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + postgres: + images: postgres:13.2 + env: + POSTGRES_USER: photoview + POSTGRES_PASSWORD: photosecret + POSTGRES_DB: photoview_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + defaults: run: working-directory: api steps: - - name: Set up Go 1.x + - name: Set up Go uses: actions/setup-go@v2 with: - go-version: ^1.13 + go-version: ^1.16 id: go - name: Check out code into the Go module directory @@ -43,8 +72,24 @@ jobs: - name: Build run: go build -v . + - name: Configure MySQL + if: ${{ matrix.database == 'mysql' }} + run: | + cp ../.github/mysql.testing.env testing.env + + - name: Configure Postgres + if: ${{ matrix.database == 'postgres' }} + run: | + cp ../.github/postgres.testing.env testing.env + + - name: Configure Sqlite + if: ${{ matrix.database == 'sqlite' }} + run: | + touch photoview_test.db + cp ../.github/sqlite.testing.env testing.env + - name: Test - run: go test -v ./... + run: go test ./... -v -database -filesystem -p 1 test-ui: name: Test UI From 6fd956d9c5222a95bc48b6c893e011ad0d690142 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 20:54:01 +0200 Subject: [PATCH 05/13] Fix "image" typo --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 83edc15f..62d3d76e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: --health-retries=5 postgres: - images: postgres:13.2 + image: postgres:13.2 env: POSTGRES_USER: photoview POSTGRES_PASSWORD: photosecret From e77f1bcd471d4202514c6a3e414313291e4c1e02 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:00:11 +0200 Subject: [PATCH 06/13] Fix mariadb env vars ghaction tests --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 62d3d76e..500a8230 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: MYSQL_DATABASE: photoview_test MYSQL_USER: photoview MYSQL_PASSWORD: photosecret + MYSQL_RANDOM_ROOT_PASSWORD: yes options: >- --health-cmd="mysqladmin ping" --health-interval=10s From d7ad507987c2052c7abb9b9e8a0233d59c285147 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:07:32 +0200 Subject: [PATCH 07/13] Fix postgres + add api test cache --- .github/postgres.testing.env | 2 +- .github/workflows/tests.yml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/postgres.testing.env b/.github/postgres.testing.env index f8bdbe23..6d50221a 100644 --- a/.github/postgres.testing.env +++ b/.github/postgres.testing.env @@ -1,2 +1,2 @@ PHOTOVIEW_DATABASE_DRIVER=postgres -PHOTOVIEW_POSTGRES_URL='photoview:photosecret@tcp(postgres)/photoview_test' +PHOTOVIEW_POSTGRES_URL=postgres://photoview:photosecret@postgres/photoview_test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 500a8230..2c3e19d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,6 +55,14 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v2 + - name: Cache Go dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Get C dependencies run: | sudo add-apt-repository ppa:strukturag/libheif From 435a9e0ab1eac133c36061a332ff09d29298796c Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:14:11 +0200 Subject: [PATCH 08/13] Add ports to ghaction test services --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c3e19d1..6d582fc5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,8 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=5 + ports: + - 3306:3306 postgres: image: postgres:13.2 @@ -40,6 +42,8 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + ports: + - 5432:5432 defaults: run: From 6bdd73c181306bd864761c0b2a9739716d02acce Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:19:45 +0200 Subject: [PATCH 09/13] ghaction test dbs try localhost --- .github/mysql.testing.env | 2 +- .github/postgres.testing.env | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/mysql.testing.env b/.github/mysql.testing.env index 4b66e67e..2f5ac13d 100644 --- a/.github/mysql.testing.env +++ b/.github/mysql.testing.env @@ -1,2 +1,2 @@ PHOTOVIEW_DATABASE_DRIVER=mysql -PHOTOVIEW_MYSQL_URL='photoview:photosecret@tcp(mariadb)/photoview_test' +PHOTOVIEW_MYSQL_URL='photoview:photosecret@tcp(127.0.0.1)/photoview_test' diff --git a/.github/postgres.testing.env b/.github/postgres.testing.env index 6d50221a..6099ebd7 100644 --- a/.github/postgres.testing.env +++ b/.github/postgres.testing.env @@ -1,2 +1,2 @@ PHOTOVIEW_DATABASE_DRIVER=postgres -PHOTOVIEW_POSTGRES_URL=postgres://photoview:photosecret@postgres/photoview_test +PHOTOVIEW_POSTGRES_URL=postgres://photoview:photosecret@127.0.0.1/photoview_test From 7b2720791e9f57c472b9ce6c805e2ec831e386f6 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:46:01 +0200 Subject: [PATCH 10/13] integrate codecov --- .github/workflows/tests.yml | 7 +- ui/package-lock.json | 250 ++++++++++++++++++++++++++++++++++++ ui/package.json | 3 + 3 files changed, 258 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d582fc5..ba21e844 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,7 +102,10 @@ jobs: cp ../.github/sqlite.testing.env testing.env - name: Test - run: go test ./... -v -database -filesystem -p 1 + run: go test ./... -v -database -filesystem -p 1 -coverprofile=coverage.txt -covermode=atomic + + - name: Upload coverage + run: bash <(curl -s https://codecov.io/bash) test-ui: name: Test UI @@ -128,4 +131,4 @@ jobs: run: npm clean-install - name: Test - run: npm test + run: npm test:ci diff --git a/ui/package-lock.json b/ui/package-lock.json index 221dce9e..6778d2f5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -66,6 +66,7 @@ "@types/url-join": "^4.0.0", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", + "codecov": "^3.8.1", "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", @@ -2685,6 +2686,15 @@ "react-dom": "*" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/aria-query": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", @@ -3233,6 +3243,18 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3333,6 +3355,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true, + "engines": { + "node": ">=0.6.10" + } + }, "node_modules/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -4504,6 +4535,25 @@ "node": ">= 0.12.0" } }, + "node_modules/codecov": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz", + "integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==", + "dev": true, + "dependencies": { + "argv": "0.0.2", + "ignore-walk": "3.0.3", + "js-yaml": "3.14.0", + "teeny-request": "6.0.1", + "urlgrey": "0.4.4" + }, + "bin": { + "codecov": "bin/codecov" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -6798,6 +6848,20 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-proxy/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -6818,6 +6882,28 @@ "npm": ">=1.3.7" } }, + "node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -6872,6 +6958,15 @@ "node": ">= 4" } }, + "node_modules/ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, "node_modules/immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -10442,6 +10537,15 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12701,6 +12805,15 @@ "node": ">=0.10.0" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, "node_modules/stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -12897,6 +13010,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "dev": true + }, "node_modules/styled-components": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.3.tgz", @@ -13106,6 +13225,19 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/teeny-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", + "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^3.3.2" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -13663,6 +13795,12 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "node_modules/urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -16334,6 +16472,12 @@ "@testing-library/dom": "^7.28.1" } }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, "@types/aria-query": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", @@ -16785,6 +16929,15 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -16868,6 +17021,12 @@ "sprintf-js": "~1.0.2" } }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, "aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -17808,6 +17967,19 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, + "codecov": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz", + "integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==", + "dev": true, + "requires": { + "argv": "0.0.2", + "ignore-walk": "3.0.3", + "js-yaml": "3.14.0", + "teeny-request": "6.0.1", + "urlgrey": "0.4.4" + } + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -19617,6 +19789,17 @@ } } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -19628,6 +19811,24 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true + } + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -19666,6 +19867,15 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -22470,6 +22680,12 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -24315,6 +24531,15 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "requires": { + "stubs": "^3.0.0" + } + }, "stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -24458,6 +24683,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "dev": true + }, "styled-components": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.3.tgz", @@ -24616,6 +24847,19 @@ } } }, + "teeny-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", + "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^3.3.2" + } + }, "temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -25051,6 +25295,12 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/ui/package.json b/ui/package.json index 83aabc91..0fd485b7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -54,10 +54,12 @@ "start": "node --experimental-modules build.mjs watch", "build": "NODE_ENV=production node --experimental-modules build.mjs", "test": "npm run lint && npm run jest", + "test:ci": "npm run lint && npm run jest:ci", "lint": "npm run lint:types & npm run lint:eslint", "lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", "lint:types": "tsc --noemit", "jest": "jest --verbose", + "jest:ci": "jest --verbose --ci --coverage && codecov", "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" }, @@ -77,6 +79,7 @@ "@types/url-join": "^4.0.0", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", + "codecov": "^3.8.1", "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", From 777747d5e7b7d1a7c4704aa41cc720c0efffa5fb Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 21:51:47 +0200 Subject: [PATCH 11/13] ghaction ui test fix cmd + add cache --- .github/workflows/tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba21e844..46bf740a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -122,6 +122,14 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Cache NPM dependencies + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: @@ -131,4 +139,4 @@ jobs: run: npm clean-install - name: Test - run: npm test:ci + run: npm run test:ci From 26ef725b7839718f5dc3665ecb3ea89a7555ba54 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 22:01:52 +0200 Subject: [PATCH 12/13] Use codecov-action instead --- .github/workflows/tests.yml | 41 +++--- ui/package-lock.json | 250 ------------------------------------ ui/package.json | 3 +- 3 files changed, 25 insertions(+), 269 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 46bf740a..d82a82b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -105,7 +105,9 @@ jobs: run: go test ./... -v -database -filesystem -p 1 -coverprofile=coverage.txt -covermode=atomic - name: Upload coverage - run: bash <(curl -s https://codecov.io/bash) + uses: codecov/codecov-action@v1 + with: + flags: api test-ui: name: Test UI @@ -120,23 +122,28 @@ jobs: node-version: [15.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Cache NPM dependencies - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + - name: Cache NPM dependencies + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm clean-install + - name: Install dependencies + run: npm clean-install - - name: Test - run: npm run test:ci + - name: Test + run: npm run test:ci + + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + flags: ui diff --git a/ui/package-lock.json b/ui/package-lock.json index 6778d2f5..221dce9e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -66,7 +66,6 @@ "@types/url-join": "^4.0.0", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", - "codecov": "^3.8.1", "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", @@ -2686,15 +2685,6 @@ "react-dom": "*" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/aria-query": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", @@ -3243,18 +3233,6 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3355,15 +3333,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true, - "engines": { - "node": ">=0.6.10" - } - }, "node_modules/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -4535,25 +4504,6 @@ "node": ">= 0.12.0" } }, - "node_modules/codecov": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz", - "integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==", - "dev": true, - "dependencies": { - "argv": "0.0.2", - "ignore-walk": "3.0.3", - "js-yaml": "3.14.0", - "teeny-request": "6.0.1", - "urlgrey": "0.4.4" - }, - "bin": { - "codecov": "bin/codecov" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -6848,20 +6798,6 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/http-proxy/node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -6882,28 +6818,6 @@ "npm": ">=1.3.7" } }, - "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -6958,15 +6872,6 @@ "node": ">= 4" } }, - "node_modules/ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "dependencies": { - "minimatch": "^3.0.4" - } - }, "node_modules/immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -10537,15 +10442,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true, - "engines": { - "node": "4.x || >=6.0.0" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12805,15 +12701,6 @@ "node": ">=0.10.0" } }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dev": true, - "dependencies": { - "stubs": "^3.0.0" - } - }, "node_modules/stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -13010,12 +12897,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "dev": true - }, "node_modules/styled-components": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.3.tgz", @@ -13225,19 +13106,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/teeny-request": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", - "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", - "dev": true, - "dependencies": { - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^4.0.0", - "node-fetch": "^2.2.0", - "stream-events": "^1.0.5", - "uuid": "^3.3.2" - } - }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -13795,12 +13663,6 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, - "node_modules/urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -16472,12 +16334,6 @@ "@testing-library/dom": "^7.28.1" } }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, "@types/aria-query": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz", @@ -16929,15 +16785,6 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -17021,12 +16868,6 @@ "sprintf-js": "~1.0.2" } }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, "aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -17967,19 +17808,6 @@ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, - "codecov": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz", - "integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==", - "dev": true, - "requires": { - "argv": "0.0.2", - "ignore-walk": "3.0.3", - "js-yaml": "3.14.0", - "teeny-request": "6.0.1", - "urlgrey": "0.4.4" - } - }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -19789,17 +19617,6 @@ } } }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -19811,24 +19628,6 @@ "sshpk": "^1.7.0" } }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "requires": { - "agent-base": "5", - "debug": "4" - }, - "dependencies": { - "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true - } - } - }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -19867,15 +19666,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, "immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -22680,12 +22470,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -24531,15 +24315,6 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dev": true, - "requires": { - "stubs": "^3.0.0" - } - }, "stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -24683,12 +24458,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "dev": true - }, "styled-components": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.3.tgz", @@ -24847,19 +24616,6 @@ } } }, - "teeny-request": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", - "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", - "dev": true, - "requires": { - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^4.0.0", - "node-fetch": "^2.2.0", - "stream-events": "^1.0.5", - "uuid": "^3.3.2" - } - }, "temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -25295,12 +25051,6 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/ui/package.json b/ui/package.json index 0fd485b7..c643bbeb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -59,7 +59,7 @@ "lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", "lint:types": "tsc --noemit", "jest": "jest --verbose", - "jest:ci": "jest --verbose --ci --coverage && codecov", + "jest:ci": "jest --verbose --ci --coverage", "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" }, @@ -79,7 +79,6 @@ "@types/url-join": "^4.0.0", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", - "codecov": "^3.8.1", "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", From 060e2008b76cc7e110df9db5459c3c372ac8d0b6 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 23 Apr 2021 23:07:18 +0200 Subject: [PATCH 13/13] Add tests for albums, users, site_info models --- api/graphql/models/album_test.go | 93 ++++++++++++++++++++++++++++ api/graphql/models/media_test.go | 41 ++++++++++-- api/graphql/models/site_info_test.go | 22 ++++++- api/graphql/models/user.go | 19 +----- api/graphql/models/user_test.go | 37 ++++++++++- 5 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 api/graphql/models/album_test.go diff --git a/api/graphql/models/album_test.go b/api/graphql/models/album_test.go new file mode 100644 index 00000000..e73e8692 --- /dev/null +++ b/api/graphql/models/album_test.go @@ -0,0 +1,93 @@ +package models_test + +import ( + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestAlbumGetChildren(t *testing.T) { + db := test_utils.DatabaseTest(t) + + rootAlbum := models.Album{ + Title: "root", + Path: "/photos", + } + + if !assert.NoError(t, db.Save(&rootAlbum).Error) { + return + } + + children := []models.Album{ + { + Title: "child1", + Path: "/photos/child1", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "child2", + Path: "/photos/child2", + ParentAlbumID: &rootAlbum.ID, + }, + { + Title: "not_child", + Path: "/videos", + }, + } + + if !assert.NoError(t, db.Save(&children).Error) { + return + } + + sub_child := models.Album{ + Title: "subchild", + Path: "/photos/child1/subchild", + ParentAlbumID: &children[0].ID, + } + + if !assert.NoError(t, db.Save(&sub_child).Error) { + return + } + + root_children, err := rootAlbum.GetChildren(db, nil) + if !assert.NoError(t, err) { + return + } + + expected_children := []*models.Album{ + { + Title: "root", + Path: "/photos", + }, + { + Title: "child1", + Path: "/photos/child1", + }, + { + Title: "child2", + Path: "/photos/child2", + }, + { + Title: "subchild", + Path: "/photos/child1/subchild", + }, + } + + assert.Equal(t, len(expected_children), len(root_children)) + + for _, expected := range expected_children { + found_expected := false + for _, item := range root_children { + if item.Title == expected.Title && item.Path == expected.Path { + found_expected = true + break + } + } + if !found_expected { + assert.Failf(t, "root children did not match", "expected to find item: %v", expected) + } + } + +} diff --git a/api/graphql/models/media_test.go b/api/graphql/models/media_test.go index 39ffac72..2cc40402 100644 --- a/api/graphql/models/media_test.go +++ b/api/graphql/models/media_test.go @@ -1,7 +1,11 @@ -package models +package models_test import ( + "fmt" "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/stretchr/testify/assert" ) func TestSanitizeMediaName(t *testing.T) { @@ -12,9 +16,36 @@ func TestSanitizeMediaName(t *testing.T) { {"..\\/", "__"}, } - for i, test := range tests { - if SanitizeMediaName(test[0]) != test[1] { - t.Errorf("SanitizeMediaName test %d failed: got '%s', expected '%s'", i, test[1], SanitizeMediaName(test[0])) - } + for _, test := range tests { + t.Run(fmt.Sprintf("sanitize: %s", test[0]), func(t *testing.T) { + assert.Equal(t, test[1], models.SanitizeMediaName(test[0])) + }) } } + +func TestMediaURLCachePath(t *testing.T) { + mediaUrl := models.MediaURL{} + mediaUrl.Media = nil + + _, err := mediaUrl.CachedPath() + assert.EqualError(t, err, "mediaURL.Media is nil") + + mediaUrl = models.MediaURL{ + Purpose: models.PhotoThumbnail, + MediaID: 1, + Media: &models.Media{ + Model: models.Model{ + ID: 1, + }, + Title: "media.jpg", + AlbumID: 2, + }, + MediaName: "media_thumb.jpg", + } + + path, err := mediaUrl.CachedPath() + + assert.NoError(t, err) + assert.Equal(t, "media_cache/2/1/media_thumb.jpg", path) + +} diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go index a947dce2..30ce0628 100644 --- a/api/graphql/models/site_info_test.go +++ b/api/graphql/models/site_info_test.go @@ -6,16 +6,36 @@ import ( "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/test_utils" "github.com/stretchr/testify/assert" + "gorm.io/gorm" ) func TestSiteInfo(t *testing.T) { db := test_utils.DatabaseTest(t) site_info, err := models.GetSiteInfo(db) - if !assert.NoError(t, err) { return } assert.Equal(t, models.DefaultSiteInfo(), *site_info) + + site_info.InitialSetup = false + site_info.PeriodicScanInterval = 360 + site_info.ConcurrentWorkers = 10 + + if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) { + return + } + + site_info, err = models.GetSiteInfo(db) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, models.SiteInfo{ + InitialSetup: false, + PeriodicScanInterval: 360, + ConcurrentWorkers: 10, + }, *site_info) + } diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index 244221b4..b3879a19 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -108,14 +108,9 @@ func ValidRootPath(rootPath string) bool { } func RegisterUser(db *gorm.DB, username string, password *string, admin bool) (*User, error) { - // if !ValidRootPath(rootPath) { - // return nil, ErrorInvalidRootPath - // } - user := User{ Username: username, - // RootPath: rootPath, - Admin: admin, + Admin: admin, } if password != nil { @@ -148,11 +143,6 @@ func (user *User) GenerateAccessToken(db *gorm.DB) (*AccessToken, error) { token_value := string(bytes) expire := time.Now().Add(14 * 24 * time.Hour) - // expireString := expire.UTC().Format("2006-01-02 15:04:05") - - // if _, err := database.Exec("INSERT INTO access_token (value, expire, user_id) VALUES (?, ?, ?)", token_value, expireString, user.UserID); err != nil { - // return nil, err - // } token := AccessToken{ UserID: user.ID, @@ -183,14 +173,11 @@ func (user *User) FillAlbums(db *gorm.DB) error { } func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) { - - // user.QueryUserAlbums(db, db.Where("id = ?", album.ID)) - // TODO: Implement this - return true, nil + panic("not implemented") } func (user *User) OwnsMedia(db *gorm.DB, media *Media) (bool, error) { // TODO: implement this - return true, nil + panic("not implemented") } diff --git a/api/graphql/models/user_test.go b/api/graphql/models/user_test.go index a8232ffc..9f945132 100644 --- a/api/graphql/models/user_test.go +++ b/api/graphql/models/user_test.go @@ -2,6 +2,7 @@ package models_test import ( "testing" + "time" "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/test_utils" @@ -13,13 +14,47 @@ func TestUserRegistrationAuthorization(t *testing.T) { password := "1234" user, err := models.RegisterUser(db, "admin", &password, true) - if !assert.NoError(t, err) { return } + assert.NotNil(t, user) assert.EqualValues(t, "admin", user.Username) assert.NotNil(t, user.Password) assert.NotEqualValues(t, "1234", user.Password) // should be hashed assert.True(t, user.Admin) + + user, err = models.AuthorizeUser(db, "admin", "1234") + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, user) + assert.EqualValues(t, "admin", user.Username) + +} + +func TestAccessToken(t *testing.T) { + db := test_utils.DatabaseTest(t) + + pass := "" + user := models.User{ + Username: "user1", + Password: &pass, + Admin: false, + } + + if !assert.NoError(t, db.Save(&user).Error) { + return + } + + access_token, err := user.GenerateAccessToken(db) + if !assert.NoError(t, err) { + return + } + + assert.NotNil(t, access_token) + assert.Equal(t, user.ID, access_token.UserID) + assert.NotEmpty(t, access_token.Value) + assert.True(t, access_token.Expire.After(time.Now())) }