Compare commits

..

No commits in common. "develop" and "v0.0.0" have entirely different histories.

110 changed files with 2396 additions and 6217 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
.env
.idea
/pkg/go/gen
/ms-tester

3
.gitmodules vendored
View file

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

View file

@ -1,9 +1,9 @@
tag = latest
gen:
@oapi-codegen --config=config.yaml ./contracts/tester/v1/openapi.yaml
dev: gen
@buf generate
dev:
@make gen
@go run main.go
build: gen
@docker build . -t ms-tester:${tag}
@#docker push ms-tester:${tag}
build:
@make gen
# TODO: build dockerfile

108
README.md
View file

@ -1,108 +0,0 @@
# ms-tester
`ms-tester` is a microservice designed for managing programming competitions. It provides backend functionality for handling problems, contests, participants, and their submissions. The service is developed in Go. PostgreSQL serves as the relational database. Pandoc is used to convert problem statements from LaTeX to HTML.
For understanding the architecture, see the [documentation](https://git.sch9.ru/new_gate/docs).
### Prerequisites
Before you begin, ensure you have the following dependencies installed:
* **Docker** and **Docker Compose**: To run PostgreSQL, Pandoc.
* **Goose**: For applying database migrations (`go install github.com/pressly/goose/v3/cmd/goose@latest`).
* **oapi-codegen**: For generating OpenAPI code (`go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest`).
### 1. Running Dependencies
You can run PostgreSQL and Pandoc using Docker Compose, for example:
```yaml
version: '3.8'
services:
pandoc:
image: pandoc/latex
ports:
- "4000:3030" # Exposes Pandoc server on port 4000 locally
command: "server" # Runs Pandoc in server mode
postgres:
image: postgres:14.1-alpine # Uses PostgreSQL 14.1 Alpine image
restart: always # Ensures the container restarts if it stops
environment:
POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: supersecretpassword # Default password (change for production!)
POSTGRES_DB: postgres # Default database name
ports:
- '5432:5432' # Exposes PostgreSQL on the standard port 5432
volumes:
- ./postgres-data:/var/lib/postgresql/data # Persists database data locally
healthcheck:
test: pg_isready -U postgres -d postgres # Command to check if PostgreSQL is ready
interval: 10s # Check every 10 seconds
timeout: 3s # Wait 3 seconds for the check to respond
retries: 5 # Try 5 times before marking as unhealthy
volumes:
postgres-data: # Defines the named volume for data persistence
```
Start the services in detached mode:
```bash
docker-compose up -d
```
### 2. Configuration
The application uses environment variables for configuration. Create a `.env` file in the project root. The minimum required variables are:
```dotenv
# Environment type (development or production)
ENV=dev # or prod
# Address of the running Pandoc service
PANDOC=http://localhost:4000
# Address and port where the ms-tester service will listen
ADDRESS=localhost:8080
# PostgreSQL connection string (Data Source Name)
# Format: postgres://user:password@host:port/database?sslmode=disable
POSTGRES_DSN=postgres://username:supersecretpassword@localhost:5432/db_name?sslmode=disable
# Secret key for signing and verifying JWT tokens
JWT_SECRET=your_super_secret_jwt_key
```
**Important:** Replace `supersecretpassword` and `your_super_secret_jwt_key` with secure, unique values, especially for a production environment.
### 3. Database Migrations
The project uses `goose` to manage the database schema.
1. Ensure `goose` is installed:
```bash
go install github.com/pressly/goose/v3/cmd/goose@latest
```
2. Apply the migrations to the running PostgreSQL database. Make sure the connection string in the command matches the `POSTGRES_DSN` from your `.env` file:
```bash
goose -dir ./migrations postgres "postgres://postgres:supersecretpassword@localhost:5432/postgres?sslmode=disable" up
```
### 4. OpenAPI Code Generation
The project uses OpenAPI to define its API. Go code for handlers and models is generated based on this specification using `oapi-codegen`.
Run the generation command:
```bash
make gen
```
### 5. Running the Application
Start the `ms-tester` service:
```bash
go run ./main.go
```
After starting, the service will be available at the address specified in the `ADDRESS` variable in your `.env` file (e.g., `http://localhost:8080`).

12
buf.gen.yaml Normal file
View file

@ -0,0 +1,12 @@
version: v1
managed:
enabled: true
go_package_prefix:
default: git.sch9.ru/new_gate/ms-tester/pkg/go/gen
plugins:
- name: go
out: pkg/go/gen
opt: paths=source_relative
- name: go-grpc
out: pkg/go/gen
opt: paths=source_relative

7
buf.yaml Normal file
View file

@ -0,0 +1,7 @@
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT

View file

@ -1,5 +0,0 @@
package: testerv1
generate:
fiber-server: true
models: true
output: ./contracts/tester/v1/tester.go

View file

@ -1,22 +0,0 @@
package config
type Config struct {
Env string `env:"ENV" env-default:"prod"`
Address string `env:"ADDRESS" required:"true"`
Pandoc string `env:"PANDOC" required:"true"`
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
RedisDSN string `env:"REDIS_DSN" required:"true"`
JWTSecret string `env:"JWT_SECRET" required:"true"`
AdminUsername string `env:"ADMIN_USERNAME" env-default:"admin"`
AdminPassword string `env:"ADMIN_PASSWORD" env-default:"admin"`
//RabbitDSN string `env:"RABBIT_DSN" required:"true"`
//InstanceName string `env:"INSTANCE_NAME" required:"true"`
//RQueueName string `env:"R_QUEUE_NAME" required:"true"`
//TQueueName string `env:"T_QUEUE_NAME" required:"true"`
//NQueueName string `env:"N_QUEUE_NAME" required:"true"`
}

@ -1 +0,0 @@
Subproject commit 91b7c6804671bcd533ab09939f21d27aacd5f793

83
go.mod
View file

@ -1,59 +1,60 @@
module git.sch9.ru/new_gate/ms-tester
go 1.23.6
go 1.21.3
toolchain go1.22.0
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/squirrel v1.5.4
github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/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/rabbitmq/amqp091-go v1.10.0
github.com/stretchr/testify v1.10.0
github.com/valkey-io/valkey-go v1.0.57
github.com/valkey-io/valkey-go/mock v1.0.57
go.uber.org/mock v0.5.1
github.com/open-policy-agent/opa v0.67.1
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.36.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
)
require (
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // 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/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // 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/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // 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/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/jackc/pgx/v5 v5.7.4
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/jackc/pgx/v5 v5.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

237
go.sum
View file

@ -1,139 +1,190 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/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/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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
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 v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
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/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/open-policy-agent/opa v0.67.1 h1:rzy26J6g1X+CKknAcx0Vfbt41KqjuSzx4E0A8DAZf3E=
github.com/open-policy-agent/opa v0.67.1/go.mod h1:aqKlHc8E2VAAylYE9x09zJYr/fYzGX+JKne89UGqFzk=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
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/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
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.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valkey-io/valkey-go v1.0.57 h1:rMpREZ7kvWwv9vHkB1WTpI9rX4dQHsvPHimSWenScvI=
github.com/valkey-io/valkey-go v1.0.57/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM=
github.com/valkey-io/valkey-go/mock v1.0.57 h1:ft06MuqCCKlob/R5dzUv4zNnNu+GaqElalApOFS5Fc4=
github.com/valkey-io/valkey-go/mock v1.0.57/go.mod h1:VDiXrmHdRCz/UT4xzMkfQEc5iHa7naDpqsZ+lotmJE8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
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/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

View file

@ -1,13 +0,0 @@
package auth
import (
"github.com/gofiber/fiber/v2"
)
type AuthHandlers interface {
ListSessions(c *fiber.Ctx) error
Terminate(c *fiber.Ctx) error
Login(c *fiber.Ctx) error
Logout(c *fiber.Ctx) error
Refresh(c *fiber.Ctx) error
}

View file

@ -1,160 +0,0 @@
package rest
import (
"context"
"encoding/base64"
"git.sch9.ru/new_gate/ms-tester/internal/auth"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v4"
"strings"
"time"
)
type Handlers struct {
authUC auth.UseCase
jwtSecret string
}
func NewHandlers(authUC auth.UseCase, jwtSecret string) *Handlers {
return &Handlers{
authUC: authUC,
jwtSecret: jwtSecret,
}
}
const (
sessionKey = "session"
)
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
const op = "sessionFromCtx"
session, ok := ctx.Value(sessionKey).(*models.Session)
if !ok {
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
}
return session, nil
}
func (h *Handlers) ListSessions(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNotImplemented)
}
func (h *Handlers) Terminate(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
err = h.authUC.Terminate(ctx, session.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
}
func (h *Handlers) Login(c *fiber.Ctx) error {
authHeader := c.Get("Authorization", "")
if authHeader == "" {
return c.SendStatus(fiber.StatusUnauthorized)
}
username, pwd, err := parseBasicAuth(authHeader)
if err != nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
credentials := &models.Credentials{
Username: strings.ToLower(username),
Password: pwd,
}
device := &models.Device{
Ip: c.IP(),
UseAgent: c.Get("User-Agent", ""),
}
ctx := c.Context()
session, err := h.authUC.Login(ctx, credentials, device)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{
SessionId: session.Id,
UserId: session.UserId,
Role: session.Role,
IssuedAt: time.Now().Unix(),
})
token, err := claims.SignedString([]byte(h.jwtSecret))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
c.Set("Authorization", "Bearer "+token)
return c.SendStatus(fiber.StatusOK)
}
func (h *Handlers) Logout(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
err = h.authUC.Logout(c.Context(), session.Id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
}
func (h *Handlers) Refresh(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
err = h.authUC.Refresh(c.Context(), session.Id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
}
func parseBasicAuth(header string) (string, string, error) {
const (
op = "parseBasicAuth"
msg = "invalid auth header"
)
authParts := strings.Split(header, " ")
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" {
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
}
decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1])
if err != nil {
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
}
authParts = strings.Split(string(decodedAuth), ":")
if len(authParts) != 2 {
return "", "", pkg.Wrap(pkg.ErrUnauthenticated, nil, op, msg)
}
return authParts[0], authParts[1], nil
}

View file

@ -1,14 +0,0 @@
package auth
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error)
Refresh(ctx context.Context, sessionId string) error
Logout(ctx context.Context, sessionId string) error
Terminate(ctx context.Context, userId int32) error
ListSessions(ctx context.Context, userId int32) ([]*models.Session, error)
}

View file

@ -1,70 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
"git.sch9.ru/new_gate/ms-tester/internal/users"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/google/uuid"
"time"
)
type UseCase struct {
usersUC users.UseCase
sessionsUC sessions.UseCase
}
func NewUseCase(usersUC users.UseCase, sessionsUC sessions.UseCase) *UseCase {
return &UseCase{
usersUC: usersUC,
sessionsUC: sessionsUC,
}
}
func (uc *UseCase) Login(ctx context.Context, credentials *models.Credentials, device *models.Device) (*models.Session, error) {
const op = "UseCase.Login"
user, err := uc.usersUC.ReadUserByUsername(ctx, credentials.Username)
if err != nil {
return nil, err
}
if !user.IsSamePwd(credentials.Password) {
return nil, pkg.Wrap(pkg.ErrNotFound, nil, op, "password mismatch")
}
session := &models.Session{
Id: uuid.NewString(),
UserId: user.Id,
Role: user.Role,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(40 * time.Minute),
UserAgent: device.UseAgent,
Ip: device.Ip,
}
err = uc.sessionsUC.CreateSession(ctx, session)
if err != nil {
return nil, err
}
return session, nil
}
func (uc *UseCase) Logout(ctx context.Context, sessionId string) error {
return uc.sessionsUC.DeleteSession(ctx, sessionId)
}
func (uc *UseCase) Refresh(ctx context.Context, sessionId string) error {
return uc.sessionsUC.UpdateSession(ctx, sessionId)
}
func (uc *UseCase) Terminate(ctx context.Context, userId int32) error {
return uc.sessionsUC.DeleteAllSessions(ctx, userId)
}
func (uc *UseCase) ListSessions(ctx context.Context, userId int32) ([]*models.Session, error) {
// TODO: implement me
panic("implement me")
}

View file

@ -1,25 +0,0 @@
package contests
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"github.com/gofiber/fiber/v2"
)
type ContestsHandlers interface {
ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error
CreateContest(c *fiber.Ctx) error
DeleteContest(c *fiber.Ctx, id int32) error
GetContest(c *fiber.Ctx, id int32) error
UpdateContest(c *fiber.Ctx, id int32) error
DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error
ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error
CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error
GetSolution(c *fiber.Ctx, id int32) error
DeleteTask(c *fiber.Ctx, id int32) error
CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error
GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error
GetTask(c *fiber.Ctx, id int32) error
}

View file

