test(web): преподавательская ведомость, материалы и аудит

This commit is contained in:
2026-10-27 20:39:29 +03:00
parent 97e14f040c
commit 16e6c3282b
7 changed files with 755 additions and 1 deletions

View File

@@ -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<AuditRow>(),
"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<AuditRow>(),
"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, Any?>,
): 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)
}
}

View File

@@ -196,6 +196,7 @@ class AdminStage4TemplateTest {
"attestations" to emptyList<GroupAttestation>(),
"publishedLabsCount" to 0,
"profileCourseId" to null,
"teacherView" to false,
)
val html = render("admin/group", model)

View File

@@ -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<Any>())
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<GroupAttestation>())
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",
)
}

View File

@@ -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<AuditEventDto>::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()
}
}

View File

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

View File

@@ -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 "<p>Текст статьи</p>",
"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("<p>Текст статьи</p>")
assertThat(html).contains("name=\"_csrf\"")
assertThat(html).contains("/learn/course/42/intro/basics/first-step")
}
private fun render(
template: String,
extra: Map<String, Any?> = 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)
}
}

View File

@@ -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<Any>())
return engine.process("teacher/course", context)
}
}