考试计划草稿、场次配置与正式发布。 教学班、考试时间、考场、监考教师关联。 自动校验考场容量。 阻止考场、监考教师和学生考试时间冲突。 管理员查看考生名单。 教师查看个人监考安排。 学生查看已发布考试日程。 SQLite 增量升级和 MySQL 正式迁移。
376 lines
16 KiB
PowerShell
376 lines
16 KiB
PowerShell
param(
|
|
[string] $ApiExecutablePath,
|
|
[string] $ApiContentRoot
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
|
$apiExecutable = if ([string]::IsNullOrWhiteSpace($ApiExecutablePath)) {
|
|
Join-Path $workspaceRoot 'src/Jiaowu.Api/bin/Debug/net10.0/Jiaowu.Api.exe'
|
|
}
|
|
else {
|
|
[System.IO.Path]::GetFullPath($ApiExecutablePath, $workspaceRoot)
|
|
}
|
|
$apiWorkingDirectory = if ([string]::IsNullOrWhiteSpace($ApiContentRoot)) {
|
|
Join-Path $workspaceRoot 'src/Jiaowu.Api'
|
|
}
|
|
else {
|
|
[System.IO.Path]::GetFullPath($ApiContentRoot, $workspaceRoot)
|
|
}
|
|
$stdoutPath = Join-Path $env:TEMP 'jiaowu-api-smoke.out.log'
|
|
$stderrPath = Join-Path $env:TEMP 'jiaowu-api-smoke.err.log'
|
|
$env:ASPNETCORE_ENVIRONMENT = 'Development'
|
|
$env:ASPNETCORE_URLS = 'http://localhost:5255'
|
|
$startParameters = @{
|
|
FilePath = $apiExecutable
|
|
WorkingDirectory = $apiWorkingDirectory
|
|
WindowStyle = 'Hidden'
|
|
RedirectStandardOutput = $stdoutPath
|
|
RedirectStandardError = $stderrPath
|
|
PassThru = $true
|
|
}
|
|
$process = Start-Process @startParameters
|
|
|
|
try {
|
|
$healthy = $false
|
|
for ($attempt = 0; $attempt -lt 30; $attempt++) {
|
|
Start-Sleep -Milliseconds 500
|
|
try {
|
|
$health = Invoke-RestMethod -Uri 'http://localhost:5255/health' -TimeoutSec 2
|
|
$healthy = $true
|
|
break
|
|
}
|
|
catch {
|
|
# The API may still be starting.
|
|
}
|
|
}
|
|
|
|
if (-not $healthy) {
|
|
Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue
|
|
Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue
|
|
throw 'API did not become healthy.'
|
|
}
|
|
|
|
$loginBody = @{
|
|
userName = 'admin'
|
|
password = 'Admin@123456'
|
|
} | ConvertTo-Json
|
|
$loginParameters = @{
|
|
Method = 'Post'
|
|
Uri = 'http://localhost:5255/api/auth/login'
|
|
ContentType = 'application/json'
|
|
Body = $loginBody
|
|
}
|
|
$login = Invoke-RestMethod @loginParameters
|
|
$headers = @{ Authorization = "Bearer $($login.token)" }
|
|
$dashboard = Invoke-RestMethod -Uri 'http://localhost:5255/api/dashboard' -Headers $headers
|
|
$campuses = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/campuses' -Headers $headers
|
|
$classes = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/classes' -Headers $headers
|
|
$counselors = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/counselors' -Headers $headers
|
|
$assignedClassCount = @($classes | Where-Object {
|
|
$null -ne $_.counselorUserId -and
|
|
@($counselors).id -contains $_.counselorUserId
|
|
}).Count
|
|
if ($assignedClassCount -lt 1) {
|
|
throw 'No administrative class is linked to a counselor account.'
|
|
}
|
|
$teachers = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' -Headers $headers
|
|
$students = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' -Headers $headers
|
|
$courses = Invoke-RestMethod -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' -Headers $headers
|
|
$curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers
|
|
if ($curriculumPlans.total -gt 0) {
|
|
$curriculumDetail = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/curriculum-plans/$($curriculumPlans.items[0].id)" `
|
|
-Headers $headers
|
|
}
|
|
$teachingTasks = Invoke-RestMethod -Uri 'http://localhost:5255/api/teaching-tasks?page=1&pageSize=10' -Headers $headers
|
|
$schedulePlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/schedules/plans' -Headers $headers
|
|
if (@($schedulePlans).Count -gt 0) {
|
|
$scheduleDetail = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/schedules/plans/$($schedulePlans[0].id)" `
|
|
-Headers $headers
|
|
}
|
|
$selectionRounds = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/course-selections/rounds' `
|
|
-Headers $headers
|
|
if (@($selectionRounds).Count -lt 1) {
|
|
throw 'Development course-selection round was not seeded.'
|
|
}
|
|
$activeSelectionRound = @($selectionRounds) |
|
|
Where-Object { $_.isAvailableNow } |
|
|
Select-Object -First 1
|
|
if ($null -eq $activeSelectionRound) {
|
|
throw 'No course-selection round is open for the smoke test.'
|
|
}
|
|
$selectionOfferings = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/course-selections/rounds/$($activeSelectionRound.id)/offerings" `
|
|
-Headers $headers
|
|
if (@($selectionOfferings).Count -lt 1) {
|
|
throw 'Development course-selection offering was not seeded.'
|
|
}
|
|
$managedUsers = Invoke-RestMethod -Uri 'http://localhost:5255/api/users' -Headers $headers
|
|
$teacherAccount = @($managedUsers) |
|
|
Where-Object { $_.userName -eq 'teacher' } |
|
|
Select-Object -First 1
|
|
if ($null -eq $teacherAccount) {
|
|
throw 'Development teacher account was not seeded.'
|
|
}
|
|
$teacherAccessBody = @{
|
|
staffNumber = $teacherAccount.staffNumber
|
|
collegeId = $teacherAccount.collegeId
|
|
roles = @('Teacher')
|
|
} | ConvertTo-Json
|
|
Invoke-RestMethod `
|
|
-Method Put `
|
|
-Uri "http://localhost:5255/api/users/$($teacherAccount.id)/roles" `
|
|
-Headers $headers `
|
|
-ContentType 'application/json' `
|
|
-Body $teacherAccessBody
|
|
|
|
$scopeScenarios = @(
|
|
@{
|
|
UserName = 'college'; Password = 'College@123456'; Scope = 'College'
|
|
Teachers = 2; Students = 3; Courses = 3
|
|
},
|
|
@{
|
|
UserName = 'counselor'; Password = 'Counselor@123456'; Scope = 'Class'
|
|
Teachers = 1; Students = 3; Courses = 1
|
|
},
|
|
@{
|
|
UserName = 'teacher'; Password = 'Teacher@123456'; Scope = 'Self'
|
|
Teachers = 1; Students = 3; Courses = 1
|
|
},
|
|
@{
|
|
UserName = 'student'; Password = 'Student@123456'; Scope = 'Self'
|
|
Teachers = 0; Students = 1; Courses = 1
|
|
}
|
|
)
|
|
$scopeChecks = foreach ($scenario in $scopeScenarios) {
|
|
$scenarioLoginBody = @{
|
|
userName = $scenario.UserName
|
|
password = $scenario.Password
|
|
} | ConvertTo-Json
|
|
$scenarioLogin = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:5255/api/auth/login' `
|
|
-ContentType 'application/json' `
|
|
-Body $scenarioLoginBody
|
|
$scenarioHeaders = @{ Authorization = "Bearer $($scenarioLogin.token)" }
|
|
$scenarioTeachers = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' `
|
|
-Headers $scenarioHeaders
|
|
$scenarioStudents = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' `
|
|
-Headers $scenarioHeaders
|
|
$scenarioCourses = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' `
|
|
-Headers $scenarioHeaders
|
|
if ($scenarioLogin.user.effectiveDataScope -ne $scenario.Scope -or
|
|
$scenarioTeachers.total -ne $scenario.Teachers -or
|
|
$scenarioStudents.total -ne $scenario.Students -or
|
|
$scenarioCourses.total -ne $scenario.Courses) {
|
|
throw ("Data-scope check failed for {0}: scope={1}, teachers={2}, students={3}, courses={4}." -f
|
|
$scenario.UserName,
|
|
$scenarioLogin.user.effectiveDataScope,
|
|
$scenarioTeachers.total,
|
|
$scenarioStudents.total,
|
|
$scenarioCourses.total)
|
|
}
|
|
"$($scenario.UserName):$($scenario.Scope)"
|
|
}
|
|
|
|
$studentLoginBody = @{
|
|
userName = 'student'
|
|
password = 'Student@123456'
|
|
} | ConvertTo-Json
|
|
$studentLogin = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:5255/api/auth/login' `
|
|
-ContentType 'application/json' `
|
|
-Body $studentLoginBody
|
|
$studentHeaders = @{ Authorization = "Bearer $($studentLogin.token)" }
|
|
$studentOptions = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/course-selections/student/options?roundId=$($activeSelectionRound.id)" `
|
|
-Headers $studentHeaders
|
|
$studentOffering = @($studentOptions.offerings) | Select-Object -First 1
|
|
if ($null -eq $studentOffering) {
|
|
throw 'Student has no eligible course-selection offering.'
|
|
}
|
|
if ($studentOffering.enrollmentStatus -ne 'Enrolled') {
|
|
$enrollmentBody = @{ offeringId = $studentOffering.id } | ConvertTo-Json
|
|
Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:5255/api/course-selections/student/enrollments' `
|
|
-Headers $studentHeaders `
|
|
-ContentType 'application/json' `
|
|
-Body $enrollmentBody |
|
|
Out-Null
|
|
}
|
|
$studentEnrollments = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/course-selections/student/enrollments?academicTermId=$($activeSelectionRound.academicTermId)" `
|
|
-Headers $studentHeaders
|
|
$activeEnrollments = @($studentEnrollments) |
|
|
Where-Object { $_.status -eq 'Enrolled' }
|
|
if ($activeEnrollments.Count -lt 1) {
|
|
throw 'Student course enrollment was not persisted.'
|
|
}
|
|
$selectionRoster = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/course-selections/offerings/$($studentOffering.id)/roster" `
|
|
-Headers $headers
|
|
if ($selectionRoster.enrolledCount -lt 1) {
|
|
throw 'Course-selection roster did not include the selected student.'
|
|
}
|
|
|
|
$gradeTasks = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/grades/sheets' `
|
|
-Headers $headers
|
|
$gradeTask = @($gradeTasks) |
|
|
Where-Object { $null -ne $_.sheet } |
|
|
Select-Object -First 1
|
|
if ($null -eq $gradeTask) {
|
|
throw 'Development grade sheet was not seeded.'
|
|
}
|
|
$gradeDetail = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/grades/sheets/$($gradeTask.sheet.id)" `
|
|
-Headers $headers
|
|
if (@($gradeDetail.sheet.records).Count -lt 1) {
|
|
throw 'Development grade sheet has no student records.'
|
|
}
|
|
|
|
$teacherLoginBody = @{
|
|
userName = 'teacher'
|
|
password = 'Teacher@123456'
|
|
} | ConvertTo-Json
|
|
$teacherLogin = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:5255/api/auth/login' `
|
|
-ContentType 'application/json' `
|
|
-Body $teacherLoginBody
|
|
$teacherHeaders = @{ Authorization = "Bearer $($teacherLogin.token)" }
|
|
$teacherGradeTasks = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/grades/sheets' `
|
|
-Headers $teacherHeaders
|
|
$teacherGradeTask = @($teacherGradeTasks) |
|
|
Where-Object { $_.id -eq $gradeTask.id } |
|
|
Select-Object -First 1
|
|
if ($null -eq $teacherGradeTask) {
|
|
throw 'Assigned teacher cannot access the grade sheet.'
|
|
}
|
|
if ($teacherGradeTask.sheet.status -eq 'Draft' -or
|
|
$teacherGradeTask.sheet.status -eq 'Returned') {
|
|
Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri "http://localhost:5255/api/grades/sheets/$($teacherGradeTask.sheet.id)/submit" `
|
|
-Headers $teacherHeaders |
|
|
Out-Null
|
|
}
|
|
|
|
$collegeLoginBody = @{
|
|
userName = 'college'
|
|
password = 'College@123456'
|
|
} | ConvertTo-Json
|
|
$collegeLogin = Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:5255/api/auth/login' `
|
|
-ContentType 'application/json' `
|
|
-Body $collegeLoginBody
|
|
$collegeHeaders = @{ Authorization = "Bearer $($collegeLogin.token)" }
|
|
$collegeGradeTasks = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/grades/sheets' `
|
|
-Headers $collegeHeaders
|
|
$collegeGradeTask = @($collegeGradeTasks) |
|
|
Where-Object { $_.id -eq $gradeTask.id } |
|
|
Select-Object -First 1
|
|
if ($collegeGradeTask.sheet.status -eq 'Submitted') {
|
|
Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri "http://localhost:5255/api/grades/sheets/$($collegeGradeTask.sheet.id)/approve" `
|
|
-Headers $collegeHeaders |
|
|
Out-Null
|
|
}
|
|
|
|
$adminGradeTasks = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/grades/sheets' `
|
|
-Headers $headers
|
|
$adminGradeTask = @($adminGradeTasks) |
|
|
Where-Object { $_.id -eq $gradeTask.id } |
|
|
Select-Object -First 1
|
|
if ($adminGradeTask.sheet.status -eq 'Approved') {
|
|
Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri "http://localhost:5255/api/grades/sheets/$($adminGradeTask.sheet.id)/publish" `
|
|
-Headers $headers |
|
|
Out-Null
|
|
}
|
|
$studentTranscript = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/grades/student/transcript' `
|
|
-Headers $studentHeaders
|
|
if (@($studentTranscript.records).Count -lt 1) {
|
|
throw 'Published grade is not visible in the student transcript.'
|
|
}
|
|
$examPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/exams/plans' -Headers $headers
|
|
if (@($examPlans).Count -lt 1) { throw 'Development exam plan was not seeded.' }
|
|
$examDetail = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/exams/plans/$($examPlans[0].id)" `
|
|
-Headers $headers
|
|
if (@($examDetail.sessions).Count -lt 1) { throw 'Exam plan has no sessions.' }
|
|
$examRoster = Invoke-RestMethod `
|
|
-Uri "http://localhost:5255/api/exams/sessions/$($examDetail.sessions[0].id)/roster" `
|
|
-Headers $headers
|
|
$studentExams = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/exams/my-schedule' `
|
|
-Headers $studentHeaders
|
|
$teacherExams = Invoke-RestMethod `
|
|
-Uri 'http://localhost:5255/api/exams/my-schedule' `
|
|
-Headers $teacherHeaders
|
|
if (@($studentExams).Count -lt 1 -or @($teacherExams).Count -lt 1) {
|
|
throw 'Personal exam schedule is not visible to student or invigilator.'
|
|
}
|
|
|
|
$frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5
|
|
$spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5
|
|
$unknownApiParameters = @{
|
|
Uri = 'http://localhost:5255/api/does-not-exist'
|
|
SkipHttpErrorCheck = $true
|
|
TimeoutSec = 5
|
|
}
|
|
$unknownApi = Invoke-WebRequest @unknownApiParameters
|
|
|
|
[pscustomobject]@{
|
|
Health = $health.status
|
|
Database = $health.database
|
|
User = $login.user.displayName
|
|
Term = $dashboard.currentTerm.name
|
|
Campuses = @($campuses).Count
|
|
CounselorAssignments = $assignedClassCount
|
|
Teachers = $teachers.total
|
|
Students = $students.total
|
|
Courses = $courses.total
|
|
Plans = $curriculumPlans.total
|
|
PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 }
|
|
TeachingTasks = $teachingTasks.total
|
|
Schedules = @($schedulePlans).Count
|
|
ScheduleEntries = if ($null -ne $scheduleDetail) { @($scheduleDetail.entries).Count } else { 0 }
|
|
SelectionRounds = @($selectionRounds).Count
|
|
SelectionOfferings = @($selectionOfferings).Count
|
|
StudentEnrollments = $activeEnrollments.Count
|
|
RosterStudents = $selectionRoster.enrolledCount
|
|
GradeSheets = @($gradeTasks).Count
|
|
GradeRecords = @($gradeDetail.sheet.records).Count
|
|
TranscriptRecords = @($studentTranscript.records).Count
|
|
ExamSessions = @($examDetail.sessions).Count
|
|
ExamRoster = @($examRoster.students).Count
|
|
AccessUpdate = $true
|
|
ScopeChecks = $scopeChecks -join ', '
|
|
StaticIndex = $frontend.Content.Contains('明序教务管理系统')
|
|
SpaFallback = $spaFallback.StatusCode
|
|
ApiNotFound = $unknownApi.StatusCode
|
|
} | Format-List
|
|
}
|
|
finally {
|
|
if (-not $process.HasExited) {
|
|
Stop-Process -Id $process.Id -Force
|
|
}
|
|
}
|