@ -1,202 +0,0 @@
package rest
import (
"context"
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/contests"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/problems"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
type Handlers struct {
problemsUC problems.UseCase
contestsUC contests.UseCase
jwtSecret string
}
func NewHandlers(problemsUC problems.UseCase, contestsUC contests.UseCase, jwtSecret string) *Handlers {
return &Handlers{
problemsUC: problemsUC,
contestsUC: contestsUC,
jwtSecret: jwtSecret,
}
}
const (
sessionKey = "session"
)
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
const op = "sessionFromCtx"
session, ok := ctx.Value(sessionKey).(*models.Session)
if !ok {
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
}
return session, nil
}
func (h *Handlers) CreateContest(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateContest(ctx, "Название контеста")
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(&testerv1.CreateContestResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contest, err := h.contestsUC.GetContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
solutions := make([]*models.SolutionsListItem, 0)
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
if err == nil { // Admin or Teacher may not participate in contest
solutions, err = h.contestsUC.GetBestSolutions(ctx, id, participantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
}
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
case models.RoleStudent:
contest, err := h.contestsUC.GetContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
participantId, err := h.contestsUC.GetParticipantId(ctx, contest.Id, session.UserId)
solutions, err := h.contestsUC.GetBestSolutions(c.Context(), id, participantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetContestResponseDTO(contest, tasks, solutions))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req testerv1.UpdateContestRequest
err := c.BodyParser(&req)
if err != nil {
return err
}
err = h.contestsUC.UpdateContest(ctx, id, models.ContestUpdate{
Title: req.Title,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteContest(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteContest(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListContests(c *fiber.Ctx, params testerv1.ListContestsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
filter := models.ContestsFilter{
Page: params.Page,
PageSize: params.PageSize,
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contestsList, err := h.contestsUC.ListContests(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListContestsResponseDTO(contestsList))
case models.RoleStudent:
filter.UserId = &session.UserId
contestsList, err := h.contestsUC.ListContests(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListContestsResponseDTO(contestsList))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -1,202 +0,0 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func GetContestResponseDTO(contest *models.Contest,
tasks []*models.TasksListItem,
solutions []*models.SolutionsListItem) *testerv1.GetContestResponse {
m := make(map[int32]*models.SolutionsListItem)
for i := 0; i < len(solutions); i++ {
m[solutions[i].TaskPosition] = solutions[i]
}
resp := testerv1.GetContestResponse{
Contest: ContestDTO(*contest),
Tasks: make([]struct {
Solution testerv1.SolutionsListItem `json:"solution"`
Task testerv1.TasksListItem `json:"task"`
}, len(tasks)),
}
for i, task := range tasks {
solution := testerv1.SolutionsListItem{}
if sol, ok := m[task.Position]; ok {
solution = SolutionsListItemDTO(*sol)
}
resp.Tasks[i] = struct {
Solution testerv1.SolutionsListItem `json:"solution"`
Task testerv1.TasksListItem `json:"task"`
}{
Solution: solution,
Task: TasksListItemDTO(*task),
}
}
return &resp
}
func ListContestsResponseDTO(contestsList *models.ContestsList) *testerv1.ListContestsResponse {
resp := testerv1.ListContestsResponse{
Contests: make([]testerv1.ContestsListItem, len(contestsList.Contests)),
Pagination: PaginationDTO(contestsList.Pagination),
}
for i, contest := range contestsList.Contests {
resp.Contests[i] = ContestsListItemDTO(*contest)
}
return &resp
}
func ListSolutionsResponseDTO(solutionsList *models.SolutionsList) *testerv1.ListSolutionsResponse {
resp := testerv1.ListSolutionsResponse{
Solutions: make([]testerv1.SolutionsListItem, len(solutionsList.Solutions)),
Pagination: PaginationDTO(solutionsList.Pagination),
}
for i, solution := range solutionsList.Solutions {
resp.Solutions[i] = SolutionsListItemDTO(*solution)
}
return &resp
}
func GetTaskResponseDTO(contest *models.Contest, tasks []*models.TasksListItem, task *models.Task) *testerv1.GetTaskResponse {
resp := testerv1.GetTaskResponse{
Contest: ContestDTO(*contest),
Tasks: make([]testerv1.TasksListItem, len(tasks)),
Task: *TaskDTO(task),
}
for i, t := range tasks {
resp.Tasks[i] = TasksListItemDTO(*t)
}
return &resp
}
func PaginationDTO(p models.Pagination) testerv1.Pagination {
return testerv1.Pagination{
Page: p.Page,
Total: p.Total,
}
}
func ContestDTO(c models.Contest) testerv1.Contest {
return testerv1.Contest{
Id: c.Id,
Title: c.Title,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func ContestsListItemDTO(c models.ContestsListItem) testerv1.ContestsListItem {
return testerv1.ContestsListItem{
Id: c.Id,
Title: c.Title,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func TasksListItemDTO(t models.TasksListItem) testerv1.TasksListItem {
return testerv1.TasksListItem{
Id: t.Id,
Position: t.Position,
Title: t.Title,
MemoryLimit: t.MemoryLimit,
ProblemId: t.ProblemId,
TimeLimit: t.TimeLimit,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
func TaskDTO(t *models.Task) *testerv1.Task {
return &testerv1.Task{
Id: t.Id,
Title: t.Title,
MemoryLimit: t.MemoryLimit,
TimeLimit: t.TimeLimit,
InputFormatHtml: t.InputFormatHtml,
LegendHtml: t.LegendHtml,
NotesHtml: t.NotesHtml,
OutputFormatHtml: t.OutputFormatHtml,
Position: t.Position,
ScoringHtml: t.ScoringHtml,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
func ParticipantsListItemDTO(p models.ParticipantsListItem) testerv1.ParticipantsListItem {
return testerv1.ParticipantsListItem{
Id: p.Id,
UserId: p.UserId,
Name: p.Name,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}
func SolutionsListItemDTO(s models.SolutionsListItem) testerv1.SolutionsListItem {
return testerv1.SolutionsListItem{
Id: s.Id,
ParticipantId: s.ParticipantId,
ParticipantName: s.ParticipantName,
State: s.State,
Score: s.Score,
Penalty: s.Penalty,
TimeStat: s.TimeStat,
MemoryStat: s.MemoryStat,
Language: s.Language,
TaskId: s.TaskId,
TaskPosition: s.TaskPosition,
TaskTitle: s.TaskTitle,
ContestId: s.ContestId,
ContestTitle: s.ContestTitle,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func SolutionDTO(s models.Solution) testerv1.Solution {
return testerv1.Solution{
Id: s.Id,
ParticipantId: s.ParticipantId,
ParticipantName: s.ParticipantName,
Solution: s.Solution,
State: s.State,
Score: s.Score,
Penalty: s.Penalty,
TimeStat: s.TimeStat,
MemoryStat: s.MemoryStat,
Language: s.Language,
TaskId: s.TaskId,
TaskPosition: s.TaskPosition,
TaskTitle: s.TaskTitle,
ContestId: s.ContestId,
ContestTitle: s.ContestTitle,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}

View file

@ -1,71 +0,0 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) GetMonitor(c *fiber.Ctx, params testerv1.GetMonitorParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
contest, err := h.contestsUC.GetContest(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
monitor, err := h.contestsUC.GetMonitor(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(ctx, params.ContestId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.GetMonitorResponse{
Contest: ContestDTO(*contest),
Tasks: make([]testerv1.TasksListItem, len(tasks)),
Participants: make([]testerv1.ParticipantsStat, len(monitor.Participants)),
SummaryPerProblem: make([]testerv1.ProblemStatSummary, len(monitor.Summary)),
}
for i, participant := range monitor.Participants {
resp.Participants[i] = testerv1.ParticipantsStat{
Id: participant.Id,
Name: participant.Name,
PenaltyInTotal: participant.PenaltyInTotal,
Solutions: make([]testerv1.SolutionsListItem, len(participant.Solutions)),
SolvedInTotal: participant.SolvedInTotal,
}
for j, solution := range participant.Solutions {
resp.Participants[i].Solutions[j] = SolutionsListItemDTO(*solution)
}
}
for i, problem := range monitor.Summary {
resp.SummaryPerProblem[i] = testerv1.ProblemStatSummary{
Id: problem.Id,
Success: problem.Success,
Total: problem.Total,
}
}
for i, task := range tasks {
resp.Tasks[i] = TasksListItemDTO(*task)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -1,116 +0,0 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) CreateParticipant(c *fiber.Ctx, params testerv1.CreateParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateParticipant(ctx, params.ContestId, params.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateParticipantResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req testerv1.UpdateParticipantRequest
err := c.BodyParser(&req)
if err != nil {
return err
}
err = h.contestsUC.UpdateParticipant(ctx, params.ParticipantId, models.ParticipantUpdate{
Name: req.Name,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteParticipant(c.Context(), params.ParticipantId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
participantsList, err := h.contestsUC.ListParticipants(c.Context(), models.ParticipantsFilter{
Page: params.Page,
PageSize: params.PageSize,
ContestId: params.ContestId,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.ListParticipantsResponse{
Participants: make([]testerv1.ParticipantsListItem, len(participantsList.Participants)),
Pagination: PaginationDTO(participantsList.Pagination),
}
for i, participant := range participantsList.Participants {
resp.Participants[i] = ParticipantsListItemDTO(*participant)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -1,148 +0,0 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
"io"
)
const (
maxSolutionSize int64 = 10 * 1024 * 1024
)
func (h *Handlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
s, err := c.FormFile("solution")
if err != nil {
return err
}
if s.Size == 0 || s.Size > maxSolutionSize {
return c.SendStatus(fiber.StatusBadRequest)
}
f, err := s.Open()
if err != nil {
return err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return err
}
id, err := h.contestsUC.CreateSolution(ctx, &models.SolutionCreation{
UserId: session.UserId,
TaskId: params.TaskId,
Language: params.Language,
Solution: string(b),
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateSolutionResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetSolution(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
solution, err := h.contestsUC.GetSolution(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
case models.RoleStudent:
_, err := h.contestsUC.GetParticipantId3(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
solution, err := h.contestsUC.GetSolution(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetSolutionResponse{Solution: SolutionDTO(*solution)})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
filter := models.SolutionsFilter{
ContestId: params.ContestId,
Page: params.Page,
PageSize: params.PageSize,
ParticipantId: params.ParticipantId,
TaskId: params.TaskId,
Language: params.Language,
Order: params.Order,
State: params.State,
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListSolutionsResponseDTO(solutionsList))
case models.RoleStudent:
if params.ContestId == nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
participantId, err := h.contestsUC.GetParticipantId(ctx, *params.ContestId, session.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
// Student cannot view other users' solutions
if params.ParticipantId != nil && *params.ParticipantId != participantId {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
filter.ParticipantId = &participantId
solutionsList, err := h.contestsUC.ListSolutions(ctx, filter)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(ListSolutionsResponseDTO(solutionsList))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -1,105 +0,0 @@
package rest
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
func (h *Handlers) CreateTask(c *fiber.Ctx, params testerv1.CreateTaskParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.contestsUC.CreateTask(ctx, params.ContestId, params.ProblemId)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateTaskResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetTask(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
contest, err := h.contestsUC.GetContest(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
t, err := h.contestsUC.GetTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
case models.RoleStudent:
_, err = h.contestsUC.GetParticipantId2(ctx, id, session.UserId)
if err != nil {
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
contest, err := h.contestsUC.GetContest(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
tasks, err := h.contestsUC.GetTasks(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
t, err := h.contestsUC.GetTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(GetTaskResponseDTO(contest, tasks, t))
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteTask(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.contestsUC.DeleteTask(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}

View file

@ -1,34 +0,0 @@
package contests
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type Repository interface {
CreateContest(ctx context.Context, title string) (int32, error)
GetContest(ctx context.Context, id int32) (*models.Contest, error)
DeleteContest(ctx context.Context, id int32) error
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
GetTask(ctx context.Context, id int32) (*models.Task, error)
DeleteTask(ctx context.Context, taskId int32) error
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error)
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
DeleteParticipant(ctx context.Context, participantId int32) error
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
GetMonitor(ctx context.Context, id int32, penalty int32) (*models.Monitor, error)
}

View file

@ -1,145 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
type Repository struct {
db *sqlx.DB
}
func NewRepository(db *sqlx.DB) *Repository {
return &Repository{
db: db,
}
}
const CreateContestQuery = "INSERT INTO contests (title) VALUES ($1) RETURNING id"
func (r *Repository) CreateContest(ctx context.Context, title string) (int32, error) {
const op = "Repository.CreateContest"
rows, err := r.db.QueryxContext(ctx, CreateContestQuery, title)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const GetContestQuery = "SELECT * from contests WHERE id=$1 LIMIT 1"
func (r *Repository) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
const op = "Repository.GetContest"
var contest models.Contest
err := r.db.GetContext(ctx, &contest, GetContestQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &contest, nil
}
const (
UpdateContestQuery = "UPDATE contests SET title = COALESCE($1, title) WHERE id = $2"
)
func (r *Repository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
const op = "Repository.UpdateContest"
_, err := r.db.ExecContext(ctx, UpdateContestQuery, contestUpdate.Title, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const DeleteContestQuery = "DELETE FROM contests WHERE id=$1"
func (r *Repository) DeleteContest(ctx context.Context, id int32) error {
const op = "Repository.DeleteContest"
_, err := r.db.ExecContext(ctx, DeleteContestQuery, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
func buildListContestsQueries(filter models.ContestsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
columns := []string{
"c.id",
"c.title",
"c.created_at",
"c.updated_at",
}
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).From("contests c")
if filter.UserId != nil {
qb = qb.Join("participants p ON c.id = p.contest_id")
qb = qb.Where(sq.Eq{"p.user_id": *filter.UserId})
}
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
if filter.Order != nil && *filter.Order < 0 {
qb = qb.OrderBy("c.created_at DESC")
} else {
qb = qb.OrderBy("c.created_at ASC")
}
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
return qb, countQb
}
func (r *Repository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
const op = "Repository.ListContests"
baseQb, countQb := buildListContestsQueries(filter)
query, args, err := baseQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var contests []*models.ContestsListItem
err = r.db.SelectContext(ctx, &contests, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
query, args, err = countQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = r.db.GetContext(ctx, &count, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.ContestsList{
Contests: contests,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}

View file

@ -1,116 +0,0 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
assert.NoError(t, err)
sqlxDB := sqlx.NewDb(db, "sqlmock")
return sqlxDB, mock
}
func TestRepository_CreateContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
contest := models.Contest{
Id: 1,
Title: "Test Contest",
}
mock.ExpectQuery(repository.CreateContestQuery).
WithArgs(contest.Title).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(contest.Id))
id, err := repo.CreateContest(ctx, contest.Title)
assert.NoError(t, err)
assert.Equal(t, contest.Id, id)
})
}
func TestRepository_GetContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
contest := models.Contest{
Id: 1,
Title: "Test Contest",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
mock.ExpectQuery(repository.GetContestQuery).
WithArgs(contest.Id).
WillReturnRows(sqlmock.NewRows([]string{"id", "title", "created_at", "updated_at"}).
AddRow(contest.Id, contest.Title, contest.CreatedAt, contest.UpdatedAt))
result, err := repo.GetContest(ctx, contest.Id)
assert.NoError(t, err)
assert.EqualExportedValues(t, &contest, result)
})
}
func TestRepository_UpdateContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var contestId int32 = 1
update := models.ContestUpdate{
Title: sp("Updated Contest"),
}
mock.ExpectExec(repository.UpdateContestQuery).
WithArgs(update.Title, contestId).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.UpdateContest(ctx, contestId, update)
assert.NoError(t, err)
})
}
func TestRepository_DeleteContest(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
mock.ExpectExec(repository.DeleteContestQuery).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteContest(ctx, 1)
assert.NoError(t, err)
})
}
func sp(s string) *string {
return &s
}

View file

@ -1,161 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
const (
// state=5 - AC
ReadStatisticsQuery = `
SELECT t.id as task_id,
t.position,
COUNT(*) as total,
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
FROM tasks t LEFT JOIN solutions s ON t.id = s.task_id
WHERE t.contest_id = $1
GROUP BY t.id, t.position
ORDER BY t.position;
`
SolutionsQuery = `
WITH RankedSolutions AS (
SELECT
s.id,
s.participant_id,
p2.name as participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
s.task_id,
t.position as task_position,
p.title as task_title,
t.contest_id,
c.title as contest_title,
s.updated_at,
s.created_at,
ROW_NUMBER() OVER (
PARTITION BY s.task_id, s.participant_id
ORDER BY s.score DESC, s.created_at
) as rn
FROM solutions s
LEFT JOIN tasks t ON s.task_id = t.id
LEFT JOIN problems p ON t.problem_id = p.id
LEFT JOIN contests c ON t.contest_id = c.id
LEFT JOIN participants p2 on s.participant_id = p2.id
WHERE t.contest_id = $1
)
SELECT
rs.id,
rs.participant_id,
rs.participant_name,
rs.state,
rs.score,
rs.penalty,
rs.time_stat,
rs.memory_stat,
rs.language,
rs.task_id,
rs.task_position,
rs.task_title,
rs.contest_id,
rs.contest_title,
rs.updated_at,
rs.created_at
FROM RankedSolutions rs
WHERE rs.rn = 1`
ParticipantsQuery = `
WITH Attempts AS (
SELECT
s.participant_id,
s.task_id,
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
SELECT MIN(s2.created_at)
FROM solutions s2
WHERE s2.participant_id = s.participant_id
AND s2.task_id = s.task_id
AND s2.state = 5
)) as failed_attempts,
MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty
FROM solutions s JOIN tasks t ON t.id = s.task_id
WHERE t.contest_id = $1
GROUP BY s.participant_id, s.task_id
)
SELECT
p.id,
p.name,
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
COALESCE(SUM(a.failed_attempts), 0) * $2 + COALESCE(SUM(a.success_penalty), 0) as penalty_in_total
FROM participants p LEFT JOIN Attempts a ON a.participant_id = p.id
WHERE p.contest_id = $1
GROUP BY p.id, p.name
`
)
func (r *Repository) GetMonitor(ctx context.Context, contestId int32, penalty int32) (*models.Monitor, error) {
const op = "Repository.GetMonitor"
rows, err := r.db.QueryxContext(ctx, ReadStatisticsQuery, contestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var monitor models.Monitor
for rows.Next() {
var stat models.ProblemStatSummary
err = rows.StructScan(&stat)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
monitor.Summary = append(monitor.Summary, &stat)
}
var solutions []*models.SolutionsListItem
err = r.db.SelectContext(ctx, &solutions, SolutionsQuery, contestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
rows3, err := r.db.QueryxContext(ctx, ParticipantsQuery, contestId, penalty)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
defer rows3.Close()
solutionsMap := make(map[int32][]*models.SolutionsListItem)
for _, solution := range solutions {
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
}
for rows3.Next() {
var stat models.ParticipantsStat
err = rows3.StructScan(&stat)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
if sols, ok := solutionsMap[stat.Id]; ok {
stat.Solutions = sols
}
monitor.Participants = append(monitor.Participants, &stat)
}
return &monitor, nil
}

View file

@ -1 +0,0 @@
package repository

View file

@ -1,126 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
const GetParticipantIdQuery = "SELECT id FROM participants WHERE user_id=$1 AND contest_id=$2 LIMIT 1"
func (r *Repository) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
const op = "Repository.GetParticipantId"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantIdQuery, userId, contestId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const GetParticipantId2Query = "SELECT p.id FROM participants p JOIN tasks t ON p.contest_id=t.contest_id WHERE user_id=$1 AND t.id=$2 LIMIT 1"
func (r *Repository) GetParticipantId2(ctx context.Context, taskId int32, userId int32) (int32, error) {
const op = "Repository.GetParticipantId2"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantId2Query, userId, taskId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const GetParticipantId3Query = "SELECT participant_id FROM solutions WHERE id=$1 LIMIT 1"
func (r *Repository) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
const op = "Repository.GetParticipantId3"
var participantId int32
err := r.db.GetContext(ctx, &participantId, GetParticipantId3Query, solutionId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return participantId, nil
}
const CreateParticipantQuery = "INSERT INTO participants (user_id, contest_id, name) VALUES ($1, $2, $3) RETURNING id"
func (r *Repository) CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
const op = "Repository.CreateParticipant"
name := ""
rows, err := r.db.QueryxContext(ctx, CreateParticipantQuery, userId, contestId, name)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, err
}
return id, nil
}
const DeleteParticipantQuery = "DELETE FROM participants WHERE id=$1"
const (
UpdateParticipantQuery = "UPDATE participants SET name = COALESCE($1, name) WHERE id = $2"
)
func (r *Repository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
const op = "Repository.UpdateParticipant"
_, err := r.db.ExecContext(ctx, UpdateParticipantQuery, participantUpdate.Name, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
func (r *Repository) DeleteParticipant(ctx context.Context, participantId int32) error {
const op = "Repository.DeleteParticipant"
_, err := r.db.ExecContext(ctx, DeleteParticipantQuery, participantId)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const (
ReadParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = $1 LIMIT $2 OFFSET $3`
CountParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = $1"
)
func (r *Repository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
const op = "Repository.ReadParticipants"
var participants []*models.ParticipantsListItem
err := r.db.SelectContext(ctx, &participants,
ReadParticipantsListQuery, filter.ContestId, filter.PageSize, filter.Offset())
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = r.db.GetContext(ctx, &count, CountParticipantsQuery, filter.ContestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.ParticipantsList{
Participants: participants,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}

View file

@ -1,51 +0,0 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestRepository_CreateParticipant(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
var (
expectedId int32 = 1
userId int32 = 2
contestId int32 = 3
)
ctx := context.Background()
mock.ExpectQuery(repository.CreateParticipantQuery).
WithArgs(userId, contestId).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
id, err := repo.CreateParticipant(ctx, contestId, userId)
assert.NoError(t, err)
assert.Equal(t, expectedId, id)
})
}
func TestRepository_DeleteParticipant(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var participantId int32 = 1
mock.ExpectExec(repository.DeleteParticipantQuery).
WithArgs(participantId).WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteParticipant(ctx, participantId)
assert.NoError(t, err)
})
}

View file

@ -1,222 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
sq "github.com/Masterminds/squirrel"
)
const (
GetSolutionQuery = "SELECT * FROM solutions WHERE id = $1"
)
func (r *Repository) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
const op = "Repository.GetSolution"
var solution models.Solution
err := r.db.GetContext(ctx, &solution, GetSolutionQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &solution, nil
}
const (
CreateSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`
)
func (r *Repository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
const op = "Repository.CreateSolution"
rows, err := r.db.QueryxContext(ctx,
CreateSolutionQuery,
creation.TaskId,
creation.ParticipantId,
creation.Language,
creation.Penalty,
creation.Solution,
)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
func buildListSolutionsQueries(filter models.SolutionsFilter) (sq.SelectBuilder, sq.SelectBuilder) {
columns := []string{
"s.id",
"s.participant_id",
"p2.name AS participant_name",
"s.state",
"s.score",
"s.penalty",
"s.time_stat",
"s.memory_stat",
"s.language",
"s.task_id",
"t.position AS task_position",
"p.title AS task_title",
"t.contest_id",
"c.title",
"s.updated_at",
"s.created_at",
}
qb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar).Select(columns...).
From("solutions s").
LeftJoin("tasks t ON s.task_id = t.id").
LeftJoin("problems p ON t.problem_id = p.id").
LeftJoin("contests c ON t.contest_id = c.id").
LeftJoin("participants p2 ON s.participant_id = p2.id")
if filter.ContestId != nil {
qb = qb.Where(sq.Eq{"s.contest_id": *filter.ContestId})
}
if filter.ParticipantId != nil {
qb = qb.Where(sq.Eq{"s.participant_id": *filter.ParticipantId})
}
if filter.TaskId != nil {
qb = qb.Where(sq.Eq{"s.task_id": *filter.TaskId})
}
if filter.Language != nil {
qb = qb.Where(sq.Eq{"s.language": *filter.Language})
}
if filter.State != nil {
qb = qb.Where(sq.Eq{"s.state": *filter.State})
}
countQb := sq.Select("COUNT(*)").FromSelect(qb, "sub")
if filter.Order != nil && *filter.Order < 0 {
qb = qb.OrderBy("s.id DESC")
} else {
qb = qb.OrderBy("s.id ASC")
}
qb = qb.Limit(uint64(filter.PageSize)).Offset(uint64(filter.Offset()))
return qb, countQb
}
func (r *Repository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
const op = "ContestRepository.ListSolutions"
baseQb, countQb := buildListSolutionsQueries(filter)
query, args, err := countQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var totalCount int32
err = r.db.GetContext(ctx, &totalCount, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
query, args, err = baseQb.ToSql()
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
rows, err := r.db.QueryxContext(ctx, query, args...)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
defer rows.Close()
solutions := make([]*models.SolutionsListItem, 0)
for rows.Next() {
var solution models.SolutionsListItem
err = rows.StructScan(&solution)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
solutions = append(solutions, &solution)
}
if err = rows.Err(); err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.SolutionsList{
Solutions: solutions,
Pagination: models.Pagination{
Total: models.Total(totalCount, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
// state=5 - AC
GetBestSolutions = `
WITH contest_tasks AS (
SELECT t.id AS task_id,
t.position AS task_position,
t.contest_id,
t.problem_id,
t.created_at,
t.updated_at,
p.title AS task_title,
c.title AS contest_title
FROM tasks t
LEFT JOIN problems p ON p.id = t.problem_id
LEFT JOIN contests c ON c.id = t.contest_id
WHERE t.contest_id = ?
),
best_solutions AS (
SELECT DISTINCT ON (s.task_id)
*
FROM solutions s
WHERE s.participant_id = ?
ORDER BY s.task_id, s.score DESC, s.created_at DESC
)
SELECT
s.id,
s.participant_id,
p.name AS participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
ct.task_id,
ct.task_position,
ct.task_title,
ct.contest_id,
ct.contest_title,
s.updated_at,
s.created_at
FROM contest_tasks ct
LEFT JOIN best_solutions s ON s.task_id = ct.task_id
LEFT JOIN participants p ON p.id = s.participant_id WHERE s.id IS NOT NULL
ORDER BY ct.task_position
`
)
func (r *Repository) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
const op = "Repository.GetBestSolutions"
var solutions []*models.SolutionsListItem
query := r.db.Rebind(GetBestSolutions)
err := r.db.SelectContext(ctx, &solutions, query, contestId, participantId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return solutions, nil
}

View file

@ -1 +0,0 @@
package repository

View file

@ -1,101 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
const CreateTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
VALUES ($1, $2, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = $2), 0) + 1)
RETURNING id
`
func (r *Repository) CreateTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
const op = "Repository.AddTask"
rows, err := r.db.QueryxContext(ctx, CreateTaskQuery, problemId, contestId)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const DeleteTaskQuery = "DELETE FROM tasks WHERE id=$1"
func (r *Repository) DeleteTask(ctx context.Context, taskId int32) error {
const op = "Repository.DeleteTask"
_, err := r.db.ExecContext(ctx, DeleteTaskQuery, taskId)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const GetTasksQuery = `SELECT tasks.id,
problem_id,
contest_id,
position,
title,
memory_limit,
time_limit,
tasks.created_at,
tasks.updated_at
FROM tasks
INNER JOIN problems ON tasks.problem_id = problems.id
WHERE contest_id = $1 ORDER BY position`
func (r *Repository) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
const op = "Repository.ReadTasks"
var tasks []*models.TasksListItem
err := r.db.SelectContext(ctx, &tasks, GetTasksQuery, contestId)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return tasks, nil
}
const (
GetTaskQuery = `
SELECT
t.id,
t.position,
p.title,
p.time_limit,
p.memory_limit,
t.problem_id,
t.contest_id,
p.legend_html,
p.input_format_html,
p.output_format_html,
p.notes_html,
p.scoring_html,
t.created_at,
t.updated_at
FROM tasks t
LEFT JOIN problems p ON t.problem_id = p.id
WHERE t.id = ?
`
)
func (r *Repository) GetTask(ctx context.Context, id int32) (*models.Task, error) {
const op = "Repository.ReadTask"
query := r.db.Rebind(GetTaskQuery)
var task models.Task
err := r.db.GetContext(ctx, &task, query, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &task, nil
}

View file

@ -1,51 +0,0 @@
package repository_test
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)
func TestRepository_CreateTask(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
var (
expectedId int32 = 1
problemId int32 = 2
contestId int32 = 3
)
ctx := context.Background()
mock.ExpectQuery(repository.CreateTaskQuery).
WithArgs(problemId, contestId).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
id, err := repo.CreateTask(ctx, contestId, problemId)
assert.NoError(t, err)
assert.Equal(t, expectedId, id)
})
}
func TestRepository_DeleteTask(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
mock.ExpectExec(repository.DeleteTaskQuery).
WithArgs(1).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteTask(ctx, 1)
assert.NoError(t, err)
})
}

View file

@ -1,34 +0,0 @@
package contests
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
CreateContest(ctx context.Context, title string) (int32, error)
GetContest(ctx context.Context, id int32) (*models.Contest, error)
DeleteContest(ctx context.Context, id int32) error
ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error)
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
CreateTask(ctx context.Context, contestId int32, taskId int32) (int32, error)
DeleteTask(ctx context.Context, taskId int32) error
GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error)
GetTask(ctx context.Context, id int32) (*models.Task, error)
CreateParticipant(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error)
GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error)
GetParticipantId3(ctx context.Context, solutionId int32) (int32, error)
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
DeleteParticipant(ctx context.Context, participantId int32) error
ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error)
GetSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error)
GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error)
GetMonitor(ctx context.Context, id int32) (*models.Monitor, error)
}

View file

@ -1,39 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/contests"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type ContestUseCase struct {
contestRepo contests.Repository
}
func NewContestUseCase(
contestRepo contests.Repository,
) *ContestUseCase {
return &ContestUseCase{
contestRepo: contestRepo,
}
}
func (uc *ContestUseCase) CreateContest(ctx context.Context, title string) (int32, error) {
return uc.contestRepo.CreateContest(ctx, title)
}
func (uc *ContestUseCase) GetContest(ctx context.Context, id int32) (*models.Contest, error) {
return uc.contestRepo.GetContest(ctx, id)
}
func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
return uc.contestRepo.UpdateContest(ctx, id, contestUpdate)
}
func (uc *ContestUseCase) DeleteContest(ctx context.Context, id int32) error {
return uc.contestRepo.DeleteContest(ctx, id)
}
func (uc *ContestUseCase) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
return uc.contestRepo.ListContests(ctx, filter)
}

View file

@ -1,10 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
return uc.contestRepo.GetMonitor(ctx, contestId, 20)
}

View file

@ -1,34 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetParticipantId(ctx context.Context, contestId int32, userId int32) (int32, error) {
return uc.contestRepo.GetParticipantId(ctx, contestId, userId)
}
func (uc *ContestUseCase) GetParticipantId2(ctx context.Context, taskId, userId int32) (int32, error) {
return uc.contestRepo.GetParticipantId2(ctx, taskId, userId)
}
func (uc *ContestUseCase) GetParticipantId3(ctx context.Context, solutionId int32) (int32, error) {
return uc.contestRepo.GetParticipantId3(ctx, solutionId)
}
func (uc *ContestUseCase) CreateParticipant(ctx context.Context, contestId int32, userId int32) (id int32, err error) {
return uc.contestRepo.CreateParticipant(ctx, contestId, userId)
}
func (uc *ContestUseCase) DeleteParticipant(ctx context.Context, participantId int32) error {
return uc.contestRepo.DeleteParticipant(ctx, participantId)
}
func (uc *ContestUseCase) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
return uc.contestRepo.ListParticipants(ctx, filter)
}
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
}

View file

@ -1,29 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) GetSolution(ctx context.Context, id int32) (*models.Solution, error) {
return uc.contestRepo.GetSolution(ctx, id)
}
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
participantId, err := uc.contestRepo.GetParticipantId2(ctx, creation.TaskId, creation.UserId)
if err != nil {
return 0, err
}
creation.ParticipantId = participantId
return uc.contestRepo.CreateSolution(ctx, creation)
}
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
return uc.contestRepo.ListSolutions(ctx, filter)
}
func (uc *ContestUseCase) GetBestSolutions(ctx context.Context, contestId int32, participantId int32) ([]*models.SolutionsListItem, error) {
return uc.contestRepo.GetBestSolutions(ctx, contestId, participantId)
}

View file

@ -1,22 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
func (uc *ContestUseCase) CreateTask(ctx context.Context, contestId int32, taskId int32) (id int32, err error) {
return uc.contestRepo.CreateTask(ctx, contestId, taskId)
}
func (uc *ContestUseCase) GetTask(ctx context.Context, id int32) (*models.Task, error) {
return uc.contestRepo.GetTask(ctx, id)
}
func (uc *ContestUseCase) GetTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
return uc.contestRepo.GetTasks(ctx, contestId)
}
func (uc *ContestUseCase) DeleteTask(ctx context.Context, taskId int32) error {
return uc.contestRepo.DeleteTask(ctx, taskId)
}

9
internal/lib/config.go Normal file
View file

@ -0,0 +1,9 @@
package lib
type Config struct {
Env string `env:"ENV" env-default:"prod"`
Pandoc string `env:"PANDOC" required:"true"`
Auth string `env:"PANDOC" required:"true"`
Address string `env:"ADDRESS" required:"true"`
PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
}

132
internal/lib/errors.go Normal file
View file

@ -0,0 +1,132 @@
package lib
import (
"errors"
"fmt"
"go.uber.org/zap/zapcore"
"runtime"
)
type code uint8
const (
ErrValidationFailed code = 1
ErrInternal code = 2
ErrExternal code = 3
ErrNoPermission code = 4
ErrUnknown code = 5
ErrDeadlineExceeded code = 6
ErrNotFound code = 7
ErrAlreadyExists code = 8
ErrConflict code = 9
ErrUnimplemented code = 10
ErrBadInput code = 11
ErrUnauthenticated code = 12
)
func (c code) String() string {
switch {
case errors.Is(c, ErrValidationFailed):
return "validation error"
case errors.Is(c, ErrInternal):
return "internal error"
case errors.Is(c, ErrExternal):
return "external error"
case errors.Is(c, ErrNoPermission):
return "permission error"
case errors.Is(c, ErrUnknown):
return "unknown error"
case errors.Is(c, ErrDeadlineExceeded):
return "deadline error"
case errors.Is(c, ErrNotFound):
return "not found error"
case errors.Is(c, ErrAlreadyExists):
return "already exists error"
case errors.Is(c, ErrConflict):
return "conflict error"
case errors.Is(c, ErrUnimplemented):
return "unimplemented error"
case errors.Is(c, ErrBadInput):
return "bad input error"
}
panic("unimplemented")
}
func (c code) Error() string {
return c.String()
}
type layer uint8
const (
LayerTransport layer = 1
LayerService layer = 2
LayerStorage layer = 3
)
func (l layer) String() string {
switch l {
case LayerTransport:
return "transport"
case LayerService:
return "service"
case LayerStorage:
return "storage"
}
panic("unimplemented")
}
func location(skip int) string {
_, file, line, _ := runtime.Caller(skip)
return fmt.Sprintf("%s:%d", file, line)
}
type Error struct {
src error
layer layer
code code
msg string
loc string
}
func wrap(src error, layer layer, class code, msg string, loc string) *Error {
return &Error{
src: src,
layer: layer,
code: class,
msg: msg,
loc: loc,
}
}
func (e *Error) Unwrap() []error {
return []error{e.src, e.code}
}
func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.code.String(), e.msg)
}
func (e *Error) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
if e.src != nil {
encoder.AddString("src", e.src.Error())
}
encoder.AddString("layer", e.layer.String())
encoder.AddString("code", e.code.String())
encoder.AddString("msg", e.msg)
return nil
}
func TransportError(src error, code code, msg string) error {
return wrap(src, LayerTransport, code, msg, location(2))
}
func ServiceError(src error, code code, msg string) error {
return wrap(src, LayerService, code, msg, location(2))
}
func StorageError(src error, code code, msg string) error {
return wrap(src, LayerStorage, code, msg, location(2))
}

17
internal/lib/lib.go Normal file
View file

@ -0,0 +1,17 @@
package lib
import (
"time"
)
func AsTimeP(t time.Time) *time.Time {
return &t
}
func AsInt32P(v int32) *int32 {
return &v
}
func AsStringP(str string) *string {
return &str
}

67
internal/lib/pandoc.go Normal file
View file

@ -0,0 +1,67 @@
package lib
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
)
type PandocClient struct {
client *http.Client
address string
}
func NewPandocClient(client *http.Client, address string) *PandocClient {
return &PandocClient{
client: client,
address: address,
}
}
type convertRequest struct {
Text string `json:"text"`
From string `json:"from"`
To string `json:"to"`
}
func (client *PandocClient) convert(ctx context.Context, text, from, to string) (string, error) {
body, err := json.Marshal(convertRequest{
Text: text,
From: from,
To: to,
})
if err != nil {
return "", err
}
buf := bytes.NewBuffer(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.address, buf)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", err
}
body, err = io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func (client *PandocClient) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) {
return client.convert(ctx, text, "latex", "html5")
}

View file

@ -1,66 +0,0 @@
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"
)
const (
TokenKey = "token"
)
func AuthMiddleware(jwtSecret string, sessionsUC sessions.UseCase) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization", "")
if authHeader == "" {
return c.Next()
}
authParts := strings.Split(authHeader, " ")
if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" {
return c.Next()
}
parsedToken, err := jwt.ParseWithClaims(authParts[1], &models.JWT{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(jwtSecret), nil
})
if err != nil {
return c.Next()
}
token, ok := parsedToken.Claims.(*models.JWT)
if !ok {
return c.Next()
}
err = token.Valid()
if err != nil {
return c.Next()
}
ctx := c.Context()
// check if session exists
_, err = sessionsUC.ReadSession(ctx, token.SessionId)
if err != nil {
if errors.Is(err, pkg.ErrNotFound) {
return c.Next()
}
return c.SendStatus(pkg.ToREST(err))
}
c.Locals(TokenKey, token)
return c.Next()
}
}

View file

@ -1,127 +0,0 @@
package models
import "time"
type Contest struct {
Id int32 `db:"id"`
Title string `db:"title"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ContestsListItem struct {
Id int32 `db:"id"`
Title string `db:"title"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
type ContestsList struct {
Contests []*ContestsListItem
Pagination Pagination
}
type ContestsFilter struct {
Page int32
PageSize int32
UserId *int32
Order *int32
}
func (f ContestsFilter) Offset() int32 {
return (f.Page - 1) * f.PageSize
}
type ContestUpdate struct {
Title *string `json:"title"`
}
type Monitor struct {
Participants []*ParticipantsStat
Summary []*ProblemStatSummary
}
type ParticipantsStat struct {
Id int32 `db:"id"`
Name string `db:"name"`
SolvedInTotal int32 `db:"solved_in_total"`
PenaltyInTotal int32 `db:"penalty_in_total"`
Solutions []*SolutionsListItem `db:"solutions"`
}
type ProblemStatSummary struct {
Id int32 `db:"task_id"`
Position int32 `db:"position"`
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,13 +0,0 @@
package models
type Pagination struct {
Page int32 `json:"page"`
Total int32 `json:"total"`
}
func Total(count int32, pageSize int32) int32 {
if count%pageSize == 0 {
return count / pageSize
}
return count/pageSize + 1
}

View file

@ -1,83 +0,0 @@
package models
import "time"
type Problem struct {
Id int32 `db:"id"`
Title string `db:"title"`
TimeLimit int32 `db:"time_limit"`
MemoryLimit int32 `db:"memory_limit"`
Legend string `db:"legend"`
InputFormat string `db:"input_format"`
OutputFormat string `db:"output_format"`
Notes string `db:"notes"`
Scoring string `db:"scoring"`
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 ProblemsListItem struct {
Id int32 `db:"id"`
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"`
SolvedCount int32 `db:"solved_count"`
}
type ProblemsList struct {
Problems []*ProblemsListItem `json:"problems"`
Pagination Pagination `json:"pagination"`
}
type ProblemsFilter struct {
Page int32
PageSize int32
}
func (f ProblemsFilter) Offset() int32 {
return (f.Page - 1) * f.PageSize
}
type ProblemUpdate struct {
Title *string `db:"title"`
MemoryLimit *int32 `db:"memory_limit"`
TimeLimit *int32 `db:"time_limit"`
Legend *string `db:"legend"`
InputFormat *string `db:"input_format"`
OutputFormat *string `db:"output_format"`
Notes *string `db:"notes"`
Scoring *string `db:"scoring"`
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"`
}
type ProblemStatement struct {
Legend string `db:"legend"`
InputFormat string `db:"input_format"`
OutputFormat string `db:"output_format"`
Notes string `db:"notes"`
Scoring string `db:"scoring"`
}
type Html5ProblemStatement struct {
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"`
}

View file

@ -1,95 +0,0 @@
package models
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"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"`
IssuedAt int64 `json:"iat"`
}
func (j JWT) Valid() error {
if uuid.Validate(j.SessionId) != nil {
return errors.New("invalid session id")
}
if j.UserId == 0 {
return errors.New("empty user id")
}
if j.IssuedAt == 0 {
return errors.New("empty issued at")
}
return nil
}
type Credentials struct {
Username string
Password string
}
type Device struct {
Ip string
UseAgent string
}

View file

@ -1,119 +0,0 @@
package models
import "time"
type Solution struct {
Id int32 `db:"id"`
ParticipantId int32 `db:"participant_id"`
ParticipantName string `db:"participant_name"`
Solution string `db:"solution"`
State int32 `db:"state"`
Score int32 `db:"score"`
Penalty int32 `db:"penalty"`
TimeStat int32 `db:"time_stat"`
MemoryStat int32 `db:"memory_stat"`
Language int32 `db:"language"`
TaskId int32 `db:"task_id"`
TaskPosition int32 `db:"task_position"`
TaskTitle string `db:"task_title"`
ContestId int32 `db:"contest_id"`
ContestTitle string `db:"contest_title"`
UpdatedAt time.Time `db:"updated_at"`
CreatedAt time.Time `db:"created_at"`
}
type SolutionCreation struct {
Solution string
TaskId int32
UserId int32
ParticipantId int32
Language int32
Penalty int32
}
type SolutionsListItem struct {
Id int32 `db:"id"`
ParticipantId int32 `db:"participant_id"`
ParticipantName string `db:"participant_name"`
State int32 `db:"state"`
Score int32 `db:"score"`
Penalty int32 `db:"penalty"`
TimeStat int32 `db:"time_stat"`
MemoryStat int32 `db:"memory_stat"`
Language int32 `db:"language"`
TaskId int32 `db:"task_id"`
TaskPosition int32 `db:"task_position"`
TaskTitle string `db:"task_title"`
ContestId int32 `db:"contest_id"`
ContestTitle string `db:"contest_title"`
UpdatedAt time.Time `db:"updated_at"`
CreatedAt time.Time `db:"created_at"`
}
type SolutionsList struct {
Solutions []*SolutionsListItem
Pagination Pagination
}
type SolutionsFilter struct {
Page int32
PageSize int32
ContestId *int32
ParticipantId *int32
TaskId *int32
Language *int32
State *int32
Order *int32
}
func (f SolutionsFilter) Offset() int32 {
return (f.Page - 1) * f.PageSize
}
//type Result int32
//
//const (
// NotTested Result = 1 // change only with schema change
// Accepted Result = 2
// WrongAnswer Result = 3
// PresentationError Result = 4
// CompilationError Result = 5
// MemoryLimitExceeded Result = 6
// TimeLimitExceeded Result = 7
// RuntimeError Result = 8
// SystemFailDuringTesting Result = 9
// Testing Result = 10
//)
//
//var ErrBadResult = errors.New("bad result")
//
//func (result Result) Valid() error {
// switch result {
// case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
// return nil
// }
// return ErrBadResult
//}
//
//type Language struct {
// Name string
// CompileCmd []string //source: src;result:executable
// RunCmd []string //source: executable
//}
//
//var Languages = []Language{
// {Name: "gcc std=c90",
// CompileCmd: []string{"gcc", "src", "-std=c90", "-o", "executable"},
// RunCmd: []string{"executable"}},
//}

View file

@ -1,71 +0,0 @@
package models
import (
"golang.org/x/crypto/bcrypt"
"time"
)
type Role int32
type User struct {
Id int32 `db:"id"`
Username string `db:"username"`
HashedPassword string `db:"hashed_pwd"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Role Role `db:"role"`
}
type UserCreation struct {
Username string
Password string
Role Role
}
func (u *UserCreation) HashPassword() error {
hpwd, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hpwd)
return nil
}
func (user *User) IsSamePwd(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
if err != nil {
return false
}
return true
}
type UsersListFilters struct {
PageSize int32
Page int32
}
func (f UsersListFilters) Offset() int32 {
return (f.Page - 1) * f.PageSize
}
type UsersList struct {
Users []*User
Pagination Pagination
}
type UserUpdate struct {
Username *string
Role *Role
}
const (
RoleGuest Role = -1
RoleStudent Role = 0
RoleTeacher Role = 1
RoleAdmin Role = 2
)
type Grant struct {
Action string
Resource string
}

View file

@ -1,15 +0,0 @@
package problems
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"github.com/gofiber/fiber/v2"
)
type ProblemsHandlers interface {
ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error
CreateProblem(c *fiber.Ctx) error
DeleteProblem(c *fiber.Ctx, id int32) error
GetProblem(c *fiber.Ctx, id int32) error
UpdateProblem(c *fiber.Ctx, id int32) error
UploadProblem(c *fiber.Ctx, id int32) error
}

View file

@ -1,261 +0,0 @@
package rest
import (
"context"
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/problems"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
"io"
)
type Handlers struct {
problemsUC problems.UseCase
jwtSecret string
}
const (
sessionKey = "session"
)
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
const op = "sessionFromCtx"
session, ok := ctx.Value(sessionKey).(*models.Session)
if !ok {
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
}
return session, nil
}
func NewHandlers(problemsUC problems.UseCase) *Handlers {
return &Handlers{
problemsUC: problemsUC,
}
}
func (h *Handlers) ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
problemsList, err := h.problemsUC.ListProblems(c.Context(), models.ProblemsFilter{
Page: params.Page,
PageSize: params.PageSize,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.ListProblemsResponse{
Problems: make([]testerv1.ProblemsListItem, len(problemsList.Problems)),
Pagination: PaginationDTO(problemsList.Pagination),
}
for i, problem := range problemsList.Problems {
resp.Problems[i] = ProblemsListItemDTO(*problem)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) CreateProblem(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
id, err := h.problemsUC.CreateProblem(c.Context(), "Название задачи")
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateProblemResponse{
Id: id,
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteProblem(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
err := h.problemsUC.DeleteProblem(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetProblem(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
problem, err := h.problemsUC.GetProblemById(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(
testerv1.GetProblemResponse{Problem: *ProblemDTO(problem)},
)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateProblem(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req testerv1.UpdateProblemRequest
err := c.BodyParser(&req)
if err != nil {
return err
}
err = h.problemsUC.UpdateProblem(c.Context(), id, &models.ProblemUpdate{
Title: req.Title,
MemoryLimit: req.MemoryLimit,
TimeLimit: req.TimeLimit,
Legend: req.Legend,
InputFormat: req.InputFormat,
OutputFormat: req.OutputFormat,
Notes: req.Notes,
Scoring: req.Scoring,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UploadProblem(c *fiber.Ctx, id int32) error {
ctx := c.Context()
//session, err := sessionFromCtx(ctx)
//if err != nil {
// return c.SendStatus(pkg.ToREST(err))
//}
session := models.Session{
Role: models.RoleAdmin,
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
a, err := c.FormFile("archive")
if err != nil {
return err
}
if a.Size == 0 { // FIXME: check max size
return c.SendStatus(fiber.StatusBadRequest)
}
f, err := a.Open()
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return err
}
if err = h.problemsUC.UploadProblem(ctx, id, data); err != nil {
return err
}
return nil
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func PaginationDTO(p models.Pagination) testerv1.Pagination {
return testerv1.Pagination{
Page: p.Page,
Total: p.Total,
}
}
func ProblemsListItemDTO(p models.ProblemsListItem) testerv1.ProblemsListItem {
return testerv1.ProblemsListItem{
Id: p.Id,
Title: p.Title,
MemoryLimit: p.MemoryLimit,
TimeLimit: p.TimeLimit,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
SolvedCount: p.SolvedCount,
}
}
func ProblemDTO(p *models.Problem) *testerv1.Problem {
return &testerv1.Problem{
Id: p.Id,
Title: p.Title,
TimeLimit: p.TimeLimit,
MemoryLimit: p.MemoryLimit,
Legend: p.Legend,
InputFormat: p.InputFormat,
OutputFormat: p.OutputFormat,
Notes: p.Notes,
Scoring: p.Scoring,
LegendHtml: p.LegendHtml,
InputFormatHtml: p.InputFormatHtml,
OutputFormatHtml: p.OutputFormatHtml,
NotesHtml: p.NotesHtml,
ScoringHtml: p.ScoringHtml,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}

View file

@ -1,32 +0,0 @@
package problems
import (
"context"
"database/sql"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
)
type Querier interface {
Rebind(query string) string
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
type Tx interface {
Querier
Commit() error
Rollback() error
}
type Repository interface {
BeginTx(ctx context.Context) (Tx, error)
DB() Querier
CreateProblem(ctx context.Context, q Querier, title string) (int32, error)
GetProblemById(ctx context.Context, q Querier, id int32) (*models.Problem, error)
DeleteProblem(ctx context.Context, q Querier, id int32) error
ListProblems(ctx context.Context, q Querier, filter models.ProblemsFilter) (*models.ProblemsList, error)
UpdateProblem(ctx context.Context, q Querier, id int32, heading *models.ProblemUpdate) error
}

View file

@ -1,175 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/problems"
"git.sch9.ru/new_gate/ms-tester/pkg"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
)
type Repository struct {
_db *sqlx.DB
}
func NewRepository(db *sqlx.DB) *Repository {
return &Repository{
_db: db,
}
}
func (r *Repository) BeginTx(ctx context.Context) (problems.Tx, error) {
tx, err := r._db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
return tx, nil
}
func (r *Repository) DB() problems.Querier {
return r._db
}
const CreateProblemQuery = "INSERT INTO problems (title) VALUES ($1) RETURNING id"
func (r *Repository) CreateProblem(ctx context.Context, q problems.Querier, title string) (int32, error) {
const op = "Repository.CreateProblem"
rows, err := q.QueryxContext(ctx, CreateProblemQuery, title)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const GetProblemByIdQuery = "SELECT * from problems WHERE id=$1 LIMIT 1"
func (r *Repository) GetProblemById(ctx context.Context, q problems.Querier, id int32) (*models.Problem, error) {
const op = "Repository.ReadProblemById"
var problem models.Problem
err := q.GetContext(ctx, &problem, GetProblemByIdQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &problem, nil
}
const DeleteProblemQuery = "DELETE FROM problems WHERE id=$1"
func (r *Repository) DeleteProblem(ctx context.Context, q problems.Querier, id int32) error {
const op = "Repository.DeleteProblem"
_, err := q.ExecContext(ctx, DeleteProblemQuery, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const (
ListProblemsQuery = `SELECT p.id,
p.title,
p.memory_limit,
p.time_limit,
p.created_at,
p.updated_at,
COALESCE(solved_count, 0) AS solved_count
FROM problems p
LEFT JOIN (SELECT t.problem_id,
COUNT(DISTINCT s.participant_id) AS solved_count
FROM solutions s
JOIN tasks t ON s.task_id = t.id
WHERE s.state = 5
GROUP BY t.problem_id) sol ON p.id = sol.problem_id
LIMIT $1 OFFSET $2`
CountProblemsQuery = "SELECT COUNT(*) FROM problems"
)
func (r *Repository) ListProblems(ctx context.Context, q problems.Querier, filter models.ProblemsFilter) (*models.ProblemsList, error) {
const op = "ContestRepository.ListProblems"
var list []*models.ProblemsListItem
err := q.SelectContext(ctx, &list, ListProblemsQuery, filter.PageSize, filter.Offset())
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = q.GetContext(ctx, &count, CountProblemsQuery)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.ProblemsList{
Problems: list,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
UpdateProblemQuery = `UPDATE problems
SET title = COALESCE($2, title),
time_limit = COALESCE($3, time_limit),
memory_limit = COALESCE($4, memory_limit),
legend = COALESCE($5, legend),
input_format = COALESCE($6, input_format),
output_format = COALESCE($7, output_format),
notes = COALESCE($8, notes),
scoring = COALESCE($9, scoring),
legend_html = COALESCE($10, legend_html),
input_format_html = COALESCE($11, input_format_html),
output_format_html = COALESCE($12, output_format_html),
notes_html = COALESCE($13, notes_html),
scoring_html = COALESCE($14, scoring_html)
WHERE id=$1`
)
func (r *Repository) UpdateProblem(ctx context.Context, q problems.Querier, id int32, problem *models.ProblemUpdate) error {
const op = "Repository.UpdateProblem"
query := q.Rebind(UpdateProblemQuery)
_, err := q.ExecContext(ctx, query,
id,
problem.Title,
problem.TimeLimit,
problem.MemoryLimit,
problem.Legend,
problem.InputFormat,
problem.OutputFormat,
problem.Notes,
problem.Scoring,
problem.LegendHtml,
problem.InputFormatHtml,
problem.OutputFormatHtml,
problem.NotesHtml,
problem.ScoringHtml,
)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}

View file

@ -1,293 +0,0 @@
package repository_test
import (
"context"
"database/sql"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
assert.NoError(t, err)
sqlxDB := sqlx.NewDb(db, "sqlmock")
return sqlxDB, mock
}
func TestRepository_CreateProblem(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
problem := models.Problem{
Id: 1,
Title: "Test Problem",
}
mock.ExpectQuery(repository.CreateProblemQuery).
WithArgs(problem.Title).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(problem.Id))
id, err := repo.CreateProblem(ctx, db, problem.Title)
assert.NoError(t, err)
assert.Equal(t, problem.Id, id)
})
}
func TestRepository_GetProblemById(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
expected := &models.Problem{
Id: 1,
Title: "Test Problem",
TimeLimit: 1000,
MemoryLimit: 1024,
Legend: "Test Legend",
InputFormat: "Test Input Format",
OutputFormat: "Test Output Format",
Notes: "Test Notes",
Scoring: "Test Scoring",
LegendHtml: "Test Legend HTML",
InputFormatHtml: "Test Input Format HTML",
OutputFormatHtml: "Test Output Format HTML",
NotesHtml: "Test Notes HTML",
ScoringHtml: "Test Scoring HTML",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
columns := []string{
"id",
"title",
"time_limit",
"memory_limit",
"legend",
"input_format",
"output_format",
"notes",
"scoring",
"legend_html",
"input_format_html",
"output_format_html",
"notes_html",
"scoring_html",
"created_at",
"updated_at",
}
rows := sqlmock.NewRows(columns).
AddRow(
expected.Id,
expected.Title,
expected.TimeLimit,
expected.MemoryLimit,
expected.Legend,
expected.InputFormat,
expected.OutputFormat,
expected.Notes,
expected.Scoring,
expected.LegendHtml,
expected.InputFormatHtml,
expected.OutputFormatHtml,
expected.NotesHtml,
expected.ScoringHtml,
expected.CreatedAt,
expected.UpdatedAt)
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(expected.Id).WillReturnRows(rows)
problem, err := repo.GetProblemById(ctx, db, expected.Id)
assert.NoError(t, err)
assert.EqualExportedValues(t, expected, problem)
})
t.Run("not found", func(t *testing.T) {
ctx := context.Background()
id := int32(1)
mock.ExpectQuery(repository.GetProblemByIdQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
_, err := repo.GetProblemById(ctx, db, id)
assert.Error(t, err)
})
}
func TestRepository_DeleteProblem(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
id := int32(1)
mock.ExpectExec(repository.DeleteProblemQuery).
WithArgs(id).WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteProblem(ctx, db, id)
assert.NoError(t, err)
})
t.Run("not found", func(t *testing.T) {
ctx := context.Background()
id := int32(1)
mock.ExpectExec(repository.DeleteProblemQuery).WithArgs(id).WillReturnError(sql.ErrNoRows)
err := repo.DeleteProblem(ctx, db, id)
assert.Error(t, err)
})
}
func TestRepository_ListProblems(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
expected := make([]*models.ProblemsListItem, 0)
for i := 0; i < 10; i++ {
problem := &models.ProblemsListItem{
Id: int32(i + 1),
Title: fmt.Sprintf("Test Problem %d", i+1),
TimeLimit: 1000,
MemoryLimit: 1024,
SolvedCount: int32(123 * i),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
expected = append(expected, problem)
}
filter := models.ProblemsFilter{
Page: 1,
PageSize: 10,
}
var totalCount int32 = 10
columns := []string{
"id",
"title",
"time_limit",
"memory_limit",
"solved_count",
"created_at",
"updated_at",
}
rows := sqlmock.NewRows(columns)
for _, problem := range expected {
rows = rows.AddRow(
problem.Id,
problem.Title,
problem.TimeLimit,
problem.MemoryLimit,
problem.SolvedCount,
problem.CreatedAt,
problem.UpdatedAt,
)
}
mock.ExpectQuery(repository.ListProblemsQuery).WillReturnRows(rows)
mock.ExpectQuery(repository.CountProblemsQuery).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
problems, err := repo.ListProblems(ctx, db, filter)
assert.NoError(t, err)
assert.Equal(t, expected, problems.Problems)
assert.Equal(t, models.Pagination{
Page: 1,
Total: 1,
}, problems.Pagination)
})
}
func TestRepository_UpdateProblem(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var id int32 = 1
update := &models.ProblemUpdate{
Title: sp("Test Problem"),
TimeLimit: ip(1000),
MemoryLimit: ip(1024),
Legend: sp("Test Legend"),
InputFormat: sp("Test Input Format"),
OutputFormat: sp("Test Output Format"),
Notes: sp("Test Notes"),
Scoring: sp("Test Scoring"),
LegendHtml: sp("Test Legend HTML"),
InputFormatHtml: sp("Test Input Format HTML"),
OutputFormatHtml: sp("Test Output Format HTML"),
NotesHtml: sp("Test Notes HTML"),
ScoringHtml: sp("Test Scoring HTML"),
}
mock.ExpectExec(repository.UpdateProblemQuery).WithArgs(
id,
update.Title,
update.TimeLimit,
update.MemoryLimit,
update.Legend,
update.InputFormat,
update.OutputFormat,
update.Notes,
update.Scoring,
update.LegendHtml,
update.InputFormatHtml,
update.OutputFormatHtml,
update.NotesHtml,
update.ScoringHtml,
).WillReturnResult(sqlmock.NewResult(1, 1))
err := repo.UpdateProblem(ctx, db, id, update)
assert.NoError(t, err)
})
}
func sp(s string) *string {
return &s
}
func ip(s int32) *int32 {
return &s
}

View file

@ -1,15 +0,0 @@
package problems
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
CreateProblem(ctx context.Context, title string) (int32, error)
GetProblemById(ctx context.Context, id int32) (*models.Problem, error)
DeleteProblem(ctx context.Context, id int32) error
ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error)
UpdateProblem(ctx context.Context, id int32, problem *models.ProblemUpdate) error
UploadProblem(ctx context.Context, id int32, archive []byte) error
}

View file

@ -1,337 +0,0 @@
package usecase
import (
"archive/zip"
"bytes"
"context"
"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/pkg"
"github.com/microcosm-cc/bluemonday"
)
type UseCase struct {
problemRepo problems.Repository
pandocClient pkg.PandocClient
}
func NewUseCase(
problemRepo problems.Repository,
pandocClient pkg.PandocClient,
) *UseCase {
return &UseCase{
problemRepo: problemRepo,
pandocClient: pandocClient,
}
}
func (u *UseCase) CreateProblem(ctx context.Context, title string) (int32, error) {
return u.problemRepo.CreateProblem(ctx, u.problemRepo.DB(), title)
}
func (u *UseCase) GetProblemById(ctx context.Context, id int32) (*models.Problem, error) {
return u.problemRepo.GetProblemById(ctx, u.problemRepo.DB(), id)
}
func (u *UseCase) DeleteProblem(ctx context.Context, id int32) error {
return u.problemRepo.DeleteProblem(ctx, u.problemRepo.DB(), id)
}
func (u *UseCase) ListProblems(ctx context.Context, filter models.ProblemsFilter) (*models.ProblemsList, error) {
return u.problemRepo.ListProblems(ctx, u.problemRepo.DB(), filter)
}
func (u *UseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate *models.ProblemUpdate) error {
if isEmpty(*problemUpdate) {
return pkg.Wrap(pkg.ErrBadInput, nil, "UpdateProblem", "empty problem update")
}
tx, err := u.problemRepo.BeginTx(ctx)
if err != nil {
return err
}
problem, err := u.problemRepo.GetProblemById(ctx, tx, id)
if err != nil {
return errors.Join(err, tx.Rollback())
}
statement := models.ProblemStatement{
Legend: problem.Legend,
InputFormat: problem.InputFormat,
OutputFormat: problem.OutputFormat,
Notes: problem.Notes,
Scoring: problem.Scoring,
}
if problemUpdate.Legend != nil {
statement.Legend = *problemUpdate.Legend
}
if problemUpdate.InputFormat != nil {
statement.InputFormat = *problemUpdate.InputFormat
}
if problemUpdate.OutputFormat != nil {
statement.OutputFormat = *problemUpdate.OutputFormat
}
if problemUpdate.Notes != nil {
statement.Notes = *problemUpdate.Notes
}
if problemUpdate.Scoring != nil {
statement.Scoring = *problemUpdate.Scoring
}
builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
if err != nil {
return errors.Join(err, tx.Rollback())
}
if builtStatement.LegendHtml != problem.LegendHtml {
problemUpdate.LegendHtml = &builtStatement.LegendHtml
}
if builtStatement.InputFormatHtml != problem.InputFormatHtml {
problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml
}
if builtStatement.OutputFormatHtml != problem.OutputFormatHtml {
problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml
}
if builtStatement.NotesHtml != problem.NotesHtml {
problemUpdate.NotesHtml = &builtStatement.NotesHtml
}
if builtStatement.ScoringHtml != problem.ScoringHtml {
problemUpdate.ScoringHtml = &builtStatement.ScoringHtml
}
err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)
if err != nil {
return errors.Join(err, tx.Rollback())
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
type ProblemProperties struct {
Title string `json:"name"`
TimeLimit int32 `json:"timeLimit"`
MemoryLimit int32 `json:"memoryLimit"`
}
func (u *UseCase) UploadProblem(ctx context.Context, id int32, data []byte) error {
locale := "russian"
defaultLocale := "english"
var localeProblem, defaultProblem string
var localeProperties, defaultProperties ProblemProperties
r := bytes.NewReader(data)
rc, err := zip.NewReader(r, int64(r.Len()))
if err != nil {
return err
}
testsZipBuf := new(bytes.Buffer)
w := zip.NewWriter(testsZipBuf)
for _, f := range rc.File {
if f.FileInfo().IsDir() {
continue
}
if f.Name == fmt.Sprintf("statements/%s/problem.tex", locale) {
localeProblem, err = readProblem(f)
if err != nil {
return err
}
}
if f.Name == fmt.Sprintf("statements/%s/problem.tex", defaultLocale) {
defaultProblem, err = readProblem(f)
if err != nil {
return err
}
}
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", locale) {
localeProperties, err = readProperties(f)
if err != nil {
return err
}
}
if f.Name == fmt.Sprintf("statements/%s/problem-properties.json", defaultLocale) {
defaultProperties, err = readProperties(f)
if err != nil {
return err
}
}
if strings.HasPrefix(f.Name, "tests/") {
if err := w.Copy(f); err != nil {
return err
}
}
}
if err := w.Close(); err != nil {
return err
}
// testsZipBuf contains test files; this is for s3
localeProperties.MemoryLimit /= 1024 * 1024
defaultProperties.MemoryLimit /= 1024 * 1024
problemUpdate := &models.ProblemUpdate{}
if localeProblem != "" {
problemUpdate.Legend = &localeProblem
problemUpdate.Title = &localeProperties.Title
problemUpdate.TimeLimit = &localeProperties.TimeLimit
problemUpdate.MemoryLimit = &localeProperties.MemoryLimit
} else {
problemUpdate.Legend = &defaultProblem
problemUpdate.Title = &defaultProperties.Title
problemUpdate.TimeLimit = &defaultProperties.TimeLimit
problemUpdate.MemoryLimit = &defaultProperties.MemoryLimit
}
if err := u.UpdateProblem(ctx, id, problemUpdate); err != nil {
return err
}
return nil
}
func readProblem(f *zip.File) (string, error) {
rc, err := f.Open()
if err != nil {
return "", err
}
defer rc.Close()
problemData, err := io.ReadAll(rc)
if err != nil {
return "", err
}
return string(problemData), nil
}
func readProperties(f *zip.File) (ProblemProperties, error) {
rc, err := f.Open()
if err != nil {
return ProblemProperties{}, err
}
defer rc.Close()
var properties ProblemProperties
if err := json.NewDecoder(rc).Decode(&properties); err != nil {
return ProblemProperties{}, err
}
return properties, nil
}
func isEmpty(p models.ProblemUpdate) bool {
return p.Title == nil &&
p.Legend == nil &&
p.InputFormat == nil &&
p.OutputFormat == nil &&
p.Notes == nil &&
p.Scoring == nil &&
p.MemoryLimit == nil &&
p.TimeLimit == nil
}
func wrap(s string) string {
return fmt.Sprintf("\\begin{document}\n%s\n\\end{document}\n", s)
}
func trimSpaces(statement models.ProblemStatement) models.ProblemStatement {
return models.ProblemStatement{
Legend: strings.TrimSpace(statement.Legend),
InputFormat: strings.TrimSpace(statement.InputFormat),
OutputFormat: strings.TrimSpace(statement.OutputFormat),
Notes: strings.TrimSpace(statement.Notes),
Scoring: strings.TrimSpace(statement.Scoring),
}
}
func sanitize(statement models.Html5ProblemStatement) models.Html5ProblemStatement {
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Globally()
p.AllowAttrs("style").Globally()
p.AllowStyles("text-align").MatchingEnum("center", "left", "right").Globally()
p.AllowStyles("display").MatchingEnum("block", "inline", "inline-block").Globally()
p.AllowStandardURLs()
p.AllowAttrs("cite").OnElements("blockquote", "q")
p.AllowAttrs("href").OnElements("a", "area")
p.AllowAttrs("src").OnElements("img")
if statement.LegendHtml != "" {
statement.LegendHtml = p.Sanitize(statement.LegendHtml)
}
if statement.InputFormatHtml != "" {
statement.InputFormatHtml = p.Sanitize(statement.InputFormatHtml)
}
if statement.OutputFormatHtml != "" {
statement.OutputFormatHtml = p.Sanitize(statement.OutputFormatHtml)
}
if statement.NotesHtml != "" {
statement.NotesHtml = p.Sanitize(statement.NotesHtml)
}
if statement.ScoringHtml != "" {
statement.ScoringHtml = p.Sanitize(statement.ScoringHtml)
}
return statement
}
func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) {
p = trimSpaces(p)
latex := models.ProblemStatement{}
if p.Legend != "" {
latex.Legend = wrap(p.Legend)
}
if p.InputFormat != "" {
latex.InputFormat = wrap(p.InputFormat)
}
if p.OutputFormat != "" {
latex.OutputFormat = wrap(p.OutputFormat)
}
if p.Notes != "" {
latex.Notes = wrap(p.Notes)
}
if p.Scoring != "" {
latex.Scoring = wrap(p.Scoring)
}
req := []string{
latex.Legend,
latex.InputFormat,
latex.OutputFormat,
latex.Notes,
latex.Scoring,
}
res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req)
if err != nil {
return models.Html5ProblemStatement{}, err
}
if len(res) != len(req) {
return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res))
}
sanitizedStatement := sanitize(models.Html5ProblemStatement{
LegendHtml: res[0],
InputFormatHtml: res[1],
OutputFormatHtml: res[2],
NotesHtml: res[3],
ScoringHtml: res[4],
})
return sanitizedStatement, nil
}

View file

@ -0,0 +1,57 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type ContestStorage interface {
CreateContest(ctx context.Context, contest *models.Contest) (int32, error)
ReadContestById(ctx context.Context, id int32) (*models.Contest, error)
UpdateContest(ctx context.Context, contest *models.Contest) error
DeleteContest(ctx context.Context, id int32) error
}
type ContestService struct {
contestStorage ContestStorage
permissionService IPermissionService
}
func NewContestService(
contestStorage ContestStorage,
permissionService IPermissionService,
) *ContestService {
return &ContestService{
contestStorage: contestStorage,
permissionService: permissionService,
}
}
func (service *ContestService) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.contestStorage.CreateContest(ctx, contest)
}
func (service *ContestService) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.contestStorage.ReadContestById(ctx, id)
}
func (service *ContestService) UpdateContest(ctx context.Context, contest *models.Contest) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.contestStorage.UpdateContest(ctx, contest)
}
func (service *ContestService) DeleteContest(ctx context.Context, id int32) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.contestStorage.DeleteContest(ctx, id)
}

View file

@ -0,0 +1,28 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type LanguageStorage interface {
ReadLanguageById(ctx context.Context, id int32) (*models.Language, error)
}
type LanguageService struct {
languageStorage LanguageStorage
}
func NewLanguageService(
languageStorage LanguageStorage,
) *LanguageService {
return &LanguageService{
languageStorage: languageStorage,
}
}
func (service *LanguageService) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) {
//userId := ctx.Value("user_id").(int32)
panic("access control is not implemented yet")
return service.languageStorage.ReadLanguageById(ctx, id)
}

View file

@ -0,0 +1,57 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type ParticipantStorage interface {
CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error)
ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error)
UpdateParticipant(ctx context.Context, participant *models.Participant) error
DeleteParticipant(ctx context.Context, id int32) error
}
type ParticipantService struct {
participantStorage ParticipantStorage
permissionService IPermissionService
}
func NewParticipantService(
participantStorage ParticipantStorage,
permissionService IPermissionService,
) *ParticipantService {
return &ParticipantService{
participantStorage: participantStorage,
permissionService: permissionService,
}
}
func (service *ParticipantService) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.participantStorage.CreateParticipant(ctx, participant)
}
func (service *ParticipantService) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
return nil, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.participantStorage.ReadParticipantById(ctx, id)
}
func (service *ParticipantService) UpdateParticipant(ctx context.Context, participant *models.Participant) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.participantStorage.UpdateParticipant(ctx, participant)
}
func (service *ParticipantService) DeleteParticipant(ctx context.Context, id int32) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.participantStorage.DeleteParticipant(ctx, id)
}

View file

@ -0,0 +1,39 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/open-policy-agent/opa/rego"
)
type PermissionService struct {
query *rego.PreparedEvalQuery
}
func NewPermissionService() *PermissionService {
query, err := rego.New(
rego.Query("allow = data.problem.rbac.allow"),
rego.Load([]string{"./opa/all.rego"}, nil),
).PrepareForEval(context.TODO())
if err != nil {
panic(err)
}
return &PermissionService{
query: &query,
}
}
func (s *PermissionService) Allowed(ctx context.Context, user *models.User, action string) bool {
input := map[string]interface{}{
"user": user,
"action": action,
}
result, err := s.query.Eval(ctx, rego.EvalInput(input))
if err != nil {
panic(err)
}
return result[0].Bindings["allow"].(bool)
}

View file

@ -0,0 +1,104 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type ProblemStorage interface {
CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error)
ReadProblemById(ctx context.Context, id int32) (*models.Problem, error)
UpdateProblem(ctx context.Context, problem *models.Problem) error
DeleteProblem(ctx context.Context, id int32) error
}
type PandocClient interface {
ConvertLatexToHtml5(ctx context.Context, text string) (string, error)
}
type IPermissionService interface {
Allowed(ctx context.Context, user *models.User, action string) bool
}
type ProblemService struct {
problemStorage ProblemStorage
pandocClient PandocClient
permissionService IPermissionService
}
func NewProblemService(
problemStorage ProblemStorage,
pandocClient PandocClient,
permissionService IPermissionService,
) *ProblemService {
return &ProblemService{
problemStorage: problemStorage,
pandocClient: pandocClient,
permissionService: permissionService,
}
}
func extractUser(ctx context.Context) *models.User {
return ctx.Value("user").(*models.User)
}
func (service *ProblemService) CanCreateProblem(ctx context.Context) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return nil
}
func (service *ProblemService) CanReadProblemById(ctx context.Context) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return nil
}
func (service *ProblemService) CanUpdateProblem(ctx context.Context) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return nil
}
func (service *ProblemService) CanDeleteProblem(ctx context.Context) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return nil
}
func (service *ProblemService) CreateProblem(ctx context.Context, problem *models.Problem) (int32, error) {
if err := service.CanCreateProblem(ctx); err != nil {
return 0, err
}
_, err := service.pandocClient.ConvertLatexToHtml5(ctx, *problem.Description)
if err != nil {
return 0, err
}
return service.problemStorage.CreateProblem(ctx, problem, nil)
}
func (service *ProblemService) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
if err := service.CanReadProblemById(ctx); err != nil {
return nil, err
}
return service.problemStorage.ReadProblemById(ctx, id)
}
func (service *ProblemService) UpdateProblem(ctx context.Context, problem *models.Problem) error {
if err := service.CanUpdateProblem(ctx); err != nil {
return err
}
return service.problemStorage.UpdateProblem(ctx, problem)
}
func (service *ProblemService) DeleteProblem(ctx context.Context, id int32) error {
if err := service.CanDeleteProblem(ctx); err != nil {
return err
}
return service.problemStorage.DeleteProblem(ctx, id)
}

View file

@ -0,0 +1,57 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type SolutionStorage interface {
CreateSolution(ctx context.Context, solution models.Solution) (int32, error)
ReadSolutionById(ctx context.Context, id int32) (models.Solution, error)
RejudgeSolution(ctx context.Context, id int32) error
DeleteSolution(ctx context.Context, id int32) error
}
type SolutionService struct {
solutionStorage SolutionStorage
permissionService IPermissionService
}
func NewSolutionService(
solutionStorage SolutionStorage,
permissionService IPermissionService,
) *SolutionService {
return &SolutionService{
solutionStorage: solutionStorage,
permissionService: permissionService,
}
}
func (service *SolutionService) CreateSolution(ctx context.Context, solution models.Solution) (int32, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.solutionStorage.CreateSolution(ctx, solution)
}
func (service *SolutionService) ReadSolutionById(ctx context.Context, id int32) (models.Solution, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "read") {
return models.Solution{}, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.solutionStorage.ReadSolutionById(ctx, id)
}
func (service *SolutionService) RejudgeSolution(ctx context.Context, id int32) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "update") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.solutionStorage.RejudgeSolution(ctx, id)
}
func (service *SolutionService) DeleteSolution(ctx context.Context, id int32) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.solutionStorage.DeleteSolution(ctx, id)
}

41
internal/services/task.go Normal file
View file

@ -0,0 +1,41 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type TaskStorage interface {
CreateTask(ctx context.Context, task models.Task) (int32, error)
DeleteTask(ctx context.Context, id int32) error
}
type TaskService struct {
taskStorage TaskStorage
permissionService IPermissionService
}
func NewTaskService(
taskStorage TaskStorage,
permissionService IPermissionService,
) *TaskService {
return &TaskService{
taskStorage: taskStorage,
permissionService: permissionService,
}
}
func (service *TaskService) CreateTask(ctx context.Context, task models.Task) (int32, error) {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "create") {
return 0, lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.taskStorage.CreateTask(ctx, task)
}
func (service *TaskService) DeleteTask(ctx context.Context, id int32) error {
if !service.permissionService.Allowed(ctx, extractUser(ctx), "delete") {
return lib.ServiceError(nil, lib.ErrNoPermission, "permission denied")
}
return service.taskStorage.DeleteTask(ctx, id)
}

29
internal/services/user.go Normal file
View file

@ -0,0 +1,29 @@
package services
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
)
type UserStorage interface {
CreateUser(ctx context.Context, user *models.User) error
ReadUserById(ctx context.Context, userId int32) (*models.User, error)
}
type UserService struct {
userStorage UserStorage
}
func NewUserService(userStorage UserStorage) *UserService {
return &UserService{
userStorage: userStorage,
}
}
func (s *UserService) CreateUser(ctx context.Context, user *models.User) error {
return s.userStorage.CreateUser(ctx, user)
}
func (s *UserService) ReadUserById(ctx context.Context, userId int32) (*models.User, error) {
return s.userStorage.ReadUserById(ctx, userId)
}

View file

@ -1,183 +0,0 @@
package repository
import (
"context"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/valkey-io/valkey-go"
"strconv"
"time"
)
type ValkeyRepository struct {
db valkey.Client
}
func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
return &ValkeyRepository{
db: db,
}
}
const SessionLifetime = time.Minute * 40
func (r *ValkeyRepository) CreateSession(ctx context.Context, session *models.Session) error {
const op = "ValkeyRepository.CreateSession"
data, err := session.JSON()
if err != nil {
return pkg.Wrap(pkg.ErrInternal, err, op, "cannot marshal session")
}
resp := r.db.Do(ctx, r.db.
B().Set().
Key(session.Key()).
Value(string(data)).
Exat(session.ExpiresAt).
Build(),
)
err = resp.Error()
if err != nil {
if valkey.IsValkeyNil(err) {
return pkg.Wrap(pkg.ErrInternal, err, op, "nil response")
}
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
}
return nil
}
const (
readSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
if #result[2] == 0 then
return nil
else
return redis.call('GET', result[2][1])
end`
)
func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
const op = "ValkeyRepository.ReadSession"
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
resp := valkey.NewLuaScript(readSessionScript).Exec(
ctx,
r.db,
nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
)
if err := resp.Error(); err != nil {
if valkey.IsValkeyNil(err) {
return nil, pkg.Wrap(pkg.ErrNotFound, err, op, "reading session")
}
return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
}
session := &models.Session{}
err := resp.DecodeJSON(session)
if err != nil {
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted")
}
return session, nil
}
const (
updateSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
return #result[2] > 0 and redis.call('EXPIRE', result[2][1], ARGV[2]) == 1`
)
var (
sessionLifetimeString = strconv.Itoa(int(SessionLifetime.Seconds()))
)
func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) error {
const op = "ValkeyRepository.UpdateSession"
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
resp := valkey.NewLuaScript(updateSessionScript).Exec(
ctx,
r.db,
nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
)
err := resp.Error()
if err != nil {
if valkey.IsValkeyNil(err) {
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
}
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
}
return nil
}
const deleteSessionScript = `local result = redis.call('SCAN', 0, 'MATCH', ARGV[1])
return #result[2] > 0 and redis.call('DEL', result[2][1]) == 1`
func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) error {
const op = "ValkeyRepository.DeleteSession"
sessionIdHash := (&models.Session{Id: sessionId}).SessionIdHash()
resp := valkey.NewLuaScript(deleteSessionScript).Exec(
ctx,
r.db,
nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
)
err := resp.Error()
if err != nil {
if valkey.IsValkeyNil(err) {
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
}
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
}
return nil
}
const (
deleteUserSessionsScript = `local cursor = 0
local dels = 0
repeat
local result = redis.call('SCAN', cursor, 'MATCH', ARGV[1])
for _,key in ipairs(result[2]) do
redis.call('DEL', key)
dels = dels + 1
end
cursor = tonumber(result[1])
until cursor == 0
return dels`
)
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
const op = "ValkeyRepository.DeleteAllSessions"
userIdHash := (&models.Session{UserId: userId}).UserIdHash()
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
ctx,
r.db,
nil,
[]string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
)
err := resp.Error()
if err != nil {
if valkey.IsValkeyNil(err) {
return pkg.Wrap(pkg.ErrNotFound, err, op, "nil response")
}
return pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error")
}
return nil
}

View file

@ -1,312 +0,0 @@
package repository_test
import (
"context"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/valkey-io/valkey-go"
"github.com/valkey-io/valkey-go/mock"
"go.uber.org/mock/gomock"
"strconv"
"testing"
"time"
)
func TestValkeyRepository_CreateSession(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewClient(ctrl)
sessionRepo := repository.NewValkeyRepository(client)
t.Run("success", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
UserId: 1,
Role: models.RoleAdmin,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(repository.SessionLifetime),
UserAgent: "Mozilla/5.0",
Ip: "127.0.0.1",
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "SET" {
return false
}
if cmd[1] != session.Key() {
return false
}
if cmd[3] != "EXAT" {
return false
}
if cmd[4] != strconv.FormatInt(session.ExpiresAt.Unix(), 10) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher)
err := sessionRepo.CreateSession(ctx, session)
require.NoError(t, err)
})
}
func TestValkeyRepository_ReadSession(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewClient(ctrl)
sessionRepo := repository.NewValkeyRepository(client)
t.Run("success", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
UserId: 1,
Role: models.RoleAdmin,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(repository.SessionLifetime),
UserAgent: "Mozilla/5.0",
Ip: "127.0.0.1",
}
matcher := mock.MatchFn(func(cmd []string) bool {
fmt.Println(cmd)
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
d, err := session.JSON()
require.NoError(t, err)
ctx := context.Background()
client.EXPECT().Do(ctx, matcher).Return(mock.Result(mock.ValkeyString(string(d))))
res, err := sessionRepo.ReadSession(ctx, session.Id)
require.NoError(t, err)
fmt.Println(res.CreatedAt.Unix(), res.ExpiresAt.UnixNano())
fmt.Println(session.CreatedAt.Unix(), session.ExpiresAt.UnixNano())
require.EqualExportedValues(t, session, res)
})
t.Run("not found", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
res, err := sessionRepo.ReadSession(ctx, session.Id)
require.ErrorIs(t, err, pkg.ErrNotFound)
require.ErrorIs(t, err, valkey.Nil)
require.Empty(t, res)
})
}
func TestValkeyRepository_UpdateSession(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewClient(ctrl)
sessionRepo := repository.NewValkeyRepository(client)
t.Run("success", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher)
err := sessionRepo.UpdateSession(ctx, session.Id)
require.NoError(t, err)
})
t.Run("not found", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
err := sessionRepo.UpdateSession(ctx, session.Id)
require.ErrorIs(t, err, pkg.ErrNotFound)
require.ErrorIs(t, err, valkey.Nil)
})
}
func TestValkeyRepository_DeleteSession(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewClient(ctrl)
sessionRepo := repository.NewValkeyRepository(client)
t.Run("success", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher)
err := sessionRepo.DeleteSession(ctx, session.Id)
require.NoError(t, err)
})
t.Run("not found", func(t *testing.T) {
session := &models.Session{
Id: uuid.NewString(),
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:*:sessionid:%s", session.SessionIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
err := sessionRepo.DeleteSession(ctx, session.Id)
require.ErrorIs(t, err, pkg.ErrNotFound)
require.ErrorIs(t, err, valkey.Nil)
})
}
func TestValkeyRepository_DeleteAllSessions(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewClient(ctrl)
sessionRepo := repository.NewValkeyRepository(client)
t.Run("success", func(t *testing.T) {
session := &models.Session{
UserId: 1,
}
matcher := mock.MatchFn(func(cmd []string) bool {
fmt.Println(cmd)
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher)
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
require.NoError(t, err)
})
t.Run("not found", func(t *testing.T) {
session := &models.Session{
UserId: 1,
}
matcher := mock.MatchFn(func(cmd []string) bool {
if cmd[0] != "EVALSHA" {
return false
}
if cmd[2] != "0" {
return false
}
if cmd[3] != fmt.Sprintf("userid:%s:sessionid:*", session.UserIdHash()) {
return false
}
return true
})
ctx := context.Background()
client.EXPECT().Do(ctx, matcher).Return(mock.ErrorResult(valkey.Nil))
err := sessionRepo.DeleteAllSessions(ctx, session.UserId)
require.ErrorIs(t, err, pkg.ErrNotFound)
require.ErrorIs(t, err, valkey.Nil)
})
}

View file

@ -1,14 +0,0 @@
package sessions
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
CreateSession(ctx context.Context, creation *models.Session) error
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
UpdateSession(ctx context.Context, sessionId string) error
DeleteSession(ctx context.Context, sessionId string) error
DeleteAllSessions(ctx context.Context, userId int32) error
}

View file

@ -1,78 +0,0 @@
package usecase
import (
"context"
"git.sch9.ru/new_gate/ms-tester/config"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
type SessionsUC struct {
sessionsRepo sessions.ValkeyRepository
cfg config.Config
}
func NewUseCase(
sessionRepo sessions.ValkeyRepository,
cfg config.Config,
) *SessionsUC {
return &SessionsUC{
sessionsRepo: sessionRepo,
cfg: cfg,
}
}
// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
func (u *SessionsUC) CreateSession(ctx context.Context, creation *models.Session) error {
const op = "UseCase.CreateSession"
err := u.sessionsRepo.CreateSession(ctx, creation)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot create session")
}
return nil
}
// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
func (u *SessionsUC) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
const op = "UseCase.ReadSession"
session, err := u.sessionsRepo.ReadSession(ctx, sessionId)
if err != nil {
return nil, pkg.Wrap(nil, err, op, "cannot read session")
}
return session, nil
}
func (u *SessionsUC) UpdateSession(ctx context.Context, sessionId string) error {
const op = "UseCase.UpdateSession"
err := u.sessionsRepo.UpdateSession(ctx, sessionId)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot update session")
}
return nil
}
func (u *SessionsUC) DeleteSession(ctx context.Context, sessionId string) error {
const op = "UseCase.DeleteSession"
err := u.sessionsRepo.DeleteSession(ctx, sessionId)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot delete session")
}
return nil
}
func (u *SessionsUC) DeleteAllSessions(ctx context.Context, userId int32) error {
const op = "UseCase.DeleteAllSessions"
err := u.sessionsRepo.DeleteAllSessions(ctx, userId)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot delete all sessions")
}
return nil
}

View file

@ -1,14 +0,0 @@
package sessions
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type ValkeyRepository interface {
CreateSession(ctx context.Context, creation *models.Session) error
ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
UpdateSession(ctx context.Context, sessionId string) error
DeleteSession(ctx context.Context, sessionId string) error
DeleteAllSessions(ctx context.Context, userId int32) error
}

View file

@ -0,0 +1,78 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type ContestStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewContestStorage(db *sqlx.DB, logger *zap.Logger) *ContestStorage {
return &ContestStorage{
db: db,
logger: logger,
}
}
func (storage *ContestStorage) CreateContest(ctx context.Context, contest *models.Contest) (int32, error) {
query := storage.db.Rebind(`
INSERT INTO contests
(name)
VALUES (?)
RETURNING id
`)
rows, err := storage.db.QueryxContext(
ctx,
query,
contest.Name,
)
if err != nil {
return 0, handlePgErr(err)
}
defer rows.Close()
var id int32
err = rows.StructScan(&id)
if err != nil {
return 0, handlePgErr(err)
}
return id, nil
}
func (storage *ContestStorage) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
var contest models.Contest
query := storage.db.Rebind("SELECT * from contests WHERE id=? LIMIT 1")
err := storage.db.GetContext(ctx, &contest, query, id)
if err != nil {
return nil, handlePgErr(err)
}
return &contest, nil
}
func (storage *ContestStorage) UpdateContest(ctx context.Context, contest *models.Contest) error {
query := storage.db.Rebind("UPDATE contests SET name=? WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, contest.Name, contest.Id)
if err != nil {
return handlePgErr(err)
}
return nil
}
func (storage *ContestStorage) DeleteContest(ctx context.Context, id int32) error {
query := storage.db.Rebind("DELETE FROM contests WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err)
}
return nil
}

View file

@ -0,0 +1,23 @@
package storage
import (
"errors"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
)
func handlePgErr(err error) error {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) {
return lib.StorageError(err, lib.ErrUnknown, "unexpected error from postgres")
}
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
// TODO: probably should specify which constraint
return lib.StorageError(err, lib.ErrConflict, pgErr.Message)
}
if pgerrcode.IsNoData(pgErr.Code) {
return lib.StorageError(err, lib.ErrNotFound, pgErr.Message)
}
return lib.StorageError(err, lib.ErrUnimplemented, "unimplemented error")
}

View file

@ -0,0 +1,28 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type LanguageStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewLanguageStorage(db *sqlx.DB, logger *zap.Logger) *LanguageStorage {
return &LanguageStorage{
db: db,
logger: logger,
}
}
func (storage *LanguageStorage) ReadLanguageById(ctx context.Context, id int32) (*models.Language, error) {
if(id<=int32(len(models.Languages))) {
return nil,lib.StorageError(nil,lib.ErrNotFound,"language not found")
}
return &models.Languages[id], nil
}

View file

@ -0,0 +1,79 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type ParticipantStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewParticipantStorage(db *sqlx.DB, logger *zap.Logger) *ParticipantStorage {
return &ParticipantStorage{
db: db,
logger: logger,
}
}
func (storage *ParticipantStorage) CreateParticipant(ctx context.Context, participant *models.Participant) (int32, error) {
query := storage.db.Rebind(`
INSERT INTO participants
(user_id,contest_id,name)
VALUES (?,?,?)
RETURNING id
`)
rows, err := storage.db.QueryxContext(
ctx,
query,
participant.UserId,
participant.ContestId,
participant.Name,
)
if err != nil {
return 0, handlePgErr(err)
}
defer rows.Close()
var id int32
err = rows.StructScan(&id)
if err != nil {
return 0, handlePgErr(err)
}
return id, nil
}
func (storage *ParticipantStorage) ReadParticipantById(ctx context.Context, id int32) (*models.Participant, error) {
var participant models.Participant
query := storage.db.Rebind("SELECT * from participants WHERE id=? LIMIT 1")
err := storage.db.GetContext(ctx, &participant, query, id)
if err != nil {
return nil, handlePgErr(err)
}
return &participant, nil
}
func (storage *ParticipantStorage) UpdateParticipant(ctx context.Context, id int32, participant models.Participant) error {
query := storage.db.Rebind("UPDATE participants SET name=?")
_, err := storage.db.ExecContext(ctx, query, participant.Name)
if err != nil {
return handlePgErr(err)
}
return nil
}
func (storage *ParticipantStorage) DeleteParticipant(ctx context.Context, id int32) error {
query := storage.db.Rebind("DELETE FROM participants WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err)
}
return nil
}

110
internal/storage/problem.go Normal file
View file

@ -0,0 +1,110 @@
package storage
import (
"context"
"errors"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type ProblemStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewProblemStorage(db *sqlx.DB, logger *zap.Logger) *ProblemStorage {
return &ProblemStorage{
db: db,
logger: logger,
}
}
func (storage *ProblemStorage) CreateProblem(ctx context.Context, problem *models.Problem, testGroupData []models.TestGroupData) (int32, error) {
tx, err := storage.db.Beginx()
if err != nil {
return 0, handlePgErr(err)
}
query := tx.Rebind(`
INSERT INTO problems
(name,description,time_limit,memory_limit)
VALUES (?, ?, ?, ?)
RETURNING id
`)
rows, err := tx.QueryxContext(
ctx,
query,
problem.Name,
problem.Description,
problem.TimeLimit,
problem.MemoryLimit,
)
if err != nil {
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
}
for _, tgd := range testGroupData {
query := tx.Rebind(`
INSERT INTO testgroups
(problem_id,testing_strategy)
VALUES ((select last_value from problems_id_seq),?)
RETURNING id
`)
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
if err != nil {
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
}
var i int32 = 0
for ; i < tgd.TestAmount; i++ {
query := tx.Rebind(`
INSERT INTO tests
(testgroup_id)
VALUES ((select last_value from testgroups_id_seq))
RETURNING id
`)
rows, err = tx.QueryxContext(ctx, query, tgd.Ts)
if err != nil {
return 0, handlePgErr(errors.Join(err, tx.Rollback()))
}
}
}
err = tx.Commit()
//add test saving
defer rows.Close()
var id int32
err = rows.StructScan(&id)
if err != nil {
return 0, handlePgErr(err)
}
return id, nil
}
func (storage *ProblemStorage) ReadProblemById(ctx context.Context, id int32) (*models.Problem, error) {
var problem models.Problem
query := storage.db.Rebind("SELECT * from problems WHERE id=? LIMIT 1")
err := storage.db.GetContext(ctx, &problem, query, id)
if err != nil {
return nil, handlePgErr(err)
}
return &problem, nil
}
func (storage *ProblemStorage) UpdateProblem(ctx context.Context, problem *models.Problem) error {
query := storage.db.Rebind("UPDATE problems SET name=?,description=?,time_limit=?,memory_limit=? WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, problem.Name, problem.Description, problem.TimeLimit, problem.MemoryLimit, problem.Id)
if err != nil {
return handlePgErr(err)
}
return nil
}
func (storage *ProblemStorage) DeleteProblem(ctx context.Context, id int32) error {
query := storage.db.Rebind("DELETE FROM problems WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err)
}
return nil
}

View file

@ -0,0 +1,114 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type SolutionStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewSolutionStorage(db *sqlx.DB, logger *zap.Logger) *SolutionStorage {
return &SolutionStorage{
db: db,
logger: logger,
}
}
// TODO: testing graph
func (storage *SolutionStorage) updateResult(ctx context.Context, participantId int32, taskId int32) error {
tx, err := storage.db.Beginx()
if err != nil {
return handlePgErr(err)
}
query := tx.Rebind("UPDATE participant_subtask AS ps SET best_score = (SELECT COALESCE(max(score),0) FROM subtaskruns WHERE subtask_id = ps.subtask_id AND solution_id IN (SELECT id FROM solutions WHERE participant_id=ps.participant_id)) WHERE participant_id = 2 AND subtask_id IN (SELECT id FROM subtasks WHERE task_id = 2)")
tx.QueryxContext(ctx, query, participantId, taskId)
query = tx.Rebind("UPDATE participant_task SET best_score = (select max(best_score) from participant_subtask WHERE participant_id = ? AND subtask_id IN (SELECT id FROM subtask WHERE task_id = ?)) WHERE participant_id = ? AND task_id = ?")
tx.QueryxContext(ctx, query, participantId, taskId, participantId, taskId)
err = tx.Commit()
if err != nil {
return handlePgErr(err)
}
return nil
}
func (storage *SolutionStorage) CreateSolution(ctx context.Context, solution *models.Solution) (int32, error) {
query := storage.db.Rebind(`
INSERT INTO solutions
(participant_id,task_id,language_id,solution_hash,result)
VALUES (?, ?, ?, ?, ?)
RETURNING id
`)
rows, err := storage.db.QueryxContext(
ctx,
query,
solution.ParticipantId,
solution.TaskId,
solution.LanguageId,
"", //FIXME
models.NotTested,
)
//TODO: add testing tree
if err != nil {
return 0, handlePgErr(err)
}
defer rows.Close()
var id int32
err = rows.StructScan(&id)
if err != nil {
return 0, handlePgErr(err)
}
return id, nil
}
func (storage *SolutionStorage) ReadSolutionById(ctx context.Context, id int32) (*models.Solution, error) {
var solution models.Solution
query := storage.db.Rebind("SELECT * from solutions WHERE id=? LIMIT 1")
err := storage.db.GetContext(ctx, &solution, query, id)
if err != nil {
return nil, handlePgErr(err)
}
return &solution, nil
}
func (storage *SolutionStorage) RejudgeSolution(ctx context.Context, id int32) error {
tx, err := storage.db.Beginx()
if err != nil {
return handlePgErr(err)
}
query := tx.Rebind("UPDATE solutions SET result = ? WHERE id = ?")
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
query = tx.Rebind("UPDATE subtaskruns SET result = ?,score = 0 WHERE solution_id = ?")
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
query = tx.Rebind("UPDATE testruns SET result = ?, score = 0 WHERE testgrouprun_id IN (SELECT id FROM tesgrouprun WHERE solution_id = ?)")
tx.QueryxContext(ctx, query, models.NotTested, id) // FIXME
err = tx.Commit()
var solution models.Solution
query = storage.db.Rebind("SELECT * from solutions WHERE id=? LIMIT 1")
err = storage.db.GetContext(ctx, &solution, query, id)
if err != nil {
return handlePgErr(err)
}
storage.updateResult(ctx, *solution.ParticipantId, *solution.TaskId) // FIXME
return nil
}
func (storage *SolutionStorage) DeleteSolution(ctx context.Context, id int32) error {
query := storage.db.Rebind("DELETE FROM solutions WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err)
}
return nil
}

80
internal/storage/task.go Normal file
View file

@ -0,0 +1,80 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type TaskStorage struct {
db *sqlx.DB
logger *zap.Logger
}
func NewTaskStorage(db *sqlx.DB, logger *zap.Logger) *TaskStorage {
return &TaskStorage{
db: db,
logger: logger,
}
}
func (storage *TaskStorage) CreateTask(ctx context.Context, task *models.Task) (int32, error) {
query := storage.db.Rebind(`
INSERT INTO tasks
(contest_id,problem_id,position,position_name)
VALUES (?, ?, ?, ?)
RETURNING id
`)
rows, err := storage.db.QueryxContext(
ctx,
query,
task.ContestId,
task.ProblemId,
task.Position,
task.PositionName,
)
if err != nil {
return 0, handlePgErr(err)
}
defer rows.Close()
var id int32
err = rows.StructScan(&id)
if err != nil {
return 0, handlePgErr(err)
}
return id, nil
}
func (storage *TaskStorage) ReadTaskById(ctx context.Context, id int32) (*models.Task, error) {
var task models.Task
query := storage.db.Rebind("SELECT * from tasks WHERE id=? LIMIT 1")
err := storage.db.GetContext(ctx, &task, query, id)
if err != nil {
return nil, handlePgErr(err)
}
return &task, nil
}
func (storage *TaskStorage) UpdateTask(ctx context.Context, id int32, task models.Task) error {
query := storage.db.Rebind("UPDATE tasks SET position=?,position_name=?")
_, err := storage.db.ExecContext(ctx, query, task.Position, task.PositionName)
if err != nil {
return handlePgErr(err)
}
return nil
}
func (storage *TaskStorage) DeleteTask(ctx context.Context, id int32) error {
query := storage.db.Rebind("DELETE FROM tasks WHERE id=?")
_, err := storage.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err)
}
return nil
}

42
internal/storage/user.go Normal file
View file

@ -0,0 +1,42 @@
package storage
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
"github.com/jmoiron/sqlx"
)
type UserStorage struct {
db *sqlx.DB
}
func NewUserStorage(db *sqlx.DB) *UserStorage {
return &UserStorage{
db: db,
}
}
func (storage *UserStorage) CreateUser(ctx context.Context, user *models.User) error {
query := storage.db.Rebind("INSERT INTO users (user_id, role) VALUES (?, ?)")
_, err := storage.db.ExecContext(ctx, query, user.UserId, user.Role)
if err != nil {
return err
}
return nil
}
func (storage *UserStorage) ReadUserById(ctx context.Context, userId int32) (*models.User, error) {
query := storage.db.Rebind(`
SELECT *
FROM users
WHERE user_id = ?
LIMIT 1;
`)
var user models.User
err := storage.db.GetContext(ctx, &user, query, userId)
if err != nil {
return nil, err
}
return &user, nil
}

View file

@ -0,0 +1,155 @@
package transport
import (
"context"
"errors"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
var defaultUser = &models.User{
UserId: nil,
Role: models.RoleSpectator.AsPointer(),
UpdatedAt: nil,
}
func extractToken(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
tokens := md.Get("token")
if len(tokens) == 0 {
return ""
}
return tokens[0]
}
func (s *TesterServer) readSessionAndReadUser(ctx context.Context, token string) (*models.User, error) {
// FIXME: possible bottle neck: should we cache it? (think of it in future)
// FIXME: maybe use single connection instead of multiple requests
userId, err := s.sessionClient.Read(ctx, &sessionv1.ReadSessionRequest{Token: token})
if err != nil {
return nil, err
}
user, err := s.userService.ReadUserById(ctx, userId.GetUserId()) // FIXME: must be cached!
if err != nil {
if errors.Is(err, lib.ErrNotFound) {
user = &models.User{
UserId: lib.AsInt32P(userId.GetUserId()),
Role: models.RoleParticipant.AsPointer(),
}
err = s.userService.CreateUser(ctx, user)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
return user, nil
}
func insertUser(ctx context.Context, user *models.User) context.Context {
return context.WithValue(ctx, "user", user)
}
func (s *TesterServer) AuthUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
token := extractToken(ctx)
if token == "" {
return handler(insertUser(ctx, defaultUser), req)
}
user, err := s.readSessionAndReadUser(ctx, token)
if err != nil {
return handler(insertUser(ctx, defaultUser), req)
}
return handler(insertUser(ctx, user), req)
}
}
type ssWrapper struct {
grpc.ServerStream
ctx context.Context
}
func (s *ssWrapper) Context() context.Context {
return s.ctx
}
func (s *TesterServer) AuthStreamInterceptor() grpc.StreamServerInterceptor {
return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
token := extractToken(ctx)
if token == "" {
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
}
user, err := s.readSessionAndReadUser(ctx, token)
if err != nil {
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, defaultUser)})
}
return handler(server, &ssWrapper{ServerStream: ss, ctx: insertUser(ctx, user)})
}
}
func ToGrpcError(err error) error {
if err == nil {
return nil
}
// should I use map instead?
switch {
case errors.Is(err, lib.ErrValidationFailed):
return status.Error(codes.InvalidArgument, err.Error())
case errors.Is(err, lib.ErrInternal):
return status.Error(codes.Internal, err.Error())
case errors.Is(err, lib.ErrExternal):
return status.Error(codes.Unavailable, err.Error())
case errors.Is(err, lib.ErrNoPermission):
return status.Error(codes.PermissionDenied, err.Error())
case errors.Is(err, lib.ErrUnknown):
return status.Error(codes.Unknown, err.Error())
case errors.Is(err, lib.ErrDeadlineExceeded):
return status.Error(codes.DeadlineExceeded, err.Error())
case errors.Is(err, lib.ErrNotFound):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, lib.ErrAlreadyExists):
return status.Error(codes.AlreadyExists, err.Error())
case errors.Is(err, lib.ErrConflict):
return status.Error(codes.Unimplemented, err.Error())
case errors.Is(err, lib.ErrUnimplemented):
return status.Error(codes.Unimplemented, err.Error())
case errors.Is(err, lib.ErrUnauthenticated):
return status.Error(codes.Unauthenticated, err.Error())
default:
return status.Error(codes.Unknown, err.Error())
}
}
func (s *TesterServer) ErrUnwrappingUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
return resp, ToGrpcError(err)
}
}
func (s *TesterServer) ErrUnwrappingStreamInterceptor() grpc.StreamServerInterceptor {
return func(server interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := handler(server, ss)
return ToGrpcError(err)
}
}

