Compare commits
2 commits
develop
...
feature/GA
Author | SHA1 | Date | |
---|---|---|---|
|
28fa38c930 | ||
|
441af4c6a2 |
72 changed files with 4925 additions and 2378 deletions
1
.gitmodules
vendored
1
.gitmodules
vendored
|
@ -1,3 +1,4 @@
|
|||
[submodule "proto"]
|
||||
path = contracts
|
||||
url = https://git.sch9.ru/new_gate/contracts
|
||||
update = rebase
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
Env string `env:"ENV" env-default:"prod"`
|
||||
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
|
||||
Pandoc string `env:"PANDOC" required:"true"`
|
||||
Address string `env:"ADDRESS" required:"true"`
|
||||
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
|
||||
JWTSecret string `env:"JWT_SECRET" 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"`
|
||||
|
||||
//RabbitDSN string `env:"RABBIT_DSN" required:"true"`
|
||||
//InstanceName string `env:"INSTANCE_NAME" required:"true"`
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 89b4b19ae383c17665f0c3176e3d4122e90e46ec
|
||||
Subproject commit 91b7c6804671bcd533ab09939f21d27aacd5f793
|
61
go.mod
61
go.mod
|
@ -4,75 +4,56 @@ 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.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
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.47
|
||||
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
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // 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-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/go-sql-driver/mysql v1.9.1 // 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-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // 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/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/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // 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/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
|
||||
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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
180
go.sum
180
go.sum
|
@ -5,98 +5,62 @@ 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/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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
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/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.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/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/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-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/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/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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
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/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=
|
||||
|
@ -110,115 +74,61 @@ 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.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/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/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/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/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/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/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=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
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.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/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/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.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=
|
||||
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=
|
||||
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=
|
||||
|
@ -227,5 +137,3 @@ 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=
|
||||
|
|
13
internal/auth/delivery.go
Normal file
13
internal/auth/delivery.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
}
|
160
internal/auth/delivery/rest/handlers.go
Normal file
160
internal/auth/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
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
|
||||
}
|
14
internal/auth/usecase.go
Normal file
14
internal/auth/usecase.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
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)
|
||||
}
|
70
internal/auth/usecase/usecase.go
Normal file
70
internal/auth/usecase/usecase.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
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")
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package tester
|
||||
package contests
|
||||
|
||||
import (
|
||||
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type Handlers interface {
|
||||
type ContestsHandlers interface {
|
||||
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
|
||||
CreateContest(c *fiber.Ctx) error
|
||||
DeleteContest(c *fiber.Ctx, id int32) error
|
||||
|
@ -14,18 +14,12 @@ type Handlers 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
|
||||
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
|
||||
CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) 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
|
||||
AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error
|
||||
CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error
|
||||
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
|
||||
GetTask(c *fiber.Ctx, id int32) error
|
||||
}
|
202
internal/contests/delivery/rest/contests_handlers.go
Normal file
202
internal/contests/delivery/rest/contests_handlers.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
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))
|
||||
}
|
||||
}
|
202
internal/contests/delivery/rest/dto.go
Normal file
202
internal/contests/delivery/rest/dto.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
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,
|
||||
}
|
||||
}
|
71
internal/contests/delivery/rest/monitor_handlers.go
Normal file
71
internal/contests/delivery/rest/monitor_handlers.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
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))
|
||||
}
|
||||
}
|
116
internal/contests/delivery/rest/participants_handlers.go
Normal file
116
internal/contests/delivery/rest/participants_handlers.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
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))
|
||||
}
|
||||
}
|
148
internal/contests/delivery/rest/solutions_handlers.go
Normal file
148
internal/contests/delivery/rest/solutions_handlers.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
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))
|
||||
}
|
||||
}
|
105
internal/contests/delivery/rest/tasks_handlers.go
Normal file
105
internal/contests/delivery/rest/tasks_handlers.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
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))
|
||||
}
|
||||
}
|
34
internal/contests/pg_repository.go
Normal file
34
internal/contests/pg_repository.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
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)
|
||||
}
|
145
internal/contests/repository/contests_pg_repository.go
Normal file
145
internal/contests/repository/contests_pg_repository.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
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
|
||||
}
|
116
internal/contests/repository/contests_pg_repository_test.go
Normal file
116
internal/contests/repository/contests_pg_repository_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
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
|
||||
}
|
161
internal/contests/repository/monitor_pg_repository.go
Normal file
161
internal/contests/repository/monitor_pg_repository.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
126
internal/contests/repository/participants_pg_repository.go
Normal file
126
internal/contests/repository/participants_pg_repository.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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)
|
||||
})
|
||||
}
|
222
internal/contests/repository/solutions_pg_repository.go
Normal file
222
internal/contests/repository/solutions_pg_repository.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package repository
|
101
internal/contests/repository/tasks_pg_repository.go
Normal file
101
internal/contests/repository/tasks_pg_repository.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
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
|
||||
}
|
51
internal/contests/repository/tasks_pg_repository_test.go
Normal file
51
internal/contests/repository/tasks_pg_repository_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
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)
|
||||
})
|
||||
}
|
34
internal/contests/usecase.go
Normal file
34
internal/contests/usecase.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
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)
|
||||
}
|
39
internal/contests/usecase/contests_usecase.go
Normal file
39
internal/contests/usecase/contests_usecase.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
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)
|
||||
}
|
10
internal/contests/usecase/monitor_usecase.go
Normal file
10
internal/contests/usecase/monitor_usecase.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
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)
|
||||
}
|
34
internal/contests/usecase/participants_usecase.go
Normal file
34
internal/contests/usecase/participants_usecase.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
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)
|
||||
}
|
29
internal/contests/usecase/solutions_usecase.go
Normal file
29
internal/contests/usecase/solutions_usecase.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
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)
|
||||
}
|
22
internal/contests/usecase/tasks_usecase.go
Normal file
22
internal/contests/usecase/tasks_usecase.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
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)
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package rest
|
||||
package middleware
|
||||
|
||||
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"
|
||||
|
@ -12,19 +15,15 @@ const (
|
|||
TokenKey = "token"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string) fiber.Handler {
|
||||
func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) 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()
|
||||
}
|
||||
|
||||
|
@ -36,34 +35,30 @@ func AuthMiddleware(jwtSecret string) 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 = 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))
|
||||
//}
|
||||
_, err = sessionsUC.ReadSession(ctx, token.SessionId)
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrNotFound) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
return c.SendStatus(pkg.ToREST(err))
|
||||
}
|
||||
|
||||
c.Locals(TokenKey, token)
|
||||
return c.Next()
|
|
@ -24,6 +24,8 @@ type ContestsList struct {
|
|||
type ContestsFilter struct {
|
||||
Page int32
|
||||
PageSize int32
|
||||
UserId *int32
|
||||
Order *int32
|
||||
}
|
||||
|
||||
func (f ContestsFilter) Offset() int32 {
|
||||
|
@ -53,3 +55,73 @@ 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"`
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
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"`
|
||||
}
|
|
@ -13,7 +13,6 @@ 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"`
|
||||
|
|
|
@ -1,20 +1,74 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Permissions []grant `json:"permissions"`
|
||||
SessionId string `json:"session_id"`
|
||||
UserId int32 `json:"user_id"`
|
||||
Role Role `json:"role"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
}
|
||||
|
||||
func (j JWT) Valid() error {
|
||||
|
@ -24,139 +78,18 @@ 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 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 Credentials struct {
|
||||
Username string
|
||||
Password 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)
|
||||
}
|
||||
type Device struct {
|
||||
Ip string
|
||||
UseAgent string
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ type Solution struct {
|
|||
type SolutionCreation struct {
|
||||
Solution string
|
||||
TaskId int32
|
||||
UserId int32
|
||||
ParticipantId int32
|
||||
Language int32
|
||||
Penalty int32
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
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"`
|
||||
}
|
71
internal/models/user.go
Normal file
71
internal/models/user.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
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
|
||||
}
|
15
internal/problems/delivery.go
Normal file
15
internal/problems/delivery.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
}
|
261
internal/problems/delivery/rest/handlers.go
Normal file
261
internal/problems/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,261 @@
|
|||
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,
|
||||
}
|
||||
}
|
32
internal/problems/pg_repository.go
Normal file
32
internal/problems/pg_repository.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
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
|
||||
}
|
175
internal/problems/repository/pg_repository.go
Normal file
175
internal/problems/repository/pg_repository.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
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
|
||||
}
|
293
internal/problems/repository/pg_repository_test.go
Normal file
293
internal/problems/repository/pg_repository_test.go
Normal file
|
@ -0,0 +1,293 @@
|
|||
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
|
||||
}
|
15
internal/problems/usecase.go
Normal file
15
internal/problems/usecase.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
}
|
|
@ -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 ProblemUseCase struct {
|
||||
problemRepo tester.ProblemPostgresRepository
|
||||
type UseCase struct {
|
||||
problemRepo problems.Repository
|
||||
pandocClient pkg.PandocClient
|
||||
}
|
||||
|
||||
func NewProblemUseCase(
|
||||
problemRepo tester.ProblemPostgresRepository,
|
||||
func NewUseCase(
|
||||
problemRepo problems.Repository,
|
||||
pandocClient pkg.PandocClient,
|
||||
) *ProblemUseCase {
|
||||
return &ProblemUseCase{
|
||||
) *UseCase {
|
||||
return &UseCase{
|
||||
problemRepo: problemRepo,
|
||||
pandocClient: pandocClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
|
||||
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.ReadProblemById(ctx, u.problemRepo.DB(), id)
|
||||
func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) {
|
||||
return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error {
|
||||
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
|
||||
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
|
||||
}
|
||||
|
||||
func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error {
|
||||
if isEmpty(problemUpdate) {
|
||||
func (u *UseCase) 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 *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpd
|
|||
return err
|
||||
}
|
||||
|
||||
problem, err := u.problemRepo.ReadProblemById(ctx, tx, id)
|
||||
problem, err := u.problemRepo.GetProblemById(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 *ProblemUseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
|
||||
|
||||
locale := "russian"
|
||||
defaultLocale := "english"
|
||||
|
@ -185,7 +185,7 @@ func (u *ProblemUseCase) UploadProblem(ctx context.Context, id int32, data []byt
|
|||
localeProperties.MemoryLimit /= 1024 * 1024
|
||||
defaultProperties.MemoryLimit /= 1024 * 1024
|
||||
|
||||
var problemUpdate models.ProblemUpdate
|
||||
problemUpdate := &models.ProblemUpdate{}
|
||||
if localeProblem != "" {
|
||||
problemUpdate.Legend = &localeProblem
|
||||
problemUpdate.Title = &localeProperties.Title
|
183
internal/sessions/repository/valkey_repository.go
Normal file
183
internal/sessions/repository/valkey_repository.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
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
|
||||
}
|
312
internal/sessions/repository/valkey_repository_test.go
Normal file
312
internal/sessions/repository/valkey_repository_test.go
Normal file
|
@ -0,0 +1,312 @@
|
|||
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)
|
||||
})
|
||||
}
|
14
internal/sessions/usecase.go
Normal file
14
internal/sessions/usecase.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
}
|
78
internal/sessions/usecase/usecase.go
Normal file
78
internal/sessions/usecase/usecase.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
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
|
||||
}
|
14
internal/sessions/valkey_repository.go
Normal file
14
internal/sessions/valkey_repository.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
}
|
|
@ -1,622 +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/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,
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
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")
|
||||
}
|
|
@ -1,690 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
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)
|
||||
})
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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)
|
||||
})
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
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)
|
||||
}
|
14
internal/users/delivery.go
Normal file
14
internal/users/delivery.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
||||
}
|
204
internal/users/delivery/rest/handlers.go
Normal file
204
internal/users/delivery/rest/handlers.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
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,
|
||||
}
|
||||
}
|
33
internal/users/pg_repository.go
Normal file
33
internal/users/pg_repository.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
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)
|
||||
}
|
156
internal/users/repository/pg_repository.go
Normal file
156
internal/users/repository/pg_repository.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
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
|
||||
}
|
222
internal/users/repository/pg_repository_test.go
Normal file
222
internal/users/repository/pg_repository_test.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
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)
|
||||
})
|
||||
}
|
15
internal/users/usecase.go
Normal file
15
internal/users/usecase.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
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)
|
||||
}
|
164
internal/users/usecase/usecase.go
Normal file
164
internal/users/usecase/usecase.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
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")
|
||||
}
|
||||
*/
|
75
main.go
75
main.go
|
@ -1,12 +1,29 @@
|
|||
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/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/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/pkg"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
|
||||
|
@ -42,20 +59,60 @@ 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)
|
||||
|
||||
problemRepo := problemsRepository.NewProblemRepository(db)
|
||||
problemUC := testerUseCase.NewProblemUseCase(problemRepo, pandocClient)
|
||||
problemsRepo := problemsRepository.NewRepository(db)
|
||||
problemsUC := problemsUseCase.NewUseCase(problemsRepo, pandocClient)
|
||||
|
||||
contestRepo := problemsRepository.NewContestRepository(db)
|
||||
contestUC := testerUseCase.NewContestUseCase(contestRepo)
|
||||
contestsRepo := contestsRepository.NewRepository(db)
|
||||
contestsUC := contestsUseCase.NewContestUseCase(contestsRepo)
|
||||
|
||||
server := fiber.New()
|
||||
|
||||
testerv1.RegisterHandlersWithOptions(server, rest.NewTesterHandlers(problemUC, contestUC), testerv1.FiberServerOptions{
|
||||
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{
|
||||
Middlewares: []testerv1.MiddlewareFunc{
|
||||
fiberlogger.New(),
|
||||
rest.AuthMiddleware(cfg.JWTSecret),
|
||||
middleware.AuthMiddleware(cfg.JWTSecret, sessionsUC),
|
||||
//rest.AuthMiddleware(cfg.JWTSecret, userUC),
|
||||
//cors.New(cors.Config{
|
||||
// AllowOrigins: "http://localhost:3000",
|
||||
|
|
|
@ -15,18 +15,38 @@ $$
|
|||
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,
|
||||
|
@ -92,7 +112,8 @@ 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();
|
||||
|
||||
|
@ -105,8 +126,8 @@ EXECUTE PROCEDURE updated_at_update();
|
|||
CREATE TABLE IF NOT EXISTS participants
|
||||
(
|
||||
id serial NOT NULL,
|
||||
user_id integer NOT NULL,
|
||||
contest_id integer NOT NULL REFERENCES contests (id),
|
||||
user_id integer NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
contest_id integer NOT NULL REFERENCES contests (id) ON DELETE CASCADE,
|
||||
name varchar(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
@ -121,7 +142,6 @@ CREATE TRIGGER on_participants_update
|
|||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS solutions
|
||||
(
|
||||
id serial NOT NULL,
|
||||
|
@ -144,7 +164,6 @@ CREATE TRIGGER on_solutions_update
|
|||
ON solutions
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE updated_at_update();
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
|
@ -160,6 +179,8 @@ 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 FUNCTION IF EXISTS updated_at_update();
|
||||
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 check_max_tasks();
|
||||
-- +goose StatementEnd
|
|
@ -1,8 +1,11 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
@ -35,3 +38,21 @@ 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")
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue