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.' } $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 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 } }