View file

@ -0,0 +1,162 @@
package transport
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"os"
)
func (s *TesterServer) CreateProblem(server problemv1.ProblemService_CreateProblemServer) error {
ctx := server.Context()
if err := s.problemService.CanCreateProblem(ctx); err != nil {
return err
}
req, err := server.Recv() // receive problem
if err != nil {
return lib.TransportError(err, lib.ErrBadInput, "can't receive problem")
}
problem := req.GetProblem()
if problem == nil {
return lib.TransportError(nil, lib.ErrBadInput, "empty problem")
}
p := &models.Problem{
Name: lib.AsStringP(problem.Name),
Description: lib.AsStringP(problem.Description),
TimeLimit: lib.AsInt32P(problem.TimeLimit),
MemoryLimit: lib.AsInt32P(problem.MemoryLimit),
}
ch := readChunks(ctx, server)
err = writeChunks(ctx, ch) // temp stub
if err != nil {
return err // FIXME
}
id, err := s.problemService.CreateProblem(ctx, p) // FIXME
if err != nil {
return err
}
err = server.SendAndClose(&problemv1.CreateProblemResponse{
Id: id,
})
if err != nil {
return lib.TransportError(err, lib.ErrBadInput, "can't send response")
}
return nil
}
func writeChunks(ctx context.Context, chunks <-chan []byte) error {
// use s3
// FIXME: use ctx?
f, err := os.Create("out.txt") // FIXME: uuidv4 as initial temp name?
if err != nil {
return err
}
defer f.Close()
var off int64 = 0
for chunk := range chunks {
_, err = f.WriteAt(chunk, off)
if err != nil {
return err
}
off += int64(len(chunk))
}
// TODO: rename file to its hash
return nil
}
func readChunks(ctx context.Context, server problemv1.ProblemService_CreateProblemServer) <-chan []byte {
ch := make(chan []byte)
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return // FIXME
default:
req, err := server.Recv()
if err != nil {
if err == io.EOF {
return // FIXME
}
if status.Code(err) == codes.Canceled {
return // FIXME
}
continue
}
test := req.GetTest()
if test == nil {
return // FIXME
}
ch <- test.Chunk
}
}
}()
return ch
}
func (s *TesterServer) ReadProblem(ctx context.Context, req *problemv1.ReadProblemRequest) (*problemv1.ReadProblemResponse, error) {
problem, err := s.problemService.ReadProblemById(ctx, req.GetId())
if err != nil {
return nil, err
}
return &problemv1.ReadProblemResponse{
Problem: &problemv1.ReadProblemResponse_Problem{
Id: *problem.Id,
Name: *problem.Name,
Description: *problem.Description,
TimeLimit: *problem.TimeLimit,
MemoryLimit: *problem.MemoryLimit,
CreatedAt: AsTimestampP(problem.CreatedAt),
UpdatedAt: AsTimestampP(problem.UpdatedAt),
},
}, nil
}
//func (s *TesterServer) UpdateProblem(ctx context.Context, req *problemv1.UpdateProblemRequest) (*emptypb.Empty, error) {
// problem := req.GetProblem()
// if problem == nil {
// return nil, status.Errorf(codes.Unknown, "") // FIXME
// }
// err := s.problemService.UpdateProblem(
// ctx,
// &models.Problem{
// Id: lib.AsInt32P(problem.Id),
// Name: problem.Name,
// Description: problem.Description,
// TimeLimit: problem.TimeLimit,
// MemoryLimit: problem.MemoryLimit,
// },
// )
// if err != nil {
// return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME
// }
//
// return &emptypb.Empty{}, nil
//}
func (s *TesterServer) DeleteProblem(ctx context.Context, req *problemv1.DeleteProblemRequest) (*emptypb.Empty, error) {
err := s.problemService.DeleteProblem(ctx, req.GetId())
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}

