v0.0.18.5 Добавлен интеграционный тест для проверки работы messages_resolvers
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
65a84c6938
commit
0ab6bde370
6
go.mod
6
go.mod
@ -12,6 +12,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/vektah/gqlparser/v2 v2.5.30
|
github.com/vektah/gqlparser/v2 v2.5.30
|
||||||
golang.org/x/crypto v0.40.0
|
golang.org/x/crypto v0.40.0
|
||||||
google.golang.org/grpc v1.74.2
|
google.golang.org/grpc v1.74.2
|
||||||
@ -23,18 +24,23 @@ require (
|
|||||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.65.0 // indirect
|
github.com/prometheus/common v0.65.0 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/text v0.27.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
11
go.sum
11
go.sum
@ -18,6 +18,7 @@ github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0
|
|||||||
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
|
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@ -51,6 +52,10 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
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.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
@ -67,11 +72,15 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
|
|||||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
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/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
|
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
|
||||||
@ -105,6 +114,8 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/
|
|||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
283
internal/http/graph/messages_resolvers_test.go
Normal file
283
internal/http/graph/messages_resolvers_test.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"tailly_back_v2/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
grpcAddress = "localhost:50052"
|
||||||
|
userIDKey = "userID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupIntegrationTest(t *testing.T) *Resolver {
|
||||||
|
// Создаем соединение с gRPC сервером
|
||||||
|
conn, err := grpc.Dial(grpcAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
require.NoError(t, err, "failed to connect to gRPC server")
|
||||||
|
|
||||||
|
return &Resolver{
|
||||||
|
MessageClient: proto.NewMessageServiceClient(conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_CreateAndSendMessages(t *testing.T) {
|
||||||
|
r := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Тест создания чата
|
||||||
|
t.Run("CreateChat", func(t *testing.T) {
|
||||||
|
resolver := &mutationResolver{r}
|
||||||
|
chat, err := resolver.CreateChat(ctx, 1, 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotZero(t, chat.ID)
|
||||||
|
assert.Equal(t, 1, chat.User1ID)
|
||||||
|
assert.Equal(t, 2, chat.User2ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Тест отправки сообщений
|
||||||
|
t.Run("SendMessages", func(t *testing.T) {
|
||||||
|
resolver := &mutationResolver{r}
|
||||||
|
|
||||||
|
// Сначала создаем чат
|
||||||
|
chat, err := resolver.CreateChat(ctx, 1, 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Отправляем 5 сообщений
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
msg, err := resolver.SendMessage(
|
||||||
|
context.WithValue(ctx, userIDKey, 1), // Добавляем senderID в контекст
|
||||||
|
int(chat.ID),
|
||||||
|
"test message "+string(rune('A'+i)),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotZero(t, msg.ID)
|
||||||
|
assert.Equal(t, "test message "+string(rune('A'+i)), msg.Content)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_MessageStream(t *testing.T) {
|
||||||
|
r := setupIntegrationTest(t)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Используем существующих пользователей
|
||||||
|
const user1ID = 5
|
||||||
|
const user2ID = 12
|
||||||
|
|
||||||
|
// Создаем чат
|
||||||
|
mutation := &mutationResolver{r}
|
||||||
|
chat, err := mutation.CreateChat(ctx, user1ID, user2ID)
|
||||||
|
require.NoError(t, err, "Failed to create chat")
|
||||||
|
|
||||||
|
// Канал для синхронизации завершения теста
|
||||||
|
testDone := make(chan struct{})
|
||||||
|
defer close(testDone)
|
||||||
|
|
||||||
|
// Канал для получения ошибок из горутин
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
// Запускаем подписку на сообщения
|
||||||
|
subscription := &subscriptionResolver{r}
|
||||||
|
msgChan, err := subscription.MessageStream(ctx, user2ID)
|
||||||
|
require.NoError(t, err, "Failed to create message stream")
|
||||||
|
|
||||||
|
// Отправляем 5 сообщений
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Logf("Recovered in sender goroutine: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
select {
|
||||||
|
case <-testDone:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, err := mutation.SendMessage(
|
||||||
|
context.WithValue(ctx, userIDKey, user1ID),
|
||||||
|
int(chat.ID),
|
||||||
|
fmt.Sprintf("stream message %d", i+1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case errChan <- fmt.Errorf("failed to send message: %w", err):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond) // Увеличиваем задержку
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Получаем сообщения из стрима
|
||||||
|
received := 0
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
lastErr = err
|
||||||
|
break
|
||||||
|
case msg, ok := <-msgChan:
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if msg == nil {
|
||||||
|
continue // Пропускаем nil-сообщения (heartbeats)
|
||||||
|
}
|
||||||
|
t.Logf("Received message %d: %s", received+1, msg.Content)
|
||||||
|
received++
|
||||||
|
if received >= 5 {
|
||||||
|
return // Успешно получили все сообщения
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
lastErr = ctx.Err()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выходим из цикла если была ошибка или завершение
|
||||||
|
if lastErr != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if received < 5 {
|
||||||
|
if lastErr != nil {
|
||||||
|
require.NoError(t, lastErr, "Didn't receive all messages")
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected 5 messages, received %d", received)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_GetChatMessages(t *testing.T) {
|
||||||
|
r := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Используем существующих пользователей
|
||||||
|
const user1ID = 5
|
||||||
|
const user2ID = 12
|
||||||
|
|
||||||
|
// Создаем чат
|
||||||
|
mutation := &mutationResolver{r}
|
||||||
|
chat, err := mutation.CreateChat(ctx, user1ID, user2ID)
|
||||||
|
require.NoError(t, err, "Failed to create chat")
|
||||||
|
|
||||||
|
// Отправляем ровно 5 сообщений
|
||||||
|
expectedContents := make([]string, 5)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
content := fmt.Sprintf("test message %d", i+1)
|
||||||
|
_, err := mutation.SendMessage(
|
||||||
|
context.WithValue(ctx, userIDKey, user1ID),
|
||||||
|
int(chat.ID),
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "Failed to send message")
|
||||||
|
expectedContents[i] = content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Даем время на обработку
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// Проверяем получение сообщений
|
||||||
|
query := &queryResolver{r}
|
||||||
|
messages, err := query.GetChatMessages(ctx, int(chat.ID), 10, 0)
|
||||||
|
require.NoError(t, err, "Failed to get chat messages")
|
||||||
|
|
||||||
|
// Проверяем что получили хотя бы 5 сообщений
|
||||||
|
assert.GreaterOrEqual(t, len(messages), 5, "Expected at least 5 messages")
|
||||||
|
|
||||||
|
// Проверяем последние 5 сообщений
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
assert.Equal(t, expectedContents[4-i], messages[i].Content, "Message content mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_GetUserChats(t *testing.T) {
|
||||||
|
r := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Создаем несколько чатов
|
||||||
|
mutation := &mutationResolver{r}
|
||||||
|
_, err := mutation.CreateChat(ctx, 1, 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = mutation.CreateChat(ctx, 1, 3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Проверяем получение чатов пользователя
|
||||||
|
query := &queryResolver{r}
|
||||||
|
chats, err := query.GetUserChats(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.GreaterOrEqual(t, len(chats), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegration_UpdateMessageStatus(t *testing.T) {
|
||||||
|
r := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Используем существующих пользователей
|
||||||
|
const user1ID = 5
|
||||||
|
const user2ID = 12
|
||||||
|
|
||||||
|
// Создаем чат
|
||||||
|
mutation := &mutationResolver{r}
|
||||||
|
chat, err := mutation.CreateChat(ctx, user1ID, user2ID)
|
||||||
|
require.NoError(t, err, "Failed to create chat")
|
||||||
|
|
||||||
|
// Отправляем тестовое сообщение
|
||||||
|
msg, err := mutation.SendMessage(
|
||||||
|
context.WithValue(ctx, userIDKey, user1ID),
|
||||||
|
int(chat.ID),
|
||||||
|
"status test",
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "Failed to send message")
|
||||||
|
|
||||||
|
// Даем время на обработку
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Определяем поддерживаемые статусы из вашего сервера
|
||||||
|
// (должны соответствовать тому, что действительно принимает сервер)
|
||||||
|
validStatuses := []struct {
|
||||||
|
name string
|
||||||
|
status string
|
||||||
|
}{
|
||||||
|
{"SENT", "SENT"},
|
||||||
|
{"DELIVERED", "DELIVERED"},
|
||||||
|
{"READ", "READ"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ts := range validStatuses {
|
||||||
|
t.Run(fmt.Sprintf("Update to %s", ts.name), func(t *testing.T) {
|
||||||
|
// Преобразуем строковый статус в MessageStatus
|
||||||
|
var msgStatus MessageStatus
|
||||||
|
switch ts.status {
|
||||||
|
case "SENT":
|
||||||
|
msgStatus = MessageStatusSent
|
||||||
|
case "DELIVERED":
|
||||||
|
msgStatus = MessageStatusDelivered
|
||||||
|
case "READ":
|
||||||
|
msgStatus = MessageStatusRead
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unsupported status: %s", ts.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMsg, err := mutation.UpdateMessageStatus(ctx, msg.ID, msgStatus)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("If this fails, check your server's message status constraints")
|
||||||
|
}
|
||||||
|
require.NoError(t, err, "Failed to update message status")
|
||||||
|
assert.Equal(t, ts.status, updatedMsg.Status, "Status mismatch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,224 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"tailly_back_v2/internal/domain"
|
|
||||||
"tailly_back_v2/internal/service"
|
|
||||||
"tailly_back_v2/internal/ws"
|
|
||||||
"tailly_back_v2/pkg/auth"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
Subprotocols: []string{"graphql-transport-ws"},
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatHandler struct {
|
|
||||||
chatService service.ChatService
|
|
||||||
hub *ws.Hub
|
|
||||||
tokenAuth *auth.TokenAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatHandler(chatService service.ChatService, hub *ws.Hub, tokenAuth *auth.TokenAuth) *ChatHandler {
|
|
||||||
return &ChatHandler{
|
|
||||||
chatService: chatService,
|
|
||||||
hub: hub,
|
|
||||||
tokenAuth: tokenAuth,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("Incoming WebSocket headers: %+v", r.Header)
|
|
||||||
log.Printf("Cookies: %+v", r.Cookies())
|
|
||||||
requestedProtocol := r.Header.Get("Sec-WebSocket-Protocol")
|
|
||||||
if requestedProtocol != "" && requestedProtocol != "graphql-transport-ws" {
|
|
||||||
http.Error(w, "Unsupported WebSocket protocol", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Requested protocols: %v", r.Header["Sec-WebSocket-Protocol"])
|
|
||||||
// 1. Проверяем куки
|
|
||||||
var token string
|
|
||||||
cookie, err := r.Cookie("accessToken")
|
|
||||||
if err == nil {
|
|
||||||
token = cookie.Value
|
|
||||||
log.Printf("WebSocket: токен из куки: %s", token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Из заголовка Authorization
|
|
||||||
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
|
|
||||||
token = strings.TrimPrefix(authHeader, "Bearer ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Из параметра URL
|
|
||||||
if token == "" {
|
|
||||||
token = r.URL.Query().Get("token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Из куков
|
|
||||||
if token == "" {
|
|
||||||
if cookie, err := r.Cookie("accessToken"); err == nil {
|
|
||||||
token = cookie.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if token == "" {
|
|
||||||
log.Println("WebSocket: токен не найден")
|
|
||||||
http.Error(w, "Token is required", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Валидация токена
|
|
||||||
userID, err := h.tokenAuth.ValidateAccessToken(token)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WebSocket: ошибка валидации токена: %v", err)
|
|
||||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("WebSocket: успешная авторизация, userID=%d", userID)
|
|
||||||
|
|
||||||
// 5. Обновление соединения
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("WebSocket upgrade error: %v", err)
|
|
||||||
http.Error(w, "Could not upgrade to WebSocket", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &ws.Client{
|
|
||||||
UserID: userID,
|
|
||||||
Send: make(chan *domain.Message, 256),
|
|
||||||
}
|
|
||||||
|
|
||||||
h.hub.RegisterClient(client)
|
|
||||||
|
|
||||||
// Добавляем контекст для управления жизненным циклом соединения
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Запускаем горутины с обработкой контекста
|
|
||||||
go h.readPump(ctx, conn, client, userID)
|
|
||||||
go h.writePump(ctx, conn, client)
|
|
||||||
|
|
||||||
// Ждем завершения
|
|
||||||
<-ctx.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) readPump(ctx context.Context, conn *websocket.Conn, client *ws.Client, userID int) {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
// Отправляем ping
|
|
||||||
if err := conn.WriteJSON(map[string]string{"type": "ping"}); err != nil {
|
|
||||||
log.Printf("Ping error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
var msg struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Payload struct {
|
|
||||||
ReceiverID int `json:"receiverId"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
} `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.ReadJSON(&msg); err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
||||||
log.Printf("WebSocket error: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Type == "pong" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка ping/pong
|
|
||||||
if msg.Type == "ping" {
|
|
||||||
conn.WriteJSON(map[string]string{"type": "pong"})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Type != "message" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем receiverId
|
|
||||||
if msg.Payload.ReceiverID == 0 {
|
|
||||||
log.Printf("Invalid receiverId: 0")
|
|
||||||
conn.WriteJSON(map[string]interface{}{
|
|
||||||
"type": "error",
|
|
||||||
"message": "Invalid receiver ID",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Логирование для отладки
|
|
||||||
log.Printf("Received message from %d to %d", userID, msg.Payload.ReceiverID)
|
|
||||||
|
|
||||||
// Создаем или находим чат
|
|
||||||
chat, err := h.chatService.GetOrCreateChat(ctx, userID, msg.Payload.ReceiverID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Chat error: %v", err)
|
|
||||||
conn.WriteJSON(map[string]interface{}{
|
|
||||||
"type": "error",
|
|
||||||
"message": "Chat error",
|
|
||||||
"details": err.Error(),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем сообщение
|
|
||||||
message, err := h.chatService.SendMessage(
|
|
||||||
ctx,
|
|
||||||
userID,
|
|
||||||
chat.ID,
|
|
||||||
msg.Payload.Content,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Message send error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рассылаем сообщение
|
|
||||||
h.hub.Broadcast(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) writePump(ctx context.Context, conn *websocket.Conn, client *ws.Client) {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case message, ok := <-client.Send:
|
|
||||||
if !ok {
|
|
||||||
// Канал закрыт
|
|
||||||
conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.WriteJSON(message); err != nil {
|
|
||||||
log.Printf("WebSocket write error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
qodana.yaml
Normal file
29
qodana.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||||
|
linter: jetbrains/qodana-go:2025.1
|
||||||
Loading…
x
Reference in New Issue
Block a user