From a7403c91ce23bf074baaee687403970f34ef9cee Mon Sep 17 00:00:00 2001 From: louis Date: Sat, 7 Oct 2023 22:20:34 +0200 Subject: [PATCH 1/7] Replace BoltDB Storage to Gorm SQL --- .gitignore | 2 +- Dockerfile | 1 + cmd/root.go | 16 +- cmd/run.go | 46 +- config.yml.dist | 6 +- go.mod | 17 +- go.sum | 37 +- internal/api/api.go | 4 +- internal/api/api_test.go | 15 +- internal/api/features_test.go | 2 +- internal/api/feed.go | 2 +- internal/api/feed_test.go | 14 +- internal/api/init.go | 4 +- internal/api/init_test.go | 14 +- internal/api/media_test.go | 4 +- internal/api/messages.go | 2 +- internal/api/messages_test.go | 30 +- internal/api/middleware/auth/auth_test.go | 10 +- internal/api/middleware/logger/logger.go | 6 +- internal/api/middleware/me/me_test.go | 6 +- .../api/middleware/message/message_test.go | 6 +- internal/api/middleware/ticker/ticker.go | 7 +- internal/api/middleware/ticker/ticker_test.go | 16 +- internal/api/middleware/user/user_test.go | 6 +- internal/api/response/message.go | 4 +- internal/api/response/settings.go | 17 +- internal/api/response/ticker.go | 2 +- internal/api/response/timeline.go | 2 +- internal/api/response/upload.go | 2 +- internal/api/response/user.go | 5 +- internal/api/settings.go | 26 +- internal/api/settings_test.go | 46 +- internal/api/tickers.go | 25 +- internal/api/tickers_test.go | 99 +-- internal/api/timeline_test.go | 10 +- internal/api/upload.go | 6 +- internal/api/upload_test.go | 22 +- internal/api/user.go | 27 +- internal/api/user_test.go | 50 +- internal/bridge/mastodon.go | 8 +- .../bridge/{bridge_mock.go => mock_Bridge.go} | 11 +- internal/config/config.go | 21 +- internal/logger/gorm.go | 50 ++ internal/logger/logrus.go | 21 + internal/storage/db_test.go | 15 - internal/storage/message.go | 34 +- internal/storage/message_test.go | 3 +- .../{storage_mock.go => mock_Storage.go} | 286 ++++----- internal/storage/setting.go | 48 +- internal/storage/sql_storage.go | 282 +++++++++ internal/storage/sql_storage_test.go | 592 ++++++++++++++++++ internal/storage/storage.go | 15 +- internal/storage/storm_storage.go | 367 ----------- internal/storage/storm_storage_test.go | 400 ------------ internal/storage/ticker.go | 90 +-- internal/storage/upload.go | 26 +- internal/storage/user.go | 33 +- internal/util/slice.go | 8 +- 58 files changed, 1580 insertions(+), 1346 deletions(-) rename internal/bridge/{bridge_mock.go => mock_Bridge.go} (87%) create mode 100644 internal/logger/gorm.go create mode 100644 internal/logger/logrus.go delete mode 100644 internal/storage/db_test.go rename internal/storage/{storage_mock.go => mock_Storage.go} (64%) create mode 100644 internal/storage/sql_storage.go create mode 100644 internal/storage/sql_storage_test.go delete mode 100644 internal/storage/storm_storage.go delete mode 100644 internal/storage/storm_storage_test.go diff --git a/.gitignore b/.gitignore index 64208280..2eb83384 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ docs/docs.go docs/swagger.json docs/swagger.yaml dist/ -ticker.db +*.db site/ uploads/ diff --git a/Dockerfile b/Dockerfile index 1c86273f..625b4167 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,3 +24,4 @@ COPY ticker /ticker USER ticker:ticker ENTRYPOINT ["/ticker"] +CMD [ "run" ] diff --git a/cmd/root.go b/cmd/root.go index 2dacca83..5143dfb3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,10 +4,11 @@ import ( "fmt" "os" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/systemli/ticker/internal/bridge" "github.com/systemli/ticker/internal/config" + "github.com/systemli/ticker/internal/logger" ) var ( @@ -17,6 +18,8 @@ var ( configPath string cfg config.Config + log = logrus.New() + rootCmd = &cobra.Command{ Use: "ticker", Short: "Service to distribute short messages", @@ -41,16 +44,7 @@ func initConfig() { } } - lvl, err := log.ParseLevel(cfg.LogLevel) - if err != nil { - panic(err) - } - if cfg.LogFormat == "json" { - log.SetFormatter(&log.JSONFormatter{}) - } else { - log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) - } - log.SetLevel(lvl) + log = logger.NewLogrus(cfg.LogLevel, cfg.LogFormat) } func Execute() { diff --git a/cmd/run.go b/cmd/run.go index d7a5416c..3c69973a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -9,11 +9,15 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sethvargo/go-password/password" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/systemli/ticker/internal/api" "github.com/systemli/ticker/internal/config" + "github.com/systemli/ticker/internal/logger" "github.com/systemli/ticker/internal/storage" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) var ( @@ -40,8 +44,41 @@ var ( log.Fatal(http.ListenAndServe(cfg.MetricsListen, nil)) }() - store := storage.NewStormStorage(cfg.Database, cfg.UploadPath) - router := api.API(cfg, store) + var dialector gorm.Dialector + switch cfg.Database.Type { + case "sqlite": + dialector = sqlite.Open(cfg.Database.DSN) + case "mysql": + dialector = mysql.Open(cfg.Database.DSN) + case "postgres": + dialector = postgres.Open(cfg.Database.DSN) + default: + log.Fatalf("unknown database type %s", cfg.Database.Type) + } + + db, err := gorm.Open(dialector, &gorm.Config{ + Logger: logger.NewGormLogger(log), + }) + if err != nil { + log.WithError(err).Fatal("could not connect to database") + } + store := storage.NewSqlStorage(db, cfg.UploadPath) + err = db.AutoMigrate( + &storage.Attachment{}, + &storage.Message{}, + &storage.Setting{}, + &storage.Ticker{}, + &storage.TickerInformation{}, + &storage.TickerMastodon{}, + &storage.TickerTelegram{}, + &storage.Upload{}, + &storage.User{}, + ) + if err != nil { + log.WithError(err).Fatal("could not migrate database") + } + + router := api.API(cfg, store, log) server := &http.Server{ Addr: cfg.Listen, Handler: router, @@ -84,7 +121,8 @@ func firstRun(store storage.Storage, config config.Config) { log.Fatal(err) } - user, err := storage.NewAdminUser(config.Initiator, pw) + user, err := storage.NewUser(config.Initiator, pw) + user.IsSuperAdmin = true if err != nil { log.Fatal("could not create first user") } diff --git a/config.yml.dist b/config.yml.dist index 1490cdf3..0c8884f0 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -6,8 +6,10 @@ log_level: "error" log_format: "json" # initiator is the email for the first admin user (see password in logs) initiator: "admin@systemli.org" -# database is the path to the bolt file -database: "ticker.db" +# configuration for the database +database: + type: "sqlite" # postgres, mysql, sqlite + dsn: "ticker.db" # postgres: "host=localhost port=5432 user=ticker dbname=ticker password=ticker sslmode=disable" # secret used for JSON Web Tokens secret: "slorp-panfil-becall-dorp-hashab-incus-biter-lyra-pelage-sarraf-drunk" # telegram configuration diff --git a/go.mod b/go.mod index b2618afd..ca36f1c9 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,6 @@ module github.com/systemli/ticker require ( github.com/appleboy/gin-jwt/v2 v2.9.1 - github.com/asdine/storm v2.1.2+incompatible github.com/disintegration/imaging v1.6.2 github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/size v0.0.0-20220102055520-f75bacbc2df3 @@ -20,6 +19,10 @@ require ( github.com/swaggo/swag v1.16.2 github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f golang.org/x/crypto v0.14.0 + gorm.io/driver/mysql v1.5.1 + gorm.io/driver/postgres v1.5.2 + gorm.io/driver/sqlite v1.5.3 + gorm.io/gorm v1.25.4 ) require ( @@ -42,6 +45,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -50,6 +54,11 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect @@ -57,6 +66,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -86,16 +96,11 @@ require ( ) require ( - github.com/DataDog/zstd v1.4.0 // indirect - github.com/Sereal/Sereal v0.0.0-20190606082811-cf1bab6c7a3a // indirect github.com/goccy/go-json v0.10.2 - github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/feeds v1.1.1 github.com/mattn/go-mastodon v0.0.6 github.com/onsi/ginkgo/v2 v2.12.1 github.com/onsi/gomega v1.28.0 - github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - go.etcd.io/bbolt v1.3.6 // indirect golang.org/x/image v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 4b7f1f31..528fcb31 100644 --- a/go.sum +++ b/go.sum @@ -38,22 +38,16 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo= -github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Sereal/Sereal v0.0.0-20190606082811-cf1bab6c7a3a h1:r3TT14Z4rWYwW2U8F1DkyM0dpvafwUsye+X3j1J5U3g= -github.com/Sereal/Sereal v0.0.0-20190606082811-cf1bab6c7a3a/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxGWVZs= github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= -github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= -github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -130,6 +124,8 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= @@ -168,8 +164,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -219,6 +213,16 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -256,6 +260,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-mastodon v0.0.6 h1:lqU1sOeeIapaDsDUL6udDZIzMb2Wqapo347VZlaOzf0= github.com/mattn/go-mastodon v0.0.6/go.mod h1:cg7RFk2pcUfHZw/IvKe1FUzmlq5KnLFqs7eV2PHplV8= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -351,15 +357,11 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= -github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -505,7 +507,6 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -625,7 +626,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -712,6 +712,15 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= +gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/api/api.go b/internal/api/api.go index 8e079fb8..53949081 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -41,7 +41,7 @@ type handler struct { // @host localhost:8080 // @BasePath /v1 -func API(config config.Config, storage storage.Storage) *gin.Engine { +func API(config config.Config, storage storage.Storage, log *logrus.Logger) *gin.Engine { handler := handler{ config: config, storage: storage, @@ -51,7 +51,7 @@ func API(config config.Config, storage storage.Storage) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() - r.Use(logger.Logger(config.LogLevel)) + r.Use(logger.Logger(log)) r.Use(gin.Recovery()) r.Use(cors.NewCORS()) r.Use(prometheus.NewPrometheus()) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 4333b3c2..34b4ac68 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -16,9 +16,12 @@ import ( "github.com/stretchr/testify/mock" "github.com/systemli/ticker/internal/api/response" "github.com/systemli/ticker/internal/config" + "github.com/systemli/ticker/internal/logger" "github.com/systemli/ticker/internal/storage" ) +var l = logger.NewLogrus("debug", "text") + func init() { logrus.SetOutput(io.Discard) gin.SetMode(gin.TestMode) @@ -26,8 +29,8 @@ func init() { func TestHealthz(t *testing.T) { c := config.NewConfig() - s := &storage.MockTickerStorage{} - r := API(c, s) + s := &storage.MockStorage{} + r := API(c, s, l) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) w := httptest.NewRecorder() @@ -39,11 +42,11 @@ func TestHealthz(t *testing.T) { func TestLoginNotSuccessful(t *testing.T) { c := config.NewConfig() - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} user := storage.User{} user.UpdatePassword("password") s.On("FindUserByEmail", mock.Anything).Return(user, nil) - r := API(c, s) + r := API(c, s, l) body := `{"username":"louis@systemli.org","password":"WRONG"}` req := httptest.NewRequest(http.MethodPost, "/v1/admin/login", strings.NewReader(body)) @@ -62,11 +65,11 @@ func TestLoginNotSuccessful(t *testing.T) { func TestLoginSuccessful(t *testing.T) { c := config.NewConfig() - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} user := storage.User{} user.UpdatePassword("password") s.On("FindUserByEmail", mock.Anything).Return(user, nil) - r := API(c, s) + r := API(c, s, l) body := `{"username":"louis@systemli.org","password":"password"}` req := httptest.NewRequest(http.MethodPost, "/v1/admin/login", strings.NewReader(body)) diff --git a/internal/api/features_test.go b/internal/api/features_test.go index 28662975..9b8f06dd 100644 --- a/internal/api/features_test.go +++ b/internal/api/features_test.go @@ -18,7 +18,7 @@ func init() { func TestGetFeatures(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, diff --git a/internal/api/feed.go b/internal/api/feed.go index b3b51b6d..9f85a6c8 100644 --- a/internal/api/feed.go +++ b/internal/api/feed.go @@ -52,7 +52,7 @@ func buildFeed(ticker storage.Ticker, messages []storage.Message) *feeds.Feed { for _, message := range messages { item := &feeds.Item{ Id: strconv.Itoa(message.ID), - Created: message.CreationDate, + Created: message.CreatedAt, Description: message.Text, Title: message.Text, Link: &feeds.Link{}, diff --git a/internal/api/feed_test.go b/internal/api/feed_test.go index be14b443..ccd3c966 100644 --- a/internal/api/feed_test.go +++ b/internal/api/feed_test.go @@ -5,7 +5,6 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" @@ -22,7 +21,7 @@ func init() { func TestGetFeedTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -38,7 +37,7 @@ func TestGetFeedMessageFetchError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("storage error")) h := handler{ @@ -58,18 +57,17 @@ func TestGetFeed(t *testing.T) { ticker := storage.Ticker{ ID: 1, Title: "Title", - Information: storage.Information{ + Information: storage.TickerInformation{ URL: "https://demoticker.org", Author: "Author", Email: "author@demoticker.org", }, } c.Set("ticker", ticker) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} message := storage.Message{ - Ticker: ticker.ID, - CreationDate: time.Now(), - Text: "Text", + TickerID: ticker.ID, + Text: "Text", } s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{message}, nil) diff --git a/internal/api/init.go b/internal/api/init.go index 95ff70b4..cb40ff4a 100644 --- a/internal/api/init.go +++ b/internal/api/init.go @@ -22,7 +22,7 @@ import ( // @Router /init [get] func (h *handler) GetInit(c *gin.Context) { settings := response.Settings{ - RefreshInterval: h.storage.GetRefreshIntervalSetting().Value.(float64), + RefreshInterval: h.storage.GetRefreshIntervalSettings().RefreshInterval, } domain, err := helper.GetDomain(c) if err != nil { @@ -32,7 +32,7 @@ func (h *handler) GetInit(c *gin.Context) { ticker, err := h.storage.FindTickerByDomain(domain) if err != nil || !ticker.Active { - settings.InactiveSettings = h.storage.GetInactiveSetting().Value + settings.InactiveSettings = h.storage.GetInactiveSettings() c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"ticker": nil, "settings": settings})) return } diff --git a/internal/api/init_test.go b/internal/api/init_test.go index 09e3cbf7..30cf5282 100644 --- a/internal/api/init_test.go +++ b/internal/api/init_test.go @@ -19,8 +19,8 @@ func TestGetInit(t *testing.T) { ticker := storage.NewTicker() ticker.Active = true - s := &storage.MockTickerStorage{} - s.On("GetRefreshIntervalSetting").Return(storage.Setting{Value: float64(10000)}) + s := &storage.MockStorage{} + s.On("GetRefreshIntervalSettings").Return(storage.DefaultRefreshIntervalSettings()) s.On("FindTickerByDomain", mock.AnythingOfType("string")).Return(ticker, nil) h := handler{ @@ -38,8 +38,8 @@ func TestGetInitInvalidDomain(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/v1/init", nil) - s := &storage.MockTickerStorage{} - s.On("GetRefreshIntervalSetting").Return(storage.Setting{Value: float64(10000)}) + s := &storage.MockStorage{} + s.On("GetRefreshIntervalSettings").Return(storage.DefaultRefreshIntervalSettings()) h := handler{ storage: s, @@ -57,9 +57,9 @@ func TestGetInitInactiveTicker(t *testing.T) { c.Request = httptest.NewRequest(http.MethodGet, "/v1/init?origin=demoticker.org", nil) ticker := storage.NewTicker() - s := &storage.MockTickerStorage{} - s.On("GetRefreshIntervalSetting").Return(storage.Setting{Value: float64(10000)}) - s.On("GetInactiveSetting").Return(storage.DefaultInactiveSetting()) + s := &storage.MockStorage{} + s.On("GetRefreshIntervalSettings").Return(storage.DefaultRefreshIntervalSettings()) + s.On("GetInactiveSettings").Return(storage.DefaultInactiveSettings()) s.On("FindTickerByDomain", mock.AnythingOfType("string")).Return(ticker, nil) h := handler{ diff --git a/internal/api/media_test.go b/internal/api/media_test.go index 368c214f..ef20f7b9 100644 --- a/internal/api/media_test.go +++ b/internal/api/media_test.go @@ -23,7 +23,7 @@ func TestGetMedia(t *testing.T) { c.Request = httptest.NewRequest(http.MethodGet, "/media", nil) upload := storage.NewUpload("image.jpg", "image/jpeg", 1) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUploadByUUID", mock.Anything).Return(upload, nil) s.On("UploadPath").Return("./uploads") @@ -42,7 +42,7 @@ func TestGetMediaNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUploadByUUID", mock.Anything).Return(storage.Upload{}, errors.New("not found")) h := handler{ diff --git a/internal/api/messages.go b/internal/api/messages.go index 34c10d3d..3bec1ef4 100644 --- a/internal/api/messages.go +++ b/internal/api/messages.go @@ -69,7 +69,7 @@ func (h *handler) PostMessage(c *gin.Context) { message := storage.NewMessage() message.Text = body.Text - message.Ticker = ticker.ID + message.TickerID = ticker.ID message.GeoInformation = body.GeoInformation message.AddAttachments(uploads) diff --git a/internal/api/messages_test.go b/internal/api/messages_test.go index 32bc62cf..0646057b 100644 --- a/internal/api/messages_test.go +++ b/internal/api/messages_test.go @@ -23,7 +23,7 @@ func TestGetMessagesTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.AddParam("tickerID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -38,7 +38,7 @@ func TestGetMessagesStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("storage error")) h := handler{ storage: s, @@ -54,7 +54,7 @@ func TestGetMessagesEmptyResult(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("not found")) h := handler{ storage: s, @@ -70,7 +70,7 @@ func TestGetMessages(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, nil) h := handler{ storage: s, @@ -85,7 +85,7 @@ func TestGetMessages(t *testing.T) { func TestGetMessageNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -100,7 +100,7 @@ func TestGetMessage(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("message", storage.Message{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -114,7 +114,7 @@ func TestGetMessage(t *testing.T) { func TestPostMessageTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -130,7 +130,7 @@ func TestPostMessageFormError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -149,7 +149,7 @@ func TestPostMessageUploadsNotFound(t *testing.T) { c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") c.AddParam("tickerID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUploadsByIDs", mock.Anything).Return([]storage.Upload{}, errors.New("storage error")) h := handler{ storage: s, @@ -169,7 +169,7 @@ func TestPostMessageStorageError(t *testing.T) { c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") c.AddParam("tickerID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUploadsByIDs", mock.Anything).Return([]storage.Upload{}, nil) s.On("SaveMessage", mock.Anything).Return(errors.New("storage error")) b := &bridge.MockBridge{} @@ -192,7 +192,7 @@ func TestPostMessage(t *testing.T) { json := `{"text":"text","attachments":[1]}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUploadsByIDs", mock.Anything).Return([]storage.Upload{}, nil) s.On("SaveMessage", mock.Anything).Return(nil) b := &bridge.MockBridge{} @@ -211,7 +211,7 @@ func TestPostMessage(t *testing.T) { func TestDeleteMessageTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -226,7 +226,7 @@ func TestDeleteMessageMessageNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -242,7 +242,7 @@ func TestDeleteMessageStorageError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.Set("message", storage.Message{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessage", mock.Anything).Return(errors.New("storage error")) b := &bridge.MockBridge{} b.On("Delete", mock.Anything, mock.Anything).Return(nil) @@ -262,7 +262,7 @@ func TestDeleteMessage(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.Set("message", storage.Message{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessage", mock.Anything).Return(nil) b := &bridge.MockBridge{} b.On("Delete", mock.Anything, mock.Anything).Return(nil) diff --git a/internal/api/middleware/auth/auth_test.go b/internal/api/middleware/auth/auth_test.go index ee011974..52daaaa3 100644 --- a/internal/api/middleware/auth/auth_test.go +++ b/internal/api/middleware/auth/auth_test.go @@ -28,7 +28,7 @@ var _ = Describe("Auth Middleware", func() { When("Authenticator", func() { Context("empty form is sent", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} authenticator := Authenticator(mockStorage) c, _ := gin.CreateTestContext(httptest.NewRecorder()) @@ -42,7 +42,7 @@ var _ = Describe("Auth Middleware", func() { }) Context("user not found", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByEmail", mock.Anything).Return(storage.User{}, errors.New("not found")) authenticator := Authenticator(mockStorage) @@ -65,7 +65,7 @@ var _ = Describe("Auth Middleware", func() { EncryptedPassword: string(hashedPassword), IsSuperAdmin: false, } - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByEmail", mock.Anything).Return(user, nil) authenticator := Authenticator(mockStorage) @@ -92,7 +92,7 @@ var _ = Describe("Auth Middleware", func() { When("Authorizator", func() { Context("storage returns no user", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByID", mock.Anything).Return(storage.User{}, errors.New("user not found")) authorizator := Authorizator(mockStorage) rr := httptest.NewRecorder() @@ -105,7 +105,7 @@ var _ = Describe("Auth Middleware", func() { }) Context("storage returns a user", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByID", mock.Anything).Return(storage.User{ID: 1}, nil) authorizator := Authorizator(mockStorage) rr := httptest.NewRecorder() diff --git a/internal/api/middleware/logger/logger.go b/internal/api/middleware/logger/logger.go index c272a77e..30972dd1 100644 --- a/internal/api/middleware/logger/logger.go +++ b/internal/api/middleware/logger/logger.go @@ -6,10 +6,6 @@ import ( ginlogrus "github.com/toorop/gin-logrus" ) -func Logger(level string) gin.HandlerFunc { - lvl, _ := logrus.ParseLevel(level) - logger := logrus.New() - logger.SetLevel(lvl) - +func Logger(logger *logrus.Logger) gin.HandlerFunc { return ginlogrus.Logger(logger) } diff --git a/internal/api/middleware/me/me_test.go b/internal/api/middleware/me/me_test.go index 3ff95b01..a91fe767 100644 --- a/internal/api/middleware/me/me_test.go +++ b/internal/api/middleware/me/me_test.go @@ -21,7 +21,7 @@ func TestStorage(t *testing.T) { var _ = Describe("Me Middleware", func() { When("id is not present", func() { It("should return an error", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mw := MeMiddleware(mockStorage) gin.SetMode(gin.ReleaseMode) w := httptest.NewRecorder() @@ -35,7 +35,7 @@ var _ = Describe("Me Middleware", func() { When("id is present", func() { Context("user not found", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByID", mock.Anything).Return(storage.User{}, errors.New("not found")) mw := MeMiddleware(mockStorage) gin.SetMode(gin.ReleaseMode) @@ -49,7 +49,7 @@ var _ = Describe("Me Middleware", func() { }) Context("user found", func() { - mockStorage := &storage.MockTickerStorage{} + mockStorage := &storage.MockStorage{} mockStorage.On("FindUserByID", mock.Anything).Return(storage.User{ID: 1}, nil) mw := MeMiddleware(mockStorage) gin.SetMode(gin.ReleaseMode) diff --git a/internal/api/middleware/message/message_test.go b/internal/api/middleware/message/message_test.go index a912500d..f0f48464 100644 --- a/internal/api/middleware/message/message_test.go +++ b/internal/api/middleware/message/message_test.go @@ -20,7 +20,7 @@ func TestPrefetchMessageParamMissing(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} mw := PrefetchMessage(s) mw(c) @@ -33,7 +33,7 @@ func TestPrefetchMessageStorageError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.AddParam("messageID", "1") c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessage", mock.Anything, mock.Anything).Return(storage.Message{}, errors.New("storage error")) mw := PrefetchMessage(s) @@ -47,7 +47,7 @@ func TestPrefetchMessage(t *testing.T) { c, _ := gin.CreateTestContext(w) c.AddParam("messageID", "1") c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} message := storage.Message{ID: 1} s.On("FindMessage", mock.Anything, mock.Anything).Return(message, nil) mw := PrefetchMessage(s) diff --git a/internal/api/middleware/ticker/ticker.go b/internal/api/middleware/ticker/ticker.go index f02bb03d..a087f70e 100644 --- a/internal/api/middleware/ticker/ticker.go +++ b/internal/api/middleware/ticker/ticker.go @@ -21,13 +21,18 @@ func PrefetchTicker(s storage.Storage) gin.HandlerFunc { } if !user.IsSuperAdmin { - if !util.Contains(user.Tickers, tickerID) { + var tickerIDs []int + for _, t := range user.Tickers { + tickerIDs = append(tickerIDs, t.ID) + } + if !util.Contains(tickerIDs, tickerID) { c.JSON(http.StatusForbidden, response.ErrorResponse(response.CodeInsufficientPermissions, response.InsufficientPermissions)) return } } ticker, err := s.FindTickerByID(tickerID) + if err != nil { c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeNotFound, response.TickerNotFound)) return diff --git a/internal/api/middleware/ticker/ticker_test.go b/internal/api/middleware/ticker/ticker_test.go index 8f9f6b9c..aeabbca4 100644 --- a/internal/api/middleware/ticker/ticker_test.go +++ b/internal/api/middleware/ticker/ticker_test.go @@ -20,7 +20,7 @@ func TestPrefetchTickerParamMissing(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} mw := PrefetchTicker(s) mw(c) @@ -32,8 +32,8 @@ func TestPrefetchTickerNoPermission(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.AddParam("tickerID", "1") - c.Set("me", storage.User{IsSuperAdmin: false, Tickers: []int{2}}) - s := &storage.MockTickerStorage{} + c.Set("me", storage.User{IsSuperAdmin: false, Tickers: []storage.Ticker{{ID: 2}}}) + s := &storage.MockStorage{} mw := PrefetchTicker(s) mw(c) @@ -46,7 +46,7 @@ func TestPrefetchTickerStorageError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.AddParam("tickerID", "1") c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, errors.New("storage error")) mw := PrefetchTicker(s) @@ -60,7 +60,7 @@ func TestPrefetchTicker(t *testing.T) { c, _ := gin.CreateTestContext(w) c.AddParam("tickerID", "1") c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} ticker := storage.Ticker{ID: 1} s.On("FindTickerByID", mock.Anything).Return(ticker, nil) mw := PrefetchTicker(s) @@ -77,7 +77,7 @@ func TestPrefetchTickerFromRequestMissingOrigin(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/v1/timeline", nil) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} mw := PrefetchTickerFromRequest(s) mw(c) @@ -93,7 +93,7 @@ func TestPrefetchTickerFromRequestTickerNotFound(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/v1/timeline", nil) c.Request.Header.Set("Origin", "https://demoticker.org") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByDomain", mock.Anything).Return(storage.Ticker{}, errors.New("not found")) mw := PrefetchTickerFromRequest(s) @@ -110,7 +110,7 @@ func TestPrefetchTickerFromRequest(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/v1/timeline", nil) c.Request.Header.Set("Origin", "https://demoticker.org") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByDomain", mock.Anything).Return(storage.Ticker{}, nil) mw := PrefetchTickerFromRequest(s) diff --git a/internal/api/middleware/user/user_test.go b/internal/api/middleware/user/user_test.go index 2a5ec869..b7612455 100644 --- a/internal/api/middleware/user/user_test.go +++ b/internal/api/middleware/user/user_test.go @@ -19,7 +19,7 @@ func init() { func TestPrefetchUserMissingParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} mw := PrefetchUser(s) mw(c) @@ -31,7 +31,7 @@ func TestPrefetchUserStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.AddParam("userID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUserByID", mock.Anything).Return(storage.User{}, errors.New("storage error")) mw := PrefetchUser(s) @@ -44,7 +44,7 @@ func TestPrefetchUser(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.AddParam("userID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} user := storage.User{ID: 1} s.On("FindUserByID", mock.Anything).Return(user, nil) mw := PrefetchUser(s) diff --git a/internal/api/response/message.go b/internal/api/response/message.go index 1c9c0868..c2cbd63f 100644 --- a/internal/api/response/message.go +++ b/internal/api/response/message.go @@ -35,9 +35,9 @@ func MessageResponse(message storage.Message, config config.Config) Message { return Message{ ID: message.ID, - CreationDate: message.CreationDate, + CreationDate: message.CreatedAt, Text: message.Text, - Ticker: message.Ticker, + Ticker: message.TickerID, TelegramURL: message.TelegramURL(), MastodonURL: message.MastodonURL(), GeoInformation: string(m), diff --git a/internal/api/response/settings.go b/internal/api/response/settings.go index 9e0fea4d..e9cfe866 100644 --- a/internal/api/response/settings.go +++ b/internal/api/response/settings.go @@ -3,20 +3,25 @@ package response import "github.com/systemli/ticker/internal/storage" type Settings struct { - RefreshInterval float64 `json:"refresh_interval,omitempty"` + RefreshInterval int `json:"refresh_interval,omitempty"` InactiveSettings interface{} `json:"inactive_settings,omitempty"` } type Setting struct { - ID int `json:"id"` Name string `json:"name"` Value interface{} `json:"value"` } -func SettingResponse(setting storage.Setting) Setting { +func InactiveSettingsResponse(inactiveSettings storage.InactiveSettings) Setting { return Setting{ - ID: setting.ID, - Name: setting.Name, - Value: setting.Value, + Name: storage.SettingInactiveName, + Value: inactiveSettings, + } +} + +func RefreshIntervalSettingsResponse(refreshIntervalSettings storage.RefreshIntervalSettings) Setting { + return Setting{ + Name: storage.SettingRefreshInterval, + Value: refreshIntervalSettings, } } diff --git a/internal/api/response/ticker.go b/internal/api/response/ticker.go index 0bb19adc..569db4d9 100644 --- a/internal/api/response/ticker.go +++ b/internal/api/response/ticker.go @@ -54,7 +54,7 @@ type Location struct { func TickerResponse(t storage.Ticker, config config.Config) Ticker { return Ticker{ ID: t.ID, - CreationDate: t.CreationDate, + CreationDate: t.CreatedAt, Domain: t.Domain, Title: t.Title, Description: t.Description, diff --git a/internal/api/response/timeline.go b/internal/api/response/timeline.go index 96678e6a..a4a1bce7 100644 --- a/internal/api/response/timeline.go +++ b/internal/api/response/timeline.go @@ -36,7 +36,7 @@ func TimelineResponse(messages []storage.Message, config config.Config) []Timeli timeline = append(timeline, TimelineEntry{ ID: message.ID, - CreationDate: message.CreationDate, + CreationDate: message.CreatedAt, Text: message.Text, GeoInformation: string(m), Attachments: attachments, diff --git a/internal/api/response/upload.go b/internal/api/response/upload.go index 85c97779..04e28a3c 100644 --- a/internal/api/response/upload.go +++ b/internal/api/response/upload.go @@ -19,7 +19,7 @@ func UploadResponse(upload storage.Upload, config config.Config) Upload { return Upload{ ID: upload.ID, UUID: upload.UUID, - CreationDate: upload.CreationDate, + CreationDate: upload.CreatedAt, URL: upload.URL(config.UploadURL), ContentType: upload.ContentType, } diff --git a/internal/api/response/user.go b/internal/api/response/user.go index 51937cb2..22e3b3cf 100644 --- a/internal/api/response/user.go +++ b/internal/api/response/user.go @@ -12,17 +12,14 @@ type User struct { Email string `json:"email"` Role string `json:"role"` IsSuperAdmin bool `json:"is_super_admin"` - Tickers []int `json:"tickers"` } func UserResponse(user storage.User) User { return User{ ID: user.ID, - CreationDate: user.CreationDate, + CreationDate: user.CreatedAt, Email: user.Email, - Role: user.Role, IsSuperAdmin: user.IsSuperAdmin, - Tickers: user.Tickers, } } diff --git a/internal/api/settings.go b/internal/api/settings.go index 3b84fe3c..f63e0097 100644 --- a/internal/api/settings.go +++ b/internal/api/settings.go @@ -15,17 +15,21 @@ func (h *handler) GetSetting(c *gin.Context) { return } - var setting storage.Setting if c.Param("name") == storage.SettingInactiveName { - setting = h.storage.GetInactiveSetting() + setting := h.storage.GetInactiveSettings() + data := map[string]interface{}{"setting": response.InactiveSettingsResponse(setting)} + c.JSON(http.StatusOK, response.SuccessResponse(data)) + return } if c.Param("name") == storage.SettingRefreshInterval { - setting = h.storage.GetRefreshIntervalSetting() + setting := h.storage.GetRefreshIntervalSettings() + data := map[string]interface{}{"setting": response.RefreshIntervalSettingsResponse(setting)} + c.JSON(http.StatusOK, response.SuccessResponse(data)) + return } - data := map[string]interface{}{"setting": response.SettingResponse(setting)} - c.JSON(http.StatusOK, response.SuccessResponse(data)) + c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.SettingNotFound)) } func (h *handler) PutInactiveSettings(c *gin.Context) { @@ -36,14 +40,14 @@ func (h *handler) PutInactiveSettings(c *gin.Context) { return } - err = h.storage.SaveInactiveSetting(value) + err = h.storage.SaveInactiveSettings(value) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) return } - setting := h.storage.GetInactiveSetting() - data := map[string]interface{}{"setting": response.SettingResponse(setting)} + setting := h.storage.GetInactiveSettings() + data := map[string]interface{}{"setting": response.InactiveSettingsResponse(setting)} c.JSON(http.StatusOK, response.SuccessResponse(data)) } @@ -55,13 +59,13 @@ func (h *handler) PutRefreshInterval(c *gin.Context) { return } - err = h.storage.SaveRefreshInterval(value.RefreshInterval) + err = h.storage.SaveRefreshIntervalSettings(storage.RefreshIntervalSettings{RefreshInterval: value.RefreshInterval}) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) return } - setting := h.storage.GetRefreshIntervalSetting() - data := map[string]interface{}{"setting": response.SettingResponse(setting)} + setting := h.storage.GetRefreshIntervalSettings() + data := map[string]interface{}{"setting": response.RefreshIntervalSettingsResponse(setting)} c.JSON(http.StatusOK, response.SuccessResponse(data)) } diff --git a/internal/api/settings_test.go b/internal/api/settings_test.go index 8c3a1e65..ccf2ded1 100644 --- a/internal/api/settings_test.go +++ b/internal/api/settings_test.go @@ -24,7 +24,7 @@ func TestGetSettingWithoutParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -32,7 +32,7 @@ func TestGetSettingWithoutParam(t *testing.T) { h.GetSetting(c) - assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, http.StatusNotFound, w.Code) } func TestGetSettingInactiveSetting(t *testing.T) { @@ -40,8 +40,8 @@ func TestGetSettingInactiveSetting(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) c.AddParam("name", storage.SettingInactiveName) - s := &storage.MockTickerStorage{} - s.On("GetInactiveSetting").Return(storage.Setting{}) + s := &storage.MockStorage{} + s.On("GetInactiveSettings").Return(storage.InactiveSettings{}) h := handler{ storage: s, config: config.NewConfig(), @@ -57,8 +57,8 @@ func TestGetSettingRefreshIntervalSetting(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) c.AddParam("name", storage.SettingRefreshInterval) - s := &storage.MockTickerStorage{} - s.On("GetRefreshIntervalSetting").Return(storage.Setting{}) + s := &storage.MockStorage{} + s.On("GetRefreshIntervalSettings").Return(storage.RefreshIntervalSettings{}) h := handler{ storage: s, config: config.NewConfig(), @@ -76,7 +76,7 @@ func TestPutInactiveSettingsMissingBody(t *testing.T) { body := `broken_json` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", strings.NewReader(body)) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -91,13 +91,13 @@ func TestPutInactiveSettingsStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - setting := storage.DefaultInactiveSetting() - body, _ := json.Marshal(setting.Value) + setting := storage.DefaultInactiveSettings() + body, _ := json.Marshal(setting) c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", bytes.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("SaveInactiveSetting", mock.Anything).Return(errors.New("storage error")) + s := &storage.MockStorage{} + s.On("SaveInactiveSettings", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, config: config.NewConfig(), @@ -112,14 +112,14 @@ func TestPutInactiveSettings(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - setting := storage.DefaultInactiveSetting() - body, _ := json.Marshal(setting.Value) + setting := storage.DefaultInactiveSettings() + body, _ := json.Marshal(setting) c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", bytes.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("SaveInactiveSetting", mock.Anything).Return(nil) - s.On("GetInactiveSetting").Return(setting) + s := &storage.MockStorage{} + s.On("SaveInactiveSettings", mock.Anything).Return(nil) + s.On("GetInactiveSettings").Return(setting) h := handler{ storage: s, config: config.NewConfig(), @@ -137,7 +137,7 @@ func TestPutRefreshIntervalSettingsMissingBody(t *testing.T) { body := `broken_json` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", strings.NewReader(body)) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -156,8 +156,8 @@ func TestPutRefreshIntervalSettingsStorageError(t *testing.T) { c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("SaveRefreshInterval", mock.Anything).Return(errors.New("storage error")) + s := &storage.MockStorage{} + s.On("SaveRefreshIntervalSettings", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, config: config.NewConfig(), @@ -172,14 +172,14 @@ func TestPutRefreshIntervalSettings(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - setting := storage.DefaultRefreshIntervalSetting() + setting := storage.DefaultRefreshIntervalSettings() body := `{"refresh_interval": 10000}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/settings", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("SaveRefreshInterval", mock.Anything).Return(nil) - s.On("GetRefreshIntervalSetting").Return(setting) + s := &storage.MockStorage{} + s.On("SaveRefreshIntervalSettings", mock.Anything).Return(nil) + s.On("GetRefreshIntervalSettings").Return(setting) h := handler{ storage: s, config: config.NewConfig(), diff --git a/internal/api/tickers.go b/internal/api/tickers.go index 2fd18465..11c54600 100644 --- a/internal/api/tickers.go +++ b/internal/api/tickers.go @@ -22,8 +22,7 @@ func (h *handler) GetTickers(c *gin.Context) { if me.IsSuperAdmin { tickers, err = h.storage.FindTickers() } else { - allowed := me.Tickers - tickers, err = h.storage.FindTickersByIDs(allowed) + tickers = me.Tickers } if err != nil { c.JSON(http.StatusNotFound, response.ErrorResponse(response.CodeDefault, response.TickerNotFound)) @@ -111,15 +110,21 @@ func (h *handler) PutTickerUsers(c *gin.Context) { return } - err = h.storage.AddUsersToTicker(ticker, body.Users) + newUsers, err := h.storage.FindUsersByIDs(body.Users) if err != nil { c.JSON(http.StatusInternalServerError, response.ErrorResponse(response.CodeDefault, response.StorageError)) return } - users, _ := h.storage.FindUsersByTicker(ticker) + ticker.Users = append(ticker.Users, newUsers...) - c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"users": response.UsersResponse(users)})) + err = h.storage.SaveTicker(&ticker) + if err != nil { + c.JSON(http.StatusInternalServerError, response.ErrorResponse(response.CodeDefault, response.StorageError)) + return + } + + c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"users": response.UsersResponse(ticker.Users)})) } func (h *handler) PutTickerTelegram(c *gin.Context) { @@ -129,7 +134,7 @@ func (h *handler) PutTickerTelegram(c *gin.Context) { return } - var body storage.Telegram + var body storage.TickerTelegram err = c.Bind(&body) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeNotFound, response.FormError)) @@ -175,7 +180,7 @@ func (h *handler) PutTickerMastodon(c *gin.Context) { return } - var body storage.Mastodon + var body storage.TickerMastodon err = c.Bind(&body) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeNotFound, response.FormError)) @@ -271,15 +276,13 @@ func (h *handler) DeleteTickerUser(c *gin.Context) { return } - err = h.storage.RemoveTickerFromUser(ticker, user) + err = h.storage.DeleteTickerUser(&ticker, &user) if err != nil { c.JSON(http.StatusInternalServerError, response.ErrorResponse(response.CodeDefault, response.StorageError)) return } - users, _ := h.storage.FindUsersByTicker(ticker) - - c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"users": response.UsersResponse(users)})) + c.JSON(http.StatusOK, response.SuccessResponse(map[string]interface{}{"users": response.UsersResponse(ticker.Users)})) } func (h *handler) ResetTicker(c *gin.Context) { diff --git a/internal/api/tickers_test.go b/internal/api/tickers_test.go index ebd50735..6fc1bb4a 100644 --- a/internal/api/tickers_test.go +++ b/internal/api/tickers_test.go @@ -24,7 +24,7 @@ func init() { func TestGetTickersForbidden(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -39,7 +39,7 @@ func TestGetTickersStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickers").Return([]storage.Ticker{}, errors.New("storage error")) h := handler{ storage: s, @@ -54,8 +54,8 @@ func TestGetTickersStorageError(t *testing.T) { func TestGetTickers(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - c.Set("me", storage.User{IsSuperAdmin: false, Tickers: []int{1}}) - s := &storage.MockTickerStorage{} + c.Set("me", storage.User{IsSuperAdmin: false, Tickers: []storage.Ticker{{ID: 2}}}) + s := &storage.MockStorage{} s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, nil) h := handler{ storage: s, @@ -70,7 +70,7 @@ func TestGetTickers(t *testing.T) { func TestGetTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -85,7 +85,7 @@ func TestGetTicker(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -99,7 +99,7 @@ func TestGetTicker(t *testing.T) { func TestGetTickerUsersTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -114,7 +114,7 @@ func TestGetTickerUsers(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUsersByTicker", mock.Anything).Return([]storage.User{}, nil) h := handler{ storage: s, @@ -131,7 +131,7 @@ func TestPostTickerFormError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers", nil) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -149,7 +149,7 @@ func TestPostTickerStorageError(t *testing.T) { body := `{"domain":"localhost","title":"title","description":"description"}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -168,7 +168,7 @@ func TestPostTicker(t *testing.T) { body := `{"domain":"localhost","title":"title","description":"description"}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/tickers", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -183,7 +183,7 @@ func TestPostTicker(t *testing.T) { func TestPutTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -200,7 +200,7 @@ func TestPutTickerFormError(t *testing.T) { c.Set("ticker", storage.Ticker{}) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", nil) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -218,7 +218,7 @@ func TestPutTickerStorageError(t *testing.T) { body := `{"domain":"localhost","title":"title","description":"description"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -237,7 +237,7 @@ func TestPutTicker(t *testing.T) { body := `{"domain":"localhost","title":"title","description":"description"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -252,7 +252,7 @@ func TestPutTicker(t *testing.T) { func TestPutTickerUsersNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -269,7 +269,7 @@ func TestPutTickerUsersFormError(t *testing.T) { c.Set("ticker", storage.Ticker{}) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/user", nil) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -287,8 +287,8 @@ func TestPutTickerUsersStorageError(t *testing.T) { body := `{"users":[1,2,3]}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/user", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("AddUsersToTicker", mock.Anything, mock.Anything).Return(errors.New("storage error")) + s := &storage.MockStorage{} + s.On("FindUsersByIDs", mock.Anything).Return(nil, errors.New("storage error")) h := handler{ storage: s, config: config.NewConfig(), @@ -306,9 +306,10 @@ func TestPutTickerUsers(t *testing.T) { body := `{"users":[1,2,3]}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/user", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} - s.On("AddUsersToTicker", mock.Anything, mock.Anything).Return(nil) + s := &storage.MockStorage{} + s.On("FindUsersByIDs", mock.Anything).Return([]storage.User{}, nil) s.On("FindUsersByTicker", mock.Anything).Return([]storage.User{}, nil) + s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, config: config.NewConfig(), @@ -322,7 +323,7 @@ func TestPutTickerUsers(t *testing.T) { func TestPutTickerTelegramTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -339,7 +340,7 @@ func TestPutTickerTelegramFormError(t *testing.T) { c.Set("ticker", storage.Ticker{}) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/telegram", nil) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -357,7 +358,7 @@ func TestPutTickerTelegramStorageError(t *testing.T) { body := `{"active":true,"channel_name":"@channel_name"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/telegram", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -376,7 +377,7 @@ func TestPutTickerTelegram(t *testing.T) { body := `{"active":true,"channel_name":"@channel_name"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/telegram", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -391,7 +392,7 @@ func TestPutTickerTelegram(t *testing.T) { func TestDeleteTickerTelegramTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -406,7 +407,7 @@ func TestDeleteTickerTelegramStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -422,7 +423,7 @@ func TestDeleteTickerTelegram(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -437,7 +438,7 @@ func TestDeleteTickerTelegram(t *testing.T) { func TestPutTickerMastodonTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -454,7 +455,7 @@ func TestPutTickerMastodonFormError(t *testing.T) { c.Set("ticker", storage.Ticker{}) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", nil) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -472,7 +473,7 @@ func TestPutTickerMastodonConnectError(t *testing.T) { body := `{"active":true,"server":"http://localhost","secret":"secret","token":"token","access_token":"access_token"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -497,7 +498,7 @@ func TestPutTickerMastodonStorageError(t *testing.T) { body := fmt.Sprintf(`{"server":"%s","token":"token","secret":"secret","access_token":"access_toklen"}`, server.URL) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -522,7 +523,7 @@ func TestPutTickerMastodon(t *testing.T) { body := fmt.Sprintf(`{"server":"%s","token":"token","secret":"secret","access_token":"access_toklen"}`, server.URL) c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/mastodon", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -537,7 +538,7 @@ func TestPutTickerMastodon(t *testing.T) { func TestDeleteTickerMastodonTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -552,7 +553,7 @@ func TestDeleteTickerMastodonStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -568,7 +569,7 @@ func TestDeleteTickerMastodon(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveTicker", mock.Anything).Return(nil) h := handler{ storage: s, @@ -583,7 +584,7 @@ func TestDeleteTickerMastodon(t *testing.T) { func TestDeleteTickerTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -598,7 +599,7 @@ func TestDeleteTickerStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessages", mock.Anything).Return(errors.New("storage error")) s.On("DeleteTicker", mock.Anything).Return(errors.New("storage error")) h := handler{ @@ -615,7 +616,7 @@ func TestDeleteTicker(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessages", mock.Anything).Return(nil) s.On("DeleteTicker", mock.Anything).Return(nil) h := handler{ @@ -631,7 +632,7 @@ func TestDeleteTicker(t *testing.T) { func TestDeleteTickerUserTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -646,7 +647,7 @@ func TestDeleteTickerUserMissingUserParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -662,7 +663,7 @@ func TestDeleteTickerUserUserNotFound(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.AddParam("userID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUserByID", mock.Anything).Return(storage.User{}, errors.New("not found")) h := handler{ storage: s, @@ -679,9 +680,9 @@ func TestDeleteTickerUserStorageError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.AddParam("userID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUserByID", mock.Anything).Return(storage.User{}, nil) - s.On("RemoveTickerFromUser", mock.Anything, mock.Anything).Return(errors.New("storage error")) + s.On("DeleteTickerUser", mock.Anything, mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, config: config.NewConfig(), @@ -697,9 +698,9 @@ func TestDeleteTickerUser(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) c.AddParam("userID", "1") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUserByID", mock.Anything).Return(storage.User{}, nil) - s.On("RemoveTickerFromUser", mock.Anything, mock.Anything).Return(nil) + s.On("DeleteTickerUser", mock.Anything, mock.Anything).Return(nil) s.On("FindUsersByTicker", mock.Anything).Return([]storage.User{}, nil) h := handler{ storage: s, @@ -714,7 +715,7 @@ func TestDeleteTickerUser(t *testing.T) { func TestResetTickerUserTickerNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -729,7 +730,7 @@ func TestResetTickerStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessages", mock.Anything).Return(errors.New("storage error")) s.On("DeleteUploadsByTicker", mock.Anything).Return(errors.New("storage error")) s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) @@ -747,7 +748,7 @@ func TestResetTicker(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteMessages", mock.Anything).Return(nil) s.On("DeleteUploadsByTicker", mock.Anything).Return(nil) s.On("SaveTicker", mock.Anything).Return(nil) diff --git a/internal/api/timeline_test.go b/internal/api/timeline_test.go index 31f51f8f..1337f958 100644 --- a/internal/api/timeline_test.go +++ b/internal/api/timeline_test.go @@ -22,7 +22,7 @@ func TestGetTimelineMissingDomain(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/v1/timeline", nil) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -40,7 +40,7 @@ func TestGetTimelineTickerNotFound(t *testing.T) { c.Request = httptest.NewRequest(http.MethodGet, "/v1/timeline", nil) c.Request.Header.Add("Origin", "https://demoticker.org") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByDomain", mock.Anything).Return(storage.Ticker{}, errors.New("not found")) h := handler{ storage: s, @@ -57,7 +57,7 @@ func TestGetTimelineTickerInactive(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{Active: false}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, @@ -73,7 +73,7 @@ func TestGetTimelineMessageFetchError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{Active: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, errors.New("storage error")) h := handler{ @@ -91,7 +91,7 @@ func TestGetTimeline(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("ticker", storage.Ticker{}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindMessagesByTickerAndPagination", mock.Anything, mock.Anything).Return([]storage.Message{}, nil) h := handler{ diff --git a/internal/api/upload.go b/internal/api/upload.go index d5f43775..0fab73cf 100644 --- a/internal/api/upload.go +++ b/internal/api/upload.go @@ -48,7 +48,11 @@ func (h *handler) PostUpload(c *gin.Context) { } if !me.IsSuperAdmin { - if !util.Contains(me.Tickers, tickerID) { + tickerIDs := make([]int, 0, len(me.Tickers)) + for _, t := range me.Tickers { + tickerIDs = append(tickerIDs, t.ID) + } + if !util.Contains(tickerIDs, tickerID) { c.JSON(http.StatusForbidden, response.ErrorResponse(response.CodeInsufficientPermissions, response.InsufficientPermissions)) return } diff --git a/internal/api/upload_test.go b/internal/api/upload_test.go index 31f5e576..36bd84d5 100644 --- a/internal/api/upload_test.go +++ b/internal/api/upload_test.go @@ -24,7 +24,7 @@ func init() { func TestPostUploadForbidden(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -40,7 +40,7 @@ func TestPostUploadMultipartError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) c.Request = httptest.NewRequest(http.MethodPost, "/upload", nil) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -61,7 +61,7 @@ func TestPostUploadMissingTickerValue(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -82,7 +82,7 @@ func TestPostUploadTickerValueWrong(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -103,7 +103,7 @@ func TestPostUploadTickerNotFound(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, errors.New("not found")) h := handler{ storage: s, @@ -125,7 +125,7 @@ func TestPostUploadWrongPermission(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) h := handler{ storage: s, @@ -147,7 +147,7 @@ func TestPostUploadMissingFiles(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) h := handler{ storage: s, @@ -179,7 +179,7 @@ func TestPostUpload(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) s.On("SaveUpload", mock.Anything).Return(nil) h := handler{ @@ -212,7 +212,7 @@ func TestPostUploadGIF(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) s.On("SaveUpload", mock.Anything).Return(nil) h := handler{ @@ -246,7 +246,7 @@ func TestPostUploadTooMuchFiles(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) s.On("SaveUpload", mock.Anything).Return(nil) h := handler{ @@ -279,7 +279,7 @@ func TestPostUploadForbiddenFileType(t *testing.T) { _ = writer.Close() c.Request = httptest.NewRequest(http.MethodPost, "/upload", body) c.Request.Header.Add("Content-Type", writer.FormDataContentType()) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindTickerByID", mock.Anything).Return(storage.Ticker{}, nil) s.On("SaveUpload", mock.Anything).Return(nil) h := handler{ diff --git a/internal/api/user.go b/internal/api/user.go index a2b77839..11cc56ac 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -59,8 +59,15 @@ func (h *handler) PostUser(c *gin.Context) { return } + // load tickers by body.Tickers + tickers, err := h.storage.FindTickersByIDs(body.Tickers) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) + return + } + user.IsSuperAdmin = body.IsSuperAdmin - user.Tickers = body.Tickers + user.Tickers = tickers err = h.storage.SaveUser(&user) if err != nil { @@ -68,7 +75,7 @@ func (h *handler) PostUser(c *gin.Context) { return } - data := map[string]interface{}{"user": user} + data := map[string]interface{}{"user": response.UserResponse(user)} c.JSON(http.StatusOK, response.SuccessResponse(data)) } @@ -83,7 +90,6 @@ func (h *handler) PutUser(c *gin.Context) { var body struct { Email string `json:"email,omitempty" validate:"email"` Password string `json:"password,omitempty" validate:"min=10"` - Role string `json:"role,omitempty"` IsSuperAdmin bool `json:"is_super_admin,omitempty"` Tickers []int `json:"tickers,omitempty"` } @@ -100,9 +106,6 @@ func (h *handler) PutUser(c *gin.Context) { if body.Password != "" { user.UpdatePassword(body.Password) } - if body.Role != "" { - user.Role = body.Role - } // You only can set/unset other users SuperAdmin property if me.ID != user.ID { @@ -110,7 +113,13 @@ func (h *handler) PutUser(c *gin.Context) { } if body.Tickers != nil { - user.Tickers = body.Tickers + tickers, err := h.storage.FindTickersByIDs(body.Tickers) + if err != nil { + c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) + return + } + + user.Tickers = tickers } err = h.storage.SaveUser(&user) @@ -119,7 +128,7 @@ func (h *handler) PutUser(c *gin.Context) { return } - data := map[string]interface{}{"user": user} + data := map[string]interface{}{"user": response.UserResponse(user)} c.JSON(http.StatusOK, response.SuccessResponse(data)) } @@ -176,6 +185,6 @@ func (h *handler) PutMe(c *gin.Context) { return } - data := map[string]interface{}{"user": me} + data := map[string]interface{}{"user": response.UserResponse(me)} c.JSON(http.StatusOK, response.SuccessResponse(data)) } diff --git a/internal/api/user_test.go b/internal/api/user_test.go index 503cc294..00e51637 100644 --- a/internal/api/user_test.go +++ b/internal/api/user_test.go @@ -22,7 +22,7 @@ func TestGetUsersStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUsers").Return([]storage.User{}, errors.New("storage error")) h := handler{ storage: s, @@ -38,7 +38,7 @@ func TestGetUsers(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUsers").Return([]storage.User{}, nil) h := handler{ @@ -54,7 +54,7 @@ func TestGetUsers(t *testing.T) { func TestGetUserMissingParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -71,7 +71,7 @@ func TestGetUserInsufficentPermission(t *testing.T) { c.Set("user", storage.User{ID: 1}) c.Set("me", storage.User{ID: 2, IsSuperAdmin: false}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -88,7 +88,7 @@ func TestGetUserStorageError(t *testing.T) { c.AddParam("userID", "1") c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("FindUserByID", mock.Anything).Return(storage.User{}, errors.New("storage error")) h := handler{ storage: s, @@ -106,7 +106,7 @@ func TestGetUserMissingPermission(t *testing.T) { c.Set("user", storage.User{ID: 2}) c.Set("me", storage.User{ID: 1, IsSuperAdmin: false}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -123,7 +123,7 @@ func TestGetUser(t *testing.T) { c.Set("user", storage.User{ID: 1}) c.Set("me", storage.User{ID: 1, IsSuperAdmin: false}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -139,7 +139,7 @@ func TestPostUserMissingBody(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = &http.Request{} - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -157,7 +157,8 @@ func TestPostUserStorageError(t *testing.T) { c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, nil) s.On("SaveUser", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -176,7 +177,8 @@ func TestPostUser(t *testing.T) { c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, nil) s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -192,7 +194,7 @@ func TestPutUserNotFound(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -211,7 +213,7 @@ func TestPutUserMissingBody(t *testing.T) { body := `broken_json` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(body)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -230,7 +232,8 @@ func TestPutUserStorageError(t *testing.T) { json := `{"email":"louis@systemli.org","password":"password1234","is_super_admin":true,"tickers":[1]}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, nil) s.On("SaveUser", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -250,7 +253,8 @@ func TestPutUser(t *testing.T) { json := `{"email":"louis@systemli.org","password":"password1234","is_super_admin":true,"tickers":[1]}` c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{{ID: 1}}, nil) s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -266,7 +270,7 @@ func TestDeleteUserMissingParam(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -282,7 +286,7 @@ func TestDeleteUserSelfUser(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) c.Set("user", storage.User{ID: 1}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} h := handler{ storage: s, config: config.NewConfig(), @@ -298,7 +302,7 @@ func TestDeleteUserStorageError(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) c.Set("user", storage.User{ID: 2}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteUser", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -315,7 +319,7 @@ func TestDeleteUser(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) c.Set("user", storage.User{ID: 2}) - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("DeleteUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -333,7 +337,7 @@ func TestPutMeUnauthenticated(t *testing.T) { json := `{"password":"password1234","new_password":"password5678"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/users/me", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -350,7 +354,7 @@ func TestPutMeFormError(t *testing.T) { json := `{"wrongparameter":"password1234","new_password":"password5678"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/users/me", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -367,7 +371,7 @@ func TestPutMeWrongPassword(t *testing.T) { json := `{"password":"wrongpassword","new_password":"password5678"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/users/me", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, @@ -384,7 +388,7 @@ func TestPutMeStorageError(t *testing.T) { json := `{"password":"password1234","new_password":"password5678"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/users/me", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveUser", mock.Anything).Return(errors.New("storage error")) h := handler{ storage: s, @@ -401,7 +405,7 @@ func TestPutMeOk(t *testing.T) { json := `{"password":"password1234","new_password":"password5678"}` c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/users/me", strings.NewReader(json)) c.Request.Header.Add("Content-Type", "application/json") - s := &storage.MockTickerStorage{} + s := &storage.MockStorage{} s.On("SaveUser", mock.Anything).Return(nil) h := handler{ storage: s, diff --git a/internal/bridge/mastodon.go b/internal/bridge/mastodon.go index 123f209e..e8f37dd6 100644 --- a/internal/bridge/mastodon.go +++ b/internal/bridge/mastodon.go @@ -50,7 +50,11 @@ func (mb *MastodonBridge) Send(ticker storage.Ticker, message *storage.Message) return err } - message.Mastodon = *status + message.Mastodon = storage.MastodonMeta{ + ID: string(status.ID), + URI: status.URI, + URL: status.URL, + } return nil } @@ -67,7 +71,7 @@ func (mb *MastodonBridge) Delete(ticker storage.Ticker, message *storage.Message ctx := context.Background() client := client(ticker) - return client.DeleteStatus(ctx, message.Mastodon.ID) + return client.DeleteStatus(ctx, mastodon.ID(message.Mastodon.ID)) } func client(ticker storage.Ticker) *mastodon.Client { diff --git a/internal/bridge/bridge_mock.go b/internal/bridge/mock_Bridge.go similarity index 87% rename from internal/bridge/bridge_mock.go rename to internal/bridge/mock_Bridge.go index 4eb9991f..9ceee83d 100644 --- a/internal/bridge/bridge_mock.go +++ b/internal/bridge/mock_Bridge.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.35.2. DO NOT EDIT. package bridge @@ -40,13 +40,12 @@ func (_m *MockBridge) Send(ticker storage.Ticker, message *storage.Message) erro return r0 } -type mockConstructorTestingTNewMockBridge interface { +// NewMockBridge creates a new instance of MockBridge. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBridge(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockBridge creates a new instance of MockBridge. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockBridge(t mockConstructorTestingTNewMockBridge) *MockBridge { +}) *MockBridge { mock := &MockBridge{} mock.Mock.Test(t) diff --git a/internal/config/config.go b/internal/config/config.go index deffc22a..c522f244 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,13 +12,13 @@ import ( ) type Config struct { - Listen string `mapstructure:"listen"` - LogLevel string `mapstructure:"log_level"` - LogFormat string `mapstructure:"log_format"` - Initiator string `mapstructure:"initiator"` - Secret string `mapstructure:"secret"` - Database string `mapstructure:"database"` - TelegramBotToken string `mapstructure:"telegram_bot_token"` + Listen string `mapstructure:"listen"` + LogLevel string `mapstructure:"log_level"` + LogFormat string `mapstructure:"log_format"` + Initiator string `mapstructure:"initiator"` + Secret string `mapstructure:"secret"` + Database Database `mapstructure:"database"` + TelegramBotToken string `mapstructure:"telegram_bot_token"` TelegramBotUser tgbotapi.User MetricsListen string `mapstructure:"metrics_listen"` UploadPath string `mapstructure:"upload_path"` @@ -26,6 +26,11 @@ type Config struct { FileBackend afero.Fs } +type Database struct { + Type string `mapstructure:"type"` + DSN string `mapstructure:"dsn"` +} + // NewConfig returns config with default values. func NewConfig() Config { secret, _ := password.Generate(64, 12, 12, false, false) @@ -36,7 +41,7 @@ func NewConfig() Config { LogFormat: "json", Initiator: "admin@systemli.org", Secret: secret, - Database: "ticker.db", + Database: Database{Type: "sqlite", DSN: "ticker.db"}, MetricsListen: ":8181", UploadPath: "uploads", UploadURL: "http://localhost:8080", diff --git a/internal/logger/gorm.go b/internal/logger/gorm.go new file mode 100644 index 00000000..409b7cf0 --- /dev/null +++ b/internal/logger/gorm.go @@ -0,0 +1,50 @@ +package logger + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm/logger" +) + +type GormLogger struct { + Log *logrus.Logger +} + +func NewGormLogger(log *logrus.Logger) *GormLogger { + return &GormLogger{Log: log} +} + +func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface { + return l +} + +func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) { + l.Log.WithContext(ctx).Infof(msg, data...) +} + +func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) { + l.Log.WithContext(ctx).Warnf(msg, data...) +} + +func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) { + l.Log.WithContext(ctx).Errorf(msg, data...) +} + +func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { + if l.Log.IsLevelEnabled(logrus.TraceLevel) { + elapsed := time.Since(begin) + sql, rows := fc() + fields := logrus.Fields{ + "sql": sql, + "rows": rows, + "elapsed_ms": float64(elapsed.Nanoseconds()) / 1e6, + } + if err != nil { + l.Log.WithContext(ctx).WithFields(fields).WithError(err).Trace("gorm: error") + } else { + l.Log.WithContext(ctx).WithFields(fields).Trace("gorm: trace") + } + } +} diff --git a/internal/logger/logrus.go b/internal/logger/logrus.go new file mode 100644 index 00000000..8fb44e59 --- /dev/null +++ b/internal/logger/logrus.go @@ -0,0 +1,21 @@ +package logger + +import "github.com/sirupsen/logrus" + +func NewLogrus(level, format string) *logrus.Logger { + logger := logrus.New() + lvl, err := logrus.ParseLevel(level) + if err != nil { + lvl = logrus.InfoLevel + } + logger.SetLevel(lvl) + + switch format { + case "json": + logger.SetFormatter(&logrus.JSONFormatter{}) + default: + logger.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) + } + + return logger +} diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go deleted file mode 100644 index 93229cb8..00000000 --- a/internal/storage/db_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package storage - -import ( - "testing" -) - -func TestOpenDBPanics(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("The code did not panic") - } - }() - - OpenDB("/x/y/z") -} diff --git a/internal/storage/message.go b/internal/storage/message.go index d62e61a8..9601abda 100644 --- a/internal/storage/message.go +++ b/internal/storage/message.go @@ -5,38 +5,40 @@ import ( "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/mattn/go-mastodon" geojson "github.com/paulmach/go.geojson" ) type Message struct { - ID int `storm:"id,increment"` - CreationDate time.Time `storm:"index"` - Ticker int `storm:"index"` + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + TickerID int `gorm:"index"` Text string Attachments []Attachment - GeoInformation geojson.FeatureCollection - Tweet Tweet - Telegram TelegramMeta - Mastodon mastodon.Status + GeoInformation geojson.FeatureCollection `gorm:"serializer:json"` + Telegram TelegramMeta `gorm:"serializer:json"` + Mastodon MastodonMeta `gorm:"serializer:json"` } func NewMessage() Message { - return Message{ - CreationDate: time.Now(), - } -} - -type Tweet struct { - ID string - UserName string + return Message{} } type TelegramMeta struct { Messages []tgbotapi.Message } +type MastodonMeta struct { + ID string `json:"id"` + URI string `json:"uri"` + URL string `json:"url"` +} + type Attachment struct { + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + MessageID int `gorm:"index"` UUID string Extension string ContentType string diff --git a/internal/storage/message_test.go b/internal/storage/message_test.go index 4e5be573..6c33e0c6 100644 --- a/internal/storage/message_test.go +++ b/internal/storage/message_test.go @@ -4,7 +4,6 @@ import ( "testing" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/mattn/go-mastodon" "github.com/stretchr/testify/assert" ) @@ -41,7 +40,7 @@ func TestMastodonURL(t *testing.T) { assert.Empty(t, message.MastodonURL()) url := "https://mastodon.social/web/@systemli/1" - message.Mastodon = mastodon.Status{ + message.Mastodon = MastodonMeta{ ID: "1", URL: url, } diff --git a/internal/storage/storage_mock.go b/internal/storage/mock_Storage.go similarity index 64% rename from internal/storage/storage_mock.go rename to internal/storage/mock_Storage.go index 11448002..aaa22b01 100644 --- a/internal/storage/storage_mock.go +++ b/internal/storage/mock_Storage.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.35.2. DO NOT EDIT. package storage @@ -7,37 +7,26 @@ import ( pagination "github.com/systemli/ticker/internal/api/pagination" ) -// MockTickerStorage is an autogenerated mock type for the TickerStorage type -type MockTickerStorage struct { +// MockStorage is an autogenerated mock type for the Storage type +type MockStorage struct { mock.Mock } -// AddUsersToTicker provides a mock function with given fields: ticker, ids -func (_m *MockTickerStorage) AddUsersToTicker(ticker Ticker, ids []int) error { - ret := _m.Called(ticker, ids) - - var r0 error - if rf, ok := ret.Get(0).(func(Ticker, []int) error); ok { - r0 = rf(ticker, ids) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // CountUser provides a mock function with given fields: -func (_m *MockTickerStorage) CountUser() (int, error) { +func (_m *MockStorage) CountUser() (int, error) { ret := _m.Called() var r0 int + var r1 error + if rf, ok := ret.Get(0).(func() (int, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() int); ok { r0 = rf() } else { r0 = ret.Get(0).(int) } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -48,7 +37,7 @@ func (_m *MockTickerStorage) CountUser() (int, error) { } // DeleteMessage provides a mock function with given fields: message -func (_m *MockTickerStorage) DeleteMessage(message Message) error { +func (_m *MockStorage) DeleteMessage(message Message) error { ret := _m.Called(message) var r0 error @@ -62,7 +51,7 @@ func (_m *MockTickerStorage) DeleteMessage(message Message) error { } // DeleteMessages provides a mock function with given fields: ticker -func (_m *MockTickerStorage) DeleteMessages(ticker Ticker) error { +func (_m *MockStorage) DeleteMessages(ticker Ticker) error { ret := _m.Called(ticker) var r0 error @@ -76,7 +65,7 @@ func (_m *MockTickerStorage) DeleteMessages(ticker Ticker) error { } // DeleteTicker provides a mock function with given fields: ticker -func (_m *MockTickerStorage) DeleteTicker(ticker Ticker) error { +func (_m *MockStorage) DeleteTicker(ticker Ticker) error { ret := _m.Called(ticker) var r0 error @@ -89,8 +78,22 @@ func (_m *MockTickerStorage) DeleteTicker(ticker Ticker) error { return r0 } +// DeleteTickerUser provides a mock function with given fields: ticker, user +func (_m *MockStorage) DeleteTickerUser(ticker *Ticker, user *User) error { + ret := _m.Called(ticker, user) + + var r0 error + if rf, ok := ret.Get(0).(func(*Ticker, *User) error); ok { + r0 = rf(ticker, user) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteUpload provides a mock function with given fields: upload -func (_m *MockTickerStorage) DeleteUpload(upload Upload) error { +func (_m *MockStorage) DeleteUpload(upload Upload) error { ret := _m.Called(upload) var r0 error @@ -104,12 +107,12 @@ func (_m *MockTickerStorage) DeleteUpload(upload Upload) error { } // DeleteUploads provides a mock function with given fields: uploads -func (_m *MockTickerStorage) DeleteUploads(uploads []Upload) { +func (_m *MockStorage) DeleteUploads(uploads []Upload) { _m.Called(uploads) } // DeleteUploadsByTicker provides a mock function with given fields: ticker -func (_m *MockTickerStorage) DeleteUploadsByTicker(ticker Ticker) error { +func (_m *MockStorage) DeleteUploadsByTicker(ticker Ticker) error { ret := _m.Called(ticker) var r0 error @@ -123,7 +126,7 @@ func (_m *MockTickerStorage) DeleteUploadsByTicker(ticker Ticker) error { } // DeleteUser provides a mock function with given fields: user -func (_m *MockTickerStorage) DeleteUser(user User) error { +func (_m *MockStorage) DeleteUser(user User) error { ret := _m.Called(user) var r0 error @@ -137,17 +140,20 @@ func (_m *MockTickerStorage) DeleteUser(user User) error { } // FindMessage provides a mock function with given fields: tickerID, messageID -func (_m *MockTickerStorage) FindMessage(tickerID int, messageID int) (Message, error) { +func (_m *MockStorage) FindMessage(tickerID int, messageID int) (Message, error) { ret := _m.Called(tickerID, messageID) var r0 Message + var r1 error + if rf, ok := ret.Get(0).(func(int, int) (Message, error)); ok { + return rf(tickerID, messageID) + } if rf, ok := ret.Get(0).(func(int, int) Message); ok { r0 = rf(tickerID, messageID) } else { r0 = ret.Get(0).(Message) } - var r1 error if rf, ok := ret.Get(1).(func(int, int) error); ok { r1 = rf(tickerID, messageID) } else { @@ -158,10 +164,14 @@ func (_m *MockTickerStorage) FindMessage(tickerID int, messageID int) (Message, } // FindMessagesByTicker provides a mock function with given fields: ticker -func (_m *MockTickerStorage) FindMessagesByTicker(ticker Ticker) ([]Message, error) { +func (_m *MockStorage) FindMessagesByTicker(ticker Ticker) ([]Message, error) { ret := _m.Called(ticker) var r0 []Message + var r1 error + if rf, ok := ret.Get(0).(func(Ticker) ([]Message, error)); ok { + return rf(ticker) + } if rf, ok := ret.Get(0).(func(Ticker) []Message); ok { r0 = rf(ticker) } else { @@ -170,7 +180,6 @@ func (_m *MockTickerStorage) FindMessagesByTicker(ticker Ticker) ([]Message, err } } - var r1 error if rf, ok := ret.Get(1).(func(Ticker) error); ok { r1 = rf(ticker) } else { @@ -181,10 +190,14 @@ func (_m *MockTickerStorage) FindMessagesByTicker(ticker Ticker) ([]Message, err } // FindMessagesByTickerAndPagination provides a mock function with given fields: ticker, _a1 -func (_m *MockTickerStorage) FindMessagesByTickerAndPagination(ticker Ticker, _a1 pagination.Pagination) ([]Message, error) { +func (_m *MockStorage) FindMessagesByTickerAndPagination(ticker Ticker, _a1 pagination.Pagination) ([]Message, error) { ret := _m.Called(ticker, _a1) var r0 []Message + var r1 error + if rf, ok := ret.Get(0).(func(Ticker, pagination.Pagination) ([]Message, error)); ok { + return rf(ticker, _a1) + } if rf, ok := ret.Get(0).(func(Ticker, pagination.Pagination) []Message); ok { r0 = rf(ticker, _a1) } else { @@ -193,7 +206,6 @@ func (_m *MockTickerStorage) FindMessagesByTickerAndPagination(ticker Ticker, _a } } - var r1 error if rf, ok := ret.Get(1).(func(Ticker, pagination.Pagination) error); ok { r1 = rf(ticker, _a1) } else { @@ -203,39 +215,21 @@ func (_m *MockTickerStorage) FindMessagesByTickerAndPagination(ticker Ticker, _a return r0, r1 } -// FindSetting provides a mock function with given fields: name -func (_m *MockTickerStorage) FindSetting(name string) (Setting, error) { - ret := _m.Called(name) - - var r0 Setting - if rf, ok := ret.Get(0).(func(string) Setting); ok { - r0 = rf(name) - } else { - r0 = ret.Get(0).(Setting) - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(name) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // FindTickerByDomain provides a mock function with given fields: domain -func (_m *MockTickerStorage) FindTickerByDomain(domain string) (Ticker, error) { +func (_m *MockStorage) FindTickerByDomain(domain string) (Ticker, error) { ret := _m.Called(domain) var r0 Ticker + var r1 error + if rf, ok := ret.Get(0).(func(string) (Ticker, error)); ok { + return rf(domain) + } if rf, ok := ret.Get(0).(func(string) Ticker); ok { r0 = rf(domain) } else { r0 = ret.Get(0).(Ticker) } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(domain) } else { @@ -246,17 +240,20 @@ func (_m *MockTickerStorage) FindTickerByDomain(domain string) (Ticker, error) { } // FindTickerByID provides a mock function with given fields: id -func (_m *MockTickerStorage) FindTickerByID(id int) (Ticker, error) { +func (_m *MockStorage) FindTickerByID(id int) (Ticker, error) { ret := _m.Called(id) var r0 Ticker + var r1 error + if rf, ok := ret.Get(0).(func(int) (Ticker, error)); ok { + return rf(id) + } if rf, ok := ret.Get(0).(func(int) Ticker); ok { r0 = rf(id) } else { r0 = ret.Get(0).(Ticker) } - var r1 error if rf, ok := ret.Get(1).(func(int) error); ok { r1 = rf(id) } else { @@ -267,10 +264,14 @@ func (_m *MockTickerStorage) FindTickerByID(id int) (Ticker, error) { } // FindTickers provides a mock function with given fields: -func (_m *MockTickerStorage) FindTickers() ([]Ticker, error) { +func (_m *MockStorage) FindTickers() ([]Ticker, error) { ret := _m.Called() var r0 []Ticker + var r1 error + if rf, ok := ret.Get(0).(func() ([]Ticker, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() []Ticker); ok { r0 = rf() } else { @@ -279,7 +280,6 @@ func (_m *MockTickerStorage) FindTickers() ([]Ticker, error) { } } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -290,10 +290,14 @@ func (_m *MockTickerStorage) FindTickers() ([]Ticker, error) { } // FindTickersByIDs provides a mock function with given fields: ids -func (_m *MockTickerStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { +func (_m *MockStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { ret := _m.Called(ids) var r0 []Ticker + var r1 error + if rf, ok := ret.Get(0).(func([]int) ([]Ticker, error)); ok { + return rf(ids) + } if rf, ok := ret.Get(0).(func([]int) []Ticker); ok { r0 = rf(ids) } else { @@ -302,7 +306,6 @@ func (_m *MockTickerStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { } } - var r1 error if rf, ok := ret.Get(1).(func([]int) error); ok { r1 = rf(ids) } else { @@ -313,17 +316,20 @@ func (_m *MockTickerStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { } // FindUploadByUUID provides a mock function with given fields: uuid -func (_m *MockTickerStorage) FindUploadByUUID(uuid string) (Upload, error) { +func (_m *MockStorage) FindUploadByUUID(uuid string) (Upload, error) { ret := _m.Called(uuid) var r0 Upload + var r1 error + if rf, ok := ret.Get(0).(func(string) (Upload, error)); ok { + return rf(uuid) + } if rf, ok := ret.Get(0).(func(string) Upload); ok { r0 = rf(uuid) } else { r0 = ret.Get(0).(Upload) } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(uuid) } else { @@ -334,10 +340,14 @@ func (_m *MockTickerStorage) FindUploadByUUID(uuid string) (Upload, error) { } // FindUploadsByIDs provides a mock function with given fields: ids -func (_m *MockTickerStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { +func (_m *MockStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { ret := _m.Called(ids) var r0 []Upload + var r1 error + if rf, ok := ret.Get(0).(func([]int) ([]Upload, error)); ok { + return rf(ids) + } if rf, ok := ret.Get(0).(func([]int) []Upload); ok { r0 = rf(ids) } else { @@ -346,7 +356,6 @@ func (_m *MockTickerStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { } } - var r1 error if rf, ok := ret.Get(1).(func([]int) error); ok { r1 = rf(ids) } else { @@ -356,34 +365,21 @@ func (_m *MockTickerStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { return r0, r1 } -// FindUploadsByMessage provides a mock function with given fields: message -func (_m *MockTickerStorage) FindUploadsByMessage(message Message) []Upload { - ret := _m.Called(message) - - var r0 []Upload - if rf, ok := ret.Get(0).(func(Message) []Upload); ok { - r0 = rf(message) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Upload) - } - } - - return r0 -} - // FindUserByEmail provides a mock function with given fields: email -func (_m *MockTickerStorage) FindUserByEmail(email string) (User, error) { +func (_m *MockStorage) FindUserByEmail(email string) (User, error) { ret := _m.Called(email) var r0 User + var r1 error + if rf, ok := ret.Get(0).(func(string) (User, error)); ok { + return rf(email) + } if rf, ok := ret.Get(0).(func(string) User); ok { r0 = rf(email) } else { r0 = ret.Get(0).(User) } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(email) } else { @@ -394,17 +390,20 @@ func (_m *MockTickerStorage) FindUserByEmail(email string) (User, error) { } // FindUserByID provides a mock function with given fields: id -func (_m *MockTickerStorage) FindUserByID(id int) (User, error) { +func (_m *MockStorage) FindUserByID(id int) (User, error) { ret := _m.Called(id) var r0 User + var r1 error + if rf, ok := ret.Get(0).(func(int) (User, error)); ok { + return rf(id) + } if rf, ok := ret.Get(0).(func(int) User); ok { r0 = rf(id) } else { r0 = ret.Get(0).(User) } - var r1 error if rf, ok := ret.Get(1).(func(int) error); ok { r1 = rf(id) } else { @@ -415,10 +414,14 @@ func (_m *MockTickerStorage) FindUserByID(id int) (User, error) { } // FindUsers provides a mock function with given fields: -func (_m *MockTickerStorage) FindUsers() ([]User, error) { +func (_m *MockStorage) FindUsers() ([]User, error) { ret := _m.Called() var r0 []User + var r1 error + if rf, ok := ret.Get(0).(func() ([]User, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() []User); ok { r0 = rf() } else { @@ -427,7 +430,6 @@ func (_m *MockTickerStorage) FindUsers() ([]User, error) { } } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -437,22 +439,25 @@ func (_m *MockTickerStorage) FindUsers() ([]User, error) { return r0, r1 } -// FindUsersByTicker provides a mock function with given fields: ticker -func (_m *MockTickerStorage) FindUsersByTicker(ticker Ticker) ([]User, error) { - ret := _m.Called(ticker) +// FindUsersByIDs provides a mock function with given fields: ids +func (_m *MockStorage) FindUsersByIDs(ids []int) ([]User, error) { + ret := _m.Called(ids) var r0 []User - if rf, ok := ret.Get(0).(func(Ticker) []User); ok { - r0 = rf(ticker) + var r1 error + if rf, ok := ret.Get(0).(func([]int) ([]User, error)); ok { + return rf(ids) + } + if rf, ok := ret.Get(0).(func([]int) []User); ok { + r0 = rf(ids) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]User) } } - var r1 error - if rf, ok := ret.Get(1).(func(Ticker) error); ok { - r1 = rf(ticker) + if rf, ok := ret.Get(1).(func([]int) error); ok { + r1 = rf(ids) } else { r1 = ret.Error(1) } @@ -460,64 +465,62 @@ func (_m *MockTickerStorage) FindUsersByTicker(ticker Ticker) ([]User, error) { return r0, r1 } -// GetInactiveSetting provides a mock function with given fields: -func (_m *MockTickerStorage) GetInactiveSetting() Setting { - ret := _m.Called() +// FindUsersByTicker provides a mock function with given fields: ticker +func (_m *MockStorage) FindUsersByTicker(ticker Ticker) ([]User, error) { + ret := _m.Called(ticker) - var r0 Setting - if rf, ok := ret.Get(0).(func() Setting); ok { - r0 = rf() + var r0 []User + var r1 error + if rf, ok := ret.Get(0).(func(Ticker) ([]User, error)); ok { + return rf(ticker) + } + if rf, ok := ret.Get(0).(func(Ticker) []User); ok { + r0 = rf(ticker) } else { - r0 = ret.Get(0).(Setting) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]User) + } } - return r0 -} - -// GetRefreshIntervalSetting provides a mock function with given fields: -func (_m *MockTickerStorage) GetRefreshIntervalSetting() Setting { - ret := _m.Called() - - var r0 Setting - if rf, ok := ret.Get(0).(func() Setting); ok { - r0 = rf() + if rf, ok := ret.Get(1).(func(Ticker) error); ok { + r1 = rf(ticker) } else { - r0 = ret.Get(0).(Setting) + r1 = ret.Error(1) } - return r0 + return r0, r1 } -// GetRefreshIntervalSettingValue provides a mock function with given fields: -func (_m *MockTickerStorage) GetRefreshIntervalSettingValue() int { +// GetInactiveSettings provides a mock function with given fields: +func (_m *MockStorage) GetInactiveSettings() InactiveSettings { ret := _m.Called() - var r0 int - if rf, ok := ret.Get(0).(func() int); ok { + var r0 InactiveSettings + if rf, ok := ret.Get(0).(func() InactiveSettings); ok { r0 = rf() } else { - r0 = ret.Get(0).(int) + r0 = ret.Get(0).(InactiveSettings) } return r0 } -// RemoveTickerFromUser provides a mock function with given fields: ticker, user -func (_m *MockTickerStorage) RemoveTickerFromUser(ticker Ticker, user User) error { - ret := _m.Called(ticker, user) +// GetRefreshIntervalSettings provides a mock function with given fields: +func (_m *MockStorage) GetRefreshIntervalSettings() RefreshIntervalSettings { + ret := _m.Called() - var r0 error - if rf, ok := ret.Get(0).(func(Ticker, User) error); ok { - r0 = rf(ticker, user) + var r0 RefreshIntervalSettings + if rf, ok := ret.Get(0).(func() RefreshIntervalSettings); ok { + r0 = rf() } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(RefreshIntervalSettings) } return r0 } -// SaveInactiveSetting provides a mock function with given fields: inactiveSettings -func (_m *MockTickerStorage) SaveInactiveSetting(inactiveSettings InactiveSettings) error { +// SaveInactiveSettings provides a mock function with given fields: inactiveSettings +func (_m *MockStorage) SaveInactiveSettings(inactiveSettings InactiveSettings) error { ret := _m.Called(inactiveSettings) var r0 error @@ -531,7 +534,7 @@ func (_m *MockTickerStorage) SaveInactiveSetting(inactiveSettings InactiveSettin } // SaveMessage provides a mock function with given fields: message -func (_m *MockTickerStorage) SaveMessage(message *Message) error { +func (_m *MockStorage) SaveMessage(message *Message) error { ret := _m.Called(message) var r0 error @@ -544,12 +547,12 @@ func (_m *MockTickerStorage) SaveMessage(message *Message) error { return r0 } -// SaveRefreshInterval provides a mock function with given fields: refreshInterval -func (_m *MockTickerStorage) SaveRefreshInterval(refreshInterval float64) error { +// SaveRefreshIntervalSettings provides a mock function with given fields: refreshInterval +func (_m *MockStorage) SaveRefreshIntervalSettings(refreshInterval RefreshIntervalSettings) error { ret := _m.Called(refreshInterval) var r0 error - if rf, ok := ret.Get(0).(func(float64) error); ok { + if rf, ok := ret.Get(0).(func(RefreshIntervalSettings) error); ok { r0 = rf(refreshInterval) } else { r0 = ret.Error(0) @@ -559,7 +562,7 @@ func (_m *MockTickerStorage) SaveRefreshInterval(refreshInterval float64) error } // SaveTicker provides a mock function with given fields: ticker -func (_m *MockTickerStorage) SaveTicker(ticker *Ticker) error { +func (_m *MockStorage) SaveTicker(ticker *Ticker) error { ret := _m.Called(ticker) var r0 error @@ -573,7 +576,7 @@ func (_m *MockTickerStorage) SaveTicker(ticker *Ticker) error { } // SaveUpload provides a mock function with given fields: upload -func (_m *MockTickerStorage) SaveUpload(upload *Upload) error { +func (_m *MockStorage) SaveUpload(upload *Upload) error { ret := _m.Called(upload) var r0 error @@ -587,7 +590,7 @@ func (_m *MockTickerStorage) SaveUpload(upload *Upload) error { } // SaveUser provides a mock function with given fields: user -func (_m *MockTickerStorage) SaveUser(user *User) error { +func (_m *MockStorage) SaveUser(user *User) error { ret := _m.Called(user) var r0 error @@ -601,7 +604,7 @@ func (_m *MockTickerStorage) SaveUser(user *User) error { } // UploadPath provides a mock function with given fields: -func (_m *MockTickerStorage) UploadPath() string { +func (_m *MockStorage) UploadPath() string { ret := _m.Called() var r0 string @@ -614,14 +617,13 @@ func (_m *MockTickerStorage) UploadPath() string { return r0 } -type mockConstructorTestingTNewMockTickerStorage interface { +// NewMockStorage creates a new instance of MockStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStorage(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockTickerStorage creates a new instance of MockTickerStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockTickerStorage(t mockConstructorTestingTNewMockTickerStorage) *MockTickerStorage { - mock := &MockTickerStorage{} +}) *MockStorage { + mock := &MockStorage{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/internal/storage/setting.go b/internal/storage/setting.go index b49ad022..1cf3df80 100644 --- a/internal/storage/setting.go +++ b/internal/storage/setting.go @@ -1,22 +1,26 @@ package storage +import "time" + const ( - SettingInactiveName = `inactive_settings` - SettingRefreshInterval = `refresh_interval` - SettingInactiveHeadline = `The ticker is currently inactive.` - SettingInactiveSubHeadline = `Please contact us if you want to use it.` - SettingInactiveDescription = `...` - SettingInactiveAuthor = `systemli.org Ticker Team` - SettingInactiveEmail = `admin@systemli.org` - SettingInactiveHomepage = `https://www.systemli.org/` - SettingInactiveTwitter = `systemli` - SettingDefaultRefreshInterval float64 = 10000 + SettingInactiveName = `inactive_settings` + SettingRefreshInterval = `refresh_interval` + SettingInactiveHeadline = `The ticker is currently inactive.` + SettingInactiveSubHeadline = `Please contact us if you want to use it.` + SettingInactiveDescription = `...` + SettingInactiveAuthor = `systemli.org Ticker Team` + SettingInactiveEmail = `admin@systemli.org` + SettingInactiveHomepage = `https://www.systemli.org/` + SettingInactiveTwitter = `systemli` + SettingDefaultRefreshInterval int = 10000 ) type Setting struct { - ID int `storm:"id,increment"` - Name string `storm:"unique"` - Value interface{} + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + Name string `gorm:"unique"` + Value string `gorm:"type:json"` } type InactiveSettings struct { @@ -30,23 +34,11 @@ type InactiveSettings struct { } type RefreshIntervalSettings struct { - RefreshInterval float64 `json:"refresh_interval" binding:"required"` -} - -func NewSetting(name string, value interface{}) Setting { - return Setting{Name: name, Value: value} -} - -func DefaultRefreshIntervalSetting() Setting { - return NewSetting(SettingRefreshInterval, DefaultRefreshIntervalSettings()) -} - -func DefaultRefreshIntervalSettings() float64 { - return SettingDefaultRefreshInterval + RefreshInterval int `json:"refresh_interval" binding:"required"` } -func DefaultInactiveSetting() Setting { - return NewSetting(SettingInactiveName, DefaultInactiveSettings()) +func DefaultRefreshIntervalSettings() RefreshIntervalSettings { + return RefreshIntervalSettings{RefreshInterval: SettingDefaultRefreshInterval} } func DefaultInactiveSettings() InactiveSettings { diff --git a/internal/storage/sql_storage.go b/internal/storage/sql_storage.go new file mode 100644 index 00000000..d5adadcc --- /dev/null +++ b/internal/storage/sql_storage.go @@ -0,0 +1,282 @@ +package storage + +import ( + "encoding/json" + "os" + + "github.com/systemli/ticker/internal/api/pagination" + "gorm.io/gorm" +) + +type SqlStorage struct { + db *gorm.DB + uploadPath string +} + +func NewSqlStorage(db *gorm.DB, uploadPath string) *SqlStorage { + return &SqlStorage{ + db: db, + uploadPath: uploadPath, + } +} + +func (s *SqlStorage) CountUser() (int, error) { + var count int64 + err := s.db.Model(&User{}).Count(&count).Error + + return int(count), err +} + +func (s *SqlStorage) FindUsers() ([]User, error) { + users := make([]User, 0) + err := s.db.Find(&users).Error + + return users, err +} + +func (s *SqlStorage) FindUserByID(id int) (User, error) { + var user User + + err := s.db.First(&user, id).Error + + return user, err +} + +func (s *SqlStorage) FindUsersByIDs(ids []int) ([]User, error) { + users := make([]User, 0) + err := s.db.Find(&users, ids).Error + + return users, err +} + +func (s *SqlStorage) FindUsersByTicker(ticker Ticker) ([]User, error) { + users := make([]User, 0) + err := s.db.Model(&ticker).Association("Users").Find(&users) + + return users, err +} + +func (s *SqlStorage) FindUserByEmail(email string) (User, error) { + var user User + + err := s.db.First(&user, "email = ?", email).Error + + return user, err +} + +func (s *SqlStorage) SaveUser(user *User) error { + return s.db.Save(user).Error +} + +func (s *SqlStorage) DeleteUser(user User) error { + return s.db.Delete(&user).Error +} + +func (s *SqlStorage) DeleteTickerUser(ticker *Ticker, user *User) error { + err := s.db.Model(ticker).Association("Users").Delete(user) + + return err +} + +func (s *SqlStorage) FindTickers() ([]Ticker, error) { + tickers := make([]Ticker, 0) + err := s.db.Find(&tickers).Error + + return tickers, err +} + +func (s *SqlStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { + tickers := make([]Ticker, 0) + err := s.db.Find(&tickers, ids).Error + + return tickers, err +} + +func (s *SqlStorage) FindTickerByDomain(domain string) (Ticker, error) { + var ticker Ticker + + err := s.db.First(&ticker, "domain = ?", domain).Error + + return ticker, err +} + +func (s *SqlStorage) FindTickerByID(id int) (Ticker, error) { + var ticker Ticker + + err := s.db.First(&ticker, id).Error + + return ticker, err +} + +func (s *SqlStorage) SaveTicker(ticker *Ticker) error { + return s.db.Save(ticker).Error +} + +func (s *SqlStorage) DeleteTicker(ticker Ticker) error { + return s.db.Delete(&ticker).Error +} + +func (s *SqlStorage) FindUploadByUUID(uuid string) (Upload, error) { + var upload Upload + + err := s.db.First(&upload, "uuid = ?", uuid).Error + + return upload, err +} + +func (s *SqlStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { + uploads := make([]Upload, 0) + err := s.db.Find(&uploads, ids).Error + + return uploads, err +} + +func (s *SqlStorage) UploadPath() string { + return s.uploadPath +} + +func (s *SqlStorage) SaveUpload(upload *Upload) error { + return s.db.Save(upload).Error +} + +func (s *SqlStorage) DeleteUpload(upload Upload) error { + var err error + + if err = os.Remove(upload.FullPath(s.uploadPath)); err != nil { + log.WithError(err).WithField("upload", upload).Error("failed to delete upload file") + } + + if err = s.db.Delete(&upload).Error; err != nil { + log.WithError(err).WithField("upload", upload).Error("failed to delete upload from database") + } + + return err +} + +func (s *SqlStorage) DeleteUploads(uploads []Upload) { + for _, upload := range uploads { + if err := s.DeleteUpload(upload); err != nil { + log.WithError(err).WithField("upload", upload).Error("failed to delete upload") + } + } +} + +func (s *SqlStorage) DeleteUploadsByTicker(ticker Ticker) error { + uploads := make([]Upload, 0) + s.db.Model(&Upload{}).Where("ticker_id = ?", ticker.ID).Find(&uploads) + + for _, upload := range uploads { + if err := s.DeleteUpload(upload); err != nil { + return err + } + } + + return nil +} + +func (s *SqlStorage) FindMessage(tickerID, messageID int) (Message, error) { + var message Message + + err := s.db.First(&message, "ticker_id = ? AND id = ?", tickerID, messageID).Error + + return message, err +} + +func (s *SqlStorage) FindMessagesByTicker(ticker Ticker) ([]Message, error) { + messages := make([]Message, 0) + err := s.db.Model(&Message{}).Where("ticker_id = ?", ticker.ID).Find(&messages).Error + + return messages, err +} + +func (s *SqlStorage) FindMessagesByTickerAndPagination(ticker Ticker, pagination pagination.Pagination) ([]Message, error) { + messages := make([]Message, 0) + query := s.db.Where("ticker_id = ?", ticker.ID) + + if pagination.GetBefore() > 0 { + query = query.Where("id < ?", pagination.GetBefore()) + } else if pagination.GetAfter() > 0 { + query = query.Where("id > ?", pagination.GetAfter()) + } + + err := query.Order("id desc").Limit(pagination.GetLimit()).Find(&messages).Error + return messages, err +} + +func (s *SqlStorage) SaveMessage(message *Message) error { + return s.db.Save(message).Error +} + +func (s *SqlStorage) DeleteMessage(message Message) error { + return s.db.Delete(&message).Error +} + +func (s *SqlStorage) DeleteMessages(ticker Ticker) error { + err := s.db.Where("ticker_id = ?", ticker.ID).Delete(&Message{}).Error + + return err +} + +func (s *SqlStorage) GetInactiveSettings() InactiveSettings { + var setting Setting + err := s.db.First(&setting, "name = ?", SettingInactiveName).Error + if err != nil { + return DefaultInactiveSettings() + } + + var inactiveSettings InactiveSettings + err = json.Unmarshal([]byte(setting.Value), &inactiveSettings) + if err != nil { + return DefaultInactiveSettings() + } + + return inactiveSettings +} + +func (s *SqlStorage) GetRefreshIntervalSettings() RefreshIntervalSettings { + var setting Setting + err := s.db.First(&setting, "name = ?", SettingRefreshInterval).Error + if err != nil { + return DefaultRefreshIntervalSettings() + } + + var refreshIntervalSettings RefreshIntervalSettings + err = json.Unmarshal([]byte(setting.Value), &refreshIntervalSettings) + if err != nil { + return DefaultRefreshIntervalSettings() + } + + return refreshIntervalSettings +} + +func (s *SqlStorage) SaveInactiveSettings(inactiveSettings InactiveSettings) error { + var setting Setting + err := s.db.First(&setting, "name = ?", SettingInactiveName).Error + if err != nil { + setting = Setting{Name: SettingInactiveName} + } + + value, err := json.Marshal(inactiveSettings) + if err != nil { + return err + } + setting.Value = string(value) + + return s.db.Save(&setting).Error +} + +func (s *SqlStorage) SaveRefreshIntervalSettings(refreshInterval RefreshIntervalSettings) error { + var setting Setting + err := s.db.First(&setting, "name = ?", SettingRefreshInterval).Error + if err != nil { + setting = Setting{Name: SettingRefreshInterval} + } + + value, err := json.Marshal(refreshInterval) + if err != nil { + return err + } + setting.Value = string(value) + + return s.db.Save(&setting).Error +} diff --git a/internal/storage/sql_storage_test.go b/internal/storage/sql_storage_test.go new file mode 100644 index 00000000..b971cf48 --- /dev/null +++ b/internal/storage/sql_storage_test.go @@ -0,0 +1,592 @@ +package storage + +import ( + "net/http" + "net/url" + "testing" + + "github.com/gin-gonic/gin" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + pagination "github.com/systemli/ticker/internal/api/pagination" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestSqlStorage(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "SqlStorage Suite") +} + +var _ = Describe("SqlStorage", func() { + db, err := gorm.Open(sqlite.Open("ticker.db"), &gorm.Config{}) + Expect(err).ToNot(HaveOccurred()) + + var storage = NewSqlStorage(db, "/uploads") + + err = db.AutoMigrate( + &Ticker{}, + &TickerInformation{}, + &TickerTelegram{}, + &TickerMastodon{}, + &User{}, + &Message{}, + &Upload{}, + &Attachment{}, + &Setting{}, + ) + Expect(err).ToNot(HaveOccurred()) + + BeforeEach(func() { + db.Exec("DELETE FROM users") + db.Exec("DELETE FROM messages") + db.Exec("DELETE FROM attachments") + db.Exec("DELETE FROM tickers") + db.Exec("DELETE FROM settings") + db.Exec("DELETE FROM uploads") + }) + + Describe("CountUser", func() { + It("returns the number of users", func() { + Expect(storage.CountUser()).To(Equal(0)) + + err := db.Create(&User{}).Error + Expect(err).ToNot(HaveOccurred()) + Expect(storage.CountUser()).To(Equal(1)) + }) + }) + + Describe("FindUsers", func() { + It("returns all users", func() { + users, err := storage.FindUsers() + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(BeEmpty()) + + err = db.Create(&User{}).Error + Expect(err).ToNot(HaveOccurred()) + + users, err = storage.FindUsers() + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(HaveLen(1)) + }) + }) + + Describe("FindUserByID", func() { + It("returns the user with the given id", func() { + user, err := storage.FindUserByID(1) + Expect(err).To(HaveOccurred()) + Expect(user).To(BeZero()) + + err = db.Create(&User{}).Error + Expect(err).ToNot(HaveOccurred()) + + user, err = storage.FindUserByID(1) + Expect(err).ToNot(HaveOccurred()) + Expect(user).ToNot(BeZero()) + }) + }) + + Describe("FindUsersByIDs", func() { + It("returns the users with the given ids", func() { + users, err := storage.FindUsersByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(BeEmpty()) + + err = db.Create(&User{}).Error + Expect(err).ToNot(HaveOccurred()) + + users, err = storage.FindUsersByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(HaveLen(1)) + }) + }) + + Describe("FindUserByEmail", func() { + It("returns the user with the given email", func() { + user, err := storage.FindUserByEmail("user@systemli.org") + Expect(err).To(HaveOccurred()) + Expect(user).To(BeZero()) + + err = db.Create(&User{Email: "user@systemli.org"}).Error + Expect(err).ToNot(HaveOccurred()) + + user, err = storage.FindUserByEmail("user@systemli.org") + Expect(err).ToNot(HaveOccurred()) + Expect(user).ToNot(BeZero()) + }) + }) + + Describe("FindUsersByTicker", func() { + It("returns the users with the given ticker", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + users, err := storage.FindUsersByTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(BeEmpty()) + + user, err := NewUser("user@systemli.org", "password") + Expect(err).ToNot(HaveOccurred()) + err = storage.SaveUser(&user) + Expect(err).ToNot(HaveOccurred()) + + ticker.Users = append(ticker.Users, user) + err = storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + users, err = storage.FindUsersByTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + Expect(users).To(HaveLen(1)) + }) + }) + + Describe("SaveUser", func() { + It("persists the user", func() { + user, err := NewUser("user@systemli.org", "password") + Expect(err).ToNot(HaveOccurred()) + + err = storage.SaveUser(&user) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&User{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + }) + }) + + Describe("DeleteUser", func() { + It("deletes the user", func() { + user, err := NewUser("user@systemli.org", "password") + Expect(err).ToNot(HaveOccurred()) + + err = storage.SaveUser(&user) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&User{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteUser(user) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&User{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("DeleteTickerUser", func() { + It("deletes the user from the ticker", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + user, err := NewUser("user@systemli.org", "password") + Expect(err).ToNot(HaveOccurred()) + err = storage.SaveUser(&user) + Expect(err).ToNot(HaveOccurred()) + + ticker.Users = append(ticker.Users, user) + err = storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&User{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteTickerUser(&ticker, &user) + Expect(err).ToNot(HaveOccurred()) + Expect(ticker.Users).To(BeEmpty()) + }) + }) + + Describe("FindTickers", func() { + It("returns all tickers", func() { + tickers, err := storage.FindTickers() + Expect(err).ToNot(HaveOccurred()) + Expect(tickers).To(BeEmpty()) + + err = db.Create(&Ticker{}).Error + Expect(err).ToNot(HaveOccurred()) + + tickers, err = storage.FindTickers() + Expect(err).ToNot(HaveOccurred()) + Expect(tickers).To(HaveLen(1)) + }) + }) + + Describe("FindTickersByIDs", func() { + It("returns the tickers with the given ids", func() { + tickers, err := storage.FindTickersByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(tickers).To(BeEmpty()) + + err = db.Create(&Ticker{}).Error + Expect(err).ToNot(HaveOccurred()) + + tickers, err = storage.FindTickersByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(tickers).To(HaveLen(1)) + }) + }) + + Describe("FindTickerByID", func() { + It("returns the ticker with the given id", func() { + ticker, err := storage.FindTickerByID(1) + Expect(err).To(HaveOccurred()) + Expect(ticker).To(BeZero()) + + err = db.Create(&Ticker{}).Error + Expect(err).ToNot(HaveOccurred()) + + ticker, err = storage.FindTickerByID(1) + Expect(err).ToNot(HaveOccurred()) + Expect(ticker).ToNot(BeZero()) + }) + }) + + Describe("FindTickerByDomain", func() { + It("returns the ticker with the given domain", func() { + ticker, err := storage.FindTickerByDomain("systemli.org") + Expect(err).To(HaveOccurred()) + Expect(ticker).To(BeZero()) + + err = db.Create(&Ticker{Domain: "systemli.org"}).Error + Expect(err).ToNot(HaveOccurred()) + + ticker, err = storage.FindTickerByDomain("systemli.org") + Expect(err).ToNot(HaveOccurred()) + Expect(ticker).ToNot(BeZero()) + }) + }) + + Describe("SaveTicker", func() { + It("persists the ticker", func() { + ticker := NewTicker() + + err = storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Ticker{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + }) + }) + + Describe("DeleteTicker", func() { + It("deletes the ticker", func() { + ticker := NewTicker() + + err = storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Ticker{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&Ticker{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("FindUploadByUUID", func() { + It("returns the upload with the given uuid", func() { + upload, err := storage.FindUploadByUUID("uuid") + Expect(err).To(HaveOccurred()) + Expect(upload).To(BeZero()) + + err = db.Create(&Upload{UUID: "uuid"}).Error + Expect(err).ToNot(HaveOccurred()) + + upload, err = storage.FindUploadByUUID("uuid") + Expect(err).ToNot(HaveOccurred()) + Expect(upload).ToNot(BeZero()) + }) + }) + + Describe("FindUploadsByIDs", func() { + It("returns the uploads with the given ids", func() { + uploads, err := storage.FindUploadsByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(uploads).To(BeEmpty()) + + err = db.Create(&Upload{}).Error + Expect(err).ToNot(HaveOccurred()) + + uploads, err = storage.FindUploadsByIDs([]int{1, 2}) + Expect(err).ToNot(HaveOccurred()) + Expect(uploads).To(HaveLen(1)) + }) + }) + + Describe("SaveUpload", func() { + It("persists the upload", func() { + upload := NewUpload("image.jpg", "content-type", 1) + + err = storage.SaveUpload(&upload) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + }) + }) + + Describe("DeleteUpload", func() { + It("deletes the upload", func() { + upload := NewUpload("image.jpg", "content-type", 1) + + err = storage.SaveUpload(&upload) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteUpload(upload) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("DeleteUploads", func() { + It("deletes the uploads", func() { + upload := NewUpload("image.jpg", "content-type", 1) + + err = storage.SaveUpload(&upload) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + uploads := []Upload{upload} + storage.DeleteUploads(uploads) + + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("DeleteUploadsByTicker", func() { + It("deletes the uploads", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + upload := NewUpload("image.jpg", "content-type", ticker.ID) + err = storage.SaveUpload(&upload) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteUploadsByTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&Upload{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("FindMessage", func() { + It("returns the message with the given id", func() { + message, err := storage.FindMessage(1, 1) + Expect(err).To(HaveOccurred()) + Expect(message).To(BeZero()) + + err = db.Create(&Message{ID: 1, TickerID: 1}).Error + Expect(err).ToNot(HaveOccurred()) + + message, err = storage.FindMessage(1, 1) + Expect(err).ToNot(HaveOccurred()) + Expect(message).ToNot(BeZero()) + }) + }) + + Describe("FindMessagesByTicker", func() { + It("returns the messages with the given ticker", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + messages, err := storage.FindMessagesByTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(BeEmpty()) + + err = db.Create(&Message{TickerID: ticker.ID}).Error + Expect(err).ToNot(HaveOccurred()) + + messages, err = storage.FindMessagesByTicker(ticker) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(HaveLen(1)) + }) + }) + + Describe("FindMessagesByTickerAndPagination", func() { + It("returns the messages with the given ticker and pagination", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + c := &gin.Context{} + p := pagination.NewPagination(c) + messages, err := storage.FindMessagesByTickerAndPagination(ticker, *p) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(BeEmpty()) + + err = db.Create(&Message{TickerID: ticker.ID}).Error + Expect(err).ToNot(HaveOccurred()) + + messages, err = storage.FindMessagesByTickerAndPagination(ticker, *p) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(HaveLen(1)) + + err = db.Create([]Message{ + {TickerID: ticker.ID, ID: 2}, + {TickerID: ticker.ID, ID: 3}, + {TickerID: ticker.ID, ID: 4}, + }).Error + Expect(err).ToNot(HaveOccurred()) + + c = &gin.Context{} + c.Request = &http.Request{URL: &url.URL{RawQuery: "limit=2"}} + p = pagination.NewPagination(c) + messages, err = storage.FindMessagesByTickerAndPagination(ticker, *p) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(HaveLen(2)) + + c = &gin.Context{} + c.Request = &http.Request{URL: &url.URL{RawQuery: "limit=2&after=2"}} + p = pagination.NewPagination(c) + messages, err = storage.FindMessagesByTickerAndPagination(ticker, *p) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(HaveLen(2)) + + c = &gin.Context{} + c.Request = &http.Request{URL: &url.URL{RawQuery: "limit=2&before=4"}} + p = pagination.NewPagination(c) + messages, err = storage.FindMessagesByTickerAndPagination(ticker, *p) + Expect(err).ToNot(HaveOccurred()) + Expect(messages).To(HaveLen(2)) + }) + }) + + Describe("SaveMessage", func() { + It("persists the message", func() { + message := NewMessage() + + err = storage.SaveMessage(&message) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Message{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + }) + }) + + Describe("DeleteMessage", func() { + It("deletes the message", func() { + message := NewMessage() + + err = storage.SaveMessage(&message) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Message{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteMessage(message) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&Message{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("DeleteMessages", func() { + It("deletes the messages", func() { + ticker := NewTicker() + err := storage.SaveTicker(&ticker) + Expect(err).ToNot(HaveOccurred()) + + message := NewMessage() + message.TickerID = ticker.ID + err = storage.SaveMessage(&message) + Expect(err).ToNot(HaveOccurred()) + + var count int64 + err = db.Model(&Message{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(1))) + + err = storage.DeleteMessages(ticker) + Expect(err).ToNot(HaveOccurred()) + + err = db.Model(&Message{}).Count(&count).Error + Expect(err).ToNot(HaveOccurred()) + Expect(count).To(Equal(int64(0))) + }) + }) + + Describe("GetInactiveSettings", func() { + It("returns the default inactive setting", func() { + setting := storage.GetInactiveSettings() + Expect(setting.Author).To(Equal(DefaultInactiveSettings().Author)) + }) + + It("returns the inactive setting", func() { + settings := InactiveSettings{ + Author: "author", + } + + err = storage.SaveInactiveSettings(settings) + Expect(err).ToNot(HaveOccurred()) + + setting := storage.GetInactiveSettings() + Expect(setting.Author).To(Equal(settings.Author)) + }) + }) + + Describe("GetRefreshIntervalSetting", func() { + It("returns the default refresh interval setting", func() { + setting := storage.GetRefreshIntervalSettings() + Expect(setting.RefreshInterval).To(Equal(DefaultRefreshIntervalSettings().RefreshInterval)) + }) + + It("returns the refresh interval setting", func() { + settings := RefreshIntervalSettings{ + RefreshInterval: 1000, + } + + err = storage.SaveRefreshIntervalSettings(settings) + Expect(err).ToNot(HaveOccurred()) + + setting := storage.GetRefreshIntervalSettings() + Expect(setting.RefreshInterval).To(Equal(settings.RefreshInterval)) + }) + }) +}) diff --git a/internal/storage/storage.go b/internal/storage/storage.go index fa88fc30..1bada116 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -11,19 +11,18 @@ type Storage interface { CountUser() (int, error) FindUsers() ([]User, error) FindUserByID(id int) (User, error) + FindUsersByIDs(ids []int) ([]User, error) FindUserByEmail(email string) (User, error) FindUsersByTicker(ticker Ticker) ([]User, error) SaveUser(user *User) error DeleteUser(user User) error - AddUsersToTicker(ticker Ticker, ids []int) error - RemoveTickerFromUser(ticker Ticker, user User) error + DeleteTickerUser(ticker *Ticker, user *User) error FindTickers() ([]Ticker, error) FindTickersByIDs(ids []int) ([]Ticker, error) FindTickerByDomain(domain string) (Ticker, error) FindTickerByID(id int) (Ticker, error) SaveTicker(ticker *Ticker) error DeleteTicker(ticker Ticker) error - FindUploadsByMessage(message Message) []Upload SaveUpload(upload *Upload) error DeleteUpload(upload Upload) error DeleteUploads(uploads []Upload) @@ -34,12 +33,10 @@ type Storage interface { SaveMessage(message *Message) error DeleteMessage(message Message) error DeleteMessages(ticker Ticker) error - FindSetting(name string) (Setting, error) - GetInactiveSetting() Setting - GetRefreshIntervalSetting() Setting - GetRefreshIntervalSettingValue() int - SaveInactiveSetting(inactiveSettings InactiveSettings) error - SaveRefreshInterval(refreshInterval float64) error + GetInactiveSettings() InactiveSettings + GetRefreshIntervalSettings() RefreshIntervalSettings + SaveInactiveSettings(inactiveSettings InactiveSettings) error + SaveRefreshIntervalSettings(refreshInterval RefreshIntervalSettings) error FindUploadByUUID(uuid string) (Upload, error) FindUploadsByIDs(ids []int) ([]Upload, error) UploadPath() string diff --git a/internal/storage/storm_storage.go b/internal/storage/storm_storage.go deleted file mode 100644 index abc23422..00000000 --- a/internal/storage/storm_storage.go +++ /dev/null @@ -1,367 +0,0 @@ -package storage - -import ( - "os" - - "github.com/asdine/storm" - "github.com/asdine/storm/q" - "github.com/sirupsen/logrus" - "github.com/systemli/ticker/internal/api/pagination" -) - -type StormStorage struct { - db *storm.DB - uploadPath string -} - -func OpenDB(path string) *storm.DB { - db, err := storm.Open(path) - if err != nil { - log.WithError(err).Panic("failed to open database file") - } - - return db -} - -func NewStormStorage(storagePath, uploadPath string) *StormStorage { - return &StormStorage{ - db: OpenDB(storagePath), - uploadPath: uploadPath, - } -} - -func (s *StormStorage) CountUser() (int, error) { - return s.db.Count(&User{}) -} - -func (s *StormStorage) FindUsers() ([]User, error) { - users := make([]User, 0) - err := s.db.Select().Reverse().Find(&users) - if err != nil && err.Error() != "not found" { - return users, err - } - - return users, nil -} - -func (s *StormStorage) FindUserByID(id int) (User, error) { - var user User - - err := s.db.One("ID", id, &user) - - return user, err -} - -func (s *StormStorage) FindUsersByTicker(ticker Ticker) ([]User, error) { - users := make([]User, 0) - err := s.db.Select().Each(new(User), func(record interface{}) error { - u := record.(*User) - - for _, id := range u.Tickers { - if id == ticker.ID { - users = append(users, *u) - } - } - - return nil - }) - - if err != nil { - return users, err - } - - return users, nil -} - -func (s *StormStorage) FindUserByEmail(email string) (User, error) { - var user User - - err := s.db.One("Email", email, &user) - return user, err -} - -func (s *StormStorage) SaveUser(user *User) error { - return s.db.Save(user) -} - -func (s *StormStorage) DeleteUser(user User) error { - return s.db.DeleteStruct(&user) -} - -func (s *StormStorage) AddUsersToTicker(ticker Ticker, ids []int) error { - users := make([]User, 0) - err := s.db.Select(q.In("ID", ids)).Find(&users) - if err != nil { - return err - } - - for _, user := range users { - if user.IsSuperAdmin { - continue - } - user.AddTicker(ticker) - err = s.SaveUser(&user) - } - - return err -} - -func (s *StormStorage) RemoveTickerFromUser(ticker Ticker, user User) error { - user.RemoveTicker(ticker) - - return s.SaveUser(&user) -} - -func (s *StormStorage) FindTickers() ([]Ticker, error) { - tickers := make([]Ticker, 0) - - err := s.db.Select().Reverse().Find(&tickers) - if err != nil && err.Error() != "not found" { - return tickers, err - } - - return tickers, nil -} - -func (s *StormStorage) FindTickersByIDs(ids []int) ([]Ticker, error) { - tickers := make([]Ticker, 0) - - err := s.db.Select(q.In("ID", ids)).Reverse().Find(&tickers) - if err != nil && err.Error() != "not found" { - return tickers, err - } - - return tickers, nil -} - -func (s *StormStorage) FindTickerByDomain(domain string) (Ticker, error) { - var ticker Ticker - - err := s.db.One("Domain", domain, &ticker) - if err != nil { - return ticker, err - } - - return ticker, nil -} - -func (s *StormStorage) FindTickerByID(id int) (Ticker, error) { - var ticker Ticker - - err := s.db.One("ID", id, &ticker) - if err != nil { - return ticker, err - } - - return ticker, nil -} - -func (s *StormStorage) SaveTicker(ticker *Ticker) error { - return s.db.Save(ticker) -} - -func (s *StormStorage) DeleteTicker(ticker Ticker) error { - return s.db.DeleteStruct(&ticker) -} - -func (s *StormStorage) FindUploadsByMessage(message Message) []Upload { - uploads := make([]Upload, 0) - - if len(message.Attachments) > 0 { - var uuids []string - for _, attachment := range message.Attachments { - uuids = append(uuids, attachment.UUID) - } - err := s.db.Select(q.In("UUID", uuids)).Find(&uploads) - if err != nil { - log.WithError(err).Error("failed to find uploads for message") - } - } - - return uploads -} - -func (s *StormStorage) FindUploadByUUID(uuid string) (Upload, error) { - var upload Upload - err := s.db.One("UUID", uuid, &upload) - return upload, err -} - -func (s *StormStorage) FindUploadsByIDs(ids []int) ([]Upload, error) { - uploads := make([]Upload, 0) - err := s.db.Select(q.In("ID", ids)).Find(&uploads) - return uploads, err -} - -func (s *StormStorage) UploadPath() string { - return s.uploadPath -} - -func (s *StormStorage) SaveUpload(upload *Upload) error { - return s.db.Save(upload) -} - -func (s *StormStorage) DeleteUpload(upload Upload) error { - var err error - //TODO: Rework with afero.FS from Config - if err = os.Remove(upload.FullPath(s.uploadPath)); err != nil { - log.WithError(err).WithField("upload", upload).Error("failed to delete upload file") - } - - if err = s.db.DeleteStruct(&upload); err != nil { - log.WithError(err).WithField("upload", upload).Error("failed to delete upload") - } - - return err -} - -func (s *StormStorage) DeleteUploads(uploads []Upload) { - for _, upload := range uploads { - err := s.DeleteUpload(upload) - if err != nil { - log.WithError(err).WithFields(logrus.Fields{"id": upload.ID, "uuid": upload.UUID}).Error("failed to delete upload") - } - } -} - -func (s *StormStorage) DeleteUploadsByTicker(ticker Ticker) error { - err := s.db.Select(q.Eq("TickerID", ticker.ID)).Delete(&Upload{}) - if err != nil && err.Error() == "not found" { - return nil - } - - return err -} - -func (s *StormStorage) FindMessage(tickerID, messageID int) (Message, error) { - var message Message - matcher := q.And(q.Eq("ID", messageID), q.Eq("Ticker", tickerID)) - err := s.db.Select(matcher).First(&message) - return message, err -} - -func (s *StormStorage) FindMessagesByTicker(ticker Ticker) ([]Message, error) { - messages := make([]Message, 0) - - err := s.db.Select(q.Eq("Ticker", ticker.ID)).Reverse().Find(&messages) - if err != nil && err.Error() == "not found" { - return messages, nil - } - return messages, err -} - -func (s *StormStorage) FindMessagesByTickerAndPagination(ticker Ticker, pagination pagination.Pagination) ([]Message, error) { - messages := make([]Message, 0) - - matcher := q.Eq("Ticker", ticker.ID) - if pagination.GetBefore() != 0 { - matcher = q.And(q.Eq("Ticker", ticker.ID), q.Lt("ID", pagination.GetBefore())) - } - if pagination.GetAfter() != 0 { - matcher = q.And(q.Eq("Ticker", ticker.ID), q.Gt("ID", pagination.GetAfter())) - } - - err := s.db.Select(matcher).OrderBy("CreationDate").Limit(pagination.GetLimit()).Reverse().Find(&messages) - if err != nil && err.Error() == "not found" { - return messages, nil - } - return messages, err -} - -func (s *StormStorage) SaveMessage(message *Message) error { - return s.db.Save(message) -} - -func (s *StormStorage) DeleteMessage(message Message) error { - uploads := s.FindUploadsByMessage(message) - - if len(uploads) > 0 { - s.DeleteUploads(uploads) - } - - return s.db.DeleteStruct(&message) -} - -func (s *StormStorage) DeleteMessages(ticker Ticker) error { - var messages []Message - if err := s.db.Find("Ticker", ticker.ID, &messages); err != nil { - log.WithField("error", err).WithField("ticker", ticker.ID).Error("failed find messages for ticker") - return err - } - - for _, message := range messages { - _ = s.DeleteMessage(message) - } - - return nil -} - -func (s *StormStorage) FindSetting(name string) (Setting, error) { - var setting Setting - err := s.db.One("Name", name, &setting) - if err != nil && err.Error() == "not found" { - return setting, err - } - - return setting, nil -} - -func (s *StormStorage) GetInactiveSetting() Setting { - setting, err := s.FindSetting(SettingInactiveName) - if err != nil { - return DefaultInactiveSetting() - } - - return setting -} - -func (s *StormStorage) GetRefreshIntervalSetting() Setting { - setting, err := s.FindSetting(SettingRefreshInterval) - if err != nil { - return DefaultRefreshIntervalSetting() - } - - return setting -} - -func (s *StormStorage) GetRefreshIntervalSettingValue() int { - setting := s.GetRefreshIntervalSetting() - - var value int - switch sv := setting.Value.(type) { - case float64: - value = int(sv) - default: - value = sv.(int) - } - - return value -} - -func (s *StormStorage) SaveInactiveSetting(inactiveSettings InactiveSettings) error { - setting, err := s.FindSetting(SettingInactiveName) - if err != nil { - setting = Setting{Name: SettingInactiveName, Value: inactiveSettings} - } else { - setting.Value = inactiveSettings - } - - return s.db.Save(&setting) -} - -func (s *StormStorage) SaveRefreshInterval(refreshInterval float64) error { - setting, err := s.FindSetting(SettingRefreshInterval) - if err != nil { - setting = Setting{Name: SettingRefreshInterval, Value: refreshInterval} - } else { - setting.Value = refreshInterval - } - return s.db.Save(&setting) -} - -func (s *StormStorage) DropAll() { - _ = s.db.Drop("User") - _ = s.db.Drop("Message") - _ = s.db.Drop("Ticker") - _ = s.db.Drop("Setting") -} diff --git a/internal/storage/storm_storage_test.go b/internal/storage/storm_storage_test.go deleted file mode 100644 index a13c24cb..00000000 --- a/internal/storage/storm_storage_test.go +++ /dev/null @@ -1,400 +0,0 @@ -package storage - -import ( - "fmt" - "net/http" - "net/url" - "os" - "strings" - "testing" - "time" - - "github.com/gin-gonic/gin" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/systemli/ticker/internal/api/pagination" -) - -func TestStorage(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Storage Suite") -} - -var _ = Describe("Storage", func() { - var storagePath = fmt.Sprintf("%s/storage_%d.db", strings.TrimSuffix(os.TempDir(), "/"), time.Now().Unix()) - var storage = NewStormStorage(storagePath, "/uploads") - - BeforeEach(func() { - storage.DropAll() - }) - - When("no ticker is present", func() { - It("shouldn't find any ticker", func() { - tickers, err := storage.FindTickers() - Expect(err).To(BeNil()) - Expect(tickers).To(HaveLen(0)) - }) - }) - - When("one ticker is present", func() { - domain := "ticker.systemli.org" - ticker := Ticker{CreationDate: time.Now(), Domain: domain} - - BeforeEach(func() { - err := storage.SaveTicker(&ticker) - Expect(err).To(BeNil()) - }) - - It("should find one ticker", func() { - tickers, err := storage.FindTickers() - Expect(err).To(BeNil()) - Expect(tickers).To(HaveLen(1)) - }) - - It("should find one ticker by domain", func() { - ticker, err := storage.FindTickerByDomain(domain) - Expect(err).To(BeNil()) - Expect(ticker).NotTo(BeNil()) - }) - - It("should find one ticker by id", func() { - ticker, err := storage.FindTickerByID(1) - Expect(err).To(BeNil()) - Expect(ticker).NotTo(BeNil()) - }) - - It("shouldn't find one ticker by misspelled domain", func() { - _, err := storage.FindTickerByDomain("missspelled") - Expect(err).NotTo(BeNil()) - }) - - It("shouldn't find one ticker by nonexisting id", func() { - _, err := storage.FindTickerByID(100) - Expect(err).NotTo(BeNil()) - }) - - It("should be possible to delete them", func() { - ticker, _ := storage.FindTickerByID(1) - err := storage.DeleteTicker(ticker) - Expect(err).To(BeNil()) - }) - - It("should be possible to add users", func() { - user, _ := NewUser("louis@systemli.org", "password") - ticker, _ := storage.FindTickerByID(1) - err := storage.SaveUser(&user) - Expect(err).To(BeNil()) - - err = storage.AddUsersToTicker(ticker, []int{user.ID}) - Expect(err).To(BeNil()) - user, _ = storage.FindUserByEmail("louis@systemli.org") - Expect(user.Tickers).To(HaveLen(1)) - }) - - It("should not add ticker to admin users", func() { - user, _ := NewAdminUser("louis@systemli.org", "password") - ticker, _ := storage.FindTickerByID(1) - err := storage.SaveUser(&user) - Expect(err).To(BeNil()) - - err = storage.AddUsersToTicker(ticker, []int{user.ID}) - Expect(err).To(BeNil()) - user, _ = storage.FindUserByEmail("louis@systemli.org") - Expect(user.Tickers).To(HaveLen(0)) - }) - - It("should be possible to remove users", func() { - ticker, _ := storage.FindTickerByID(1) - user, _ := NewUser("louis@systemli.org", "password") - user.AddTicker(ticker) - err := storage.SaveUser(&user) - Expect(err).To(BeNil()) - - err = storage.RemoveTickerFromUser(ticker, user) - Expect(err).To(BeNil()) - user, _ = storage.FindUserByEmail("louis@systemli.org") - Expect(user.Tickers).To(HaveLen(0)) - - }) - }) - - When("two tickers are present", func() { - domain1 := "ticker.systemli.org" - domain2 := "ticker.tem.li" - ticker1 := Ticker{CreationDate: time.Now(), Domain: domain1} - ticker2 := Ticker{CreationDate: time.Now(), Domain: domain2} - - BeforeEach(func() { - err := storage.SaveTicker(&ticker1) - Expect(err).To(BeNil()) - err = storage.SaveTicker(&ticker2) - Expect(err).To(BeNil()) - }) - - It("should find two ticker", func() { - tickers, err := storage.FindTickers() - Expect(err).To(BeNil()) - Expect(tickers).To(HaveLen(2)) - }) - - It("should find two tickers by their ids", func() { - tickers, err := storage.FindTickersByIDs([]int{ticker1.ID, ticker2.ID}) - Expect(err).To(BeNil()) - Expect(tickers).To(HaveLen(2)) - }) - }) - - When("one user is present", func() { - email := "user@systemli.org" - user := User{Email: email, Tickers: []int{2}} - - BeforeEach(func() { - err := storage.SaveUser(&user) - Expect(err).To(BeNil()) - }) - - It("should find one user", func() { - users, err := storage.FindUsers() - Expect(err).To(BeNil()) - Expect(users).To(HaveLen(1)) - }) - - It("should find one user by email", func() { - user, err := storage.FindUserByEmail(email) - Expect(err).To(BeNil()) - Expect(user.Email).To(Equal(email)) - }) - - It("should find one user by ID", func() { - user, err := storage.FindUserByID(1) - Expect(err).To(BeNil()) - Expect(user.ID).To(Equal(1)) - }) - - It("should be possible to delete them", func() { - user, _ := storage.FindUserByID(1) - err := storage.DeleteUser(user) - Expect(err).To(BeNil()) - }) - - It("should find users for a ticker with id 2", func() { - ticker := Ticker{ID: 2} - users, err := storage.FindUsersByTicker(ticker) - Expect(err).To(BeNil()) - Expect(users).To(HaveLen(1)) - }) - - It("shouldn't find users for a ticker with id 1", func() { - ticker := Ticker{ID: 1} - users, err := storage.FindUsersByTicker(ticker) - Expect(err).To(BeNil()) - Expect(users).To(HaveLen(0)) - }) - - It("should be return the correct count", func() { - c, err := storage.CountUser() - Expect(err).To(BeNil()) - Expect(c).To(Equal(1)) - }) - }) - - When("no messages are present", func() { - It("shouldn't find any messages", func() { - ticker := Ticker{ID: 1, Active: true} - ctx := gin.Context{} - pagination := pagination.NewPagination(&ctx) - messages, err := storage.FindMessagesByTickerAndPagination(ticker, *pagination) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(0)) - }) - }) - - When("messages are present", func() { - var ticker Ticker - var message Message - - BeforeEach(func() { - ticker = NewTicker() - ticker.Active = true - err := storage.SaveTicker(&ticker) - Expect(err).To(BeNil()) - message = NewMessage() - message.Ticker = ticker.ID - err = storage.SaveMessage(&message) - Expect(err).To(BeNil()) - }) - - It("should return all messages", func() { - messages, err := storage.FindMessagesByTicker(ticker) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(1)) - }) - - It("should return no messages", func() { - ticker1 := NewTicker() - messages, err := storage.FindMessagesByTicker(ticker1) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(0)) - }) - - It("should return messages on active ticker", func() { - ctx := gin.Context{} - pagination := pagination.NewPagination(&ctx) - messages, err := storage.FindMessagesByTickerAndPagination(ticker, *pagination) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(1)) - }) - - It("should not return newer messages on active ticker", func() { - ctx := gin.Context{ - Request: &http.Request{ - URL: &url.URL{RawQuery: fmt.Sprintf("after=%d", message.ID)}, - }, - } - pagination := pagination.NewPagination(&ctx) - messages, err := storage.FindMessagesByTickerAndPagination(ticker, *pagination) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(0)) - }) - - It("should not return older messages on active ticker", func() { - ctx := gin.Context{ - Request: &http.Request{ - URL: &url.URL{RawQuery: fmt.Sprintf("before=%d", message.ID)}, - }, - } - pagination := pagination.NewPagination(&ctx) - messages, err := storage.FindMessagesByTickerAndPagination(ticker, *pagination) - Expect(err).To(BeNil()) - Expect(messages).To(HaveLen(0)) - }) - - It("should return the message when queried", func() { - found, err := storage.FindMessage(message.Ticker, message.ID) - Expect(err).To(BeNil()) - Expect(found.ID).To(Equal(message.ID)) - }) - - It("should return no uploads", func() { - uploads := storage.FindUploadsByMessage(message) - Expect(uploads).To(HaveLen(0)) - }) - - It("should return no uploads when reference is invalid", func() { - message.Attachments = []Attachment{{UUID: "invalid", ContentType: "text/plain", Extension: "txt"}} - _ = storage.SaveMessage(&message) - - uploads := storage.FindUploadsByMessage(message) - Expect(uploads).To(HaveLen(0)) - }) - - It("should return uploads", func() { - upload := NewUpload("image.jpg", "image/jpeg", 1) - err := storage.SaveUpload(&upload) - Expect(err).To(BeNil()) - message.AddAttachment(upload) - err = storage.SaveMessage(&message) - Expect(err).To(BeNil()) - - uploads := storage.FindUploadsByMessage(message) - Expect(uploads).To(HaveLen(1)) - }) - - It("should be possible to delete the message", func() { - err := storage.DeleteMessage(message) - Expect(err).To(BeNil()) - }) - - It("should be possible to delete messages by ticker", func() { - err := storage.DeleteMessages(ticker) - Expect(err).To(BeNil()) - }) - }) - - When("uploads are present", func() { - ticker := NewTicker() - _ = storage.SaveTicker(&ticker) - upload := NewUpload("filename.txt", "text/plain", ticker.ID) - err := storage.SaveUpload(&upload) - Expect(err).To(BeNil()) - - It("should return upload by uuid", func() { - u, err := storage.FindUploadByUUID(upload.UUID) - Expect(err).To(BeNil()) - Expect(u.UUID).To(Equal(upload.UUID)) - }) - - It("should return upload by ids", func() { - u, err := storage.FindUploadsByIDs([]int{upload.ID}) - Expect(err).To(BeNil()) - Expect(u).To(HaveLen(1)) - }) - - It("can deleted a specific one", func() { - err := storage.DeleteUpload(upload) - Expect(err).To(BeNil()) - }) - - It("can delete multiple ones", func() { - storage.DeleteUploads([]Upload{upload}) - _, err := storage.FindUploadByUUID(upload.UUID) - Expect(err).NotTo(BeNil()) - }) - - It("can be deleted by ticker", func() { - err := storage.DeleteUploadsByTicker(ticker) - Expect(err).To(BeNil()) - _, err = storage.FindUploadByUUID(upload.UUID) - Expect(err).NotTo(BeNil()) - }) - }) - - When("settings are fetched", func() { - It("should return default refresh interval", func() { - refreshInterval := storage.GetRefreshIntervalSetting() - Expect(refreshInterval.Value.(float64)).To(Equal(SettingDefaultRefreshInterval)) - }) - - It("should return default inactive settings", func() { - InactiveSetting := storage.GetInactiveSetting() - Expect(InactiveSetting.Value).To(Equal(DefaultInactiveSettings())) - }) - - refreshInterval := float64(20000) - err := storage.SaveRefreshInterval(refreshInterval) - fmt.Println(err) - Expect(err).To(BeNil()) - - It("should return the user defined refresh interval", func() { - err := storage.SaveRefreshInterval(2000) - Expect(err).To(BeNil()) - refreshIntervalSetting := storage.GetRefreshIntervalSetting() - Expect(refreshIntervalSetting.Value).To(Equal(float64(2000))) - refreshInterval := storage.GetRefreshIntervalSettingValue() - Expect(refreshInterval).To(Equal(2000)) - }) - - It("should update the existing refresh interval", func() { - err := storage.SaveRefreshInterval(2000) - Expect(err).To(BeNil()) - err = storage.SaveRefreshInterval(1000) - Expect(err).To(BeNil()) - }) - - It("should return the user defined inactive settings", func() { - inactiveSettings := InactiveSettings{ - Headline: "Headline", - SubHeadline: "SubHeadline", - Description: "New Description", - Author: "Author", - Email: "Email", - Homepage: "Homepage", - Twitter: "Twitter", - } - err := storage.SaveInactiveSetting(inactiveSettings) - Expect(err).To(BeNil()) - setting := storage.GetInactiveSetting() - Expect(setting.Name).To(Equal(SettingInactiveName)) - }) - }) -}) diff --git a/internal/storage/ticker.go b/internal/storage/ticker.go index a92996f1..dec52db5 100644 --- a/internal/storage/ticker.go +++ b/internal/storage/ticker.go @@ -4,74 +4,87 @@ import ( "time" "github.com/mattn/go-mastodon" + "gorm.io/gorm" ) type Ticker struct { - ID int `storm:"id,increment"` - CreationDate time.Time `storm:"index"` - Domain string `storm:"unique"` - Title string - Description string - Active bool - Information Information - Telegram Telegram - Mastodon Mastodon - Location Location + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + Domain string `gorm:"unique"` + Title string + Description string + Active bool + Information TickerInformation + Telegram TickerTelegram + Mastodon TickerMastodon + Location TickerLocation + Users []User `gorm:"many2many:user_tickers;"` } func NewTicker() Ticker { - return Ticker{ - CreationDate: time.Now(), - } + return Ticker{} } func (t *Ticker) Reset() { t.Active = false t.Description = "" - t.Information = Information{} - t.Location = Location{} + t.Information = TickerInformation{} + t.Location = TickerLocation{} t.Telegram.Reset() t.Mastodon.Reset() } -type Information struct { - Author string - URL string - Email string - Twitter string - Facebook string - Telegram string +type TickerInformation struct { + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + TickerID int + Author string + URL string + Email string + Twitter string + Facebook string + Telegram string } -type Telegram struct { +type TickerTelegram struct { + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + TickerID int `gorm:"index"` Active bool `json:"active"` ChannelName string `json:"channel_name"` } -func (tg *Telegram) Reset() { +func (tg *TickerTelegram) Reset() { tg.Active = false tg.ChannelName = "" } -func (tg *Telegram) Connected() bool { +func (tg *TickerTelegram) Connected() bool { return tg.ChannelName != "" } -type Mastodon struct { - Active bool `json:"active"` - Server string `json:"server"` - Token string `json:"token"` - Secret string `json:"secret"` - AccessToken string `json:"access_token"` - User mastodon.Account +type TickerMastodon struct { + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + TickerID int `gorm:"index"` + Active bool `json:"active"` + Server string `json:"server"` + Token string `json:"token"` + Secret string `json:"secret"` + AccessToken string `json:"access_token"` + User mastodon.Account `gorm:"type:json"` } -func (m *Mastodon) Connected() bool { +func (m *TickerMastodon) Connected() bool { return m.Token != "" && m.Secret != "" && m.AccessToken != "" } -func (m *Mastodon) Reset() { +func (m *TickerMastodon) Reset() { m.Active = false m.Server = "" m.Token = "" @@ -80,7 +93,10 @@ func (m *Mastodon) Reset() { m.User = mastodon.Account{} } -type Location struct { - Lat float64 - Lon float64 +type TickerLocation struct { + gorm.Model + ID int `gorm:"primaryKey"` + TickerID int + Lat float64 + Lon float64 } diff --git a/internal/storage/upload.go b/internal/storage/upload.go index cc6ca5a9..75237baa 100644 --- a/internal/storage/upload.go +++ b/internal/storage/upload.go @@ -9,13 +9,14 @@ import ( ) type Upload struct { - ID int `storm:"id,increment"` - UUID string `storm:"index,unique"` - CreationDate time.Time `storm:"index"` - TickerID int `storm:"index"` - Path string - Extension string - ContentType string + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + UUID string `gorm:"index;unique"` + TickerID int `gorm:"index"` + Path string + Extension string + ContentType string } func NewUpload(filename, contentType string, tickerID int) Upload { @@ -26,12 +27,11 @@ func NewUpload(filename, contentType string, tickerID int) Upload { path := fmt.Sprintf("%d/%d", now.Year(), now.Month()) return Upload{ - CreationDate: now, - Path: path, - UUID: uuid.String(), - TickerID: tickerID, - Extension: ext, - ContentType: contentType, + Path: path, + UUID: uuid.String(), + TickerID: tickerID, + Extension: ext, + ContentType: contentType, } } diff --git a/internal/storage/user.go b/internal/storage/user.go index 28c95a77..62a19cbf 100644 --- a/internal/storage/user.go +++ b/internal/storage/user.go @@ -3,28 +3,24 @@ package storage import ( "time" - "github.com/systemli/ticker/internal/util" "golang.org/x/crypto/bcrypt" ) type User struct { - ID int `storm:"id,increment"` - CreationDate time.Time `storm:"index"` - Email string `storm:"unique"` - Role string + ID int `gorm:"primaryKey"` + CreatedAt time.Time + UpdatedAt time.Time + Email string `storm:"unique"` EncryptedPassword string IsSuperAdmin bool - Tickers []int + Tickers []Ticker `gorm:"many2many:user_tickers;"` } func NewUser(email, password string) (User, error) { user := User{ - CreationDate: time.Now(), IsSuperAdmin: false, Email: email, - Tickers: []int{}, EncryptedPassword: "", - Role: "", } pw, err := hashPassword(password) @@ -37,30 +33,11 @@ func NewUser(email, password string) (User, error) { return user, nil } -func NewAdminUser(email, password string) (User, error) { - user, err := NewUser(email, password) - if err != nil { - return user, err - } - - user.IsSuperAdmin = true - - return user, err -} - func (u *User) Authenticate(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(u.EncryptedPassword), []byte(password)) return err == nil } -func (u *User) AddTicker(ticker Ticker) { - u.Tickers = util.Append(u.Tickers, ticker.ID) -} - -func (u *User) RemoveTicker(ticker Ticker) { - u.Tickers = util.Remove(u.Tickers, ticker.ID) -} - func (u *User) UpdatePassword(password string) { pw, err := hashPassword(password) if err != nil { diff --git a/internal/util/slice.go b/internal/util/slice.go index 49ce0976..980797e9 100644 --- a/internal/util/slice.go +++ b/internal/util/slice.go @@ -1,6 +1,6 @@ package util -//Contains returns true when s is in slice. +// Contains returns true when s is in slice. func Contains(slice []int, s int) bool { for _, value := range slice { if value == s { @@ -10,7 +10,7 @@ func Contains(slice []int, s int) bool { return false } -//Append appends the integer when it not already in the slice. +// Append appends the integer when it not already in the slice. func Append(slice []int, s int) []int { if Contains(slice, s) { return slice @@ -19,7 +19,7 @@ func Append(slice []int, s int) []int { return append(slice, s) } -//Remove element from slice. +// Remove element from slice. func Remove(slice []int, s int) []int { for i := range slice { if slice[i] == s { @@ -30,7 +30,7 @@ func Remove(slice []int, s int) []int { return slice } -//ContainsString returns true when s in slice. +// ContainsString returns true when s in slice. func ContainsString(list []string, s string) bool { for _, b := range list { if b == s { From e46e13fd2b2c54fa2cdfc7868ec3d58650e5c992 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 11:05:06 +0200 Subject: [PATCH 2/7] Ignore mock files --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index ca2113eb..c278b943 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,2 @@ ignore: - - "**/*_mock.go" + - "**/mock_*.go" From 43e07383ff8e098dff4fef0de811437b93c22ae3 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 21:20:58 +0200 Subject: [PATCH 3/7] Extend Test Cases --- internal/api/user.go | 2 -- internal/api/user_test.go | 59 +++++++++++++++++++++++++++++++++++ internal/storage/user_test.go | 22 +++++++++++-- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/internal/api/user.go b/internal/api/user.go index 11cc56ac..5ebd4292 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -52,14 +52,12 @@ func (h *handler) PostUser(c *gin.Context) { return } - //TODO: Validation user, err := storage.NewUser(body.Email, body.Password) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) return } - // load tickers by body.Tickers tickers, err := h.storage.FindTickersByIDs(body.Tickers) if err != nil { c.JSON(http.StatusBadRequest, response.ErrorResponse(response.CodeDefault, response.StorageError)) diff --git a/internal/api/user_test.go b/internal/api/user_test.go index 00e51637..b358260b 100644 --- a/internal/api/user_test.go +++ b/internal/api/user_test.go @@ -150,6 +150,24 @@ func TestPostUserMissingBody(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) } +func TestPostUserTooLongPassword(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + json := `{"email":"louis@systemli.org","password":"swusp-dud-gust-grong-yuz-swuft-plaft-glact-skast-swem-yen-kom-tut-prisp-gont"}` + c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) + c.Request.Header.Add("Content-Type", "application/json") + c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) + s := &storage.MockStorage{} + h := handler{ + storage: s, + config: config.NewConfig(), + } + + h.PostUser(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + func TestPostUserStorageError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -170,6 +188,26 @@ func TestPostUserStorageError(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) } +func TestPostUserStorageError2(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + json := `{"email":"louis@systemli.org","password":"password1234"}` + c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) + c.Request.Header.Add("Content-Type", "application/json") + c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, errors.New("storage error")) + s.On("SaveUser", mock.Anything).Return(errors.New("storage error")) + h := handler{ + storage: s, + config: config.NewConfig(), + } + + h.PostUser(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + func TestPostUser(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -245,6 +283,27 @@ func TestPutUserStorageError(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) } +func TestPutUserStorageError2(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("me", storage.User{ID: 1, IsSuperAdmin: true}) + c.Set("user", storage.User{}) + json := `{"email":"louis@systemli.org","password":"password1234","is_super_admin":true,"tickers":[1]}` + c.Request = httptest.NewRequest(http.MethodPost, "/v1/admin/users", strings.NewReader(json)) + c.Request.Header.Add("Content-Type", "application/json") + s := &storage.MockStorage{} + s.On("FindTickersByIDs", mock.Anything).Return([]storage.Ticker{}, errors.New("storage error")) + s.On("SaveUser", mock.Anything).Return(errors.New("storage error")) + h := handler{ + storage: s, + config: config.NewConfig(), + } + + h.PutUser(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + func TestPutUser(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/internal/storage/user_test.go b/internal/storage/user_test.go index 732f37b8..5e4dde7f 100644 --- a/internal/storage/user_test.go +++ b/internal/storage/user_test.go @@ -6,19 +6,35 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + Password = "password" + TooLongPassword = "swusp-dud-gust-grong-yuz-swuft-plaft-glact-skast-swem-yen-kom-tut-prisp-gont" +) + func TestUserAuthenticate(t *testing.T) { - user, err := NewUser("louis@systemli.org", "password") + user, err := NewUser("louis@systemli.org", Password) assert.Nil(t, err) assert.False(t, user.Authenticate("wrong")) - assert.True(t, user.Authenticate("password")) + assert.True(t, user.Authenticate(Password)) } func TestUserUpdatePassword(t *testing.T) { - user, err := NewUser("louis@systemli.org", "password") + user, err := NewUser("louis@systemli.org", Password) assert.Nil(t, err) oldEncPassword := user.EncryptedPassword user.UpdatePassword("newPassword") assert.NotEqual(t, oldEncPassword, user.EncryptedPassword) + + user.UpdatePassword(TooLongPassword) + assert.NotEqual(t, oldEncPassword, user.EncryptedPassword) +} + +func TestNewUser(t *testing.T) { + _, err := NewUser("user@systemli.org", Password) + assert.Nil(t, err) + + _, err = NewUser("user@systemli.org", TooLongPassword) + assert.NotNil(t, err) } From c864e4f2f2207df327484b15f44cb29b647b57d7 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 21:24:18 +0200 Subject: [PATCH 4/7] Extend Test Cases --- internal/api/tickers_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/api/tickers_test.go b/internal/api/tickers_test.go index 6fc1bb4a..77aebfeb 100644 --- a/internal/api/tickers_test.go +++ b/internal/api/tickers_test.go @@ -299,6 +299,27 @@ func TestPutTickerUsersStorageError(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, w.Code) } +func TestPutTickerUsersStorageError2(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("ticker", storage.Ticker{}) + body := `{"users":[1,2,3]}` + c.Request = httptest.NewRequest(http.MethodPut, "/v1/admin/tickers/1/user", strings.NewReader(body)) + c.Request.Header.Add("Content-Type", "application/json") + s := &storage.MockStorage{} + s.On("FindUsersByIDs", mock.Anything).Return([]storage.User{}, nil) + s.On("FindUsersByTicker", mock.Anything).Return([]storage.User{}, nil) + s.On("SaveTicker", mock.Anything).Return(errors.New("storage error")) + h := handler{ + storage: s, + config: config.NewConfig(), + } + + h.PutTickerUsers(c) + + assert.Equal(t, http.StatusInternalServerError, w.Code) +} + func TestPutTickerUsers(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) From 619cb9e171087f3614ab357584fdb922cb11ec79 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 21:36:29 +0200 Subject: [PATCH 5/7] Extend Test Cases --- internal/api/response/settings_test.go | 26 ++++++++ internal/api/response/ticker_test.go | 82 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 internal/api/response/settings_test.go create mode 100644 internal/api/response/ticker_test.go diff --git a/internal/api/response/settings_test.go b/internal/api/response/settings_test.go new file mode 100644 index 00000000..c29d8e12 --- /dev/null +++ b/internal/api/response/settings_test.go @@ -0,0 +1,26 @@ +package response + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/systemli/ticker/internal/storage" +) + +func TestInactiveSettingsResponse(t *testing.T) { + inactiveSettings := storage.DefaultInactiveSettings() + + setting := InactiveSettingsResponse(inactiveSettings) + + assert.Equal(t, storage.SettingInactiveName, setting.Name) + assert.Equal(t, inactiveSettings, setting.Value) +} + +func TestRefreshIntervalSettingsResponse(t *testing.T) { + refreshIntervalSettings := storage.DefaultRefreshIntervalSettings() + + setting := RefreshIntervalSettingsResponse(refreshIntervalSettings) + + assert.Equal(t, storage.SettingRefreshInterval, setting.Name) + assert.Equal(t, refreshIntervalSettings, setting.Value) +} diff --git a/internal/api/response/ticker_test.go b/internal/api/response/ticker_test.go new file mode 100644 index 00000000..489e4f39 --- /dev/null +++ b/internal/api/response/ticker_test.go @@ -0,0 +1,82 @@ +package response + +import ( + "testing" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/mattn/go-mastodon" + "github.com/stretchr/testify/assert" + "github.com/systemli/ticker/internal/config" + "github.com/systemli/ticker/internal/storage" +) + +func TestTickersResponse(t *testing.T) { + ticker := storage.Ticker{ + ID: 1, + CreatedAt: time.Now(), + Domain: "example.com", + Title: "Example", + Description: "Example", + Active: true, + Information: storage.TickerInformation{ + Author: "Example", + URL: "https://example.com", + Email: "contact@example.com", + Twitter: "@example", + Facebook: "https://facebook.com/example", + Telegram: "example", + }, + Telegram: storage.TickerTelegram{ + Active: true, + ChannelName: "example", + }, + Mastodon: storage.TickerMastodon{ + Active: true, + Server: "https://example.com", + User: mastodon.Account{ + Username: "example", + DisplayName: "Example", + Avatar: "https://example.com/avatar.png", + }, + }, + Location: storage.TickerLocation{ + Lat: 0.0, + Lon: 0.0, + }, + } + + config := config.Config{ + TelegramBotUser: tgbotapi.User{ + UserName: "ticker", + }, + } + + tickerResponse := TickersResponse([]storage.Ticker{ticker}, config) + + assert.Equal(t, 1, len(tickerResponse)) + assert.Equal(t, ticker.ID, tickerResponse[0].ID) + assert.Equal(t, ticker.CreatedAt, tickerResponse[0].CreationDate) + assert.Equal(t, ticker.Domain, tickerResponse[0].Domain) + assert.Equal(t, ticker.Title, tickerResponse[0].Title) + assert.Equal(t, ticker.Description, tickerResponse[0].Description) + assert.Equal(t, ticker.Active, tickerResponse[0].Active) + assert.Equal(t, ticker.Information.Author, tickerResponse[0].Information.Author) + assert.Equal(t, ticker.Information.URL, tickerResponse[0].Information.URL) + assert.Equal(t, ticker.Information.Email, tickerResponse[0].Information.Email) + assert.Equal(t, ticker.Information.Twitter, tickerResponse[0].Information.Twitter) + assert.Equal(t, ticker.Information.Facebook, tickerResponse[0].Information.Facebook) + assert.Equal(t, ticker.Information.Telegram, tickerResponse[0].Information.Telegram) + assert.Equal(t, ticker.Telegram.Active, tickerResponse[0].Telegram.Active) + assert.Equal(t, ticker.Telegram.Connected(), tickerResponse[0].Telegram.Connected) + assert.Equal(t, config.TelegramBotUser.UserName, tickerResponse[0].Telegram.BotUsername) + assert.Equal(t, ticker.Telegram.ChannelName, tickerResponse[0].Telegram.ChannelName) + assert.Equal(t, ticker.Mastodon.Active, tickerResponse[0].Mastodon.Active) + assert.Equal(t, ticker.Mastodon.Connected(), tickerResponse[0].Mastodon.Connected) + assert.Equal(t, ticker.Mastodon.User.Username, tickerResponse[0].Mastodon.Name) + assert.Equal(t, ticker.Mastodon.Server, tickerResponse[0].Mastodon.Server) + assert.Equal(t, ticker.Mastodon.User.DisplayName, tickerResponse[0].Mastodon.ScreenName) + assert.Equal(t, ticker.Mastodon.User.Avatar, tickerResponse[0].Mastodon.ImageURL) + assert.Equal(t, ticker.Location.Lat, tickerResponse[0].Location.Lat) + assert.Equal(t, ticker.Location.Lon, tickerResponse[0].Location.Lon) +} From 968407fab6bf8fa14d383338334e704b9821d105 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 21:41:13 +0200 Subject: [PATCH 6/7] Extend Test Cases --- internal/api/response/user_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 internal/api/response/user_test.go diff --git a/internal/api/response/user_test.go b/internal/api/response/user_test.go new file mode 100644 index 00000000..9f277ada --- /dev/null +++ b/internal/api/response/user_test.go @@ -0,0 +1,27 @@ +package response + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/systemli/ticker/internal/storage" +) + +func TestUsersResponse(t *testing.T) { + users := []storage.User{ + { + ID: 1, + CreatedAt: time.Now(), + Email: "user@systemli.org", + IsSuperAdmin: true, + }, + } + + usersResponse := UsersResponse(users) + assert.Equal(t, 1, len(usersResponse)) + assert.Equal(t, users[0].ID, usersResponse[0].ID) + assert.Equal(t, users[0].CreatedAt, usersResponse[0].CreationDate) + assert.Equal(t, users[0].Email, usersResponse[0].Email) + assert.Equal(t, users[0].IsSuperAdmin, usersResponse[0].IsSuperAdmin) +} From 3f77394053f426c522988ef17862df9d1111b1a8 Mon Sep 17 00:00:00 2001 From: louis Date: Sun, 8 Oct 2023 21:44:20 +0200 Subject: [PATCH 7/7] Extend Test Cases --- internal/storage/ticker_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/storage/ticker_test.go b/internal/storage/ticker_test.go index 498a893c..d59e84b4 100644 --- a/internal/storage/ticker_test.go +++ b/internal/storage/ticker_test.go @@ -12,6 +12,14 @@ func TestTickerMastodonConnect(t *testing.T) { assert.False(t, ticker.Mastodon.Connected()) } +func TestTickerTelegramConnected(t *testing.T) { + assert.False(t, ticker.Telegram.Connected()) + + ticker.Telegram.ChannelName = "ChannelName" + + assert.True(t, ticker.Telegram.Connected()) +} + func TestTickerReset(t *testing.T) { ticker.Active = true ticker.Description = "Description"