diff --git a/tests/main_test.go b/tests/main_test.go index 49e24e3..98a2f1d 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -3,10 +3,13 @@ package tests import ( "bytes" "encoding/json" + "fmt" "gitea.pena/SQuiz/common/model" "github.com/stretchr/testify/assert" + "io" "net/http" "os" + "strings" "sync" "testing" "time" @@ -882,3 +885,7225 @@ func TestManualDone_Security(t *testing.T) { } // todo 7.3.7 7.3.8 7.3.9 7.4 +// TODO ВСЕ ЧТО НИЖЕ ДЕЛАЛ КУРСОР НАДО ВСЕ ТЕСТЫ ПЕРЕПРОВЕРИТЬ ПОКА ЧТО ПРОВЕРЕННО ТОЛЬКО НАЛИЧИЕ ДЛЯ КАЖДОГО ТЕСТ КЕЙСА +func createLeadTargetRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/account/leadtarget", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCreateLeadTarget_Success(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "example@mail.com", + "name": "Example Channel", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) +} + +func TestCreateLeadTarget_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/account/leadtarget", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := createLeadTargetRequest("invalid_token", map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := createLeadTargetRequest(expiredToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCreateLeadTarget_InputValidation(t *testing.T) { + t.Run("MissingRequiredFields", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidType", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "invalid", + "quizID": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidTargetFormat", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "invalid_email", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyBody", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCreateLeadTarget_Duplication(t *testing.T) { + t.Run("ExistingTarget", func(t *testing.T) { + // Сначала создаем target + resp1, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 456, + "target": "duplicate@mail.com", + }) + assert.NoError(t, err) + resp1.Body.Close() + + // Пытаемся создать тот же target снова + resp2, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 456, + "target": "duplicate@mail.com", + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusAlreadyReported, resp2.StatusCode) + }) + + t.Run("DifferentCase", func(t *testing.T) { + // Сначала создаем target + resp1, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 789, + "target": "case@mail.com", + }) + assert.NoError(t, err) + resp1.Body.Close() + + // Пытаемся создать тот же target с другим регистром + resp2, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 789, + "target": "CASE@mail.com", + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusAlreadyReported, resp2.StatusCode) + }) +} + +func TestCreateLeadTarget_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": "1' OR '1'='1", + "target": "example@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCreateLeadTarget_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 999, + "target": "perf@mail.com", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(1000)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 1000 + index, + "target": fmt.Sprintf("load%d@mail.com", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestCreateLeadTarget_BoundaryCases(t *testing.T) { + t.Run("MaxLengthFields", func(t *testing.T) { + longEmail := strings.Repeat("a", 100) + "@domain.com" + longName := strings.Repeat("b", 200) + + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": longEmail, + "name": longName, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("SpecialCharacters", func(t *testing.T) { + resp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "special!@#$%^&*()@domain.com", + "name": "Special Name!", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +// todo 8.3.8 8.4 8.5 + +func updateLeadTargetRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("PUT", baseURL+"/account/leadtarget", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestUpdateLeadTarget_Success(t *testing.T) { + // Сначала создаем target для обновления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "old@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := createResult["id"] + + // Обновляем target + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": targetID, + "target": "new_target@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, targetID, result["id"]) + assert.Equal(t, "new_target@mail.com", result["target"]) +} + +func TestUpdateLeadTarget_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("PUT", baseURL+"/account/leadtarget", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := updateLeadTargetRequest("invalid_token", map[string]interface{}{ + "id": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := updateLeadTargetRequest(expiredToken, map[string]interface{}{ + "id": 123, + "target": "example@mail.com", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestUpdateLeadTarget_InputValidation(t *testing.T) { + t.Run("MissingRequiredFields", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": 123, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": "invalid", + "target": "example@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidTargetFormat", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": 123, + "target": "invalid_email", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyBody", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestUpdateLeadTarget_Existence(t *testing.T) { + t.Run("NonExistentID", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": 999999, + "target": "example@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) +} + +func TestUpdateLeadTarget_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": "1' OR '1'='1", + "target": "example@mail.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": 123, + "target": "", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestUpdateLeadTarget_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + // Создаем target для обновления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 456, + "target": "perf@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := createResult["id"] + + start := time.Now() + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": targetID, + "target": "updated_perf@mail.com", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(1000)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 30; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Создаем target для обновления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 1000 + index, + "target": fmt.Sprintf("load%d@mail.com", index), + }) + if err != nil { + return + } + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + if err != nil { + return + } + targetID := createResult["id"] + + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": targetID, + "target": fmt.Sprintf("updated_load%d@mail.com", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestUpdateLeadTarget_BoundaryCases(t *testing.T) { + t.Run("MaxLengthTarget", func(t *testing.T) { + // Создаем target для обновления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 789, + "target": "old@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := createResult["id"] + + longEmail := strings.Repeat("a", 100) + "@domain.com" + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": targetID, + "target": longEmail, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("SpecialCharacters", func(t *testing.T) { + // Создаем target для обновления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 999, + "target": "old@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := createResult["id"] + + resp, err := updateLeadTargetRequest(validToken, map[string]interface{}{ + "id": targetID, + "target": "special!@#$%^&*()@domain.com", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +// todo 9.3.8 9.4 9.5 + +func deleteLeadTargetRequest(token string, targetID string) (*http.Response, error) { + req, err := http.NewRequest("DELETE", baseURL+"/account/leadtarget/"+targetID, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + return http.DefaultClient.Do(req) +} + +func TestDeleteLeadTarget_Success(t *testing.T) { + // Сначала создаем target для удаления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "delete@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := fmt.Sprintf("%v", createResult["id"]) + + // Удаляем target + resp, err := deleteLeadTargetRequest(validToken, targetID) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, targetID, fmt.Sprintf("%v", result["id"])) +} + +func TestDeleteLeadTarget_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("DELETE", baseURL+"/account/leadtarget/123", nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := deleteLeadTargetRequest("invalid_token", "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := deleteLeadTargetRequest(expiredToken, "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestDeleteLeadTarget_InputValidation(t *testing.T) { + t.Run("InvalidID", func(t *testing.T) { + resp, err := deleteLeadTargetRequest(validToken, "invalid_id") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyID", func(t *testing.T) { + req, err := http.NewRequest("DELETE", baseURL+"/account/leadtarget/", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+validToken) + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := deleteLeadTargetRequest(validToken, "999999") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +func TestDeleteLeadTarget_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := deleteLeadTargetRequest(validToken, "1' OR '1'='1") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := deleteLeadTargetRequest(validToken, "") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestDeleteLeadTarget_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + // Создаем target для удаления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 456, + "target": "perf_delete@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := fmt.Sprintf("%v", createResult["id"]) + + start := time.Now() + resp, err := deleteLeadTargetRequest(validToken, targetID) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(1000)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 30; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Создаем target для удаления + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 2000 + index, + "target": fmt.Sprintf("load_delete%d@mail.com", index), + }) + if err != nil { + return + } + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + if err != nil { + return + } + targetID := fmt.Sprintf("%v", createResult["id"]) + + resp, err := deleteLeadTargetRequest(validToken, targetID) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestDeleteLeadTarget_AlreadyDeleted(t *testing.T) { + // Сначала создаем target + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 789, + "target": "already_deleted@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := fmt.Sprintf("%v", createResult["id"]) + + // Удаляем target первый раз + resp1, err := deleteLeadTargetRequest(validToken, targetID) + assert.NoError(t, err) + resp1.Body.Close() + + // Пытаемся удалить тот же target снова + resp2, err := deleteLeadTargetRequest(validToken, targetID) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) +} + +// todo 10.3.8 10.3.9 10.4 10.5 + +func getLeadTargetByQuizIDRequest(token string, quizID string) (*http.Response, error) { + req, err := http.NewRequest("GET", baseURL+"/account/leadtarget/"+quizID, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + return http.DefaultClient.Do(req) +} + +func TestGetLeadTargetByQuizID_Success(t *testing.T) { + // Сначала создаем target для получения + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 123, + "target": "get@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + + // Получаем target по quizID + resp, err := getLeadTargetByQuizIDRequest(validToken, "123") + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) + assert.Equal(t, float64(123), result["quizID"]) + assert.Equal(t, "mail", result["type"]) + assert.Equal(t, "get@mail.com", result["target"]) +} + +func TestGetLeadTargetByQuizID_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("GET", baseURL+"/account/leadtarget/123", nil) + assert.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest("invalid_token", "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest(expiredToken, "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetLeadTargetByQuizID_InputValidation(t *testing.T) { + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest(validToken, "invalid_id") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyQuizID", func(t *testing.T) { + req, err := http.NewRequest("GET", baseURL+"/account/leadtarget/", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+validToken) + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest(validToken, "999999") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) +} + +func TestGetLeadTargetByQuizID_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest(validToken, "1' OR '1'='1") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := getLeadTargetByQuizIDRequest(validToken, "") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestGetLeadTargetByQuizID_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + // Создаем target для получения + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 456, + "target": "perf_get@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + start := time.Now() + resp, err := getLeadTargetByQuizIDRequest(validToken, "456") + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(300)) + }) + + t.Run("LoadTest", func(t *testing.T) { + // Создаем target для тестирования + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 789, + "target": "load_get@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getLeadTargetByQuizIDRequest(validToken, "789") + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +func TestGetLeadTargetByQuizID_DeletedTarget(t *testing.T) { + // Сначала создаем target + createResp, err := createLeadTargetRequest(validToken, map[string]interface{}{ + "type": "mail", + "quizID": 999, + "target": "deleted@mail.com", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + targetID := fmt.Sprintf("%v", createResult["id"]) + + // Удаляем target + deleteResp, err := deleteLeadTargetRequest(validToken, targetID) + assert.NoError(t, err) + deleteResp.Body.Close() + + // Пытаемся получить удаленный target + resp, err := getLeadTargetByQuizIDRequest(validToken, "999") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + +// todo 11.3.8 11.3.9 11.4 11.5 + +func createQuestionRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/question/create", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCreateQuestion_Success(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Какой основной компонент воздуха?", + "type": "variant", + "description": "Выберите один правильный ответ.", + "required": true, + "page": 1, + "content": "{}", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) + assert.Equal(t, float64(12345), result["quiz_id"]) + assert.Equal(t, "Какой основной компонент воздуха?", result["title"]) + assert.Equal(t, "variant", result["type"]) + assert.Equal(t, "Выберите один правильный ответ.", result["description"]) + assert.Equal(t, true, result["required"]) + assert.Equal(t, float64(1), result["page"]) + assert.Equal(t, "{}", result["content"]) + assert.NotEmpty(t, result["created_at"]) +} + +func TestCreateQuestion_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/question/create", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := createQuestionRequest("invalid_token", map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := createQuestionRequest(expiredToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCreateQuestion_InputValidation(t *testing.T) { + t.Run("MissingRequiredFields", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "title": "Test Question", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": "invalid", + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidType", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "invalid_type", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidRequired", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + "required": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidPage", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + "page": "not_number", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidContent", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + "content": "invalid_json", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyBody", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCreateQuestion_DifferentTypes(t *testing.T) { + questionTypes := []string{"text", "variant", "images", "select", "varimg", "emoji", "date", "number", "page", "rating", "result", "file"} + + for _, questionType := range questionTypes { + t.Run(questionType, func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": fmt.Sprintf("Test %s Question", questionType), + "type": questionType, + "content": "{}", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} + +func TestCreateQuestion_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": "1' OR '1'='1", + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "", + "type": "variant", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, "", result["title"]) + }) +} + +func TestCreateQuestion_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Performance Test Question", + "type": "variant", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345 + index, + "title": fmt.Sprintf("Load Test Question %d", index), + "type": "variant", + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestCreateQuestion_BoundaryCases(t *testing.T) { + t.Run("MaxLengthTitle", func(t *testing.T) { + longTitle := strings.Repeat("a", 512) + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": longTitle, + "type": "variant", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("LongDescription", func(t *testing.T) { + longDescription := strings.Repeat("b", 1000) + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + "description": longDescription, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("SpecialCharacters", func(t *testing.T) { + resp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Special!@#$%^&*() Question", + "type": "variant", + "description": "Description with special chars: !@#$%^&*()", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +// todo 12.3.4 12.3.5 12.3.6 12.3.7 12.3.8 12.4 12.5 + +func getQuestionListRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/question/getList", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetQuestionList_Success(t *testing.T) { + // Сначала создаем несколько вопросов для тестирования + for i := 0; i < 3; i++ { + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": fmt.Sprintf("Test Question %d", i), + "type": "variant", + }) + assert.NoError(t, err) + createResp.Body.Close() + } + + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": 1, + "quiz_id": 12345, + "type": "variant", + "deleted": false, + "required": true, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["count"]) + assert.NotEmpty(t, result["items"]) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 10) + + if len(items) > 0 { + firstItem, ok := items[0].(map[string]interface{}) + assert.True(t, ok) + assert.NotEmpty(t, firstItem["id"]) + assert.Equal(t, float64(12345), firstItem["quiz_id"]) + assert.Equal(t, "variant", firstItem["type"]) + } +} + +func TestGetQuestionList_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/question/getList", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getQuestionListRequest("invalid_token", map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getQuestionListRequest(expiredToken, map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetQuestionList_InputValidation(t *testing.T) { + t.Run("InvalidPagination", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": "invalid", + "page": "invalid", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "quiz_id": "invalid", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidType", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "type": "invalid_type", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidBoolean", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "deleted": "not_boolean", + "required": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidTimeRange", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "from": 1000, + "to": 500, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestGetQuestionList_Pagination(t *testing.T) { + // Создаем вопросы для тестирования пагинации + for i := 0; i < 15; i++ { + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12346, + "title": fmt.Sprintf("Pagination Question %d", i), + "type": "text", + }) + assert.NoError(t, err) + createResp.Body.Close() + } + + t.Run("FirstPage", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 1, + "quiz_id": 12346, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("SecondPage", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 2, + "quiz_id": 12346, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("EmptyPage", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 100, + "quiz_id": 12346, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.Empty(t, items) + }) +} + +func TestGetQuestionList_Filters(t *testing.T) { + // Создаем вопросы разных типов + questionTypes := []string{"text", "variant", "select"} + for _, questionType := range questionTypes { + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12347, + "title": fmt.Sprintf("Filter Question %s", questionType), + "type": questionType, + }) + assert.NoError(t, err) + createResp.Body.Close() + } + + t.Run("FilterByType", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "quiz_id": 12347, + "type": "text", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + + for _, item := range items { + question, ok := item.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, "text", question["type"]) + } + }) + + t.Run("FilterBySearch", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "quiz_id": 12347, + "search": "Filter", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.NotEmpty(t, items) + }) + + t.Run("FilterByRequired", func(t *testing.T) { + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "quiz_id": 12347, + "required": true, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + + for _, item := range items { + question, ok := item.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, true, question["required"]) + } + }) +} + +func TestGetQuestionList_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": 1, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getQuestionListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 1, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 13.3.4 13.3.5 13.3.6 13.4 13.5 + +func editQuestionRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("PATCH", baseURL+"/question/edit", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestEditQuestion_Success(t *testing.T) { + // Сначала создаем вопрос для редактирования + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Original Question", + "type": "variant", + "required": true, + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Редактируем вопрос + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": "Обновленный заголовок вопроса?", + "desc": "Новое описание для вопроса.", + "type": "text", + "required": false, + "content": "{\"placeholder\":\"Введите ваш ответ здесь\"}", + "page": 2, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, questionID, result["updated"]) +} + +func TestEditQuestion_SingleField(t *testing.T) { + // Сначала создаем вопрос для редактирования + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Single Field Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Редактируем только заголовок + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": "Только заголовок обновлен", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, questionID, result["updated"]) +} + +func TestEditQuestion_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 123, + "title": "Test Question", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("PATCH", baseURL+"/question/edit", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := editQuestionRequest("invalid_token", map[string]interface{}{ + "id": 123, + "title": "Test Question", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := editQuestionRequest(expiredToken, map[string]interface{}{ + "id": 123, + "title": "Test Question", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestEditQuestion_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "title": "Запрос без ID", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + "title": "Невалидный ID", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": 99999, + "title": "Несуществующий вопрос", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidTitle", func(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Пытаемся установить слишком длинный заголовок + longTitle := strings.Repeat("a", 513) + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": longTitle, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidType", func(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "type": "invalid_type", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidRequired", func(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "required": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidContent", func(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "content": "invalid_json", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestEditQuestion_Security(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Security Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + t.Run("SQLInjection", func(t *testing.T) { + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": "1' OR '1'='1", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, questionID, result["updated"]) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": "", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, questionID, result["updated"]) + }) +} + +func TestEditQuestion_Performance(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Performance Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": "Updated Performance Test Question", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 30; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Создаем вопрос для редактирования + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345 + index, + "title": fmt.Sprintf("Load Test Question %d", index), + "type": "variant", + }) + if err != nil { + return + } + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + if err != nil { + return + } + questionID := createResult["id"] + + resp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": fmt.Sprintf("Updated Load Test Question %d", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +// todo 14.3.5 14.3.6 14.3.7 14.4 14.5 + +func copyQuestionRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/question/copy", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCopyQuestion_Success(t *testing.T) { + // Сначала создаем оригинальный вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Какой основной компонент воздуха?", + "type": "variant", + "description": "Выберите один правильный ответ из предложенных.", + "required": true, + "page": 1, + "content": "{}", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + originalID := createResult["id"] + + // Копируем вопрос + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": originalID, + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) + assert.NotEqual(t, originalID, result["id"]) // Новый ID должен отличаться + assert.Equal(t, float64(202), result["quiz_id"]) + assert.Equal(t, "Какой основной компонент воздуха?", result["title"]) + assert.Equal(t, "variant", result["type"]) + assert.Equal(t, "Выберите один правильный ответ из предложенных.", result["description"]) + assert.Equal(t, true, result["required"]) + assert.Equal(t, float64(1), result["page"]) + assert.Equal(t, "{}", result["content"]) + assert.Equal(t, false, result["deleted"]) + assert.NotEmpty(t, result["created_at"]) + assert.NotEmpty(t, result["updated_at"]) +} + +func TestCopyQuestion_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + "quiz_id": 202, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/question/copy", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := copyQuestionRequest("invalid_token", map[string]interface{}{ + "id": 101, + "quiz_id": 202, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := copyQuestionRequest(expiredToken, map[string]interface{}{ + "id": 101, + "quiz_id": 202, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCopyQuestion_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("MissingQuizID", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": "invalid", + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": 101, + "quiz_id": "invalid", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": 99999, + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCopyQuestion_Security(t *testing.T) { + // Сначала создаем оригинальный вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Security Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + originalID := createResult["id"] + + t.Run("SQLInjection", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": "1' OR '1'='1", + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttack", func(t *testing.T) { + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": originalID, + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) + }) +} + +func TestCopyQuestion_Performance(t *testing.T) { + // Сначала создаем оригинальный вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Performance Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + originalID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": originalID, + "quiz_id": 202, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 30; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + // Создаем оригинальный вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345 + index, + "title": fmt.Sprintf("Load Test Question %d", index), + "type": "variant", + }) + if err != nil { + return + } + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + if err != nil { + return + } + originalID := createResult["id"] + + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": originalID, + "quiz_id": 202 + index, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestCopyQuestion_OriginalPreserved(t *testing.T) { + // Сначала создаем оригинальный вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Original Question", + "type": "variant", + "required": true, + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + originalID := createResult["id"] + originalTitle := createResult["title"] + + // Копируем вопрос + resp, err := copyQuestionRequest(validToken, map[string]interface{}{ + "id": originalID, + "quiz_id": 202, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var copyResult map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(©Result) + assert.NoError(t, err) + copyID := copyResult["id"] + + // Проверяем, что оригинальный вопрос остался без изменений + assert.NotEqual(t, originalID, copyID) + assert.Equal(t, originalTitle, copyResult["title"]) +} + +// todo 15.3.4 15.3.5 15.3.6 15.4 15.5 + +func getQuestionHistoryRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/question/history", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetQuestionHistory_Success(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Original Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Редактируем вопрос несколько раз для создания истории + for i := 1; i <= 3; i++ { + editResp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": fmt.Sprintf("Updated Question Version %d", i), + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + // Получаем историю вопроса + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["items"]) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 10) + assert.Greater(t, len(items), 0) + + // Проверяем структуру первой записи истории + if len(items) > 0 { + firstItem, ok := items[0].(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, questionID, firstItem["id"]) + assert.Equal(t, float64(12345), firstItem["quiz_id"]) + assert.NotEmpty(t, firstItem["version"]) + assert.NotEmpty(t, firstItem["created_at"]) + assert.NotEmpty(t, firstItem["updated_at"]) + } +} + +func TestGetQuestionHistory_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/question/history", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getQuestionHistoryRequest("invalid_token", map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(expiredToken, map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetQuestionHistory_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidLimit", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": "ten", + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidPage", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": 10, + "p": "one", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NegativeLimit", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": -5, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("ZeroPage", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": 10, + "p": 0, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": 99999, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestGetQuestionHistory_Pagination(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Pagination Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Создаем много версий для тестирования пагинации + for i := 1; i <= 15; i++ { + editResp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": fmt.Sprintf("Version %d", i), + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + t.Run("FirstPage", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 5, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("SecondPage", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 5, + "p": 2, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("EmptyPage", func(t *testing.T) { + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 5, + "p": 100, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.Empty(t, items) + }) +} + +func TestGetQuestionHistory_NewQuestion(t *testing.T) { + // Создаем новый вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "New Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Получаем историю нового вопроса + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.Len(t, items, 1) // Должна быть только одна запись для нового вопроса + + if len(items) > 0 { + firstItem, ok := items[0].(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, questionID, firstItem["id"]) + assert.Equal(t, float64(1), firstItem["version"]) // Версия должна быть 1 + } +} + +func TestGetQuestionHistory_Performance(t *testing.T) { + // Сначала создаем вопрос с историей + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Performance Test Question", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Создаем несколько версий + for i := 1; i <= 5; i++ { + editResp, err := editQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + "title": fmt.Sprintf("Version %d", i), + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 10, + "p": 1, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getQuestionHistoryRequest(validToken, map[string]interface{}{ + "id": questionID, + "l": 5, + "p": 1, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 16.3.4 16.3.5 16.4 16.5 + +func deleteQuestionRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("DELETE", baseURL+"/question/delete", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestDeleteQuestion_Success(t *testing.T) { + // Сначала создаем вопрос для удаления + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Question to Delete", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Удаляем вопрос + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, questionID, result["deactivated"]) +} + +func TestDeleteQuestion_Idempotency(t *testing.T) { + // Сначала создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Question for Idempotency Test", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Первое удаление + resp1, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Повторное удаление того же вопроса + resp2, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) + + // Проверяем, что оба ответа содержат правильный ID + var result1, result2 map[string]interface{} + err = json.NewDecoder(resp1.Body).Decode(&result1) + assert.NoError(t, err) + err = json.NewDecoder(resp2.Body).Decode(&result2) + assert.NoError(t, err) + + assert.Equal(t, questionID, result1["deactivated"]) + assert.Equal(t, questionID, result2["deactivated"]) +} + +func TestDeleteQuestion_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("DELETE", baseURL+"/question/delete", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := deleteQuestionRequest("invalid_token", map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := deleteQuestionRequest(expiredToken, map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestDeleteQuestion_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": 99999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestDeleteQuestion_AlreadyDeleted(t *testing.T) { + // Создаем вопрос + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": "Question to Delete Twice", + "type": "variant", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + questionID := createResult["id"] + + // Удаляем вопрос + resp1, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Пытаемся удалить уже удаленный вопрос + resp2, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionID, + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) // Идемпотентность + + var result1, result2 map[string]interface{} + err = json.NewDecoder(resp1.Body).Decode(&result1) + assert.NoError(t, err) + err = json.NewDecoder(resp2.Body).Decode(&result2) + assert.NoError(t, err) + + assert.Equal(t, questionID, result1["deactivated"]) + assert.Equal(t, questionID, result2["deactivated"]) +} + +func TestDeleteQuestion_Performance(t *testing.T) { + // Создаем несколько вопросов для тестирования производительности + var questionIDs []interface{} + for i := 0; i < 10; i++ { + createResp, err := createQuestionRequest(validToken, map[string]interface{}{ + "quiz_id": 12345, + "title": fmt.Sprintf("Performance Test Question %d", i), + "type": "variant", + }) + assert.NoError(t, err) + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + createResp.Body.Close() + assert.NoError(t, err) + questionIDs = append(questionIDs, createResult["id"]) + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": questionIDs[0], + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("BulkDelete", func(t *testing.T) { + var wg sync.WaitGroup + for i := 1; i < len(questionIDs); i++ { + wg.Add(1) + go func(id interface{}) { + defer wg.Done() + resp, err := deleteQuestionRequest(validToken, map[string]interface{}{ + "id": id, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(questionIDs[i]) + } + wg.Wait() + }) +} + +// todo 17.3.5 17.3.6 17.4 17.5 + +func createQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/quiz/create", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCreateQuiz_Success(t *testing.T) { + t.Run("MinimalQuiz", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Новый квиз по истории", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + assert.NotEmpty(t, result["id"]) + assert.NotEmpty(t, result["qid"]) + assert.NotEmpty(t, result["accountid"]) + assert.Equal(t, "Новый квиз по истории", result["name"]) + assert.Equal(t, "draft", result["status"]) // Значение по умолчанию + assert.Equal(t, false, result["deleted"]) + assert.Equal(t, false, result["archived"]) + assert.Equal(t, float64(1), result["version"]) + assert.NotEmpty(t, result["created_at"]) + assert.NotEmpty(t, result["updated_at"]) + }) + + t.Run("FullQuiz", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Полный квиз по географии", + "description": "Детальный тест на знание столиц и стран.", + "fingerprinting": true, + "repeatable": true, + "note_prevented": false, + "mail_notifications": true, + "unique_answers": false, + "config": "{\"showCorrectAnswers\": true}", + "status": "start", + "limit": 100, + "due_to": 1700000000, + "question_cnt": 10, + "time_of_passing": 3600, + "pausable": true, + "super": false, + "group_id": nil, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + assert.NotEmpty(t, result["id"]) + assert.Equal(t, "Полный квиз по географии", result["name"]) + assert.Equal(t, "Детальный тест на знание столиц и стран.", result["description"]) + assert.Equal(t, true, result["fingerprinting"]) + assert.Equal(t, true, result["repeatable"]) + assert.Equal(t, false, result["note_prevented"]) + assert.Equal(t, true, result["mail_notifications"]) + assert.Equal(t, false, result["unique_answers"]) + assert.Equal(t, "{\"showCorrectAnswers\": true}", result["config"]) + assert.Equal(t, "start", result["status"]) + assert.Equal(t, float64(100), result["limit"]) + assert.Equal(t, float64(1700000000), result["due_to"]) + assert.Equal(t, float64(10), result["question_cnt"]) + assert.Equal(t, float64(3600), result["time_of_passing"]) + assert.Equal(t, true, result["pausable"]) + assert.Equal(t, false, result["super"]) + assert.Nil(t, result["group_id"]) + }) +} + +func TestCreateQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "name": "Test Quiz", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/quiz/create", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := createQuizRequest("invalid_token", map[string]interface{}{ + "name": "Test Quiz", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := createQuizRequest(expiredToken, map[string]interface{}{ + "name": "Test Quiz", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCreateQuiz_InputValidation(t *testing.T) { + t.Run("MissingName", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "description": "Test description", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NameTooLong", func(t *testing.T) { + longName := strings.Repeat("a", 701) // Больше 700 символов + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": longName, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidStatus", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Test Quiz", + "status": "invalid_status", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidFingerprinting", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Test Quiz", + "fingerprinting": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NegativeLimit", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Test Quiz", + "limit": -5, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidDueTo", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Test Quiz", + "due_to": "not_timestamp", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidConfig", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Test Quiz", + "config": "invalid json", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCreateQuiz_StatusValues(t *testing.T) { + statuses := []string{"draft", "template", "stop", "start"} + + for _, status := range statuses { + t.Run("Status_"+status, func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Quiz with status %s", status), + "status": status, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, status, result["status"]) + }) + } +} + +func TestCreateQuiz_DefaultValues(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Quiz with defaults", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем значения по умолчанию + assert.Equal(t, "draft", result["status"]) + assert.Equal(t, false, result["fingerprinting"]) + assert.Equal(t, false, result["repeatable"]) + assert.Equal(t, false, result["note_prevented"]) + assert.Equal(t, true, result["pausable"]) + assert.Equal(t, false, result["super"]) +} + +func TestCreateQuiz_Conflict(t *testing.T) { + // Создаем первый квиз + resp1, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Duplicate Quiz Name", + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusCreated, resp1.StatusCode) + + // Пытаемся создать квиз с тем же именем + resp2, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Duplicate Quiz Name", + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusConflict, resp2.StatusCode) +} + +func TestCreateQuiz_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "'; DROP TABLE quizzes; --", + "description": "'; DELETE FROM users; --", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + // Должен либо вернуть ошибку валидации, либо успешно создать квиз с экранированными данными + if resp.StatusCode == http.StatusCreated { + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, "'; DROP TABLE quizzes; --", result["name"]) + } else { + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + }) + + t.Run("XSS", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "", + "description": "", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + if resp.StatusCode == http.StatusCreated { + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, "", result["name"]) + } else { + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + }) +} + +func TestCreateQuiz_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Performance Test Quiz", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Load Test Quiz %d", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +func TestCreateQuiz_SuperQuiz(t *testing.T) { + t.Run("SuperQuizWithoutGroup", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Super Quiz", + "super": true, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, true, result["super"]) + assert.Nil(t, result["group_id"]) + }) + + t.Run("NonSuperQuizWithGroup", func(t *testing.T) { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Group Quiz", + "super": false, + "group_id": 123, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, false, result["super"]) + assert.Equal(t, float64(123), result["group_id"]) + }) +} + +// todo 18.3.4 18.3.5 18.3.6 18.3.7 18.3.8 18.4 18.5 + +func getQuizListRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/quiz/getList", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetQuizList_Success(t *testing.T) { + // Сначала создаем несколько квизов для тестирования + quizNames := []string{"Квиз по географии", "Квиз по истории", "Квиз по математике"} + var quizIDs []interface{} + + for _, name := range quizNames { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": name, + "status": "start", + }) + assert.NoError(t, err) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + resp.Body.Close() + assert.NoError(t, err) + quizIDs = append(quizIDs, result["id"]) + } + + t.Run("BasicList", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + assert.NotEmpty(t, result["count"]) + assert.NotEmpty(t, result["items"]) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + + if len(items) > 0 { + firstItem, ok := items[0].(map[string]interface{}) + assert.True(t, ok) + assert.NotEmpty(t, firstItem["id"]) + assert.NotEmpty(t, firstItem["name"]) + assert.NotEmpty(t, firstItem["status"]) + } + }) + + t.Run("WithFilters", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": 1, + "search": "география", + "status": "start", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + + // Проверяем, что все возвращенные квизы содержат "география" в названии + for _, item := range items { + quiz, ok := item.(map[string]interface{}) + assert.True(t, ok) + name, ok := quiz["name"].(string) + assert.True(t, ok) + assert.Contains(t, strings.ToLower(name), "география") + } + }) +} + +func TestGetQuizList_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/quiz/getList", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getQuizListRequest("invalid_token", map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getQuizListRequest(expiredToken, map[string]interface{}{ + "limit": 10, + "page": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetQuizList_InputValidation(t *testing.T) { + t.Run("InvalidLimit", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": "not_integer", + "page": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidPage", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": "not_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("ZeroLimit", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 0, + "page": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("ZeroPage", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": 0, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidFrom", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "from": "not_timestamp", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidTo", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "to": "not_timestamp", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidStatus", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "status": "invalid_status", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidDeleted", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "deleted": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidArchived", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "archived": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidSuper", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "super": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidGroupID", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "group_id": "not_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestGetQuizList_Pagination(t *testing.T) { + // Создаем много квизов для тестирования пагинации + for i := 0; i < 15; i++ { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Pagination Test Quiz %d", i), + "status": "draft", + }) + assert.NoError(t, err) + resp.Body.Close() + } + + t.Run("FirstPage", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("SecondPage", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 2, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 5) + }) + + t.Run("EmptyPage", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 100, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.Empty(t, items) + }) +} + +func TestGetQuizList_Filters(t *testing.T) { + // Создаем квизы с разными статусами + statuses := []string{"draft", "start", "stop", "template"} + for _, status := range statuses { + resp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Filter Test Quiz %s", status), + "status": status, + }) + assert.NoError(t, err) + resp.Body.Close() + } + + t.Run("StatusFilter", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "status": "draft", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + + // Проверяем, что все квизы имеют статус "draft" + for _, item := range items { + quiz, ok := item.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, "draft", quiz["status"]) + } + }) + + t.Run("SearchFilter", func(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "search": "Filter Test", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + + // Проверяем, что все квизы содержат "Filter Test" в названии + for _, item := range items { + quiz, ok := item.(map[string]interface{}) + assert.True(t, ok) + name, ok := quiz["name"].(string) + assert.True(t, ok) + assert.Contains(t, name, "Filter Test") + } + }) + + t.Run("TimeRangeFilter", func(t *testing.T) { + now := time.Now().Unix() + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "from": now - 86400, // 24 часа назад + "to": now, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем, что возвращаются квизы + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.GreaterOrEqual(t, len(items), 0) + }) +} + +func TestGetQuizList_DefaultValues(t *testing.T) { + resp, err := getQuizListRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok) + assert.LessOrEqual(t, len(items), 10) // Значение по умолчанию для limit +} + +func TestGetQuizList_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 10, + "page": 1, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(1000)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getQuizListRequest(validToken, map[string]interface{}{ + "limit": 5, + "page": 1, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 19.3.4 19.3.5 19.3.6 19.4 19.5 + +func editQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("PATCH", baseURL+"/quiz/edit", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestEditQuiz_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для редактирования", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Обновляем несколько полей + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": "Обновленное название квиза", + "desc": "Новое описание", + "status": "start", + "limit": 150, + "fp": true, + "rep": false, + "conf": "{}", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, quizID, result["updated"]) +} + +func TestEditQuiz_OneField(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для смены статуса", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Меняем только статус + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "status": "stop", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, quizID, result["updated"]) +} + +func TestEditQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + "name": "Test Quiz", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("PATCH", baseURL+"/quiz/edit", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := editQuizRequest("invalid_token", map[string]interface{}{ + "id": 101, + "name": "Test Quiz", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := editQuizRequest(expiredToken, map[string]interface{}{ + "id": 101, + "name": "Test Quiz", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestEditQuiz_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "name": "Без ID", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + "name": "Невалидный ID", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 99999, + "name": "Несуществующий квиз", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("NameTooLong", func(t *testing.T) { + longName := strings.Repeat("a", 701) + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "name": longName, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("InvalidStatus", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "status": "invalid_status", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("InvalidFP", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "fp": "not_boolean", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("InvalidLimit", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "limit": -5, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("InvalidConf", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "conf": "invalid json", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusBadRequest, 422}, resp.StatusCode) + }) +} + +func TestEditQuiz_Security(t *testing.T) { + t.Run("SQLInjection", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "name": "'; DROP TABLE quizzes; --", + "desc": "'; DELETE FROM users; --", + "conf": "{}", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusOK, http.StatusBadRequest, 422}, resp.StatusCode) + }) + + t.Run("XSS", func(t *testing.T) { + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": 101, + "name": "", + "desc": "", + "conf": "{}", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Contains(t, []int{http.StatusOK, http.StatusBadRequest, 422}, resp.StatusCode) + }) +} + +func TestEditQuiz_Performance(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": "Быстрое обновление", + }) + duration := time.Since(start) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + resp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": fmt.Sprintf("Load Test Quiz %d", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +// todo 20.3.5 20.4 20.5 + +func copyQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/quiz/copy", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCopyQuiz_Success(t *testing.T) { + // Сначала создаем оригинальный квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Оригинальный квиз для копирования", + "description": "Описание оригинала", + "status": "start", + "limit": 50, + "config": "{}", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + origID := createResult["id"] + origName := createResult["name"] + + // Копируем квиз + resp, err := copyQuizRequest(validToken, map[string]interface{}{ + "id": origID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEqual(t, origID, result["id"]) + assert.NotEmpty(t, result["qid"]) + assert.Equal(t, origName, result["name"]) + assert.Equal(t, false, result["deleted"]) + assert.Equal(t, false, result["archived"]) + assert.Equal(t, "draft", result["status"]) + assert.Equal(t, float64(1), result["version"]) + assert.Equal(t, nil, result["version_comment"]) + assert.Equal(t, float64(0), result["session_count"]) + assert.Equal(t, float64(0), result["passed_count"]) + assert.Equal(t, float64(0), result["average_time"]) + assert.NotEmpty(t, result["created_at"]) + assert.NotEmpty(t, result["updated_at"]) +} + +func TestCopyQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/quiz/copy", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := copyQuizRequest("invalid_token", map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := copyQuizRequest(expiredToken, map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCopyQuiz_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := copyQuizRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := copyQuizRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := copyQuizRequest(validToken, map[string]interface{}{ + "id": 99999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCopyQuiz_Performance(t *testing.T) { + // Создаем оригинальный квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для копирования (перфоманс)", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + origID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := copyQuizRequest(validToken, map[string]interface{}{ + "id": origID, + }) + duration := time.Since(start) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 20; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := copyQuizRequest(validToken, map[string]interface{}{ + "id": origID, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 21.3.4 21.3.5 21.4 21.5 + +func getQuizHistoryRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/quiz/history", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetQuizHistory_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для истории", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Редактируем квиз несколько раз для создания истории + for i := 1; i <= 3; i++ { + editResp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": fmt.Sprintf("Обновленный квиз версия %d", i), + "status": "start", + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + // Получаем историю квиза + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 5, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.LessOrEqual(t, len(result), 5) + assert.Greater(t, len(result), 0) + + // Проверяем структуру первой записи истории + if len(result) > 0 { + firstItem := result[0] + assert.Equal(t, quizID, firstItem["id"]) + assert.NotEmpty(t, firstItem["version"]) + assert.NotEmpty(t, firstItem["created_at"]) + assert.NotEmpty(t, firstItem["updated_at"]) + } +} + +func TestGetQuizHistory_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/quiz/history", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getQuizHistoryRequest("invalid_token", map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getQuizHistoryRequest(expiredToken, map[string]interface{}{ + "id": 101, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetQuizHistory_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidLimit", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": "ten", + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidPage", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": 101, + "l": 10, + "p": "one", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": 99999, + "l": 10, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Empty(t, result) + }) +} + +func TestGetQuizHistory_Pagination(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для пагинации истории", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Создаем много версий + for i := 1; i <= 15; i++ { + editResp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": fmt.Sprintf("Версия %d", i), + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + t.Run("FirstPage", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 5, + "p": 1, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.LessOrEqual(t, len(result), 5) + }) + + t.Run("SecondPage", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 5, + "p": 2, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.LessOrEqual(t, len(result), 5) + }) + + t.Run("EmptyPage", func(t *testing.T) { + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 5, + "p": 100, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Empty(t, result) + }) +} + +func TestGetQuizHistory_NewQuiz(t *testing.T) { + // Создаем новый квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Новый квиз для истории", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем историю нового квиза + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Len(t, result, 1) // Должна быть только одна запись для нового квиза + + if len(result) > 0 { + firstItem := result[0] + assert.Equal(t, quizID, firstItem["id"]) + assert.Equal(t, float64(1), firstItem["version"]) + } +} + +func TestGetQuizHistory_Performance(t *testing.T) { + // Создаем квиз с историей + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности истории", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Создаем несколько версий + for i := 1; i <= 5; i++ { + editResp, err := editQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + "name": fmt.Sprintf("Версия %d", i), + }) + assert.NoError(t, err) + editResp.Body.Close() + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 10, + "p": 1, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LoadTest", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getQuizHistoryRequest(validToken, map[string]interface{}{ + "id": quizID, + "l": 5, + "p": 1, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 22.3.4 22.3.5 22.4 22.5 + +func deleteQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("DELETE", baseURL+"/quiz/delete", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestDeleteQuiz_Success(t *testing.T) { + // Сначала создаем квиз для удаления + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для удаления", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Удаляем квиз + resp, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, quizID, result["deactivated"]) +} + +func TestDeleteQuiz_Idempotency(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для идемпотентности", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Первое удаление + resp1, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Повторное удаление того же квиза + resp2, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) + + // Проверяем, что оба ответа содержат правильный ID + var result1, result2 map[string]interface{} + err = json.NewDecoder(resp1.Body).Decode(&result1) + assert.NoError(t, err) + err = json.NewDecoder(resp2.Body).Decode(&result2) + assert.NoError(t, err) + + assert.Equal(t, quizID, result1["deactivated"]) + assert.Equal(t, quizID, result2["deactivated"]) +} + +func TestDeleteQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("DELETE", baseURL+"/quiz/delete", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := deleteQuizRequest("invalid_token", map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := deleteQuizRequest(expiredToken, map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestDeleteQuiz_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := deleteQuizRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": 99999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) // Идемпотентность + }) +} + +func TestDeleteQuiz_Performance(t *testing.T) { + // Создаем несколько квизов для тестирования производительности + var quizIDs []interface{} + for i := 0; i < 10; i++ { + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Квиз для удаления %d", i), + "status": "draft", + }) + assert.NoError(t, err) + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + createResp.Body.Close() + assert.NoError(t, err) + quizIDs = append(quizIDs, createResult["id"]) + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": quizIDs[0], + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("BulkDelete", func(t *testing.T) { + var wg sync.WaitGroup + for i := 1; i < len(quizIDs); i++ { + wg.Add(1) + go func(id interface{}) { + defer wg.Done() + resp, err := deleteQuizRequest(validToken, map[string]interface{}{ + "id": id, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(quizIDs[i]) + } + wg.Wait() + }) +} + +// todo 23.3.4 23.3.5 23.4 23.5 + +func archiveQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("PATCH", baseURL+"/quiz/archive", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestArchiveQuiz_Success(t *testing.T) { + // Сначала создаем квиз для архивации + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для архивации", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Архивируем квиз + resp, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, quizID, result["deactivated"]) +} + +func TestArchiveQuiz_Idempotency(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для архивации (идемпотентность)", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Первая архивация + resp1, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Повторная архивация того же квиза + resp2, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": quizID, + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) + + // Проверяем, что оба ответа содержат правильный ID + var result1, result2 map[string]interface{} + err = json.NewDecoder(resp1.Body).Decode(&result1) + assert.NoError(t, err) + err = json.NewDecoder(resp2.Body).Decode(&result2) + assert.NoError(t, err) + + assert.Equal(t, quizID, result1["deactivated"]) + assert.Equal(t, quizID, result2["deactivated"]) +} + +func TestArchiveQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("PATCH", baseURL+"/quiz/archive", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := archiveQuizRequest("invalid_token", map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := archiveQuizRequest(expiredToken, map[string]interface{}{ + "id": 101, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestArchiveQuiz_InputValidation(t *testing.T) { + t.Run("MissingID", func(t *testing.T) { + resp, err := archiveQuizRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": "not_an_integer", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentID", func(t *testing.T) { + resp, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": 99999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) // Идемпотентность + }) +} + +func TestArchiveQuiz_Performance(t *testing.T) { + // Создаем несколько квизов для тестирования производительности + var quizIDs []interface{} + for i := 0; i < 10; i++ { + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": fmt.Sprintf("Квиз для архивации %d", i), + "status": "draft", + }) + assert.NoError(t, err) + + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + createResp.Body.Close() + assert.NoError(t, err) + quizIDs = append(quizIDs, createResult["id"]) + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": quizIDs[0], + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("BulkArchive", func(t *testing.T) { + var wg sync.WaitGroup + for i := 1; i < len(quizIDs); i++ { + wg.Add(1) + go func(id interface{}) { + defer wg.Done() + resp, err := archiveQuizRequest(validToken, map[string]interface{}{ + "id": id, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(quizIDs[i]) + } + wg.Wait() + }) +} + +// todo 24.3.4 24.3.5 24.4 24.5 + +func moveQuizRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/quiz/move", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestMoveQuiz_Success(t *testing.T) { + // Сначала создаем квиз для переноса + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для переноса", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizQID := createResult["qid"] + + // Переносим квиз + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "qid": quizQID, + "accountID": "new-account-id", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) +} + +func TestMoveQuiz_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "qid": "test-qid", + "accountID": "new-account", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/quiz/move", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := moveQuizRequest("invalid_token", map[string]interface{}{ + "qid": "test-qid", + "accountID": "new-account", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestMoveQuiz_InputValidation(t *testing.T) { + t.Run("MissingQID", func(t *testing.T) { + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "accountID": "new-account", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("MissingAccountID", func(t *testing.T) { + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "qid": "test-qid", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQID", func(t *testing.T) { + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "qid": "non-existent-qid", + "accountID": "new-account", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentAccountID", func(t *testing.T) { + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "qid": "test-qid", + "accountID": "non-existent-account", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestMoveQuiz_Performance(t *testing.T) { + // Создаем квиз для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности переноса", + "status": "draft", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizQID := createResult["qid"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := moveQuizRequest(validToken, map[string]interface{}{ + "qid": quizQID, + "accountID": "performance-test-account", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) +} + +// todo 25.3.4 25.4 25.5 + +func createQuizTemplateRequest(token string) (*http.Response, error) { + req, err := http.NewRequest("POST", baseURL+"/quiz/template", nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCreateQuizTemplate_Success(t *testing.T) { + resp, err := createQuizTemplateRequest(validToken) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result["id"]) + assert.IsType(t, float64(0), result["id"]) // ID должен быть числом +} + +func TestCreateQuizTemplate_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("POST", baseURL+"/quiz/template", nil) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := createQuizTemplateRequest("invalid_token") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCreateQuizTemplate_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := createQuizTemplateRequest(validToken) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("MultipleTemplates", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := createQuizTemplateRequest(validToken) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 26.3.3 26.4 26.5 + +func getResultsRequest(token string, quizId string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/results/getResults/"+quizId, bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetResults_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты квиза + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotNil(t, result["total_count"]) + assert.NotNil(t, result["results"]) + assert.IsType(t, []interface{}{}, result["results"]) +} + +func TestGetResults_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/results/getResults/12345", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getResultsRequest("invalid_token", "12345", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetResults_InputValidation(t *testing.T) { + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getResultsRequest(validToken, "invalid-quiz-id", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := getResultsRequest(validToken, "99999", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.Equal(t, float64(0), result["total_count"]) + assert.Empty(t, result["results"]) + }) +} + +func TestGetResults_Pagination(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для пагинации результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("FirstPage", func(t *testing.T) { + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 5, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + results := result["results"].([]interface{}) + assert.LessOrEqual(t, len(results), 5) + }) + + t.Run("SecondPage", func(t *testing.T) { + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 2, + "Limit": 5, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("EmptyPage", func(t *testing.T) { + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 100, + "Limit": 5, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + results := result["results"].([]interface{}) + assert.Empty(t, results) + }) +} + +func TestGetResults_Filtering(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для фильтрации результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("WithDateRange", func(t *testing.T) { + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "2023-01-01", + "To": "2023-12-31", + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("NewResultsOnly", func(t *testing.T) { + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "New": true, + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +func TestGetResults_Performance(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 5, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 27.3.4 27.4 27.5 + +func deleteResultRequest(token string, resultId string) (*http.Response, error) { + req, err := http.NewRequest("DELETE", baseURL+"/results/"+resultId, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestDeleteResult_Success(t *testing.T) { + // Сначала создаем квиз и получаем результаты + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для удаления результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты квиза + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + // Если есть результаты, удаляем первый + if len(results) > 0 { + firstResult := results[0].(map[string]interface{}) + resultID := fmt.Sprintf("%v", firstResult["id"]) + + // Удаляем результат + resp, err := deleteResultRequest(validToken, resultID) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + } +} + +func TestDeleteResult_Idempotency(t *testing.T) { + // Создаем квиз и получаем результат для тестирования идемпотентности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для идемпотентности удаления результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 5, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + if len(results) > 0 { + firstResult := results[0].(map[string]interface{}) + resultID := fmt.Sprintf("%v", firstResult["id"]) + + // Первое удаление + resp1, err := deleteResultRequest(validToken, resultID) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Повторное удаление того же результата + resp2, err := deleteResultRequest(validToken, resultID) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) + } +} + +func TestDeleteResult_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("DELETE", baseURL+"/results/123456", nil) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := deleteResultRequest("invalid_token", "123456") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestDeleteResult_InputValidation(t *testing.T) { + t.Run("InvalidResultID", func(t *testing.T) { + resp, err := deleteResultRequest(validToken, "not_a_number") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentResultID", func(t *testing.T) { + resp, err := deleteResultRequest(validToken, "99999999") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) // Идемпотентность + }) +} + +func TestDeleteResult_Performance(t *testing.T) { + // Создаем квиз и получаем результаты для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности удаления результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + if len(results) > 0 { + firstResult := results[0].(map[string]interface{}) + resultID := fmt.Sprintf("%v", firstResult["id"]) + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := deleteResultRequest(validToken, resultID) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + } + + t.Run("BulkDelete", func(t *testing.T) { + // Получаем больше результатов для массового удаления + getMoreResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 20, + }) + assert.NoError(t, err) + defer getMoreResultsResp.Body.Close() + + var moreResultsData map[string]interface{} + err = json.NewDecoder(getMoreResultsResp.Body).Decode(&moreResultsData) + assert.NoError(t, err) + moreResults := moreResultsData["results"].([]interface{}) + + var wg sync.WaitGroup + for i := 0; i < len(moreResults) && i < 5; i++ { + wg.Add(1) + go func(result interface{}) { + defer wg.Done() + resultMap := result.(map[string]interface{}) + resultID := fmt.Sprintf("%v", resultMap["id"]) + resp, err := deleteResultRequest(validToken, resultID) + if err == nil && resp != nil { + resp.Body.Close() + } + }(moreResults[i]) + } + wg.Wait() + }) +} + +// todo 28.3.4 28.3.5 28.3.6 28.4 28.5 + +func updateResultsStatusRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("PATCH", baseURL+"/result/seen", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestUpdateResultsStatus_Success(t *testing.T) { + // Сначала создаем квиз и получаем результаты + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для обновления статуса результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты квиза + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + // Если есть результаты, обновляем статус первых трех + if len(results) >= 3 { + var answerIDs []int64 + for i := 0; i < 3; i++ { + result := results[i].(map[string]interface{}) + answerIDs = append(answerIDs, int64(result["id"].(float64))) + } + + // Обновляем статус результатов + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": answerIDs, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + } +} + +func TestUpdateResultsStatus_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "Answers": []int64{101, 102, 103}, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("PATCH", baseURL+"/result/seen", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := updateResultsStatusRequest("invalid_token", map[string]interface{}{ + "Answers": []int64{101, 102, 103}, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestUpdateResultsStatus_InputValidation(t *testing.T) { + t.Run("MissingAnswers", func(t *testing.T) { + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyAnswers", func(t *testing.T) { + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": []int64{}, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidAnswersType", func(t *testing.T) { + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": "not_an_array", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentAnswers", func(t *testing.T) { + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": []int64{999999, 999998, 999997}, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) // Идемпотентность + }) +} + +func TestUpdateResultsStatus_Idempotency(t *testing.T) { + // Создаем квиз и получаем результаты + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для идемпотентности обновления статуса", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 5, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + if len(results) >= 2 { + var answerIDs []int64 + for i := 0; i < 2; i++ { + result := results[i].(map[string]interface{}) + answerIDs = append(answerIDs, int64(result["id"].(float64))) + } + + // Первое обновление + resp1, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": answerIDs, + }) + assert.NoError(t, err) + defer resp1.Body.Close() + assert.Equal(t, http.StatusOK, resp1.StatusCode) + + // Повторное обновление тех же результатов + resp2, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": answerIDs, + }) + assert.NoError(t, err) + defer resp2.Body.Close() + assert.Equal(t, http.StatusOK, resp2.StatusCode) + } +} + +func TestUpdateResultsStatus_Performance(t *testing.T) { + // Создаем квиз и получаем результаты для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности обновления статуса", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 20, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + if len(results) > 0 { + var answerIDs []int64 + for i := 0; i < len(results) && i < 10; i++ { + result := results[i].(map[string]interface{}) + answerIDs = append(answerIDs, int64(result["id"].(float64))) + } + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": answerIDs, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("LargeBatch", func(t *testing.T) { + // Тестируем обновление большого количества результатов + largeBatch := make([]int64, 100) + for i := 0; i < 100; i++ { + largeBatch[i] = int64(1000000 + i) // Используем несуществующие ID + } + + resp, err := updateResultsStatusRequest(validToken, map[string]interface{}{ + "Answers": largeBatch, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} + +// todo 29.3.2 29.3.3 29.4 29.5 + +func exportResultsRequest(token string, quizID string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/results/"+quizID+"/export", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestExportResults_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для экспорта результатов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Экспортируем результаты + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "2023-01-01T00:00:00Z", + "To": "2023-12-31T23:59:59Z", + "New": false, + "Page": 1, + "Limit": 100, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", resp.Header.Get("Content-Type")) + + // Проверяем, что ответ содержит данные + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NotEmpty(t, body) +} + +func TestExportResults_WithFilters(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для фильтрации экспорта", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("NewResultsOnly", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "New": true, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", resp.Header.Get("Content-Type")) + }) + + t.Run("WithDateRange", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "2023-02-01T00:00:00Z", + "To": "2023-02-28T23:59:59Z", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", resp.Header.Get("Content-Type")) + }) + + t.Run("WithPagination", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 2, + "Limit": 50, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", resp.Header.Get("Content-Type")) + }) +} + +func TestExportResults_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/results/123/export", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := exportResultsRequest("invalid_token", "123", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestExportResults_InputValidation(t *testing.T) { + // Создаем квиз для тестирования + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для валидации экспорта", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("InvalidDateFormat", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "not-a-date", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, "invalid-quiz-id", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, "99999", map[string]interface{}{ + "Page": 1, + "Limit": 10, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) // Возвращает пустой файл + }) +} + +func TestExportResults_Performance(t *testing.T) { + // Создаем квиз для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности экспорта", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 100, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(2000)) // Экспорт может занимать больше времени + }) + + t.Run("LargeExport", func(t *testing.T) { + resp, err := exportResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 1000, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +// todo 30.3.6 30.3.7 30.4 30.5 + +func getResultRequest(token string, resultID string) (*http.Response, error) { + req, err := http.NewRequest("GET", baseURL+"/result/"+resultID, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetResult_Success(t *testing.T) { + // Сначала создаем квиз и получаем результаты + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для получения результата", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты квиза + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 5, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + // Если есть результаты, получаем детали первого + if len(results) > 0 { + firstResult := results[0].(map[string]interface{}) + resultID := fmt.Sprintf("%v", firstResult["id"]) + + // Получаем детали результата + resp, err := getResultRequest(validToken, resultID) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var answers []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&answers) + assert.NoError(t, err) + + // Проверяем структуру ответа + if len(answers) > 0 { + answer := answers[0] + assert.NotEmpty(t, answer["Id"]) + assert.NotEmpty(t, answer["content"]) + assert.NotEmpty(t, answer["question_id"]) + assert.NotEmpty(t, answer["QuizId"]) + assert.NotEmpty(t, answer["CreatedAt"]) + assert.IsType(t, false, answer["Result"]) + assert.IsType(t, false, answer["new"]) + assert.IsType(t, false, answer["Deleted"]) + assert.IsType(t, false, answer["Start"]) + } + } +} + +func TestGetResult_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("GET", baseURL+"/result/abc123xyz", nil) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getResultRequest("invalid_token", "abc123xyz") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetResult_InputValidation(t *testing.T) { + t.Run("EmptyResultID", func(t *testing.T) { + resp, err := getResultRequest(validToken, "") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentResultID", func(t *testing.T) { + resp, err := getResultRequest(validToken, "nonexistent123") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var answers []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&answers) + assert.NoError(t, err) + assert.Empty(t, answers) + }) + + t.Run("InvalidResultID", func(t *testing.T) { + resp, err := getResultRequest(validToken, "invalid-result-id!") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestGetResult_Performance(t *testing.T) { + // Создаем квиз и получаем результат для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности результата", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем результаты + getResultsResp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "Page": 1, + "Limit": 1, + }) + assert.NoError(t, err) + defer getResultsResp.Body.Close() + + var resultsData map[string]interface{} + err = json.NewDecoder(getResultsResp.Body).Decode(&resultsData) + assert.NoError(t, err) + results := resultsData["results"].([]interface{}) + + if len(results) > 0 { + firstResult := results[0].(map[string]interface{}) + resultID := fmt.Sprintf("%v", firstResult["id"]) + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getResultRequest(validToken, resultID) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + } +} + +// todo 31.3.3 31.4 31.5 + +func getDeviceStatsRequest(token string, quizID string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/statistic/"+quizID+"/devices", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetDeviceStats_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для статистики устройств", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем статистику по устройствам + resp, err := getDeviceStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем структуру ответа + assert.NotNil(t, result["Device"]) + assert.NotNil(t, result["OS"]) + assert.NotNil(t, result["Browser"]) + + // Проверяем, что это словари с процентным распределением + deviceStats := result["Device"].(map[string]interface{}) + osStats := result["OS"].(map[string]interface{}) + browserStats := result["Browser"].(map[string]interface{}) + + // Проверяем, что значения являются числами (процентами) + for _, value := range deviceStats { + assert.IsType(t, float64(0), value) + } + for _, value := range osStats { + assert.IsType(t, float64(0), value) + } + for _, value := range browserStats { + assert.IsType(t, float64(0), value) + } +} + +func TestGetDeviceStats_WithoutDateRange(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для статистики без диапазона дат", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем статистику без фильтрации по датам + resp, err := getDeviceStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotNil(t, result["Device"]) + assert.NotNil(t, result["OS"]) + assert.NotNil(t, result["Browser"]) +} + +func TestGetDeviceStats_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/statistic/12345/devices", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getDeviceStatsRequest("invalid_token", "12345", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetDeviceStats_InputValidation(t *testing.T) { + // Создаем квиз для тестирования + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для валидации статистики устройств", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("InvalidDateFormat", func(t *testing.T) { + resp, err := getDeviceStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "not_a_timestamp", + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getDeviceStatsRequest(validToken, "invalid-quiz-id", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := getDeviceStatsRequest(validToken, "99999", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + // Проверяем, что возвращаются пустые статистики + deviceStats := result["Device"].(map[string]interface{}) + osStats := result["OS"].(map[string]interface{}) + browserStats := result["Browser"].(map[string]interface{}) + assert.Empty(t, deviceStats) + assert.Empty(t, osStats) + assert.Empty(t, browserStats) + }) +} + +func TestGetDeviceStats_Performance(t *testing.T) { + // Создаем квиз для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности статистики устройств", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getDeviceStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getDeviceStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 32.3.2 32.3.3 32.4 32.5 + +func getGeneralStatsRequest(token string, quizID string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/statistic/"+quizID+"/general", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetGeneralStats_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для общей статистики", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем общую статистику + resp, err := getGeneralStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем структуру ответа + assert.NotNil(t, result["Open"]) + assert.NotNil(t, result["Result"]) + assert.NotNil(t, result["AvTime"]) + assert.NotNil(t, result["Conversion"]) + + // Проверяем, что это объекты с временными метками + openStats := result["Open"].(map[string]interface{}) + resultStats := result["Result"].(map[string]interface{}) + avTimeStats := result["AvTime"].(map[string]interface{}) + conversionStats := result["Conversion"].(map[string]interface{}) + + // Проверяем типы значений + for _, value := range openStats { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + } + for _, value := range resultStats { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + } + for _, value := range avTimeStats { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + } + for _, value := range conversionStats { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + assert.LessOrEqual(t, value.(float64), float64(100)) + } +} + +func TestGetGeneralStats_WithoutDateRange(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для общей статистики без диапазона", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем общую статистику без фильтрации по датам + resp, err := getGeneralStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotNil(t, result["Open"]) + assert.NotNil(t, result["Result"]) + assert.NotNil(t, result["AvTime"]) + assert.NotNil(t, result["Conversion"]) +} + +func TestGetGeneralStats_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/statistic/12345/general", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getGeneralStatsRequest("invalid_token", "12345", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetGeneralStats_InputValidation(t *testing.T) { + // Создаем квиз для тестирования + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для валидации общей статистики", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("InvalidDateFormat", func(t *testing.T) { + resp, err := getGeneralStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "not_a_timestamp", + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getGeneralStatsRequest(validToken, "invalid-quiz-id", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := getGeneralStatsRequest(validToken, "99999", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + // Проверяем, что возвращаются пустые статистики + openStats := result["Open"].(map[string]interface{}) + resultStats := result["Result"].(map[string]interface{}) + avTimeStats := result["AvTime"].(map[string]interface{}) + conversionStats := result["Conversion"].(map[string]interface{}) + assert.Empty(t, openStats) + assert.Empty(t, resultStats) + assert.Empty(t, avTimeStats) + assert.Empty(t, conversionStats) + }) +} + +func TestGetGeneralStats_Performance(t *testing.T) { + // Создаем квиз для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности общей статистики", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getGeneralStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getGeneralStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 33.3.2 33.3.3 33.4 33.5 + +func getQuestionStatsRequest(token string, quizID string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/statistic/"+quizID+"/questions", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetQuestionStats_Success(t *testing.T) { + // Сначала создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для статистики вопросов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем статистику по вопросам + resp, err := getQuestionStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем, что это массив объектов QuestionStat + assert.NotEmpty(t, result) + + if len(result) > 0 { + questionStat := result[0] + + // Проверяем структуру ответа + assert.NotNil(t, questionStat["Funnel"]) + assert.NotNil(t, questionStat["FunnelData"]) + assert.NotNil(t, questionStat["Results"]) + assert.NotNil(t, questionStat["Questions"]) + + // Проверяем типы данных + funnel := questionStat["Funnel"].([]interface{}) + funnelData := questionStat["FunnelData"].([]interface{}) + results := questionStat["Results"].(map[string]interface{}) + questions := questionStat["Questions"].(map[string]interface{}) + + // Проверяем ограничения на размеры массивов + assert.LessOrEqual(t, len(funnel), 3) + assert.LessOrEqual(t, len(funnelData), 4) + + // Проверяем типы значений в Funnel + for _, value := range funnel { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + assert.LessOrEqual(t, value.(float64), float64(100)) + } + + // Проверяем типы значений в FunnelData + for _, value := range funnelData { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + } + + // Проверяем типы значений в Results + for _, value := range results { + assert.IsType(t, float64(0), value) + assert.GreaterOrEqual(t, value.(float64), float64(0)) + assert.LessOrEqual(t, value.(float64), float64(100)) + } + + // Проверяем типы значений в Questions + for _, questionData := range questions { + questionMap := questionData.(map[string]interface{}) + for _, percentage := range questionMap { + assert.IsType(t, float64(0), percentage) + assert.GreaterOrEqual(t, percentage.(float64), float64(0)) + assert.LessOrEqual(t, percentage.(float64), float64(100)) + } + } + } +} + +func TestGetQuestionStats_WithoutDateRange(t *testing.T) { + // Создаем квиз + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для статистики вопросов без диапазона", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + // Получаем статистику по вопросам без фильтрации по датам + resp, err := getQuestionStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{}) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result) +} + +func TestGetQuestionStats_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/statistic/12345/questions", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getQuestionStatsRequest("invalid_token", "12345", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetQuestionStats_InputValidation(t *testing.T) { + // Создаем квиз для тестирования + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для валидации статистики вопросов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("InvalidDateFormat", func(t *testing.T) { + resp, err := getQuestionStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": "not_a_timestamp", + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidQuizID", func(t *testing.T) { + resp, err := getQuestionStatsRequest(validToken, "invalid-quiz-id", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("NonExistentQuizID", func(t *testing.T) { + resp, err := getQuestionStatsRequest(validToken, "99999", map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var result []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + assert.NotEmpty(t, result) + + // Проверяем, что возвращаются пустые статистики + if len(result) > 0 { + questionStat := result[0] + funnel := questionStat["Funnel"].([]interface{}) + funnelData := questionStat["FunnelData"].([]interface{}) + results := questionStat["Results"].(map[string]interface{}) + questions := questionStat["Questions"].(map[string]interface{}) + assert.Empty(t, funnel) + assert.Empty(t, funnelData) + assert.Empty(t, results) + assert.Empty(t, questions) + } + }) +} + +func TestGetQuestionStats_Performance(t *testing.T) { + // Создаем квиз для тестирования производительности + createResp, err := createQuizRequest(validToken, map[string]interface{}{ + "name": "Квиз для теста производительности статистики вопросов", + "status": "start", + }) + assert.NoError(t, err) + defer createResp.Body.Close() + var createResult map[string]interface{} + err = json.NewDecoder(createResp.Body).Decode(&createResult) + assert.NoError(t, err) + quizID := createResult["id"] + + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getQuestionStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getQuestionStatsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + "From": 1700000000, + "To": 1709999999, + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 34.4.2 34.4.3 34.4.4 34.5 + +// todo ПРОПУСК 35 36 тест кесов + +func getTelegramPoolRequest(token string) (*http.Response, error) { + req, err := http.NewRequest("GET", baseURL+"/telegram/pool", nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestGetTelegramPool_Success(t *testing.T) { + resp, err := getTelegramPoolRequest(validToken) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var accounts []map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&accounts) + assert.NoError(t, err) + + // Проверяем структуру каждого аккаунта + for _, account := range accounts { + // Проверяем обязательные поля + assert.NotEmpty(t, account["ID"]) + assert.NotEmpty(t, account["ApiID"]) + assert.NotEmpty(t, account["ApiHash"]) + assert.NotEmpty(t, account["PhoneNumber"]) + assert.NotEmpty(t, account["Status"]) + assert.NotEmpty(t, account["CreatedAt"]) + + // Проверяем типы данных + assert.IsType(t, float64(0), account["ID"]) + assert.IsType(t, float64(0), account["ApiID"]) + assert.IsType(t, "", account["ApiHash"]) + assert.IsType(t, "", account["PhoneNumber"]) + assert.IsType(t, "", account["Status"]) + assert.IsType(t, "", account["CreatedAt"]) + assert.IsType(t, false, account["Deleted"]) + + // Проверяем, что аккаунт не удален + assert.Equal(t, false, account["Deleted"]) + + // Проверяем, что статус соответствует допустимым значениям + status := account["Status"].(string) + assert.Contains(t, []string{"active", "inactive", "ban"}, status) + + // Проверяем формат даты (ISO 8601) + createdAt := account["CreatedAt"].(string) + _, err := time.Parse(time.RFC3339, createdAt) + assert.NoError(t, err) + } +} + +func TestGetTelegramPool_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("GET", baseURL+"/telegram/pool", nil) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := getTelegramPoolRequest("invalid_token") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := getTelegramPoolRequest(expiredToken) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetTelegramPool_Security(t *testing.T) { + t.Run("SQLInjectionAttempt", func(t *testing.T) { + resp, err := getTelegramPoolRequest("' OR 1=1 --") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("XSSAttempt", func(t *testing.T) { + resp, err := getTelegramPoolRequest("") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestGetTelegramPool_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := getTelegramPoolRequest(validToken) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(1000)) // Менее 1 секунды + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + resp, err := getTelegramPoolRequest(validToken) + if err == nil && resp != nil { + resp.Body.Close() + } + }() + } + wg.Wait() + }) +} + +// todo 37.3.3 37.3.4 37.3.5 37.4 37.5 + +func createTelegramRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/telegram/create", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestCreateTelegram_Success(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем, что возвращается signature + assert.NotEmpty(t, result["signature"]) + assert.IsType(t, "", result["signature"]) +} + +func TestCreateTelegram_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/telegram/create", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := createTelegramRequest("invalid_token", map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestCreateTelegram_InputValidation(t *testing.T) { + t.Run("MissingApiID", func(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("MissingApiHash", func(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": 123456, + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("MissingPhoneNumber", func(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidApiID", func(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": "not_a_number", + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidPhoneNumber", func(t *testing.T) { + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "invalid_phone", + "password": "secure_password", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestCreateTelegram_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := createTelegramRequest(validToken, map[string]interface{}{ + "api_id": 123456, + "api_hash": "abcdef1234567890abcdef1234567890", + "phone_number": "+12345678901", + "password": "secure_password", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) +} + +// todo 38.3.2 38.3.3 38.4 38.5 + +func deleteTelegramRequest(token string, id string) (*http.Response, error) { + req, err := http.NewRequest("DELETE", baseURL+"/telegram/"+id, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestDeleteTelegram_Success(t *testing.T) { + // Сначала получаем список аккаунтов, чтобы найти существующий ID + poolResp, err := getTelegramPoolRequest(validToken) + assert.NoError(t, err) + defer poolResp.Body.Close() + + var accounts []map[string]interface{} + err = json.NewDecoder(poolResp.Body).Decode(&accounts) + assert.NoError(t, err) + + // Если есть аккаунты, удаляем первый + if len(accounts) > 0 { + firstAccount := accounts[0] + accountID := fmt.Sprintf("%v", firstAccount["ID"]) + + // Удаляем аккаунт + resp, err := deleteTelegramRequest(validToken, accountID) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + } +} + +func TestDeleteTelegram_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + req, err := http.NewRequest("DELETE", baseURL+"/telegram/123", nil) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := deleteTelegramRequest("invalid_token", "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := deleteTelegramRequest(expiredToken, "123") + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestDeleteTelegram_InputValidation(t *testing.T) { + t.Run("NonExistentID", func(t *testing.T) { + resp, err := deleteTelegramRequest(validToken, "99999") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidID", func(t *testing.T) { + resp, err := deleteTelegramRequest(validToken, "!@#") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("EmptyID", func(t *testing.T) { + resp, err := deleteTelegramRequest(validToken, "") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestDeleteTelegram_Security(t *testing.T) { + t.Run("SQLInjectionAttempt", func(t *testing.T) { + resp, err := deleteTelegramRequest(validToken, "' OR 1=1 --") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSAttempt", func(t *testing.T) { + resp, err := deleteTelegramRequest(validToken, "") + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestDeleteTelegram_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := deleteTelegramRequest(validToken, "123") + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) +} + +// todo 39.3.4 39.3.5 39.4 39.5 + +func setTelegramCodeRequest(token string, body map[string]interface{}) (*http.Response, error) { + payload, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", baseURL+"/telegram/setCode", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +func TestSetTelegramCode_Success(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "123456", + "signature": "abc123signature", + }) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + var result map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&result) + assert.NoError(t, err) + + // Проверяем, что возвращается ID + assert.NotEmpty(t, result["id"]) + assert.IsType(t, float64(0), result["id"]) +} + +func TestSetTelegramCode_Auth(t *testing.T) { + t.Run("NoToken", func(t *testing.T) { + payload, err := json.Marshal(map[string]interface{}{ + "code": "123456", + "signature": "abc123signature", + }) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", baseURL+"/telegram/setCode", bytes.NewReader(payload)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InvalidToken", func(t *testing.T) { + resp, err := setTelegramCodeRequest("invalid_token", map[string]interface{}{ + "code": "123456", + "signature": "abc123signature", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("ExpiredToken", func(t *testing.T) { + resp, err := setTelegramCodeRequest(expiredToken, map[string]interface{}{ + "code": "123456", + "signature": "abc123signature", + }) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +func TestSetTelegramCode_InputValidation(t *testing.T) { + t.Run("MissingCode", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "signature": "abc123signature", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("MissingSignature", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "123456", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidCodeType", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": 123456, + "signature": "abc123signature", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InvalidSignatureType", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "123456", + "signature": 12345, + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestSetTelegramCode_Security(t *testing.T) { + t.Run("XSSInCode", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "", + "signature": "abc123signature", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("SQLInjectionInCode", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "' OR 1=1 --", + "signature": "abc123signature", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("XSSInSignature", func(t *testing.T) { + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "123456", + "signature": "", + }) + assert.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +func TestSetTelegramCode_Performance(t *testing.T) { + t.Run("ResponseTime", func(t *testing.T) { + start := time.Now() + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": "123456", + "signature": "abc123signature", + }) + duration := time.Since(start) + + assert.NoError(t, err) + defer resp.Body.Close() + assert.Less(t, duration.Milliseconds(), int64(500)) + }) + + t.Run("ConcurrentRequests", func(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + resp, err := setTelegramCodeRequest(validToken, map[string]interface{}{ + "code": fmt.Sprintf("12345%d", index), + "signature": fmt.Sprintf("signature%d", index), + }) + if err == nil && resp != nil { + resp.Body.Close() + } + }(i) + } + wg.Wait() + }) +} + +// todo 40.4.4 40.4.5 40.4.6 40.5 40.6