add task service

This commit is contained in:
Timur Kh.
2025-12-17 10:40:23 +03:00
parent 843c50a103
commit 6537aded89
10 changed files with 697 additions and 3 deletions
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"datarush/internal/task/config"
"datarush/internal/task/server"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
srv := server.New(cfg)
if err := srv.Start(); err != nil {
log.Fatalf("failed to start server: %v", err)
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down task server...")
srv.Stop()
log.Println("task server stopped")
}
+6 -1
View File
@@ -6,16 +6,18 @@ toolchain go1.24.9
require (
github.com/Masterminds/squirrel v1.5.4
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.28.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/golang/mock v1.6.0
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.16.0
github.com/stretchr/testify v1.10.0
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.42.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
@@ -23,6 +25,7 @@ require (
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -33,6 +36,7 @@ require (
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.43.0 // indirect
@@ -41,6 +45,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
tool (
+27 -2
View File
@@ -35,7 +35,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -52,11 +51,12 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -109,6 +109,7 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
@@ -123,16 +124,40 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
+74
View File
@@ -0,0 +1,74 @@
package config
import (
"fmt"
"log"
"net"
"os"
"strconv"
"github.com/joho/godotenv"
)
type Config struct {
GRPCPort int
GRPCEnableReflection bool
HTTPPort int
LogLevel string
DBHost string
DBPort int
DBUser string
DBPassword string
DBName string
}
func Load() (*Config, error) {
_ = godotenv.Load()
return &Config{
GRPCPort: mustGetInt("TASK_GRPC_PORT", 50053),
GRPCEnableReflection: mustGetBool("TASK_GRPC_ENABLE_REFLECTION", false),
HTTPPort: mustGetInt("TASK_HTTP_PORT", 8082),
LogLevel: getEnv("LOG_LEVEL", "info"),
DBHost: getEnv("POSTGRES_HOST", "localhost"),
DBPort: mustGetInt("POSTGRES_PORT", 5432),
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
}, nil
}
func getEnv(key, def string) string {
if val := os.Getenv(key); val != "" {
return val
}
return def
}
func mustGetInt(key string, def int) int {
val := getEnv(key, strconv.Itoa(def))
n, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("invalid int for %s: %v", key, err)
}
return n
}
func mustGetBool(key string, def bool) bool {
val := getEnv(key, strconv.FormatBool(def))
b, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid bool for %s: %v", key, err)
}
return b
}
func (c Config) BuildPostgresConnStr() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName)
}
func (c Config) BuildPostgresDSN() string {
return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable",
c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName)
}
+50
View File
@@ -0,0 +1,50 @@
package grpc
import (
"context"
pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb"
)
type TaskService interface {
CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error)
GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error)
EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error)
DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error)
ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error)
GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error)
}
type TaskHandler struct {
pb.UnimplementedTaskServiceServer
service TaskService
}
func NewTaskHandler(service TaskService) *TaskHandler {
return &TaskHandler{service: service}
}
func (h *TaskHandler) CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error) {
return h.service.CreateTask(ctx, req)
}
func (h *TaskHandler) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) {
return h.service.GetTask(ctx, req)
}
func (h *TaskHandler) EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) {
return h.service.EditTask(ctx, req)
}
func (h *TaskHandler) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) {
return h.service.DeleteTask(ctx, req)
}
func (h *TaskHandler) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) {
return h.service.ListCompetitionTasks(ctx, req)
}
func (h *TaskHandler) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) {
return h.service.GetTaskAttachments(ctx, req)
}
@@ -0,0 +1,79 @@
package postgres
import (
"context"
"github.com/jmoiron/sqlx"
"datarush/pkg/api/task"
)
type TaskRepository struct {
db *sqlx.DB
}
func NewTaskRepository(db *sqlx.DB) *TaskRepository {
return &TaskRepository{db: db}
}
func (r *TaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Task, error) {
query := `INSERT INTO tasks (competition_id, title, description, in_competition_position, max_points, max_attempts, type)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at`
var createdTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.CompetitionId, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type).StructScan(&createdTask)
if err != nil {
return nil, err
}
createdTask.CompetitionId = t.CompetitionId
createdTask.Title = t.Title
createdTask.Description = t.Description
createdTask.InCompetitionPosition = t.InCompetitionPosition
createdTask.MaxPoints = t.MaxPoints
createdTask.MaxAttempts = t.MaxAttempts
createdTask.Type = t.Type
return &createdTask, nil
}
func (r *TaskRepository) GetTask(ctx context.Context, id string) (*task.Task, error) {
var t task.Task
err := r.db.GetContext(ctx, &t, "SELECT * FROM tasks WHERE id = $1", id)
return &t, err
}
func (r *TaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task, error) {
query := `UPDATE tasks SET title = $1, description = $2, in_competition_position = $3, max_points = $4, max_attempts = $5, type = $6, updated_at = now()
WHERE id = $7 RETURNING updated_at`
var updatedTask task.Task
err := r.db.QueryRowxContext(ctx, query, t.Title, t.Description, t.InCompetitionPosition, t.MaxPoints, t.MaxAttempts, t.Type, t.Id).StructScan(&updatedTask)
if err != nil {
return nil, err
}
t.UpdatedAt = updatedTask.UpdatedAt
return t, nil
}
func (r *TaskRepository) DeleteTask(ctx context.Context, id string) error {
_, err := r.db.ExecContext(ctx, "DELETE FROM tasks WHERE id = $1", id)
return err
}
func (r *TaskRepository) ListCompetitionTasks(ctx context.Context, competitionID string) ([]*task.Task, error) {
var tasks []*task.Task
err := r.db.SelectContext(ctx, &tasks, "SELECT * FROM tasks WHERE competition_id = $1", competitionID)
return tasks, err
}
func (r *TaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) {
var attachments []*task.TaskAttachment
query := "SELECT * FROM task_attachments WHERE task_id = $1"
args := []interface{}{taskID}
if !showPrivate {
query += " AND is_public = true"
}
err := r.db.SelectContext(ctx, &attachments, query, args...)
return attachments, err
}
+96
View File
@@ -0,0 +1,96 @@
package server
import (
"fmt"
"log"
"net"
"time"
"datarush/internal/task/config"
grpcHandlers "datarush/internal/task/handler/grpc"
taskPostgresRepo "datarush/internal/task/repository/postgres"
"datarush/internal/task/service"
pb "datarush/pkg/api/task"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
httpReadTimeout = 10 * time.Second
httpWriteTimeout = 10 * time.Second
httpIdleTimeout = 60 * time.Second
)
type Server struct {
grpcServer *grpc.Server
config *config.Config
db *sqlx.DB
}
func New(cfg *config.Config) *Server {
return &Server{
config: cfg,
}
}
func (s *Server) Start() error {
db, err := sqlx.Connect("postgres", s.config.BuildPostgresConnStr())
if err != nil {
return fmt.Errorf("failed to connect to postgres: %w", err)
}
s.db = db
if err := s.registerGRPCServices(); err != nil {
return fmt.Errorf("failed to register gRPC services: %w", err)
}
go func() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.GRPCPort))
if err != nil {
log.Fatalf("failed to listen on grpc port: %v", err)
}
log.Printf("starting gRPC server on port %d", s.config.GRPCPort)
if err := s.grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve gRPC: %v", err)
}
}()
return nil
}
func (s *Server) registerGRPCServices() error {
s.grpcServer = grpc.NewServer()
taskRepo := taskPostgresRepo.NewTaskRepository(s.db)
taskService := service.NewTaskService(taskRepo)
taskHandler := grpcHandlers.NewTaskHandler(taskService)
pb.RegisterTaskServiceServer(s.grpcServer, taskHandler)
if s.config.GRPCEnableReflection {
reflection.Register(s.grpcServer)
}
return nil
}
func (s *Server) Stop() {
log.Println("shutting down task server...")
if s.grpcServer != nil {
s.grpcServer.GracefulStop()
}
if s.db != nil {
if err := s.db.Close(); err != nil {
log.Printf("failed to close database: %v", err)
}
}
log.Println("task server stopped")
}
@@ -0,0 +1,131 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: internal/task/service/service.go
//
// Generated by this command:
//
// mockgen -source=internal/task/service/service.go -destination=internal/task/service/mocks/mock_repository.go -package=mocks
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
task "datarush/pkg/api/task"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockTaskRepository is a mock of TaskRepository interface.
type MockTaskRepository struct {
ctrl *gomock.Controller
recorder *MockTaskRepositoryMockRecorder
isgomock struct{}
}
// MockTaskRepositoryMockRecorder is the mock recorder for MockTaskRepository.
type MockTaskRepositoryMockRecorder struct {
mock *MockTaskRepository
}
// NewMockTaskRepository creates a new mock instance.
func NewMockTaskRepository(ctrl *gomock.Controller) *MockTaskRepository {
mock := &MockTaskRepository{ctrl: ctrl}
mock.recorder = &MockTaskRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTaskRepository) EXPECT() *MockTaskRepositoryMockRecorder {
return m.recorder
}
// CreateTask mocks base method.
func (m *MockTaskRepository) CreateTask(ctx context.Context, t *task.Task) (*task.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTask", ctx, t)
ret0, _ := ret[0].(*task.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateTask indicates an expected call of CreateTask.
func (mr *MockTaskRepositoryMockRecorder) CreateTask(ctx, t any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTask", reflect.TypeOf((*MockTaskRepository)(nil).CreateTask), ctx, t)
}
// DeleteTask mocks base method.
func (m *MockTaskRepository) DeleteTask(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTask", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteTask indicates an expected call of DeleteTask.
func (mr *MockTaskRepositoryMockRecorder) DeleteTask(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTask", reflect.TypeOf((*MockTaskRepository)(nil).DeleteTask), ctx, id)
}
// EditTask mocks base method.
func (m *MockTaskRepository) EditTask(ctx context.Context, t *task.Task) (*task.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EditTask", ctx, t)
ret0, _ := ret[0].(*task.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EditTask indicates an expected call of EditTask.
func (mr *MockTaskRepositoryMockRecorder) EditTask(ctx, t any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditTask", reflect.TypeOf((*MockTaskRepository)(nil).EditTask), ctx, t)
}
// GetTask mocks base method.
func (m *MockTaskRepository) GetTask(ctx context.Context, id string) (*task.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTask", ctx, id)
ret0, _ := ret[0].(*task.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTask indicates an expected call of GetTask.
func (mr *MockTaskRepositoryMockRecorder) GetTask(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTask", reflect.TypeOf((*MockTaskRepository)(nil).GetTask), ctx, id)
}
// GetTaskAttachments mocks base method.
func (m *MockTaskRepository) GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*task.TaskAttachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTaskAttachments", ctx, taskID, showPrivate)
ret0, _ := ret[0].([]*task.TaskAttachment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTaskAttachments indicates an expected call of GetTaskAttachments.
func (mr *MockTaskRepositoryMockRecorder) GetTaskAttachments(ctx, taskID, showPrivate any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskAttachments", reflect.TypeOf((*MockTaskRepository)(nil).GetTaskAttachments), ctx, taskID, showPrivate)
}
// ListCompetitionTasks mocks base method.
func (m *MockTaskRepository) ListCompetitionTasks(ctx context.Context, competitionID string) ([]*task.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListCompetitionTasks", ctx, competitionID)
ret0, _ := ret[0].([]*task.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListCompetitionTasks indicates an expected call of ListCompetitionTasks.
func (mr *MockTaskRepositoryMockRecorder) ListCompetitionTasks(ctx, competitionID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCompetitionTasks", reflect.TypeOf((*MockTaskRepository)(nil).ListCompetitionTasks), ctx, competitionID)
}
+62
View File
@@ -0,0 +1,62 @@
package service
import (
"context"
pb "datarush/pkg/api/task"
"google.golang.org/protobuf/types/known/emptypb"
)
type TaskRepository interface {
CreateTask(ctx context.Context, t *pb.Task) (*pb.Task, error)
GetTask(ctx context.Context, id string) (*pb.Task, error)
EditTask(ctx context.Context, t *pb.Task) (*pb.Task, error)
DeleteTask(ctx context.Context, id string) error
ListCompetitionTasks(ctx context.Context, competitionID string) ([]*pb.Task, error)
GetTaskAttachments(ctx context.Context, taskID string, showPrivate bool) ([]*pb.TaskAttachment, error)
}
type TaskService struct {
repo TaskRepository
}
func NewTaskService(repo TaskRepository) *TaskService {
return &TaskService{repo: repo}
}
func (s *TaskService) CreateTask(ctx context.Context, req *pb.Task) (*pb.Task, error) {
return s.repo.CreateTask(ctx, req)
}
func (s *TaskService) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) {
return s.repo.GetTask(ctx, req.TaskId)
}
func (s *TaskService) EditTask(ctx context.Context, req *pb.Task) (*pb.Task, error) {
return s.repo.EditTask(ctx, req)
}
func (s *TaskService) DeleteTask(ctx context.Context, req *pb.DeleteTaskRequest) (*emptypb.Empty, error) {
err := s.repo.DeleteTask(ctx, req.TaskId)
return &emptypb.Empty{}, err
}
func (s *TaskService) ListCompetitionTasks(ctx context.Context, req *pb.ListCompetitionTasksRequest) (*pb.ListCompetitionTasksResponse, error) {
tasks, err := s.repo.ListCompetitionTasks(ctx, req.CompetitionId)
if err != nil {
return nil, err
}
return &pb.ListCompetitionTasksResponse{Tasks: tasks}, nil
}
func (s *TaskService) GetTaskAttachments(ctx context.Context, req *pb.GetTaskAttachmentsRequest) (*pb.GetTaskAttachmentsResponse, error) {
showPrivate := false
if req.ShowPrivate != nil {
showPrivate = *req.ShowPrivate
}
attachments, err := s.repo.GetTaskAttachments(ctx, req.TaskId, showPrivate)
if err != nil {
return nil, err
}
return &pb.GetTaskAttachmentsResponse{Attachments: attachments}, nil
}
+140
View File
@@ -0,0 +1,140 @@
package service
import (
"context"
"testing"
"datarush/internal/task/service/mocks"
pb "datarush/pkg/api/task"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestTaskService(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockTaskRepository(ctrl)
service := NewTaskService(mockRepo)
ctx := context.Background()
t.Run("CreateTask", func(t *testing.T) {
task := &pb.Task{
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
InCompetitionPosition: 1,
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
}
mockRepo.EXPECT().CreateTask(ctx, task).Return(task, nil)
createdTask, err := service.CreateTask(ctx, task)
assert.NoError(t, err)
assert.Equal(t, task, createdTask)
})
t.Run("GetTask", func(t *testing.T) {
taskID := "task1"
req := &pb.GetTaskRequest{TaskId: taskID}
expectedTask := &pb.Task{
Id: taskID,
CompetitionId: "comp1",
Title: "Test Task",
Description: "This is a test task",
InCompetitionPosition: 1,
MaxPoints: 100,
MaxAttempts: 10,
Type: pb.TaskType_TASK_TYPE_INPUT,
CreatedAt: timestamppb.Now(),
UpdatedAt: timestamppb.Now(),
}
mockRepo.EXPECT().GetTask(ctx, taskID).Return(expectedTask, nil)
task, err := service.GetTask(ctx, req)
assert.NoError(t, err)
assert.Equal(t, expectedTask, task)
})
t.Run("EditTask", func(t *testing.T) {
task := &pb.Task{
Id: "task1",
CompetitionId: "comp1",
Title: "Updated Test Task",
Description: "This is an updated test task",
InCompetitionPosition: 1,
MaxPoints: 150,
MaxAttempts: 5,
Type: pb.TaskType_TASK_TYPE_CHECKER,
}
mockRepo.EXPECT().EditTask(ctx, task).Return(task, nil)
updatedTask, err := service.EditTask(ctx, task)
assert.NoError(t, err)
assert.Equal(t, task, updatedTask)
})
t.Run("DeleteTask", func(t *testing.T) {
taskID := "task1"
req := &pb.DeleteTaskRequest{TaskId: taskID}
mockRepo.EXPECT().DeleteTask(ctx, taskID).Return(nil)
_, err := service.DeleteTask(ctx, req)
assert.NoError(t, err)
})
t.Run("ListCompetitionTasks", func(t *testing.T) {
competitionID := "comp1"
req := &pb.ListCompetitionTasksRequest{CompetitionId: competitionID}
expectedTasks := []*pb.Task{
{Id: "task1", CompetitionId: competitionID, Title: "Task 1"},
{Id: "task2", CompetitionId: competitionID, Title: "Task 2"},
}
mockRepo.EXPECT().ListCompetitionTasks(ctx, competitionID).Return(expectedTasks, nil)
resp, err := service.ListCompetitionTasks(ctx, req)
assert.NoError(t, err)
assert.Equal(t, expectedTasks, resp.Tasks)
})
t.Run("GetTaskAttachments", func(t *testing.T) {
taskID := "task1"
showPrivate := true
req := &pb.GetTaskAttachmentsRequest{TaskId: taskID, ShowPrivate: &showPrivate}
expectedAttachments := []*pb.TaskAttachment{
{Id: "att1", FileUrl: "url1", IsPublic: true},
{Id: "att2", FileUrl: "url2", IsPublic: false},
}
mockRepo.EXPECT().GetTaskAttachments(ctx, taskID, showPrivate).Return(expectedAttachments, nil)
resp, err := service.GetTaskAttachments(ctx, req)
assert.NoError(t, err)
assert.Equal(t, expectedAttachments, resp.Attachments)
})
t.Run("GetTaskAttachments - show public only", func(t *testing.T) {
taskID := "task1"
showPrivate := false
req := &pb.GetTaskAttachmentsRequest{TaskId: taskID, ShowPrivate: &showPrivate}
expectedAttachments := []*pb.TaskAttachment{
{Id: "att1", FileUrl: "url1", IsPublic: true},
}
mockRepo.EXPECT().GetTaskAttachments(ctx, taskID, showPrivate).Return(expectedAttachments, nil)
resp, err := service.GetTaskAttachments(ctx, req)
assert.NoError(t, err)
assert.Equal(t, expectedAttachments, resp.Attachments)
})
}