diff --git a/.gitmodules b/.gitmodules index f616f3b..6fd0375 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "proto"] path = contracts url = https://git.sch9.ru/new_gate/contracts - update = rebase diff --git a/config/config.go b/config/config.go index 7c2bfa4..c061e5a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,18 +1,11 @@ package config type Config struct { - Env string `env:"ENV" env-default:"prod"` - - Address string `env:"ADDRESS" required:"true"` - + Env string `env:"ENV" env-default:"prod"` Pandoc string `env:"PANDOC" required:"true"` + Address string `env:"ADDRESS" required:"true"` PostgresDSN string `env:"POSTGRES_DSN" required:"true"` - RedisDSN string `env:"REDIS_DSN" required:"true"` - - JWTSecret string `env:"JWT_SECRET" required:"true"` - - AdminUsername string `env:"ADMIN_USERNAME" env-default:"admin"` - AdminPassword string `env:"ADMIN_PASSWORD" env-default:"admin"` + JWTSecret string `env:"JWT_SECRET" required:"true"` //RabbitDSN string `env:"RABBIT_DSN" required:"true"` //InstanceName string `env:"INSTANCE_NAME" required:"true"` diff --git a/contracts b/contracts index 91b7c68..89b4b19 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 91b7c6804671bcd533ab09939f21d27aacd5f793 +Subproject commit 89b4b19ae383c17665f0c3176e3d4122e90e46ec diff --git a/go.mod b/go.mod index b705472..42f6dcd 100644 --- a/go.mod +++ b/go.mod @@ -4,56 +4,75 @@ go 1.23.6 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/Masterminds/squirrel v1.5.4 github.com/gofiber/fiber/v2 v2.52.6 - github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/golang-jwt/jwt/v4 v4.5.1 github.com/google/uuid v1.6.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 - github.com/microcosm-cc/bluemonday v1.0.27 github.com/oapi-codegen/runtime v1.1.1 + github.com/open-policy-agent/opa v1.2.0 github.com/rabbitmq/amqp091-go v1.10.0 github.com/stretchr/testify v1.10.0 - github.com/valkey-io/valkey-go v1.0.57 - github.com/valkey-io/valkey-go/mock v1.0.57 - go.uber.org/mock v0.5.1 + github.com/valkey-io/valkey-go v1.0.47 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.36.0 ) require ( - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-sql-driver/mysql v1.9.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.21.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tchap/go-patricia/v2 v2.3.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/protobuf v1.36.3 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) require ( github.com/BurntSushi/toml v1.3.2 // indirect - github.com/jackc/pgx/v5 v5.7.4 + github.com/jackc/pgx/v5 v5.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a18b9bc..16d2140 100644 --- a/go.sum +++ b/go.sum @@ -5,62 +5,98 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= -github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -74,61 +110,115 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/open-policy-agent/opa v1.2.0 h1:88NDVCM0of1eO6Z4AFeL3utTEtMuwloFmWWU7dRV1z0= +github.com/open-policy-agent/opa v1.2.0/go.mod h1:30euUmOvuBoebRCcJ7DMF42bRBOPznvt0ACUMYDUGVY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/valkey-io/valkey-go v1.0.57 h1:rMpREZ7kvWwv9vHkB1WTpI9rX4dQHsvPHimSWenScvI= -github.com/valkey-io/valkey-go v1.0.57/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM= -github.com/valkey-io/valkey-go/mock v1.0.57 h1:ft06MuqCCKlob/R5dzUv4zNnNu+GaqElalApOFS5Fc4= -github.com/valkey-io/valkey-go/mock v1.0.57/go.mod h1:VDiXrmHdRCz/UT4xzMkfQEc5iHa7naDpqsZ+lotmJE8= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/valkey-io/valkey-go v1.0.47 h1:fW5+m2BaLAbxB1EWEEWmj+i2n+YcYFBDG/jKs6qu5j8= +github.com/valkey-io/valkey-go v1.0.47/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= -go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -137,3 +227,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/auth/delivery.go b/internal/auth/delivery.go deleted file mode 100644 index 45a5bb7..0000000 --- a/internal/auth/delivery.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth - -import ( - "github.com/gofiber/fiber/v2" -) - -type AuthHandlers interface { - ListSessions(c *fiber.Ctx) error - Terminate(c *fiber.Ctx) error - Login(c *fiber.Ctx) error - Logout(c *fiber.Ctx) error - Refresh(c *fiber.Ctx) error -} diff --git a/internal/auth/delivery/rest/handlers.go b/internal/auth/delivery/rest/handlers.go deleted file mode 100644 index 7d95003..0000000 --- a/internal/auth/delivery/rest/handlers.go +++ /dev/null @@ -1,160 +0,0 @@ -package rest - -import ( - "context" - "encoding/base64" - "git.sch9.ru/new_gate/ms-tester/internal/auth" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v4" - "strings" - "time" -) - -type Handlers struct { - authUC auth.UseCase - jwtSecret string -} - -func NewHandlers(authUC auth.UseCase, jwtSecret string) *Handlers { - return &Handlers{ - authUC: authUC, - jwtSecret: jwtSecret, - } -} - -const ( - sessionKey = "session" -) - -func sessionFromCtx(ctx context.Context) (*models.Session, error) { - const op = "sessionFromCtx" - - session, ok := ctx.Value(sessionKey).(*models.Session) - if !ok { - return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "") - } - - return session, nil -} - -func (h *Handlers) ListSessions(c *fiber.Ctx) error { - return c.SendStatus(fiber.StatusNotImplemented) -} - -func (h *Handlers) Terminate(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - err = h.authUC.Terminate(ctx, session.UserId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) -} - -func (h *Handlers) Login(c *fiber.Ctx) error { - authHeader := c.Get("Authorization", "") - if authHeader == "" { - return c.SendStatus(fiber.StatusUnauthorized) - } - - username, pwd, err := parseBasicAuth(authHeader) - if err != nil { - return c.SendStatus(fiber.StatusUnauthorized) - } - - credentials := &models.Credentials{ - Username: strings.ToLower(username), - Password: pwd, - } - device := &models.Device{ - Ip: c.IP(), - UseAgent: c.Get("User-Agent", ""), - } - - ctx := c.Context() - - session, err := h.authUC.Login(ctx, credentials, device) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{ - SessionId: session.Id, - UserId: session.UserId, - Role: session.Role, - IssuedAt: time.Now().Unix(), - }) - - token, err := claims.SignedString([]byte(h.jwtSecret)) - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - - c.Set("Authorization", "Bearer "+token) - - return c.SendStatus(fiber.StatusOK) -} - -func (h *Handlers) Logout(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - err = h.authUC.Logout(c.Context(), session.Id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) -} - -func (h *Handlers) Refresh(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - err = h.authUC.Refresh(c.Context(), session.Id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) -} - -func parseBasicAuth(header string) (string, string, error) { - const ( - op = "parseBasicAuth" - msg = "invalid auth header" - ) - - authParts := strings.Split(header, " ") - if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" { - return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg) - } - - decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1]) - if err != nil { - return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg) - } - - authParts = strings.Split(string(decodedAuth), ":") - if len(authParts) != 2 { - return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg) - } - - return authParts[0], authParts[1], nil -} diff --git a/internal/auth/usecase.go b/internal/auth/usecase.go deleted file mode 100644 index 0199add..0000000 --- a/internal/auth/usecase.go +++ /dev/null @@ -1,14 +0,0 @@ -package auth - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type UseCase interface { - Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error) - Refresh(ctx context.Context, sessionId string) error - Logout(ctx context.Context, sessionId string) error - Terminate(ctx context.Context, userId int32) error - ListSessions(ctx context.Context, userId int32) ([]*models.Session, error) -} diff --git a/internal/auth/usecase/usecase.go b/internal/auth/usecase/usecase.go deleted file mode 100644 index 1e13dde..0000000 --- a/internal/auth/usecase/usecase.go +++ /dev/null @@ -1,70 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/sessions" - "git.sch9.ru/new_gate/ms-tester/internal/users" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/google/uuid" - "time" -) - -type UseCase struct { - usersUC users.UseCase - sessionsUC sessions.UseCase -} - -func NewUseCase(usersUC users.UseCase, sessionsUC sessions.UseCase) *UseCase { - return &UseCase{ - usersUC: usersUC, - sessionsUC: sessionsUC, - } -} - -func (uc *UseCase) Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error) { - const op = "UseCase.Login" - - user, err := uc.usersUC.ReadUserByUsername(ctx, credentials.Username) - if err != nil { - return nil, err - } - - if !user.IsSamePwd(credentials.Password) { - return nil, pkg.Wrap(pkg.ErrNotFound, nil, op, "password mismatch") - } - - session := &models.Session{ - Id: uuid.NewString(), - UserId: user.Id, - Role: user.Role, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(40 * time.Minute), - UserAgent: device.UseAgent, - Ip: device.Ip, - } - - err = uc.sessionsUC.CreateSession(ctx, session) - if err != nil { - return nil, err - } - - return session, nil -} - -func (uc *UseCase) Logout(ctx context.Context, sessionId string) error { - return uc.sessionsUC.DeleteSession(ctx, sessionId) -} - -func (uc *UseCase) Refresh(ctx context.Context, sessionId string) error { - return uc.sessionsUC.UpdateSession(ctx, sessionId) -} - -func (uc *UseCase) Terminate(ctx context.Context, userId int32) error { - return uc.sessionsUC.DeleteAllSessions(ctx, userId) -} - -func (uc *UseCase) ListSessions(ctx context.Context, userId int32) ([]*models.Session, error) { - // TODO: implement me - panic("implement me") -} diff --git a/internal/contests/delivery/rest/contests_handlers.go b/internal/contests/delivery/rest/contests_handlers.go deleted file mode 100644 index c6a1972..0000000 --- a/internal/contests/delivery/rest/contests_handlers.go +++ /dev/null @@ -1,202 +0,0 @@ -package rest - -import ( - "context" - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/contests" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/problems" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" -) - -type Handlers struct { - problemsUC problems.UseCase - contestsUC contests.UseCase - - jwtSecret string -} - -func NewHandlers(problemsUC problems.UseCase, contestsUC contests.UseCase, jwtSecret string) *Handlers { - return &Handlers{ - problemsUC: problemsUC, - contestsUC: contestsUC, - - jwtSecret: jwtSecret, - } -} - -const ( - sessionKey = "session" -) - -func sessionFromCtx(ctx context.Context) (*models.Session, error) { - const op = "sessionFromCtx" - - session, ok := ctx.Value(sessionKey).(*models.Session) - if !ok { - return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "") - } - - return session, nil -} - -func (h *Handlers) CreateContest(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - id, err := h.contestsUC.CreateContest(ctx, "Название контеста") - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(&testerv1.CreateContestResponse{ - Id: id, - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) GetContest(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - contest, err := h.contestsUC.GetContest(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - tasks, err := h.contestsUC.GetTasks(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - solutions := make([]*models.SolutionsListItem, 0) - participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId) - if err == nil { // Admin or Teacher may not participate in contest - solutions, err = h.contestsUC.GetBestSolutions(ctx, id, participantId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - } - - return c.JSON(GetContestResponseDTO(contest, tasks, solutions)) - case models.RoleStudent: - contest, err := h.contestsUC.GetContest(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - tasks, err := h.contestsUC.GetTasks(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId) - solutions, err := h.contestsUC.GetBestSolutions(c.Context(), id, participantId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(GetContestResponseDTO(contest, tasks, solutions)) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) UpdateContest(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - var req testerv1.UpdateContestRequest - err := c.BodyParser(&req) - if err != nil { - return err - } - - err = h.contestsUC.UpdateContest(ctx, id, models.ContestUpdate{ - Title: req.Title, - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) DeleteContest(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - err := h.contestsUC.DeleteContest(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - filter := models.ContestsFilter{ - Page: params.Page, - PageSize: params.PageSize, - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - contestsList, err := h.contestsUC.ListContests(ctx, filter) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(ListContestsResponseDTO(contestsList)) - case models.RoleStudent: - filter.UserId = &session.UserId - contestsList, err := h.contestsUC.ListContests(ctx, filter) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(ListContestsResponseDTO(contestsList)) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} diff --git a/internal/contests/delivery/rest/dto.go b/internal/contests/delivery/rest/dto.go deleted file mode 100644 index e2b909b..0000000 --- a/internal/contests/delivery/rest/dto.go +++ /dev/null @@ -1,202 +0,0 @@ -package rest - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -func GetContestResponseDTO(contest *models.Contest, - tasks []*models.TasksListItem, - solutions []*models.SolutionsListItem) *testerv1.GetContestResponse { - - m := make(map[int32]*models.SolutionsListItem) - - for i := 0; i < len(solutions); i++ { - m[solutions[i].TaskPosition] = solutions[i] - } - - resp := testerv1.GetContestResponse{ - Contest: ContestDTO(*contest), - Tasks: make([]struct { - Solution testerv1.SolutionsListItem `json:"solution"` - Task testerv1.TasksListItem `json:"task"` - }, len(tasks)), - } - - for i, task := range tasks { - solution := testerv1.SolutionsListItem{} - if sol, ok := m[task.Position]; ok { - solution = SolutionsListItemDTO(*sol) - } - resp.Tasks[i] = struct { - Solution testerv1.SolutionsListItem `json:"solution"` - Task testerv1.TasksListItem `json:"task"` - }{ - Solution: solution, - Task: TasksListItemDTO(*task), - } - } - - return &resp -} - -func ListContestsResponseDTO(contestsList *models.ContestsList) *testerv1.ListContestsResponse { - resp := testerv1.ListContestsResponse{ - Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)), - Pagination: PaginationDTO(contestsList.Pagination), - } - - for i, contest := range contestsList.Contests { - resp.Contests[i] = ContestsListItemDTO(*contest) - } - - return &resp -} - -func ListSolutionsResponseDTO(solutionsList *models.SolutionsList) *testerv1.ListSolutionsResponse { - resp := testerv1.ListSolutionsResponse{ - Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)), - Pagination: PaginationDTO(solutionsList.Pagination), - } - - for i, solution := range solutionsList.Solutions { - resp.Solutions[i] = SolutionsListItemDTO(*solution) - } - - return &resp -} - -func GetTaskResponseDTO(contest *models.Contest, tasks []*models.TasksListItem, task *models.Task) *testerv1.GetTaskResponse { - resp := testerv1.GetTaskResponse{ - Contest: ContestDTO(*contest), - Tasks: make([]testerv1.TasksListItem, len(tasks)), - Task: *TaskDTO(task), - } - - for i, t := range tasks { - resp.Tasks[i] = TasksListItemDTO(*t) - } - - return &resp -} - -func PaginationDTO(p models.Pagination) testerv1.Pagination { - return testerv1.Pagination{ - Page: p.Page, - Total: p.Total, - } -} - -func ContestDTO(c models.Contest) testerv1.Contest { - return testerv1.Contest{ - Id: c.Id, - Title: c.Title, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, - } -} - -func ContestsListItemDTO(c models.ContestsListItem) testerv1.ContestsListItem { - return testerv1.ContestsListItem{ - Id: c.Id, - Title: c.Title, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, - } -} - -func TasksListItemDTO(t models.TasksListItem) testerv1.TasksListItem { - return testerv1.TasksListItem{ - Id: t.Id, - Position: t.Position, - Title: t.Title, - MemoryLimit: t.MemoryLimit, - ProblemId: t.ProblemId, - TimeLimit: t.TimeLimit, - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, - } -} - -func TaskDTO(t *models.Task) *testerv1.Task { - return &testerv1.Task{ - Id: t.Id, - Title: t.Title, - MemoryLimit: t.MemoryLimit, - TimeLimit: t.TimeLimit, - - InputFormatHtml: t.InputFormatHtml, - LegendHtml: t.LegendHtml, - NotesHtml: t.NotesHtml, - OutputFormatHtml: t.OutputFormatHtml, - Position: t.Position, - ScoringHtml: t.ScoringHtml, - - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, - } -} - -func ParticipantsListItemDTO(p models.ParticipantsListItem) testerv1.ParticipantsListItem { - return testerv1.ParticipantsListItem{ - Id: p.Id, - UserId: p.UserId, - Name: p.Name, - CreatedAt: p.CreatedAt, - UpdatedAt: p.UpdatedAt, - } -} - -func SolutionsListItemDTO(s models.SolutionsListItem) testerv1.SolutionsListItem { - return testerv1.SolutionsListItem{ - Id: s.Id, - - ParticipantId: s.ParticipantId, - ParticipantName: s.ParticipantName, - - State: s.State, - Score: s.Score, - Penalty: s.Penalty, - TimeStat: s.TimeStat, - MemoryStat: s.MemoryStat, - Language: s.Language, - - TaskId: s.TaskId, - TaskPosition: s.TaskPosition, - TaskTitle: s.TaskTitle, - - ContestId: s.ContestId, - ContestTitle: s.ContestTitle, - - CreatedAt: s.CreatedAt, - UpdatedAt: s.UpdatedAt, - } -} - -func SolutionDTO(s models.Solution) testerv1.Solution { - return testerv1.Solution{ - Id: s.Id, - - ParticipantId: s.ParticipantId, - ParticipantName: s.ParticipantName, - - Solution: s.Solution, - - State: s.State, - Score: s.Score, - Penalty: s.Penalty, - TimeStat: s.TimeStat, - MemoryStat: s.MemoryStat, - Language: s.Language, - - TaskId: s.TaskId, - TaskPosition: s.TaskPosition, - TaskTitle: s.TaskTitle, - - ContestId: s.ContestId, - ContestTitle: s.ContestTitle, - - CreatedAt: s.CreatedAt, - UpdatedAt: s.UpdatedAt, - } -} diff --git a/internal/contests/delivery/rest/monitor_handlers.go b/internal/contests/delivery/rest/monitor_handlers.go deleted file mode 100644 index d916421..0000000 --- a/internal/contests/delivery/rest/monitor_handlers.go +++ /dev/null @@ -1,71 +0,0 @@ -package rest - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" -) - -func (h *Handlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher, models.RoleStudent: - contest, err := h.contestsUC.GetContest(ctx, params.ContestId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - monitor, err := h.contestsUC.GetMonitor(ctx, params.ContestId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - tasks, err := h.contestsUC.GetTasks(ctx, params.ContestId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - resp := testerv1.GetMonitorResponse{ - Contest: ContestDTO(*contest), - Tasks: make([]testerv1.TasksListItem, len(tasks)), - Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)), - SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)), - } - - for i, participant := range monitor.Participants { - resp.Participants[i] = testerv1.ParticipantsStat{ - Id: participant.Id, - Name: participant.Name, - PenaltyInTotal: participant.PenaltyInTotal, - Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)), - SolvedInTotal: participant.SolvedInTotal, - } - - for j, solution := range participant.Solutions { - resp.Participants[i].Solutions[j] = SolutionsListItemDTO(*solution) - } - } - - for i, problem := range monitor.Summary { - resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{ - Id: problem.Id, - Success: problem.Success, - Total: problem.Total, - } - } - - for i, task := range tasks { - resp.Tasks[i] = TasksListItemDTO(*task) - } - return c.JSON(resp) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} diff --git a/internal/contests/delivery/rest/participants_handlers.go b/internal/contests/delivery/rest/participants_handlers.go deleted file mode 100644 index dce23b8..0000000 --- a/internal/contests/delivery/rest/participants_handlers.go +++ /dev/null @@ -1,116 +0,0 @@ -package rest - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" -) - -func (h *Handlers) CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - id, err := h.contestsUC.CreateParticipant(ctx, params.ContestId, params.UserId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.CreateParticipantResponse{ - Id: id, - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - var req testerv1.UpdateParticipantRequest - err := c.BodyParser(&req) - if err != nil { - return err - } - - err = h.contestsUC.UpdateParticipant(ctx, params.ParticipantId, models.ParticipantUpdate{ - Name: req.Name, - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - - } -} - -func (h *Handlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{ - Page: params.Page, - PageSize: params.PageSize, - ContestId: params.ContestId, - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - resp := testerv1.ListParticipantsResponse{ - Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)), - Pagination: PaginationDTO(participantsList.Pagination), - } - - for i, participant := range participantsList.Participants { - resp.Participants[i] = ParticipantsListItemDTO(*participant) - } - - return c.JSON(resp) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} diff --git a/internal/contests/delivery/rest/solutions_handlers.go b/internal/contests/delivery/rest/solutions_handlers.go deleted file mode 100644 index b2ff3d7..0000000 --- a/internal/contests/delivery/rest/solutions_handlers.go +++ /dev/null @@ -1,148 +0,0 @@ -package rest - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" - "io" -) - -const ( - maxSolutionSize int64 = 10 * 1024 * 1024 -) - -func (h *Handlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher, models.RoleStudent: - s, err := c.FormFile("solution") - if err != nil { - return err - } - - if s.Size == 0 || s.Size > maxSolutionSize { - return c.SendStatus(fiber.StatusBadRequest) - } - - f, err := s.Open() - if err != nil { - return err - } - defer f.Close() - - b, err := io.ReadAll(f) - if err != nil { - return err - } - - id, err := h.contestsUC.CreateSolution(ctx, &models.SolutionCreation{ - UserId: session.UserId, - TaskId: params.TaskId, - Language: params.Language, - Solution: string(b), - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.CreateSolutionResponse{ - Id: id, - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) GetSolution(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - solution, err := h.contestsUC.GetSolution(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)}) - case models.RoleStudent: - _, err := h.contestsUC.GetParticipantId3(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - solution, err := h.contestsUC.GetSolution(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)}) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - filter := models.SolutionsFilter{ - ContestId: params.ContestId, - Page: params.Page, - PageSize: params.PageSize, - ParticipantId: params.ParticipantId, - TaskId: params.TaskId, - Language: params.Language, - Order: params.Order, - State: params.State, - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - solutionsList, err := h.contestsUC.ListSolutions(ctx, filter) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(ListSolutionsResponseDTO(solutionsList)) - case models.RoleStudent: - if params.ContestId == nil { - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } - - participantId, err := h.contestsUC.GetParticipantId(ctx, *params.ContestId, session.UserId) - if err != nil { - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } - - // Student cannot view other users' solutions - if params.ParticipantId != nil && *params.ParticipantId != participantId { - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } - - filter.ParticipantId = &participantId - solutionsList, err := h.contestsUC.ListSolutions(ctx, filter) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(ListSolutionsResponseDTO(solutionsList)) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} diff --git a/internal/contests/delivery/rest/tasks_handlers.go b/internal/contests/delivery/rest/tasks_handlers.go deleted file mode 100644 index 22732fa..0000000 --- a/internal/contests/delivery/rest/tasks_handlers.go +++ /dev/null @@ -1,105 +0,0 @@ -package rest - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" -) - -func (h *Handlers) CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - id, err := h.contestsUC.CreateTask(ctx, params.ContestId, params.ProblemId) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.CreateTaskResponse{ - Id: id, - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) GetTask(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - contest, err := h.contestsUC.GetContest(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - tasks, err := h.contestsUC.GetTasks(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - t, err := h.contestsUC.GetTask(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(GetTaskResponseDTO(contest, tasks, t)) - case models.RoleStudent: - _, err = h.contestsUC.GetParticipantId2(ctx, id, session.UserId) - if err != nil { - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } - - contest, err := h.contestsUC.GetContest(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - tasks, err := h.contestsUC.GetTasks(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - t, err := h.contestsUC.GetTask(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(GetTaskResponseDTO(contest, tasks, t)) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) DeleteTask(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - err := h.contestsUC.DeleteTask(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} diff --git a/internal/contests/pg_repository.go b/internal/contests/pg_repository.go deleted file mode 100644 index c9f1cc5..0000000 --- a/internal/contests/pg_repository.go +++ /dev/null @@ -1,34 +0,0 @@ -package contests - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type Repository interface { - CreateContest(ctx context.Context, title string) (int32, error) - GetContest(ctx context.Context, id int32) (*models.Contest, error) - DeleteContest(ctx context.Context, id int32) error - UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error - ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) - - CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error) - GetTask(ctx context.Context, id int32) (*models.Task, error) - DeleteTask(ctx context.Context, taskId int32) error - GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) - - GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) - GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) - GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) - CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) - DeleteParticipant(ctx context.Context, participantId int32) error - UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error - ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) - - GetSolution(ctx context.Context, id int32) (*models.Solution, error) - CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) - ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) - GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) - - GetMonitor(ctx context.Context, id int32, penalty int32) (*models.Monitor, error) -} diff --git a/internal/contests/repository/contests_pg_repository.go b/internal/contests/repository/contests_pg_repository.go deleted file mode 100644 index 4a721f3..0000000 --- a/internal/contests/repository/contests_pg_repository.go +++ /dev/null @@ -1,145 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - sq "github.com/Masterminds/squirrel" - "github.com/jmoiron/sqlx" -) - -type Repository struct { - db *sqlx.DB -} - -func NewRepository(db *sqlx.DB) *Repository { - return &Repository{ - db: db, - } -} - -const CreateContestQuery = "INSERT INTO contests (title) VALUES ($1) RETURNING id" - -func (r *Repository) CreateContest(ctx context.Context, title string) (int32, error) { - const op = "Repository.CreateContest" - - rows, err := r.db.QueryxContext(ctx, CreateContestQuery, title) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return id, nil -} - -const GetContestQuery = "SELECT * from contests WHERE id=$1 LIMIT 1" - -func (r *Repository) GetContest(ctx context.Context, id int32) (*models.Contest, error) { - const op = "Repository.GetContest" - - var contest models.Contest - err := r.db.GetContext(ctx, &contest, GetContestQuery, id) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - return &contest, nil -} - -const ( - UpdateContestQuery = "UPDATE contests SET title = COALESCE($1, title) WHERE id = $2" -) - -func (r *Repository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error { - const op = "Repository.UpdateContest" - - _, err := r.db.ExecContext(ctx, UpdateContestQuery, contestUpdate.Title, id) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} - -const DeleteContestQuery = "DELETE FROM contests WHERE id=$1" - -func (r *Repository) DeleteContest(ctx context.Context, id int32) error { - const op = "Repository.DeleteContest" - - _, err := r.db.ExecContext(ctx, DeleteContestQuery, id) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} - -func buildListContestsQueries(filter models.ContestsFilter) (sq.SelectBuilder, sq.SelectBuilder) { - columns := []string{ - "c.id", - "c.title", - "c.created_at", - "c.updated_at", - } - - qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).From("contests c") - - if filter.UserId != nil { - qb = qb.Join("participants p ON c.id = p.contest_id") - qb = qb.Where(sq.Eq{"p.user_id": *filter.UserId}) - } - - countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub") - - if filter.Order != nil && *filter.Order < 0 { - qb = qb.OrderBy("c.created_at DESC") - } else { - qb = qb.OrderBy("c.created_at ASC") - } - - qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset())) - - return qb, countQb -} - -func (r *Repository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) { - const op = "Repository.ListContests" - - baseQb, countQb := buildListContestsQueries(filter) - - query, args, err := baseQb.ToSql() - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var contests []*models.ContestsListItem - err = r.db.SelectContext(ctx, &contests, query, args...) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - query, args, err = countQb.ToSql() - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var count int32 - err = r.db.GetContext(ctx, &count, query, args...) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &models.ContestsList{ - Contests: contests, - Pagination: models.Pagination{ - Total: models.Total(count, filter.PageSize), - Page: filter.Page, - }, - }, nil -} diff --git a/internal/contests/repository/contests_pg_repository_test.go b/internal/contests/repository/contests_pg_repository_test.go deleted file mode 100644 index 593994b..0000000 --- a/internal/contests/repository/contests_pg_repository_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package repository_test - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/contests/repository" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "github.com/DATA-DOG/go-sqlmock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing. -func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) { - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - assert.NoError(t, err) - sqlxDB := sqlx.NewDb(db, "sqlmock") - return sqlxDB, mock -} - -func TestRepository_CreateContest(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - contest := models.Contest{ - Id: 1, - Title: "Test Contest", - } - - mock.ExpectQuery(repository.CreateContestQuery). - WithArgs(contest.Title). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(contest.Id)) - - id, err := repo.CreateContest(ctx, contest.Title) - assert.NoError(t, err) - assert.Equal(t, contest.Id, id) - }) -} - -func TestRepository_GetContest(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - contest := models.Contest{ - Id: 1, - Title: "Test Contest", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - mock.ExpectQuery(repository.GetContestQuery). - WithArgs(contest.Id). - WillReturnRows(sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}). - AddRow(contest.Id, contest.Title, contest.CreatedAt, contest.UpdatedAt)) - - result, err := repo.GetContest(ctx, contest.Id) - assert.NoError(t, err) - assert.EqualExportedValues(t, &contest, result) - }) -} - -func TestRepository_UpdateContest(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - var contestId int32 = 1 - update := models.ContestUpdate{ - Title: sp("Updated Contest"), - } - - mock.ExpectExec(repository.UpdateContestQuery). - WithArgs(update.Title, contestId). - WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.UpdateContest(ctx, contestId, update) - assert.NoError(t, err) - }) -} - -func TestRepository_DeleteContest(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - mock.ExpectExec(repository.DeleteContestQuery). - WithArgs(1). - WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.DeleteContest(ctx, 1) - assert.NoError(t, err) - }) -} - -func sp(s string) *string { - return &s -} diff --git a/internal/contests/repository/monitor_pg_repository.go b/internal/contests/repository/monitor_pg_repository.go deleted file mode 100644 index 94487fa..0000000 --- a/internal/contests/repository/monitor_pg_repository.go +++ /dev/null @@ -1,161 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" -) - -const ( - // state=5 - AC - ReadStatisticsQuery = ` -SELECT t.id as task_id, - t.position, - COUNT(*) as total, - COUNT(CASE WHEN s.state = 5 THEN 1 END) as success -FROM tasks t LEFT JOIN solutions s ON t.id = s.task_id -WHERE t.contest_id = $1 -GROUP BY t.id, t.position -ORDER BY t.position; -` - - SolutionsQuery = ` -WITH RankedSolutions AS ( - SELECT - s.id, - - s.participant_id, - p2.name as participant_name, - - s.state, - s.score, - s.penalty, - s.time_stat, - s.memory_stat, - s.language, - - s.task_id, - t.position as task_position, - p.title as task_title, - - t.contest_id, - c.title as contest_title, - - s.updated_at, - s.created_at, - ROW_NUMBER() OVER ( - PARTITION BY s.task_id, s.participant_id - ORDER BY s.score DESC, s.created_at - ) as rn - FROM solutions s - LEFT JOIN tasks t ON s.task_id = t.id - LEFT JOIN problems p ON t.problem_id = p.id - LEFT JOIN contests c ON t.contest_id = c.id - LEFT JOIN participants p2 on s.participant_id = p2.id - WHERE t.contest_id = $1 -) -SELECT - rs.id, - - rs.participant_id, - rs.participant_name, - - rs.state, - rs.score, - rs.penalty, - rs.time_stat, - rs.memory_stat, - rs.language, - - rs.task_id, - rs.task_position, - rs.task_title, - - rs.contest_id, - rs.contest_title, - - rs.updated_at, - rs.created_at -FROM RankedSolutions rs -WHERE rs.rn = 1` - - ParticipantsQuery = ` -WITH Attempts AS ( - SELECT - s.participant_id, - s.task_id, - COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < ( - SELECT MIN(s2.created_at) - FROM solutions s2 - WHERE s2.participant_id = s.participant_id - AND s2.task_id = s.task_id - AND s2.state = 5 - )) as failed_attempts, - MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty - FROM solutions s JOIN tasks t ON t.id = s.task_id - WHERE t.contest_id = $1 - GROUP BY s.participant_id, s.task_id -) -SELECT - p.id, - p.name, - COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total, - COALESCE(SUM(a.failed_attempts), 0) * $2 + COALESCE(SUM(a.success_penalty), 0) as penalty_in_total -FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id -WHERE p.contest_id = $1 -GROUP BY p.id, p.name -` -) - -func (r *Repository) GetMonitor(ctx context.Context, contestId int32, penalty int32) (*models.Monitor, error) { - const op = "Repository.GetMonitor" - - rows, err := r.db.QueryxContext(ctx, ReadStatisticsQuery, contestId) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - defer rows.Close() - - var monitor models.Monitor - for rows.Next() { - var stat models.ProblemStatSummary - err = rows.StructScan(&stat) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - monitor.Summary = append(monitor.Summary, &stat) - } - - var solutions []*models.SolutionsListItem - err = r.db.SelectContext(ctx, &solutions, SolutionsQuery, contestId) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - rows3, err := r.db.QueryxContext(ctx, ParticipantsQuery, contestId, penalty) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - defer rows3.Close() - - solutionsMap := make(map[int32][]*models.SolutionsListItem) - for _, solution := range solutions { - solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution) - } - - for rows3.Next() { - var stat models.ParticipantsStat - err = rows3.StructScan(&stat) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - if sols, ok := solutionsMap[stat.Id]; ok { - stat.Solutions = sols - } - - monitor.Participants = append(monitor.Participants, &stat) - } - - return &monitor, nil -} diff --git a/internal/contests/repository/monitor_pg_repository_test.go b/internal/contests/repository/monitor_pg_repository_test.go deleted file mode 100644 index 50a4378..0000000 --- a/internal/contests/repository/monitor_pg_repository_test.go +++ /dev/null @@ -1 +0,0 @@ -package repository diff --git a/internal/contests/repository/participants_pg_repository.go b/internal/contests/repository/participants_pg_repository.go deleted file mode 100644 index ccd966e..0000000 --- a/internal/contests/repository/participants_pg_repository.go +++ /dev/null @@ -1,126 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" -) - -const GetParticipantIdQuery = "SELECT id FROM participants WHERE user_id=$1 AND contest_id=$2 LIMIT 1" - -func (r *Repository) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) { - const op = "Repository.GetParticipantId" - - var participantId int32 - err := r.db.GetContext(ctx, &participantId, GetParticipantIdQuery, userId, contestId) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return participantId, nil -} - -const GetParticipantId2Query = "SELECT p.id FROM participants p JOIN tasks t ON p.contest_id=t.contest_id WHERE user_id=$1 AND t.id=$2 LIMIT 1" - -func (r *Repository) GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) { - const op = "Repository.GetParticipantId2" - - var participantId int32 - err := r.db.GetContext(ctx, &participantId, GetParticipantId2Query, userId, taskId) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return participantId, nil -} - -const GetParticipantId3Query = "SELECT participant_id FROM solutions WHERE id=$1 LIMIT 1" - -func (r *Repository) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) { - const op = "Repository.GetParticipantId3" - - var participantId int32 - err := r.db.GetContext(ctx, &participantId, GetParticipantId3Query, solutionId) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return participantId, nil -} - -const CreateParticipantQuery = "INSERT INTO participants (user_id, contest_id, name) VALUES ($1, $2, $3) RETURNING id" - -func (r *Repository) CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) { - const op = "Repository.CreateParticipant" - - name := "" - rows, err := r.db.QueryxContext(ctx, CreateParticipantQuery, userId, contestId, name) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, err - } - return id, nil -} - -const DeleteParticipantQuery = "DELETE FROM participants WHERE id=$1" - -const ( - UpdateParticipantQuery = "UPDATE participants SET name = COALESCE($1, name) WHERE id = $2" -) - -func (r *Repository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error { - const op = "Repository.UpdateParticipant" - - _, err := r.db.ExecContext(ctx, UpdateParticipantQuery, participantUpdate.Name, id) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} - -func (r *Repository) DeleteParticipant(ctx context.Context, participantId int32) error { - const op = "Repository.DeleteParticipant" - - _, err := r.db.ExecContext(ctx, DeleteParticipantQuery, participantId) - if err != nil { - return pkg.HandlePgErr(err, op) - } - return nil -} - -const ( - ReadParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = $1 LIMIT $2 OFFSET $3` - CountParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = $1" -) - -func (r *Repository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) { - const op = "Repository.ReadParticipants" - - var participants []*models.ParticipantsListItem - err := r.db.SelectContext(ctx, &participants, - ReadParticipantsListQuery, filter.ContestId, filter.PageSize, filter.Offset()) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var count int32 - err = r.db.GetContext(ctx, &count, CountParticipantsQuery, filter.ContestId) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &models.ParticipantsList{ - Participants: participants, - Pagination: models.Pagination{ - Total: models.Total(count, filter.PageSize), - Page: filter.Page, - }, - }, nil -} diff --git a/internal/contests/repository/participants_pg_repository_test.go b/internal/contests/repository/participants_pg_repository_test.go deleted file mode 100644 index 4607947..0000000 --- a/internal/contests/repository/participants_pg_repository_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package repository_test - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/contests/repository" - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestRepository_CreateParticipant(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - var ( - expectedId int32 = 1 - userId int32 = 2 - contestId int32 = 3 - ) - ctx := context.Background() - - mock.ExpectQuery(repository.CreateParticipantQuery). - WithArgs(userId, contestId). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId)) - - id, err := repo.CreateParticipant(ctx, contestId, userId) - assert.NoError(t, err) - assert.Equal(t, expectedId, id) - }) -} - -func TestRepository_DeleteParticipant(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - var participantId int32 = 1 - - mock.ExpectExec(repository.DeleteParticipantQuery). - WithArgs(participantId).WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.DeleteParticipant(ctx, participantId) - assert.NoError(t, err) - }) -} diff --git a/internal/contests/repository/solutions_pg_repository.go b/internal/contests/repository/solutions_pg_repository.go deleted file mode 100644 index c02a16c..0000000 --- a/internal/contests/repository/solutions_pg_repository.go +++ /dev/null @@ -1,222 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - sq "github.com/Masterminds/squirrel" -) - -const ( - GetSolutionQuery = "SELECT * FROM solutions WHERE id = $1" -) - -func (r *Repository) GetSolution(ctx context.Context, id int32) (*models.Solution, error) { - const op = "Repository.GetSolution" - - var solution models.Solution - err := r.db.GetContext(ctx, &solution, GetSolutionQuery, id) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &solution, nil -} - -const ( - CreateSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution) -VALUES ($1, $2, $3, $4, $5) -RETURNING id` -) - -func (r *Repository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { - const op = "Repository.CreateSolution" - - rows, err := r.db.QueryxContext(ctx, - CreateSolutionQuery, - creation.TaskId, - creation.ParticipantId, - creation.Language, - creation.Penalty, - creation.Solution, - ) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return id, nil -} - -func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) { - columns := []string{ - "s.id", - "s.participant_id", - "p2.name AS participant_name", - "s.state", - "s.score", - "s.penalty", - "s.time_stat", - "s.memory_stat", - "s.language", - "s.task_id", - "t.position AS task_position", - "p.title AS task_title", - "t.contest_id", - "c.title", - "s.updated_at", - "s.created_at", - } - - qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...). - From("solutions s"). - LeftJoin("tasks t ON s.task_id = t.id"). - LeftJoin("problems p ON t.problem_id = p.id"). - LeftJoin("contests c ON t.contest_id = c.id"). - LeftJoin("participants p2 ON s.participant_id = p2.id") - - if filter.ContestId != nil { - qb = qb.Where(sq.Eq{"s.contest_id": *filter.ContestId}) - } - if filter.ParticipantId != nil { - qb = qb.Where(sq.Eq{"s.participant_id": *filter.ParticipantId}) - } - if filter.TaskId != nil { - qb = qb.Where(sq.Eq{"s.task_id": *filter.TaskId}) - } - if filter.Language != nil { - qb = qb.Where(sq.Eq{"s.language": *filter.Language}) - } - if filter.State != nil { - qb = qb.Where(sq.Eq{"s.state": *filter.State}) - } - - countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub") - - if filter.Order != nil && *filter.Order < 0 { - qb = qb.OrderBy("s.id DESC") - } else { - qb = qb.OrderBy("s.id ASC") - } - - qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset())) - - return qb, countQb -} - -func (r *Repository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) { - const op = "ContestRepository.ListSolutions" - - baseQb, countQb := buildListSolutionsQueries(filter) - - query, args, err := countQb.ToSql() - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var totalCount int32 - err = r.db.GetContext(ctx, &totalCount, query, args...) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - query, args, err = baseQb.ToSql() - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - rows, err := r.db.QueryxContext(ctx, query, args...) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - defer rows.Close() - - solutions := make([]*models.SolutionsListItem, 0) - for rows.Next() { - var solution models.SolutionsListItem - err = rows.StructScan(&solution) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - solutions = append(solutions, &solution) - } - - if err = rows.Err(); err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &models.SolutionsList{ - Solutions: solutions, - Pagination: models.Pagination{ - Total: models.Total(totalCount, filter.PageSize), - Page: filter.Page, - }, - }, nil -} - -const ( - // state=5 - AC - GetBestSolutions = ` - WITH contest_tasks AS ( - SELECT t.id AS task_id, - t.position AS task_position, - t.contest_id, - t.problem_id, - t.created_at, - t.updated_at, - p.title AS task_title, - c.title AS contest_title - FROM tasks t - LEFT JOIN problems p ON p.id = t.problem_id - LEFT JOIN contests c ON c.id = t.contest_id - WHERE t.contest_id = ? -), - best_solutions AS ( - SELECT DISTINCT ON (s.task_id) - * - FROM solutions s - WHERE s.participant_id = ? - ORDER BY s.task_id, s.score DESC, s.created_at DESC - ) -SELECT - s.id, - s.participant_id, - p.name AS participant_name, - s.state, - s.score, - s.penalty, - s.time_stat, - s.memory_stat, - s.language, - ct.task_id, - ct.task_position, - ct.task_title, - ct.contest_id, - ct.contest_title, - s.updated_at, - s.created_at -FROM contest_tasks ct - LEFT JOIN best_solutions s ON s.task_id = ct.task_id - LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL -ORDER BY ct.task_position -` -) - -func (r *Repository) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) { - const op = "Repository.GetBestSolutions" - var solutions []*models.SolutionsListItem - query := r.db.Rebind(GetBestSolutions) - err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId) - - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return solutions, nil -} diff --git a/internal/contests/repository/solutions_pg_repository_test.go b/internal/contests/repository/solutions_pg_repository_test.go deleted file mode 100644 index 50a4378..0000000 --- a/internal/contests/repository/solutions_pg_repository_test.go +++ /dev/null @@ -1 +0,0 @@ -package repository diff --git a/internal/contests/repository/tasks_pg_repository.go b/internal/contests/repository/tasks_pg_repository.go deleted file mode 100644 index 5a8fa88..0000000 --- a/internal/contests/repository/tasks_pg_repository.go +++ /dev/null @@ -1,101 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" -) - -const CreateTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position) -VALUES ($1, $2, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = $2), 0) + 1) -RETURNING id -` - -func (r *Repository) CreateTask(ctx context.Context, contestId int32, problemId int32) (int32, error) { - const op = "Repository.AddTask" - - rows, err := r.db.QueryxContext(ctx, CreateTaskQuery, problemId, contestId) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - return id, nil -} - -const DeleteTaskQuery = "DELETE FROM tasks WHERE id=$1" - -func (r *Repository) DeleteTask(ctx context.Context, taskId int32) error { - const op = "Repository.DeleteTask" - - _, err := r.db.ExecContext(ctx, DeleteTaskQuery, taskId) - if err != nil { - return pkg.HandlePgErr(err, op) - } - return nil -} - -const GetTasksQuery = `SELECT tasks.id, - problem_id, - contest_id, - position, - title, - memory_limit, - time_limit, - tasks.created_at, - tasks.updated_at -FROM tasks - INNER JOIN problems ON tasks.problem_id = problems.id -WHERE contest_id = $1 ORDER BY position` - -func (r *Repository) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) { - const op = "Repository.ReadTasks" - - var tasks []*models.TasksListItem - err := r.db.SelectContext(ctx, &tasks, GetTasksQuery, contestId) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - return tasks, nil -} - -const ( - GetTaskQuery = ` - SELECT - t.id, - t.position, - p.title, - p.time_limit, - p.memory_limit, - t.problem_id, - t.contest_id, - p.legend_html, - p.input_format_html, - p.output_format_html, - p.notes_html, - p.scoring_html, - t.created_at, - t.updated_at - FROM tasks t - LEFT JOIN problems p ON t.problem_id = p.id - WHERE t.id = ? - ` -) - -func (r *Repository) GetTask(ctx context.Context, id int32) (*models.Task, error) { - const op = "Repository.ReadTask" - - query := r.db.Rebind(GetTaskQuery) - var task models.Task - err := r.db.GetContext(ctx, &task, query, id) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &task, nil -} diff --git a/internal/contests/repository/tasks_pg_repository_test.go b/internal/contests/repository/tasks_pg_repository_test.go deleted file mode 100644 index afc0f41..0000000 --- a/internal/contests/repository/tasks_pg_repository_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package repository_test - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/contests/repository" - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestRepository_CreateTask(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - var ( - expectedId int32 = 1 - problemId int32 = 2 - contestId int32 = 3 - ) - ctx := context.Background() - - mock.ExpectQuery(repository.CreateTaskQuery). - WithArgs(problemId, contestId). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId)) - - id, err := repo.CreateTask(ctx, contestId, problemId) - assert.NoError(t, err) - assert.Equal(t, expectedId, id) - }) -} - -func TestRepository_DeleteTask(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - mock.ExpectExec(repository.DeleteTaskQuery). - WithArgs(1). - WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.DeleteTask(ctx, 1) - assert.NoError(t, err) - }) -} diff --git a/internal/contests/usecase.go b/internal/contests/usecase.go deleted file mode 100644 index ca49791..0000000 --- a/internal/contests/usecase.go +++ /dev/null @@ -1,34 +0,0 @@ -package contests - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type UseCase interface { - CreateContest(ctx context.Context, title string) (int32, error) - GetContest(ctx context.Context, id int32) (*models.Contest, error) - DeleteContest(ctx context.Context, id int32) error - ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) - UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error - - CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error) - DeleteTask(ctx context.Context, taskId int32) error - GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) - GetTask(ctx context.Context, id int32) (*models.Task, error) - - CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) - GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) - GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) - GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) - UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error - DeleteParticipant(ctx context.Context, participantId int32) error - ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) - - GetSolution(ctx context.Context, id int32) (*models.Solution, error) - CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) - ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) - GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) - - GetMonitor(ctx context.Context, id int32) (*models.Monitor, error) -} diff --git a/internal/contests/usecase/contests_usecase.go b/internal/contests/usecase/contests_usecase.go deleted file mode 100644 index e0f9453..0000000 --- a/internal/contests/usecase/contests_usecase.go +++ /dev/null @@ -1,39 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/contests" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type ContestUseCase struct { - contestRepo contests.Repository -} - -func NewContestUseCase( - contestRepo contests.Repository, -) *ContestUseCase { - return &ContestUseCase{ - contestRepo: contestRepo, - } -} - -func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) { - return uc.contestRepo.CreateContest(ctx, title) -} - -func (uc *ContestUseCase) GetContest(ctx context.Context, id int32) (*models.Contest, error) { - return uc.contestRepo.GetContest(ctx, id) -} - -func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error { - return uc.contestRepo.UpdateContest(ctx, id, contestUpdate) -} - -func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error { - return uc.contestRepo.DeleteContest(ctx, id) -} - -func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) { - return uc.contestRepo.ListContests(ctx, filter) -} diff --git a/internal/contests/usecase/monitor_usecase.go b/internal/contests/usecase/monitor_usecase.go deleted file mode 100644 index 563f361..0000000 --- a/internal/contests/usecase/monitor_usecase.go +++ /dev/null @@ -1,10 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -func (uc *ContestUseCase) GetMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { - return uc.contestRepo.GetMonitor(ctx, contestId, 20) -} diff --git a/internal/contests/usecase/participants_usecase.go b/internal/contests/usecase/participants_usecase.go deleted file mode 100644 index 30ca4e2..0000000 --- a/internal/contests/usecase/participants_usecase.go +++ /dev/null @@ -1,34 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -func (uc *ContestUseCase) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) { - return uc.contestRepo.GetParticipantId(ctx, contestId, userId) -} - -func (uc *ContestUseCase) GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) { - return uc.contestRepo.GetParticipantId2(ctx, taskId, userId) -} - -func (uc *ContestUseCase) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) { - return uc.contestRepo.GetParticipantId3(ctx, solutionId) -} - -func (uc *ContestUseCase) CreateParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) { - return uc.contestRepo.CreateParticipant(ctx, contestId, userId) -} - -func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error { - return uc.contestRepo.DeleteParticipant(ctx, participantId) -} - -func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) { - return uc.contestRepo.ListParticipants(ctx, filter) -} - -func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error { - return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate) -} diff --git a/internal/contests/usecase/solutions_usecase.go b/internal/contests/usecase/solutions_usecase.go deleted file mode 100644 index 8c79dcc..0000000 --- a/internal/contests/usecase/solutions_usecase.go +++ /dev/null @@ -1,29 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -func (uc *ContestUseCase) GetSolution(ctx context.Context, id int32) (*models.Solution, error) { - return uc.contestRepo.GetSolution(ctx, id) -} - -func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { - participantId, err := uc.contestRepo.GetParticipantId2(ctx, creation.TaskId, creation.UserId) - if err != nil { - return 0, err - } - - creation.ParticipantId = participantId - - return uc.contestRepo.CreateSolution(ctx, creation) -} - -func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) { - return uc.contestRepo.ListSolutions(ctx, filter) -} - -func (uc *ContestUseCase) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) { - return uc.contestRepo.GetBestSolutions(ctx, contestId, participantId) -} diff --git a/internal/contests/usecase/tasks_usecase.go b/internal/contests/usecase/tasks_usecase.go deleted file mode 100644 index 5d2d5a0..0000000 --- a/internal/contests/usecase/tasks_usecase.go +++ /dev/null @@ -1,22 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -func (uc *ContestUseCase) CreateTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) { - return uc.contestRepo.CreateTask(ctx, contestId, taskId) -} - -func (uc *ContestUseCase) GetTask(ctx context.Context, id int32) (*models.Task, error) { - return uc.contestRepo.GetTask(ctx, id) -} - -func (uc *ContestUseCase) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) { - return uc.contestRepo.GetTasks(ctx, contestId) -} - -func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error { - return uc.contestRepo.DeleteTask(ctx, taskId) -} diff --git a/internal/models/contest.go b/internal/models/contest.go index 4fd94d7..3646d1b 100644 --- a/internal/models/contest.go +++ b/internal/models/contest.go @@ -24,8 +24,6 @@ type ContestsList struct { type ContestsFilter struct { Page int32 PageSize int32 - UserId *int32 - Order *int32 } func (f ContestsFilter) Offset() int32 { @@ -55,73 +53,3 @@ type ProblemStatSummary struct { Success int32 `db:"success"` Total int32 `db:"total"` } - -type Task struct { - Id int32 `db:"id"` - Position int32 `db:"position"` - Title string `db:"title"` - TimeLimit int32 `db:"time_limit"` - MemoryLimit int32 `db:"memory_limit"` - - ProblemId int32 `db:"problem_id"` - ContestId int32 `db:"contest_id"` - - LegendHtml string `db:"legend_html"` - InputFormatHtml string `db:"input_format_html"` - OutputFormatHtml string `db:"output_format_html"` - NotesHtml string `db:"notes_html"` - ScoringHtml string `db:"scoring_html"` - - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -type TasksListItem struct { - Id int32 `db:"id"` - ProblemId int32 `db:"problem_id"` - ContestId int32 `db:"contest_id"` - Position int32 `db:"position"` - Title string `db:"title"` - MemoryLimit int32 `db:"memory_limit"` - TimeLimit int32 `db:"time_limit"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -type Participant struct { - Id int32 `db:"id"` - UserId int32 `db:"user_id"` - ContestId int32 `db:"contest_id"` - Name string `db:"name"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -type ParticipantsListItem struct { - Id int32 `db:"id"` - UserId int32 `db:"user_id"` - ContestId int32 `db:"contest_id"` - Name string `db:"name"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -type ParticipantsList struct { - Participants []*ParticipantsListItem - Pagination Pagination -} - -type ParticipantsFilter struct { - Page int32 - PageSize int32 - - ContestId int32 -} - -func (f ParticipantsFilter) Offset() int32 { - return (f.Page - 1) * f.PageSize -} - -type ParticipantUpdate struct { - Name *string `json:"name"` -} diff --git a/internal/models/participant.go b/internal/models/participant.go new file mode 100644 index 0000000..f277a3d --- /dev/null +++ b/internal/models/participant.go @@ -0,0 +1,41 @@ +package models + +import "time" + +type Participant struct { + Id int32 `db:"id"` + UserId int32 `db:"user_id"` + ContestId int32 `db:"contest_id"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type ParticipantsListItem struct { + Id int32 `db:"id"` + UserId int32 `db:"user_id"` + ContestId int32 `db:"contest_id"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type ParticipantsList struct { + Participants []*ParticipantsListItem + Pagination Pagination +} + +type ParticipantsFilter struct { + Page int32 + PageSize int32 + + ContestId int32 +} + +func (f ParticipantsFilter) Offset() int32 { + return (f.Page - 1) * f.PageSize +} + +type ParticipantUpdate struct { + Name *string `json:"name"` +} diff --git a/internal/models/problem.go b/internal/models/problem.go index fc3563d..aad030d 100644 --- a/internal/models/problem.go +++ b/internal/models/problem.go @@ -13,6 +13,7 @@ type Problem struct { OutputFormat string `db:"output_format"` Notes string `db:"notes"` Scoring string `db:"scoring"` + LatexSummary string `db:"latex_summary"` LegendHtml string `db:"legend_html"` InputFormatHtml string `db:"input_format_html"` diff --git a/internal/models/session.go b/internal/models/session.go index 1d249d4..446475a 100644 --- a/internal/models/session.go +++ b/internal/models/session.go @@ -1,74 +1,20 @@ package models import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" + "context" "errors" - "fmt" "github.com/google/uuid" - "strconv" - "time" + "github.com/open-policy-agent/opa/v1/rego" ) -type Session struct { - Id string `json:"id"` - UserId int32 `json:"user_id"` - Role Role `json:"role"` - CreatedAt time.Time `json:"created_at"` - ExpiresAt time.Time `json:"expires_at"` - UserAgent string `json:"user_agent"` - Ip string `json:"ip"` -} - -func (s *Session) Valid() error { - if uuid.Validate(s.Id) != nil { - return errors.New("invalid session id") - } - if s.UserId == 0 { - return errors.New("empty user id") - } - if s.CreatedAt.IsZero() { - return errors.New("empty created at") - } - if s.ExpiresAt.IsZero() { - return errors.New("empty expires at") - } - if s.UserAgent == "" { - return errors.New("empty user agent") - } - if s.Ip == "" { - return errors.New("empty ip") - } - return nil -} - -func (s *Session) JSON() ([]byte, error) { - return json.Marshal(s) -} - -func (s *Session) UserIdHash() string { - return sha256string(strconv.FormatInt(int64(s.UserId), 10)) -} - -func (s *Session) SessionIdHash() string { - return sha256string(s.Id) -} - -func (s *Session) Key() string { - return fmt.Sprintf("userid:%s:sessionid:%s", s.UserIdHash(), s.SessionIdHash()) -} -func sha256string(s string) string { - hasher := sha256.New() - hasher.Write([]byte(s)) - return hex.EncodeToString(hasher.Sum(nil)) -} - type JWT struct { - SessionId string `json:"session_id"` - UserId int32 `json:"user_id"` - Role Role `json:"role"` - IssuedAt int64 `json:"iat"` + SessionId string `json:"session_id"` + UserId int32 `json:"user_id"` + Role Role `json:"role"` + ExpiresAt int64 `json:"exp"` + IssuedAt int64 `json:"iat"` + NotBefore int64 `json:"nbf"` + Permissions []grant `json:"permissions"` } func (j JWT) Valid() error { @@ -78,18 +24,139 @@ func (j JWT) Valid() error { if j.UserId == 0 { return errors.New("empty user id") } + if j.ExpiresAt == 0 { + return errors.New("empty expires at") + } if j.IssuedAt == 0 { return errors.New("empty issued at") } + if j.NotBefore == 0 { + return errors.New("empty not before") + } + if len(j.Permissions) == 0 { + return errors.New("empty permissions") + } return nil } -type Credentials struct { - Username string - Password string +type Role int32 + +const ( + RoleGuest Role = -1 + RoleStudent Role = 0 + RoleTeacher Role = 1 + RoleAdmin Role = 2 +) + +func (r Role) String() string { + switch r { + case RoleGuest: + return "guest" + case RoleStudent: + return "student" + case RoleTeacher: + return "teacher" + case RoleAdmin: + return "admin" + } + + panic("invalid role") } -type Device struct { - Ip string - UseAgent string +type Action string + +const ( + Create Action = "create" + Read Action = "read" + Update Action = "update" + Delete Action = "delete" +) + +type Resource string + +const ( + ResourceAnotherUser Resource = "another-user" + ResourceMeUser Resource = "me-user" + ResourceListUser Resource = "list-user" + + ResourceOwnSession Resource = "own-session" +) + +type grant struct { + Action Action `json:"action"` + Resource Resource `json:"resource"` +} + +var Grants = map[string][]grant{ + RoleGuest.String(): {}, + RoleStudent.String(): { + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceOwnSession}, + }, + RoleTeacher.String(): { + {Create, ResourceAnotherUser}, + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Read, ResourceListUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceOwnSession}, + }, + RoleAdmin.String(): { + {Create, ResourceAnotherUser}, + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Read, ResourceListUser}, + {Update, ResourceAnotherUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceAnotherUser}, + {Delete, ResourceOwnSession}, + }, +} + +const module = `package app.rbac + +default allow := false + +allow if { + some grant in input.role_grants[input.role] + + input.action == grant.action + input.resource == grant.resource +} +` + +var query rego.PreparedEvalQuery + +func (r Role) HasPermission(action Action, resource Resource) bool { + ctx := context.TODO() + + input := map[string]interface{}{ + "action": action, + "resource": resource, + "role": r.String(), + "role_grants": Grants, + } + + results, err := query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + panic(err) + } + + return results.Allowed() +} + +func init() { + var err error + ctx := context.TODO() + + query, err = rego.New( + rego.Query("data.app.rbac.allow"), + rego.Module("ms-auth.rego", module), + ).PrepareForEval(ctx) + + if err != nil { + panic(err) + } } diff --git a/internal/models/solution.go b/internal/models/solution.go index da7e8a6..869bed5 100644 --- a/internal/models/solution.go +++ b/internal/models/solution.go @@ -31,7 +31,6 @@ type Solution struct { type SolutionCreation struct { Solution string TaskId int32 - UserId int32 ParticipantId int32 Language int32 Penalty int32 diff --git a/internal/models/task.go b/internal/models/task.go new file mode 100644 index 0000000..04e2c55 --- /dev/null +++ b/internal/models/task.go @@ -0,0 +1,35 @@ +package models + +import "time" + +type Task struct { + Id int32 `db:"id"` + Position int32 `db:"position"` + Title string `db:"title"` + TimeLimit int32 `db:"time_limit"` + MemoryLimit int32 `db:"memory_limit"` + + ProblemId int32 `db:"problem_id"` + ContestId int32 `db:"contest_id"` + + LegendHtml string `db:"legend_html"` + InputFormatHtml string `db:"input_format_html"` + OutputFormatHtml string `db:"output_format_html"` + NotesHtml string `db:"notes_html"` + ScoringHtml string `db:"scoring_html"` + + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type TasksListItem struct { + Id int32 `db:"id"` + ProblemId int32 `db:"problem_id"` + ContestId int32 `db:"contest_id"` + Position int32 `db:"position"` + Title string `db:"title"` + MemoryLimit int32 `db:"memory_limit"` + TimeLimit int32 `db:"time_limit"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/models/user.go b/internal/models/user.go deleted file mode 100644 index 3f4c379..0000000 --- a/internal/models/user.go +++ /dev/null @@ -1,71 +0,0 @@ -package models - -import ( - "golang.org/x/crypto/bcrypt" - "time" -) - -type Role int32 - -type User struct { - Id int32 `db:"id"` - Username string `db:"username"` - HashedPassword string `db:"hashed_pwd"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - Role Role `db:"role"` -} - -type UserCreation struct { - Username string - Password string - Role Role -} - -func (u *UserCreation) HashPassword() error { - hpwd, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) - if err != nil { - return err - } - u.Password = string(hpwd) - return nil -} - -func (user *User) IsSamePwd(password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password)) - if err != nil { - return false - } - return true -} - -type UsersListFilters struct { - PageSize int32 - Page int32 -} - -func (f UsersListFilters) Offset() int32 { - return (f.Page - 1) * f.PageSize -} - -type UsersList struct { - Users []*User - Pagination Pagination -} - -type UserUpdate struct { - Username *string - Role *Role -} - -const ( - RoleGuest Role = -1 - RoleStudent Role = 0 - RoleTeacher Role = 1 - RoleAdmin Role = 2 -) - -type Grant struct { - Action string - Resource string -} diff --git a/internal/problems/delivery.go b/internal/problems/delivery.go deleted file mode 100644 index 7f371e4..0000000 --- a/internal/problems/delivery.go +++ /dev/null @@ -1,15 +0,0 @@ -package problems - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "github.com/gofiber/fiber/v2" -) - -type ProblemsHandlers interface { - ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error - CreateProblem(c *fiber.Ctx) error - DeleteProblem(c *fiber.Ctx, id int32) error - GetProblem(c *fiber.Ctx, id int32) error - UpdateProblem(c *fiber.Ctx, id int32) error - UploadProblem(c *fiber.Ctx, id int32) error -} diff --git a/internal/problems/delivery/rest/handlers.go b/internal/problems/delivery/rest/handlers.go deleted file mode 100644 index 337aebc..0000000 --- a/internal/problems/delivery/rest/handlers.go +++ /dev/null @@ -1,261 +0,0 @@ -package rest - -import ( - "context" - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/problems" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" - "io" -) - -type Handlers struct { - problemsUC problems.UseCase - - jwtSecret string -} - -const ( - sessionKey = "session" -) - -func sessionFromCtx(ctx context.Context) (*models.Session, error) { - const op = "sessionFromCtx" - - session, ok := ctx.Value(sessionKey).(*models.Session) - if !ok { - return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "") - } - - return session, nil -} - -func NewHandlers(problemsUC problems.UseCase) *Handlers { - return &Handlers{ - problemsUC: problemsUC, - } -} - -func (h *Handlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{ - Page: params.Page, - PageSize: params.PageSize, - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - resp := testerv1.ListProblemsResponse{ - Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)), - Pagination: PaginationDTO(problemsList.Pagination), - } - - for i, problem := range problemsList.Problems { - resp.Problems[i] = ProblemsListItemDTO(*problem) - } - return c.JSON(resp) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) CreateProblem(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи") - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.CreateProblemResponse{ - Id: id, - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) DeleteProblem(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - err := h.problemsUC.DeleteProblem(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) GetProblem(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - problem, err := h.problemsUC.GetProblemById(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON( - testerv1.GetProblemResponse{Problem: *ProblemDTO(problem)}, - ) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) UpdateProblem(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - var req testerv1.UpdateProblemRequest - err := c.BodyParser(&req) - if err != nil { - return err - } - - err = h.problemsUC.UpdateProblem(c.Context(), id, &models.ProblemUpdate{ - Title: req.Title, - MemoryLimit: req.MemoryLimit, - TimeLimit: req.TimeLimit, - - Legend: req.Legend, - InputFormat: req.InputFormat, - OutputFormat: req.OutputFormat, - Notes: req.Notes, - Scoring: req.Scoring, - }) - - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) UploadProblem(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - //session, err := sessionFromCtx(ctx) - //if err != nil { - // return c.SendStatus(pkg.ToREST(err)) - //} - - session := models.Session{ - Role: models.RoleAdmin, - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - a, err := c.FormFile("archive") - if err != nil { - return err - } - - if a.Size == 0 { // FIXME: check max size - return c.SendStatus(fiber.StatusBadRequest) - } - - f, err := a.Open() - if err != nil { - return err - } - defer f.Close() - - data, err := io.ReadAll(f) - if err != nil { - return err - } - - if err = h.problemsUC.UploadProblem(ctx, id, data); err != nil { - return err - } - return nil - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func PaginationDTO(p models.Pagination) testerv1.Pagination { - return testerv1.Pagination{ - Page: p.Page, - Total: p.Total, - } -} - -func ProblemsListItemDTO(p models.ProblemsListItem) testerv1.ProblemsListItem { - return testerv1.ProblemsListItem{ - Id: p.Id, - Title: p.Title, - MemoryLimit: p.MemoryLimit, - TimeLimit: p.TimeLimit, - CreatedAt: p.CreatedAt, - UpdatedAt: p.UpdatedAt, - SolvedCount: p.SolvedCount, - } -} - -func ProblemDTO(p *models.Problem) *testerv1.Problem { - return &testerv1.Problem{ - Id: p.Id, - Title: p.Title, - TimeLimit: p.TimeLimit, - MemoryLimit: p.MemoryLimit, - - Legend: p.Legend, - InputFormat: p.InputFormat, - OutputFormat: p.OutputFormat, - Notes: p.Notes, - Scoring: p.Scoring, - - LegendHtml: p.LegendHtml, - InputFormatHtml: p.InputFormatHtml, - OutputFormatHtml: p.OutputFormatHtml, - NotesHtml: p.NotesHtml, - ScoringHtml: p.ScoringHtml, - - CreatedAt: p.CreatedAt, - UpdatedAt: p.UpdatedAt, - } -} diff --git a/internal/problems/pg_repository.go b/internal/problems/pg_repository.go deleted file mode 100644 index 678274a..0000000 --- a/internal/problems/pg_repository.go +++ /dev/null @@ -1,32 +0,0 @@ -package problems - -import ( - "context" - "database/sql" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "github.com/jmoiron/sqlx" -) - -type Querier interface { - Rebind(query string) string - QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) - GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error -} - -type Tx interface { - Querier - Commit() error - Rollback() error -} - -type Repository interface { - BeginTx(ctx context.Context) (Tx, error) - DB() Querier - CreateProblem(ctx context.Context, q Querier, title string) (int32, error) - GetProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error) - DeleteProblem(ctx context.Context, q Querier, id int32) error - ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) - UpdateProblem(ctx context.Context, q Querier, id int32, heading *models.ProblemUpdate) error -} diff --git a/internal/problems/repository/pg_repository.go b/internal/problems/repository/pg_repository.go deleted file mode 100644 index 5de265f..0000000 --- a/internal/problems/repository/pg_repository.go +++ /dev/null @@ -1,175 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/problems" - "git.sch9.ru/new_gate/ms-tester/pkg" - - "git.sch9.ru/new_gate/ms-tester/internal/models" - "github.com/jmoiron/sqlx" -) - -type Repository struct { - _db *sqlx.DB -} - -func NewRepository(db *sqlx.DB) *Repository { - return &Repository{ - _db: db, - } -} - -func (r *Repository) BeginTx(ctx context.Context) (problems.Tx, error) { - tx, err := r._db.BeginTxx(ctx, nil) - if err != nil { - return nil, err - } - - return tx, nil -} - -func (r *Repository) DB() problems.Querier { - return r._db -} - -const CreateProblemQuery = "INSERT INTO problems (title) VALUES ($1) RETURNING id" - -func (r *Repository) CreateProblem(ctx context.Context, q problems.Querier, title string) (int32, error) { - const op = "Repository.CreateProblem" - - rows, err := q.QueryxContext(ctx, CreateProblemQuery, title) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return id, nil -} - -const GetProblemByIdQuery = "SELECT * from problems WHERE id=$1 LIMIT 1" - -func (r *Repository) GetProblemById(ctx context.Context, q problems.Querier, id int32) (*models.Problem, error) { - const op = "Repository.ReadProblemById" - - var problem models.Problem - err := q.GetContext(ctx, &problem, GetProblemByIdQuery, id) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &problem, nil -} - -const DeleteProblemQuery = "DELETE FROM problems WHERE id=$1" - -func (r *Repository) DeleteProblem(ctx context.Context, q problems.Querier, id int32) error { - const op = "Repository.DeleteProblem" - - _, err := q.ExecContext(ctx, DeleteProblemQuery, id) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} - -const ( - ListProblemsQuery = `SELECT p.id, - p.title, - p.memory_limit, - p.time_limit, - p.created_at, - p.updated_at, - COALESCE(solved_count, 0) AS solved_count -FROM problems p - LEFT JOIN (SELECT t.problem_id, - COUNT(DISTINCT s.participant_id) AS solved_count - FROM solutions s - JOIN tasks t ON s.task_id = t.id - WHERE s.state = 5 - GROUP BY t.problem_id) sol ON p.id = sol.problem_id -LIMIT $1 OFFSET $2` - CountProblemsQuery = "SELECT COUNT(*) FROM problems" -) - -func (r *Repository) ListProblems(ctx context.Context, q problems.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) { - const op = "ContestRepository.ListProblems" - - var list []*models.ProblemsListItem - err := q.SelectContext(ctx, &list, ListProblemsQuery, filter.PageSize, filter.Offset()) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var count int32 - err = q.GetContext(ctx, &count, CountProblemsQuery) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &models.ProblemsList{ - Problems: list, - Pagination: models.Pagination{ - Total: models.Total(count, filter.PageSize), - Page: filter.Page, - }, - }, nil -} - -const ( - UpdateProblemQuery = `UPDATE problems -SET title = COALESCE($2, title), - time_limit = COALESCE($3, time_limit), - memory_limit = COALESCE($4, memory_limit), - - legend = COALESCE($5, legend), - input_format = COALESCE($6, input_format), - output_format = COALESCE($7, output_format), - notes = COALESCE($8, notes), - scoring = COALESCE($9, scoring), - - legend_html = COALESCE($10, legend_html), - input_format_html = COALESCE($11, input_format_html), - output_format_html = COALESCE($12, output_format_html), - notes_html = COALESCE($13, notes_html), - scoring_html = COALESCE($14, scoring_html) - -WHERE id=$1` -) - -func (r *Repository) UpdateProblem(ctx context.Context, q problems.Querier, id int32, problem *models.ProblemUpdate) error { - const op = "Repository.UpdateProblem" - - query := q.Rebind(UpdateProblemQuery) - _, err := q.ExecContext(ctx, query, - id, - - problem.Title, - problem.TimeLimit, - problem.MemoryLimit, - - problem.Legend, - problem.InputFormat, - problem.OutputFormat, - problem.Notes, - problem.Scoring, - - problem.LegendHtml, - problem.InputFormatHtml, - problem.OutputFormatHtml, - problem.NotesHtml, - problem.ScoringHtml, - ) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} diff --git a/internal/problems/repository/pg_repository_test.go b/internal/problems/repository/pg_repository_test.go deleted file mode 100644 index f2bc2f2..0000000 --- a/internal/problems/repository/pg_repository_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package repository_test - -import ( - "context" - "database/sql" - "fmt" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/problems/repository" - "github.com/DATA-DOG/go-sqlmock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing. -func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) { - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - assert.NoError(t, err) - sqlxDB := sqlx.NewDb(db, "sqlmock") - return sqlxDB, mock -} - -func TestRepository_CreateProblem(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - problem := models.Problem{ - Id: 1, - Title: "Test Problem", - } - - mock.ExpectQuery(repository.CreateProblemQuery). - WithArgs(problem.Title). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(problem.Id)) - - id, err := repo.CreateProblem(ctx, db, problem.Title) - assert.NoError(t, err) - assert.Equal(t, problem.Id, id) - }) -} - -func TestRepository_GetProblemById(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - expected := &models.Problem{ - Id: 1, - Title: "Test Problem", - TimeLimit: 1000, - MemoryLimit: 1024, - Legend: "Test Legend", - InputFormat: "Test Input Format", - OutputFormat: "Test Output Format", - Notes: "Test Notes", - Scoring: "Test Scoring", - LegendHtml: "Test Legend HTML", - InputFormatHtml: "Test Input Format HTML", - OutputFormatHtml: "Test Output Format HTML", - NotesHtml: "Test Notes HTML", - ScoringHtml: "Test Scoring HTML", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - columns := []string{ - "id", - "title", - "time_limit", - "memory_limit", - - "legend", - "input_format", - "output_format", - "notes", - "scoring", - - "legend_html", - "input_format_html", - "output_format_html", - "notes_html", - "scoring_html", - - "created_at", - "updated_at", - } - - rows := sqlmock.NewRows(columns). - AddRow( - expected.Id, - expected.Title, - expected.TimeLimit, - expected.MemoryLimit, - - expected.Legend, - expected.InputFormat, - expected.OutputFormat, - expected.Notes, - expected.Scoring, - - expected.LegendHtml, - expected.InputFormatHtml, - expected.OutputFormatHtml, - expected.NotesHtml, - expected.ScoringHtml, - - expected.CreatedAt, - expected.UpdatedAt) - - mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(expected.Id).WillReturnRows(rows) - - problem, err := repo.GetProblemById(ctx, db, expected.Id) - assert.NoError(t, err) - assert.EqualExportedValues(t, expected, problem) - }) - - t.Run("not found", func(t *testing.T) { - ctx := context.Background() - - id := int32(1) - - mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(id).WillReturnError(sql.ErrNoRows) - - _, err := repo.GetProblemById(ctx, db, id) - assert.Error(t, err) - }) -} - -func TestRepository_DeleteProblem(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - id := int32(1) - - mock.ExpectExec(repository.DeleteProblemQuery). - WithArgs(id).WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.DeleteProblem(ctx, db, id) - assert.NoError(t, err) - }) - - t.Run("not found", func(t *testing.T) { - ctx := context.Background() - id := int32(1) - - mock.ExpectExec(repository.DeleteProblemQuery).WithArgs(id).WillReturnError(sql.ErrNoRows) - - err := repo.DeleteProblem(ctx, db, id) - assert.Error(t, err) - }) -} - -func TestRepository_ListProblems(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - expected := make([]*models.ProblemsListItem, 0) - for i := 0; i < 10; i++ { - problem := &models.ProblemsListItem{ - Id: int32(i + 1), - Title: fmt.Sprintf("Test Problem %d", i+1), - TimeLimit: 1000, - MemoryLimit: 1024, - SolvedCount: int32(123 * i), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - expected = append(expected, problem) - } - - filter := models.ProblemsFilter{ - Page: 1, - PageSize: 10, - } - - var totalCount int32 = 10 - - columns := []string{ - "id", - "title", - "time_limit", - "memory_limit", - "solved_count", - "created_at", - "updated_at", - } - - rows := sqlmock.NewRows(columns) - for _, problem := range expected { - rows = rows.AddRow( - problem.Id, - problem.Title, - problem.TimeLimit, - problem.MemoryLimit, - problem.SolvedCount, - problem.CreatedAt, - problem.UpdatedAt, - ) - } - - mock.ExpectQuery(repository.ListProblemsQuery).WillReturnRows(rows) - mock.ExpectQuery(repository.CountProblemsQuery). - WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount)) - - problems, err := repo.ListProblems(ctx, db, filter) - assert.NoError(t, err) - assert.Equal(t, expected, problems.Problems) - assert.Equal(t, models.Pagination{ - Page: 1, - Total: 1, - }, problems.Pagination) - }) -} - -func TestRepository_UpdateProblem(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - var id int32 = 1 - - update := &models.ProblemUpdate{ - Title: sp("Test Problem"), - TimeLimit: ip(1000), - MemoryLimit: ip(1024), - Legend: sp("Test Legend"), - InputFormat: sp("Test Input Format"), - OutputFormat: sp("Test Output Format"), - Notes: sp("Test Notes"), - Scoring: sp("Test Scoring"), - LegendHtml: sp("Test Legend HTML"), - InputFormatHtml: sp("Test Input Format HTML"), - OutputFormatHtml: sp("Test Output Format HTML"), - NotesHtml: sp("Test Notes HTML"), - ScoringHtml: sp("Test Scoring HTML"), - } - - mock.ExpectExec(repository.UpdateProblemQuery).WithArgs( - id, - - update.Title, - update.TimeLimit, - update.MemoryLimit, - - update.Legend, - update.InputFormat, - update.OutputFormat, - update.Notes, - update.Scoring, - - update.LegendHtml, - update.InputFormatHtml, - update.OutputFormatHtml, - update.NotesHtml, - update.ScoringHtml, - ).WillReturnResult(sqlmock.NewResult(1, 1)) - - err := repo.UpdateProblem(ctx, db, id, update) - assert.NoError(t, err) - }) -} - -func sp(s string) *string { - return &s -} - -func ip(s int32) *int32 { - return &s -} diff --git a/internal/problems/usecase.go b/internal/problems/usecase.go deleted file mode 100644 index bbe1c71..0000000 --- a/internal/problems/usecase.go +++ /dev/null @@ -1,15 +0,0 @@ -package problems - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type UseCase interface { - CreateProblem(ctx context.Context, title string) (int32, error) - GetProblemById(ctx context.Context, id int32) (*models.Problem, error) - DeleteProblem(ctx context.Context, id int32) error - ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) - UpdateProblem(ctx context.Context, id int32, problem *models.ProblemUpdate) error - UploadProblem(ctx context.Context, id int32, archive []byte) error -} diff --git a/internal/sessions/repository/valkey_repository.go b/internal/sessions/repository/valkey_repository.go deleted file mode 100644 index c26372d..0000000 --- a/internal/sessions/repository/valkey_repository.go +++ /dev/null @@ -1,183 +0,0 @@ -package repository - -import ( - "context" - "fmt" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/valkey-io/valkey-go" - "strconv" - "time" -) - -type ValkeyRepository struct { - db valkey.Client -} - -func NewValkeyRepository(db valkey.Client) *ValkeyRepository { - return &ValkeyRepository{ - db: db, - } -} - -const SessionLifetime = time.Minute * 40 - -func (r *ValkeyRepository) CreateSession(ctx context.Context, session *models.Session) error { - const op = "ValkeyRepository.CreateSession" - - data, err := session.JSON() - if err != nil { - return pkg.Wrap(pkg.ErrInternal, err, op, "cannot marshal session") - } - - resp := r.db.Do(ctx, r.db. - B().Set(). - Key(session.Key()). - Value(string(data)). - Exat(session.ExpiresAt). - Build(), - ) - - err = resp.Error() - if err != nil { - if valkey.IsValkeyNil(err) { - return pkg.Wrap(pkg.ErrInternal, err, op, "nil response") - } - return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") - } - - return nil -} - -const ( - readSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1]) -if #result[2] == 0 then - return nil -else - return redis.call('GET', result[2][1]) -end` -) - -func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) { - const op = "ValkeyRepository.ReadSession" - - sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash() - - resp := valkey.NewLuaScript(readSessionScript).Exec( - ctx, - r.db, - nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)}, - ) - - if err := resp.Error(); err != nil { - if valkey.IsValkeyNil(err) { - return nil, pkg.Wrap(pkg.ErrNotFound, err, op, "reading session") - } - return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") - } - - session := &models.Session{} - - err := resp.DecodeJSON(session) - if err != nil { - return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted") - } - - return session, nil -} - -const ( - updateSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1]) -return #result[2] > 0 and redis.call('EXPIRE', result[2][1], ARGV[2]) == 1` -) - -var ( - sessionLifetimeString = strconv.Itoa(int(SessionLifetime.Seconds())) -) - -func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) error { - const op = "ValkeyRepository.UpdateSession" - - sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash() - - resp := valkey.NewLuaScript(updateSessionScript).Exec( - ctx, - r.db, - nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString}, - ) - - err := resp.Error() - if err != nil { - if valkey.IsValkeyNil(err) { - return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response") - } - return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") - } - - return nil -} - -const deleteSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1]) -return #result[2] > 0 and redis.call('DEL', result[2][1]) == 1` - -func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) error { - const op = "ValkeyRepository.DeleteSession" - - sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash() - - resp := valkey.NewLuaScript(deleteSessionScript).Exec( - ctx, - r.db, - nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)}, - ) - - err := resp.Error() - if err != nil { - if valkey.IsValkeyNil(err) { - return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response") - } - return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") - } - - return nil -} - -const ( - deleteUserSessionsScript = `local cursor = 0 -local dels = 0 -repeat - local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1]) - for _,key in ipairs(result[2]) do - redis.call('DEL', key) - dels = dels + 1 - end - cursor = tonumber(result[1]) -until cursor == 0 -return dels` -) - -func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error { - const op = "ValkeyRepository.DeleteAllSessions" - - userIdHash := (&models.Session{UserId: userId}).UserIdHash() - - resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec( - ctx, - r.db, - nil, - []string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)}, - ) - - err := resp.Error() - if err != nil { - if valkey.IsValkeyNil(err) { - return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response") - } - return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") - } - - return nil -} diff --git a/internal/sessions/repository/valkey_repository_test.go b/internal/sessions/repository/valkey_repository_test.go deleted file mode 100644 index 9acdacf..0000000 --- a/internal/sessions/repository/valkey_repository_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package repository_test - -import ( - "context" - "fmt" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/sessions/repository" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "github.com/valkey-io/valkey-go" - "github.com/valkey-io/valkey-go/mock" - "go.uber.org/mock/gomock" - "strconv" - "testing" - "time" -) - -func TestValkeyRepository_CreateSession(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock.NewClient(ctrl) - sessionRepo := repository.NewValkeyRepository(client) - - t.Run("success", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - UserId: 1, - Role: models.RoleAdmin, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(repository.SessionLifetime), - UserAgent: "Mozilla/5.0", - Ip: "127.0.0.1", - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "SET" { - return false - } - if cmd[1] != session.Key() { - return false - } - if cmd[3] != "EXAT" { - return false - } - if cmd[4] != strconv.FormatInt(session.ExpiresAt.Unix(), 10) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher) - err := sessionRepo.CreateSession(ctx, session) - require.NoError(t, err) - }) -} - -func TestValkeyRepository_ReadSession(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock.NewClient(ctrl) - sessionRepo := repository.NewValkeyRepository(client) - - t.Run("success", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - UserId: 1, - Role: models.RoleAdmin, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(repository.SessionLifetime), - UserAgent: "Mozilla/5.0", - Ip: "127.0.0.1", - } - - matcher := mock.MatchFn(func(cmd []string) bool { - fmt.Println(cmd) - - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - d, err := session.JSON() - require.NoError(t, err) - ctx := context.Background() - client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(string(d)))) - res, err := sessionRepo.ReadSession(ctx, session.Id) - require.NoError(t, err) - fmt.Println(res.CreatedAt.Unix(), res.ExpiresAt.UnixNano()) - fmt.Println(session.CreatedAt.Unix(), session.ExpiresAt.UnixNano()) - require.EqualExportedValues(t, session, res) - }) - - t.Run("not found", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil)) - res, err := sessionRepo.ReadSession(ctx, session.Id) - require.ErrorIs(t, err, pkg.ErrNotFound) - require.ErrorIs(t, err, valkey.Nil) - require.Empty(t, res) - }) -} - -func TestValkeyRepository_UpdateSession(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock.NewClient(ctrl) - sessionRepo := repository.NewValkeyRepository(client) - - t.Run("success", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher) - err := sessionRepo.UpdateSession(ctx, session.Id) - require.NoError(t, err) - }) - - t.Run("not found", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil)) - err := sessionRepo.UpdateSession(ctx, session.Id) - require.ErrorIs(t, err, pkg.ErrNotFound) - require.ErrorIs(t, err, valkey.Nil) - }) -} - -func TestValkeyRepository_DeleteSession(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock.NewClient(ctrl) - sessionRepo := repository.NewValkeyRepository(client) - - t.Run("success", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher) - err := sessionRepo.DeleteSession(ctx, session.Id) - require.NoError(t, err) - }) - - t.Run("not found", func(t *testing.T) { - session := &models.Session{ - Id: uuid.NewString(), - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil)) - err := sessionRepo.DeleteSession(ctx, session.Id) - require.ErrorIs(t, err, pkg.ErrNotFound) - require.ErrorIs(t, err, valkey.Nil) - }) -} - -func TestValkeyRepository_DeleteAllSessions(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock.NewClient(ctrl) - sessionRepo := repository.NewValkeyRepository(client) - - t.Run("success", func(t *testing.T) { - session := &models.Session{ - UserId: 1, - } - - matcher := mock.MatchFn(func(cmd []string) bool { - fmt.Println(cmd) - - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher) - err := sessionRepo.DeleteAllSessions(ctx, session.UserId) - require.NoError(t, err) - }) - - t.Run("not found", func(t *testing.T) { - session := &models.Session{ - UserId: 1, - } - - matcher := mock.MatchFn(func(cmd []string) bool { - if cmd[0] != "EVALSHA" { - return false - } - if cmd[2] != "0" { - return false - } - if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) { - return false - } - return true - }) - - ctx := context.Background() - client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil)) - err := sessionRepo.DeleteAllSessions(ctx, session.UserId) - require.ErrorIs(t, err, pkg.ErrNotFound) - require.ErrorIs(t, err, valkey.Nil) - }) -} diff --git a/internal/sessions/usecase.go b/internal/sessions/usecase.go deleted file mode 100644 index d565d7a..0000000 --- a/internal/sessions/usecase.go +++ /dev/null @@ -1,14 +0,0 @@ -package sessions - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type UseCase interface { - CreateSession(ctx context.Context, creation *models.Session) error - ReadSession(ctx context.Context, sessionId string) (*models.Session, error) - UpdateSession(ctx context.Context, sessionId string) error - DeleteSession(ctx context.Context, sessionId string) error - DeleteAllSessions(ctx context.Context, userId int32) error -} diff --git a/internal/sessions/usecase/usecase.go b/internal/sessions/usecase/usecase.go deleted file mode 100644 index 06ae511..0000000 --- a/internal/sessions/usecase/usecase.go +++ /dev/null @@ -1,78 +0,0 @@ -package usecase - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/config" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/sessions" - "git.sch9.ru/new_gate/ms-tester/pkg" -) - -type SessionsUC struct { - sessionsRepo sessions.ValkeyRepository - cfg config.Config -} - -func NewUseCase( - sessionRepo sessions.ValkeyRepository, - cfg config.Config, -) *SessionsUC { - return &SessionsUC{ - sessionsRepo: sessionRepo, - cfg: cfg, - } -} - -// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! -func (u *SessionsUC) CreateSession(ctx context.Context, creation *models.Session) error { - const op = "UseCase.CreateSession" - - err := u.sessionsRepo.CreateSession(ctx, creation) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot create session") - } - - return nil -} - -// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! -func (u *SessionsUC) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) { - const op = "UseCase.ReadSession" - - session, err := u.sessionsRepo.ReadSession(ctx, sessionId) - if err != nil { - return nil, pkg.Wrap(nil, err, op, "cannot read session") - } - return session, nil -} - -func (u *SessionsUC) UpdateSession(ctx context.Context, sessionId string) error { - const op = "UseCase.UpdateSession" - - err := u.sessionsRepo.UpdateSession(ctx, sessionId) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot update session") - } - return nil -} - -func (u *SessionsUC) DeleteSession(ctx context.Context, sessionId string) error { - const op = "UseCase.DeleteSession" - - err := u.sessionsRepo.DeleteSession(ctx, sessionId) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot delete session") - } - return nil -} - -func (u *SessionsUC) DeleteAllSessions(ctx context.Context, userId int32) error { - const op = "UseCase.DeleteAllSessions" - - err := u.sessionsRepo.DeleteAllSessions(ctx, userId) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot delete all sessions") - } - - return nil -} diff --git a/internal/sessions/valkey_repository.go b/internal/sessions/valkey_repository.go deleted file mode 100644 index 737a9c2..0000000 --- a/internal/sessions/valkey_repository.go +++ /dev/null @@ -1,14 +0,0 @@ -package sessions - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type ValkeyRepository interface { - CreateSession(ctx context.Context, creation *models.Session) error - ReadSession(ctx context.Context, sessionId string) (*models.Session, error) - UpdateSession(ctx context.Context, sessionId string) error - DeleteSession(ctx context.Context, sessionId string) error - DeleteAllSessions(ctx context.Context, userId int32) error -} diff --git a/internal/contests/delivery.go b/internal/tester/delivery.go similarity index 66% rename from internal/contests/delivery.go rename to internal/tester/delivery.go index 2f7c5f0..44493b5 100644 --- a/internal/contests/delivery.go +++ b/internal/tester/delivery.go @@ -1,11 +1,11 @@ -package contests +package tester import ( testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" "github.com/gofiber/fiber/v2" ) -type ContestsHandlers interface { +type Handlers interface { ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error CreateContest(c *fiber.Ctx) error DeleteContest(c *fiber.Ctx, id int32) error @@ -14,12 +14,18 @@ type ContestsHandlers interface { DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error - CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error + AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error + ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error + CreateProblem(c *fiber.Ctx) error + DeleteProblem(c *fiber.Ctx, id int32) error + GetProblem(c *fiber.Ctx, id int32) error + UpdateProblem(c *fiber.Ctx, id int32) error + UploadProblem(c *fiber.Ctx, id int32) error ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error GetSolution(c *fiber.Ctx, id int32) error DeleteTask(c *fiber.Ctx, id int32) error - CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error + AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error GetTask(c *fiber.Ctx, id int32) error } diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go new file mode 100644 index 0000000..8c017b4 --- /dev/null +++ b/internal/tester/delivery/rest/handlers.go @@ -0,0 +1,622 @@ +package rest + +import ( + testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" + "git.sch9.ru/new_gate/ms-tester/internal/models" + "git.sch9.ru/new_gate/ms-tester/internal/tester" + "git.sch9.ru/new_gate/ms-tester/pkg" + "github.com/gofiber/fiber/v2" + "io" +) + +type TesterHandlers struct { + problemsUC tester.ProblemUseCase + contestsUC tester.ContestUseCase +} + +func NewTesterHandlers(problemsUC tester.ProblemUseCase, contestsUC tester.ContestUseCase) *TesterHandlers { + return &TesterHandlers{ + problemsUC: problemsUC, + contestsUC: contestsUC, + } +} + +func (h *TesterHandlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error { + contestsList, err := h.contestsUC.ListContests(c.Context(), models.ContestsFilter{ + Page: params.Page, + PageSize: params.PageSize, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.ListContestsResponse{ + Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)), + Pagination: P2P(contestsList.Pagination), + } + + for i, contest := range contestsList.Contests { + resp.Contests[i] = CLI2CLI(*contest) + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error { + problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{ + Page: params.Page, + PageSize: params.PageSize, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.ListProblemsResponse{ + Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)), + Pagination: P2P(problemsList.Pagination), + } + + for i, problem := range problemsList.Problems { + resp.Problems[i] = PLI2PLI(*problem) + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) CreateContest(c *fiber.Ctx) error { + id, err := h.contestsUC.CreateContest(c.Context(), "Название контеста") + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.CreateContestResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) DeleteContest(c *fiber.Ctx, id int32) error { + err := h.contestsUC.DeleteContest(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) GetContest(c *fiber.Ctx, id int32) error { + contest, err := h.contestsUC.ReadContestById(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + //token, ok := c.Locals(TokenKey).(*models.JWT) + //if !ok { + // return c.SendStatus(fiber.StatusUnauthorized) + //} + + tasks, err := h.contestsUC.ReadTasks(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + var participantId int32 = 2 + + solutions, err := h.contestsUC.ReadBestSolutions(c.Context(), id, participantId) + + m := make(map[int32]*models.Solution) + + for i := 0; i < len(solutions); i++ { + m[solutions[i].TaskPosition] = solutions[i] + } + resp := testerv1.GetContestResponse{ + Contest: C2C(*contest), + Tasks: make([]struct { + Solution testerv1.Solution `json:"solution"` + Task testerv1.TasksListItem `json:"task"` + }, len(tasks)), + } + + for i, task := range tasks { + solution := testerv1.Solution{} + if sol, ok := m[task.Position]; ok { + solution = S2S(*sol) + } + resp.Tasks[i] = struct { + Solution testerv1.Solution `json:"solution"` + Task testerv1.TasksListItem `json:"task"` + }{ + Solution: solution, + Task: TLI2TLI(*task), + } + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error { + err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error { + id, err := h.contestsUC.AddParticipant(c.Context(), params.ContestId, params.UserId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.AddParticipantResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) DeleteTask(c *fiber.Ctx, id int32) error { + err := h.contestsUC.DeleteTask(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error { + id, err := h.contestsUC.AddTask(c.Context(), params.ContestId, params.ProblemId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.AddTaskResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) CreateProblem(c *fiber.Ctx) error { + id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи") + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.CreateProblemResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) DeleteProblem(c *fiber.Ctx, id int32) error { + err := h.problemsUC.DeleteProblem(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) + +} + +func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error { + problem, err := h.problemsUC.ReadProblemById(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON( + testerv1.GetProblemResponse{Problem: *PR2PR(problem)}, + ) +} + +func (h *TesterHandlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error { + participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{ + Page: params.Page, + PageSize: params.PageSize, + ContestId: params.ContestId, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.ListParticipantsResponse{ + Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)), + Pagination: P2P(participantsList.Pagination), + } + + for i, participant := range participantsList.Participants { + resp.Participants[i] = PTLI2PTLI(*participant) + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) UpdateProblem(c *fiber.Ctx, id int32) error { + var req testerv1.UpdateProblemRequest + err := c.BodyParser(&req) + if err != nil { + return err + } + + err = h.problemsUC.UpdateProblem(c.Context(), id, models.ProblemUpdate{ + Title: req.Title, + MemoryLimit: req.MemoryLimit, + TimeLimit: req.TimeLimit, + + Legend: req.Legend, + InputFormat: req.InputFormat, + OutputFormat: req.OutputFormat, + Notes: req.Notes, + Scoring: req.Scoring, + }) + + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) UploadProblem(c *fiber.Ctx, id int32) error { + var req testerv1.UploadProblemRequest + err := c.BodyParser(&req) + if err != nil { + return err + } + + data, err := req.Archive.Bytes() + if err != nil { + return err + } + if err = h.problemsUC.UploadProblem(c.Context(), id, data); err != nil { + return err + } + return nil +} + +func (h *TesterHandlers) UpdateContest(c *fiber.Ctx, id int32) error { + var req testerv1.UpdateContestRequest + err := c.BodyParser(&req) + if err != nil { + return err + } + + err = h.contestsUC.UpdateContest(c.Context(), id, models.ContestUpdate{ + Title: req.Title, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error { + var req testerv1.UpdateParticipantRequest + err := c.BodyParser(&req) + if err != nil { + return err + } + + err = h.contestsUC.UpdateParticipant(c.Context(), params.ParticipantId, models.ParticipantUpdate{ + Name: req.Name, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *TesterHandlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error { + solutionsList, err := h.contestsUC.ListSolutions(c.Context(), models.SolutionsFilter{ + ContestId: params.ContestId, + Page: params.Page, + PageSize: params.PageSize, + ParticipantId: params.ParticipantId, + TaskId: params.TaskId, + Language: params.Language, + Order: params.Order, + State: params.State, + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.ListSolutionsResponse{ + Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)), + Pagination: P2P(solutionsList.Pagination), + } + + for i, solution := range solutionsList.Solutions { + resp.Solutions[i] = SLI2SLI(*solution) + } + + return c.JSON(resp) +} + +const ( + maxSolutionSize int64 = 10 * 1024 * 1024 +) + +func (h *TesterHandlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error { + s, err := c.FormFile("solution") + if err != nil { + return err + } + + if s.Size == 0 || s.Size > maxSolutionSize { + return c.SendStatus(fiber.StatusBadRequest) + } + + f, err := s.Open() + if err != nil { + return err + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + id, err := h.contestsUC.CreateSolution(c.Context(), &models.SolutionCreation{ + TaskId: params.TaskId, + ParticipantId: 1, + Language: params.Language, + Penalty: 0, + Solution: string(b), + }) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(testerv1.CreateSolutionResponse{ + Id: id, + }) +} + +func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error { + solution, err := h.contestsUC.ReadSolution(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON( + testerv1.GetSolutionResponse{Solution: S2S(*solution)}, + ) +} + +func (h *TesterHandlers) GetTask(c *fiber.Ctx, id int32) error { + contest, err := h.contestsUC.ReadContestById(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + tasks, err := h.contestsUC.ReadTasks(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + t, err := h.contestsUC.ReadTask(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.GetTaskResponse{ + Contest: C2C(*contest), + Tasks: make([]testerv1.TasksListItem, len(tasks)), + Task: *T2T(t), + } + + for i, task := range tasks { + resp.Tasks[i] = TLI2TLI(*task) + } + + return c.JSON(resp) +} + +func (h *TesterHandlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error { + contest, err := h.contestsUC.ReadContestById(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + monitor, err := h.contestsUC.ReadMonitor(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + tasks, err := h.contestsUC.ReadTasks(c.Context(), params.ContestId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + resp := testerv1.GetMonitorResponse{ + Contest: C2C(*contest), + Tasks: make([]testerv1.TasksListItem, len(tasks)), + Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)), + SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)), + } + + for i, participant := range monitor.Participants { + resp.Participants[i] = testerv1.ParticipantsStat{ + Id: participant.Id, + Name: participant.Name, + PenaltyInTotal: participant.PenaltyInTotal, + Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)), + SolvedInTotal: participant.SolvedInTotal, + } + + for j, solution := range participant.Solutions { + resp.Participants[i].Solutions[j] = SLI2SLI(*solution) + } + } + + for i, problem := range monitor.Summary { + resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{ + Id: problem.Id, + Success: problem.Success, + Total: problem.Total, + } + } + + for i, task := range tasks { + resp.Tasks[i] = TLI2TLI(*task) + } + + return c.JSON(resp) +} + +func P2P(p models.Pagination) testerv1.Pagination { + return testerv1.Pagination{ + Page: p.Page, + Total: p.Total, + } +} + +func C2C(c models.Contest) testerv1.Contest { + return testerv1.Contest{ + Id: c.Id, + Title: c.Title, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} + +func CLI2CLI(c models.ContestsListItem) testerv1.ContestsListItem { + return testerv1.ContestsListItem{ + Id: c.Id, + Title: c.Title, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} + +func PLI2PLI(p models.ProblemsListItem) testerv1.ProblemsListItem { + return testerv1.ProblemsListItem{ + Id: p.Id, + Title: p.Title, + MemoryLimit: p.MemoryLimit, + TimeLimit: p.TimeLimit, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + SolvedCount: p.SolvedCount, + } +} + +func TLI2TLI(t models.TasksListItem) testerv1.TasksListItem { + return testerv1.TasksListItem{ + Id: t.Id, + Position: t.Position, + Title: t.Title, + MemoryLimit: t.MemoryLimit, + ProblemId: t.ProblemId, + TimeLimit: t.TimeLimit, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } +} + +func T2T(t *models.Task) *testerv1.Task { + return &testerv1.Task{ + Id: t.Id, + Title: t.Title, + MemoryLimit: t.MemoryLimit, + TimeLimit: t.TimeLimit, + + InputFormatHtml: t.InputFormatHtml, + LegendHtml: t.LegendHtml, + NotesHtml: t.NotesHtml, + OutputFormatHtml: t.OutputFormatHtml, + Position: t.Position, + ScoringHtml: t.ScoringHtml, + + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } +} + +func PR2PR(p *models.Problem) *testerv1.Problem { + return &testerv1.Problem{ + Id: p.Id, + Title: p.Title, + TimeLimit: p.TimeLimit, + MemoryLimit: p.MemoryLimit, + + Legend: p.Legend, + InputFormat: p.InputFormat, + OutputFormat: p.OutputFormat, + Notes: p.Notes, + Scoring: p.Scoring, + + LegendHtml: p.LegendHtml, + InputFormatHtml: p.InputFormatHtml, + OutputFormatHtml: p.OutputFormatHtml, + NotesHtml: p.NotesHtml, + ScoringHtml: p.ScoringHtml, + + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } +} + +func PTLI2PTLI(p models.ParticipantsListItem) testerv1.ParticipantsListItem { + return testerv1.ParticipantsListItem{ + Id: p.Id, + UserId: p.UserId, + Name: p.Name, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } +} + +func SLI2SLI(s models.SolutionsListItem) testerv1.SolutionsListItem { + return testerv1.SolutionsListItem{ + Id: s.Id, + + ParticipantId: s.ParticipantId, + ParticipantName: s.ParticipantName, + + State: s.State, + Score: s.Score, + Penalty: s.Penalty, + TimeStat: s.TimeStat, + MemoryStat: s.MemoryStat, + Language: s.Language, + + TaskId: s.TaskId, + TaskPosition: s.TaskPosition, + TaskTitle: s.TaskTitle, + + ContestId: s.ContestId, + ContestTitle: s.ContestTitle, + + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + } +} + +func S2S(s models.Solution) testerv1.Solution { + return testerv1.Solution{ + Id: s.Id, + + ParticipantId: s.ParticipantId, + ParticipantName: s.ParticipantName, + + Solution: s.Solution, + + State: s.State, + Score: s.Score, + Penalty: s.Penalty, + TimeStat: s.TimeStat, + MemoryStat: s.MemoryStat, + Language: s.Language, + + TaskId: s.TaskId, + TaskPosition: s.TaskPosition, + TaskTitle: s.TaskTitle, + + ContestId: s.ContestId, + ContestTitle: s.ContestTitle, + + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + } +} diff --git a/internal/middleware/auth.go b/internal/tester/delivery/rest/middlewares.go similarity index 67% rename from internal/middleware/auth.go rename to internal/tester/delivery/rest/middlewares.go index cb243ad..b0e984d 100644 --- a/internal/middleware/auth.go +++ b/internal/tester/delivery/rest/middlewares.go @@ -1,11 +1,8 @@ -package middleware +package rest import ( - "errors" "fmt" "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/sessions" - "git.sch9.ru/new_gate/ms-tester/pkg" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v4" "strings" @@ -15,15 +12,19 @@ const ( TokenKey = "token" ) -func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) fiber.Handler { +func AuthMiddleware(jwtSecret string) fiber.Handler { return func(c *fiber.Ctx) error { + const op = "AuthMiddleware" + authHeader := c.Get("Authorization", "") if authHeader == "" { + c.Locals(TokenKey, nil) return c.Next() } authParts := strings.Split(authHeader, " ") if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" { + c.Locals(TokenKey, nil) return c.Next() } @@ -35,30 +36,34 @@ func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) fiber.Handler return []byte(jwtSecret), nil }) if err != nil { + c.Locals(TokenKey, nil) return c.Next() } token, ok := parsedToken.Claims.(*models.JWT) if !ok { + c.Locals(TokenKey, nil) return c.Next() } err = token.Valid() if err != nil { + c.Locals(TokenKey, nil) return c.Next() } - ctx := c.Context() + //ctx := c.Context() // check if session exists - _, err = sessionsUC.ReadSession(ctx, token.SessionId) - if err != nil { - if errors.Is(err, pkg.ErrNotFound) { - return c.Next() - } - - return c.SendStatus(pkg.ToREST(err)) - } + //_, err = userUC.ReadSession(ctx, token.SessionId) + //if err != nil { + // if errors.Is(err, pkg.ErrNotFound) { + // c.Locals(TokenKey, nil) + // return c.Next() + // } + // + // return c.SendStatus(pkg.ToREST(err)) + //} c.Locals(TokenKey, token) return c.Next() diff --git a/internal/tester/pg_repository.go b/internal/tester/pg_repository.go new file mode 100644 index 0000000..78b29cf --- /dev/null +++ b/internal/tester/pg_repository.go @@ -0,0 +1,53 @@ +package tester + +import ( + "context" + "database/sql" + "git.sch9.ru/new_gate/ms-tester/internal/models" + "github.com/jmoiron/sqlx" +) + +type Querier interface { + Rebind(query string) string + QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error +} + +type Tx interface { + Querier + Commit() error + Rollback() error +} + +type ProblemPostgresRepository interface { + BeginTx(ctx context.Context) (Tx, error) + DB() Querier + CreateProblem(ctx context.Context, q Querier, title string) (int32, error) + ReadProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error) + DeleteProblem(ctx context.Context, q Querier, id int32) error + ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) + UpdateProblem(ctx context.Context, q Querier, id int32, heading models.ProblemUpdate) error +} + +type ContestRepository interface { + CreateContest(ctx context.Context, title string) (int32, error) + ReadContestById(ctx context.Context, id int32) (*models.Contest, error) + DeleteContest(ctx context.Context, id int32) error + AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error) + DeleteTask(ctx context.Context, taskId int32) error + AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) + DeleteParticipant(ctx context.Context, participantId int32) error + ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) + ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) + ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) + UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error + UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error + ReadSolution(ctx context.Context, id int32) (*models.Solution, error) + CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) + ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) + ReadTask(ctx context.Context, id int32) (*models.Task, error) + ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) + ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) +} diff --git a/internal/tester/repository/error.go b/internal/tester/repository/error.go new file mode 100644 index 0000000..53b33a0 --- /dev/null +++ b/internal/tester/repository/error.go @@ -0,0 +1,27 @@ +package repository + +import ( + "database/sql" + "errors" + "git.sch9.ru/new_gate/ms-tester/pkg" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" +) + +func handlePgErr(err error, op string) error { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { + return pkg.Wrap(pkg.ErrBadInput, err, op, pgErr.Message) + } + if pgerrcode.IsNoData(pgErr.Code) { + return pkg.Wrap(pkg.ErrNotFound, err, op, pgErr.Message) + } + } + + if errors.Is(err, sql.ErrNoRows) { + return pkg.Wrap(pkg.ErrNotFound, err, op, "no rows found") + } + + return pkg.Wrap(pkg.ErrUnhandled, err, op, "unexpected error") +} diff --git a/internal/tester/repository/pg_contests_repository.go b/internal/tester/repository/pg_contests_repository.go new file mode 100644 index 0000000..94c34e2 --- /dev/null +++ b/internal/tester/repository/pg_contests_repository.go @@ -0,0 +1,690 @@ +package repository + +import ( + "context" + "fmt" + "git.sch9.ru/new_gate/ms-tester/internal/models" + "github.com/jmoiron/sqlx" + "strings" +) + +type ContestRepository struct { + db *sqlx.DB +} + +func NewContestRepository(db *sqlx.DB) *ContestRepository { + return &ContestRepository{ + db: db, + } +} + +const createContestQuery = "INSERT INTO contests (title) VALUES (?) RETURNING id" + +func (r *ContestRepository) CreateContest(ctx context.Context, title string) (int32, error) { + const op = "ContestRepository.CreateContest" + + query := r.db.Rebind(createContestQuery) + + rows, err := r.db.QueryxContext(ctx, query, title) + if err != nil { + return 0, handlePgErr(err, op) + } + + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, handlePgErr(err, op) + } + + return id, nil +} + +const readContestByIdQuery = "SELECT * from contests WHERE id=? LIMIT 1" + +func (r *ContestRepository) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) { + const op = "ContestRepository.ReadContestById" + + var contest models.Contest + query := r.db.Rebind(readContestByIdQuery) + err := r.db.GetContext(ctx, &contest, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + return &contest, nil +} + +const deleteContestQuery = "DELETE FROM contests WHERE id=?" + +func (r *ContestRepository) DeleteContest(ctx context.Context, id int32) error { + const op = "ContestRepository.DeleteContest" + + query := r.db.Rebind(deleteContestQuery) + _, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return handlePgErr(err, op) + } + + return nil +} + +const addTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position) +VALUES (?, ?, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = ?), 0) + 1) +RETURNING id +` + +func (r *ContestRepository) AddTask(ctx context.Context, contestId int32, problemId int32) (int32, error) { + const op = "ContestRepository.AddTask" + + query := r.db.Rebind(addTaskQuery) + rows, err := r.db.QueryxContext(ctx, query, problemId, contestId, contestId) + if err != nil { + return 0, handlePgErr(err, op) + } + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, handlePgErr(err, op) + } + return id, nil +} + +const deleteTaskQuery = "DELETE FROM tasks WHERE id=?" + +func (r *ContestRepository) DeleteTask(ctx context.Context, taskId int32) error { + const op = "ContestRepository.DeleteTask" + + query := r.db.Rebind(deleteTaskQuery) + _, err := r.db.ExecContext(ctx, query, taskId) + if err != nil { + return handlePgErr(err, op) + } + return nil +} + +const addParticipantQuery = "INSERT INTO participants (user_id ,contest_id, name) VALUES (?, ?, ?) RETURNING id" + +func (r *ContestRepository) AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) { + const op = "ContestRepository.AddParticipant" + + query := r.db.Rebind(addParticipantQuery) + name := "" + rows, err := r.db.QueryxContext(ctx, query, contestId, userId, name) + if err != nil { + return 0, handlePgErr(err, op) + } + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, err + } + return id, nil +} + +const deleteParticipantQuery = "DELETE FROM participants WHERE id=?" + +func (r *ContestRepository) DeleteParticipant(ctx context.Context, participantId int32) error { + const op = "ContestRepository.DeleteParticipant" + + query := r.db.Rebind(deleteParticipantQuery) + _, err := r.db.ExecContext(ctx, query, participantId) + if err != nil { + return handlePgErr(err, op) + } + return nil +} + +const readTasksQuery = `SELECT tasks.id, + problem_id, + contest_id, + position, + title, + memory_limit, + time_limit, + tasks.created_at, + tasks.updated_at +FROM tasks + INNER JOIN problems ON tasks.problem_id = problems.id +WHERE contest_id = ? ORDER BY position` + +func (r *ContestRepository) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) { + const op = "ContestRepository.ReadTasks" + + var tasks []*models.TasksListItem + query := r.db.Rebind(readTasksQuery) + err := r.db.SelectContext(ctx, &tasks, query, contestId) + if err != nil { + return nil, handlePgErr(err, op) + } + return tasks, nil +} + +const ( + readContestsListQuery = `SELECT id, title, created_at, updated_at FROM contests LIMIT ? OFFSET ?` + countContestsQuery = "SELECT COUNT(*) FROM contests" +) + +func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) { + const op = "ContestRepository.ReadTasks" + + var contests []*models.ContestsListItem + query := r.db.Rebind(readContestsListQuery) + err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset()) + if err != nil { + return nil, handlePgErr(err, op) + } + + query = r.db.Rebind(countContestsQuery) + var count int32 + err = r.db.GetContext(ctx, &count, query) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &models.ContestsList{ + Contests: contests, + Pagination: models.Pagination{ + Total: models.Total(count, filter.PageSize), + Page: filter.Page, + }, + }, nil +} + +const ( + readParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = ? LIMIT ? OFFSET ?` + countParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = ?" +) + +func (r *ContestRepository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) { + const op = "ContestRepository.ReadParticipants" + + if filter.PageSize > 20 { + filter.PageSize = 1 + } + + var participants []*models.ParticipantsListItem + query := r.db.Rebind(readParticipantsListQuery) + err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset()) + if err != nil { + return nil, handlePgErr(err, op) + } + + query = r.db.Rebind(countParticipantsQuery) + var count int32 + err = r.db.GetContext(ctx, &count, query, filter.ContestId) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &models.ParticipantsList{ + Participants: participants, + Pagination: models.Pagination{ + Total: models.Total(count, filter.PageSize), + Page: filter.Page, + }, + }, nil +} + +const ( + updateContestQuery = "UPDATE contests SET title = COALESCE(?, title) WHERE id = ?" +) + +func (r *ContestRepository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error { + const op = "ContestRepository.UpdateContest" + + query := r.db.Rebind(updateContestQuery) + _, err := r.db.ExecContext(ctx, query, contestUpdate.Title, id) + if err != nil { + return handlePgErr(err, op) + } + + return nil +} + +const ( + updateParticipantQuery = "UPDATE participants SET name = COALESCE(?, name) WHERE id = ?" +) + +func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error { + const op = "ContestRepository.UpdateParticipant" + + query := r.db.Rebind(updateParticipantQuery) + _, err := r.db.ExecContext(ctx, query, participantUpdate.Name, id) + if err != nil { + return handlePgErr(err, op) + } + + return nil +} + +const ( + readSolutionQuery = "SELECT * FROM solutions WHERE id = ?" +) + +func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) { + const op = "ContestRepository.ReadSolution" + + query := r.db.Rebind(readSolutionQuery) + var solution models.Solution + err := r.db.GetContext(ctx, &solution, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &solution, nil +} + +const ( + createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution) +VALUES (?, ?, ?, ?, ?) +RETURNING id` +) + +func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { + const op = "ContestRepository.CreateSolution" + + query := r.db.Rebind(createSolutionQuery) + + rows, err := r.db.QueryxContext(ctx, + query, + creation.TaskId, + creation.ParticipantId, + creation.Language, + creation.Penalty, + creation.Solution, + ) + if err != nil { + return 0, handlePgErr(err, op) + } + + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, handlePgErr(err, op) + } + + return id, nil +} + +func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) { + const op = "ContestRepository.ListSolutions" + + baseQuery := ` +SELECT s.id, + + s.participant_id, + p2.name as participant_name, + + s.state, + s.score, + s.penalty, + s.time_stat, + s.memory_stat, + s.language, + + s.task_id, + t.position as task_position, + p.title as task_title, + + t.contest_id, + c.title, + + s.updated_at, + s.created_at +FROM solutions s + LEFT JOIN tasks t ON s.task_id = t.id + LEFT JOIN problems p ON t.problem_id = p.id + LEFT JOIN contests c ON t.contest_id = c.id + LEFT JOIN participants p2 on s.participant_id = p2.id +WHERE 1=1 + ` + + var conditions []string + var args []interface{} + + if filter.ContestId != nil { + conditions = append(conditions, "s.contest_id = ?") + args = append(args, *filter.ContestId) + } + if filter.ParticipantId != nil { + conditions = append(conditions, "s.participant_id = ?") + args = append(args, *filter.ParticipantId) + } + if filter.TaskId != nil { + conditions = append(conditions, "s.task_id = ?") + args = append(args, *filter.TaskId) + } + if filter.Language != nil { + conditions = append(conditions, "s.language = ?") + args = append(args, *filter.Language) + } + if filter.State != nil { + conditions = append(conditions, "s.state = ?") + args = append(args, *filter.State) + } + + if len(conditions) > 0 { + baseQuery += " AND " + strings.Join(conditions, " AND ") + } + + if filter.Order != nil { + orderDirection := "ASC" + if *filter.Order < 0 { + orderDirection = "DESC" + } + baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection) + } + + countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table" + var totalCount int32 + err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount) + if err != nil { + return nil, handlePgErr(err, op) + } + + offset := (filter.Page - 1) * filter.PageSize + baseQuery += " LIMIT ? OFFSET ?" + args = append(args, filter.PageSize, offset) + + rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...) + if err != nil { + return nil, handlePgErr(err, op) + } + defer rows.Close() + + var solutions []*models.SolutionsListItem + for rows.Next() { + var solution models.SolutionsListItem + err = rows.StructScan(&solution) + if err != nil { + return nil, handlePgErr(err, op) + } + solutions = append(solutions, &solution) + } + + if err = rows.Err(); err != nil { + return nil, handlePgErr(err, op) + } + + return &models.SolutionsList{ + Solutions: solutions, + Pagination: models.Pagination{ + Total: models.Total(totalCount, filter.PageSize), + Page: filter.Page, + }, + }, nil +} + +const ( + readTaskQuery = ` + SELECT + t.id, + t.position, + p.title, + p.time_limit, + p.memory_limit, + t.problem_id, + t.contest_id, + p.legend_html, + p.input_format_html, + p.output_format_html, + p.notes_html, + p.scoring_html, + t.created_at, + t.updated_at + FROM tasks t + LEFT JOIN problems p ON t.problem_id = p.id + WHERE t.id = ? + ` +) + +func (r *ContestRepository) ReadTask(ctx context.Context, id int32) (*models.Task, error) { + const op = "ContestRepository.ReadTask" + + query := r.db.Rebind(readTaskQuery) + var task models.Task + err := r.db.GetContext(ctx, &task, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &task, nil +} + +const ( + // state=5 - AC + readStatisticsQuery = ` +SELECT t.id as task_id, + t.position, + COUNT(*) as total, + COUNT(CASE WHEN s.state = 5 THEN 1 END) as success +FROM tasks t + LEFT JOIN solutions s ON t.id = s.task_id +WHERE t.contest_id = ? +GROUP BY t.id, t.position +ORDER BY t.position; +` + + solutionsQuery = ` +WITH RankedSolutions AS ( + SELECT + s.id, + + s.participant_id, + p2.name as participant_name, + + s.state, + s.score, + s.penalty, + s.time_stat, + s.memory_stat, + s.language, + + s.task_id, + t.position as task_position, + p.title as task_title, + + t.contest_id, + c.title as contest_title, + + s.updated_at, + s.created_at, + ROW_NUMBER() OVER ( + PARTITION BY s.participant_id, s.task_id + ORDER BY + CASE WHEN s.state = 5 THEN 0 ELSE 1 END, + s.created_at + ) as rn + FROM solutions s + LEFT JOIN tasks t ON s.task_id = t.id + LEFT JOIN problems p ON t.problem_id = p.id + LEFT JOIN contests c ON t.contest_id = c.id + LEFT JOIN participants p2 on s.participant_id = p2.id + WHERE t.contest_id = ? +) +SELECT + rs.id, + + rs.participant_id, + rs.participant_name, + + rs.state, + rs.score, + rs.penalty, + rs.time_stat, + rs.memory_stat, + rs.language, + + rs.task_id, + rs.task_position, + rs.task_title, + + rs.contest_id, + rs.contest_title, + + rs.updated_at, + rs.created_at +FROM RankedSolutions rs +WHERE rs.rn = 1; + +` + + participantsQuery = ` +WITH Attempts AS ( + SELECT + s.participant_id, + s.task_id, + COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < ( + SELECT MIN(s2.created_at) + FROM solutions s2 + WHERE s2.participant_id = s.participant_id + AND s2.task_id = s.task_id + AND s2.state = 5 + )) as failed_attempts, + MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty + FROM solutions s + JOIN tasks t ON t.id = s.task_id + WHERE t.contest_id = :contest_id + GROUP BY s.participant_id, s.task_id +) +SELECT + p.id, + p.name, + COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total, + COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL + THEN a.failed_attempts * :penalty + a.success_penalty + ELSE 0 END), 0) as penalty_in_total +FROM participants p + LEFT JOIN Attempts a ON a.participant_id = p.id +WHERE p.contest_id = :contest_id +GROUP BY p.id, p.name +` +) + +func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { + const op = "ContestRepository.ReadMonitor" + + query := r.db.Rebind(readStatisticsQuery) + rows, err := r.db.QueryxContext(ctx, query, contestId) + if err != nil { + return nil, handlePgErr(err, op) + } + defer rows.Close() + + var monitor models.Monitor + for rows.Next() { + var stat models.ProblemStatSummary + err = rows.StructScan(&stat) + if err != nil { + return nil, handlePgErr(err, op) + } + monitor.Summary = append(monitor.Summary, &stat) + } + + var solutions []*models.SolutionsListItem + err = r.db.SelectContext(ctx, &solutions, r.db.Rebind(solutionsQuery), contestId) + if err != nil { + return nil, handlePgErr(err, op) + } + + penalty := int32(20) // FIXME + namedQuery := r.db.Rebind(participantsQuery) + rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{ + "contest_id": contestId, + "penalty": penalty, + }) + if err != nil { + return nil, handlePgErr(err, op) + } + defer rows3.Close() + + solutionsMap := make(map[int32][]*models.SolutionsListItem) + for _, solution := range solutions { + solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution) + } + + for rows3.Next() { + var stat models.ParticipantsStat + err = rows3.StructScan(&stat) + if err != nil { + return nil, handlePgErr(err, op) + } + + if sols, ok := solutionsMap[stat.Id]; ok { + stat.Solutions = sols + } + + monitor.Participants = append(monitor.Participants, &stat) + } + + return &monitor, nil +} + +const ( + // state=5 - AC + readBestSolutions = ` + WITH contest_tasks AS ( + SELECT t.id AS task_id, + t.position AS task_position, + t.contest_id, + t.problem_id, + t.created_at, + t.updated_at, + p.title AS task_title, + c.title AS contest_title + FROM tasks t + LEFT JOIN problems p ON p.id = t.problem_id + LEFT JOIN contests c ON c.id = t.contest_id + WHERE t.contest_id = ? +), + best_solutions AS ( + SELECT DISTINCT ON (s.task_id) + * + FROM solutions s + WHERE s.participant_id = ? + ORDER BY s.task_id, s.score DESC, s.created_at DESC + ) +SELECT + s.id, + s.participant_id, + p.name AS participant_name, + s.solution, + s.state, + s.score, + s.penalty, + s.time_stat, + s.memory_stat, + s.language, + ct.task_id, + ct.task_position, + ct.task_title, + ct.contest_id, + ct.contest_title, + s.updated_at, + s.created_at +FROM contest_tasks ct + LEFT JOIN best_solutions s ON s.task_id = ct.task_id + LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL +ORDER BY ct.task_position +` +) + +func (r *ContestRepository) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) { + const op = "ContestRepository.ReadBestSolutions" + var solutions []*models.Solution + query := r.db.Rebind(readBestSolutions) + err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId) + + if err != nil { + return nil, handlePgErr(err, op) + } + + return solutions, nil +} diff --git a/internal/tester/repository/pg_contests_repository_test.go b/internal/tester/repository/pg_contests_repository_test.go new file mode 100644 index 0000000..be2cccd --- /dev/null +++ b/internal/tester/repository/pg_contests_repository_test.go @@ -0,0 +1,154 @@ +package repository + +import ( + "context" + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "testing" +) + +func TestContestRepository_CreateContest(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + + t.Run("valid contest creation", func(t *testing.T) { + title := "Contest title" + + rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + mock.ExpectQuery(sqlxDB.Rebind(createContestQuery)).WithArgs(title).WillReturnRows(rows) + + id, err := contestRepo.CreateContest(context.Background(), title) + require.NoError(t, err) + require.Equal(t, int32(1), id) + }) +} + +func TestContestRepository_DeleteContest(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + + t.Run("valid contest deletion", func(t *testing.T) { + id := int32(1) + rows := sqlmock.NewResult(1, 1) + + mock.ExpectExec(sqlxDB.Rebind(deleteContestQuery)).WithArgs(id).WillReturnResult(rows) + + err = contestRepo.DeleteContest(context.Background(), id) + require.NoError(t, err) + }) +} + +func TestContestRepository_AddTask(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + + t.Run("valid task additional", func(t *testing.T) { + taskId := int32(1) + contestId := int32(1) + + rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + mock.ExpectQuery(sqlxDB.Rebind(addTaskQuery)).WithArgs(taskId, contestId, contestId).WillReturnRows(rows) + + id, err := contestRepo.AddTask(context.Background(), contestId, taskId) + + require.NoError(t, err) + require.Equal(t, int32(1), id) + + }) +} + +func TestContestRepository_DeleteTask(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + t.Run("valid task deletion", func(t *testing.T) { + id := int32(1) + rows := sqlmock.NewResult(1, 1) + + mock.ExpectExec(sqlxDB.Rebind(deleteTaskQuery)).WithArgs(id).WillReturnResult(rows) + + err = contestRepo.DeleteTask(context.Background(), id) + require.NoError(t, err) + }) +} + +func TestContestRepository_AddParticipant(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + + t.Run("valid participant addition", func(t *testing.T) { + contestId := int32(1) + userId := int32(1) + name := "" + + rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + mock.ExpectQuery(sqlxDB.Rebind(addParticipantQuery)).WithArgs(contestId, userId, name).WillReturnRows(rows) + + id, err := contestRepo.AddParticipant(context.Background(), contestId, userId) + + require.NoError(t, err) + require.Equal(t, int32(1), id) + }) +} + +func TestContestRepository_DeleteParticipant(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + contestRepo := NewContestRepository(sqlxDB, zap.NewNop()) + + t.Run("valid participant deletion", func(t *testing.T) { + id := int32(1) + rows := sqlmock.NewResult(1, 1) + + mock.ExpectExec(sqlxDB.Rebind(deleteParticipantQuery)).WithArgs(id).WillReturnResult(rows) + + err = contestRepo.DeleteParticipant(context.Background(), id) + require.NoError(t, err) + }) +} diff --git a/internal/tester/repository/pg_problems_repository.go b/internal/tester/repository/pg_problems_repository.go new file mode 100644 index 0000000..d4f16ad --- /dev/null +++ b/internal/tester/repository/pg_problems_repository.go @@ -0,0 +1,184 @@ +package repository + +import ( + "context" + + "git.sch9.ru/new_gate/ms-tester/internal/models" + "git.sch9.ru/new_gate/ms-tester/internal/tester" + "github.com/jmoiron/sqlx" +) + +type ProblemRepository struct { + _db *sqlx.DB +} + +func NewProblemRepository(db *sqlx.DB) *ProblemRepository { + return &ProblemRepository{ + _db: db, + } +} + +func (r *ProblemRepository) BeginTx(ctx context.Context) (tester.Tx, error) { + tx, err := r._db.BeginTxx(ctx, nil) + if err != nil { + return nil, err + } + + return tx, nil +} + +func (r *ProblemRepository) DB() tester.Querier { + return r._db +} + +const createProblemQuery = "INSERT INTO problems (title) VALUES (?) RETURNING id" + +func (r *ProblemRepository) CreateProblem(ctx context.Context, q tester.Querier, title string) (int32, error) { + const op = "ProblemRepository.CreateProblem" + + query := q.Rebind(createProblemQuery) + rows, err := q.QueryxContext(ctx, query, title) + if err != nil { + return 0, handlePgErr(err, op) + } + + defer rows.Close() + var id int32 + rows.Next() + err = rows.Scan(&id) + if err != nil { + return 0, handlePgErr(err, op) + } + + return id, nil +} + +const readProblemQuery = "SELECT * from problems WHERE id=? LIMIT 1" + +func (r *ProblemRepository) ReadProblemById(ctx context.Context, q tester.Querier, id int32) (*models.Problem, error) { + const op = "ProblemRepository.ReadProblemById" + + var problem models.Problem + query := q.Rebind(readProblemQuery) + err := q.GetContext(ctx, &problem, query, id) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &problem, nil +} + +const deleteProblemQuery = "DELETE FROM problems WHERE id=?" + +func (r *ProblemRepository) DeleteProblem(ctx context.Context, q tester.Querier, id int32) error { + const op = "ProblemRepository.DeleteProblem" + + query := q.Rebind(deleteProblemQuery) + _, err := q.ExecContext(ctx, query, id) + if err != nil { + return handlePgErr(err, op) + } + + return nil +} + +const ( + ListProblemsQuery = ` + SELECT + p.id,p.title,p.memory_limit,p.time_limit,p.created_at,p.updated_at, + COALESCE(solved_count, 0) AS solved_count + FROM problems p + LEFT JOIN ( + SELECT + t.problem_id, + COUNT(DISTINCT s.participant_id) AS solved_count + FROM solutions s + JOIN tasks t ON s.task_id = t.id + WHERE s.state = 5 + GROUP BY t.problem_id + ) sol ON p.id = sol.problem_id + LIMIT ? OFFSET ?` + CountProblemsQuery = "SELECT COUNT(*) FROM problems" +) + +func (r *ProblemRepository) ListProblems(ctx context.Context, q tester.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) { + const op = "ContestRepository.ListProblems" + + if filter.PageSize > 20 || filter.PageSize < 1 { + filter.PageSize = 1 + } + + var problems []*models.ProblemsListItem + query := q.Rebind(ListProblemsQuery) + err := q.SelectContext(ctx, &problems, query, filter.PageSize, filter.Offset()) + if err != nil { + return nil, handlePgErr(err, op) + } + + query = q.Rebind(CountProblemsQuery) + + var count int32 + err = q.GetContext(ctx, &count, query) + if err != nil { + return nil, handlePgErr(err, op) + } + + return &models.ProblemsList{ + Problems: problems, + Pagination: models.Pagination{ + Total: models.Total(count, filter.PageSize), + Page: filter.Page, + }, + }, nil +} + +const ( + UpdateProblemQuery = `UPDATE problems +SET title = COALESCE(?, title), + time_limit = COALESCE(?, time_limit), + memory_limit = COALESCE(?, memory_limit), + + legend = COALESCE(?, legend), + input_format = COALESCE(?, input_format), + output_format = COALESCE(?, output_format), + notes = COALESCE(?, notes), + scoring = COALESCE(?, scoring), + + legend_html = COALESCE(?, legend_html), + input_format_html = COALESCE(?, input_format_html), + output_format_html = COALESCE(?, output_format_html), + notes_html = COALESCE(?, notes_html), + scoring_html = COALESCE(?, scoring_html) + +WHERE id=?` +) + +func (r *ProblemRepository) UpdateProblem(ctx context.Context, q tester.Querier, id int32, problem models.ProblemUpdate) error { + const op = "ProblemRepository.UpdateProblem" + + query := q.Rebind(UpdateProblemQuery) + _, err := q.ExecContext(ctx, query, + problem.Title, + problem.TimeLimit, + problem.MemoryLimit, + + problem.Legend, + problem.InputFormat, + problem.OutputFormat, + problem.Notes, + problem.Scoring, + + problem.LegendHtml, + problem.InputFormatHtml, + problem.OutputFormatHtml, + problem.NotesHtml, + problem.ScoringHtml, + + id, + ) + if err != nil { + return handlePgErr(err, op) + } + + return nil +} diff --git a/internal/tester/repository/pg_problems_repository_test.go b/internal/tester/repository/pg_problems_repository_test.go new file mode 100644 index 0000000..76d67cf --- /dev/null +++ b/internal/tester/repository/pg_problems_repository_test.go @@ -0,0 +1,58 @@ +package repository + +import ( + "context" + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "testing" +) + +func TestProblemRepository_CreateProblem(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + problemRepo := NewProblemRepository(sqlxDB, zap.NewNop()) + + t.Run("valid problem creation", func(t *testing.T) { + title := "Problem title" + + rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + mock.ExpectQuery(sqlxDB.Rebind(createProblemQuery)).WithArgs(title).WillReturnRows(rows) + + id, err := problemRepo.CreateProblem(context.Background(), title) + require.NoError(t, err) + require.Equal(t, int32(1), id) + }) +} + +func TestProblemRepository_DeleteProblem(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + require.NoError(t, err) + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + defer sqlxDB.Close() + + problemRepo := NewProblemRepository(sqlxDB, zap.NewNop()) + + t.Run("valid problem deletion", func(t *testing.T) { + id := int32(1) + rows := sqlmock.NewResult(1, 1) + + mock.ExpectExec(sqlxDB.Rebind(deleteProblemQuery)).WithArgs(id).WillReturnResult(rows) + + err = problemRepo.DeleteProblem(context.Background(), id) + require.NoError(t, err) + }) +} diff --git a/internal/tester/usecase.go b/internal/tester/usecase.go new file mode 100644 index 0000000..a9088a2 --- /dev/null +++ b/internal/tester/usecase.go @@ -0,0 +1,37 @@ +package tester + +import ( + "context" + + "git.sch9.ru/new_gate/ms-tester/internal/models" +) + +type ProblemUseCase interface { + CreateProblem(ctx context.Context, title string) (int32, error) + ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) + DeleteProblem(ctx context.Context, id int32) error + ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) + UpdateProblem(ctx context.Context, id int32, problem models.ProblemUpdate) error + UploadProblem(ctx context.Context, id int32, archive []byte) error +} + +type ContestUseCase interface { + CreateContest(ctx context.Context, title string) (int32, error) + ReadContestById(ctx context.Context, id int32) (*models.Contest, error) + DeleteContest(ctx context.Context, id int32) error + AddTask(ctx context.Context, contestId int32, taskId int32) (int32, error) + DeleteTask(ctx context.Context, taskId int32) error + AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) + DeleteParticipant(ctx context.Context, participantId int32) error + ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) + ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) + ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) + UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error + UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error + ReadSolution(ctx context.Context, id int32) (*models.Solution, error) + CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) + ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) + ReadTask(ctx context.Context, id int32) (*models.Task, error) + ReadMonitor(ctx context.Context, id int32) (*models.Monitor, error) + ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) +} diff --git a/internal/tester/usecase/contests_usecase.go b/internal/tester/usecase/contests_usecase.go new file mode 100644 index 0000000..28761dd --- /dev/null +++ b/internal/tester/usecase/contests_usecase.go @@ -0,0 +1,91 @@ +package usecase + +import ( + "context" + "git.sch9.ru/new_gate/ms-tester/internal/models" + "git.sch9.ru/new_gate/ms-tester/internal/tester" +) + +type ContestUseCase struct { + contestRepo tester.ContestRepository +} + +func NewContestUseCase( + contestRepo tester.ContestRepository, +) *ContestUseCase { + return &ContestUseCase{ + contestRepo: contestRepo, + } +} + +func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) { + return uc.contestRepo.CreateContest(ctx, title) +} + +func (uc *ContestUseCase) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) { + return uc.contestRepo.ReadContestById(ctx, id) +} + +func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error { + return uc.contestRepo.DeleteContest(ctx, id) +} + +func (uc *ContestUseCase) AddTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) { + return uc.contestRepo.AddTask(ctx, contestId, taskId) +} + +func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error { + return uc.contestRepo.DeleteTask(ctx, taskId) +} + +func (uc *ContestUseCase) AddParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) { + return uc.contestRepo.AddParticipant(ctx, contestId, userId) +} + +func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error { + return uc.contestRepo.DeleteParticipant(ctx, participantId) +} + +func (uc *ContestUseCase) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) { + return uc.contestRepo.ReadTasks(ctx, contestId) +} + +func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) { + return uc.contestRepo.ListContests(ctx, filter) +} + +func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) { + return uc.contestRepo.ListParticipants(ctx, filter) +} + +func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error { + return uc.contestRepo.UpdateContest(ctx, id, contestUpdate) +} + +func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error { + return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate) +} + +func (uc *ContestUseCase) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) { + return uc.contestRepo.ReadSolution(ctx, id) +} + +func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) { + return uc.contestRepo.CreateSolution(ctx, creation) +} + +func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) { + return uc.contestRepo.ListSolutions(ctx, filter) +} + +func (uc *ContestUseCase) ReadTask(ctx context.Context, id int32) (*models.Task, error) { + return uc.contestRepo.ReadTask(ctx, id) +} + +func (uc *ContestUseCase) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) { + return uc.contestRepo.ReadMonitor(ctx, contestId) +} + +func (uc *ContestUseCase) ReadBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.Solution, error) { + return uc.contestRepo.ReadBestSolutions(ctx, contestId, participantId) +} diff --git a/internal/problems/usecase/usecase.go b/internal/tester/usecase/problems_usecase.go similarity index 88% rename from internal/problems/usecase/usecase.go rename to internal/tester/usecase/problems_usecase.go index b73557b..27a9c37 100644 --- a/internal/problems/usecase/usecase.go +++ b/internal/tester/usecase/problems_usecase.go @@ -7,48 +7,48 @@ import ( "encoding/json" "errors" "fmt" - "git.sch9.ru/new_gate/ms-tester/internal/problems" "io" "strings" "git.sch9.ru/new_gate/ms-tester/internal/models" + "git.sch9.ru/new_gate/ms-tester/internal/tester" "git.sch9.ru/new_gate/ms-tester/pkg" "github.com/microcosm-cc/bluemonday" ) -type UseCase struct { - problemRepo problems.Repository +type ProblemUseCase struct { + problemRepo tester.ProblemPostgresRepository pandocClient pkg.PandocClient } -func NewUseCase( - problemRepo problems.Repository, +func NewProblemUseCase( + problemRepo tester.ProblemPostgresRepository, pandocClient pkg.PandocClient, -) *UseCase { - return &UseCase{ +) *ProblemUseCase { + return &ProblemUseCase{ problemRepo: problemRepo, pandocClient: pandocClient, } } -func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) { +func (u *ProblemUseCase) CreateProblem(ctx context.Context, title string) (int32, error) { return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title) } -func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) { - return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id) +func (u *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) { + return u.problemRepo.ReadProblemById(ctx, u.problemRepo.DB(), id) } -func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error { +func (u *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error { return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id) } -func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) { +func (u *ProblemUseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) { return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter) } -func (u *UseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate *models.ProblemUpdate) error { - if isEmpty(*problemUpdate) { +func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error { + if isEmpty(problemUpdate) { return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update") } @@ -57,7 +57,7 @@ func (u *UseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate *mo return err } - problem, err := u.problemRepo.GetProblemById(ctx, tx, id) + problem, err := u.problemRepo.ReadProblemById(ctx, tx, id) if err != nil { return errors.Join(err, tx.Rollback()) } @@ -126,7 +126,7 @@ type ProblemProperties struct { MemoryLimit int32 `json:"memoryLimit"` } -func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error { +func (u *ProblemUseCase) UploadProblem(ctx context.Context, id int32, data []byte) error { locale := "russian" defaultLocale := "english" @@ -185,7 +185,7 @@ func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) erro localeProperties.MemoryLimit /= 1024 * 1024 defaultProperties.MemoryLimit /= 1024 * 1024 - problemUpdate := &models.ProblemUpdate{} + var problemUpdate models.ProblemUpdate if localeProblem != "" { problemUpdate.Legend = &localeProblem problemUpdate.Title = &localeProperties.Title diff --git a/internal/users/delivery.go b/internal/users/delivery.go deleted file mode 100644 index 4ef4149..0000000 --- a/internal/users/delivery.go +++ /dev/null @@ -1,14 +0,0 @@ -package users - -import ( - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "github.com/gofiber/fiber/v2" -) - -type UsersHandlers interface { - ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error - CreateUser(c *fiber.Ctx) error - DeleteUser(c *fiber.Ctx, id int32) error - GetUser(c *fiber.Ctx, id int32) error - UpdateUser(c *fiber.Ctx, id int32) error -} diff --git a/internal/users/delivery/rest/handlers.go b/internal/users/delivery/rest/handlers.go deleted file mode 100644 index b15b60a..0000000 --- a/internal/users/delivery/rest/handlers.go +++ /dev/null @@ -1,204 +0,0 @@ -package rest - -import ( - "context" - testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/users" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/gofiber/fiber/v2" -) - -type Handlers struct { - usersUC users.UseCase -} - -func NewHandlers(usersUC users.UseCase) *Handlers { - return &Handlers{ - usersUC: usersUC, - } -} - -const ( - sessionKey = "session" -) - -func sessionFromCtx(ctx context.Context) (*models.Session, error) { - const op = "sessionFromCtx" - - session, ok := ctx.Value(sessionKey).(*models.Session) - if !ok { - return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "") - } - - return session, nil -} - -func (h *Handlers) CreateUser(c *fiber.Ctx) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - var req = &testerv1.CreateUserRequest{} - err := c.BodyParser(req) - if err != nil { - return c.SendStatus(fiber.StatusBadRequest) - } - - id, err := h.usersUC.CreateUser(ctx, - &models.UserCreation{ - Username: req.Username, - Password: req.Password, - Role: models.RoleStudent, - }, - ) - - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.CreateUserResponse{Id: id}) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) GetUser(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher, models.RoleStudent: - user, err := h.usersUC.ReadUserById(c.Context(), id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.JSON(testerv1.GetUserResponse{ - User: UserDTO(*user), - }) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) UpdateUser(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin: - var req = &testerv1.UpdateUserRequest{} - err := c.BodyParser(req) - if err != nil { - return c.SendStatus(fiber.StatusBadRequest) - } - - err = h.usersUC.UpdateUser(c.Context(), id, &models.UserUpdate{ - Username: req.Username, - Role: RoleDTO(req.Role), - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) DeleteUser(c *fiber.Ctx, id int32) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin: - ctx := c.Context() - - err := h.usersUC.DeleteUser(ctx, id) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - return c.SendStatus(fiber.StatusOK) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func (h *Handlers) ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error { - ctx := c.Context() - - session, err := sessionFromCtx(ctx) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - switch session.Role { - case models.RoleAdmin, models.RoleTeacher: - usersList, err := h.usersUC.ListUsers(c.Context(), models.UsersListFilters{ - PageSize: params.PageSize, - Page: params.Page, - }) - if err != nil { - return c.SendStatus(pkg.ToREST(err)) - } - - resp := testerv1.ListUsersResponse{ - Users: make([]testerv1.User, len(usersList.Users)), - Pagination: PaginationDTO(usersList.Pagination), - } - - for i, user := range usersList.Users { - resp.Users[i] = UserDTO(*user) - } - - return c.JSON(resp) - default: - return c.SendStatus(pkg.ToREST(pkg.NoPermission)) - } -} - -func RoleDTO(i *int32) *models.Role { - if i == nil { - return nil - } - ii := models.Role(*i) - return &ii -} - -func PaginationDTO(p models.Pagination) testerv1.Pagination { - return testerv1.Pagination{ - Page: p.Page, - Total: p.Total, - } -} - -// UserDTO sanitizes password -func UserDTO(u models.User) testerv1.User { - return testerv1.User{ - Id: u.Id, - Username: u.Username, - Role: int32(u.Role), - CreatedAt: u.CreatedAt, - ModifiedAt: u.UpdatedAt, - } -} diff --git a/internal/users/pg_repository.go b/internal/users/pg_repository.go deleted file mode 100644 index 63506f0..0000000 --- a/internal/users/pg_repository.go +++ /dev/null @@ -1,33 +0,0 @@ -package users - -import ( - "context" - "database/sql" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "github.com/jmoiron/sqlx" -) - -type Querier interface { - Rebind(query string) string - QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) - GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error -} - -type Tx interface { - Querier - Commit() error - Rollback() error -} - -type Repository interface { - BeginTx(ctx context.Context) (Tx, error) - DB() Querier - CreateUser(ctx context.Context, q Querier, user *models.UserCreation) (int32, error) - ReadUserByUsername(ctx context.Context, q Querier, username string) (*models.User, error) - ReadUserById(ctx context.Context, q Querier, id int32) (*models.User, error) - UpdateUser(ctx context.Context, q Querier, id int32, update *models.UserUpdate) error - DeleteUser(ctx context.Context, q Querier, id int32) error - ListUsers(ctx context.Context, q Querier, filters models.UsersListFilters) (*models.UsersList, error) -} diff --git a/internal/users/repository/pg_repository.go b/internal/users/repository/pg_repository.go deleted file mode 100644 index e314c7e..0000000 --- a/internal/users/repository/pg_repository.go +++ /dev/null @@ -1,156 +0,0 @@ -package repository - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/users" - "git.sch9.ru/new_gate/ms-tester/pkg" - "github.com/jmoiron/sqlx" -) - -type Repository struct { - _db *sqlx.DB -} - -func NewRepository(db *sqlx.DB) *Repository { - return &Repository{ - _db: db, - } -} - -func (r *Repository) BeginTx(ctx context.Context) (users.Tx, error) { - tx, err := r._db.BeginTxx(ctx, nil) - if err != nil { - return nil, err - } - - return tx, nil -} - -func (r *Repository) DB() users.Querier { - return r._db -} - -const CreateUserQuery = ` -INSERT INTO users - (username, hashed_pwd, role) -VALUES ($1, $2, $3) -RETURNING id -` - -func (r *Repository) CreateUser(ctx context.Context, q users.Querier, user *models.UserCreation) (int32, error) { - const op = "Caller.CreateUser" - - rows, err := q.QueryxContext( - ctx, - CreateUserQuery, - user.Username, - user.Password, - user.Role, - ) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - defer rows.Close() - var id int32 - rows.Next() - err = rows.Scan(&id) - if err != nil { - return 0, pkg.HandlePgErr(err, op) - } - - return id, nil -} - -const ReadUserByUsernameQuery = "SELECT * from users WHERE username=$1 LIMIT 1" - -func (r *Repository) ReadUserByUsername(ctx context.Context, q users.Querier, username string) (*models.User, error) { - const op = "Caller.ReadUserByUsername" - - var user models.User - err := q.GetContext(ctx, &user, ReadUserByUsernameQuery, username) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - return &user, nil -} - -const ReadUserByIdQuery = "SELECT * from users WHERE id=$1 LIMIT 1" - -func (r *Repository) ReadUserById(ctx context.Context, q users.Querier, id int32) (*models.User, error) { - const op = "Caller.ReadUserById" - - var user models.User - err := q.GetContext(ctx, &user, ReadUserByIdQuery, id) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - return &user, nil -} - -const UpdateUserQuery = ` -UPDATE users -SET username = COALESCE($1, username), - role = COALESCE($2, role) -WHERE id = $3 -` - -func (r *Repository) UpdateUser(ctx context.Context, q users.Querier, id int32, update *models.UserUpdate) error { - const op = "Caller.UpdateUser" - - _, err := q.ExecContext( - ctx, - UpdateUserQuery, - update.Username, - update.Role, - id, - ) - - if err != nil { - return pkg.HandlePgErr(err, op) - } - return nil -} - -const DeleteUserQuery = "DELETE FROM users WHERE id = $1" - -func (r *Repository) DeleteUser(ctx context.Context, q users.Querier, id int32) error { - const op = "Caller.DeleteUser" - - _, err := q.ExecContext(ctx, DeleteUserQuery, id) - if err != nil { - return pkg.HandlePgErr(err, op) - } - - return nil -} - -const ( - ListUsersQuery = "SELECT * FROM users LIMIT $1 OFFSET $2" - CountUsersQuery = "SELECT COUNT(*) FROM users" -) - -func (r *Repository) ListUsers(ctx context.Context, q users.Querier, filters models.UsersListFilters) (*models.UsersList, error) { - const op = "Caller.ListUsers" - - list := make([]*models.User, 0) - err := q.SelectContext(ctx, &list, ListUsersQuery, filters.PageSize, filters.Offset()) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - var count int32 - err = q.GetContext(ctx, &count, CountUsersQuery) - if err != nil { - return nil, pkg.HandlePgErr(err, op) - } - - return &models.UsersList{ - Users: list, - Pagination: models.Pagination{ - Total: models.Total(count, filters.PageSize), - Page: filters.Page, - }, - }, nil -} diff --git a/internal/users/repository/pg_repository_test.go b/internal/users/repository/pg_repository_test.go deleted file mode 100644 index caf3a49..0000000 --- a/internal/users/repository/pg_repository_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package repository_test - -import ( - "context" - "database/sql" - "testing" - "time" - - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/users/repository" - "github.com/DATA-DOG/go-sqlmock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" -) - -// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing. -func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) { - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - assert.NoError(t, err) - sqlxDB := sqlx.NewDb(db, "sqlmock") - return sqlxDB, mock -} - -func TestRepository_CreateUser(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - var expectedId int32 = 1 - user := &models.UserCreation{ - Username: "testuser", - Password: "hashed-password", - Role: models.RoleAdmin, - } - - mock.ExpectQuery(repository.CreateUserQuery). - WithArgs(user.Username, sqlmock.AnyArg(), user.Role). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId)) - - id, err := repo.CreateUser(ctx, db, user) - assert.NoError(t, err) - assert.Equal(t, expectedId, id) - }) -} - -func TestRepository_ReadUserByUsername(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - expected := &models.User{ - Id: 1, - Username: "testuser", - HashedPassword: "hashed-password", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Role: models.RoleAdmin, - } - - columns := []string{ - "id", - "username", - "hashed_pwd", - "created_at", - "updated_at", - "role", - } - - rows := sqlmock.NewRows(columns).AddRow( - expected.Id, - expected.Username, - expected.HashedPassword, - expected.CreatedAt, - expected.UpdatedAt, - expected.Role, - ) - - mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(expected.Username).WillReturnRows(rows) - - user, err := repo.ReadUserByUsername(ctx, db, expected.Username) - assert.NoError(t, err) - assert.Equal(t, expected, user) - }) - - t.Run("not found", func(t *testing.T) { - ctx := context.Background() - - username := "testuser" - - mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(username).WillReturnError(sql.ErrNoRows) - - user, err := repo.ReadUserByUsername(ctx, db, username) - assert.Error(t, err) - assert.Nil(t, user) - }) -} - -func TestRepository_ReadUserById(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - expected := &models.User{ - Id: 1, - Username: "testuser", - Role: models.RoleAdmin, - } - - mock.ExpectQuery(repository.ReadUserByIdQuery). - WithArgs(expected.Id). - WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}). - AddRow(expected.Id, expected.Username, expected.Role)) - - user, err := repo.ReadUserById(ctx, db, expected.Id) - assert.NoError(t, err) - assert.Equal(t, expected, user) - }) - - t.Run("not found", func(t *testing.T) { - ctx := context.Background() - - userID := int32(1) - - mock.ExpectQuery(repository.ReadUserByIdQuery).WithArgs(userID).WillReturnError(sql.ErrNoRows) - - user, err := repo.ReadUserById(ctx, db, userID) - assert.Error(t, err) - assert.Nil(t, user) - }) -} - -func TestRepository_UpdateUser(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - userID := int32(1) - username := "testuser" - role := models.RoleStudent - update := &models.UserUpdate{ - Username: &username, - Role: &role, - } - - mock.ExpectExec(repository.UpdateUserQuery). - WithArgs(update.Username, update.Role, userID). - WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.UpdateUser(ctx, db, userID, update) - assert.NoError(t, err) - }) -} - -func TestRepository_DeleteUser(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - userID := int32(1) - - mock.ExpectExec(repository.DeleteUserQuery). - WithArgs(userID). - WillReturnResult(sqlmock.NewResult(0, 1)) - - err := repo.DeleteUser(ctx, db, userID) - assert.NoError(t, err) - }) -} - -func TestRepository_ListUsers(t *testing.T) { - db, mock := setupTestDB(t) - defer db.Close() - - repo := repository.NewRepository(db) - - t.Run("success", func(t *testing.T) { - ctx := context.Background() - - filters := models.UsersListFilters{ - Page: 1, - PageSize: 10, - } - expectedUsers := []*models.User{ - {Id: 1, Username: "user1", Role: models.RoleAdmin}, - {Id: 2, Username: "user2", Role: models.RoleStudent}, - } - totalCount := int32(2) - - mock.ExpectQuery(repository.ListUsersQuery). - WithArgs(filters.PageSize, filters.Offset()). - WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}). - AddRow(expectedUsers[0].Id, expectedUsers[0].Username, expectedUsers[0].Role). - AddRow(expectedUsers[1].Id, expectedUsers[1].Username, expectedUsers[1].Role)) - - mock.ExpectQuery(repository.CountUsersQuery). - WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount)) - - result, err := repo.ListUsers(ctx, db, filters) - assert.NoError(t, err) - assert.Equal(t, expectedUsers, result.Users) - assert.Equal(t, models.Pagination{Total: 1, Page: 1}, result.Pagination) - }) -} diff --git a/internal/users/usecase.go b/internal/users/usecase.go deleted file mode 100644 index a132637..0000000 --- a/internal/users/usecase.go +++ /dev/null @@ -1,15 +0,0 @@ -package users - -import ( - "context" - "git.sch9.ru/new_gate/ms-tester/internal/models" -) - -type UseCase interface { - CreateUser(ctx context.Context, user *models.UserCreation) (int32, error) - ReadUserById(ctx context.Context, id int32) (*models.User, error) - ReadUserByUsername(ctx context.Context, username string) (*models.User, error) - UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error - DeleteUser(ctx context.Context, id int32) error - ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error) -} diff --git a/internal/users/usecase/usecase.go b/internal/users/usecase/usecase.go deleted file mode 100644 index 1b28928..0000000 --- a/internal/users/usecase/usecase.go +++ /dev/null @@ -1,164 +0,0 @@ -package usecase - -import ( - "context" - "errors" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/sessions" - "git.sch9.ru/new_gate/ms-tester/internal/users" - "git.sch9.ru/new_gate/ms-tester/pkg" -) - -type UsersUC struct { - sessionRepo sessions.ValkeyRepository - usersRepo users.Repository -} - -func NewUseCase(sessionRepo sessions.ValkeyRepository, usersRepo users.Repository) *UsersUC { - return &UsersUC{ - sessionRepo: sessionRepo, - usersRepo: usersRepo, - } -} - -func (u *UsersUC) CreateUser(ctx context.Context, user *models.UserCreation) (int32, error) { - const op = "UseCase.CreateUser" - - err := user.HashPassword() - if err != nil { - return 0, pkg.Wrap(pkg.ErrBadInput, err, op, "bad password") - } - - id, err := u.usersRepo.CreateUser(ctx, u.usersRepo.DB(), user) - if err != nil { - return 0, pkg.Wrap(nil, err, op, "can't create user") - } - - return id, nil -} - -func (u *UsersUC) ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error) { - const op = "UseCase.ListUsers" - - usersList, err := u.usersRepo.ListUsers(ctx, u.usersRepo.DB(), filters) - if err != nil { - return nil, pkg.Wrap(nil, err, op, "can't list users") - } - - return usersList, nil -} - -func (u *UsersUC) UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error { - const op = "UseCase.UpdateUser" - - tx, err := u.usersRepo.BeginTx(ctx) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot start transaction") - } - - err = u.usersRepo.UpdateUser(ctx, tx, id, update) - if err != nil { - return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot update user") - } - err = u.sessionRepo.DeleteAllSessions(ctx, id) - if err != nil { - return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions") - } - err = tx.Commit() - if err != nil { - return pkg.Wrap(nil, err, op, "cannot commit transaction") - } - - return nil -} - -// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! -func (u *UsersUC) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) { - const op = "UseCase.ReadUserByUsername" - - user, err := u.usersRepo.ReadUserByUsername(ctx, u.usersRepo.DB(), username) - if err != nil { - return nil, pkg.Wrap(nil, err, op, "can't read user by username") - } - return user, nil -} - -func (u *UsersUC) ReadUserById(ctx context.Context, id int32) (*models.User, error) { - const op = "UseCase.ReadUserById" - - user, err := u.usersRepo.ReadUserById(ctx, u.usersRepo.DB(), id) - if err != nil { - return nil, pkg.Wrap(nil, err, op, "can't read user by id") - } - return user, nil -} - -func (u *UsersUC) DeleteUser(ctx context.Context, id int32) error { - const op = "UseCase.DeleteUser" - - tx, err := u.usersRepo.BeginTx(ctx) - if err != nil { - return pkg.Wrap(nil, err, op, "cannot start transaction") - } - - err = u.usersRepo.DeleteUser(ctx, tx, id) - if err != nil { - return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete user") - } - - err = u.sessionRepo.DeleteAllSessions(ctx, id) - if err != nil { - return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions") - } - err = tx.Commit() - if err != nil { - return pkg.Wrap(nil, err, op, "cannot commit transaction") - } - - return nil -} - -/* -func ValidEmail(str string) error { - emailAddress, err := mail.ParseAddress(str) - if err != nil || emailAddress.Address != str { - return errors.New("invalid email") - } - return nil -} - -func ValidUsername(str string) error { - if len(str) < 5 { - return errors.New("too short username") - } - if len(str) > 70 { - return errors.New("too long username") - } - if err := ValidEmail(str); err == nil { - return errors.New("username cannot be an email") - } - return nil -} - -func ValidPassword(str string) error { - if len(str) < 5 { - return errors.New("too short password") - } - if len(str) > 70 { - return errors.New("too long password") - } - return nil -} - -func ValidRole(role models.Role) error { - switch role { - case models.RoleAdmin: - return nil - case models.RoleTeacher: - return nil - case models.RoleStudent: - return nil - } - return errors.New("invalid role") -} -*/ diff --git a/main.go b/main.go index 6f29a89..ce4db72 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,12 @@ package main import ( - "context" "fmt" "git.sch9.ru/new_gate/ms-tester/config" testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1" - "git.sch9.ru/new_gate/ms-tester/internal/auth" - authHandlers "git.sch9.ru/new_gate/ms-tester/internal/auth/delivery/rest" - authUseCase "git.sch9.ru/new_gate/ms-tester/internal/auth/usecase" - "git.sch9.ru/new_gate/ms-tester/internal/contests" - contestsHandlers "git.sch9.ru/new_gate/ms-tester/internal/contests/delivery/rest" - contestsRepository "git.sch9.ru/new_gate/ms-tester/internal/contests/repository" - contestsUseCase "git.sch9.ru/new_gate/ms-tester/internal/contests/usecase" - "git.sch9.ru/new_gate/ms-tester/internal/middleware" - "git.sch9.ru/new_gate/ms-tester/internal/models" - "git.sch9.ru/new_gate/ms-tester/internal/problems" - problemsHandlers "git.sch9.ru/new_gate/ms-tester/internal/problems/delivery/rest" - problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/problems/repository" - problemsUseCase "git.sch9.ru/new_gate/ms-tester/internal/problems/usecase" - sessionsRepository "git.sch9.ru/new_gate/ms-tester/internal/sessions/repository" - sessionsUseCase "git.sch9.ru/new_gate/ms-tester/internal/sessions/usecase" - "git.sch9.ru/new_gate/ms-tester/internal/users" - usersHandlers "git.sch9.ru/new_gate/ms-tester/internal/users/delivery/rest" - usersRepository "git.sch9.ru/new_gate/ms-tester/internal/users/repository" - usersUseCase "git.sch9.ru/new_gate/ms-tester/internal/users/usecase" + "git.sch9.ru/new_gate/ms-tester/internal/tester/delivery/rest" + problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/tester/repository" + testerUseCase "git.sch9.ru/new_gate/ms-tester/internal/tester/usecase" "git.sch9.ru/new_gate/ms-tester/pkg" "github.com/gofiber/fiber/v2" fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" @@ -59,60 +42,20 @@ func main() { defer db.Close() logger.Info("successfully connected to postgres") - logger.Info("connecting to redis") - vk, err := pkg.NewValkeyClient(cfg.RedisDSN) - if err != nil { - logger.Fatal(fmt.Sprintf("error connecting to redis: %s", err.Error())) - } - logger.Info("successfully connected to redis") - - usersRepo := usersRepository.NewRepository(db) - - _, err = usersRepo.CreateUser(context.Background(), - usersRepo.DB(), &models.UserCreation{ - Username: cfg.AdminUsername, - Password: cfg.AdminPassword, - Role: models.RoleAdmin, - }) - if err != nil { - logger.Error(fmt.Sprintf("error creating admin user: %s", err.Error())) - } - - sessionsRepo := sessionsRepository.NewValkeyRepository(vk) - sessionsUC := sessionsUseCase.NewUseCase(sessionsRepo, cfg) - - usersUC := usersUseCase.NewUseCase(sessionsRepo, usersRepo) - - authUC := authUseCase.NewUseCase(usersUC, sessionsUC) - pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc) - problemsRepo := problemsRepository.NewRepository(db) - problemsUC := problemsUseCase.NewUseCase(problemsRepo, pandocClient) + problemRepo := problemsRepository.NewProblemRepository(db) + problemUC := testerUseCase.NewProblemUseCase(problemRepo, pandocClient) - contestsRepo := contestsRepository.NewRepository(db) - contestsUC := contestsUseCase.NewContestUseCase(contestsRepo) + contestRepo := problemsRepository.NewContestRepository(db) + contestUC := testerUseCase.NewContestUseCase(contestRepo) server := fiber.New() - type MergedHandlers struct { - users.UsersHandlers - auth.AuthHandlers - contests.ContestsHandlers - problems.ProblemsHandlers - } - - merged := MergedHandlers{ - usersHandlers.NewHandlers(usersUC), - authHandlers.NewHandlers(authUC, cfg.JWTSecret), - contestsHandlers.NewHandlers(problemsUC, contestsUC, cfg.JWTSecret), - problemsHandlers.NewHandlers(problemsUC), - } - - testerv1.RegisterHandlersWithOptions(server, merged, testerv1.FiberServerOptions{ + testerv1.RegisterHandlersWithOptions(server, rest.NewTesterHandlers(problemUC, contestUC), testerv1.FiberServerOptions{ Middlewares: []testerv1.MiddlewareFunc{ fiberlogger.New(), - middleware.AuthMiddleware(cfg.JWTSecret, sessionsUC), + rest.AuthMiddleware(cfg.JWTSecret), //rest.AuthMiddleware(cfg.JWTSecret, userUC), //cors.New(cors.Config{ // AllowOrigins: "http://localhost:3000", diff --git a/migrations/20240727123308_initial.sql b/migrations/20240727123308_initial.sql index f4adc18..87a50ad 100644 --- a/migrations/20240727123308_initial.sql +++ b/migrations/20240727123308_initial.sql @@ -15,38 +15,18 @@ $$ DECLARE max_on_contest_tasks_amount integer := 50; BEGIN - IF (SELECT count(*) - FROM tasks - WHERE contest_id = NEW.contest_id) >= ( - max_on_contest_tasks_amount - ) THEN - RAISE EXCEPTION 'Exceeded max tasks for this contest'; - END IF; + IF ( + SELECT count(*) FROM tasks + WHERE contest_id = NEW.contest_id + ) >= ( + max_on_contest_tasks_amount + ) THEN + RAISE EXCEPTION 'Exceeded max tasks for this contest'; +END IF; RETURN NEW; END; $$; -CREATE TABLE IF NOT EXISTS users -( - id serial NOT NULL, - username varchar(70) UNIQUE NOT NULL, - hashed_pwd varchar(60) NOT NULL, - role integer NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - - PRIMARY KEY (id), - CHECK (length(username) != 0 AND username = lower(username) AND username = trim(username)), - CHECK (length(hashed_pwd) != 0), - CHECK (role BETWEEN 0 AND 2) -); - -CREATE TRIGGER on_users_update - BEFORE UPDATE - ON users - FOR EACH ROW -EXECUTE PROCEDURE updated_at_update(); - CREATE TABLE IF NOT EXISTS problems ( id serial NOT NULL, @@ -112,8 +92,7 @@ CREATE TABLE IF NOT EXISTS tasks ); CREATE TRIGGER max_tasks_on_contest_check - BEFORE INSERT - ON tasks + BEFORE INSERT ON tasks FOR EACH STATEMENT EXECUTE FUNCTION check_max_tasks(); @@ -126,8 +105,8 @@ EXECUTE PROCEDURE updated_at_update(); CREATE TABLE IF NOT EXISTS participants ( id serial NOT NULL, - user_id integer NOT NULL REFERENCES users (id) ON DELETE CASCADE, - contest_id integer NOT NULL REFERENCES contests (id) ON DELETE CASCADE, + user_id integer NOT NULL, + contest_id integer NOT NULL REFERENCES contests (id), name varchar(64) NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), @@ -142,6 +121,7 @@ CREATE TRIGGER on_participants_update FOR EACH ROW EXECUTE PROCEDURE updated_at_update(); + CREATE TABLE IF NOT EXISTS solutions ( id serial NOT NULL, @@ -164,6 +144,7 @@ CREATE TRIGGER on_solutions_update ON solutions FOR EACH ROW EXECUTE PROCEDURE updated_at_update(); + -- +goose StatementEnd -- +goose Down @@ -179,8 +160,6 @@ DROP TRIGGER IF EXISTS on_problems_update ON problems; DROP TABLE IF EXISTS problems; DROP TRIGGER IF EXISTS on_contests_update ON contests; DROP TABLE IF EXISTS contests; -DROP TRIGGER IF EXISTS on_users_update ON users; -DROP TABLE IF EXISTS users; -DROP FUNCTION IF EXISTS updated_at_update(); +DROP FUNCTION IF EXISTS updated_at_update(); DROP FUNCTION IF EXISTS check_max_tasks(); -- +goose StatementEnd \ No newline at end of file diff --git a/pkg/errors.go b/pkg/errors.go index 0169a09..6efa146 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -1,11 +1,8 @@ package pkg import ( - "database/sql" "errors" "fmt" - "github.com/jackc/pgerrcode" - "github.com/jackc/pgx/v5/pgconn" "net/http" ) @@ -38,21 +35,3 @@ func ToREST(err error) int { return http.StatusInternalServerError } - -func HandlePgErr(err error, op string) error { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { - return Wrap(ErrBadInput, err, op, pgErr.Message) - } - if pgerrcode.IsNoData(pgErr.Code) { - return Wrap(ErrNotFound, err, op, pgErr.Message) - } - } - - if errors.Is(err, sql.ErrNoRows) { - return Wrap(ErrNotFound, err, op, "no rows found") - } - - return Wrap(ErrUnhandled, err, op, "unexpected error") -}