View file

@ -0,0 +1,95 @@
package transport
import (
"context"
"git.sch9.ru/new_gate/ms-tester/pkg/models"
problemv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/problem/v1"
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
"net"
"time"
"google.golang.org/grpc"
)
type ProblemService interface {
CanCreateProblem(ctx context.Context) error
CreateProblem(ctx context.Context, problem *models.Problem) (int32, error)
ReadProblemById(ctx context.Context, id int32) (*models.Problem, error)
UpdateProblem(ctx context.Context, problem *models.Problem) error
DeleteProblem(ctx context.Context, id int32) error
}
type SessionClient interface {
Read(ctx context.Context,
in *sessionv1.ReadSessionRequest,
opts ...grpc.CallOption,
) (*sessionv1.ReadSessionResponse, error)
}
type UserService interface {
CreateUser(ctx context.Context, user *models.User) error
ReadUserById(ctx context.Context, userId int32) (*models.User, error)
}
type TesterServer struct {
problemv1.UnimplementedProblemServiceServer
problemService ProblemService
sessionClient SessionClient
userService UserService
grpcServer *grpc.Server
logger *zap.Logger
}
func NewTesterServer(
problemService ProblemService,
sessionClient SessionClient,
userService UserService,
logger *zap.Logger,
) *TesterServer {
server := &TesterServer{
problemService: problemService,
sessionClient: sessionClient,
userService: userService,
logger: logger,
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(server.ErrUnwrappingUnaryInterceptor()),
grpc.StreamInterceptor(server.ErrUnwrappingStreamInterceptor()),
grpc.UnaryInterceptor(server.AuthUnaryInterceptor()),
grpc.StreamInterceptor(server.AuthStreamInterceptor()),
)
problemv1.RegisterProblemServiceServer(grpcServer, server)
server.grpcServer = grpcServer
return server
}
func (s *TesterServer) Start(lis net.Listener) error {
return s.grpcServer.Serve(lis)
}
func (s *TesterServer) Stop() {
s.grpcServer.GracefulStop()
}
func AsTimeP(t *timestamppb.Timestamp) *time.Time {
if t == nil {
return nil
}
tt := t.AsTime()
return &tt
}
func AsTimestampP(t *time.Time) *timestamppb.Timestamp {
if t == nil {
return nil
}
return timestamppb.New(*t)
}

