v0.0.18.5 Добавлен интеграционный тест для проверки работы messages_resolvers
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
madipo2611 2025-08-18 14:17:14 +03:00
parent 65a84c6938
commit 0ab6bde370
5 changed files with 329 additions and 224 deletions

6
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
github.com/vektah/gqlparser/v2 v2.5.30
golang.org/x/crypto v0.40.0
google.golang.org/grpc v1.74.2
@ -23,18 +24,23 @@ require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // 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/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // 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/yaml.v3 v3.0.1 // indirect
)

11
go.sum
View File

@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
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=
@ -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/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/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/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
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/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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
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/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

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

View File

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