diff --git a/go.mod b/go.mod index 25e4793..edbd546 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module courses-microservice go 1.24.1 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/Masterminds/squirrel v1.5.4 github.com/gin-contrib/cors v1.7.5 github.com/gin-gonic/gin v1.10.0 diff --git a/go.sum b/go.sum index a3dbe28..1905edf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= @@ -96,6 +98,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= diff --git a/internal/handlers/assignments/assignments_test.go b/internal/handlers/assignments/assignments_test.go index 0c5b30f..8b1de0e 100644 --- a/internal/handlers/assignments/assignments_test.go +++ b/internal/handlers/assignments/assignments_test.go @@ -318,7 +318,7 @@ func TestAddAttachment_Success(t *testing.T) { _, err = io.Copy(fileWriter, strings.NewReader("test file content")) assert.NoError(t, err) - writer.Close() + _ = writer.Close() router := createRouterWithJWT(presetUserID.String()) diff --git a/internal/handlers/courses/courses_test.go b/internal/handlers/courses/courses_test.go index d539a5f..0df9a6f 100644 --- a/internal/handlers/courses/courses_test.go +++ b/internal/handlers/courses/courses_test.go @@ -363,7 +363,7 @@ func TestAddAttachment_Success(t *testing.T) { _, err = io.Copy(fileWriter, strings.NewReader("test file content")) assert.NoError(t, err) - writer.Close() + _ = writer.Close() router := createRouterWithJWT(presetUserID.String()) diff --git a/internal/handlers/modules/modules_test.go b/internal/handlers/modules/modules_test.go index c70eb47..8730721 100644 --- a/internal/handlers/modules/modules_test.go +++ b/internal/handlers/modules/modules_test.go @@ -305,7 +305,7 @@ func TestAddAttachment_Success(t *testing.T) { _, err = io.Copy(fileWriter, strings.NewReader("test file content")) assert.NoError(t, err) - writer.Close() + _ = writer.Close() router := createRouterWithJWT(presetUserID.String()) diff --git a/internal/middleware/jwt_auth_test.go b/internal/middleware/jwt_auth_test.go index ff7795c..9925d5f 100644 --- a/internal/middleware/jwt_auth_test.go +++ b/internal/middleware/jwt_auth_test.go @@ -2,6 +2,7 @@ package middleware_test import ( "courses-microservice/config" + "courses-microservice/internal/errors" "courses-microservice/internal/middleware" "net/http" "net/http/httptest" @@ -104,3 +105,98 @@ func TestJWTAuthMiddleware_WithErrorHandler(t *testing.T) { assert.Contains(t, w.Body.String(), "Invalid JWT token") }) } + +func TestErrorHandlerMiddleware_NoErrors(t *testing.T) { + router := gin.New() + router.Use(middleware.ErrorHandlerMiddleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + resp := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + router.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "ok", resp.Body.String()) +} + +func TestErrorHandlerMiddleware_ErrorAlreadyWritten(t *testing.T) { + router := gin.New() + router.Use(middleware.ErrorHandlerMiddleware()) + router.GET("/test", func(c *gin.Context) { + c.String(http.StatusBadRequest, "bad") + _ = c.Error(errors.NewBadRequestError("won't be handled")) + }) + + resp := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + router.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, "bad", resp.Body.String()) +} + +func TestErrorHandlerMiddleware_KnownErrors(t *testing.T) { + tests := []struct { + name string + err error + statusCode int + message string + }{ + { + name: "BadRequestError", + err: errors.NewBadRequestError("bad request"), + statusCode: http.StatusBadRequest, + message: "bad request", + }, + { + name: "UnauthorizedError", + err: errors.NewUnauthorizedError("unauthorized"), + statusCode: http.StatusUnauthorized, + message: "unauthorized", + }, + { + name: "ForbiddenError", + err: errors.NewForbiddenError("forbidden"), + statusCode: http.StatusForbidden, + message: "forbidden", + }, + { + name: "NotFoundError", + err: errors.NewNotFoundError("not found"), + statusCode: http.StatusNotFound, + message: "not found", + }, + { + name: "ConflictError", + err: errors.NewConflictError("conflict"), + statusCode: http.StatusConflict, + message: "conflict", + }, + { + name: "InternalServerError", + err: errors.NewInternalServerError("internal issue"), + statusCode: http.StatusInternalServerError, + message: "internal issue", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router := gin.New() + router.Use(middleware.ErrorHandlerMiddleware()) + router.GET("/test", func(c *gin.Context) { + _ = c.Error(tt.err) + }) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + resp := httptest.NewRecorder() + + router.ServeHTTP(resp, req) + + assert.Equal(t, tt.statusCode, resp.Code) + assert.Contains(t, resp.Body.String(), tt.message) + }) + } +} diff --git a/internal/repository/assignments_test.go b/internal/repository/assignments_test.go index 23ccd8d..c6761ad 100644 --- a/internal/repository/assignments_test.go +++ b/internal/repository/assignments_test.go @@ -231,6 +231,133 @@ func TestGetAssignmentByID_NotFound(t *testing.T) { assert.NoError(t, mock.ExpectationsWereMet()) } +func TestCreateOrUpdateAssignmentSubmission_Success(t *testing.T) { + mock, repo := setupMockRepoForAssignments(t) + + assignmentID := uuid.New() + studentID := uuid.New() + submission := &models.AssignmentSubmission{ + Text: "test submission", + Attachment: "file.png", + Comment: "Looks good", + Score: 90, + Reviewed: true, + IgnoreDeadline: false, + } + + // Mock expected query and returned row + mock.ExpectQuery("INSERT INTO assignment_submissions"). + WithArgs( + assignmentID, + studentID, + submission.Text, + submission.Attachment, + submission.Comment, + submission.Score, + submission.Reviewed, + submission.IgnoreDeadline, + ). + WillReturnRows(pgxmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow(uuid.New(), time.Now(), time.Now())) + + err := repo.CreateOrUpdateAssignmentSubmission(assignmentID, studentID, submission) + assert.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetAssignmentSubmissionByID_Success(t *testing.T) { + mock, repo := setupMockRepoForAssignments(t) + + submissionID := uuid.New() + expected := &models.AssignmentSubmission{ + Id: submissionID, + AssignmentId: uuid.New(), + StudentId: uuid.New(), + Text: "Great work!", + Attachment: "submission.pdf", + Comment: "Reviewed", + Score: 95, + Reviewed: true, + IgnoreDeadline: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + mock.ExpectQuery("SELECT id, assignment_id, student_id, text, attachment, comment"). + WithArgs(submissionID). + WillReturnRows( + pgxmock.NewRows([]string{ + "id", "assignment_id", "student_id", "text", "attachment", "comment", "score", + "reviewed", "ignore_deadline", "created_at", "updated_at", + }).AddRow( + expected.Id, + expected.AssignmentId, + expected.StudentId, + expected.Text, + expected.Attachment, + expected.Comment, + expected.Score, + expected.Reviewed, + expected.IgnoreDeadline, + expected.CreatedAt, + expected.UpdatedAt, + ), + ) + + result, err := repo.GetAssignmentSubmissionByID(submissionID) + assert.NoError(t, err) + assert.Equal(t, expected.Id, result.Id) + assert.Equal(t, expected.Text, result.Text) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetAssignmentSubmissionByStudent_Success(t *testing.T) { + mock, repo := setupMockRepoForAssignments(t) + + assignmentID := uuid.New() + studentID := uuid.New() + expected := &models.AssignmentSubmission{ + Id: uuid.New(), + AssignmentId: assignmentID, + StudentId: studentID, + Text: "My submission", + Attachment: "homework.docx", + Comment: "Looks good", + Score: 88, + Reviewed: true, + IgnoreDeadline: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + mock.ExpectQuery("SELECT id, assignment_id, student_id, text, attachment, comment"). + WithArgs(assignmentID, studentID). + WillReturnRows( + pgxmock.NewRows([]string{ + "id", "assignment_id", "student_id", "text", "attachment", "comment", "score", + "reviewed", "ignore_deadline", "created_at", "updated_at", + }).AddRow( + expected.Id, + expected.AssignmentId, + expected.StudentId, + expected.Text, + expected.Attachment, + expected.Comment, + expected.Score, + expected.Reviewed, + expected.IgnoreDeadline, + expected.CreatedAt, + expected.UpdatedAt, + ), + ) + + result, err := repo.GetAssignmentSubmissionByStudent(assignmentID, studentID) + assert.NoError(t, err) + assert.Equal(t, expected.Id, result.Id) + assert.Equal(t, expected.Text, result.Text) + assert.NoError(t, mock.ExpectationsWereMet()) +} + /* HELPER FUNCTIONS */ func setupMockRepoForAssignments(t *testing.T) (pgxmock.PgxConnIface, repository.CoursesRepository) { diff --git a/internal/repository/exams_test.go b/internal/repository/exams_test.go index 2bc2714..07a4286 100644 --- a/internal/repository/exams_test.go +++ b/internal/repository/exams_test.go @@ -1 +1,292 @@ package repository_test + +import ( + "courses-microservice/config" + "courses-microservice/internal/dto" + "courses-microservice/internal/models" + "courses-microservice/internal/repository" + "testing" + "time" + + "github.com/google/uuid" + "github.com/pashagolub/pgxmock/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateExam_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + + now := time.Now() + exam := &models.Exam{ + CourseId: uuid.New(), + Title: "Final Exam", + Instructions: "Answer all questions", + MaxScore: 100, + PassingScore: 60, + Deadline: now.Add(24 * time.Hour), + } + + examID := uuid.New() + mock.ExpectQuery("INSERT INTO exams"). + WithArgs( + exam.CourseId, + exam.Title, + exam.Instructions, + exam.ExamData, + exam.MaxScore, + exam.PassingScore, + exam.Deadline, + ).WillReturnRows( + pgxmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow(examID, now, now), + ) + + err := repo.CreateExam(exam) + require.NoError(t, err) + assert.Equal(t, examID, exam.Id) + assert.WithinDuration(t, now, exam.CreatedAt, time.Second) + assert.WithinDuration(t, now, exam.UpdatedAt, time.Second) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestDeleteExam_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + + examID := uuid.New() + mock.ExpectExec("DELETE FROM exams"). + WithArgs(examID). + WillReturnResult(pgxmock.NewResult("DELETE", 1)) // simulate 1 row deleted + + err := repo.DeleteExam(examID) + require.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpdateExam_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + examID := uuid.New() + + expectedUpdatedAt := time.Now() + exam := &models.Exam{ + Title: "Updated Exam", + Instructions: "Updated instructions", + MaxScore: 100, + PassingScore: 60, + Deadline: expectedUpdatedAt.Add(24 * time.Hour), + } + + mock.ExpectQuery("UPDATE exams"). + WithArgs( + exam.Title, + exam.Instructions, + exam.ExamData, + exam.MaxScore, + exam.PassingScore, + exam.Deadline, + examID, + ). + WillReturnRows(pgxmock.NewRows([]string{"updated_at"}).AddRow(expectedUpdatedAt)) + + err := repo.UpdateExam(examID, exam) + require.NoError(t, err) + assert.Equal(t, expectedUpdatedAt, exam.UpdatedAt) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetExamsByStudent_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + userID := uuid.New() + page := 1 + + examID := uuid.New() + courseID := uuid.New() + now := time.Now() + + examData := models.ExamQuestionsData{} + + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "instructions", "exam_data", "max_score", "passing_score", "deadline", "created_at", "updated_at", + }).AddRow( + examID, courseID, "Sample Exam", "Instructions", + examData, + 100, 60, + now.Add(24*time.Hour), now, now, + ) + + mock.ExpectQuery("SELECT e.id, e.course_id, e.title.*FROM exams e.*WHERE er.student_id"). + WithArgs(userID, config.CoursesPerPage, config.CoursesPerPage*(page-1)). + WillReturnRows(rows) + + exams, err := repo.GetExamsByStudent(userID, page) + require.NoError(t, err) + require.Len(t, exams, 1) + assert.Equal(t, "Sample Exam", exams[0].Title) + assert.Equal(t, 100, exams[0].MaxScore) + assert.Equal(t, examData, exams[0].ExamData) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetExamsByCourse_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + courseID := uuid.New() + page := 1 + now := time.Now() + examID := uuid.New() + examData := models.ExamQuestionsData{} + + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "instructions", "exam_data", "max_score", "passing_score", "deadline", "created_at", "updated_at", + }).AddRow( + examID, courseID, "Midterm", "Read all questions", + examData, + 100, 60, + now.Add(48*time.Hour), now, now, + ) + + mock.ExpectQuery(`SELECT id, course_id, title.*FROM exams.*WHERE course_id =`). // keep regex loose + WithArgs(courseID, config.CoursesPerPage, config.CoursesPerPage*(page-1)). + WillReturnRows(rows) + + _, err := repo.GetExamsByCourse(courseID, page) + require.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetExamByID_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + examID := uuid.New() + courseID := uuid.New() + now := time.Now() + examData := models.ExamQuestionsData{} + + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "instructions", "exam_data", "max_score", "passing_score", "deadline", "created_at", "updated_at", + }).AddRow( + examID, + courseID, + "Final Exam", + "Follow instructions carefully", + examData, + 100, + 70, + now.Add(72*time.Hour), + now, + now, + ) + + mock.ExpectQuery("SELECT id, course_id, title, instructions, exam_data, max_score, passing_score, deadline, created_at, updated_at FROM exams WHERE id ="). + WithArgs(examID). + WillReturnRows(rows) + + exam, err := repo.GetExamByID(examID) + require.NoError(t, err) + require.NotNil(t, exam) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestCreateOrUpdateExamSubmission_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + + examID := uuid.New() + studentID := uuid.New() + now := time.Now() + + submission := &models.ExamSubmission{} + + mock.ExpectQuery("INSERT INTO exam_submissions"). + WithArgs( + examID, + studentID, + submission.Answers, + submission.Comment, + submission.Score, + submission.Reviewed, + ). + WillReturnRows(pgxmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow(uuid.New(), now, now), + ) + + err := repo.CreateOrUpdateExamSubmission(examID, studentID, submission) + require.NoError(t, err) + assert.NotEqual(t, uuid.Nil, submission.Id) + assert.False(t, submission.CreatedAt.IsZero()) + assert.False(t, submission.UpdatedAt.IsZero()) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetExamSubmissionByID_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + + submissionID := uuid.New() + examID := uuid.New() + studentID := uuid.New() + now := time.Now() + answers := []dto.ExamSubmissionAnswersDTO{} + + rows := pgxmock.NewRows([]string{ + "id", "exam_id", "student_id", "answers", "comment", "score", "reviewed", "created_at", "updated_at", + }).AddRow( + submissionID, + examID, + studentID, + answers, + "Good job", + 90, + true, + now, + now, + ) + + mock.ExpectQuery("SELECT id, exam_id, student_id, answers, comment, score, reviewed, created_at, updated_at FROM exam_submissions WHERE id = \\$1"). + WithArgs(submissionID). + WillReturnRows(rows) + + submission, err := repo.GetExamSubmissionByID(submissionID) + require.NoError(t, err) + require.NotNil(t, submission) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetExamSubmissionByStudent_Success(t *testing.T) { + mock, repo := setupMockRepoForExams(t) + + examID := uuid.New() + studentID := uuid.New() + submissionID := uuid.New() + now := time.Now() + answers := []dto.ExamSubmissionAnswersDTO{} + + rows := pgxmock.NewRows([]string{ + "id", "exam_id", "student_id", "answers", "comment", "score", "reviewed", "created_at", "updated_at", + }).AddRow( + submissionID, + examID, + studentID, + answers, + "Well done", + 85, + false, + now, + now, + ) + + mock.ExpectQuery(`SELECT id, exam_id, student_id, answers, comment, score, reviewed, created_at, updated_at FROM exam_submissions WHERE exam_id = \$1 AND student_id = \$2`). + WithArgs(examID, studentID). + WillReturnRows(rows) + + submission, err := repo.GetExamSubmissionByStudent(examID, studentID) + require.NoError(t, err) + require.NotNil(t, submission) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +/* HELPER FUNCTIONS */ + +func setupMockRepoForExams(t *testing.T) (pgxmock.PgxConnIface, repository.CoursesRepository) { + mock, err := pgxmock.NewConn() + assert.NoError(t, err) + + repo := repository.NewCoursesRepository(mock) + return mock, repo +} diff --git a/internal/repository/memory/repository_memory.go b/internal/repository/memory/repository_memory.go index 23987bd..a5ce03b 100644 --- a/internal/repository/memory/repository_memory.go +++ b/internal/repository/memory/repository_memory.go @@ -565,7 +565,7 @@ func (c *InMemoryCoursesRepository) GetModuleById(moduleId uuid.UUID) (*models.M return module, nil } - return nil, errors.NewNotFoundError("Module not found") + return nil, errors.NewNotFoundError("Module not found [GET]") } func (c *InMemoryCoursesRepository) GetModulesByCourse(courseID uuid.UUID) ([]models.Module, error) { diff --git a/internal/repository/modules.go b/internal/repository/modules.go index af68435..38e6c80 100644 --- a/internal/repository/modules.go +++ b/internal/repository/modules.go @@ -33,7 +33,6 @@ func (r *CoursesRepositoryImpl) CreateModule(module *models.Module) error { } return nil - } func (r *CoursesRepositoryImpl) UpdateModule(moduleId uuid.UUID, module *models.Module) error { diff --git a/internal/repository/modules_test.go b/internal/repository/modules_test.go index 2bc2714..ff05fee 100644 --- a/internal/repository/modules_test.go +++ b/internal/repository/modules_test.go @@ -1 +1,235 @@ package repository_test + +import ( + "courses-microservice/internal/models" + "courses-microservice/internal/repository" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/pashagolub/pgxmock/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateModule_Success(t *testing.T) { + mock, repo := setupMockRepoForModules(t) + + module := &models.Module{ + CourseId: uuid.New(), + Title: "Module Title", + Content: "Module Content", + Position: 1, + Attachments: []string{"attachment1.png", "attachment2.pdf"}, + } + + createdAt := time.Now() + updatedAt := createdAt + + rows := pgxmock.NewRows([]string{"id", "attachments", "created_at", "updated_at"}). + AddRow(uuid.New(), module.Attachments, createdAt, updatedAt) + + mock.ExpectQuery(`INSERT INTO modules \(course_id, title, content, position\) VALUES \(\$1, \$2, \$3, \$4\) RETURNING id, attachments, created_at, updated_at`). + WithArgs(module.CourseId, module.Title, module.Content, module.Position). + WillReturnRows(rows) + + err := repo.CreateModule(module) + + require.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpdateModule_Success(t *testing.T) { + mock, repo := setupMockRepoForModules(t) + moduleId := uuid.New() + module := &models.Module{ + Title: "Updated Title", + Content: "Updated Content", + Attachments: []string{"file1.png", "file2.pdf"}, + Position: 2, + } + + updatedAt := time.Now() + + mock.ExpectQuery(`UPDATE modules SET title = \$1, content = \$2, attachments = \$3, position = \$4 WHERE id = \$5 RETURNING updated_at`). + WithArgs(module.Title, module.Content, module.Attachments, module.Position, moduleId). + WillReturnRows(pgxmock.NewRows([]string{"updated_at"}).AddRow(updatedAt)) + + err := repo.UpdateModule(moduleId, module) + + require.NoError(t, err) + assert.Equal(t, updatedAt, module.UpdatedAt) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestDeleteModule(t *testing.T) { + mock, repo := setupMockRepoForModules(t) + moduleId := uuid.New() + + t.Run("Success", func(t *testing.T) { + mock.ExpectExec(`DELETE FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnResult(pgxmock.NewResult("DELETE", 1)) // 1 row affected + + err := repo.DeleteModule(moduleId) + require.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("NotFound", func(t *testing.T) { + mock.ExpectExec(`DELETE FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnResult(pgxmock.NewResult("DELETE", 0)) // 0 rows affected + + err := repo.DeleteModule(moduleId) + require.Error(t, err) + assert.Contains(t, err.Error(), "No module found with id") + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("InternalError", func(t *testing.T) { + mock.ExpectExec(`DELETE FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnError(fmt.Errorf("db error")) + + err := repo.DeleteModule(moduleId) + require.Error(t, err) + assert.Contains(t, err.Error(), "Internal server error") + assert.NoError(t, mock.ExpectationsWereMet()) + }) +} + +func TestGetModuleById(t *testing.T) { + mock, repo := setupMockRepoForModules(t) + moduleId := uuid.New() + + now := time.Now() + attachments := []string{"file1.pdf", "file2.png"} // Example attachments slice + + t.Run("Success", func(t *testing.T) { + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "content", "attachments", "position", "created_at", "updated_at", + }).AddRow( + moduleId, + uuid.New(), + "Module Title", + "Module Content", + attachments, + 1, + now, + now, + ) + + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnRows(rows) + + module, err := repo.GetModuleById(moduleId) + require.NoError(t, err) + require.NotNil(t, module) + assert.Equal(t, moduleId, module.Id) + assert.Equal(t, "Module Title", module.Title) + assert.Equal(t, attachments, module.Attachments) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("NotFound", func(t *testing.T) { + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnError(pgx.ErrNoRows) + + module, err := repo.GetModuleById(moduleId) + require.Error(t, err) + assert.Nil(t, module) + assert.Contains(t, err.Error(), "No module found with id") + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("InternalError", func(t *testing.T) { + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE id = \$1`). + WithArgs(moduleId). + WillReturnError(fmt.Errorf("db error")) + + module, err := repo.GetModuleById(moduleId) + require.Error(t, err) + assert.Nil(t, module) + assert.Contains(t, err.Error(), "Internal server error") + assert.NoError(t, mock.ExpectationsWereMet()) + }) +} + +func TestGetModulesByCourse(t *testing.T) { + mock, repo := setupMockRepoForModules(t) + courseID := uuid.New() + + now := time.Now() + attachments1 := []string{"file1.pdf"} + attachments2 := []string{"file2.docx"} + + t.Run("Success", func(t *testing.T) { + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "content", "attachments", "position", "created_at", "updated_at", + }).AddRow( + uuid.New(), courseID, "Module 1", "Content 1", attachments1, 1, now, now, + ).AddRow( + uuid.New(), courseID, "Module 2", "Content 2", attachments2, 2, now, now, + ) + + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE course_id = \$1 ORDER BY position`). + WithArgs(courseID). + WillReturnRows(rows) + + modules, err := repo.GetModulesByCourse(courseID) + require.NoError(t, err) + require.Len(t, modules, 2) + assert.Equal(t, "Module 1", modules[0].Title) + assert.Equal(t, attachments1, modules[0].Attachments) + assert.Equal(t, 1, modules[0].Position) + assert.Equal(t, "Module 2", modules[1].Title) + assert.Equal(t, attachments2, modules[1].Attachments) + assert.Equal(t, 2, modules[1].Position) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("QueryError", func(t *testing.T) { + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE course_id = \$1 ORDER BY position`). + WithArgs(courseID). + WillReturnError(fmt.Errorf("query error")) + + modules, err := repo.GetModulesByCourse(courseID) + require.Error(t, err) + assert.Nil(t, modules) + assert.Contains(t, err.Error(), "Internal server error") + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("ScanError", func(t *testing.T) { + rows := pgxmock.NewRows([]string{ + "id", "course_id", "title", "content", "attachments", "position", "created_at", "updated_at", + }).AddRow( + "invalid-uuid", courseID, "Module 1", "Content", attachments1, 1, now, now, + ) + + mock.ExpectQuery(`SELECT id, course_id, title, content, attachments, position, created_at, updated_at FROM modules WHERE course_id = \$1 ORDER BY position`). + WithArgs(courseID). + WillReturnRows(rows) + + modules, err := repo.GetModulesByCourse(courseID) + require.Error(t, err) + assert.Nil(t, modules) + assert.Contains(t, err.Error(), "Internal server error") + assert.NoError(t, mock.ExpectationsWereMet()) + }) +} + +/* HELPER FUNCTIONS */ + +func setupMockRepoForModules(t *testing.T) (pgxmock.PgxConnIface, repository.CoursesRepository) { + mock, err := pgxmock.NewConn() + assert.NoError(t, err) + + repo := repository.NewCoursesRepository(mock) + return mock, repo +} diff --git a/internal/services/modules/modules_test.go b/internal/services/modules/modules_test.go index a71236d..db8b1d4 100644 --- a/internal/services/modules/modules_test.go +++ b/internal/services/modules/modules_test.go @@ -175,26 +175,49 @@ func TestPatchModule_Success(t *testing.T) { } func TestOrganizeModuleAttachments(t *testing.T) { - modules, _ := repo.GetModulesByCourse(courseID) - modulePos := []dto.ModulePositionDTO{} - for i, m := range modules { - modulePos = append(modulePos, dto.ModulePositionDTO{ - Id: m.Id, - Position: i, - }) + course := &models.Course{OwnerID: professorIDs[0], Title: "Test Course"} + _ = repo.AddCourse(course) + _ = repo.AddAuxProfessor(course.Id, professorIDs[1]) + ids := []uuid.UUID{} + modules := []models.Module{ + { + CourseId: course.Id, + Title: "Module 1", + Content: "Content 1", + Position: 0, + }, + { + CourseId: course.Id, + Title: "Module 2", + Content: "Content 2", + Position: 1, + }, + } + for _, module := range modules { + _ = repo.CreateModule(&module) + ids = append(ids, module.Id) + } + + modulePos := []dto.ModulePositionDTO{ + { + Id: ids[0], + Position: 1, + }, + { + Id: ids[1], + Position: 0, + }, } // Not professor - err := service.OrganizeModules(courseID, uuid.New(), modulePos) + err := service.OrganizeModules(course.Id, uuid.New(), modulePos) assert.Error(t, err) assert.IsType(t, err, &errors.UnauthorizedError{}) assert.Contains(t, err.Error(), "not authorized") // Success - err = service.OrganizeModules(courseID, professorIDs[1], modulePos) + err = service.OrganizeModules(course.Id, professorIDs[1], modulePos) assert.NoError(t, err) - organizedModules, _ := repo.GetModulesByCourse(courseID) - assert.Equal(t, len(modules), len(organizedModules)) } /* ATTACHMENTS */ diff --git a/internal/utils/generate_dto_test.go b/internal/utils/generate_dto_test.go new file mode 100644 index 0000000..d55bbb8 --- /dev/null +++ b/internal/utils/generate_dto_test.go @@ -0,0 +1,118 @@ +package utils_test + +import ( + "courses-microservice/internal/dto" + "courses-microservice/internal/models" + "courses-microservice/internal/utils" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestGenerateDTO(t *testing.T) { + course := models.Course{ + Id: uuid.New(), + OwnerID: uuid.New(), + Title: "Test Course", + Description: "This is a test course", + Syllabus: "Syllabus content", + } + auxProfessors := []uuid.UUID{uuid.New(), uuid.New()} + students := []dto.StudentInCourseDTO{ + { + Id: uuid.New(), + FinalGrade: 5, + }, + } + canJoin := true + courseDTO := utils.NewCourseResponseDTO(&course, auxProfessors, students, canJoin) + assert.Equal(t, course.Id, courseDTO.Id) + assert.Equal(t, course.OwnerID, courseDTO.OwnerID) + assert.Equal(t, course.Title, courseDTO.Title) + assert.Equal(t, course.Description, courseDTO.Description) + assert.Equal(t, course.Syllabus, courseDTO.Syllabus) +} + +func TestGenerateAssignmentDTO(t *testing.T) { + assignment := models.Assignment{ + Id: uuid.New(), + CourseId: uuid.New(), + Title: "Test Assignment", + Instructions: "Complete the assignment", + Attachments: []string{"attachment1.pdf", "attachment2.docx"}, + } + status := models.AssignmentPending + submissionID := uuid.New() + assignmentDTO := utils.NewAssignmentResponseDTO(&assignment, status, &submissionID) + assert.Equal(t, assignment.Id, assignmentDTO.Id) + assert.Equal(t, assignment.CourseId, assignmentDTO.CourseId) + assert.Equal(t, assignment.Title, assignmentDTO.Title) + assert.Equal(t, assignment.Instructions, assignmentDTO.Instructions) + assert.Equal(t, assignment.Attachments, assignmentDTO.Attachments) + assert.Equal(t, status, models.AssignmentStatus(assignmentDTO.Status)) + assert.Equal(t, submissionID, *assignmentDTO.SubmissionId) +} + +func TestGenerateAssignmentSubmissionDTO(t *testing.T) { + submission := models.AssignmentSubmission{ + Id: uuid.New(), + AssignmentId: uuid.New(), + StudentId: uuid.New(), + Text: "This is a submission", + Attachment: "submission.pdf", + Comment: "Good job!", + } + isStudent := true + submissionDTO := utils.NewAssignmentSubmissionResponseDTO(&submission, isStudent) + assert.Equal(t, submission.Id, submissionDTO.Id) + assert.Equal(t, submission.AssignmentId, submissionDTO.AssignmentId) + assert.Equal(t, submission.StudentId, submissionDTO.StudentId) + assert.Equal(t, submission.Text, submissionDTO.Text) + assert.Equal(t, submission.Attachment, submissionDTO.Attachment) + assert.Equal(t, submission.Comment, submissionDTO.Comment) +} + +func TestGenerateExamDTO(t *testing.T) { + exam := models.Exam{ + Id: uuid.New(), + CourseId: uuid.New(), + Title: "Test Exam", + Instructions: "Follow the instructions carefully", + MaxScore: 100, + PassingScore: 60, + } + examDTO := utils.NewExamResponseDTO(&exam, models.ExamPending, nil) + assert.Equal(t, exam.Id, examDTO.Id) + assert.Equal(t, exam.CourseId, examDTO.CourseId) + assert.Equal(t, exam.Title, examDTO.Title) + assert.Equal(t, exam.Instructions, examDTO.Instructions) + assert.Equal(t, exam.MaxScore, examDTO.MaxScore) + assert.Equal(t, exam.PassingScore, examDTO.PassingScore) +} + +func TestGenerateExamSubmissionDTO(t *testing.T) { + submission := models.ExamSubmission{ + Id: uuid.New(), + ExamId: uuid.New(), + StudentId: uuid.New(), + Comment: "Well done!", + } + submissionDTO := utils.NewExamSubmissionResponseDTO(&submission, true) + assert.Equal(t, submission.Id, submissionDTO.Id) + assert.Equal(t, submission.ExamId, submissionDTO.ExamId) + assert.Equal(t, submission.StudentId, submissionDTO.StudentId) + assert.Equal(t, submission.Comment, submissionDTO.Comment) +} + +func TestModuleResponseDTO(t *testing.T) { + module := models.Module{ + Id: uuid.New(), + CourseId: uuid.New(), + Title: "Test Module", + } + moduleDTO := utils.NewModuleResponseDTO(&module) + assert.Equal(t, module.Id, moduleDTO.Id) + assert.Equal(t, module.CourseId, moduleDTO.CourseId) + assert.Equal(t, module.Title, moduleDTO.Title) +}