mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
Refactor database storage
This commit is contained in:
@@ -4,23 +4,19 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/builders"
|
||||
"github.com/mxpv/podsync/pkg/config"
|
||||
"github.com/mxpv/podsync/pkg/feeds"
|
||||
"github.com/mxpv/podsync/pkg/handler"
|
||||
"github.com/mxpv/podsync/pkg/stats"
|
||||
"github.com/mxpv/podsync/pkg/storage"
|
||||
"github.com/mxpv/podsync/pkg/support"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -30,14 +26,14 @@ func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create core sevices
|
||||
// Create core services
|
||||
|
||||
cfg, err := config.ReadConfiguration()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
database, err := createPg(cfg.PostgresConnectionURL)
|
||||
database, err := storage.NewPG(cfg.PostgresConnectionURL, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -62,7 +58,7 @@ func main() {
|
||||
}
|
||||
|
||||
feed, err := feeds.NewFeedService(
|
||||
feeds.WithPostgres(database),
|
||||
feeds.WithStorage(database),
|
||||
feeds.WithStats(statistics),
|
||||
feeds.WithBuilder(api.ProviderYoutube, youtube),
|
||||
feeds.WithBuilder(api.ProviderVimeo, vimeo),
|
||||
@@ -88,35 +84,9 @@ func main() {
|
||||
|
||||
log.Printf("shutting down server")
|
||||
|
||||
srv.Shutdown(ctx)
|
||||
database.Close()
|
||||
statistics.Close()
|
||||
_ = srv.Shutdown(ctx)
|
||||
_ = database.Close()
|
||||
_ = statistics.Close()
|
||||
|
||||
log.Printf("server gracefully stopped")
|
||||
}
|
||||
|
||||
func createPg(connectionURL string) (*pg.DB, error) {
|
||||
opts, err := pg.ParseURL(connectionURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If host format is "projection:region:host", than use Google SQL Proxy
|
||||
// See https://github.com/go-pg/pg/issues/576
|
||||
if strings.Count(opts.Addr, ":") == 2 {
|
||||
log.Print("using GCP SQL proxy")
|
||||
opts.Dialer = func(network, addr string) (net.Conn, error) {
|
||||
return proxy.Dial(addr)
|
||||
}
|
||||
}
|
||||
|
||||
db := pg.Connect(opts)
|
||||
|
||||
// Check database connectivity
|
||||
if _, err := db.ExecOne("SELECT 1"); err != nil {
|
||||
db.Close()
|
||||
return nil, errors.Wrap(err, "failed to check database connectivity")
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
16
go.mod
16
go.mod
@@ -3,12 +3,13 @@ module github.com/mxpv/podsync
|
||||
require (
|
||||
cloud.google.com/go v0.25.0 // indirect
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7
|
||||
github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 // indirect
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851 // indirect
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||
github.com/eduncan911/podcast v1.3.0 // indirect
|
||||
github.com/garyburd/redigo v1.6.0 // indirect
|
||||
github.com/gin-contrib/sessions v0.0.0-20170731012558-a71ea9167c61
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
|
||||
@@ -16,7 +17,6 @@ require (
|
||||
github.com/go-pg/pg v6.14.2+incompatible
|
||||
github.com/go-redis/redis v6.12.0+incompatible
|
||||
github.com/golang/mock v1.1.1
|
||||
github.com/golang/protobuf v1.1.0 // indirect
|
||||
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
|
||||
github.com/gorilla/sessions v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect
|
||||
@@ -24,9 +24,12 @@ require (
|
||||
github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f // indirect
|
||||
github.com/magiconair/properties v1.8.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||
github.com/memcachier/mc v2.0.1+incompatible // indirect
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 // indirect
|
||||
github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18
|
||||
github.com/onsi/ginkgo v1.7.0 // indirect
|
||||
github.com/onsi/gomega v1.4.3 // indirect
|
||||
github.com/pelletier/go-toml v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -36,16 +39,17 @@ require (
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec // indirect
|
||||
github.com/spf13/pflag v1.0.1 // indirect
|
||||
github.com/spf13/viper v1.0.2
|
||||
github.com/stretchr/objx v0.1.1 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // indirect
|
||||
github.com/ugorji/go v1.1.1 // indirect
|
||||
github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf
|
||||
golang.org/x/net v0.0.0-20180719001425-81d44fd177a9
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
|
||||
golang.org/x/sys v0.0.0-20180715085529-ac767d655b30 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56
|
||||
google.golang.org/appengine v1.1.0 // indirect
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
39
go.sum
39
go.sum
@@ -2,6 +2,8 @@ cloud.google.com/go v0.25.0 h1:6vD6xZTc8Jo6To8gHxFDRVsMvWFDgY3rugNszcDalN8=
|
||||
cloud.google.com/go v0.25.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec h1:1VPruZMM1WQC7POhjxbZOWK564cuFz1hlpwYW6ocM4E=
|
||||
github.com/BrianHicks/finch v0.0.0-20140409222414-419bd73c29ec/go.mod h1:+hWo/MWgY8VtjZvdrYM2nPRMaK40zX2iPsH/qD0+Xs0=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7 h1:Clo7QBZv+fHzjCgVp4ELlbIsY5rScCmj+4VCfoMfqtQ=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170929212804-61590edac4c7/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
||||
github.com/boj/redistore v0.0.0-20160128113310-fc113767cd6b h1:PfxLkkgJYE095CKZji++BNwZjxWfoAF21WFPzkzOZEs=
|
||||
@@ -12,6 +14,8 @@ github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20180621172731-4e5d6d543851/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eduncan911/podcast v1.3.0 h1:lVCar1J39mMNWR2SbGzPjeUbCKEkQ6/pt/7beQqK6fk=
|
||||
github.com/eduncan911/podcast v1.3.0/go.mod h1:C7Q04QZtv7LW/1X67mc1zwsktpZ68kbxsUS3CYWniJg=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
@@ -28,8 +32,8 @@ github.com/go-redis/redis v6.12.0+incompatible h1:s+64XI+z/RXqGHz2fQSgRJOEwqqSXe
|
||||
github.com/go-redis/redis v6.12.0+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
|
||||
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
@@ -40,6 +44,8 @@ github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0Pr
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/kidstuff/mongostore v0.0.0-20180412085134-db2a8b4fac1f h1:84d0qxD9AiuBNpeK5TkYwTKKNezsYxIVn8nWh0pq51E=
|
||||
@@ -48,12 +54,19 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun9knAVAR8tg/aHJEr5DgtcbqyvzacK+CDCaI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700 h1:39PjdU78pNilVLU9tjWVDGt/rziIKKkKHuzWbH1kBbw=
|
||||
github.com/mxpv/patreon-go v0.0.0-20171031001022-1d2f253ac700/go.mod h1:ksYjm2GAbGlgIP7jO9Q5/AdyE4MwwEbgQ+lFMx3hyiM=
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18 h1:YYsu49Y42JA+CSs9+z2MGBdGxb5jklpagLp5QPJ6BwQ=
|
||||
github.com/mxpv/podcast v0.0.0-20170823220358-fe328ad87d18/go.mod h1:bKrqwMF8O3PciTG92w0992h/d7Aj7CuIF5uTNEl3pNY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
@@ -72,28 +85,42 @@ github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso=
|
||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
|
||||
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf h1:cgAKVljim9RJRcJNGjnBUajXj1FupBSdWwW4JaQG7vk=
|
||||
github.com/ventu-io/go-shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:6rZqAOk/eYX5FJyjQJ6Z3RBSN389IXX2ijwW4FcggaM=
|
||||
golang.org/x/net v0.0.0-20180719001425-81d44fd177a9 h1:BN4Q1JAtkPK2SJ//1vLij2bjj2ZPrBi6w1VEpdevHDA=
|
||||
golang.org/x/net v0.0.0-20180719001425-81d44fd177a9/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd h1:QQhib242ErYDSMitlBm8V7wYCm/1a25hV8qMadIKLPA=
|
||||
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sys v0.0.0-20180715085529-ac767d655b30 h1:4bYUqrXBoiI7UFQeibUwFhvcHfaEeL75O3lOcZa964o=
|
||||
golang.org/x/sys v0.0.0-20180715085529-ac767d655b30/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56 h1:iDRbkenn0VZEo05mHiCtN6/EfbZj7x1Rg+tPjB5HiQc=
|
||||
google.golang.org/api v0.0.0-20180718221112-efcb5f25ac56/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
@@ -5,12 +5,11 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
itunes "github.com/mxpv/podcast"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/ventu-io/go-shortid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,11 +26,18 @@ type builder interface {
|
||||
Build(feed *model.Feed) (podcast *itunes.Podcast, err error)
|
||||
}
|
||||
|
||||
type storage interface {
|
||||
SaveFeed(feed *model.Feed) error
|
||||
GetFeed(hashID string) (*model.Feed, error)
|
||||
GetMetadata(hashID string) (*model.Feed, error)
|
||||
Downgrade(userID string, featureLevel int) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
sid *shortid.Shortid
|
||||
stats stats
|
||||
db *pg.DB
|
||||
builders map[api.Provider]builder
|
||||
generator IDGen
|
||||
stats stats
|
||||
db storage
|
||||
builders map[api.Provider]builder
|
||||
}
|
||||
|
||||
func (s Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*model.Feed, error) {
|
||||
@@ -65,7 +71,7 @@ func (s Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*
|
||||
}
|
||||
|
||||
// Generate short id
|
||||
hashId, err := s.sid.Generate()
|
||||
hashId, err := s.generator.Generate()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate id for feed")
|
||||
}
|
||||
@@ -87,34 +93,15 @@ func (s Service) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity)
|
||||
return "", fmt.Errorf("failed to get builder for URL: %s", req.URL)
|
||||
}
|
||||
|
||||
// Save to database
|
||||
_, err = s.db.Model(feed).Insert()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to save feed to database")
|
||||
if err := s.db.SaveFeed(feed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return feed.HashID, nil
|
||||
}
|
||||
|
||||
func (s Service) QueryFeed(hashID string) (*model.Feed, error) {
|
||||
lastAccess := time.Now().UTC()
|
||||
|
||||
feed := &model.Feed{}
|
||||
res, err := s.db.Model(feed).
|
||||
Set("last_access = ?", lastAccess).
|
||||
Where("hash_id = ?", hashID).
|
||||
Returning("*").
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to query feed: %s", hashID)
|
||||
}
|
||||
|
||||
if res.RowsAffected() != 1 {
|
||||
return nil, api.ErrNotFound
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
return s.db.GetFeed(hashID)
|
||||
}
|
||||
|
||||
func (s Service) BuildFeed(hashID string) (*itunes.Podcast, error) {
|
||||
@@ -146,13 +133,7 @@ func (s Service) BuildFeed(hashID string) (*itunes.Podcast, error) {
|
||||
}
|
||||
|
||||
func (s Service) GetMetadata(hashID string) (*api.Metadata, error) {
|
||||
feed := &model.Feed{}
|
||||
err := s.db.
|
||||
Model(feed).
|
||||
Where("hash_id = ?", hashID).
|
||||
Column("provider", "format", "quality", "user_id").
|
||||
Select()
|
||||
|
||||
feed, err := s.db.GetMetadata(hashID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -173,64 +154,19 @@ func (s Service) GetMetadata(hashID string) (*api.Metadata, error) {
|
||||
func (s Service) Downgrade(patronID string, featureLevel int) error {
|
||||
log.Printf("Downgrading patron '%s' to feature level %d", patronID, featureLevel)
|
||||
|
||||
if featureLevel > api.ExtendedFeatures {
|
||||
return nil
|
||||
if err := s.db.Downgrade(patronID, featureLevel); err != nil {
|
||||
log.Printf("! downgrade failed")
|
||||
return err
|
||||
}
|
||||
|
||||
if featureLevel == api.ExtendedFeatures {
|
||||
const maxPages = 150
|
||||
res, err := s.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("page_size = ?", maxPages).
|
||||
Where("user_id = ? AND page_size > ?", patronID, maxPages).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("! failed to reduce page sizes for patron '%s': %v", patronID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
res, err = s.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("feature_level = ?", api.ExtendedFeatures).
|
||||
Where("user_id = ?", patronID, maxPages).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
if featureLevel == api.DefaultFeatures {
|
||||
res, err := s.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("page_size = ?", 50).
|
||||
Set("feature_level = ?", api.DefaultFeatures).
|
||||
Set("format = ?", api.FormatVideo).
|
||||
Set("quality = ?", api.QualityHigh).
|
||||
Where("user_id = ?", patronID).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unsupported downgrade type")
|
||||
log.Printf("updated user '%s' to feature level %d", patronID, featureLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
type feedOption func(*Service)
|
||||
|
||||
//noinspection GoExportedFuncWithUnexportedType
|
||||
func WithPostgres(db *pg.DB) feedOption {
|
||||
func WithStorage(db storage) feedOption {
|
||||
return func(service *Service) {
|
||||
service.db = db
|
||||
}
|
||||
@@ -251,14 +187,14 @@ func WithStats(m stats) feedOption {
|
||||
}
|
||||
|
||||
func NewFeedService(opts ...feedOption) (*Service, error) {
|
||||
sid, err := shortid.New(1, shortid.DefaultABC, uint64(time.Now().UnixNano()))
|
||||
idGen, err := NewIDGen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := &Service{
|
||||
sid: sid,
|
||||
builders: make(map[api.Provider]builder),
|
||||
generator: idGen,
|
||||
builders: make(map[api.Provider]builder),
|
||||
}
|
||||
|
||||
for _, fn := range opts {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: feeds.go
|
||||
|
||||
// Package feeds is a generated GoMock package.
|
||||
package feeds
|
||||
|
||||
import (
|
||||
@@ -29,34 +30,34 @@ func NewMockstats(ctrl *gomock.Controller) *Mockstats {
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *Mockstats) EXPECT() *MockstatsMockRecorder {
|
||||
return _m.recorder
|
||||
func (m *Mockstats) EXPECT() *MockstatsMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Inc mocks base method
|
||||
func (_m *Mockstats) Inc(metric string, hashID string) (int64, error) {
|
||||
ret := _m.ctrl.Call(_m, "Inc", metric, hashID)
|
||||
func (m *Mockstats) Inc(metric, hashID string) (int64, error) {
|
||||
ret := m.ctrl.Call(m, "Inc", metric, hashID)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Inc indicates an expected call of Inc
|
||||
func (_mr *MockstatsMockRecorder) Inc(arg0, arg1 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Inc", reflect.TypeOf((*Mockstats)(nil).Inc), arg0, arg1)
|
||||
func (mr *MockstatsMockRecorder) Inc(metric, hashID interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inc", reflect.TypeOf((*Mockstats)(nil).Inc), metric, hashID)
|
||||
}
|
||||
|
||||
// Get mocks base method
|
||||
func (_m *Mockstats) Get(metric string, hashID string) (int64, error) {
|
||||
ret := _m.ctrl.Call(_m, "Get", metric, hashID)
|
||||
func (m *Mockstats) Get(metric, hashID string) (int64, error) {
|
||||
ret := m.ctrl.Call(m, "Get", metric, hashID)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get
|
||||
func (_mr *MockstatsMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Get", reflect.TypeOf((*Mockstats)(nil).Get), arg0, arg1)
|
||||
func (mr *MockstatsMockRecorder) Get(metric, hashID interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*Mockstats)(nil).Get), metric, hashID)
|
||||
}
|
||||
|
||||
// Mockbuilder is a mock of builder interface
|
||||
@@ -78,19 +79,92 @@ func NewMockbuilder(ctrl *gomock.Controller) *Mockbuilder {
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *Mockbuilder) EXPECT() *MockbuilderMockRecorder {
|
||||
return _m.recorder
|
||||
func (m *Mockbuilder) EXPECT() *MockbuilderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Build mocks base method
|
||||
func (_m *Mockbuilder) Build(feed *model.Feed) (*podcast.Podcast, error) {
|
||||
ret := _m.ctrl.Call(_m, "Build", feed)
|
||||
func (m *Mockbuilder) Build(feed *model.Feed) (*podcast.Podcast, error) {
|
||||
ret := m.ctrl.Call(m, "Build", feed)
|
||||
ret0, _ := ret[0].(*podcast.Podcast)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Build indicates an expected call of Build
|
||||
func (_mr *MockbuilderMockRecorder) Build(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Build", reflect.TypeOf((*Mockbuilder)(nil).Build), arg0)
|
||||
func (mr *MockbuilderMockRecorder) Build(feed interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*Mockbuilder)(nil).Build), feed)
|
||||
}
|
||||
|
||||
// Mockstorage is a mock of storage interface
|
||||
type Mockstorage struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockstorageMockRecorder
|
||||
}
|
||||
|
||||
// MockstorageMockRecorder is the mock recorder for Mockstorage
|
||||
type MockstorageMockRecorder struct {
|
||||
mock *Mockstorage
|
||||
}
|
||||
|
||||
// NewMockstorage creates a new mock instance
|
||||
func NewMockstorage(ctrl *gomock.Controller) *Mockstorage {
|
||||
mock := &Mockstorage{ctrl: ctrl}
|
||||
mock.recorder = &MockstorageMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *Mockstorage) EXPECT() *MockstorageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// SaveFeed mocks base method
|
||||
func (m *Mockstorage) SaveFeed(feed *model.Feed) error {
|
||||
ret := m.ctrl.Call(m, "SaveFeed", feed)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveFeed indicates an expected call of SaveFeed
|
||||
func (mr *MockstorageMockRecorder) SaveFeed(feed interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveFeed", reflect.TypeOf((*Mockstorage)(nil).SaveFeed), feed)
|
||||
}
|
||||
|
||||
// GetFeed mocks base method
|
||||
func (m *Mockstorage) GetFeed(hashID string) (*model.Feed, error) {
|
||||
ret := m.ctrl.Call(m, "GetFeed", hashID)
|
||||
ret0, _ := ret[0].(*model.Feed)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetFeed indicates an expected call of GetFeed
|
||||
func (mr *MockstorageMockRecorder) GetFeed(hashID interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeed", reflect.TypeOf((*Mockstorage)(nil).GetFeed), hashID)
|
||||
}
|
||||
|
||||
// GetMetadata mocks base method
|
||||
func (m *Mockstorage) GetMetadata(hashID string) (*model.Feed, error) {
|
||||
ret := m.ctrl.Call(m, "GetMetadata", hashID)
|
||||
ret0, _ := ret[0].(*model.Feed)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetMetadata indicates an expected call of GetMetadata
|
||||
func (mr *MockstorageMockRecorder) GetMetadata(hashID interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*Mockstorage)(nil).GetMetadata), hashID)
|
||||
}
|
||||
|
||||
// Downgrade mocks base method
|
||||
func (m *Mockstorage) Downgrade(userID string, featureLevel int) error {
|
||||
ret := m.ctrl.Call(m, "Downgrade", userID, featureLevel)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Downgrade indicates an expected call of Downgrade
|
||||
func (mr *MockstorageMockRecorder) Downgrade(userID, featureLevel interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Downgrade", reflect.TypeOf((*Mockstorage)(nil).Downgrade), userID, featureLevel)
|
||||
}
|
||||
|
@@ -5,12 +5,12 @@ package feeds
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ventu-io/go-shortid"
|
||||
)
|
||||
|
||||
var feed = &model.Feed{
|
||||
@@ -27,10 +27,15 @@ func TestService_CreateFeed(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
db := NewMockstorage(ctrl)
|
||||
db.EXPECT().SaveFeed(gomock.Any()).Times(1).Return(nil)
|
||||
|
||||
gen, _ := NewIDGen()
|
||||
|
||||
s := Service{
|
||||
sid: shortid.GetDefault(),
|
||||
db: createDatabase(t),
|
||||
builders: map[api.Provider]builder{api.ProviderYoutube: nil},
|
||||
generator: gen,
|
||||
db: db,
|
||||
builders: map[api.Provider]builder{api.ProviderYoutube: nil},
|
||||
}
|
||||
|
||||
req := &api.CreateFeedRequest{
|
||||
@@ -53,8 +58,10 @@ func TestService_makeFeed(t *testing.T) {
|
||||
Format: api.FormatAudio,
|
||||
}
|
||||
|
||||
gen, _ := NewIDGen()
|
||||
|
||||
s := Service{
|
||||
sid: shortid.GetDefault(),
|
||||
generator: gen,
|
||||
}
|
||||
|
||||
feed, err := s.makeFeed(req, &api.Identity{})
|
||||
@@ -76,6 +83,18 @@ func TestService_makeFeed(t *testing.T) {
|
||||
require.Equal(t, api.FormatAudio, feed.Format)
|
||||
}
|
||||
|
||||
func TestService_QueryFeed(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
db := NewMockstorage(ctrl)
|
||||
db.EXPECT().GetFeed("123").Times(1).Return(nil, nil)
|
||||
|
||||
s := Service{db: db}
|
||||
_, err := s.QueryFeed("123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestService_GetFeed(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
@@ -83,7 +102,10 @@ func TestService_GetFeed(t *testing.T) {
|
||||
stats := NewMockstats(ctrl)
|
||||
stats.EXPECT().Inc(MetricQueries, feed.HashID).Return(int64(10), nil)
|
||||
|
||||
s := Service{db: createDatabase(t), stats: stats}
|
||||
stor := NewMockstorage(ctrl)
|
||||
stor.EXPECT().GetFeed(feed.HashID).Times(1).Return(feed, nil)
|
||||
|
||||
s := Service{db: stor, stats: stats}
|
||||
|
||||
_, err := s.BuildFeed(feed.HashID)
|
||||
require.NoError(t, err)
|
||||
@@ -106,126 +128,41 @@ func TestService_BuildFeedQuotaCheck(t *testing.T) {
|
||||
stats := NewMockstats(ctrl)
|
||||
stats.EXPECT().Inc(MetricQueries, f.HashID).Return(int64(api.ExtendedPaginationQueryLimit)+1, nil)
|
||||
|
||||
s := Service{db: createDatabase(t), stats: stats}
|
||||
stor := NewMockstorage(ctrl)
|
||||
stor.EXPECT().GetFeed(f.HashID).Times(1).Return(f, nil)
|
||||
|
||||
err := s.db.Insert(f)
|
||||
require.NoError(t, err)
|
||||
s := Service{db: stor, stats: stats}
|
||||
|
||||
_, err = s.BuildFeed(f.HashID)
|
||||
_, err := s.BuildFeed(f.HashID)
|
||||
require.Equal(t, api.ErrQuotaExceeded, err)
|
||||
}
|
||||
|
||||
func TestService_WrongID(t *testing.T) {
|
||||
s := Service{db: createDatabase(t)}
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
stor := NewMockstorage(ctrl)
|
||||
stor.EXPECT().GetFeed(gomock.Any()).Times(1).Return(nil, errors.New("not found"))
|
||||
|
||||
s := Service{db: stor}
|
||||
|
||||
_, err := s.BuildFeed("invalid_feed_id")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestService_UpdateLastAccess(t *testing.T) {
|
||||
s := Service{db: createDatabase(t)}
|
||||
|
||||
feed1, err := s.QueryFeed(feed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
feed2, err := s.QueryFeed(feed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, feed2.LastAccess.After(feed1.LastAccess))
|
||||
}
|
||||
|
||||
func TestService_GetMetadata(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
stor := NewMockstorage(ctrl)
|
||||
stor.EXPECT().GetMetadata(feed.HashID).Times(1).Return(feed, nil)
|
||||
|
||||
stats := NewMockstats(ctrl)
|
||||
stats.EXPECT().Inc(MetricDownloads, feed.HashID).Return(int64(10), nil)
|
||||
|
||||
s := Service{
|
||||
db: createDatabase(t),
|
||||
stats: stats,
|
||||
}
|
||||
s := Service{db: stor, stats: stats}
|
||||
|
||||
m, err := s.GetMetadata(feed.HashID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(10), m.Downloads)
|
||||
}
|
||||
|
||||
func TestService_DowngradeToAnonymous(t *testing.T) {
|
||||
s := Service{db: createDatabase(t)}
|
||||
|
||||
feed := &model.Feed{
|
||||
HashID: "123456",
|
||||
UserID: "123456",
|
||||
ItemID: "123456",
|
||||
Provider: api.ProviderVimeo,
|
||||
LinkType: api.LinkTypeGroup,
|
||||
PageSize: 150,
|
||||
Quality: api.QualityLow,
|
||||
Format: api.FormatAudio,
|
||||
FeatureLevel: api.ExtendedFeatures,
|
||||
}
|
||||
|
||||
err := s.db.Insert(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Downgrade(feed.UserID, api.DefaultFeatures)
|
||||
require.NoError(t, err)
|
||||
|
||||
downgraded := &model.Feed{FeedID: feed.FeedID}
|
||||
err = s.db.Select(downgraded)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 50, downgraded.PageSize)
|
||||
require.Equal(t, api.QualityHigh, downgraded.Quality)
|
||||
require.Equal(t, api.FormatVideo, downgraded.Format)
|
||||
require.Equal(t, api.DefaultFeatures, downgraded.FeatureLevel)
|
||||
}
|
||||
|
||||
func TestService_DowngradeToExtendedFeatures(t *testing.T) {
|
||||
s := Service{db: createDatabase(t)}
|
||||
|
||||
feed := &model.Feed{
|
||||
HashID: "123456",
|
||||
UserID: "123456",
|
||||
ItemID: "123456",
|
||||
Provider: api.ProviderVimeo,
|
||||
LinkType: api.LinkTypeGroup,
|
||||
PageSize: 500,
|
||||
Quality: api.QualityLow,
|
||||
Format: api.FormatAudio,
|
||||
FeatureLevel: api.ExtendedFeatures,
|
||||
}
|
||||
|
||||
err := s.db.Insert(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Downgrade(feed.UserID, api.ExtendedFeatures)
|
||||
require.NoError(t, err)
|
||||
|
||||
downgraded := &model.Feed{FeedID: feed.FeedID}
|
||||
err = s.db.Select(downgraded)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 150, downgraded.PageSize)
|
||||
require.Equal(t, feed.Quality, downgraded.Quality)
|
||||
require.Equal(t, feed.Format, downgraded.Format)
|
||||
require.Equal(t, api.ExtendedFeatures, downgraded.FeatureLevel)
|
||||
}
|
||||
|
||||
func createDatabase(t *testing.T) *pg.DB {
|
||||
opts, err := pg.ParseURL("postgres://postgres:@localhost/podsync?sslmode=disable")
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
db := pg.Connect(opts)
|
||||
|
||||
_, err = db.Model(&model.Feed{}).Where("1=1").Delete()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = db.Insert(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
24
pkg/feeds/id_gen.go
Normal file
24
pkg/feeds/id_gen.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package feeds
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ventu-io/go-shortid"
|
||||
)
|
||||
|
||||
type IDGen struct {
|
||||
sid *shortid.Shortid
|
||||
}
|
||||
|
||||
func NewIDGen() (IDGen, error) {
|
||||
sid, err := shortid.New(1, shortid.DefaultABC, uint64(time.Now().UnixNano()))
|
||||
if err != nil {
|
||||
return IDGen{}, err
|
||||
}
|
||||
|
||||
return IDGen{sid}, nil
|
||||
}
|
||||
|
||||
func (id IDGen) Generate() (string, error) {
|
||||
return id.sid.Generate()
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"time"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
)
|
||||
|
||||
type Pledge struct {
|
184
pkg/storage/pg.go
Normal file
184
pkg/storage/pg.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
type Postgres struct {
|
||||
db *pg.DB
|
||||
}
|
||||
|
||||
func NewPG(connectionURL string, ping bool) (Postgres, error) {
|
||||
opts, err := pg.ParseURL(connectionURL)
|
||||
if err != nil {
|
||||
return Postgres{}, err
|
||||
}
|
||||
|
||||
// If host format is "projection:region:host", than use Google SQL Proxy
|
||||
// See https://github.com/go-pg/pg/issues/576
|
||||
if strings.Count(opts.Addr, ":") == 2 {
|
||||
log.Print("using GCP SQL proxy")
|
||||
opts.Dialer = func(network, addr string) (net.Conn, error) {
|
||||
return proxy.Dial(addr)
|
||||
}
|
||||
}
|
||||
|
||||
db := pg.Connect(opts)
|
||||
|
||||
// Check database connectivity
|
||||
if ping {
|
||||
if _, err := db.ExecOne("SELECT 1"); err != nil {
|
||||
_ = db.Close()
|
||||
return Postgres{}, errors.Wrap(err, "failed to check database connectivity")
|
||||
}
|
||||
}
|
||||
|
||||
return Postgres{db: db}, nil
|
||||
}
|
||||
|
||||
func (p Postgres) SaveFeed(feed *model.Feed) error {
|
||||
_, err := p.db.Model(feed).Insert()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to save feed to database")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Postgres) GetFeed(hashID string) (*model.Feed, error) {
|
||||
lastAccess := time.Now().UTC()
|
||||
|
||||
feed := &model.Feed{}
|
||||
res, err := p.db.Model(feed).
|
||||
Set("last_access = ?", lastAccess).
|
||||
Where("hash_id = ?", hashID).
|
||||
Returning("*").
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to query feed: %s", hashID)
|
||||
}
|
||||
|
||||
if res.RowsAffected() != 1 {
|
||||
return nil, api.ErrNotFound
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func (p Postgres) GetMetadata(hashID string) (*model.Feed, error) {
|
||||
feed := &model.Feed{}
|
||||
err := p.db.
|
||||
Model(feed).
|
||||
Where("hash_id = ?", hashID).
|
||||
Column("provider", "format", "quality", "user_id").
|
||||
Select()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func (p Postgres) Downgrade(patronID string, featureLevel int) error {
|
||||
if featureLevel > api.ExtendedFeatures {
|
||||
return nil
|
||||
}
|
||||
|
||||
if featureLevel == api.ExtendedFeatures {
|
||||
const maxPages = 150
|
||||
_, err := p.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("page_size = ?", maxPages).
|
||||
Where("user_id = ? AND page_size > ?", patronID, maxPages).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to reduce page sizes for patron '%s'", patronID)
|
||||
}
|
||||
|
||||
_, err = p.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("feature_level = ?", api.ExtendedFeatures).
|
||||
Where("user_id = ?", patronID, maxPages).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to downgrade patron '%s' to feature level %d", patronID, featureLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if featureLevel == api.DefaultFeatures {
|
||||
_, err := p.db.
|
||||
Model(&model.Feed{}).
|
||||
Set("page_size = ?", 50).
|
||||
Set("feature_level = ?", api.DefaultFeatures).
|
||||
Set("format = ?", api.FormatVideo).
|
||||
Set("quality = ?", api.QualityHigh).
|
||||
Where("user_id = ?", patronID).
|
||||
Update()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to downgrade patron '%s' to feature level %d", patronID, featureLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unsupported downgrade type")
|
||||
}
|
||||
|
||||
func (p Postgres) AddPledge(pledge *model.Pledge) error {
|
||||
return p.db.Insert(pledge)
|
||||
}
|
||||
|
||||
func (p Postgres) UpdatePledge(patronID string, pledge *model.Pledge) error {
|
||||
updateColumns := []string{
|
||||
"declined_since",
|
||||
"amount_cents",
|
||||
"total_historical_amount_cents",
|
||||
"outstanding_payment_amount_cents",
|
||||
"is_paused",
|
||||
}
|
||||
|
||||
res, err := p.db.Model(pledge).Column(updateColumns...).Where("patron_id = ?", patronID).Update()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update pledge %d for user %s: %v", pledge.PledgeID, patronID, err)
|
||||
}
|
||||
|
||||
if res.RowsAffected() != 1 {
|
||||
return errors.Wrapf(err, "unexpected number of updated rows: %d for user %s", res.RowsAffected(), patronID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Postgres) DeletePledge(pledge *model.Pledge) error {
|
||||
err := p.db.Delete(pledge)
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Postgres) GetPledge(patronID string) (*model.Pledge, error) {
|
||||
pledge := &model.Pledge{}
|
||||
return pledge, p.db.Model(pledge).Where("patron_id = ?", patronID).Limit(1).Select()
|
||||
}
|
||||
|
||||
func (p Postgres) Close() error {
|
||||
return p.db.Close()
|
||||
}
|
@@ -1,3 +1,7 @@
|
||||
package storage
|
||||
|
||||
//noinspection SpellCheckingInspection
|
||||
const pgsql = `
|
||||
BEGIN;
|
||||
|
||||
-- Pledges
|
||||
@@ -55,4 +59,6 @@ CREATE TABLE IF NOT EXISTS feeds (
|
||||
CREATE INDEX IF NOT EXISTS feeds_hash_id_idx ON feeds(hash_id);
|
||||
CREATE INDEX IF NOT EXISTS feeds_user_id_idx ON feeds(user_id);
|
||||
|
||||
COMMIT;
|
||||
COMMIT;
|
||||
END;
|
||||
`
|
226
pkg/storage/pg_test.go
Normal file
226
pkg/storage/pg_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
)
|
||||
|
||||
var (
|
||||
testPledge = &model.Pledge{PledgeID: 12345, AmountCents: 400, PatronID: 1, CreatedAt: time.Now()}
|
||||
testFeed = &model.Feed{FeedID: 1, HashID: "3", UserID: "3", ItemID: "4", LinkType: api.LinkTypeChannel, Provider: api.ProviderVimeo, Format: api.FormatAudio ,Quality: api.QualityLow}
|
||||
)
|
||||
|
||||
func TestPostgres_SaveFeed(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.SaveFeed(testFeed)
|
||||
require.NoError(t, err)
|
||||
|
||||
find := &model.Feed{FeedID: 1}
|
||||
err = stor.db.Model(find).Select()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, testFeed.FeedID, find.FeedID)
|
||||
require.Equal(t, testFeed.HashID, find.HashID)
|
||||
require.Equal(t, testFeed.UserID, find.UserID)
|
||||
require.Equal(t, testFeed.ItemID, find.ItemID)
|
||||
require.Equal(t, testFeed.LinkType, find.LinkType)
|
||||
require.Equal(t, testFeed.Provider, find.Provider)
|
||||
}
|
||||
|
||||
func TestPostgres_GetFeed(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.SaveFeed(testFeed)
|
||||
require.NoError(t, err)
|
||||
|
||||
find, err := stor.GetFeed(testFeed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, testFeed.FeedID, find.FeedID)
|
||||
require.Equal(t, testFeed.HashID, find.HashID)
|
||||
require.Equal(t, testFeed.UserID, find.UserID)
|
||||
require.Equal(t, testFeed.ItemID, find.ItemID)
|
||||
require.Equal(t, testFeed.LinkType, find.LinkType)
|
||||
require.Equal(t, testFeed.Provider, find.Provider)
|
||||
}
|
||||
|
||||
func TestService_UpdateLastAccess(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.db.Insert(testFeed)
|
||||
require.NoError(t, err)
|
||||
|
||||
feed1, err := stor.GetFeed(testFeed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
feed2, err := stor.GetFeed(testFeed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, feed2.LastAccess.After(feed1.LastAccess))
|
||||
}
|
||||
|
||||
func TestPostgres_GetMetadata(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.SaveFeed(testFeed)
|
||||
require.NoError(t, err)
|
||||
|
||||
find, err := stor.GetMetadata(testFeed.HashID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, testFeed.UserID, find.UserID)
|
||||
require.Equal(t, testFeed.Provider, find.Provider)
|
||||
require.Equal(t, testFeed.Quality, find.Quality)
|
||||
require.Equal(t, testFeed.Format, find.Format)
|
||||
}
|
||||
|
||||
func TestService_DowngradeToAnonymous(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
feed := &model.Feed{
|
||||
HashID: "123456",
|
||||
UserID: "123456",
|
||||
ItemID: "123456",
|
||||
Provider: api.ProviderVimeo,
|
||||
LinkType: api.LinkTypeGroup,
|
||||
PageSize: 150,
|
||||
Quality: api.QualityLow,
|
||||
Format: api.FormatAudio,
|
||||
FeatureLevel: api.ExtendedFeatures,
|
||||
}
|
||||
|
||||
err := stor.db.Insert(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stor.Downgrade(feed.UserID, api.DefaultFeatures)
|
||||
require.NoError(t, err)
|
||||
|
||||
downgraded := &model.Feed{FeedID: feed.FeedID}
|
||||
err = stor.db.Select(downgraded)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 50, downgraded.PageSize)
|
||||
require.Equal(t, api.QualityHigh, downgraded.Quality)
|
||||
require.Equal(t, api.FormatVideo, downgraded.Format)
|
||||
require.Equal(t, api.DefaultFeatures, downgraded.FeatureLevel)
|
||||
}
|
||||
|
||||
func TestService_DowngradeToExtendedFeatures(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
feed := &model.Feed{
|
||||
HashID: "123456",
|
||||
UserID: "123456",
|
||||
ItemID: "123456",
|
||||
Provider: api.ProviderVimeo,
|
||||
LinkType: api.LinkTypeGroup,
|
||||
PageSize: 500,
|
||||
Quality: api.QualityLow,
|
||||
Format: api.FormatAudio,
|
||||
FeatureLevel: api.ExtendedFeatures,
|
||||
}
|
||||
|
||||
err := stor.db.Insert(feed)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stor.Downgrade(feed.UserID, api.ExtendedFeatures)
|
||||
require.NoError(t, err)
|
||||
|
||||
downgraded := &model.Feed{FeedID: feed.FeedID}
|
||||
err = stor.db.Select(downgraded)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 150, downgraded.PageSize)
|
||||
require.Equal(t, feed.Quality, downgraded.Quality)
|
||||
require.Equal(t, feed.Format, downgraded.Format)
|
||||
require.Equal(t, api.ExtendedFeatures, downgraded.FeatureLevel)
|
||||
}
|
||||
|
||||
func TestPostgres_AddPledge(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.AddPledge(testPledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
pledge := &model.Pledge{PledgeID: 12345}
|
||||
err = stor.db.Select(pledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, int64(12345), pledge.PledgeID)
|
||||
require.Equal(t, 400, pledge.AmountCents)
|
||||
}
|
||||
|
||||
func TestPostgres_UpdatePledge(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.AddPledge(testPledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stor.UpdatePledge("1", &model.Pledge{AmountCents: 999})
|
||||
require.NoError(t, err)
|
||||
|
||||
pledge := &model.Pledge{PledgeID: 12345}
|
||||
err = stor.db.Select(pledge)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 999, pledge.AmountCents)
|
||||
}
|
||||
|
||||
func TestPostgres_DeletePledge(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.AddPledge(testPledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stor.DeletePledge(testPledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stor.db.Select(&model.Pledge{PledgeID: 12345})
|
||||
require.Equal(t, pg.ErrNoRows, err)
|
||||
}
|
||||
|
||||
func TestPostgres_GetPledge(t *testing.T) {
|
||||
stor := createPG(t)
|
||||
defer func() { _ = stor.Close() }()
|
||||
|
||||
err := stor.AddPledge(testPledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
pledge, err := stor.GetPledge("1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 400, pledge.AmountCents)
|
||||
require.Equal(t, int64(12345), pledge.PledgeID)
|
||||
}
|
||||
|
||||
// docker run -it --rm -p 5432:5432 -e POSTGRES_DB=podsync postgres
|
||||
func createPG(t *testing.T) Postgres {
|
||||
const localConnectionString = "postgres://postgres:@localhost/podsync?sslmode=disable"
|
||||
|
||||
postgres, err := NewPG(localConnectionString, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = postgres.db.Exec(pgsql)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, obj := range []interface{}{&model.Pledge{}, &model.Feed{}} {
|
||||
_, err = postgres.db.Model(obj).Where("1=1").Delete()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return postgres
|
||||
}
|
@@ -5,22 +5,29 @@ import (
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/mxpv/patreon-go"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
creatorID = "2822191"
|
||||
)
|
||||
|
||||
type Patreon struct {
|
||||
db *pg.DB
|
||||
type storage interface {
|
||||
AddPledge(pledge *model.Pledge) error
|
||||
UpdatePledge(patronID string, pledge *model.Pledge) error
|
||||
DeletePledge(pledge *model.Pledge) error
|
||||
GetPledge(patronID string) (*model.Pledge, error)
|
||||
}
|
||||
|
||||
func (h Patreon) toModel(pledge *patreon.Pledge) (*model.Pledge, error) {
|
||||
type Patreon struct {
|
||||
db storage
|
||||
}
|
||||
|
||||
func ToModel(pledge *patreon.Pledge) (*model.Pledge, error) {
|
||||
pledgeID, err := strconv.ParseInt(pledge.ID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse pledge id: %s", pledge.ID)
|
||||
@@ -63,53 +70,32 @@ func (h Patreon) toModel(pledge *patreon.Pledge) (*model.Pledge, error) {
|
||||
}
|
||||
|
||||
func (h Patreon) Hook(pledge *patreon.Pledge, event string) error {
|
||||
obj, err := h.toModel(pledge)
|
||||
obj, err := ToModel(pledge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch event {
|
||||
case patreon.EventCreatePledge:
|
||||
return h.db.Insert(obj)
|
||||
return h.db.AddPledge(obj)
|
||||
case patreon.EventUpdatePledge:
|
||||
// Update comes with different PledgeID from Patreon, so do update by user ID
|
||||
patronID := pledge.Relationships.Patron.Data.ID
|
||||
|
||||
updateColumns := []string{
|
||||
"declined_since",
|
||||
"amount_cents",
|
||||
"total_historical_amount_cents",
|
||||
"outstanding_payment_amount_cents",
|
||||
"is_paused",
|
||||
}
|
||||
|
||||
res, err := h.db.Model(obj).Column(updateColumns...).Where("patron_id = ?patron_id").Update()
|
||||
if err != nil {
|
||||
log.Printf("! failed to update pledge %s for user %s: %v", pledge.ID, patronID, err)
|
||||
if err := h.db.UpdatePledge(patronID, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.RowsAffected() != 1 {
|
||||
log.Printf("! unexpected number of updated rows: %d for user %s", res.RowsAffected(), patronID)
|
||||
return errors.New("unexpected update result")
|
||||
}
|
||||
|
||||
return nil
|
||||
case patreon.EventDeletePledge:
|
||||
err := h.db.Delete(obj)
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
return h.db.DeletePledge(obj)
|
||||
default:
|
||||
return fmt.Errorf("unknown event: %s", event)
|
||||
}
|
||||
}
|
||||
|
||||
func (h Patreon) FindPledge(patronID string) (*model.Pledge, error) {
|
||||
p := &model.Pledge{}
|
||||
return p, h.db.Model(p).Where("patron_id = ?", patronID).Limit(1).Select()
|
||||
return h.db.GetPledge(patronID)
|
||||
}
|
||||
|
||||
func (h Patreon) GetFeatureLevelByID(patronID string) (level int) {
|
||||
@@ -152,6 +138,6 @@ func (h Patreon) GetFeatureLevelFromAmount(amount int) int {
|
||||
return api.DefaultFeatures
|
||||
}
|
||||
|
||||
func NewPatreon(db *pg.DB) *Patreon {
|
||||
func NewPatreon(db storage) *Patreon {
|
||||
return &Patreon{db: db}
|
||||
}
|
||||
|
83
pkg/support/patreon_mock_test.go
Normal file
83
pkg/support/patreon_mock_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: patreon.go
|
||||
|
||||
// Package support is a generated GoMock package.
|
||||
package support
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
model "github.com/mxpv/podsync/pkg/model"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// Mockstorage is a mock of storage interface
|
||||
type Mockstorage struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockstorageMockRecorder
|
||||
}
|
||||
|
||||
// MockstorageMockRecorder is the mock recorder for Mockstorage
|
||||
type MockstorageMockRecorder struct {
|
||||
mock *Mockstorage
|
||||
}
|
||||
|
||||
// NewMockstorage creates a new mock instance
|
||||
func NewMockstorage(ctrl *gomock.Controller) *Mockstorage {
|
||||
mock := &Mockstorage{ctrl: ctrl}
|
||||
mock.recorder = &MockstorageMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *Mockstorage) EXPECT() *MockstorageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddPledge mocks base method
|
||||
func (m *Mockstorage) AddPledge(pledge *model.Pledge) error {
|
||||
ret := m.ctrl.Call(m, "AddPledge", pledge)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddPledge indicates an expected call of AddPledge
|
||||
func (mr *MockstorageMockRecorder) AddPledge(pledge interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPledge", reflect.TypeOf((*Mockstorage)(nil).AddPledge), pledge)
|
||||
}
|
||||
|
||||
// UpdatePledge mocks base method
|
||||
func (m *Mockstorage) UpdatePledge(patronID string, pledge *model.Pledge) error {
|
||||
ret := m.ctrl.Call(m, "UpdatePledge", patronID, pledge)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdatePledge indicates an expected call of UpdatePledge
|
||||
func (mr *MockstorageMockRecorder) UpdatePledge(patronID, pledge interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePledge", reflect.TypeOf((*Mockstorage)(nil).UpdatePledge), patronID, pledge)
|
||||
}
|
||||
|
||||
// DeletePledge mocks base method
|
||||
func (m *Mockstorage) DeletePledge(pledge *model.Pledge) error {
|
||||
ret := m.ctrl.Call(m, "DeletePledge", pledge)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeletePledge indicates an expected call of DeletePledge
|
||||
func (mr *MockstorageMockRecorder) DeletePledge(pledge interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePledge", reflect.TypeOf((*Mockstorage)(nil).DeletePledge), pledge)
|
||||
}
|
||||
|
||||
// GetPledge mocks base method
|
||||
func (m *Mockstorage) GetPledge(patronID string) (*model.Pledge, error) {
|
||||
ret := m.ctrl.Call(m, "GetPledge", patronID)
|
||||
ret0, _ := ret[0].(*model.Pledge)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPledge indicates an expected call of GetPledge
|
||||
func (mr *MockstorageMockRecorder) GetPledge(patronID interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPledge", reflect.TypeOf((*Mockstorage)(nil).GetPledge), patronID)
|
||||
}
|
@@ -1,96 +1,113 @@
|
||||
//go:generate mockgen -source=patreon.go -destination=patreon_mock_test.go -package=support
|
||||
|
||||
package support
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/mxpv/patreon-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mxpv/podsync/pkg/api"
|
||||
"github.com/mxpv/podsync/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
func TestToModel(t *testing.T) {
|
||||
pledge := createPledge()
|
||||
|
||||
hook := createHandler(t)
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
modelPledge, err := ToModel(pledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
model := &model.Pledge{PledgeID: 12345}
|
||||
err = hook.db.Select(model)
|
||||
require.Equal(t, modelPledge.PledgeID, int64(12345))
|
||||
require.Equal(t, modelPledge.AmountCents, 400)
|
||||
require.Equal(t, modelPledge.PatronID, int64(67890))
|
||||
require.NotNil(t, modelPledge.CreatedAt)
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
pledge := createPledge()
|
||||
expected, _ := ToModel(pledge)
|
||||
|
||||
storage := NewMockstorage(ctrl)
|
||||
storage.EXPECT().AddPledge(gomock.Eq(expected)).Times(1).Return(nil)
|
||||
|
||||
hook := Patreon{db: storage}
|
||||
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pledge.Attributes.AmountCents, model.AmountCents)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
pledge := createPledge()
|
||||
expected, _ := ToModel(pledge)
|
||||
|
||||
hook := createHandler(t)
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
storage := NewMockstorage(ctrl)
|
||||
storage.EXPECT().UpdatePledge("67890", gomock.Eq(expected))
|
||||
|
||||
hook := Patreon{db: storage}
|
||||
err := hook.Hook(pledge, patreon.EventUpdatePledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
pledge.Attributes.AmountCents = 999
|
||||
|
||||
err = hook.Hook(pledge, patreon.EventUpdatePledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
model := &model.Pledge{PledgeID: 12345}
|
||||
err = hook.db.Select(model)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 999, model.AmountCents)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
pledge := createPledge()
|
||||
hook := createHandler(t)
|
||||
expected, _ := ToModel(pledge)
|
||||
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
require.NoError(t, err)
|
||||
storage := NewMockstorage(ctrl)
|
||||
storage.EXPECT().DeletePledge(expected)
|
||||
|
||||
err = hook.Hook(pledge, patreon.EventDeletePledge)
|
||||
hook := Patreon{db: storage}
|
||||
err := hook.Hook(pledge, patreon.EventDeletePledge)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFindPledge(t *testing.T) {
|
||||
pledge := createPledge()
|
||||
hook := createHandler(t)
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
require.NoError(t, err)
|
||||
expected := &model.Pledge{}
|
||||
|
||||
res, err := hook.FindPledge("67890")
|
||||
storage := NewMockstorage(ctrl)
|
||||
storage.EXPECT().GetPledge("123").Times(1).Return(expected, nil)
|
||||
|
||||
hook := Patreon{db: storage}
|
||||
res, err := hook.FindPledge("123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, res.AmountCents, pledge.Attributes.AmountCents)
|
||||
require.Equal(t, expected, res)
|
||||
}
|
||||
|
||||
func TestGetFeatureLevel(t *testing.T) {
|
||||
pledge := createPledge()
|
||||
hook := createHandler(t)
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
err := hook.Hook(pledge, patreon.EventCreatePledge)
|
||||
pledge := createPledge()
|
||||
storage := NewMockstorage(ctrl)
|
||||
|
||||
ret, err := ToModel(pledge)
|
||||
require.NoError(t, err)
|
||||
|
||||
storage.EXPECT().GetPledge(pledge.Relationships.Patron.Data.ID).Return(ret, nil)
|
||||
storage.EXPECT().GetPledge("xyz").Return(nil, errors.New("not found"))
|
||||
|
||||
hook := Patreon{db: storage}
|
||||
|
||||
require.Equal(t, api.PodcasterFeature, hook.GetFeatureLevelByID(creatorID))
|
||||
require.Equal(t, api.DefaultFeatures, hook.GetFeatureLevelByID("xyz"))
|
||||
require.Equal(t, api.ExtendedPagination, hook.GetFeatureLevelByID(pledge.Relationships.Patron.Data.ID))
|
||||
}
|
||||
|
||||
func createHandler(t *testing.T) *Patreon {
|
||||
opts, err := pg.ParseURL("postgres://postgres:@localhost/podsync?sslmode=disable")
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
db := pg.Connect(opts)
|
||||
|
||||
_, err = db.Model(&model.Pledge{}).Where("1=1").Delete()
|
||||
require.NoError(t, err)
|
||||
|
||||
return NewPatreon(db)
|
||||
}
|
||||
|
||||
func createPledge() *patreon.Pledge {
|
||||
pledge := &patreon.Pledge{
|
||||
ID: "12345",
|
||||
|
Reference in New Issue
Block a user