View file

@ -1,14 +0,0 @@
package users
import (
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"github.com/gofiber/fiber/v2"
)
type UsersHandlers interface {
ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error
CreateUser(c *fiber.Ctx) error
DeleteUser(c *fiber.Ctx, id int32) error
GetUser(c *fiber.Ctx, id int32) error
UpdateUser(c *fiber.Ctx, id int32) error
}

View file

@ -1,204 +0,0 @@
package rest
import (
"context"
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/users"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
)
type Handlers struct {
usersUC users.UseCase
}
func NewHandlers(usersUC users.UseCase) *Handlers {
return &Handlers{
usersUC: usersUC,
}
}
const (
sessionKey = "session"
)
func sessionFromCtx(ctx context.Context) (*models.Session, error) {
const op = "sessionFromCtx"
session, ok := ctx.Value(sessionKey).(*models.Session)
if !ok {
return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "")
}
return session, nil
}
func (h *Handlers) CreateUser(c *fiber.Ctx) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
var req = &testerv1.CreateUserRequest{}
err := c.BodyParser(req)
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
id, err := h.usersUC.CreateUser(ctx,
&models.UserCreation{
Username: req.Username,
Password: req.Password,
Role: models.RoleStudent,
},
)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateUserResponse{Id: id})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) GetUser(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher, models.RoleStudent:
user, err := h.usersUC.ReadUserById(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.GetUserResponse{
User: UserDTO(*user),
})
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) UpdateUser(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin:
var req = &testerv1.UpdateUserRequest{}
err := c.BodyParser(req)
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
err = h.usersUC.UpdateUser(c.Context(), id, &models.UserUpdate{
Username: req.Username,
Role: RoleDTO(req.Role),
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) DeleteUser(c *fiber.Ctx, id int32) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin:
ctx := c.Context()
err := h.usersUC.DeleteUser(ctx, id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.SendStatus(fiber.StatusOK)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func (h *Handlers) ListUsers(c *fiber.Ctx, params testerv1.ListUsersParams) error {
ctx := c.Context()
session, err := sessionFromCtx(ctx)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
switch session.Role {
case models.RoleAdmin, models.RoleTeacher:
usersList, err := h.usersUC.ListUsers(c.Context(), models.UsersListFilters{
PageSize: params.PageSize,
Page: params.Page,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.ListUsersResponse{
Users: make([]testerv1.User, len(usersList.Users)),
Pagination: PaginationDTO(usersList.Pagination),
}
for i, user := range usersList.Users {
resp.Users[i] = UserDTO(*user)
}
return c.JSON(resp)
default:
return c.SendStatus(pkg.ToREST(pkg.NoPermission))
}
}
func RoleDTO(i *int32) *models.Role {
if i == nil {
return nil
}
ii := models.Role(*i)
return &ii
}
func PaginationDTO(p models.Pagination) testerv1.Pagination {
return testerv1.Pagination{
Page: p.Page,
Total: p.Total,
}
}
// UserDTO sanitizes password
func UserDTO(u models.User) testerv1.User {
return testerv1.User{
Id: u.Id,
Username: u.Username,
Role: int32(u.Role),
CreatedAt: u.CreatedAt,
ModifiedAt: u.UpdatedAt,
}
}

View file

@ -1,33 +0,0 @@
package users
import (
"context"
"database/sql"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
)
type Querier interface {
Rebind(query string) string
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
type Tx interface {
Querier
Commit() error
Rollback() error
}
type Repository interface {
BeginTx(ctx context.Context) (Tx, error)
DB() Querier
CreateUser(ctx context.Context, q Querier, user *models.UserCreation) (int32, error)
ReadUserByUsername(ctx context.Context, q Querier, username string) (*models.User, error)
ReadUserById(ctx context.Context, q Querier, id int32) (*models.User, error)
UpdateUser(ctx context.Context, q Querier, id int32, update *models.UserUpdate) error
DeleteUser(ctx context.Context, q Querier, id int32) error
ListUsers(ctx context.Context, q Querier, filters models.UsersListFilters) (*models.UsersList, error)
}

View file

@ -1,156 +0,0 @@
package repository
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/users"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/jmoiron/sqlx"
)
type Repository struct {
_db *sqlx.DB
}
func NewRepository(db *sqlx.DB) *Repository {
return &Repository{
_db: db,
}
}
func (r *Repository) BeginTx(ctx context.Context) (users.Tx, error) {
tx, err := r._db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
return tx, nil
}
func (r *Repository) DB() users.Querier {
return r._db
}
const CreateUserQuery = `
INSERT INTO users
(username, hashed_pwd, role)
VALUES ($1, $2, $3)
RETURNING id
`
func (r *Repository) CreateUser(ctx context.Context, q users.Querier, user *models.UserCreation) (int32, error) {
const op = "Caller.CreateUser"
rows, err := q.QueryxContext(
ctx,
CreateUserQuery,
user.Username,
user.Password,
user.Role,
)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, pkg.HandlePgErr(err, op)
}
return id, nil
}
const ReadUserByUsernameQuery = "SELECT * from users WHERE username=$1 LIMIT 1"
func (r *Repository) ReadUserByUsername(ctx context.Context, q users.Querier, username string) (*models.User, error) {
const op = "Caller.ReadUserByUsername"
var user models.User
err := q.GetContext(ctx, &user, ReadUserByUsernameQuery, username)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &user, nil
}
const ReadUserByIdQuery = "SELECT * from users WHERE id=$1 LIMIT 1"
func (r *Repository) ReadUserById(ctx context.Context, q users.Querier, id int32) (*models.User, error) {
const op = "Caller.ReadUserById"
var user models.User
err := q.GetContext(ctx, &user, ReadUserByIdQuery, id)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &user, nil
}
const UpdateUserQuery = `
UPDATE users
SET username = COALESCE($1, username),
role = COALESCE($2, role)
WHERE id = $3
`
func (r *Repository) UpdateUser(ctx context.Context, q users.Querier, id int32, update *models.UserUpdate) error {
const op = "Caller.UpdateUser"
_, err := q.ExecContext(
ctx,
UpdateUserQuery,
update.Username,
update.Role,
id,
)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const DeleteUserQuery = "DELETE FROM users WHERE id = $1"
func (r *Repository) DeleteUser(ctx context.Context, q users.Querier, id int32) error {
const op = "Caller.DeleteUser"
_, err := q.ExecContext(ctx, DeleteUserQuery, id)
if err != nil {
return pkg.HandlePgErr(err, op)
}
return nil
}
const (
ListUsersQuery = "SELECT * FROM users LIMIT $1 OFFSET $2"
CountUsersQuery = "SELECT COUNT(*) FROM users"
)
func (r *Repository) ListUsers(ctx context.Context, q users.Querier, filters models.UsersListFilters) (*models.UsersList, error) {
const op = "Caller.ListUsers"
list := make([]*models.User, 0)
err := q.SelectContext(ctx, &list, ListUsersQuery, filters.PageSize, filters.Offset())
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
var count int32
err = q.GetContext(ctx, &count, CountUsersQuery)
if err != nil {
return nil, pkg.HandlePgErr(err, op)
}
return &models.UsersList{
Users: list,
Pagination: models.Pagination{
Total: models.Total(count, filters.PageSize),
Page: filters.Page,
},
}, nil
}

View file

@ -1,222 +0,0 @@
package repository_test
import (
"context"
"database/sql"
"testing"
"time"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/users/repository"
"github.com/DATA-DOG/go-sqlmock"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
)
// setupTestDB creates a mocked sqlx.DB and sqlmock instance for testing.
func setupTestDB(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock) {
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
assert.NoError(t, err)
sqlxDB := sqlx.NewDb(db, "sqlmock")
return sqlxDB, mock
}
func TestRepository_CreateUser(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
var expectedId int32 = 1
user := &models.UserCreation{
Username: "testuser",
Password: "hashed-password",
Role: models.RoleAdmin,
}
mock.ExpectQuery(repository.CreateUserQuery).
WithArgs(user.Username, sqlmock.AnyArg(), user.Role).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedId))
id, err := repo.CreateUser(ctx, db, user)
assert.NoError(t, err)
assert.Equal(t, expectedId, id)
})
}
func TestRepository_ReadUserByUsername(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
expected := &models.User{
Id: 1,
Username: "testuser",
HashedPassword: "hashed-password",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Role: models.RoleAdmin,
}
columns := []string{
"id",
"username",
"hashed_pwd",
"created_at",
"updated_at",
"role",
}
rows := sqlmock.NewRows(columns).AddRow(
expected.Id,
expected.Username,
expected.HashedPassword,
expected.CreatedAt,
expected.UpdatedAt,
expected.Role,
)
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(expected.Username).WillReturnRows(rows)
user, err := repo.ReadUserByUsername(ctx, db, expected.Username)
assert.NoError(t, err)
assert.Equal(t, expected, user)
})
t.Run("not found", func(t *testing.T) {
ctx := context.Background()
username := "testuser"
mock.ExpectQuery(repository.ReadUserByUsernameQuery).WithArgs(username).WillReturnError(sql.ErrNoRows)
user, err := repo.ReadUserByUsername(ctx, db, username)
assert.Error(t, err)
assert.Nil(t, user)
})
}
func TestRepository_ReadUserById(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
expected := &models.User{
Id: 1,
Username: "testuser",
Role: models.RoleAdmin,
}
mock.ExpectQuery(repository.ReadUserByIdQuery).
WithArgs(expected.Id).
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
AddRow(expected.Id, expected.Username, expected.Role))
user, err := repo.ReadUserById(ctx, db, expected.Id)
assert.NoError(t, err)
assert.Equal(t, expected, user)
})
t.Run("not found", func(t *testing.T) {
ctx := context.Background()
userID := int32(1)
mock.ExpectQuery(repository.ReadUserByIdQuery).WithArgs(userID).WillReturnError(sql.ErrNoRows)
user, err := repo.ReadUserById(ctx, db, userID)
assert.Error(t, err)
assert.Nil(t, user)
})
}
func TestRepository_UpdateUser(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
userID := int32(1)
username := "testuser"
role := models.RoleStudent
update := &models.UserUpdate{
Username: &username,
Role: &role,
}
mock.ExpectExec(repository.UpdateUserQuery).
WithArgs(update.Username, update.Role, userID).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.UpdateUser(ctx, db, userID, update)
assert.NoError(t, err)
})
}
func TestRepository_DeleteUser(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
userID := int32(1)
mock.ExpectExec(repository.DeleteUserQuery).
WithArgs(userID).
WillReturnResult(sqlmock.NewResult(0, 1))
err := repo.DeleteUser(ctx, db, userID)
assert.NoError(t, err)
})
}
func TestRepository_ListUsers(t *testing.T) {
db, mock := setupTestDB(t)
defer db.Close()
repo := repository.NewRepository(db)
t.Run("success", func(t *testing.T) {
ctx := context.Background()
filters := models.UsersListFilters{
Page: 1,
PageSize: 10,
}
expectedUsers := []*models.User{
{Id: 1, Username: "user1", Role: models.RoleAdmin},
{Id: 2, Username: "user2", Role: models.RoleStudent},
}
totalCount := int32(2)
mock.ExpectQuery(repository.ListUsersQuery).
WithArgs(filters.PageSize, filters.Offset()).
WillReturnRows(sqlmock.NewRows([]string{"id", "username", "role"}).
AddRow(expectedUsers[0].Id, expectedUsers[0].Username, expectedUsers[0].Role).
AddRow(expectedUsers[1].Id, expectedUsers[1].Username, expectedUsers[1].Role))
mock.ExpectQuery(repository.CountUsersQuery).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalCount))
result, err := repo.ListUsers(ctx, db, filters)
assert.NoError(t, err)
assert.Equal(t, expectedUsers, result.Users)
assert.Equal(t, models.Pagination{Total: 1, Page: 1}, result.Pagination)
})
}

