Pārlūkot izejas kodu

feat: validate actions from the client prior to publishing

Fela Maslen 5 gadi atpakaļ
vecāks
revīzija
dcccd7a8eb

+ 8 - 1
gmus-backend/go.mod

@@ -3,17 +3,23 @@ module github.com/felamaslen/gmus-backend
 go 1.15
 
 require (
+	github.com/alicebob/miniredis v2.5.0+incompatible // indirect
+	github.com/alicebob/miniredis/v2 v2.14.1
 	github.com/anyhon/engine v0.1.0
 	github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc
-	github.com/go-redis/redis/v7 v7.4.0
+	github.com/elliotchance/redismock v1.5.3
+	github.com/go-playground/validator/v10 v10.4.1
+	github.com/go-redis/redis v6.15.9+incompatible
 	github.com/gofrs/uuid v3.2.0+incompatible // indirect
 	github.com/golang-migrate/migrate/v4 v4.13.0
+	github.com/gomodule/redigo v1.8.3 // indirect
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.4.2
 	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.2.0
 	github.com/joho/godotenv v1.3.0
+	github.com/leodido/go-urn v1.2.1 // indirect
 	github.com/lib/pq v1.3.0
 	github.com/nxadm/tail v1.4.5 // indirect
 	github.com/onsi/ginkgo v1.14.2
@@ -21,4 +27,5 @@ require (
 	github.com/rs/cors v1.7.0
 	github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc // indirect
 	golang.org/x/sys v0.0.0-20201214095126-aec9a390925b // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 )

+ 31 - 7
gmus-backend/go.sum

@@ -41,6 +41,12 @@ github.com/ClickHouse/clickhouse-go v1.3.12/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhH
 github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
+github.com/alicebob/miniredis/v2 v2.14.1 h1:GjlbSeoJ24bzdLRs13HoMEeaRZx9kg5nHoRW7QV/nCs=
+github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg=
 github.com/anyhon/engine v0.1.0 h1:z8LToJ0A7O3om+Iz+FfFDTqzmBYiIMBhGyFNvySq5NI=
 github.com/anyhon/engine v0.1.0/go.mod h1:1sir2IWytku5IeTl8s92+MJygKx5Y5IQhEq/+PX8RC0=
 github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
@@ -85,6 +91,8 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
 github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elliotchance/redismock v1.5.3 h1:Lgi2CLfVB3PamPI1SPqjJf5AiGisPFMWvIOCiRIq+sI=
+github.com/elliotchance/redismock v1.5.3/go.mod h1:8FFsGWghPUyP7nqj/UYXr2xqd6U2iNMxS4S5+Xadl5A=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -96,8 +104,16 @@ github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
-github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -139,6 +155,8 @@ github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v1.8.3 h1:HR0kYDX2RJZvAup8CsiJwxB4dTCSC0AaUq6S4SiLwUc=
+github.com/gomodule/redigo v1.8.3/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
 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/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
@@ -242,6 +260,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -264,13 +285,11 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
 github.com/nxadm/tail v1.4.5 h1:obHEce3upls1IBn1gTw/o7bCv7OJb6Ib/o7wNO+4eKw=
 github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
 github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
 github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
@@ -308,6 +327,7 @@ github.com/snowflakedb/gosnowflake v1.3.5/go.mod h1:13Ky+lxzIm3VqNDZJdyvu9MCGy+W
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -315,6 +335,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
@@ -322,6 +344,8 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0
 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/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
+github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
 go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -394,7 +418,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -428,6 +451,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -443,7 +467,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -623,7 +646,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -631,6 +653,8 @@ gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 55 - 8
gmus-backend/pkg/server/actions.go

@@ -2,16 +2,19 @@ package server
 
 import (
 	"encoding/json"
+	"errors"
+	"fmt"
 
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-playground/validator/v10"
+	"github.com/go-redis/redis"
 )
 
 type ActionType string
 
 const (
 	StateSet          ActionType = "STATE_SET"
-	ClientListUpdated            = "CLIENT_LIST_UPDATED"
+	ClientListUpdated ActionType = "CLIENT_LIST_UPDATED"
 )
 
 type Action struct {
@@ -20,7 +23,7 @@ type Action struct {
 	Payload    interface{} `json:"payload"`
 }
 
-func broadcastAction(l *logger.Logger, thisPodClients *map[string]*Client, action *Action) []error {
+func BroadcastAction(l *logger.Logger, thisPodClients *map[string]*Client, action *Action) []error {
 	var errors []error
 
 	for _, client := range *thisPodClients {
@@ -33,13 +36,57 @@ func broadcastAction(l *logger.Logger, thisPodClients *map[string]*Client, actio
 	return errors
 }
 
-func publishAction(rdb *redis.Client, action *Action) error {
-	pubsubPayload, err := json.Marshal(action)
-	if err != nil {
-		return err
+func validateAction(action *Action) (validatedAction *Action, err error) {
+	switch action.Type {
+	case StateSet:
+		var remarshaledPayload []byte
+		remarshaledPayload, err = json.Marshal(action.Payload)
+		if err != nil {
+			return
+		}
+
+		var playerState MusicPlayer
+		err = json.Unmarshal(remarshaledPayload, &playerState)
+		if err != nil {
+			return
+		}
+
+		v := validator.New()
+		err = v.Struct(playerState)
+		if err != nil {
+			err = errors.New(err.Error())
+			return
+		}
+
+		validatedAction = &Action{
+			Type:       StateSet,
+			FromClient: action.FromClient,
+			Payload:    playerState,
+		}
+		return
+	default:
+		err = errors.New(fmt.Sprintf("Invalid client action type: %s", action.Type))
+		return
 	}
-	if _, err := rdb.Publish(TOPIC_BROADCAST, pubsubPayload).Result(); err != nil {
+}
+
+func PublishAction(rdb redis.Cmdable, action []byte) error {
+	if _, err := rdb.Publish(TOPIC_BROADCAST, action).Result(); err != nil {
 		return err
 	}
 	return nil
 }
+
+func PublishActionFromClient(rdb redis.Cmdable, action *Action) error {
+	validatedAction, validationErr := validateAction(action)
+	if validationErr != nil {
+		return validationErr
+	}
+
+	pubsubPayload, err := json.Marshal(validatedAction)
+	if err != nil {
+		return err
+	}
+
+	return PublishAction(rdb, pubsubPayload)
+}

+ 240 - 0
gmus-backend/pkg/server/actions_test.go

@@ -0,0 +1,240 @@
+package server_test
+
+import (
+	"encoding/json"
+
+	"github.com/felamaslen/gmus-backend/pkg/server"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/alicebob/miniredis/v2"
+	"github.com/elliotchance/redismock"
+	"github.com/go-redis/redis"
+)
+
+func newTestRedis() *redismock.ClientMock {
+	mr, err := miniredis.Run()
+	if err != nil {
+		panic(err)
+	}
+
+	client := redis.NewClient(&redis.Options{
+		Addr: mr.Addr(),
+	})
+
+	return redismock.NewNiceMock(client)
+}
+
+var _ = Describe("Server actions", func() {
+	var rdb *redismock.ClientMock
+	BeforeEach(func() {
+		rdb = newTestRedis()
+		rdb.On("Publish").Return(redis.NewIntResult(0, nil))
+	})
+
+	Describe("PublishActionFromClient", func() {
+		Describe("state set actions", func() {
+			Context("when the action is valid", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": 123,
+    "playing": true,
+    "currentTime": 94,
+    "seekTime": -1,
+    "master": "some-master-client"
+  }
+}
+`), &action)
+				myClient := "my-client"
+				action.FromClient = &myClient
+
+				songId := 123
+				expectedAction := server.Action{
+					Type:       server.StateSet,
+					FromClient: &myClient,
+					Payload: server.MusicPlayer{
+						SongId:      &songId,
+						Playing:     true,
+						CurrentTime: 94,
+						SeekTime:    -1,
+						Master:      "some-master-client",
+					},
+				}
+				expectedActionString, jsonErr := json.Marshal(expectedAction)
+				if jsonErr != nil {
+					panic(jsonErr)
+				}
+
+				BeforeEach(func() {
+					rdb.On("Publish", server.TOPIC_BROADCAST, expectedActionString).Return(redis.NewIntResult(0, nil))
+				})
+
+				It("should publish the action to the redis pubsub", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).NotTo(BeEmpty())
+					Expect(err).To(BeNil())
+				})
+			})
+
+			Context("when the song ID is non-positive", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": 0,
+    "playing": true,
+    "currentTime": 94,
+    "seekTime": -1,
+    "master": "some-master-client"
+  }
+}
+`), &action)
+
+				It("should not publish a message", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).To(BeEmpty())
+					Expect(err).NotTo(BeNil())
+				})
+			})
+
+			Context("when the song ID is null", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": null,
+    "playing": false,
+    "currentTime": 0,
+    "seekTime": -1,
+    "master": "some-master-client"
+  }
+}
+`), &action)
+
+				expectedAction := server.Action{
+					Type: server.StateSet,
+					Payload: server.MusicPlayer{
+						SongId:      nil,
+						Playing:     false,
+						CurrentTime: 0,
+						SeekTime:    -1,
+						Master:      "some-master-client",
+					},
+				}
+
+				expectedActionString, jsonErr := json.Marshal(expectedAction)
+				if jsonErr != nil {
+					panic(jsonErr)
+				}
+
+				BeforeEach(func() {
+					rdb.On("Publish", server.TOPIC_BROADCAST, expectedActionString).Return(redis.NewIntResult(0, nil))
+				})
+
+				It("should publish a message", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).NotTo(BeEmpty())
+					Expect(err).To(BeNil())
+				})
+			})
+
+			Context("when the current time is negative", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": 123,
+    "playing": false,
+    "currentTime": -32,
+    "seekTime": -1,
+    "master": "some-master-client"
+  }
+}
+`), &action)
+
+				It("should not publish a message", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).To(BeEmpty())
+					Expect(err).NotTo(BeNil())
+				})
+			})
+
+			Context("when the seek time is less than -1", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": 123,
+    "playing": false,
+    "currentTime": 13,
+    "seekTime": -3,
+    "master": "some-master-client"
+  }
+}
+`), &action)
+
+				It("should not publish a message", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).To(BeEmpty())
+					Expect(err).NotTo(BeNil())
+				})
+			})
+
+			Context("when the master is empty", func() {
+				var action server.Action
+				json.Unmarshal([]byte(`
+{
+  "type": "STATE_SET",
+  "payload": {
+    "songId": 123,
+    "playing": false,
+    "currentTime": 13,
+    "seekTime": -3,
+    "master": ""
+  }
+}
+`), &action)
+
+				It("should not publish a message", func() {
+					err := server.PublishActionFromClient(rdb, &action)
+					Expect(rdb.Calls).To(BeEmpty())
+					Expect(err).NotTo(BeNil())
+				})
+			})
+		})
+
+		Describe("when the action is unrecognised", func() {
+			var action server.Action
+			// CLIENT_LIST_UPDATED should only ever come from the server
+			err := json.Unmarshal([]byte(`
+{
+  "type": "CLIENT_LIST_UPDATED",
+  "payload": [
+    { "name": "client-a", "lastPing": 123 },
+    { "name": "client-b", "lastPing": 456 }
+  ]
+}
+`), &action)
+			if err != nil {
+				panic(err)
+			}
+
+			BeforeEach(func() {
+				rdb.On("Publish", server.TOPIC_BROADCAST, "").Return(redis.NewIntResult(0, nil))
+			})
+
+			It("should not publish a message", func() {
+				err := server.PublishActionFromClient(rdb, &action)
+				Expect(rdb.Calls).To(BeEmpty())
+				Expect(err).NotTo(BeNil())
+			})
+		})
+	})
+})

+ 13 - 12
gmus-backend/pkg/server/clients.go

@@ -1,11 +1,12 @@
 package server
 
 import (
+	"encoding/json"
 	"net/http"
 	"time"
 
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 )
 
 func getClientNameFromRequest(r *http.Request) string {
@@ -22,7 +23,7 @@ func endSubscription(sub *redis.PubSub) error {
 	return nil
 }
 
-func publishClientList(l *logger.Logger, rdb *redis.Client) error {
+func publishClientList(l *logger.Logger, rdb redis.Cmdable) error {
 	clients, err := rdb.ZRangeWithScores(KEY_CLIENT_NAMES, 0, -1).Result()
 	if err != nil {
 		return err
@@ -36,12 +37,12 @@ func publishClientList(l *logger.Logger, rdb *redis.Client) error {
 		})
 	}
 
-	actionClientListUpdated := Action{
+	actionClientListUpdated, err := json.Marshal(Action{
 		Type:    ClientListUpdated,
 		Payload: members,
-	}
+	})
 
-	if err := publishAction(rdb, &actionClientListUpdated); err != nil {
+	if err := PublishAction(rdb, actionClientListUpdated); err != nil {
 		return err
 	}
 	return nil
@@ -53,11 +54,11 @@ func (c *Client) send(message interface{}) error {
 	return c.conn.WriteJSON(message)
 }
 
-func (c *Client) exposeToNetwork(l *logger.Logger, rdb *redis.Client) error {
+func (c *Client) exposeToNetwork(l *logger.Logger, rdb redis.Cmdable) error {
 	// Expose the client to all pods running the server
 	now := time.Now().Unix()
 
-	if _, err := rdb.ZAdd(KEY_CLIENT_NAMES, &redis.Z{
+	if _, err := rdb.ZAdd(KEY_CLIENT_NAMES, redis.Z{
 		Score:  float64(now),
 		Member: c.name,
 	}).Result(); err != nil {
@@ -69,7 +70,7 @@ func (c *Client) exposeToNetwork(l *logger.Logger, rdb *redis.Client) error {
 	return nil
 }
 
-func (c *Client) disposeFromNetwork(l *logger.Logger, rdb *redis.Client) error {
+func (c *Client) disposeFromNetwork(l *logger.Logger, rdb redis.Cmdable) error {
 	// Make sure other clients know when one goes away
 	if _, err := rdb.ZRem(KEY_CLIENT_NAMES, c.name).Result(); err != nil {
 		return err
@@ -80,7 +81,7 @@ func (c *Client) disposeFromNetwork(l *logger.Logger, rdb *redis.Client) error {
 	return nil
 }
 
-func (c *Client) subscribeToMe(l *logger.Logger, rdb *redis.Client) {
+func (c *Client) subscribeToMe(l *logger.Logger, rdb redis.Cmdable) {
 	// Subscribe this pod to messages from the client. This pod is responsible for
 	// onward publishing to other pods where necessary, via internal pubsub
 
@@ -99,14 +100,14 @@ func (c *Client) subscribeToMe(l *logger.Logger, rdb *redis.Client) {
 		} else {
 			actionFromClient.FromClient = &c.name
 
-			if err := publishAction(rdb, &actionFromClient); err != nil {
+			if err := PublishActionFromClient(rdb, &actionFromClient); err != nil {
 				l.Error("Error publishing action from client: %v\n", err)
 			}
 		}
 	}
 }
 
-func (c *Client) onConnect(l *logger.Logger, rdb *redis.Client) error {
+func (c *Client) onConnect(l *logger.Logger, rdb redis.Cmdable) error {
 	l.Verbose("[Client connected] %s\n", c.name)
 
 	if err := c.exposeToNetwork(l, rdb); err != nil {
@@ -119,7 +120,7 @@ func (c *Client) onConnect(l *logger.Logger, rdb *redis.Client) error {
 	return nil
 }
 
-func (c *Client) onDisconnect(l *logger.Logger, rdb *redis.Client) error {
+func (c *Client) onDisconnect(l *logger.Logger, rdb redis.Cmdable) error {
 	l.Verbose("[Client disconnected] %s\n", c.name)
 
 	if err := c.disposeFromNetwork(l, rdb); err != nil {

+ 9 - 9
gmus-backend/pkg/server/fetch.go

@@ -9,7 +9,7 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/logger"
 	"github.com/felamaslen/gmus-backend/pkg/read"
 	"github.com/felamaslen/gmus-backend/pkg/repository"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 	"github.com/jmoiron/sqlx"
 )
 
@@ -17,7 +17,7 @@ type ArtistsResponse struct {
 	Artists []string `json:"artists"`
 }
 
-func routeFetchArtists(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchArtists(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	db := database.GetConnection()
 	artists, err := repository.SelectAllArtists(db)
 	if err != nil {
@@ -40,7 +40,7 @@ type AlbumsResponse struct {
 	Albums []string `json:"albums"`
 }
 
-func routeFetchAlbums(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchAlbums(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	artist := r.URL.Query().Get("artist")
 
 	db := database.GetConnection()
@@ -67,7 +67,7 @@ type SongsResponse struct {
 	Songs  *[]*read.SongExternal `json:"songs"`
 }
 
-func routeFetchSongs(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchSongs(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	artist := r.URL.Query().Get("artist")
 
 	db := database.GetConnection()
@@ -100,7 +100,7 @@ func validateSongId(w http.ResponseWriter, r *http.Request) (id int, err error)
 	return
 }
 
-func routeFetchSongInfo(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchSongInfo(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	id, err := validateSongId(w, r)
 	if err != nil {
 		return nil
@@ -161,14 +161,14 @@ func respondWithSongOrNull(db *sqlx.DB, w http.ResponseWriter, song *read.Song)
 	return nil
 }
 
-func routeFetchNextSong(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchNextSong(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	id, err := validateSongId(w, r)
 	if err != nil {
 		return nil
 	}
 
 	db := database.GetConnection()
-	nextSong, err := repository.GetNextSong(db, int64(id))
+	nextSong, err := repository.GetNextSong(db, id)
 	if err != nil {
 		return err
 	}
@@ -179,14 +179,14 @@ func routeFetchNextSong(l *logger.Logger, rdb *redis.Client, w http.ResponseWrit
 	return nil
 }
 
-func routeFetchPrevSong(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func routeFetchPrevSong(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	id, err := validateSongId(w, r)
 	if err != nil {
 		return nil
 	}
 
 	db := database.GetConnection()
-	prevSong, err := repository.GetPrevSong(db, int64(id))
+	prevSong, err := repository.GetPrevSong(db, id)
 	if err != nil {
 		return err
 	}

+ 3 - 3
gmus-backend/pkg/server/handler.go

@@ -4,14 +4,14 @@ import (
 	"net/http"
 
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 )
 
-type RouteHandler func(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error
+type RouteHandler func(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error
 
 func routeHandler(
 	l *logger.Logger,
-	rdb *redis.Client,
+	rdb redis.Cmdable,
 	handler RouteHandler,
 ) func(w http.ResponseWriter, r *http.Request) {
 

+ 4 - 4
gmus-backend/pkg/server/pubsub.go

@@ -7,7 +7,7 @@ import (
 	"time"
 
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/websocket"
 )
@@ -19,7 +19,7 @@ var upgrader = websocket.Upgrader{
 }
 
 func handleClientSubscription(thisPodClients *map[string]*Client) RouteHandler {
-	return func(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+	return func(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 		clientName := getClientNameFromRequest(r)
 		if len(clientName) == 0 {
 			w.WriteHeader(400)
@@ -94,7 +94,7 @@ func subscribeToBroadcast(
 					l.Debug("[<-Client] %s (%s)\n", actionFromPubsub.Type, *actionFromPubsub.FromClient)
 				}
 
-				errors := broadcastAction(l, thisPodClients, &actionFromPubsub)
+				errors := BroadcastAction(l, thisPodClients, &actionFromPubsub)
 
 				if len(errors) > 0 {
 					l.Warn("Error broadcasting: %v\n", errors)
@@ -104,7 +104,7 @@ func subscribeToBroadcast(
 	}
 }
 
-func pruneDisappearedClients(l *logger.Logger, rdb *redis.Client) {
+func pruneDisappearedClients(l *logger.Logger, rdb redis.Cmdable) {
 	for {
 		now := time.Now().Unix()
 		rdb.ZRemRangeByScore(KEY_CLIENT_NAMES, "0", fmt.Sprintf("%d", now-CLIENT_TTL_SEC))

+ 1 - 1
gmus-backend/pkg/server/server.go

@@ -7,7 +7,7 @@ import (
 
 	"github.com/felamaslen/gmus-backend/pkg/config"
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 	"github.com/gorilla/mux"
 	"github.com/rs/cors"
 )

+ 13 - 0
gmus-backend/pkg/server/server_suite_test.go

@@ -0,0 +1,13 @@
+package server_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+func TestRepository(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Server Suite")
+}

+ 2 - 2
gmus-backend/pkg/server/stream.go

@@ -10,10 +10,10 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/database"
 	"github.com/felamaslen/gmus-backend/pkg/logger"
 	"github.com/felamaslen/gmus-backend/pkg/repository"
-	"github.com/go-redis/redis/v7"
+	"github.com/go-redis/redis"
 )
 
-func streamSong(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+func streamSong(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	songIdQuery := r.URL.Query().Get("songid")
 	songId, err := strconv.Atoi(songIdQuery)
 	if err != nil {

+ 8 - 6
gmus-backend/pkg/server/state.go → gmus-backend/pkg/server/types.go

@@ -29,12 +29,14 @@ type Member struct {
 // The master client is responsible for:
 // 1. Playing the music
 // 2. Keeping the server updated regularly about the current state
+//
+// This type here is merely used for validation of client state messages.
+// Each client implementation MUST adhere to this spec.
 
 type MusicPlayer struct {
-	SongId      int     `json:"songId"`
-	Playing     bool    `json:"playing"`
-	CurrentTime float32 `json:"currentTime"`
-	SeekTime    int     `json:"setTime"`
-
-	Master string `json:"master"`
+	SongId      *int    `json:"songId" validate:"omitempty,gte=1"`
+	Playing     bool    `json:"playing" validate:"-"`
+	CurrentTime float32 `json:"currentTime" validate:"gte=0"`
+	SeekTime    float32 `json:"seekTime" validate:"min=-1"`
+	Master      string  `json:"master" validate:"required"`
 }