diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c3324d8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: Go CI + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.25' + + - name: Clean Go cache + run: | + go clean -cache + go clean -modcache + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('service/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download dependencies + run: | + cd chat-service + go mod download + + - name: Run tests + run: | + cd chat-service + go test -v -race ./... + env: + DB_HOST: localhost + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: postgres + DB_NAME: postgres + + - name: Run vet + run: | + cd chat-service + go vet ./... + + build: + name: Build + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.25' + + - name: Build server + run: | + cd chat-service + go build -v ./cmd/server + + - name: Build all packages + run: | + cd chat-service + go build -v ./... diff --git a/chat-service/cmd/server/main.go b/chat-service/cmd/server/main.go new file mode 100644 index 0000000..7bb3ce9 --- /dev/null +++ b/chat-service/cmd/server/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + + "github.com/Lemper29/ChatRoom/chat-service/internal/server" + "github.com/Lemper29/ChatRoom/chat-service/internal/storage/db" + "gorm.io/driver/postgres" +) + +func main() { + cfg := postgres.Config{ + DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable", + PreferSimpleProtocol: true, + } + + storage, err := db.NewPostgresStorage(cfg) + if err != nil { + log.Fatalf("DB error: %v", err) + } + + server := server.NewGrpcServer(":8080", storage) + + if err := server.Server(); err != nil { + log.Fatalf("Server error: %v", err) + } +} diff --git a/chat-service/go.mod b/chat-service/go.mod new file mode 100644 index 0000000..20fa319 --- /dev/null +++ b/chat-service/go.mod @@ -0,0 +1,36 @@ +module github.com/Lemper29/ChatRoom/chat-service + +go 1.25.1 + +require ( + github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8 + github.com/stretchr/testify v1.8.1 + google.golang.org/grpc v1.75.1 + gorm.io/gorm v1.25.10 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sync v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gorm.io/driver/postgres v1.6.0 +) diff --git a/chat-service/go.sum b/chat-service/go.sum new file mode 100644 index 0000000..01973dd --- /dev/null +++ b/chat-service/go.sum @@ -0,0 +1,86 @@ +github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8 h1:nfZhrBMHVyyGQjDtBpeZMA15QofmiFj6BOMfajfPNXw= +github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8/go.mod h1:HLBRSvrN+oE7CD/R5y8szNIbnPFEt/WHFPmdXyMl0xk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/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.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.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +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= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +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/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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.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= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/chat-service/internal/server/server.go b/chat-service/internal/server/server.go new file mode 100644 index 0000000..e147e7d --- /dev/null +++ b/chat-service/internal/server/server.go @@ -0,0 +1,42 @@ +package server + +import ( + "fmt" + "net" + + "github.com/Lemper29/ChatRoom/chat-service/internal/service" + "github.com/Lemper29/ChatRoom/chat-service/internal/storage" + pb "github.com/Lemper29/ChatRoom/gen/go/v1" + "google.golang.org/grpc" +) + +type Server struct { + pb.UnimplementedChatServiceServer + service *service.Service + addr string +} + +func NewGrpcServer(addr string, storage storage.Storage) *Server { + return &Server{ + addr: addr, + service: service.NewService(storage), + } +} + +func (s *Server) Server() error { + lis, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("failed to listen: %v", err) + } + + grpcServer := grpc.NewServer() + pb.RegisterChatServiceServer(grpcServer, s) + + fmt.Printf("Server starting on %s\n", s.addr) + + if err := grpcServer.Serve(lis); err != nil { + return fmt.Errorf("failed to serve: %v", err) + } + + return nil +} diff --git a/chat-service/internal/service/service.go b/chat-service/internal/service/service.go new file mode 100644 index 0000000..f960fa0 --- /dev/null +++ b/chat-service/internal/service/service.go @@ -0,0 +1,81 @@ +package service + +import ( + "context" + "fmt" + "io" + "log" + + "github.com/Lemper29/ChatRoom/chat-service/internal/storage" + "github.com/Lemper29/ChatRoom/chat-service/pkg/models" + pb "github.com/Lemper29/ChatRoom/gen/go/v1" +) + +type Service struct { + pb.UnimplementedChatServiceServer + repo storage.Storage +} + +func NewService(repo storage.Storage) *Service { + return &Service{ + repo: repo, + } +} + +func (s *Service) Connect(stream pb.ChatService_ConnectServer) error { + for { + firstMsg, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + log.Printf("Received from %s: %s in RoomId %s", firstMsg.GetUsername(), firstMsg.GetContent(), firstMsg.GetRoomId()) + + modelsChatMessage := models.NewChatMessage( + firstMsg.RoomId, + firstMsg.UserId, + firstMsg.Username, + firstMsg.Content, + firstMsg.Type, + ) + + response := pb.ChatMessage{ + Id: modelsChatMessage.ID, + Type: modelsChatMessage.Type, + RoomId: modelsChatMessage.RoomID, + UserId: modelsChatMessage.UserID, + Username: modelsChatMessage.Username, + Content: modelsChatMessage.Content, + Timestamp: modelsChatMessage.Timestamp, + } + + if err := stream.Send(&response); err != nil { + log.Printf("Error sending message: %v", err) + return err + } + } +} + +func (s *Service) CreateChat(ctx context.Context, req *pb.CreateChatRequest) (*pb.CreateChatResponse, error) { + reqCreateChat := models.CreateChatRequest{ + RoomID: req.RoomId, + UserID: req.UserId, + Name: req.Name, + Description: req.Description, + } + + createChat, err := s.repo.CreateChat(ctx, &reqCreateChat) + if err != nil { + return nil, fmt.Errorf("failed to create chat room: %w", err) + } + + resCreateChat := pb.CreateChatResponse{ + RoomId: createChat.RoomID, + Name: createChat.Name, + } + + return &resCreateChat, nil +} diff --git a/chat-service/internal/service/service_test.go b/chat-service/internal/service/service_test.go new file mode 100644 index 0000000..64d8fcb --- /dev/null +++ b/chat-service/internal/service/service_test.go @@ -0,0 +1,107 @@ +package service + +import ( + "context" + "testing" + + "github.com/Lemper29/ChatRoom/chat-service/pkg/models" + pb "github.com/Lemper29/ChatRoom/gen/go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockRepository struct { + mock.Mock +} + +func (m *MockRepository) CreateChat(ctx context.Context, req *models.CreateChatRequest) (*models.CreateChatResponse, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.CreateChatResponse), args.Error(1) +} + +func TestCreateChat_Success(t *testing.T) { + // Создаем mock репозитория + mockRepo := new(MockRepository) + s := NewService(mockRepo) + + // Подготавливаем тестовые данные + testReq := &pb.CreateChatRequest{ + RoomId: "room-123", + UserId: "user-456", + Name: "Test Chat", + Description: "Test Description", + } + + expectedRepoReq := &models.CreateChatRequest{ + RoomID: testReq.RoomId, + UserID: testReq.UserId, + Name: testReq.Name, + Description: testReq.Description, + } + + // Исправляем: используем models.CreateChatResponse вместо models.Chat + expectedResponse := &models.CreateChatResponse{ + RoomID: "room-123", + Name: "Test Chat", + } + + // Настраиваем ожидания для mock + mockRepo.On("CreateChat", mock.Anything, expectedRepoReq).Return(expectedResponse, nil) + + // Вызываем тестируемый метод + ctx := context.Background() + res, err := s.CreateChat(ctx, testReq) + + // Проверяем результаты + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, "room-123", res.RoomId) + assert.Equal(t, "Test Chat", res.Name) + + // Проверяем что mock был вызван с правильными параметрами + mockRepo.AssertCalled(t, "CreateChat", mock.Anything, expectedRepoReq) +} + +func TestCreateChat_RepositoryError(t *testing.T) { + // Создаем mock репозитория + mockRepo := new(MockRepository) + s := NewService(mockRepo) + + testReq := &pb.CreateChatRequest{ + RoomId: "room-123", + UserId: "user-456", + Name: "Test Chat", + Description: "Test Description", + } + + expectedRepoReq := &models.CreateChatRequest{ + RoomID: testReq.RoomId, + UserID: testReq.UserId, + Name: testReq.Name, + Description: testReq.Description, + } + + // Настраиваем mock на возврат ошибки + mockRepo.On("CreateChat", mock.Anything, expectedRepoReq).Return((*models.CreateChatResponse)(nil), assert.AnError) + + // Вызываем тестируемый метод + ctx := context.Background() + res, err := s.CreateChat(ctx, testReq) + + // Проверяем что получили ошибку + assert.Error(t, err) + assert.Nil(t, res) + assert.Contains(t, err.Error(), "failed to create chat") +} + +// Дополнительные тесты +func TestNewService(t *testing.T) { + mockRepo := new(MockRepository) + service := NewService(mockRepo) + + assert.NotNil(t, service) + assert.Equal(t, mockRepo, service.repo) +} diff --git a/chat-service/internal/storage/db/postgres.go b/chat-service/internal/storage/db/postgres.go new file mode 100644 index 0000000..05d62da --- /dev/null +++ b/chat-service/internal/storage/db/postgres.go @@ -0,0 +1,65 @@ +package db + +import ( + "context" + "fmt" + "time" + + "github.com/Lemper29/ChatRoom/chat-service/internal/storage" + "github.com/Lemper29/ChatRoom/chat-service/pkg/models" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type PostgresDB struct { + db *gorm.DB +} + +func NewPostgresStorage(config postgres.Config) (storage.Storage, error) { + db, err := gorm.Open(postgres.New(config), &gorm.Config{ + PrepareStmt: true, + }) + if err != nil { + return nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + fmt.Printf("Postgres Database successful") + + return &PostgresDB{db: db}, nil +} + +func (p *PostgresDB) CreateChat(ctx context.Context, req *models.CreateChatRequest) (*models.CreateChatResponse, error) { + // Создаем объект комнаты для БД + chatRoom := &models.ChatRoom{ + ID: req.RoomID, + Name: req.Name, + } + + // Создаем запись в БД + if err := p.db.WithContext(ctx).Create(chatRoom).Error; err != nil { + return nil, fmt.Errorf("failed to create chat room: %w", err) + } + + // Получаем созданную запись (опционально, но полезно для получения полных данных) + var createdRoom models.ChatRoom + if err := p.db.WithContext(ctx).Where("id = ?", req.RoomID).First(&createdRoom).Error; err != nil { + return nil, fmt.Errorf("failed to fetch created chat room: %w", err) + } + + // Формируем ответ + response := &models.CreateChatResponse{ + RoomID: createdRoom.ID, + Name: chatRoom.Name, + } + + return response, nil +} diff --git a/chat-service/internal/storage/storage.go b/chat-service/internal/storage/storage.go new file mode 100644 index 0000000..358a60a --- /dev/null +++ b/chat-service/internal/storage/storage.go @@ -0,0 +1,11 @@ +package storage + +import ( + "context" + + "github.com/Lemper29/ChatRoom/chat-service/pkg/models" +) + +type Storage interface { + CreateChat(ctx context.Context, req *models.CreateChatRequest) (*models.CreateChatResponse, error) +} diff --git a/chat-service/pkg/models/models.go b/chat-service/pkg/models/models.go new file mode 100644 index 0000000..006f534 --- /dev/null +++ b/chat-service/pkg/models/models.go @@ -0,0 +1,53 @@ +package models + +import ( + "fmt" + "time" + + pb "github.com/Lemper29/ChatRoom/gen/go/v1" +) + +type ChatMessage struct { + ID string + Type pb.MessageType + RoomID string + UserID string + Username string + Content string + Timestamp int64 +} + +type ChatRoom struct { + ID string `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` +} + +type CreateChatRequest struct { + RoomID string + UserID string + Name string + Description string +} + +type CreateChatResponse struct { + RoomID string + Name string +} + +// Создание нового сообщения +func NewChatMessage(roomID, userID, username, content string, msgType pb.MessageType) *ChatMessage { + return &ChatMessage{ + ID: generateID(), + Type: msgType, + RoomID: roomID, + UserID: userID, + Username: username, + Content: content, + Timestamp: time.Now().Unix(), + } +} + +// Генерация ID (упрощенная версия) +func generateID() string { + return time.Now().Format("20060102150405") + "-" + fmt.Sprintf("%d", time.Now().UnixNano()) +} diff --git a/service/cmd/server/main.go b/service/cmd/server/main.go index 3c11360..7bb3ce9 100644 --- a/service/cmd/server/main.go +++ b/service/cmd/server/main.go @@ -4,12 +4,24 @@ import ( "log" "github.com/Lemper29/ChatRoom/chat-service/internal/server" + "github.com/Lemper29/ChatRoom/chat-service/internal/storage/db" + "gorm.io/driver/postgres" ) func main() { - server := server.NewGrpcServer("localhost:8080") + cfg := postgres.Config{ + DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable", + PreferSimpleProtocol: true, + } + + storage, err := db.NewPostgresStorage(cfg) + if err != nil { + log.Fatalf("DB error: %v", err) + } + + server := server.NewGrpcServer(":8080", storage) if err := server.Server(); err != nil { - log.Fatalf("Server err: %v", err) + log.Fatalf("Server error: %v", err) } } diff --git a/service/go.mod b/service/go.mod index ae90311..5c681dd 100644 --- a/service/go.mod +++ b/service/go.mod @@ -3,8 +3,9 @@ module github.com/Lemper29/ChatRoom/chat-service go 1.25.1 require ( - github.com/Lemper29/ChatRoom v0.0.0-20251005131034-e79e8aac02bb + github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8 google.golang.org/grpc v1.75.1 + gorm.io/gorm v1.25.10 ) require ( @@ -16,7 +17,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/sync v0.15.0 // indirect - gorm.io/gorm v1.25.10 // indirect ) require ( diff --git a/service/go.sum b/service/go.sum index eb0df3b..3d189f6 100644 --- a/service/go.sum +++ b/service/go.sum @@ -1,6 +1,8 @@ -github.com/Lemper29/ChatRoom v0.0.0-20251005131034-e79e8aac02bb h1:9W4xspTiRnImyYTiGtXrdhY8+RCqoHQdRHfQPRB/B7Y= -github.com/Lemper29/ChatRoom v0.0.0-20251005131034-e79e8aac02bb/go.mod h1:HLBRSvrN+oE7CD/R5y8szNIbnPFEt/WHFPmdXyMl0xk= +github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8 h1:nfZhrBMHVyyGQjDtBpeZMA15QofmiFj6BOMfajfPNXw= +github.com/Lemper29/ChatRoom v0.0.0-20251007122616-713807057ca8/go.mod h1:HLBRSvrN+oE7CD/R5y8szNIbnPFEt/WHFPmdXyMl0xk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -23,10 +25,13 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -59,6 +64,8 @@ google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aO google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= diff --git a/service/internal/server/server.go b/service/internal/server/server.go index eb84a0a..e147e7d 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -4,22 +4,26 @@ import ( "fmt" "net" + "github.com/Lemper29/ChatRoom/chat-service/internal/service" + "github.com/Lemper29/ChatRoom/chat-service/internal/storage" pb "github.com/Lemper29/ChatRoom/gen/go/v1" "google.golang.org/grpc" ) -type server struct { +type Server struct { pb.UnimplementedChatServiceServer - addr string + service *service.Service + addr string } -func NewGrpcServer(addr string) *server { - return &server{ - addr: addr, +func NewGrpcServer(addr string, storage storage.Storage) *Server { + return &Server{ + addr: addr, + service: service.NewService(storage), } } -func (s *server) Server() error { +func (s *Server) Server() error { lis, err := net.Listen("tcp", s.addr) if err != nil { return fmt.Errorf("failed to listen: %v", err) diff --git a/service/internal/service/service.go b/service/internal/service/service.go index f52025a..f960fa0 100644 --- a/service/internal/service/service.go +++ b/service/internal/service/service.go @@ -1,6 +1,8 @@ package service import ( + "context" + "fmt" "io" "log" @@ -9,18 +11,18 @@ import ( pb "github.com/Lemper29/ChatRoom/gen/go/v1" ) -type service struct { +type Service struct { pb.UnimplementedChatServiceServer repo storage.Storage } -func NewService(repo storage.Storage) *service { - return &service{ +func NewService(repo storage.Storage) *Service { + return &Service{ repo: repo, } } -func (s *service) Connect(stream pb.ChatService_ConnectServer) error { +func (s *Service) Connect(stream pb.ChatService_ConnectServer) error { for { firstMsg, err := stream.Recv() if err == io.EOF { @@ -56,3 +58,24 @@ func (s *service) Connect(stream pb.ChatService_ConnectServer) error { } } } + +func (s *Service) CreateChat(ctx context.Context, req *pb.CreateChatRequest) (*pb.CreateChatResponse, error) { + reqCreateChat := models.CreateChatRequest{ + RoomID: req.RoomId, + UserID: req.UserId, + Name: req.Name, + Description: req.Description, + } + + createChat, err := s.repo.CreateChat(ctx, &reqCreateChat) + if err != nil { + return nil, fmt.Errorf("failed to create chat room: %w", err) + } + + resCreateChat := pb.CreateChatResponse{ + RoomId: createChat.RoomID, + Name: createChat.Name, + } + + return &resCreateChat, nil +} diff --git a/service/internal/storage/db/postgres.go b/service/internal/storage/db/postgres.go index 07f4d9c..05d62da 100644 --- a/service/internal/storage/db/postgres.go +++ b/service/internal/storage/db/postgres.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/Lemper29/ChatRoom/chat-service/internal/storage" "github.com/Lemper29/ChatRoom/chat-service/pkg/models" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -14,7 +15,7 @@ type PostgresDB struct { db *gorm.DB } -func NewPostgresStorage(config postgres.Config) (*gorm.DB, error) { +func NewPostgresStorage(config postgres.Config) (storage.Storage, error) { db, err := gorm.Open(postgres.New(config), &gorm.Config{ PrepareStmt: true, }) @@ -33,7 +34,32 @@ func NewPostgresStorage(config postgres.Config) (*gorm.DB, error) { fmt.Printf("Postgres Database successful") - return db, nil + return &PostgresDB{db: db}, nil } -func (p *PostgresDB) CreateChat(ctx context.Context, req *models.CreateChatRequest) (*models.CreateChatResponse, error) +func (p *PostgresDB) CreateChat(ctx context.Context, req *models.CreateChatRequest) (*models.CreateChatResponse, error) { + // Создаем объект комнаты для БД + chatRoom := &models.ChatRoom{ + ID: req.RoomID, + Name: req.Name, + } + + // Создаем запись в БД + if err := p.db.WithContext(ctx).Create(chatRoom).Error; err != nil { + return nil, fmt.Errorf("failed to create chat room: %w", err) + } + + // Получаем созданную запись (опционально, но полезно для получения полных данных) + var createdRoom models.ChatRoom + if err := p.db.WithContext(ctx).Where("id = ?", req.RoomID).First(&createdRoom).Error; err != nil { + return nil, fmt.Errorf("failed to fetch created chat room: %w", err) + } + + // Формируем ответ + response := &models.CreateChatResponse{ + RoomID: createdRoom.ID, + Name: chatRoom.Name, + } + + return response, nil +} diff --git a/service/pkg/models/models.go b/service/pkg/models/models.go index 7171570..006f534 100644 --- a/service/pkg/models/models.go +++ b/service/pkg/models/models.go @@ -17,6 +17,11 @@ type ChatMessage struct { Timestamp int64 } +type ChatRoom struct { + ID string `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` +} + type CreateChatRequest struct { RoomID string UserID string @@ -26,6 +31,7 @@ type CreateChatRequest struct { type CreateChatResponse struct { RoomID string + Name string } // Создание нового сообщения