View file

@ -1,15 +0,0 @@
package users
import (
"context"
"git.sch9.ru/new_gate/ms-tester/internal/models"
)
type UseCase interface {
CreateUser(ctx context.Context, user *models.UserCreation) (int32, error)
ReadUserById(ctx context.Context, id int32) (*models.User, error)
ReadUserByUsername(ctx context.Context, username string) (*models.User, error)
UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error
DeleteUser(ctx context.Context, id int32) error
ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error)
}

View file

@ -1,164 +0,0 @@
package usecase
import (
"context"
"errors"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/sessions"
"git.sch9.ru/new_gate/ms-tester/internal/users"
"git.sch9.ru/new_gate/ms-tester/pkg"
)
type UsersUC struct {
sessionRepo sessions.ValkeyRepository
usersRepo users.Repository
}
func NewUseCase(sessionRepo sessions.ValkeyRepository, usersRepo users.Repository) *UsersUC {
return &UsersUC{
sessionRepo: sessionRepo,
usersRepo: usersRepo,
}
}
func (u *UsersUC) CreateUser(ctx context.Context, user *models.UserCreation) (int32, error) {
const op = "UseCase.CreateUser"
err := user.HashPassword()
if err != nil {
return 0, pkg.Wrap(pkg.ErrBadInput, err, op, "bad password")
}
id, err := u.usersRepo.CreateUser(ctx, u.usersRepo.DB(), user)
if err != nil {
return 0, pkg.Wrap(nil, err, op, "can't create user")
}
return id, nil
}
func (u *UsersUC) ListUsers(ctx context.Context, filters models.UsersListFilters) (*models.UsersList, error) {
const op = "UseCase.ListUsers"
usersList, err := u.usersRepo.ListUsers(ctx, u.usersRepo.DB(), filters)
if err != nil {
return nil, pkg.Wrap(nil, err, op, "can't list users")
}
return usersList, nil
}
func (u *UsersUC) UpdateUser(ctx context.Context, id int32, update *models.UserUpdate) error {
const op = "UseCase.UpdateUser"
tx, err := u.usersRepo.BeginTx(ctx)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot start transaction")
}
err = u.usersRepo.UpdateUser(ctx, tx, id, update)
if err != nil {
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot update user")
}
err = u.sessionRepo.DeleteAllSessions(ctx, id)
if err != nil {
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
}
err = tx.Commit()
if err != nil {
return pkg.Wrap(nil, err, op, "cannot commit transaction")
}
return nil
}
// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE!
func (u *UsersUC) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) {
const op = "UseCase.ReadUserByUsername"
user, err := u.usersRepo.ReadUserByUsername(ctx, u.usersRepo.DB(), username)
if err != nil {
return nil, pkg.Wrap(nil, err, op, "can't read user by username")
}
return user, nil
}
func (u *UsersUC) ReadUserById(ctx context.Context, id int32) (*models.User, error) {
const op = "UseCase.ReadUserById"
user, err := u.usersRepo.ReadUserById(ctx, u.usersRepo.DB(), id)
if err != nil {
return nil, pkg.Wrap(nil, err, op, "can't read user by id")
}
return user, nil
}
func (u *UsersUC) DeleteUser(ctx context.Context, id int32) error {
const op = "UseCase.DeleteUser"
tx, err := u.usersRepo.BeginTx(ctx)
if err != nil {
return pkg.Wrap(nil, err, op, "cannot start transaction")
}
err = u.usersRepo.DeleteUser(ctx, tx, id)
if err != nil {
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete user")
}
err = u.sessionRepo.DeleteAllSessions(ctx, id)
if err != nil {
return pkg.Wrap(nil, errors.Join(err, tx.Rollback()), op, "cannot delete all sessions")
}
err = tx.Commit()
if err != nil {
return pkg.Wrap(nil, err, op, "cannot commit transaction")
}
return nil
}
/*
func ValidEmail(str string) error {
emailAddress, err := mail.ParseAddress(str)
if err != nil || emailAddress.Address != str {
return errors.New("invalid email")
}
return nil
}
func ValidUsername(str string) error {
if len(str) < 5 {
return errors.New("too short username")
}
if len(str) > 70 {
return errors.New("too long username")
}
if err := ValidEmail(str); err == nil {
return errors.New("username cannot be an email")
}
return nil
}
func ValidPassword(str string) error {
if len(str) < 5 {
return errors.New("too short password")
}
if len(str) > 70 {
return errors.New("too long password")
}
return nil
}
func ValidRole(role models.Role) error {
switch role {
case models.RoleAdmin:
return nil
case models.RoleTeacher:
return nil
case models.RoleStudent:
return nil
}
return errors.New("invalid role")
}
*/

