From 16e6c3282b174a50c5b5fc42cc828ab3cdb0b698 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Tue, 27 Oct 2026 20:39:29 +0300 Subject: [PATCH] =?UTF-8?q?test(web):=20=D0=BF=D1=80=D0=B5=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C,=20=D0=BC=D0=B0=D1=82=D0=B5=D1=80=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lms/web/admin/AdminAuditTemplateTest.kt | 136 ++++++++++++++ .../lms/web/admin/AdminStage4TemplateTest.kt | 1 + .../web/admin/TeacherGradebookTemplateTest.kt | 139 ++++++++++++++ .../kotlin/ru/ysu/lms/web/api/AuditDtoTest.kt | 65 +++++++ .../lms/web/session/AdminInterceptorTest.kt | 149 ++++++++++++++- .../TeacherCourseContentTemplateTest.kt | 176 ++++++++++++++++++ .../web/teacher/TeacherCourseTemplateTest.kt | 90 +++++++++ 7 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 web/src/test/kotlin/ru/ysu/lms/web/admin/AdminAuditTemplateTest.kt create mode 100644 web/src/test/kotlin/ru/ysu/lms/web/admin/TeacherGradebookTemplateTest.kt create mode 100644 web/src/test/kotlin/ru/ysu/lms/web/api/AuditDtoTest.kt create mode 100644 web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseContentTemplateTest.kt create mode 100644 web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseTemplateTest.kt diff --git a/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminAuditTemplateTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminAuditTemplateTest.kt new file mode 100644 index 0000000..3c999a2 --- /dev/null +++ b/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminAuditTemplateTest.kt @@ -0,0 +1,136 @@ +package ru.ysu.lms.web.admin + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockServletContext +import org.thymeleaf.context.WebContext +import org.thymeleaf.spring6.SpringTemplateEngine +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import org.thymeleaf.web.servlet.JakartaServletWebApplication +import java.util.Locale + +class AdminAuditTemplateTest { + private val engine = + SpringTemplateEngine().apply { + setTemplateResolver( + ClassLoaderTemplateResolver().apply { + prefix = "templates/" + suffix = ".html" + characterEncoding = "UTF-8" + templateMode = TemplateMode.HTML + }, + ) + } + + @Test + fun `renders audit events with labels and filter`() { + val rows = + listOf( + AuditRow( + occurredMsk = "01.09.2026 00:00", + actorLabel = "admin", + action = "GRADE_SET", + actionLabel = "Выставлена оценка", + entityType = "GRADE", + entityId = "g1", + entityLabel = "Оценка · g1", + courseId = "c1", + studentId = "s1", + details = "Оценка 5", + ), + AuditRow( + occurredMsk = "31.08.2026 23:50", + actorLabel = "система", + action = "STUDENT_TRANSFER", + actionLabel = "Перевод студента", + entityType = "STUDENT", + entityId = null, + entityLabel = "Студент", + courseId = null, + studentId = null, + details = "", + ), + ) + + val html = + render( + "admin/audit", + mapOf( + "events" to rows, + "action" to "GRADE_SET", + "limit" to 100, + "actionOptions" to AuditViews.actionOptions("GRADE_SET"), + ), + ) + + assertThat(html).contains("Аудит") + assertThat(html).contains("name=\"action\"") + assertThat(html).contains("name=\"limit\"") + assertThat(html).contains("value=\"GRADE_SET\"") + assertThat(html).contains("Все действия") + assertThat(html).contains("01.09.2026 00:00") + assertThat(html).contains("31.08.2026 23:50") + assertThat(html).contains("admin") + assertThat(html).contains("система") + assertThat(html).contains("Выставлена оценка") + assertThat(html).contains("Перевод студента") + assertThat(html).contains("Оценка · g1") + assertThat(html).contains("c1") + assertThat(html).contains("s1") + assertThat(html).contains("Оценка 5") + } + + @Test + fun `renders empty audit state`() { + val html = + render( + "admin/audit", + mapOf( + "events" to emptyList(), + "action" to "", + "limit" to 100, + "actionOptions" to AuditViews.actionOptions(null), + ), + ) + + assertThat(html).contains("Аудит") + assertThat(html).contains("Записей аудита нет.") + } + + @Test + fun `keeps unknown action filter selectable`() { + val html = + render( + "admin/audit", + mapOf( + "events" to emptyList(), + "action" to "CUSTOM_ACTION", + "limit" to 100, + "actionOptions" to AuditViews.actionOptions("CUSTOM_ACTION"), + ), + ) + + assertThat(html).contains("value=\"CUSTOM_ACTION\"") + assertThat(html).contains("CUSTOM_ACTION") + } + + private fun render( + template: String, + variables: Map, + ): String { + val servletContext = MockServletContext() + val application = JakartaServletWebApplication.buildApplication(servletContext) + val exchange = + application.buildExchange( + MockHttpServletRequest(servletContext), + MockHttpServletResponse(), + ) + val context = WebContext(exchange, Locale.forLanguageTag("ru")) + context.setVariable("csrfToken", "csrf") + variables.forEach { (name, value) -> context.setVariable(name, value) } + return engine.process(template, context) + } +} diff --git a/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminStage4TemplateTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminStage4TemplateTest.kt index 3b7629d..8434d43 100644 --- a/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminStage4TemplateTest.kt +++ b/web/src/test/kotlin/ru/ysu/lms/web/admin/AdminStage4TemplateTest.kt @@ -196,6 +196,7 @@ class AdminStage4TemplateTest { "attestations" to emptyList(), "publishedLabsCount" to 0, "profileCourseId" to null, + "teacherView" to false, ) val html = render("admin/group", model) diff --git a/web/src/test/kotlin/ru/ysu/lms/web/admin/TeacherGradebookTemplateTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/admin/TeacherGradebookTemplateTest.kt new file mode 100644 index 0000000..a4a7dd2 --- /dev/null +++ b/web/src/test/kotlin/ru/ysu/lms/web/admin/TeacherGradebookTemplateTest.kt @@ -0,0 +1,139 @@ +package ru.ysu.lms.web.admin + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockServletContext +import org.thymeleaf.context.WebContext +import org.thymeleaf.spring6.SpringTemplateEngine +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import org.thymeleaf.web.servlet.JakartaServletWebApplication +import ru.ysu.lms.web.api.GroupDetailDto +import ru.ysu.lms.web.api.LabSummaryDto +import ru.ysu.lms.web.api.StudentRowDto +import java.util.Locale + +class TeacherGradebookTemplateTest { + private val engine = + SpringTemplateEngine().apply { + setTemplateResolver( + ClassLoaderTemplateResolver().apply { + prefix = "templates/" + suffix = ".html" + characterEncoding = "UTF-8" + templateMode = TemplateMode.HTML + }, + ) + } + + @Test + fun `teacher gradebook keeps grading and defense forms`() { + val html = render() + + assertThat(html).contains("action=\"/admin/groups/g1/grades\"") + assertThat(html).contains("action=\"/admin/defenses/save\"") + assertThat(html).contains("/admin/groups/g1/publish") + assertThat(html).contains("name=\"courseId\"") + assertThat(html).contains("value=\"c1\"") + } + + @Test + fun `teacher gradebook has no admin-only controls`() { + val html = render() + + assertThat(html).doesNotContain("/credentials") + assertThat(html).doesNotContain("Пароли") + assertThat(html).doesNotContain("password-dot") + assertThat(html).doesNotContain("reset-password") + assertThat(html).doesNotContain("academic-leave") + assertThat(html).doesNotContain("/students/s1/archive") + assertThat(html).doesNotContain("/students/s1/expel") + assertThat(html).doesNotContain("/students/s1/unenroll") + assertThat(html).doesNotContain("/attestations") + assertThat(html).doesNotContain("modal-add-attestation") + assertThat(html).doesNotContain("modal-add-student") + assertThat(html).doesNotContain("Добавить студента") + } + + @Test + fun `teacher gradebook links profiles to teacher course`() { + val html = render() + + assertThat(html).contains("/teacher/courses/c1/students/s1") + assertThat(html).doesNotContain("/admin/courses/c1/students/s1") + } + + @Test + fun `admin view is used when teacherView attribute is missing`() { + val html = render(teacherView = null) + + assertThat(html).contains("/credentials") + assertThat(html).contains("/students/s1/reset-password") + assertThat(html).contains("/admin/courses/c1/students/s1") + assertThat(html).doesNotContain("/teacher/courses/c1/students/s1") + } + + private fun render(teacherView: Boolean? = true): String { + val group = + GroupDetailDto( + id = "g1", + name = "ПИ-21БО", + students = + listOf( + StudentRowDto( + id = "s1", + studentNo = "123456", + fullName = "Иванов Иван", + status = "ACTIVE", + mustChangePassword = true, + ), + ), + ) + val lab = LabSummaryDto(id = "l1", number = 1, title = "Введение", status = "published") + val servletContext = MockServletContext() + val application = JakartaServletWebApplication.buildApplication(servletContext) + val exchange = + application.buildExchange( + MockHttpServletRequest(servletContext), + MockHttpServletResponse(), + ) + val context = WebContext(exchange, Locale.forLanguageTag("ru")) + context.setVariable("csrfToken", "csrf") + if (teacherView != null) context.setVariable("teacherView", teacherView) + context.setVariable("profileCourseId", "c1") + context.setVariable("group", group) + context.setVariable("labs", listOf(lab)) + context.setVariable("candidates", emptyList()) + context.setVariable("studentGrades", listOf(studentRow())) + context.setVariable("labPublication", mapOf("l1" to GroupLabPublication(allPublished = true, publishedCount = 1, total = 1))) + context.setVariable("labSubmissionCounts", mapOf("l1" to 1)) + context.setVariable("attestations", emptyList()) + context.setVariable("publishedLabsCount", 1) + return engine.process("admin/group", context) + } + + private fun studentRow(): GroupStudentRow = + GroupStudentRow( + studentId = "s1", + fullName = "Иванов Иван", + studentNo = "123456", + basis = "", + status = "ACTIVE", + mustChangePassword = true, + cells = + listOf( + GroupLabCell( + labId = "l1", + labNumber = 1, + labTitle = "Введение", + score = "4", + published = true, + ), + ), + variantsSummary = "", + commentsJson = "[]", + averageScore = "4", + ) +} diff --git a/web/src/test/kotlin/ru/ysu/lms/web/api/AuditDtoTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/api/AuditDtoTest.kt new file mode 100644 index 0000000..39421b3 --- /dev/null +++ b/web/src/test/kotlin/ru/ysu/lms/web/api/AuditDtoTest.kt @@ -0,0 +1,65 @@ +package ru.ysu.lms.web.api + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import tools.jackson.databind.ObjectMapper + +class AuditDtoTest { + private val mapper = ObjectMapper() + + @Test + fun `deserializes audit event list with nulls`() { + val json = + """ + [ + { + "id": "e1", + "occurredAt": "2026-08-31T21:00:00Z", + "actorAccountId": "a1", + "actorLogin": "admin", + "action": "GRADE_SET", + "entityType": "GRADE", + "entityId": "g1", + "courseId": "c1", + "studentId": "s1", + "details": "Оценка 5", + "unknownField": "ignored" + }, + { + "id": "e2", + "occurredAt": "2026-09-01T10:15:00Z", + "actorAccountId": null, + "actorLogin": null, + "action": "STUDENT_TRANSFER", + "entityType": "STUDENT", + "entityId": null, + "courseId": null, + "studentId": null, + "details": "" + } + ] + """.trimIndent() + + val events = mapper.readValue(json, Array::class.java).toList() + + assertThat(events).hasSize(2) + assertThat(events[0].id).isEqualTo("e1") + assertThat(events[0].occurredAt).isEqualTo("2026-08-31T21:00:00Z") + assertThat(events[0].actorAccountId).isEqualTo("a1") + assertThat(events[0].actorLogin).isEqualTo("admin") + assertThat(events[0].action).isEqualTo("GRADE_SET") + assertThat(events[0].entityType).isEqualTo("GRADE") + assertThat(events[0].entityId).isEqualTo("g1") + assertThat(events[0].courseId).isEqualTo("c1") + assertThat(events[0].studentId).isEqualTo("s1") + assertThat(events[0].details).isEqualTo("Оценка 5") + + assertThat(events[1].id).isEqualTo("e2") + assertThat(events[1].actorAccountId).isNull() + assertThat(events[1].actorLogin).isNull() + assertThat(events[1].entityId).isNull() + assertThat(events[1].courseId).isNull() + assertThat(events[1].studentId).isNull() + assertThat(events[1].details).isEmpty() + } +} diff --git a/web/src/test/kotlin/ru/ysu/lms/web/session/AdminInterceptorTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/session/AdminInterceptorTest.kt index bcc36a8..a64d72a 100644 --- a/web/src/test/kotlin/ru/ysu/lms/web/session/AdminInterceptorTest.kt +++ b/web/src/test/kotlin/ru/ysu/lms/web/session/AdminInterceptorTest.kt @@ -5,10 +5,12 @@ import org.springframework.mock.web.MockHttpSession import org.springframework.stereotype.Controller import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping class AdminInterceptorTest { @Controller @@ -19,8 +21,74 @@ class AdminInterceptorTest { @GetMapping("/admin/labs/{id}") fun labDetail(): String = "admin/lab" + @GetMapping("/admin/labs/{id}/schema") + fun labSchema(): String = "admin/lab" + @GetMapping("/admin/users") fun users(): String = "admin/users" + + @GetMapping("/admin/groups/{id}") + fun group(): String = "admin/group" + + @GetMapping("/admin/groups") + fun groups(): String = "admin/groups" + + @GetMapping("/admin/groups/{id}/assign-modal") + fun assignModal(): String = "admin/assign-fragment" + + @GetMapping("/admin/groups/{id}/submissions.zip") + fun submissions(): String = "admin/group" + + @GetMapping("/admin/submissions/{id}/download") + fun downloadSubmission(): String = "admin/group" + + @GetMapping("/admin/defense-watch") + fun defenseWatch(): String = "admin/group" + + @GetMapping("/admin/comment-templates") + fun commentTemplates(): String = "admin/group" + + @PostMapping("/admin/groups") + fun createGroup(): String = "admin/groups" + + @PostMapping("/admin/groups/import") + fun importGroup(): String = "admin/groups" + + @PostMapping("/admin/groups/{id}/attestations") + fun createAttestation(): String = "admin/group" + + @PostMapping("/admin/comment-templates/delete") + fun deleteCommentTemplate(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/grades") + fun grades(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/publish") + fun publish(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/publish-student") + fun publishStudent(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/set-level") + fun setLevel(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/pin-variant") + fun pinVariant(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/distribute") + fun distribute(): String = "admin/group" + + @PostMapping("/admin/groups/{id}/distribute-all") + fun distributeAll(): String = "admin/group" + + @PostMapping("/admin/defenses/save") + fun saveDefense(): String = "admin/group" + + @PostMapping("/admin/defenses/{id}/issue-code") + fun issueCode(): String = "admin/group" + + @PostMapping("/admin/defenses/{id}/reset") + fun resetDefense(): String = "admin/group" } private val mockMvc: MockMvc = @@ -37,10 +105,83 @@ class AdminInterceptorTest { } @Test - fun `teacher may open lab detail`() { + fun `teacher may open lab detail and schema`() { mockMvc .perform(get("/admin/labs/l1").session(sessionFor("TEACHER"))) .andExpect(status().isOk) + mockMvc + .perform(get("/admin/labs/l1/schema").session(sessionFor("TEACHER"))) + .andExpect(status().isOk) + } + + @Test + fun `teacher may open gradebook page and its fragments`() { + val session = sessionFor("TEACHER") + mockMvc + .perform(get("/admin/groups/g1").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/groups/g1/assign-modal").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/groups/g1/submissions.zip").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/submissions/s1/download").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/defense-watch").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/comment-templates").session(session)) + .andExpect(status().isOk) + } + + @Test + fun `teacher may run gradebook actions`() { + val session = sessionFor("TEACHER") + listOf( + "/admin/groups/g1/grades", + "/admin/groups/g1/publish", + "/admin/groups/g1/publish-student", + "/admin/groups/g1/set-level", + "/admin/groups/g1/pin-variant", + "/admin/groups/g1/distribute", + ).forEach { url -> + mockMvc + .perform(post(url).session(session)) + .andExpect(status().isOk) + } + } + + @Test + fun `teacher may run defense actions`() { + val session = sessionFor("TEACHER") + mockMvc + .perform(post("/admin/defenses/save").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(post("/admin/defenses/d1/issue-code").session(session)) + .andExpect(status().isOk) + mockMvc + .perform(post("/admin/defenses/d1/reset").session(session)) + .andExpect(status().isOk) + } + + @Test + fun `teacher is redirected from admin-only group operations`() { + val session = sessionFor("TEACHER") + listOf("/admin/groups", "/admin/groups/import", "/admin/groups/g1/attestations", "/admin/comment-templates/delete") + .forEach { url -> + mockMvc + .perform(post(url).session(session)) + .andExpect(status().is3xxRedirection) + .andExpect(redirectedUrl("/teacher")) + } + mockMvc + .perform(get("/admin/groups").session(session)) + .andExpect(status().is3xxRedirection) + .andExpect(redirectedUrl("/teacher")) } @Test @@ -64,6 +205,12 @@ class AdminInterceptorTest { mockMvc .perform(get("/admin/users").session(sessionFor("ADMIN"))) .andExpect(status().isOk) + mockMvc + .perform(post("/admin/groups").session(sessionFor("ADMIN"))) + .andExpect(status().isOk) + mockMvc + .perform(get("/admin/groups/g1").session(sessionFor("ADMIN"))) + .andExpect(status().isOk) } @Test diff --git a/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseContentTemplateTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseContentTemplateTest.kt new file mode 100644 index 0000000..ca3169f --- /dev/null +++ b/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseContentTemplateTest.kt @@ -0,0 +1,176 @@ +package ru.ysu.lms.web.teacher + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockServletContext +import org.thymeleaf.context.WebContext +import org.thymeleaf.spring6.SpringTemplateEngine +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import org.thymeleaf.web.servlet.JakartaServletWebApplication +import ru.ysu.lms.web.api.ContentArticleDto +import ru.ysu.lms.web.api.ContentArticleNodeDto +import ru.ysu.lms.web.api.ContentChapterNodeDto +import ru.ysu.lms.web.api.ContentSectionNodeDto +import ru.ysu.lms.web.api.ContentTreeNodeDto +import ru.ysu.lms.web.api.CourseDetailDto +import ru.ysu.lms.web.session.SessionUser +import java.util.Locale + +class TeacherCourseContentTemplateTest { + private val engine = + SpringTemplateEngine().apply { + setTemplateResolver( + ClassLoaderTemplateResolver().apply { + prefix = "templates/" + suffix = ".html" + characterEncoding = "UTF-8" + templateMode = TemplateMode.HTML + }, + ) + } + + private val course = CourseDetailDto(id = "c1", courseNo = 42, title = "Алгоритмы") + + private val tree = + ContentTreeNodeDto( + chapters = + listOf( + ContentChapterNodeDto( + id = "ch1", + title = "Введение", + slug = "intro", + published = true, + sections = + listOf( + ContentSectionNodeDto( + id = "sec1", + title = "Основы", + slug = "basics", + published = false, + articles = + listOf( + ContentArticleNodeDto( + id = "art1", + title = "Первый шаг", + slug = "first-step", + published = false, + ), + ), + ), + ), + ), + ), + ) + + @Test + fun `content page renders create forms and full tree`() { + val html = render("teacher/content") + + assertThat(html).contains("Материалы курса") + assertThat(html).contains("Алгоритмы") + assertThat(html).contains("action=\"/teacher/courses/c1/content/chapters\"") + assertThat(html).contains("name=\"title\"") + assertThat(html).contains("Введение") + assertThat(html).contains("Основы") + assertThat(html).contains("Первый шаг") + assertThat(html).contains("Разделов: 1") + assertThat(html).contains("Статей: 1") + assertThat(html).contains("name=\"_csrf\"") + assertThat(html).contains("/learn/course/42") + } + + @Test + fun `content page links articles to teacher edit page and publishes every level`() { + val html = render("teacher/content") + + assertThat(html).contains("/teacher/courses/c1/content/intro/basics/first-step") + assertThat(html).contains("/teacher/courses/c1/content/chapters/ch1/published") + assertThat(html).contains("/teacher/courses/c1/content/sections/sec1/published") + assertThat(html).contains("/teacher/courses/c1/content/articles/art1/published") + assertThat(html).contains("/teacher/courses/c1/content/chapters/ch1/rename") + assertThat(html).contains("/teacher/courses/c1/content/sections/sec1/rename") + assertThat(html).contains("/teacher/courses/c1/content/chapters/ch1/delete") + assertThat(html).contains("/teacher/courses/c1/content/sections/sec1/delete") + assertThat(html).contains("/teacher/courses/c1/content/articles/art1/delete") + } + + @Test + fun `content page renders article and section creation forms`() { + val html = render("teacher/content") + + assertThat(html).contains("action=\"/teacher/courses/c1/content/sections\"") + assertThat(html).contains("name=\"chapterId\"") + assertThat(html).contains("action=\"/teacher/courses/c1/content/articles\"") + assertThat(html).contains("name=\"sectionId\"") + assertThat(html).contains("name=\"bodyMd\"") + assertThat(html).contains("Добавить статью") + assertThat(html).contains("Добавить раздел") + } + + @Test + fun `article page shows editable markdown, publish and delete forms and preview`() { + val article = + ContentArticleDto( + id = "art1", + chapterSlug = "intro", + chapterTitle = "Введение", + sectionSlug = "basics", + sectionTitle = "Основы", + slug = "first-step", + title = "Первый шаг", + bodyMd = "# Первый шаг\n\nТекст статьи", + published = false, + ) + val html = + render( + "teacher/content-article", + mapOf( + "article" to article, + "bodyHtml" to "

Текст статьи

", + "back" to "/teacher/courses/c1/content/intro/basics/first-step", + ), + ) + + assertThat(html).contains("name=\"bodyMd\"") + assertThat(html).contains("rows=\"18\"") + assertThat(html).contains("# Первый шаг") + assertThat(html).contains("action=\"/teacher/courses/c1/content/articles/art1/save\"") + assertThat(html).contains("/teacher/courses/c1/content/articles/art1/delete") + assertThat(html).contains("/teacher/courses/c1/content/articles/art1/published") + assertThat(html).contains("

Текст статьи

") + assertThat(html).contains("name=\"_csrf\"") + assertThat(html).contains("/learn/course/42/intro/basics/first-step") + } + + private fun render( + template: String, + extra: Map = emptyMap(), + ): String { + val servletContext = MockServletContext() + val application = JakartaServletWebApplication.buildApplication(servletContext) + val exchange = + application.buildExchange( + MockHttpServletRequest(servletContext), + MockHttpServletResponse(), + ) + val context = WebContext(exchange, Locale.forLanguageTag("ru")) + context.setVariable("csrfToken", "csrf") + context.setVariable( + "currentUser", + SessionUser( + id = "t1", + login = "teacher", + role = "TEACHER", + name = "Преподаватель", + mustChangePassword = false, + ), + ) + context.setVariable("course", course) + context.setVariable("tree", tree) + extra.forEach { (name, value) -> context.setVariable(name, value) } + return engine.process(template, context) + } +} diff --git a/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseTemplateTest.kt b/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseTemplateTest.kt new file mode 100644 index 0000000..0542f31 --- /dev/null +++ b/web/src/test/kotlin/ru/ysu/lms/web/teacher/TeacherCourseTemplateTest.kt @@ -0,0 +1,90 @@ +package ru.ysu.lms.web.teacher + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockServletContext +import org.thymeleaf.context.WebContext +import org.thymeleaf.spring6.SpringTemplateEngine +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import org.thymeleaf.web.servlet.JakartaServletWebApplication +import ru.ysu.lms.web.api.CourseDetailDto +import ru.ysu.lms.web.api.CourseGroupDto +import ru.ysu.lms.web.api.CourseGroupOptionDto +import ru.ysu.lms.web.session.SessionUser +import java.util.Locale + +class TeacherCourseTemplateTest { + private val engine = + SpringTemplateEngine().apply { + setTemplateResolver( + ClassLoaderTemplateResolver().apply { + prefix = "templates/" + suffix = ".html" + characterEncoding = "UTF-8" + templateMode = TemplateMode.HTML + }, + ) + } + + @Test + fun `renders attached group names and gradebook links`() { + val html = render() + + assertThat(html).contains("ПИ-21БО") + assertThat(html).contains("/admin/groups/g1") + assertThat(html).contains("courseId=c1") + assertThat(html).contains("Ведомость") + assertThat(html).contains("/teacher/courses/c1/groups/detach") + } + + @Test + fun `renders group attach form with available groups`() { + val html = render() + + assertThat(html).contains("action=\"/teacher/courses/c1/groups/attach\"") + assertThat(html).contains("name=\"groupId\"") + assertThat(html).contains("ПИ-22БО") + assertThat(html).contains("Привязать") + } + + private fun render(): String { + val course = + CourseDetailDto( + id = "c1", + courseNo = 1, + title = "Программирование", + groups = listOf(CourseGroupDto(groupId = "g1", addedAt = "2026-01-01T00:00:00Z")), + ) + val servletContext = MockServletContext() + val application = JakartaServletWebApplication.buildApplication(servletContext) + val exchange = + application.buildExchange( + MockHttpServletRequest(servletContext), + MockHttpServletResponse(), + ) + val context = WebContext(exchange, Locale.forLanguageTag("ru")) + context.setVariable("csrfToken", "csrf") + context.setVariable( + "currentUser", + SessionUser( + id = "t1", + login = "teacher", + role = "TEACHER", + name = "Преподаватель", + mustChangePassword = false, + ), + ) + context.setVariable("course", course) + context.setVariable("groups", course.groups) + context.setVariable("groupNames", mapOf("g1" to "ПИ-21БО")) + context.setVariable( + "availableGroups", + listOf(CourseGroupOptionDto(id = "g2", name = "ПИ-22БО", status = "ACTIVE")), + ) + context.setVariable("labs", emptyList()) + return engine.process("teacher/course", context) + } +}