Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Vyacheslav1557
28fa38c930 fix: fix UploadProblem data format 2025-04-23 16:02:40 +05:00
Vyacheslav1557
441af4c6a2 feat: merge auth&tester 2025-04-22 20:44:52 +05:00
72 changed files with 4925 additions and 2378 deletions

1
.gitmodules vendored
View file

@ -1,3 +1,4 @@
[submodule "proto"]
path = contracts
url = https://git.sch9.ru/new_gate/contracts
update = rebase

View file

@ -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
View file

@ -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
View file

@ -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
View 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
}

View 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
View 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)
}

View 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")
}

View file

@ -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
}

View 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))
}
}

View 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,
}
}

View 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))
}
}

View 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))
}
}

View 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))
}
}

View 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))
}
}

View 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)
}

View 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
}

View 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
}

View 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
}

View file

@ -0,0 +1 @@
package repository

View 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
}

View 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_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)
})
}

View 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
}

View file

@ -0,0 +1 @@
package repository

View 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
}

View 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)
})
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View file

@ -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()

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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"`

View file

@ -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
}

View file

@ -31,6 +31,7 @@ type Solution struct {
type SolutionCreation struct {
Solution string
TaskId int32
UserId int32
ParticipantId int32
Language int32
Penalty int32

View file

@ -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
View 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
}

View 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
}

View 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,
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View file

@ -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

View 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
}

View 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)
})
}

View 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
}

View 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
}

View 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
}

View file

@ -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,
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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)
}

View file

@ -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)
}

View 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
}

View 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,
}
}

View 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)
}

View 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
}

View 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
View 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)
}

View 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
View file

@ -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",

View file

@ -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

View file

@ -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")
}