124
main.go
View file

@ -1,34 +1,19 @@
package main
import (
"context"
"fmt"
"git.sch9.ru/new_gate/ms-tester/config"
testerv1 "git.sch9.ru/new_gate/ms-tester/contracts/tester/v1"
"git.sch9.ru/new_gate/ms-tester/internal/auth"
authHandlers "git.sch9.ru/new_gate/ms-tester/internal/auth/delivery/rest"
authUseCase "git.sch9.ru/new_gate/ms-tester/internal/auth/usecase"
"git.sch9.ru/new_gate/ms-tester/internal/contests"
contestsHandlers "git.sch9.ru/new_gate/ms-tester/internal/contests/delivery/rest"
contestsRepository "git.sch9.ru/new_gate/ms-tester/internal/contests/repository"
contestsUseCase "git.sch9.ru/new_gate/ms-tester/internal/contests/usecase"
"git.sch9.ru/new_gate/ms-tester/internal/middleware"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"git.sch9.ru/new_gate/ms-tester/internal/problems"
problemsHandlers "git.sch9.ru/new_gate/ms-tester/internal/problems/delivery/rest"
problemsRepository "git.sch9.ru/new_gate/ms-tester/internal/problems/repository"
problemsUseCase "git.sch9.ru/new_gate/ms-tester/internal/problems/usecase"
sessionsRepository "git.sch9.ru/new_gate/ms-tester/internal/sessions/repository"
sessionsUseCase "git.sch9.ru/new_gate/ms-tester/internal/sessions/usecase"
"git.sch9.ru/new_gate/ms-tester/internal/users"
usersHandlers "git.sch9.ru/new_gate/ms-tester/internal/users/delivery/rest"
usersRepository "git.sch9.ru/new_gate/ms-tester/internal/users/repository"
usersUseCase "git.sch9.ru/new_gate/ms-tester/internal/users/usecase"
"git.sch9.ru/new_gate/ms-tester/pkg"
"github.com/gofiber/fiber/v2"
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
"git.sch9.ru/new_gate/ms-tester/internal/services"
"git.sch9.ru/new_gate/ms-tester/internal/storage"
"git.sch9.ru/new_gate/ms-tester/internal/transport"
sessionv1 "git.sch9.ru/new_gate/ms-tester/pkg/go/gen/proto/session/v1"
"github.com/ilyakaznacheev/cleanenv"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net"
"net/http"
"os"
"os/signal"
@ -36,7 +21,7 @@ import (
)
func main() {
var cfg config.Config
var cfg lib.Config
err := cleanenv.ReadConfig(".env", &cfg)
if err != nil {
panic(fmt.Sprintf("error reading config: %s", err.Error()))
@ -51,87 +36,44 @@ func main() {
panic(fmt.Sprintf(`error reading config: env expected "prod" or "dev", got "%s"`, cfg.Env))
}
logger.Info("connecting to postgres")
db, err := pkg.NewPostgresDB(cfg.PostgresDSN)
db, err := sqlx.Connect("pgx", cfg.PostgresDSN)
if err != nil {
panic(err)
}
defer db.Close()
logger.Info("successfully connected to postgres")
logger.Info("connecting to redis")
vk, err := pkg.NewValkeyClient(cfg.RedisDSN)
//contestStorage := storage.NewContestStorage(db, logger)
//contestService := services.NewContestService(contestStorage)
pandocClient := lib.NewPandocClient(&http.Client{}, cfg.Pandoc)
grpcSessionClient, err := grpc.NewClient(cfg.Auth, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logger.Fatal(fmt.Sprintf("error connecting to redis: %s", err.Error()))
panic(err)
}
logger.Info("successfully connected to redis")
sessionClient := sessionv1.NewSessionServiceClient(grpcSessionClient)
usersRepo := usersRepository.NewRepository(db)
permissionService := services.NewPermissionService()
_, err = usersRepo.CreateUser(context.Background(),
usersRepo.DB(), &models.UserCreation{
Username: cfg.AdminUsername,
Password: cfg.AdminPassword,
Role: models.RoleAdmin,
})
problemStorage := storage.NewProblemStorage(db, logger)
problemService := services.NewProblemService(problemStorage, pandocClient, permissionService)
userStorage := storage.NewUserStorage(db)
userService := services.NewUserService(userStorage)
server := transport.NewTesterServer(problemService, sessionClient, userService, logger)
lis, err := net.Listen("tcp", cfg.Address)
if err != nil {
logger.Error(fmt.Sprintf("error creating admin user: %s", err.Error()))
panic(err)
}
sessionsRepo := sessionsRepository.NewValkeyRepository(vk)
sessionsUC := sessionsUseCase.NewUseCase(sessionsRepo, cfg)
usersUC := usersUseCase.NewUseCase(sessionsRepo, usersRepo)
authUC := authUseCase.NewUseCase(usersUC, sessionsUC)
pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc)
problemsRepo := problemsRepository.NewRepository(db)
problemsUC := problemsUseCase.NewUseCase(problemsRepo, pandocClient)
contestsRepo := contestsRepository.NewRepository(db)
contestsUC := contestsUseCase.NewContestUseCase(contestsRepo)
server := fiber.New()
type MergedHandlers struct {
users.UsersHandlers
auth.AuthHandlers
contests.ContestsHandlers
problems.ProblemsHandlers
}
merged := MergedHandlers{
usersHandlers.NewHandlers(usersUC),
authHandlers.NewHandlers(authUC, cfg.JWTSecret),
contestsHandlers.NewHandlers(problemsUC, contestsUC, cfg.JWTSecret),
problemsHandlers.NewHandlers(problemsUC),
}
testerv1.RegisterHandlersWithOptions(server, merged, testerv1.FiberServerOptions{
Middlewares: []testerv1.MiddlewareFunc{
fiberlogger.New(),
middleware.AuthMiddleware(cfg.JWTSecret, sessionsUC),
//rest.AuthMiddleware(cfg.JWTSecret, userUC),
//cors.New(cors.Config{
// AllowOrigins: "http://localhost:3000",
// AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
// AllowHeaders: "Content-Type,Set-Cookie,Credentials",
// AllowCredentials: true,
//}),
},
})
go func() {
err := server.Listen(cfg.Address)
if err != nil {
logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error()))
if err := server.Start(lis); err != nil {
logger.Fatal("error starting server", zap.Error(err))
}
}()
logger.Info(fmt.Sprintf("server started on %s", cfg.Address))
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)

View file

@ -1,5 +1,280 @@
-- +goose Up
-- +goose StatementBegin
--CREATE TABLE IF NOT EXISTS languages
--(
-- id serial NOT NULL,
-- name VARCHAR(60) NOT NULL,
-- build_file_hash CHAR(128) NULL,
-- execute_file_hash CHAR(128) NULL,
-- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
--
-- PRIMARY KEY (id)
--);
CREATE TABLE IF NOT EXISTS problems
(
id serial NOT NULL,
name VARCHAR(300) NOT NULL,
description TEXT NOT NULL,
time_limit INT NOT NULL,
memory_limit INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(id)
);
CREATE INDEX ON problems USING BTREE (id);
CREATE TABLE IF NOT EXISTS contests
(
id serial NOT NULL,
name VARCHAR(300) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY(id)
);
CREATE INDEX ON contests USING BTREE (id);
CREATE TABLE IF NOT EXISTS tasks
(
id SERIAL NOT NULL,
contest_id INT REFERENCES contests ON DELETE CASCADE,
problem_id INT REFERENCES problems ON DELETE CASCADE,
position INT NOT NULL,
position_name VARCHAR(10) NOT NULL, -- problem name like: A,B,A1,etc
UNIQUE (contest_id,problem_id),
UNIQUE (contest_id,position),
UNIQUE (contest_id,position_name),
PRIMARY KEY (id)
);
CREATE INDEX ON tasks USING BTREE (id);
CREATE INDEX ON tasks USING BTREE (contest_id);
CREATE INDEX ON tasks USING BTREE (problem_id);
CREATE TABLE IF NOT EXISTS testgroups
(
id serial NOT NULL,
problem_id INT REFERENCES problems ON DELETE CASCADE,
testing_strategy INT NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX ON testgroups USING BTREE (id);
CREATE INDEX ON testgroups USING BTREE (problem_id);
CREATE TABLE IF NOT EXISTS subtasks
(
id SERIAL NOT NULL,
contest_id INT REFERENCES contests ON DELETE CASCADE,
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
task_id INT REFERENCES tasks ON DELETE CASCADE,
UNIQUE (contest_id,testgroup_id),
PRIMARY KEY (id)
);
CREATE INDEX ON subtasks USING BTREE (id);
CREATE INDEX ON subtasks USING BTREE (contest_id);
CREATE INDEX ON subtasks USING BTREE (testgroup_id);
CREATE TABLE IF NOT EXISTS participants
(
id serial NOT NULL,
user_id INT NOT NULL,
contest_id INT REFERENCES contests ON DELETE CASCADE,
name varchar(200) NOT NULL,
UNIQUE (user_id,contest_id),
PRIMARY KEY (id)
);
CREATE INDEX ON participants USING BTREE (id);
CREATE INDEX ON participants USING BTREE (user_id);
CREATE INDEX ON participants USING BTREE (contest_id);
CREATE TABLE IF NOT EXISTS solutions
(
id serial NOT NULL,
participant_id INT REFERENCES participants ON DELETE CASCADE,
task_id INT REFERENCES problems ON DELETE CASCADE,
language_id INT NOT NULL,REFERENCES languages ON DELETE CASCADE,
solution_hash CHAR(128) NOT NULL,
result INT NOT NULL,
score INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
CREATE INDEX ON solutions USING BTREE (id);
CREATE INDEX ON solutions USING BTREE (id,participant_id,task_id,language_id);
CREATE TABLE IF NOT EXISTS tests
(
id serial NOT NULL,
--problem_id INT NOT NULL,
testgroup_id INT REFERENCES testgroups ON DELETE CASCADE,
PRIMARY KEY (id)
);
CREATE INDEX ON tests USING BTREE (id);
CREATE INDEX ON tests USING BTREE (testgroup_id);
CREATE TABLE IF NOT EXISTS subtaskruns
(
id serial NOT NULL,
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
solution_id INT REFERENCES solutions ON DELETE CASCADE,
result INT NOT NULL,
score INT NOT NULL,
UNIQUE (subtask_id,solution_id),
PRIMARY KEY (id)
);
CREATE INDEX ON subtaskruns USING BTREE (id);
CREATE INDEX ON subtaskruns USING BTREE (result);
CREATE INDEX ON subtaskruns USING BTREE (solution_id);
CREATE TABLE IF NOT EXISTS testruns
(
id serial NOT NULL,
test_id INT REFERENCES tests ON DELETE CASCADE,
--solution_id INT REFERENCES solutions ON DELETE CASCADE,
subtaskrun_id INT REFERENCES subtaskruns ON DELETE CASCADE,
result INT NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX ON testruns USING BTREE (id);
CREATE INDEX ON testruns USING BTREE (result);
CREATE INDEX ON testruns USING BTREE (subtaskrun_id);
CREATE TABLE IF NOT EXISTS participant_subtask
(
participant_id INT REFERENCES participants ON DELETE CASCADE,
subtask_id INT REFERENCES subtasks ON DELETE CASCADE,
--result INT NOT NULL,
best_score INT NOT NULL
);
CREATE TABLE IF NOT EXISTS participant_task
(
participant_id INT REFERENCES participants ON DELETE CASCADE,
task_id INT REFERENCES tasks ON DELETE CASCADE,
--result INT NOT NULL,
best_score INT NOT NULL,
penalty INT NOT NULL
);
CREATE TABLE IF NOT EXISTS users
(
user_id INT NOT NULL,
role INT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK ( role BETWEEN 0 AND 3),
PRIMARY KEY (user_id)
);
CREATE FUNCTION on_new_participant() RETURNS TRIGGER AS
$$ BEGIN
--RAISE NOTICE 'NEW.ID:%, NEW.contest_id:%', NEW.id,NEW.contest_id;
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT NEW.id,id,0,0 FROM tasks WHERE contest_id=NEW.contest_id;
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT NEW.id,id,0 FROM subtasks WHERE contest_id=NEW.contest_id;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER new_participant_trg AFTER INSERT ON participants FOR EACH ROW EXECUTE FUNCTION on_new_participant();
CREATE FUNCTION on_new_task() RETURNS TRIGGER AS
$$ BEGIN
INSERT INTO participant_task (participant_id,task_id,best_score,penalty) SELECT id,NEW.id,0,0 FROM participants WHERE contest_id=NEW.contest_id;
INSERT INTO subtasks(contest_id,testgroup_id,task_id) SELECT NEW.contest_id,id,NEW.id FROM testgroups WHERE problem_id = NEW.problem_id;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER new_task_trg AFTER INSERT ON tasks FOR EACH ROW EXECUTE FUNCTION on_new_task();
CREATE FUNCTION on_new_subtask() RETURNS TRIGGER AS
$$ BEGIN
INSERT INTO participant_subtask (participant_id,subtask_id,best_score) SELECT id,NEW.id,0 FROM participants WHERE contest_id=NEW.contest_id;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER new_subtask_trg AFTER INSERT ON subtasks FOR EACH ROW EXECUTE FUNCTION on_new_subtask();
CREATE FUNCTION on_new_solution() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO subtaskruns (subtask_id,solution_id,result,score) (SELECT id,NEW.id,0,0 FROM subtasks WHERE task_id = NEW.task_id);
--INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,str.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id IN (SELECT subtask_id FROM subtaskruns AS str WHERE solution_id=NEW.id)));
RETURN NEW;
END;
$$;
CREATE TRIGGER on_new_solution_trg AFTER INSERT ON solutions FOR EACH ROW EXECUTE FUNCTION on_new_solution();
CREATE FUNCTION on_new_subtaskrun() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO testruns (test_id,subtaskrun_id,result) (SELECT id,NEW.id,0 FROM tests WHERE testgroup_id IN (SELECT testgroup_id FROM subtasks WHERE id=NEW.subtask_id));
RETURN NEW;
END;
$$;
CREATE TRIGGER on_new_subtaskrun_trg AFTER INSERT ON subtaskruns FOR EACH ROW EXECUTE FUNCTION on_new_subtaskrun();
CREATE FUNCTION updated_at_update() RETURNS TRIGGER
LANGUAGE plpgsql AS
$$
@ -9,178 +284,33 @@ BEGIN
END;
$$;
CREATE FUNCTION check_max_tasks() RETURNS TRIGGER
LANGUAGE plpgsql AS
$$
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;
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,
title varchar(64) NOT NULL,
time_limit integer NOT NULL DEFAULT 1000,
memory_limit integer NOT NULL DEFAULT 64,
legend varchar(10240) NOT NULL DEFAULT '',
input_format varchar(10240) NOT NULL DEFAULT '',
output_format varchar(10240) NOT NULL DEFAULT '',
notes varchar(10240) NOT NULL DEFAULT '',
scoring varchar(10240) NOT NULL DEFAULT '',
legend_html varchar(10240) NOT NULL DEFAULT '',
input_format_html varchar(10240) NOT NULL DEFAULT '',
output_format_html varchar(10240) NOT NULL DEFAULT '',
notes_html varchar(10240) NOT NULL DEFAULT '',
scoring_html varchar(10240) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
CHECK (length(title) != 0),
CHECK (memory_limit BETWEEN 4 and 1024),
CHECK (time_limit BETWEEN 250 and 5000)
);
CREATE TRIGGER on_problems_update
BEFORE UPDATE
ON problems
FOR EACH ROW
EXECUTE PROCEDURE updated_at_update();
CREATE TABLE IF NOT EXISTS contests
(
id serial NOT NULL,
title varchar(64) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
CHECK (length(title) != 0)
);
CREATE TRIGGER on_contests_update
BEFORE UPDATE
ON contests
FOR EACH ROW
EXECUTE PROCEDURE updated_at_update();
CREATE TABLE IF NOT EXISTS tasks
(
id serial NOT NULL,
problem_id integer REFERENCES problems (id) ON DELETE SET NULL,
contest_id integer REFERENCES contests (id) ON DELETE SET NULL,
position integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
UNIQUE (problem_id, contest_id),
UNIQUE (contest_id, position),
CHECK (position >= 0)
);
CREATE TRIGGER max_tasks_on_contest_check
BEFORE INSERT
ON tasks
FOR EACH STATEMENT
EXECUTE FUNCTION check_max_tasks();
CREATE TRIGGER on_tasks_update
BEFORE UPDATE
ON tasks
FOR EACH ROW
EXECUTE PROCEDURE updated_at_update();
CREATE TABLE IF NOT EXISTS participants
(
id serial NOT NULL,
user_id integer NOT NULL REFERENCES users (id) ON DELETE CASCADE,
contest_id integer NOT NULL REFERENCES contests (id) ON DELETE CASCADE,
name varchar(64) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
UNIQUE (user_id, contest_id),
CHECK (length(name) != 0)
);
CREATE TRIGGER on_participants_update
BEFORE UPDATE
ON participants
FOR EACH ROW
EXECUTE PROCEDURE updated_at_update();
CREATE TABLE IF NOT EXISTS solutions
(
id serial NOT NULL,
task_id integer REFERENCES tasks (id) ON DELETE SET NULL,
participant_id integer REFERENCES participants (id) ON DELETE SET NULL,
solution varchar(1048576) NOT NULL,
state integer NOT NULL DEFAULT 1,
score integer NOT NULL DEFAULT 0,
penalty integer NOT NULL,
time_stat integer NOT NULL DEFAULT 0,
memory_stat integer NOT NULL DEFAULT 0,
language integer NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id)
);
CREATE TRIGGER on_solutions_update
BEFORE UPDATE
ON solutions
FOR EACH ROW
EXECUTE PROCEDURE updated_at_update();
--CREATE TRIGGER languages_upd_trg BEFORE UPDATE ON languages FOR EACH ROW EXECUTE FUNCTION updated_at_update();
CREATE TRIGGER problems_upd_trg BEFORE UPDATE ON problems FOR EACH ROW EXECUTE FUNCTION updated_at_update();
CREATE TRIGGER contests_upd_trg BEFORE UPDATE ON contests FOR EACH ROW EXECUTE FUNCTION updated_at_update();
CREATE TRIGGER users_upd_trg BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION updated_at_update();
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS on_solutions_update ON solutions;
DROP TABLE IF EXISTS solutions;
DROP TRIGGER IF EXISTS on_participants_update ON participants;
DROP TABLE IF EXISTS participants;
DROP TRIGGER IF EXISTS on_tasks_update ON tasks;
DROP TRIGGER IF EXISTS max_tasks_on_contest_check ON tasks;
DROP TABLE IF EXISTS tasks;
DROP TRIGGER IF EXISTS on_problems_update ON problems;
DROP TABLE IF EXISTS problems;
DROP TRIGGER IF EXISTS on_contests_update ON contests;
DROP TABLE IF EXISTS contests;
DROP TRIGGER IF EXISTS on_users_update ON users;
DROP TABLE IF EXISTS users;
DROP FUNCTION IF EXISTS updated_at_update();
DROP FUNCTION IF EXISTS check_max_tasks();
DROP FUNCTION IF EXISTS updated_at_update CASCADE;
DROP FUNCTION IF EXISTS on_new_participant CASCADE;
DROP FUNCTION IF EXISTS on_new_task CASCADE;
DROP FUNCTION IF EXISTS on_new_subtask CASCADE;
DROP FUNCTION IF EXISTS on_new_solution CASCADE;
DROP FUNCTION IF EXISTS on_new_subtaskrun CASCADE;
DROP TABLE IF EXISTS tests CASCADE;
DROP TABLE IF EXISTS solutions CASCADE;
--DROP TABLE IF EXISTS languages CASCADE;
DROP TABLE IF EXISTS testgroups CASCADE;
DROP TABLE IF EXISTS testruns CASCADE;
DROP TABLE IF EXISTS subtaskruns CASCADE;
DROP TABLE IF EXISTS problems CASCADE;
DROP TABLE IF EXISTS contests CASCADE;
DROP TABLE IF EXISTS tasks CASCADE;
DROP TABLE IF EXISTS subtasks CASCADE;
DROP TABLE IF EXISTS participants CASCADE;
DROP TABLE IF EXISTS participant_task CASCADE;
DROP TABLE IF EXISTS participant_subtask CASCADE;
DROP TABLE IF EXISTS users CASCADE;
-- +goose StatementEnd

44
opa/all.rego Normal file
View file

@ -0,0 +1,44 @@
package problem.rbac
import rego.v1
spectator := 0
participant := 1
moderator := 2
admin := 3
permissions := {
"read": is_spectator,
"participate": is_participant,
"update": is_moderator,
"create": is_moderator,
"delete": is_moderator,
}
default allow := false
allow if is_admin
allow if {
permissions[input.action]
}
default is_admin := false
is_admin if {
input.user.role == admin
}
default is_moderator := false
is_moderator if {
input.user.role >= moderator
}
default is_participant := false
is_participant if {
input.user.role >= participant
}
default is_spectator := true
is_spectator if {
input.user.role >= spectator
}

View file

@ -1 +0,0 @@
package pkg

View file

@ -1,58 +0,0 @@
package pkg
import (
"database/sql"
"errors"
"fmt"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
"net/http"
)
var (
NoPermission = errors.New("no permission")
ErrUnauthenticated = errors.New("unauthenticated")
ErrUnhandled = errors.New("unhandled")
ErrNotFound = errors.New("not found")
ErrBadInput = errors.New("bad input")
ErrInternal = errors.New("internal")
)
func Wrap(basic error, err error, op string, msg string) error {
return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg))
}
func ToREST(err error) int {
switch {
case errors.Is(err, ErrUnauthenticated):
return http.StatusUnauthorized
case errors.Is(err, ErrBadInput):
return http.StatusBadRequest
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
case errors.Is(err, ErrInternal):
return http.StatusInternalServerError
case errors.Is(err, NoPermission):
return http.StatusForbidden
}
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")
}

10
pkg/models/contest.go Normal file
View file

@ -0,0 +1,10 @@
package models
import "time"
type Contest struct {
Id *int `db:"id"`
Name *string `db:"name"`
CreatedAt *time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
}

14
pkg/models/language.go Normal file
View file

@ -0,0 +1,14 @@
package models
type Language struct {
Name string
CompileCmd []string //source: src;result:executable
RunCmd []string //source: executable
}
var Languages = [...]Language {
{Name : "gcc std=c90",
CompileCmd : []string{"gcc", "src","-std=c90","-o","executable"},
RunCmd : []string{"executable"}},
}

View file

@ -0,0 +1,8 @@
package models
type Participant struct {
Id *int32 `db:"id"`
UserId *int32 `db:"user_id"`
ContestId *int32 `db:"contest_id"`
Name *string `db:"name"`
}

13
pkg/models/problem.go Normal file
View file

@ -0,0 +1,13 @@
package models
import "time"
type Problem struct {
Id *int32 `db:"id"`
Name *string `db:"name"`
Description *string `db:"description"`
TimeLimit *int32 `db:"time_limit"`
MemoryLimit *int32 `db:"memory_limit"`
CreatedAt *time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
}

28
pkg/models/result.go Normal file
View file

@ -0,0 +1,28 @@
package models
import (
"errors"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
)
type Result int32
const (
NotTested Result = 0 // change only with schema change
Accepted Result = 1
CompilationError Result = 2
MemoryLimitExceeded Result = 3
TimeLimitExceeded Result = 4
SystemFailDuringTesting Result = 5
Testing Result = 6
)
var ErrBadResult = errors.New("bad result")
func (result Result) Valid() error {
switch result {
case NotTested, Accepted, TimeLimitExceeded, MemoryLimitExceeded, CompilationError, SystemFailDuringTesting:
return nil
}
return lib.ServiceError(ErrBadResult, lib.ErrValidationFailed, "bad result")
}

53
pkg/models/role.go Normal file
View file

@ -0,0 +1,53 @@
package models
import (
"errors"
"git.sch9.ru/new_gate/ms-tester/internal/lib"
)
type Role int32
const (
RoleSpectator Role = 0
RoleParticipant Role = 1
RoleModerator Role = 2
RoleAdmin Role = 3
)
func (role Role) IsAdmin() bool {
return role == RoleAdmin
}
func (role Role) IsModerator() bool {
return role == RoleModerator
}
func (role Role) IsParticipant() bool {
return role == RoleParticipant
}
func (role Role) IsSpectator() bool {
return role == RoleSpectator
}
func (role Role) AtLeast(other Role) bool {
return role >= other
}
func (role Role) AtMost(other Role) bool {
return role <= other
}
var ErrBadRole = errors.New("bad role")
func (role Role) Valid() error {
switch role {
case RoleSpectator, RoleParticipant, RoleModerator, RoleAdmin:
return nil
}
return lib.ServiceError(ErrBadRole, lib.ErrValidationFailed, "bad role")
}
func (role Role) AsPointer() *Role {
return &role
}

14
pkg/models/solution.go Normal file
View file

@ -0,0 +1,14 @@
package models
import "time"
type Solution struct {
Id *int32 `db:"id"`
ParticipantId *int32 `db:"participant_id"`
TaskId *int32 `db:"task_id"`
LanguageId *int32 `db:"language_id"`
SolutionHash *string `db:"solution_hash"`
Result *int32 `db:"result"`
Score *int32 `db:"score"`
CreatedAt *time.Time `db:"created_at"`
}

Some files were not shown because too many files have changed in this diff Show more