@@ -0,0 +1,560 @@
|
||||
# 冒烟测试执行脚本(HTTP 层逐条断言)
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\acceptance.ps1 -Base http://127.0.0.1:18090 -Db <测试库路径>
|
||||
param(
|
||||
[string]$Base = 'http://127.0.0.1:18090',
|
||||
[string]$Db = 'F:\hupeE的代肝平台\testrun\daili.db',
|
||||
[string]$Python = 'E:\Miniconda3\python.exe',
|
||||
[string]$Tools = 'F:\hupeE的代肝平台\scripts\dbtool.py',
|
||||
[string]$LogFile = 'F:\hupeE的代肝平台\testrun\acceptance-result.txt'
|
||||
)
|
||||
|
||||
try { Add-Type -AssemblyName System.Net.Http -ErrorAction Stop } catch { }
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$env:PYTHONIOENCODING = 'utf-8'
|
||||
$script:results = @()
|
||||
$script:fail = 0
|
||||
$T = (Get-Date).Date
|
||||
$today = $T.ToString('yyyy-MM-dd')
|
||||
|
||||
function D([int]$n) { $T.AddDays($n).ToString('yyyy-MM-dd') }
|
||||
|
||||
function New-Sess {
|
||||
$handler = New-Object System.Net.Http.HttpClientHandler
|
||||
$handler.AllowAutoRedirect = $true
|
||||
$handler.UseCookies = $true
|
||||
$handler.CookieContainer = New-Object System.Net.CookieContainer
|
||||
$client = New-Object System.Net.Http.HttpClient($handler)
|
||||
$client.Timeout = [TimeSpan]::FromSeconds(30)
|
||||
return $client
|
||||
}
|
||||
|
||||
function Send {
|
||||
param($Sess, [string]$Method, [string]$Path, $Body = $null, $Headers = $null)
|
||||
$req = New-Object System.Net.Http.HttpRequestMessage
|
||||
$req.RequestUri = [Uri]("$Base$Path")
|
||||
if ($Method -eq 'POST') { $req.Method = [System.Net.Http.HttpMethod]::Post }
|
||||
else { $req.Method = [System.Net.Http.HttpMethod]::Get }
|
||||
if ($null -ne $Body) {
|
||||
$pairs = New-Object 'System.Collections.Generic.List[System.Collections.Generic.KeyValuePair[string,string]]'
|
||||
foreach ($k in $Body.Keys) {
|
||||
$kv = New-Object 'System.Collections.Generic.KeyValuePair[string,string]'($k, [string]$Body[$k])
|
||||
$pairs.Add($kv)
|
||||
}
|
||||
$req.Content = [System.Net.Http.FormUrlEncodedContent]::new([System.Collections.Generic.IEnumerable[System.Collections.Generic.KeyValuePair[string,string]]]$pairs)
|
||||
}
|
||||
if ($null -ne $Headers) {
|
||||
foreach ($k in $Headers.Keys) { [void]$req.Headers.TryAddWithoutValidation($k, [string]$Headers[$k]) }
|
||||
}
|
||||
try {
|
||||
$resp = $Sess.SendAsync($req).Result
|
||||
$text = $resp.Content.ReadAsStringAsync().Result
|
||||
$final = $resp.RequestMessage.RequestUri.AbsolutePath
|
||||
$resp.Dispose()
|
||||
return @{ Status = [int]$resp.StatusCode; Content = $text; Path = $final }
|
||||
} catch {
|
||||
return @{ Status = 0; Content = ''; Path = ''; Err = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
function Check {
|
||||
param([string]$Id, [string]$Name, $Cond, [string]$Detail = '')
|
||||
$ok = ($Cond -eq $true)
|
||||
if ($ok) {
|
||||
Write-Host (" [PASS] {0} {1}" -f $Id, $Name) -ForegroundColor Green
|
||||
$script:results += [pscustomobject]@{ ID = $Id; Name = $Name; Result = 'PASS'; Detail = $Detail }
|
||||
} else {
|
||||
Write-Host (" [FAIL] {0} {1} {2}" -f $Id, $Name, $Detail) -ForegroundColor Red
|
||||
$script:results += [pscustomobject]@{ ID = $Id; Name = $Name; Result = 'FAIL'; Detail = $Detail }
|
||||
$script:fail++
|
||||
}
|
||||
}
|
||||
|
||||
function Has { param($Text, [string]$Pattern) return ([string]$Text) -match $Pattern }
|
||||
function Flash { param($Text) $m = [regex]::Match([string]$Text, '<div class="flash[^>]*>([^<]*)</div>'); return $m.Groups[1].Value }
|
||||
function SubIDs { param($Text) return @([regex]::Matches([string]$Text, 'id="sub-(\d+)"') | ForEach-Object { [int]$_.Groups[1].Value }) }
|
||||
function RowCount { param($Text) return [regex]::Matches([string]$Text, 'class="tail"').Count }
|
||||
function Dbtool { param([string[]]$ToolArgs) return (& $Python $Tools @ToolArgs) -join ' ' }
|
||||
|
||||
$admin = New-Sess
|
||||
$anon = New-Sess
|
||||
|
||||
Write-Host "`n=== 1. 启动与部署(S) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $anon 'GET' '/healthz'
|
||||
Check 'S-02' '健康检查 /healthz 返回 ok' ($r.Status -eq 200 -and $r.Content -match 'ok')
|
||||
|
||||
$r = Send $anon 'GET' '/'
|
||||
Check 'S-03' '首访跳 /setup' ($r.Path -eq '/setup' -and (Has $r.Content '创建第一个管理员')) "path=$($r.Path)"
|
||||
|
||||
$okStatic = $true; $detail = @()
|
||||
foreach ($p in '/static/app.css', '/static/screen.css', '/static/app.js', '/static/screen.js', '/static/favicon.svg') {
|
||||
$r = Send $anon 'GET' $p
|
||||
$okStatic = $okStatic -and ($r.Status -eq 200) -and ($r.Content.Length -gt 100)
|
||||
$detail += "$p=$($r.Status)/$($r.Content.Length)"
|
||||
}
|
||||
Check 'S-04' '静态资源可访问' $okStatic ($detail -join ' ')
|
||||
|
||||
Write-Host "`n=== 2. 账户与权限(A) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' '/setup' @{ username = 'ab'; display_name = 'x'; password = 'daili123'; password2 = 'daili123' }
|
||||
Check 'A-01' '用户名过短被拒' ($r.Status -eq 400 -and (Has $r.Content '用户名至少 3 个字符')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/setup' @{ username = 'admin'; display_name = '老板'; password = '123'; password2 = '123' }
|
||||
Check 'A-02' '密码过短被拒' ($r.Status -eq 400 -and (Has $r.Content '密码至少 6 位')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/setup' @{ username = 'admin'; display_name = '老板'; password = 'daili123'; password2 = 'daili999' }
|
||||
Check 'A-03' '两次密码不一致被拒' ($r.Status -eq 400 -and (Has $r.Content '两次输入的密码不一致')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/setup' @{ username = 'admin'; display_name = '老板'; password = 'daili123'; password2 = 'daili123' }
|
||||
Check 'A-04' '初始化管理员成功并自动登录' ($r.Status -eq 200 -and $r.Path -eq '/packages') "path=$($r.Path)"
|
||||
|
||||
Write-Host "`n=== 3. 套餐(P) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'GET' '/packages'
|
||||
Check 'P-01' '套餐空态提示' ((Has $r.Content '还没有套餐'))
|
||||
|
||||
$r = Send $admin 'POST' '/packages' @{ name = '包月日常'; unit = 'day'; default_amount = '30'; price = '300'; color = '#2F5D50'; note = '每天日常 + 周常'; sort_order = '1' }
|
||||
Check 'P-02' '新增按日套餐' ((Has $r.Content '套餐已创建')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' '/packages' @{ name = '深渊十次'; unit = 'count'; default_amount = '10'; price = '180'; color = '#6B4E9B'; note = '按次结算'; sort_order = '2' }
|
||||
Check 'P-03' '新增按次套餐' ((Has $r.Content '套餐已创建')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' '/packages' @{ name = ''; unit = 'day'; default_amount = '0'; color = ''; sort_order = '0' }
|
||||
Check 'P-04' '套餐必填校验' ($r.Status -eq 400 -and (Has $r.Content '套餐名称不能为空')) "status=$($r.Status)"
|
||||
|
||||
Write-Host "`n=== 4. 登录与会话(A) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' '/logout'
|
||||
$r = Send $admin 'GET' '/calendar'
|
||||
Check 'A-05' '登出后受保护页面跳登录' ($r.Path -like '/login*')
|
||||
|
||||
$r = Send $admin 'POST' '/login' @{ username = 'admin'; password = 'wrongpass'; next = '/calendar' }
|
||||
Check 'A-06' '错误口令被拒' ($r.Status -eq 400 -and (Has $r.Content '用户名或密码不正确')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/login' @{ username = 'admin'; password = 'daili123'; next = '/calendar' }
|
||||
Check 'A-04b' '重新登录成功' ($r.Path -eq '/calendar' -and (Has $r.Content '欢迎回来')) "path=$($r.Path)"
|
||||
|
||||
$screenPublic = ((Send $admin 'GET' '/settings').Content -match '(?s)大屏</dt>.*?免登录')
|
||||
$r = Send $anon 'GET' '/customers'
|
||||
$p1 = $r.Path
|
||||
$r = Send $anon 'GET' '/screen'
|
||||
$p2 = $r.Path
|
||||
$screenOk = if ($screenPublic) { $p2 -eq '/screen' } else { $p2 -like '/login*' }
|
||||
Check 'A-07' '未登录拦截后台页(大屏按配置)' (($p1 -like '/login*') -and $screenOk) "customers=$p1 screen=$p2 public=$screenPublic"
|
||||
|
||||
$r = Send $admin 'POST' '/packages' @{ name = '恶意套餐'; unit = 'day'; default_amount = '1'; color = '#2F5D50' } @{ Origin = 'http://evil.example' }
|
||||
Check 'A-12' '跨站 POST 被拒绝' ($r.Status -eq 403) "status=$($r.Status)"
|
||||
|
||||
Write-Host "`n=== 5. 客户(C) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '1001'; name = '张三'; game_name = '王者荣耀'; game_server = '微信区'; contact = 'wx:1001'; tags = '老客户,包月 老客户'; source = '抖音'; note = '主用例' }
|
||||
$custPath = $r.Path
|
||||
$custID = ($custPath -split '/')[-1]
|
||||
Check 'C-01' '新建客户成功' ($custPath -like '/customers/*') "path=$custPath"
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '1001'; name = '张三' }
|
||||
Check 'C-02' '尾号+姓名重复被拒' ($r.Status -eq 400 -and (Has $r.Content '已存在')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '1001'; name = '李四'; game_name = '王者荣耀' }
|
||||
$custLiID = ($r.Path -split '/')[-1]
|
||||
Check 'C-03' '同尾号不同姓名允许' ($r.Path -like '/customers/*') "path=$($r.Path)"
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = ''; name = '无尾号' }
|
||||
Check 'C-04' '尾号为空被拒' ($r.Status -eq 400 -and (Has $r.Content '手机尾号不能为空')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '123456789'; name = '超长尾号' }
|
||||
Check 'C-05' '尾号超长被拒' ($r.Status -eq 400 -and (Has $r.Content '最长 8 位')) "status=$($r.Status)"
|
||||
|
||||
$r = Send $admin 'GET' '/customers?q=1001'
|
||||
Check 'C-06' '尾号搜索命中两人' ((Has $r.Content '张三') -and (Has $r.Content '李四') -and (RowCount $r.Content) -eq 2) "rows=$(RowCount $r.Content)"
|
||||
|
||||
$r = Send $admin 'GET' '/customers?q=%E7%8E%8B%E8%80%85'
|
||||
Check 'C-07a' '按游戏名搜索' ((RowCount $r.Content) -ge 2) "rows=$(RowCount $r.Content)"
|
||||
$r = Send $admin 'GET' '/customers?q=wx%3A1001'
|
||||
Check 'C-07b' '按联系方式搜索' ((Has $r.Content '张三')) "rows=$(RowCount $r.Content)"
|
||||
$r = Send $admin 'GET' '/customers?q=%E6%9D%8E%E5%9B%9B'
|
||||
Check 'C-07c' '按姓名搜索' ((RowCount $r.Content) -eq 1) "rows=$(RowCount $r.Content)"
|
||||
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
$tagVal = Dbtool @('scalar', $Db, "SELECT tags FROM customers WHERE id=$custID")
|
||||
Check 'C-08' '标签归一化(逗号/空格/去重)' ($tagVal.Trim() -eq '老客户,包月') "tags=$tagVal"
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custID/edit" @{ tail = '1001'; name = '张三'; game_name = '王者荣耀'; game_server = '安卓微信区'; contact = 'wx:1001'; tags = '老客户,包月'; source = '抖音'; note = '已编辑' }
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
Check 'C-09' '编辑客户生效' ((Has $r.Content '安卓微信区') -and (Has $r.Content '已编辑')) "flash=$(Flash $r.Content)"
|
||||
|
||||
# 批量客户:用于分页
|
||||
for ($i = 1; $i -le 22; $i++) {
|
||||
$tail = 'P' + $i.ToString('000')
|
||||
Send $admin 'POST' '/customers' @{ tail = $tail; name = "分页客户$i" } | Out-Null
|
||||
}
|
||||
$r = Send $admin 'GET' '/customers'
|
||||
$total = [int][regex]::Match($r.Content, '共 (\d+) 位').Groups[1].Value
|
||||
$r2 = Send $admin 'GET' '/customers?page=2'
|
||||
$expect = [Math]::Min(20, $total - 20)
|
||||
Check 'C-10' '客户分页正常' (($r2.Status -eq 200) -and (RowCount $r2.Content) -eq $expect -and $total -ge 24) "total=$total page2=$(RowCount $r2.Content) expect=$expect"
|
||||
|
||||
Write-Host "`n=== 6. 订阅(U) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custID/subscriptions" @{ package_id = '1'; start_date = $today; days = '30'; note = '首单' }
|
||||
Check 'U-01' '开通按日订阅(结束日=T+29)' ((Has $r.Content '已开通')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
$p1ID = (SubIDs $r.Content)[0]
|
||||
$detail = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'U-01b' '按日剩余 30 天' ((Has $detail '剩 30 天')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custID/subscriptions" @{ package_id = '2'; start_date = $today; total_count = '10'; end_date = (D 10) }
|
||||
Check 'U-02' '开通按次订阅(10 次 / 有效期 T+10)' ((Has $r.Content '已开通')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
$p2ID = (SubIDs $r.Content)[1]
|
||||
Check 'U-02b' '按次剩余 10/10 次' ((Has $r.Content '剩 10 / 10 次')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custID/subscriptions" @{ package_id = '1'; start_date = $today; days = '30' }
|
||||
Check 'U-03' '重复订阅被拦截' ((Has $r.Content '已订阅此套餐')) "flash=$(Flash $r.Content)"
|
||||
|
||||
# P-05:新增套餐 D(id=3),验证既有订阅不受影响
|
||||
$before = (Send $admin 'GET' "/customers/$custID").Content
|
||||
$r = Send $admin 'POST' '/packages' @{ name = '副本五次'; unit = 'count'; default_amount = '5'; price = '120'; color = '#41556B'; note = '无有效期'; sort_order = '3' }
|
||||
Check 'P-05' '新增套餐 D 成功' ((Has $r.Content '套餐已创建')) ""
|
||||
$after = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'P-05b' '新增套餐后既有订阅剩余量不变' ((Has $after '剩 30 天') -and (Has $after '剩 10 / 10 次')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custID/subscriptions" @{ package_id = '3'; start_date = $today; total_count = '5' }
|
||||
Check 'U-04' '开通第三个套餐(无有效期按次)' ((Has $r.Content '已开通')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
$ids = SubIDs $r.Content
|
||||
$p3ID = $ids[2]
|
||||
Check 'U-04b' '三条订阅并存' ($ids.Count -eq 3) "count=$($ids.Count)"
|
||||
|
||||
$r = Send $admin 'GET' '/subscriptions?state=active'
|
||||
Check 'U-05a' '筛选:进行中' ((Has $r.Content '张三') -and (Has $r.Content '进行中')) ""
|
||||
$r = Send $admin 'GET' '/subscriptions?state=due7'
|
||||
Check 'U-05b' '筛选:7 天内到期' ($r.Status -eq 200) "status=$($r.Status)"
|
||||
$r = Send $admin 'GET' '/subscriptions?state=closed'
|
||||
Check 'U-05c' '筛选:已结束(应为空)' ($r.Status -eq 200 -and (Has $r.Content '没有订阅')) "flash=$(Has $r.Content '没有订阅')"
|
||||
$r = Send $admin 'GET' '/subscriptions?package_id=2'
|
||||
Check 'U-06' '按套餐筛选' ((Has $r.Content '张三') -and -not (Has $r.Content '阿豪')) ""
|
||||
|
||||
# 过期与未开始
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '9001'; name = '阿豪' }
|
||||
$custHaoID = ($r.Path -split '/')[-1]
|
||||
Send $admin 'POST' "/customers/$custHaoID/subscriptions" @{ package_id = '1'; start_date = (D -40); days = '30' } | Out-Null
|
||||
$r = Send $admin 'GET' "/customers/$custHaoID"
|
||||
$haoSubID = (SubIDs $r.Content)[0]
|
||||
Check 'U-07a' '已过期订阅显示「已到期」' ((Has $r.Content '已到期')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/customers/$custLiID/subscriptions" @{ package_id = '1'; start_date = (D 5); days = '30' } | Out-Null
|
||||
$r = Send $admin 'GET' "/customers/$custLiID"
|
||||
$liSubID = (SubIDs $r.Content)[0]
|
||||
Check 'U-07b' '未开始订阅显示「未开始」' ((Has $r.Content '未开始')) ""
|
||||
|
||||
Write-Host "`n=== 7. 套餐编辑与归档(P) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' '/packages/3/edit' @{ name = '副本五次(改)'; unit = 'count'; default_amount = '5'; price = '120'; color = '#41556B'; sort_order = '3' }
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
Check 'P-06' '编辑套餐后订阅同步显示新名称' ((Has $r.Content '副本五次\(改\)')) "flash=$(Flash $r.Content)"
|
||||
Send $admin 'POST' '/packages/3/edit' @{ name = '副本五次'; unit = 'count'; default_amount = '5'; price = '120'; color = '#41556B'; sort_order = '3' } | Out-Null
|
||||
|
||||
$r = Send $admin 'POST' '/packages/2/archive'
|
||||
Check 'P-07a' '归档套餐' ((Has $r.Content '已归档')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'POST' "/customers/$custLiID/subscriptions" @{ package_id = '2'; start_date = $today; total_count = '10' }
|
||||
Check 'P-07b' '归档后不能新建该套餐订阅' ((Has $r.Content '已归档,不能新建')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
Check 'P-08' '归档后已有订阅照常显示' ((Has $r.Content '深渊十次')) ""
|
||||
Send $admin 'POST' '/packages/2/archive' | Out-Null
|
||||
|
||||
Write-Host "`n=== 8. 追加时长(E) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p1ID/extend" @{ new_end_date = (D 39); note = '未到期续费'; back = "/customers/$custID" }
|
||||
Check 'E-01' '未到期追加:起点=T+30,追加 10 天' ($r -ne $null -and (Has $r.Content '已追加 10 天')) "flash=$(Flash $r.Content)"
|
||||
$detail = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'E-01b' '追加后剩余 40 天' ((Has $detail '剩 40 天')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$haoSubID/extend" @{ new_end_date = (D 29); note = '过期续费'; back = "/customers/$custHaoID" }
|
||||
Check 'E-02' '已到期追加:从今天起算 30 天' ((Has $r.Content '已追加 30 天')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p1ID/extend" @{ new_end_date = (D 10); back = "/customers/$custID" }
|
||||
$d2 = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'E-03' '早于计算起点被拒且数据不变' ((Has $r.Content '追加失败') -and (Has $r.Content '早于计算起点') -and (Has $d2 '剩 40 天')) "flash=$(Flash $r.Content)"
|
||||
|
||||
Check 'E-04' '追加留痕(起点/旧→新/天数/备注/操作人)' ((Has $detail '未到期续费') -and (Has $detail '追加天数') -and (Has $detail (D 30)) -and (Has $detail '老板')) ""
|
||||
Check 'E-05' '新建订阅有初始「开通」记录' ((Has $detail '开通') -and (Has $detail '\+30 天')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p1ID/pause" @{ back = "/customers/$custID" }
|
||||
$r = Send $admin 'POST' "/subscriptions/$p1ID/extend" @{ new_end_date = (D 49); note = '冻结期续费'; back = "/customers/$custID" }
|
||||
$d3 = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'E-06' '暂停中追加后回到进行中' ((Has $r.Content '已追加 10 天') -and -not (Has $d3 '暂停中')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$liSubID/close"
|
||||
$r = Send $admin 'POST' "/subscriptions/$liSubID/extend" @{ new_end_date = (D 40); back = "/customers/$custLiID" }
|
||||
Check 'E-07' '已结束订阅不可追加' ((Has $r.Content '请先「重新启用」')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/extend" @{ add_count = '5'; note = '加购 5 次'; back = "/customers/$custID" }
|
||||
$d4 = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'E-08' '按次追加次数' ((Has $r.Content '已追加 5 次') -and (Has $d4 '剩 15 / 15 次')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/extend" @{ add_count = '5'; new_end_opt = (D 30); note = '加次并延期'; back = "/customers/$custID" }
|
||||
$d5 = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'E-09' '按次追加可同时改有效期' ((Has $d5 ('有效期至 ' + (D 30)))) ""
|
||||
|
||||
$r = Send $admin 'GET' '/logs?q=%E8%BF%BD%E5%8A%A0'
|
||||
Check 'E-10' '操作日志可查追加记录' ((Has $r.Content '追加时长') -or (Has $r.Content '追加次数')) ""
|
||||
|
||||
Write-Host "`n=== 9. 暂停 / 恢复(G) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$haoSubID/pause" @{ back = "/customers/$custHaoID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custHaoID").Content
|
||||
Check 'G-01' '暂停后状态为暂停中' ((Has $r.Content '已暂停') -and (Has $d '暂停中')) "flash=$(Flash $r.Content)"
|
||||
|
||||
Dbtool @('backdate-pause', $Db, "$haoSubID", '5') | Out-Null
|
||||
$r = Send $admin 'POST' "/subscriptions/$haoSubID/resume" @{ back = "/customers/$custHaoID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custHaoID").Content
|
||||
Check 'G-02' '恢复后结束日顺延 5 天' ((Has $r.Content '顺延 5 天')) "flash=$(Flash $r.Content)"
|
||||
Check 'G-03' '暂停期间的 5 天被补回(剩余 35 天)' ((Has $d '剩 35 天')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$haoSubID/pause" @{ back = "/customers/$custHaoID" }
|
||||
$r = Send $admin 'POST' "/subscriptions/$haoSubID/pause" @{ back = "/customers/$custHaoID" }
|
||||
Check 'G-04' '重复暂停被拒' ((Has $r.Content '已处于暂停状态')) "flash=$(Flash $r.Content)"
|
||||
Send $admin 'POST' "/subscriptions/$haoSubID/resume" @{ back = "/customers/$custHaoID" } | Out-Null
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$liSubID/pause" @{ back = "/customers/$custLiID" }
|
||||
Check 'G-05' '已结束订阅不可暂停' ((Has $r.Content '无法暂停')) "flash=$(Flash $r.Content)"
|
||||
|
||||
# 专用客户:验证暂停后不出现在日历
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '7001'; name = '单测' }
|
||||
$custCalID = ($r.Path -split '/')[-1]
|
||||
Send $admin 'POST' "/customers/$custCalID/subscriptions" @{ package_id = '1'; start_date = $today; days = '30' } | Out-Null
|
||||
$r = Send $admin 'GET' "/customers/$custCalID"
|
||||
$calSubID = (SubIDs $r.Content)[0]
|
||||
$before = (Send $admin 'GET' "/calendar?d=$today").Content
|
||||
Send $admin 'POST' "/subscriptions/$calSubID/pause" @{ back = "/customers/$custCalID" } | Out-Null
|
||||
$after = (Send $admin 'GET' "/calendar?d=$today").Content
|
||||
$link = "/subscriptions/$calSubID/checkin"
|
||||
Check 'G-06' '暂停后不出现在日历待打清单' ((Has $before $link) -and (-not (Has $after $link))) "before=$(Has $before $link) after=$(Has $after $link)"
|
||||
Send $admin 'POST' "/subscriptions/$calSubID/resume" @{ back = "/customers/$custCalID" } | Out-Null
|
||||
|
||||
Write-Host "`n=== 10. 打卡(K) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$calSubID/checkin" @{ date = $today; status = 'done'; note = '今日已打'; back = "/calendar?d=$today" }
|
||||
$cal = (Send $admin 'GET' "/calendar?d=$today").Content
|
||||
Check 'K-01' '按日打卡生成记录并标记完成' ((Has $r.Content '已记录') -and (Has $cal '已完成') -and (-not (Has $cal ("/subscriptions/$calSubID/checkin")))) "flash=$(Flash $r.Content)"
|
||||
$d = (Send $admin 'GET' "/customers/$custCalID").Content
|
||||
Check 'K-02' '按日打卡不扣天数' ((Has $d '剩 30 天')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'K-03' '按次打卡扣 1 次' ((Has $d '剩 19 / 20 次')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'K-04' '同日重复打卡不重复扣次' ((Has $d '剩 19 / 20 次')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/checkin" @{ date = $today; status = 'skipped'; back = "/customers/$custID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'K-05' '改为跳过返还 1 次' ((Has $d '剩 20 / 20 次')) ""
|
||||
|
||||
Send $admin 'POST' "/subscriptions/$p2ID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" } | Out-Null
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
$ciID = (Dbtool @('scalar', $Db, "SELECT id FROM checkins WHERE sub_id=$p2ID AND service_date='$today'")).Trim()
|
||||
$r = Send $admin 'POST' "/checkins/$ciID/delete" @{ back = "/customers/$custID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'K-06' '撤销打卡返还次数' ((Has $r.Content '已撤销') -and (Has $d '剩 20 / 20 次')) "flash=$(Flash $r.Content)"
|
||||
|
||||
Dbtool @('exec', $Db, "UPDATE subscriptions SET used_count=total_count WHERE id=$p2ID") | Out-Null
|
||||
$r = Send $admin 'POST' "/subscriptions/$p2ID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
Check 'K-07' '次数用尽时打卡被拒' ((Has $r.Content '剩余次数不足')) "flash=$(Flash $r.Content)"
|
||||
Dbtool @('exec', $Db, "UPDATE subscriptions SET used_count=0 WHERE id=$p2ID") | Out-Null
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$liSubID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custLiID" }
|
||||
Check 'K-08' '已结束订阅不可打卡' ((Has $r.Content '订阅已结束,无法打卡')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$r = Send $admin 'GET' "/customers/$custID"
|
||||
Check 'K-11' '无有效期订阅卡片有「今日打卡」按钮' ((Has $r.Content "/subscriptions/$p3ID/checkin") -and (Has $r.Content '今日打卡')) ""
|
||||
|
||||
$r = Send $admin 'POST' "/subscriptions/$p3ID/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
$d = (Send $admin 'GET' "/customers/$custID").Content
|
||||
Check 'K-09' '无有效期按次订阅可打卡扣次' ((Has $d '剩 4 / 5 次')) "flash=$(Flash $r.Content)"
|
||||
Check 'K-10' '打卡历史含日期/备注/操作人' ((Has $d '今日已打') -or (Has $d '完成')) ""
|
||||
|
||||
Write-Host "`n=== 11. 日历(L) ===" -ForegroundColor Cyan
|
||||
|
||||
$cal = (Send $admin 'GET' '/calendar').Content
|
||||
Check 'L-01' '默认当月且今天选中' ((Has $cal '今天</span>') -and (Has $cal ($T.ToString('M 月 d 日')))) ""
|
||||
$r = Send $admin 'GET' '/calendar?y=2026&m=12'
|
||||
Check 'L-02a' '月份切换到 2026-12' ((Has $r.Content '2026 年 12 月')) ""
|
||||
$r = Send $admin 'GET' '/calendar?y=2027&m=1'
|
||||
Check 'L-02b' '跨年切换到 2027-01' ((Has $r.Content '2027 年 1 月')) ""
|
||||
$r = Send $admin 'GET' '/calendar?y=2020&m=1'
|
||||
Check 'L-03' '「回到今天」入口存在' ((Has $r.Content '回到今天')) ""
|
||||
$cal = (Send $admin 'GET' "/calendar?d=$today").Content
|
||||
Check 'L-04' '格子显示色签与负荷' ((Has $cal '单测') -and (Has $cal '包月') -and (Has $cal 'heat-')) ""
|
||||
Check 'L-05' '点日期切换抽屉' ((Has $cal ($T.ToString('M 月 d 日'))) -and (Has $cal '当日待打')) ""
|
||||
$r = Send $admin 'GET' "/calendar?d=$(D -1)"
|
||||
Check 'L-06' '过去日期标记已过去' ((Has $r.Content '已过去')) ""
|
||||
Check 'L-07' '近 7 天到期区块' ((Has $cal '近 7 天到期')) ""
|
||||
Check 'L-08' '无有效期按次订阅不进日历' (-not (Has $cal ("/subscriptions/$p3ID/checkin"))) ""
|
||||
$css = (Send $admin 'GET' '/static/app.css').Content
|
||||
Check 'L-09' '移动端样式规则存在' ((Has $css '@media \(max-width: 560px\)') -and (Has $cal 'viewport')) ""
|
||||
|
||||
Write-Host "`n=== 12. 大屏(D) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $anon 'GET' '/screen'
|
||||
$d1Ok = if ($screenPublic) { $r.Status -eq 200 -and $r.Path -eq '/screen' } else { $r.Path -like '/login*' }
|
||||
Check 'D-01' '大屏访问符合 screen_public 配置' $d1Ok "path=$($r.Path) public=$screenPublic"
|
||||
|
||||
$screen = (Send $admin 'GET' '/screen').Content
|
||||
$customers = (Send $admin 'GET' '/customers').Content
|
||||
$custN = [regex]::Match($customers, '共 (\d+) 位').Groups[1].Value
|
||||
$dueToday = [regex]::Match((Send $admin 'GET' '/subscriptions?state=due_today').Content, '共 (\d+) 条').Groups[1].Value
|
||||
$due7 = [regex]::Match((Send $admin 'GET' '/subscriptions?state=due7').Content, '共 (\d+) 条').Groups[1].Value
|
||||
$kpiCust = [regex]::Match($screen, '(?s)客户</span>\s*<b>(\d+)</b>').Groups[1].Value
|
||||
$kpiDue = [regex]::Match($screen, '(?s)今日到期</span>\s*<b>(\d+)</b>').Groups[1].Value
|
||||
$kpi7 = [regex]::Match($screen, '(?s)7 天内到期</span>\s*<b>(\d+)</b>').Groups[1].Value
|
||||
Check 'D-02' '四张 KPI 与后台一致' (($kpiCust -eq $custN) -and ($kpiDue -eq $dueToday) -and ($kpi7 -eq $due7)) "cust=$kpiCust/$custN due=$kpiDue/$dueToday 7=$kpi7/$due7"
|
||||
Check 'D-03' '热力月历渲染' ((Has $screen '到期热力') -and (Has $screen 'heat-cell')) ""
|
||||
$topN = [regex]::Matches($screen, 'class="top-item').Count
|
||||
Check 'D-04' 'TOP 榜不超过 10 条' ((Has $screen '即将到期 TOP 10') -and ($topN -ge 1) -and ($topN -le 10)) "count=$topN"
|
||||
$todayTasks = [regex]::Match($cal, '(\d+) 待打').Groups[1].Value
|
||||
$screenTasks = [regex]::Match($screen, '<span>(\d+) / (\d+) 已完成</span>').Groups[2].Value
|
||||
Check 'D-05' '今日待打数与日历一致' ($todayTasks -eq $screenTasks -and $todayTasks -ne '') "calendar=$todayTasks screen=$screenTasks"
|
||||
Check 'D-06' '最近追加流水渲染' ((Has $screen '最近追加')) ""
|
||||
Check 'D-07' '自动刷新秒数取自设置' ((Has $screen '<meta http-equiv="refresh" content="30">')) ""
|
||||
|
||||
Write-Host "`n=== 13. 日志 / 导出 / 备份(R) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'GET' '/logs'
|
||||
Check 'R-01' '日志列表倒序分页' ((Has $r.Content '共 ') -and (Has $r.Content '操作人')) ""
|
||||
$r = Send $admin 'GET' '/logs?q=%E6%89%93%E5%8D%A1'
|
||||
Check 'R-02' '日志搜索命中' ((Has $r.Content '打卡')) ""
|
||||
$r = Send $admin 'GET' '/logs'
|
||||
Check 'R-03' '关键动作均入库' ((Has $r.Content '追加时长') -and (Has $r.Content '暂停订阅') -and (Has $r.Content '打卡') -and (Has $r.Content '新建客户')) ""
|
||||
|
||||
$respCsv = $admin.GetAsync("$Base/export/customers.csv").Result
|
||||
$bytes = $respCsv.Content.ReadAsByteArrayAsync().Result
|
||||
$csv = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
$bom = ($bytes.Length -gt 2) -and ($bytes[0] -eq 0xEF) -and ($bytes[1] -eq 0xBB) -and ($bytes[2] -eq 0xBF)
|
||||
$lines = ($csv -split "`n").Count
|
||||
$custNow = [int][regex]::Match((Send $admin 'GET' '/customers').Content, '共 (\d+) 位').Groups[1].Value
|
||||
Check 'R-04' '客户 CSV:BOM + 表头 + 行数匹配' ($bom -and (Has $csv '手机尾号') -and ($lines -ge ($custNow + 1))) "bom=$bom lines=$lines customers=$custNow"
|
||||
$csv2 = (Send $admin 'GET' '/export/subscriptions.csv').Content
|
||||
Check 'R-05' '订阅 CSV 含剩余/类型/状态' ((Has $csv2 '剩余') -and (Has $csv2 '类型') -and (Has $csv2 '原文状态')) ""
|
||||
$csv3 = (Send $admin 'GET' '/export/logs.csv').Content
|
||||
Check 'R-06' '日志 CSV 含操作人/动作' ((Has $csv3 '操作人') -and (Has $csv3 '动作')) ""
|
||||
|
||||
$r = Send $admin 'POST' '/settings/backup'
|
||||
Check 'R-07' '手动备份生成文件' ((Has $r.Content '已备份')) "flash=$(Flash $r.Content)"
|
||||
$verify = (& $Python 'F:\hupeE的代肝平台\scripts\verify-backup.py' 'F:\hupeE的代肝平台\testrun\backup') -join ' '
|
||||
Check 'R-07b' '备份文件可被标准 SQLite 打开且有数据' (($verify -match 'OK') -and ($verify -match 'customers\s+(\d+)')) $verify
|
||||
|
||||
Send $admin 'POST' '/settings' @{ warn_days = '3'; screen_refresh = '30' } | Out-Null
|
||||
$keepShown = [int][regex]::Match((Send $admin 'GET' '/settings').Content, '保留 (\d+) 份').Groups[1].Value
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
Start-Sleep -Milliseconds 1200
|
||||
Send $admin 'POST' '/settings/backup' | Out-Null
|
||||
}
|
||||
$bk = @(Get-ChildItem 'F:\hupeE的代肝平台\testrun\backup' -Filter 'daili-*.db')
|
||||
$pruneOk = ($keepShown -ge 1) -and ($bk.Count -le $keepShown) -and (($keepShown -gt 3) -or ($bk.Count -eq $keepShown))
|
||||
Check 'R-08' "备份保留份数生效(配置 $keepShown 份)" $pruneOk "count=$($bk.Count) keep=$keepShown"
|
||||
|
||||
Write-Host "`n=== 14. 非功能 / 安全(N) ===" -ForegroundColor Cyan
|
||||
|
||||
function TimeOf([string]$path) {
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
Send $admin 'GET' $path | Out-Null
|
||||
$sw.Stop()
|
||||
return $sw.ElapsedMilliseconds
|
||||
}
|
||||
$tCal = @(1..3 | ForEach-Object { TimeOf "/calendar?d=$today" } | Measure-Object -Minimum).Minimum
|
||||
$tCust = @(1..3 | ForEach-Object { TimeOf '/customers' } | Measure-Object -Minimum).Minimum
|
||||
$tScr = @(1..3 | ForEach-Object { TimeOf '/screen' } | Measure-Object -Minimum).Minimum
|
||||
Check 'N-01' '页面响应时间达标' (($tCal -lt 150) -and ($tCust -lt 100) -and ($tScr -lt 100)) "calendar=${tCal}ms customers=${tCust}ms screen=${tScr}ms"
|
||||
|
||||
Write-Host " (造 300 条订阅后复测日历)" -ForegroundColor DarkGray
|
||||
Dbtool @('bulk', $Db, '300') | Out-Null
|
||||
$tBulk = @(1..3 | ForEach-Object { TimeOf "/calendar?d=$today" } | Measure-Object -Minimum).Minimum
|
||||
Check 'N-02' '300 条订阅下日历仍快' ($tBulk -lt 500) "calendar=${tBulk}ms"
|
||||
|
||||
$r = Send $admin 'POST' '/customers' @{ tail = '0002'; name = '<script>alert(1)</script>' }
|
||||
$xssID = ($r.Path -split '/')[-1]
|
||||
$xss = (Send $admin 'GET' "/customers/$xssID").Content
|
||||
Check 'N-03' 'XSS 内容被转义' ((-not (Has $xss '<script>alert\(1\)</script>')) -and (Has $xss '<script>')) ""
|
||||
|
||||
$r = Send $admin 'GET' '/customers/999999'
|
||||
Check 'N-04' '不存在的客户返回 404' ($r.Status -eq 404) "status=$($r.Status)"
|
||||
$r = Send $admin 'GET' '/nope'
|
||||
Check 'N-07' '未知路径返回 404' ($r.Status -eq 404) "status=$($r.Status)"
|
||||
|
||||
$hdr = (& curl.exe -s -i -X POST -d 'username=admin&password=daili123&next=/calendar' "$Base/login") -join "`n"
|
||||
$hasHttpOnly = $hdr -match 'Set-Cookie: daili_sid=[^;]+;[^\r\n]*HttpOnly'
|
||||
$hasSameSite = $hdr -match 'SameSite=Lax'
|
||||
Check 'N-05' '会话 Cookie 含 HttpOnly + SameSite' ($hasHttpOnly -and $hasSameSite) "HttpOnly=$hasHttpOnly SameSite=$hasSameSite"
|
||||
|
||||
Write-Host "`n=== 15. 账户收尾(A) ===" -ForegroundColor Cyan
|
||||
|
||||
$r = Send $admin 'POST' '/settings/users' @{ username = 'helper'; display_name = '小工'; password = 'helper123' }
|
||||
Check 'A-08a' '新增第二个管理员' ((Has $r.Content '已创建')) "flash=$(Flash $r.Content)"
|
||||
|
||||
$helper = New-Sess
|
||||
$r = Send $helper 'POST' '/login' @{ username = 'helper'; password = 'helper123'; next = '/customers' }
|
||||
$helperView = (Send $helper 'GET' '/customers').Content
|
||||
$helperN = [regex]::Match($helperView, '共 (\d+) 位').Groups[1].Value
|
||||
$adminView = (Send $admin 'GET' '/customers').Content
|
||||
$adminN = [regex]::Match($adminView, '共 (\d+) 位').Groups[1].Value
|
||||
Check 'A-08b' '多管理员共享同一套数据' ($helperN -eq $adminN -and $helperN -ne '') "helper=$helperN admin=$adminN"
|
||||
|
||||
$settingsHtml = (Send $admin 'GET' '/settings').Content
|
||||
$toggles = [regex]::Matches($settingsHtml, '/settings/users/\d+/toggle').Count
|
||||
Check 'A-10' '不能停用自己的账户' ((Has $settingsHtml '当前账户') -and $toggles -eq 1) "toggles=$toggles"
|
||||
|
||||
$helperID = [regex]::Match($settingsHtml, '/settings/users/(\d+)/toggle').Groups[1].Value
|
||||
$r = Send $admin 'POST' "/settings/users/$helperID/toggle"
|
||||
Check 'A-09a' '停用账户' ((Has $r.Content '停用账户成功')) "flash=$(Flash $r.Content)"
|
||||
$helper2 = New-Sess
|
||||
$r = Send $helper2 'POST' '/login' @{ username = 'helper'; password = 'helper123'; next = '/calendar' }
|
||||
Check 'A-09b' '停用后无法登录' ($r.Status -eq 400 -and (Has $r.Content '用户名或密码不正确')) "status=$($r.Status)"
|
||||
Send $admin 'POST' "/settings/users/$helperID/toggle" | Out-Null
|
||||
|
||||
$r = Send $admin 'POST' '/settings/password' @{ old_password = 'nope'; password = 'daili456'; password2 = 'daili456' }
|
||||
Check 'A-11a' '原密码错误被拒' ((Has $r.Content '原密码不正确')) "flash=$(Flash $r.Content)"
|
||||
$r = Send $admin 'POST' '/settings/password' @{ old_password = 'daili123'; password = 'daili456'; password2 = 'daili456' }
|
||||
Check 'A-11b' '修改密码成功' ((Has $r.Content '密码已更新')) "flash=$(Flash $r.Content)"
|
||||
$old = New-Sess
|
||||
$r = Send $old 'POST' '/login' @{ username = 'admin'; password = 'daili123'; next = '/calendar' }
|
||||
$oldFail = ($r.Status -eq 400 -and (Has $r.Content '用户名或密码不正确'))
|
||||
$new = New-Sess
|
||||
$r = Send $new 'POST' '/login' @{ username = 'admin'; password = 'daili456'; next = '/calendar' }
|
||||
Check 'A-11c' '旧密码失效、新密码可用' ($oldFail -and $r.Path -eq '/calendar') "oldFail=$oldFail path=$($r.Path)"
|
||||
|
||||
# 回滚口令,避免影响后续人工验证
|
||||
Send $admin 'POST' '/settings/password' @{ old_password = 'daili456'; password = 'daili123'; password2 = 'daili123' } | Out-Null
|
||||
|
||||
Write-Host "`n=== 16. 配置文件(CF) ===" -ForegroundColor Cyan
|
||||
|
||||
$settingsHtml = (Send $admin 'GET' '/settings').Content
|
||||
Check 'CF-01' '设置页展示配置文件路径' ((Has $settingsHtml 'daili.ini')) ""
|
||||
Check 'CF-02' '设置页展示备份目录与保留份数' ((Has $settingsHtml '备份') -and (Has $settingsHtml '保留')) ""
|
||||
$anon2 = New-Sess
|
||||
$r = Send $anon2 'GET' '/screen'
|
||||
Check 'CF-03' '大屏免登录(screen_public = true)' ($r.Status -eq 200 -and $r.Path -eq '/screen') "status=$($r.Status) path=$($r.Path)"
|
||||
$logs = @(Get-ChildItem 'F:\hupeE的代肝平台\testrun\logs' -Filter '*.log' -ErrorAction SilentlyContinue)
|
||||
$logText = ''
|
||||
if ($logs.Count -ge 1) { $logText = Get-Content $logs[0].FullName -Raw -Encoding UTF8 }
|
||||
Check 'CF-04' '运行日志落盘' ($logs.Count -ge 1 -and $logText.Length -gt 20) "files=$($logs.Count) bytes=$($logText.Length)"
|
||||
Write-Host "`n=== 汇总 ===" -ForegroundColor Cyan
|
||||
$pass = @($script:results | Where-Object { $_.Result -eq 'PASS' }).Count
|
||||
$fail = @($script:results | Where-Object { $_.Result -eq 'FAIL' }).Count
|
||||
Write-Host (" 通过 {0} 项,失败 {1} 项,共 {2} 项" -f $pass, $fail, $script:results.Count) -ForegroundColor ($(if ($fail -eq 0) { 'Green' } else { 'Red' }))
|
||||
|
||||
$script:results | ForEach-Object { "{0}|{1}|{2}|{3}" -f $_.ID, $_.Result, $_.Name, $_.Detail } |
|
||||
Set-Content -Path $LogFile -Encoding UTF8
|
||||
Write-Host (" 结果已写入 {0}" -f $LogFile) -ForegroundColor DarkGray
|
||||
|
||||
if ($fail -gt 0) {
|
||||
Write-Host "`n失败明细:" -ForegroundColor Red
|
||||
$script:results | Where-Object { $_.Result -eq 'FAIL' } | ForEach-Object {
|
||||
Write-Host (" {0} {1} {2}" -f $_.ID, $_.Name, $_.Detail) -ForegroundColor Red
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,233 @@
|
||||
# 配置文件专项测试:验证 daili.ini 的生成、解析、覆盖与各项生效
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\config-test.ps1
|
||||
param(
|
||||
[string]$Exe = 'F:\hupeE的代肝平台\daili.exe',
|
||||
[string]$Work = 'F:\hupeE的代肝平台\cfgtest-run',
|
||||
[string]$LanIp = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
|
||||
$script:fail = 0
|
||||
$script:results = @()
|
||||
function Check {
|
||||
param([string]$Id, [string]$Name, $Cond, [string]$Detail = '')
|
||||
if ($Cond -eq $true) {
|
||||
Write-Host (" [PASS] {0} {1}" -f $Id, $Name) -ForegroundColor Green
|
||||
$script:results += "PASS|$Id|$Name|$Detail"
|
||||
} else {
|
||||
Write-Host (" [FAIL] {0} {1} {2}" -f $Id, $Name, $Detail) -ForegroundColor Red
|
||||
$script:results += "FAIL|$Id|$Name|$Detail"
|
||||
$script:fail++
|
||||
}
|
||||
}
|
||||
|
||||
function New-Client {
|
||||
$h = New-Object System.Net.Http.HttpClientHandler
|
||||
$h.UseCookies = $true
|
||||
$h.AllowAutoRedirect = $true
|
||||
$c = New-Object System.Net.Http.HttpClient($h)
|
||||
$c.Timeout = [TimeSpan]::FromSeconds(10)
|
||||
return $c
|
||||
}
|
||||
|
||||
function Post-Form($client, $url, $kv) {
|
||||
$pairs = New-Object 'System.Collections.Generic.List[System.Collections.Generic.KeyValuePair[string,string]]'
|
||||
foreach ($k in $kv.Keys) {
|
||||
$pairs.Add([System.Collections.Generic.KeyValuePair[string,string]]::new($k, [string]$kv[$k]))
|
||||
}
|
||||
return $client.PostAsync($url, [System.Net.Http.FormUrlEncodedContent]::new($pairs)).Result
|
||||
}
|
||||
|
||||
function Get-Status($client, $url) {
|
||||
try {
|
||||
$r = $client.GetAsync($url).Result
|
||||
return @{ Code = [int]$r.StatusCode; Path = $r.RequestMessage.RequestUri.AbsolutePath; Body = $r.Content.ReadAsStringAsync().Result }
|
||||
} catch {
|
||||
return @{ Code = 0; Path = ''; Body = '' }
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-Up([int]$port, [int]$seconds = 15) {
|
||||
$c = New-Client
|
||||
for ($i = 0; $i -lt ($seconds * 4); $i++) {
|
||||
$r = Get-Status $c "http://127.0.0.1:$port/healthz"
|
||||
if ($r.Code -eq 200) { return $true }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Start-App([string]$dir, [string[]]$extra, [int]$port) {
|
||||
$out = Join-Path $dir 'stdout.log'
|
||||
$err = Join-Path $dir 'stderr.log'
|
||||
$p = Start-Process -FilePath (Join-Path $dir 'daili.exe') -ArgumentList $extra -WorkingDirectory $dir `
|
||||
-WindowStyle Hidden -PassThru -RedirectStandardOutput $out -RedirectStandardError $err
|
||||
$ok = Wait-Up $port
|
||||
return @{ Proc = $p; Out = $out; Err = $err; Up = $ok }
|
||||
}
|
||||
|
||||
function Stop-App($h) {
|
||||
if ($h -and $h.Proc) { Stop-Process -Id $h.Proc.Id -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Milliseconds 600
|
||||
}
|
||||
|
||||
function New-CaseDir([string]$name) {
|
||||
$dir = Join-Path $Work $name
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
Copy-Item $Exe (Join-Path $dir 'daili.exe') -Force
|
||||
return $dir
|
||||
}
|
||||
|
||||
function Write-Conf([string]$dir, [string]$text) {
|
||||
[System.IO.File]::WriteAllText((Join-Path $dir 'daili.ini'), $text, (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
if (-not $LanIp) {
|
||||
$LanIp = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.PrefixOrigin -ne 'WellKnown' } |
|
||||
Select-Object -First 1 -ExpandProperty IPAddress)
|
||||
}
|
||||
if (-not $LanIp) { $LanIp = '192.168.1.1' }
|
||||
|
||||
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Work | Out-Null
|
||||
|
||||
Write-Host "`n=== 配置文件专项测试(LAN IP: $LanIp) ===" -ForegroundColor Cyan
|
||||
$c = New-Client
|
||||
|
||||
# ---------- CFG-01 / CFG-02:首启生成默认配置 + 默认端口 17777 ----------
|
||||
Write-Host "`n-- CFG-01/02 首次启动生成默认配置 --" -ForegroundColor Cyan
|
||||
$d1 = New-CaseDir 'default'
|
||||
$h = Start-App $d1 @('-no-open') 17777
|
||||
$ini = Join-Path $d1 'daili.ini'
|
||||
Check 'CFG-01a' '首次启动自动生成 daili.ini' (Test-Path $ini)
|
||||
$iniText = Get-Content $ini -Raw -Encoding UTF8
|
||||
Check 'CFG-01b' '默认配置含注释与默认监听' (($iniText -match '代打台 配置文件') -and ($iniText -match 'listen_ip = 0\.0\.0\.0') -and ($iniText -match 'listen_port = 17777') -and ($iniText -match 'screen_public = true'))
|
||||
$r = Get-Status $c 'http://127.0.0.1:17777/healthz'
|
||||
Check 'CFG-02' '默认监听 17777 可用' ($h.Up -and $r.Code -eq 200) "health=$($r.Code)"
|
||||
Stop-App $h
|
||||
$logDir = Join-Path $d1 'logs'
|
||||
$logs = @(Get-ChildItem $logDir -Filter 'daili-*.log' -ErrorAction SilentlyContinue)
|
||||
$logText = ''
|
||||
if ($logs.Count -ge 1) { $logText = Get-Content $logs[0].FullName -Raw -Encoding UTF8 }
|
||||
Check 'CFG-07' '运行日志按天落盘' ($logs.Count -ge 1 -and $logText.Length -gt 20) "files=$($logs.Count) bytes=$($logText.Length)"
|
||||
|
||||
# ---------- CFG-03:只监听本机 ----------
|
||||
Write-Host "`n-- CFG-03 仅本机监听 --" -ForegroundColor Cyan
|
||||
$d2 = New-CaseDir 'loopback'
|
||||
Write-Conf $d2 @"
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 17901
|
||||
open_browser = false
|
||||
log_enabled = false
|
||||
backup_enabled = false
|
||||
"@
|
||||
$h = Start-App $d2 @('-no-open') 17901
|
||||
$local = (Test-NetConnection -ComputerName 127.0.0.1 -Port 17901 -InformationLevel Quiet -WarningAction SilentlyContinue)
|
||||
$lan = (Test-NetConnection -ComputerName $LanIp -Port 17901 -InformationLevel Quiet -WarningAction SilentlyContinue)
|
||||
Check 'CFG-03' '127.0.0.1 只开本机,局域网不可连' ($h.Up -and $local -and -not $lan) "local=$local lan=$lan"
|
||||
$banner = Get-Content (Join-Path $d2 'stdout.log') -Raw -Encoding UTF8
|
||||
Check 'CFG-03b' '横幅不再打印局域网地址' (-not ($banner -match '局域网')) ""
|
||||
Stop-App $h
|
||||
|
||||
# ---------- CFG-04:非法配置报错退出且不改文件 ----------
|
||||
Write-Host "`n-- CFG-04 非法配置 --" -ForegroundColor Cyan
|
||||
$d3 = New-CaseDir 'invalid'
|
||||
Write-Conf $d3 "# 故意写错`nlisten_ip = 0.0.0.0`nlisten_port = 99999`n"
|
||||
$badIni = Join-Path $d3 'daili.ini'
|
||||
$hashBefore = (Get-FileHash $badIni -Algorithm SHA256).Hash
|
||||
$outText = & (Join-Path $d3 'daili.exe') -no-open 2>&1 | Out-String
|
||||
$code = $LASTEXITCODE
|
||||
$hashAfter = (Get-FileHash $badIni -Algorithm SHA256).Hash
|
||||
Check 'CFG-04a' '非法配置退出码为 1' ($code -eq 1) "code=$code"
|
||||
Check 'CFG-04b' '错误信息指出行号' ($outText -match '第 3 行' -and $outText -match '1-65535') ""
|
||||
Check 'CFG-04c' '原配置文件未被改动' ($hashBefore -eq $hashAfter) ""
|
||||
|
||||
# ---------- CFG-05:命令行覆盖配置 ----------
|
||||
Write-Host "`n-- CFG-05 命令行覆盖 --" -ForegroundColor Cyan
|
||||
$d4 = New-CaseDir 'override'
|
||||
Write-Conf $d4 @"
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 17902
|
||||
open_browser = false
|
||||
log_enabled = false
|
||||
backup_enabled = false
|
||||
"@
|
||||
$h = Start-App $d4 @('-no-open', '-addr', '127.0.0.1:17903') 17903
|
||||
$p17902 = (Test-NetConnection -ComputerName 127.0.0.1 -Port 17902 -InformationLevel Quiet -WarningAction SilentlyContinue)
|
||||
Check 'CFG-05' '-addr 覆盖配置文件端口' ($h.Up -and -not $p17902) "up17903=$($h.Up) up17902=$p17902"
|
||||
Stop-App $h
|
||||
|
||||
# ---------- CFG-06:大屏鉴权开关 ----------
|
||||
Write-Host "`n-- CFG-06 大屏访问控制 --" -ForegroundColor Cyan
|
||||
$d5 = New-CaseDir 'screen-private'
|
||||
Write-Conf $d5 @"
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 17904
|
||||
open_browser = false
|
||||
screen_public = false
|
||||
log_enabled = false
|
||||
backup_enabled = false
|
||||
"@
|
||||
$h = Start-App $d5 @('-no-open') 17904
|
||||
$anon = New-Client
|
||||
$r = Get-Status $anon 'http://127.0.0.1:17904/screen'
|
||||
Check 'CFG-06' 'screen_public=false 时大屏需登录' (($r.Path -like '/login*') -or ($r.Path -eq '/setup')) "path=$($r.Path)"
|
||||
Stop-App $h
|
||||
|
||||
# ---------- CFG-08:备份目录与保留份数来自配置 ----------
|
||||
Write-Host "`n-- CFG-08 备份目录与保留份数 --" -ForegroundColor Cyan
|
||||
$d6 = New-CaseDir 'backup'
|
||||
$bdir = Join-Path $d6 'my-backup'
|
||||
Write-Conf $d6 @"
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 17905
|
||||
open_browser = false
|
||||
log_enabled = false
|
||||
backup_enabled = true
|
||||
backup_dir = $bdir
|
||||
backup_keep = 1
|
||||
"@
|
||||
$h = Start-App $d6 @('-no-open') 17905
|
||||
$c6 = New-Client
|
||||
Post-Form $c6 'http://127.0.0.1:17905/setup' @{ username = 'admin'; display_name = '老板'; password = 'daili123'; password2 = 'daili123' } | Out-Null
|
||||
Post-Form $c6 'http://127.0.0.1:17905/packages' @{ name = '包月日常'; unit = 'day'; default_amount = '30'; color = '#2F5D50'; sort_order = '1' } | Out-Null
|
||||
Post-Form $c6 'http://127.0.0.1:17905/customers' @{ tail = '3001'; name = '备份客户'; game_name = ''; game_server = ''; contact = ''; tags = ''; source = ''; note = '' } | Out-Null
|
||||
Start-Sleep -Milliseconds 1200
|
||||
Post-Form $c6 'http://127.0.0.1:17905/settings/backup' @{} | Out-Null
|
||||
Start-Sleep -Milliseconds 1200
|
||||
Post-Form $c6 'http://127.0.0.1:17905/settings/backup' @{} | Out-Null
|
||||
$files = @(Get-ChildItem $bdir -Filter '*.db' -ErrorAction SilentlyContinue)
|
||||
Check 'CFG-08a' '备份写入配置指定的目录' ($files.Count -ge 1) "dir=$bdir files=$($files.Count)"
|
||||
Check 'CFG-08b' 'backup_keep=1 时只保留 1 份' ($files.Count -eq 1) "files=$($files.Count)"
|
||||
Stop-App $h
|
||||
|
||||
# ---------- CFG-09:时区固定 ----------
|
||||
Write-Host "`n-- CFG-09 时区固定 --" -ForegroundColor Cyan
|
||||
$d7 = New-CaseDir 'timezone'
|
||||
Write-Conf $d7 @"
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 17906
|
||||
open_browser = false
|
||||
log_enabled = true
|
||||
backup_enabled = false
|
||||
timezone = Asia/Shanghai
|
||||
"@
|
||||
$h = Start-App $d7 @('-no-open') 17906
|
||||
$errText = Get-Content (Join-Path $d7 'stderr.log') -Raw -Encoding UTF8
|
||||
Check 'CFG-09' '时区固定为 Asia/Shanghai 生效' ($errText -match '时区已固定为 Asia/Shanghai') ""
|
||||
Stop-App $h
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
$pass = @($script:results | Where-Object { $_ -like 'PASS|*' }).Count
|
||||
$fail = @($script:results | Where-Object { $_ -like 'FAIL|*' }).Count
|
||||
Write-Host "`n=== 汇总:通过 $pass 项,失败 $fail 项 ===" -ForegroundColor ($(if ($fail -eq 0) { 'Green' } else { 'Red' }))
|
||||
New-Item -ItemType Directory -Force -Path 'F:\hupeE的代肝平台\test-evidence' | Out-Null
|
||||
$script:results | Set-Content 'F:\hupeE的代肝平台\test-evidence\config-result.txt' -Encoding UTF8
|
||||
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
||||
if ($fail -gt 0) { exit 1 }
|
||||
exit 0
|
||||
@@ -0,0 +1,94 @@
|
||||
"""测试辅助:直接读写测试库,用于构造 HTTP 层做不到的前置条件。
|
||||
|
||||
用法:
|
||||
python dbtool.py backdate-pause <db> <sub_id> <days> 把暂停开始时间提前 N 天
|
||||
python dbtool.py bulk <db> <count> 批量造客户+按日订阅(包月日常)
|
||||
python dbtool.py exec <db> <sql> 执行一条 SQL
|
||||
python dbtool.py scalar <db> <sql> 输出第一条结果的第一列
|
||||
python dbtool.py integrity <db> 完整性检查
|
||||
"""
|
||||
import sqlite3
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
|
||||
def connect(path):
|
||||
con = sqlite3.connect(path, timeout=10)
|
||||
con.execute("PRAGMA busy_timeout=10000")
|
||||
con.execute("PRAGMA foreign_keys=ON")
|
||||
return con
|
||||
|
||||
|
||||
def backdate_pause(db, sub_id, days):
|
||||
con = connect(db)
|
||||
now = datetime.datetime.now() - datetime.timedelta(days=int(days))
|
||||
stamp = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
cur = con.execute(
|
||||
"UPDATE sub_pauses SET start_at=? WHERE sub_id=? AND end_at IS NULL",
|
||||
(stamp, int(sub_id)))
|
||||
con.commit()
|
||||
print("updated rows:", cur.rowcount, "start_at:", stamp)
|
||||
con.close()
|
||||
|
||||
|
||||
def bulk(db, count):
|
||||
con = connect(db)
|
||||
today = datetime.date.today()
|
||||
start = (today - datetime.timedelta(days=5)).isoformat()
|
||||
end = (today + datetime.timedelta(days=25)).isoformat()
|
||||
stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
pkg = con.execute("SELECT id FROM packages WHERE unit='day' ORDER BY id LIMIT 1").fetchone()
|
||||
if not pkg:
|
||||
print("FAIL: 没有按日套餐")
|
||||
sys.exit(1)
|
||||
pkg_id = pkg[0]
|
||||
created = 0
|
||||
for i in range(int(count)):
|
||||
tail = "B%04d" % i
|
||||
con.execute(
|
||||
"INSERT OR IGNORE INTO customers(tail,name,created_at,updated_at) VALUES(?,?,?,?)",
|
||||
(tail, "批量客户%d" % i, stamp, stamp))
|
||||
row = con.execute("SELECT id FROM customers WHERE tail=?", (tail,)).fetchone()
|
||||
cid = row[0]
|
||||
con.execute(
|
||||
"INSERT OR IGNORE INTO subscriptions(customer_id,package_id,unit,status,start_date,end_date,"
|
||||
"total_count,used_count,created_at,updated_at) VALUES(?,?,'day','active',?,?,0,0,?,?)",
|
||||
(cid, pkg_id, start, end, stamp, stamp))
|
||||
created += 1
|
||||
con.commit()
|
||||
total = con.execute("SELECT COUNT(*) FROM subscriptions").fetchone()[0]
|
||||
print("bulk customers:", created, "subscriptions total:", total)
|
||||
con.close()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
cmd = sys.argv[1]
|
||||
db = sys.argv[2]
|
||||
if cmd == "backdate-pause":
|
||||
backdate_pause(db, sys.argv[3], sys.argv[4])
|
||||
elif cmd == "bulk":
|
||||
bulk(db, sys.argv[3])
|
||||
elif cmd == "exec":
|
||||
con = connect(db)
|
||||
cur = con.execute(sys.argv[3])
|
||||
con.commit()
|
||||
print("rows:", cur.rowcount)
|
||||
con.close()
|
||||
elif cmd == "scalar":
|
||||
con = connect(db)
|
||||
print(con.execute(sys.argv[3]).fetchone()[0])
|
||||
con.close()
|
||||
elif cmd == "integrity":
|
||||
con = connect(db)
|
||||
print(con.execute("PRAGMA integrity_check").fetchone()[0])
|
||||
con.close()
|
||||
else:
|
||||
print("未知命令:", cmd)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
# 数据可移植性测试:单文件、只拷 daili.db 无损迁移、删库即重置
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\portability-test.ps1
|
||||
param(
|
||||
[string]$Exe = 'F:\hupeE的代肝平台\daili.exe',
|
||||
[string]$Work = 'F:\hupeE的代肝平台\portable-run',
|
||||
[int]$PortA = 17781,
|
||||
[int]$PortB = 17782
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
|
||||
$script:fail = 0
|
||||
$script:results = @()
|
||||
function Check {
|
||||
param([string]$Id, [string]$Name, $Cond, [string]$Detail = '')
|
||||
if ($Cond -eq $true) {
|
||||
Write-Host (" [PASS] {0} {1}" -f $Id, $Name) -ForegroundColor Green
|
||||
$script:results += "PASS|$Id|$Name|$Detail"
|
||||
} else {
|
||||
Write-Host (" [FAIL] {0} {1} {2}" -f $Id, $Name, $Detail) -ForegroundColor Red
|
||||
$script:results += "FAIL|$Id|$Name|$Detail"
|
||||
$script:fail++
|
||||
}
|
||||
}
|
||||
|
||||
function New-Client {
|
||||
$h = New-Object System.Net.Http.HttpClientHandler
|
||||
$h.UseCookies = $true
|
||||
$h.AllowAutoRedirect = $true
|
||||
$c = New-Object System.Net.Http.HttpClient($h)
|
||||
$c.Timeout = [TimeSpan]::FromSeconds(10)
|
||||
return $c
|
||||
}
|
||||
|
||||
function Post-Form($client, $url, $kv) {
|
||||
$pairs = New-Object 'System.Collections.Generic.List[System.Collections.Generic.KeyValuePair[string,string]]'
|
||||
foreach ($k in $kv.Keys) { $pairs.Add([System.Collections.Generic.KeyValuePair[string,string]]::new($k, [string]$kv[$k])) }
|
||||
return $client.PostAsync($url, [System.Net.Http.FormUrlEncodedContent]::new($pairs)).Result
|
||||
}
|
||||
|
||||
function Get-Page($client, $url) {
|
||||
try {
|
||||
$r = $client.GetAsync($url).Result
|
||||
return @{ Code = [int]$r.StatusCode; Path = $r.RequestMessage.RequestUri.AbsolutePath; Body = $r.Content.ReadAsStringAsync().Result }
|
||||
} catch { return @{ Code = 0; Path = ''; Body = '' } }
|
||||
}
|
||||
|
||||
function Wait-Up([int]$port, [int]$seconds = 15) {
|
||||
$c = New-Client
|
||||
for ($i = 0; $i -lt ($seconds * 4); $i++) {
|
||||
if ((Get-Page $c "http://127.0.0.1:$port/healthz").Code -eq 200) { return $true }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Start-App([string]$dir, [int]$port) {
|
||||
$p = Start-Process -FilePath (Join-Path $dir 'daili.exe') -ArgumentList @('-addr', "127.0.0.1:$port", '-no-open') `
|
||||
-WorkingDirectory $dir -WindowStyle Hidden -PassThru `
|
||||
-RedirectStandardOutput (Join-Path $dir 'stdout.log') -RedirectStandardError (Join-Path $dir 'stderr.log')
|
||||
return @{ Proc = $p; Up = (Wait-Up $port) }
|
||||
}
|
||||
|
||||
function Stop-App($h) {
|
||||
if ($h -and $h.Proc) { Stop-Process -Id $h.Proc.Id -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Milliseconds 700
|
||||
}
|
||||
|
||||
function New-AppDir([string]$name) {
|
||||
$dir = Join-Path $Work $name
|
||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
Copy-Item $Exe (Join-Path $dir 'daili.exe') -Force
|
||||
return $dir
|
||||
}
|
||||
|
||||
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $Work | Out-Null
|
||||
$today = (Get-Date).Date.ToString('yyyy-MM-dd')
|
||||
$newEnd = (Get-Date).Date.AddDays(45).ToString('yyyy-MM-dd')
|
||||
|
||||
Write-Host "`n=== 数据可移植性测试 ===" -ForegroundColor Cyan
|
||||
|
||||
# ---------- 1. A 目录造数据 ----------
|
||||
Write-Host "`n-- 1. 在 A 目录造数据 --" -ForegroundColor Cyan
|
||||
$dirA = New-AppDir 'A'
|
||||
$hA = Start-App $dirA $PortA
|
||||
Check 'P-01' 'A 目录启动成功' $hA.Up ""
|
||||
|
||||
$cA = New-Client
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/setup" @{ username = 'admin'; display_name = '老板'; password = 'daili123'; password2 = 'daili123' } | Out-Null
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/packages" @{ name = '包月日常'; unit = 'day'; default_amount = '30'; price = '300'; color = '#2F5D50'; note = ''; sort_order = '1' } | Out-Null
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/customers" @{ tail = '8001'; name = '迁移客户'; game_name = '王者荣耀'; game_server = ''; contact = 'wx:8001'; tags = '老客户'; source = ''; note = '迁移前数据' } | Out-Null
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/customers/1/subscriptions" @{ package_id = '1'; start_date = $today; days = '30'; note = '首单' } | Out-Null
|
||||
$detail = (Get-Page $cA "http://127.0.0.1:$PortA/customers/1").Body
|
||||
$subId = [regex]::Match($detail, 'id="sub-(\d+)"').Groups[1].Value
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/subscriptions/$subId/extend" @{ new_end_date = $newEnd; note = '迁移前续费'; back = '/customers/1' } | Out-Null
|
||||
Post-Form $cA "http://127.0.0.1:$PortA/subscriptions/$subId/checkin" @{ date = $today; status = 'done'; note = '迁移前打卡'; back = '/customers/1' } | Out-Null
|
||||
$detail = (Get-Page $cA "http://127.0.0.1:$PortA/customers/1").Body
|
||||
Check 'P-02' '造数成功(剩 46 天 + 打卡)' ($detail -match '剩 46 天' -and $detail -match '迁移前打卡') ""
|
||||
|
||||
Stop-App $hA
|
||||
|
||||
# ---------- 2. 关闭后应当只有一个数据库文件 ----------
|
||||
Write-Host "`n-- 2. 关闭后检查文件形态 --" -ForegroundColor Cyan
|
||||
$dbA = Join-Path $dirA 'daili.db'
|
||||
$sidecars = @()
|
||||
foreach ($s in '-wal', '-shm', '-journal') {
|
||||
if (Test-Path ($dbA + $s)) { $sidecars += $s }
|
||||
}
|
||||
Check 'P-03' '关闭后只有 daili.db(无 -wal/-shm/-journal)' ((Test-Path $dbA) -and $sidecars.Count -eq 0) "sidecars=$($sidecars -join ',')"
|
||||
$sizeA = (Get-Item $dbA).Length
|
||||
Check 'P-04' '主库文件包含全部数据(远大于空库 4KB)' ($sizeA -gt 20000) "size=$sizeA"
|
||||
|
||||
# ---------- 3. 只拷贝 daili.db 到 B 目录 ----------
|
||||
Write-Host "`n-- 3. 只拷贝 daili.db 到 B 目录 --" -ForegroundColor Cyan
|
||||
$dirB = New-AppDir 'B'
|
||||
Copy-Item $dbA (Join-Path $dirB 'daili.db') -Force
|
||||
$hB = Start-App $dirB $PortB
|
||||
Check 'P-05' 'B 目录用拷贝的库启动成功' $hB.Up ""
|
||||
|
||||
$cB = New-Client
|
||||
$login = Post-Form $cB "http://127.0.0.1:$PortB/login" @{ username = 'admin'; password = 'daili123'; next = '/customers' }
|
||||
$page = (Get-Page $cB "http://127.0.0.1:$PortB/customers").Body
|
||||
Check 'P-06' '原管理员账号可用(口令随库迁移)' ($page -match '共 1 位' -and $page -match '迁移客户') ""
|
||||
|
||||
$detailB = (Get-Page $cB "http://127.0.0.1:$PortB/customers/1").Body
|
||||
Check 'P-07' '订阅与剩余天数完整' ($detailB -match '剩 46 天') ""
|
||||
Check 'P-08' '打卡与追加记录完整' ($detailB -match '迁移前打卡' -and $detailB -match '迁移前续费') ""
|
||||
Check 'P-09' '套餐配置完整' ((Get-Page $cB "http://127.0.0.1:$PortB/packages").Body -match '包月日常') ""
|
||||
$logs = (Get-Page $cB "http://127.0.0.1:$PortB/logs").Body
|
||||
Check 'P-10' '操作日志完整' ($logs -match '追加时长' -and $logs -match '打卡') ""
|
||||
|
||||
Stop-App $hB
|
||||
for ($i = 0; $i -lt 10 -and (Test-Path ($dbA + '-wal')); $i++) { Start-Sleep -Milliseconds 200 }
|
||||
$bSide = @()
|
||||
foreach ($s in '-wal', '-shm', '-journal') { if (Test-Path ((Join-Path $dirB 'daili.db') + $s)) { $bSide += $s } }
|
||||
Check 'P-11' 'B 目录运行后仍无旁路文件' ($bSide.Count -eq 0) "sidecars=$($bSide -join ',')"
|
||||
|
||||
# ---------- 4. 删除 daili.db = 重置 ----------
|
||||
Write-Host "`n-- 4. 删除 daili.db 重置数据 --" -ForegroundColor Cyan
|
||||
foreach ($s in '', '-wal', '-shm', '-journal') { Remove-Item ($dbA + $s) -Force -ErrorAction SilentlyContinue }
|
||||
$hA2 = Start-App $dirA $PortA
|
||||
Check 'P-12' '删库后可正常启动' $hA2.Up ""
|
||||
$cA2 = New-Client
|
||||
$root = Get-Page $cA2 "http://127.0.0.1:$PortA/"
|
||||
Check 'P-13' '删库后回到初始化(数据已清空)' ($root.Path -eq '/setup' -and $root.Body -match '创建第一个管理员') "path=$($root.Path)"
|
||||
Stop-App $hA2
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
$pass = @($script:results | Where-Object { $_ -like 'PASS|*' }).Count
|
||||
$fail = @($script:results | Where-Object { $_ -like 'FAIL|*' }).Count
|
||||
Write-Host "`n=== 汇总:通过 $pass 项,失败 $fail 项 ===" -ForegroundColor ($(if ($fail -eq 0) { 'Green' } else { 'Red' }))
|
||||
New-Item -ItemType Directory -Force -Path 'F:\hupeE的代肝平台\test-evidence' | Out-Null
|
||||
$script:results | Set-Content 'F:\hupeE的代肝平台\test-evidence\portability-result.txt' -Encoding UTF8
|
||||
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
||||
if ($fail -gt 0) { exit 1 }
|
||||
exit 0
|
||||
@@ -0,0 +1,95 @@
|
||||
# 生成演示数据(便于看效果、试用手感;可对空库反复运行)
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\seed.ps1 -Base http://127.0.0.1:18080
|
||||
param(
|
||||
[string]$Base = 'http://127.0.0.1:18080',
|
||||
[string]$User = 'admin',
|
||||
[string]$Pass = 'daili123'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
|
||||
function Send {
|
||||
param([string]$Method, [string]$Path, $Body)
|
||||
$p = @{ Uri = "$Base$Path"; WebSession = $session; UseBasicParsing = $true; MaximumRedirection = 5; Method = $Method }
|
||||
if ($null -ne $Body) { $p.Body = $Body }
|
||||
try { Invoke-WebRequest @p } catch [System.Net.WebException] { $null }
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
function D([int]$offset) { $now.AddDays($offset).ToString('yyyy-MM-dd') }
|
||||
|
||||
# 登录
|
||||
$r = Send 'GET' '/setup'
|
||||
if ($r -and $r.Content -match '创建第一个管理员') {
|
||||
Send 'POST' '/setup' @{ username = $User; display_name = '老板'; password = $Pass; password2 = $Pass } | Out-Null
|
||||
} else {
|
||||
Send 'POST' '/login' @{ username = $User; password = $Pass; next = '/calendar' } | Out-Null
|
||||
}
|
||||
|
||||
# 套餐(1、2 已存在则跳过创建)
|
||||
$pkgs = @(
|
||||
@{ name = '包月日常'; unit = 'day'; amount = 30; price = 300; color = '#2F5D50'; note = '每天日常 + 周常' },
|
||||
@{ name = '深渊十次'; unit = 'count'; amount = 10; price = 180; color = '#6B4E9B'; note = '按次结算,打完扣次' },
|
||||
@{ name = '赛季深渊'; unit = 'day'; amount = 90; price = 900; color = '#B45309'; note = '整赛季包干' },
|
||||
@{ name = '每周副本'; unit = 'count'; amount = 5; price = 120; color = '#41556B'; note = '每周 5 次' }
|
||||
)
|
||||
foreach ($p in $pkgs) {
|
||||
Send 'POST' '/packages' @{
|
||||
name = $p.name; unit = $p.unit; default_amount = "$($p.amount)"
|
||||
price = "$($p.price)"; color = $p.color; note = $p.note; sort_order = '1'
|
||||
} | Out-Null
|
||||
}
|
||||
|
||||
# 客户 + 订阅
|
||||
$demo = @(
|
||||
@{ tail = '1204'; name = '小林'; pkg = 1; start = -12; days = 30; note = '日常 + 周常' },
|
||||
@{ tail = '5533'; name = '阿飞'; pkg = 1; start = -28; days = 30; note = '快到期,提醒续费' },
|
||||
@{ tail = '9088'; name = '老王'; pkg = 1; start = -30; days = 30; note = '今天到期' },
|
||||
@{ tail = '4321'; name = '妹妹'; pkg = 3; start = -60; days = 90; note = '整赛季' },
|
||||
@{ tail = '7788'; name = '大鹏'; pkg = 2; start = -5; count = 10; end = 3; note = '按次' },
|
||||
@{ tail = '6677'; name = '猫猫'; pkg = 4; start = -2; count = 5; end = 20; note = '每周副本' },
|
||||
@{ tail = '3321'; name = '阿豪'; pkg = 1; start = -40; days = 30; note = '已过期两周' },
|
||||
@{ tail = '9900'; name = '小雨'; pkg = 3; start = 2; days = 90; note = '还没开始' }
|
||||
)
|
||||
|
||||
foreach ($d in $demo) {
|
||||
$r = Send 'POST' '/customers' @{
|
||||
tail = $d.tail; name = $d.name; game_name = '王者荣耀'; game_server = '安卓微信区'
|
||||
contact = ('wx: ' + $d.tail); tags = '老客户'; source = '朋友介绍'; note = $d.note
|
||||
}
|
||||
$path = $r.BaseResponse.ResponseUri.AbsolutePath
|
||||
if ($path -notlike '/customers/*') { continue }
|
||||
$id = ($path -split '/')[-1]
|
||||
if ($d.days) {
|
||||
Send 'POST' "/customers/$id/subscriptions" @{
|
||||
package_id = "$($d.pkg)"; start_date = (D $d.start); days = "$($d.days)"; note = $d.note
|
||||
} | Out-Null
|
||||
} else {
|
||||
Send 'POST' "/customers/$id/subscriptions" @{
|
||||
package_id = "$($d.pkg)"; start_date = (D $d.start); total_count = "$($d.count)"; end_date = (D $d.end); note = $d.note
|
||||
} | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# 今天的两条打卡
|
||||
$r = Send 'GET' '/customers?q=1204'
|
||||
if ($r.Content -match '/customers/(\d+)') {
|
||||
$cid = $matches[1]
|
||||
$r2 = Send 'GET' "/customers/$cid"
|
||||
$ids = [regex]::Matches($r2.Content, 'id="sub-(\d+)"') | ForEach-Object { $_.Groups[1].Value }
|
||||
if ($ids.Count -gt 0) {
|
||||
Send 'POST' "/subscriptions/$($ids[0])/checkin" @{ date = (D 0); status = 'done'; note = '今日已打'; back = '/calendar' } | Out-Null
|
||||
}
|
||||
}
|
||||
$r = Send 'GET' '/customers?q=4321'
|
||||
if ($r.Content -match '/customers/(\d+)') {
|
||||
$cid = $matches[1]
|
||||
$r2 = Send 'GET' "/customers/$cid"
|
||||
$ids = [regex]::Matches($r2.Content, 'id="sub-(\d+)"') | ForEach-Object { $_.Groups[1].Value }
|
||||
if ($ids.Count -gt 0) {
|
||||
Send 'POST' "/subscriptions/$($ids[0])/checkin" @{ date = (D 0); status = 'done'; note = '深渊打完'; back = '/calendar' } | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host '演示数据已写入。' -ForegroundColor Green
|
||||
@@ -0,0 +1,127 @@
|
||||
# 用无头 Chrome 截图,做视觉验收
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\shots.ps1 -Base http://127.0.0.1:18080
|
||||
param(
|
||||
[string]$Base = 'http://127.0.0.1:18080',
|
||||
[string]$User = 'admin',
|
||||
[string]$Pass = 'daili123',
|
||||
[string]$OutDir = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
if (-not $OutDir) { $OutDir = Join-Path $root 'shots' }
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
|
||||
$chrome = 'C:\Program Files\Google\Chrome\Application\chrome.exe'
|
||||
if (-not (Test-Path $chrome)) { throw '找不到 Chrome' }
|
||||
|
||||
# 1) 登录拿会话 cookie
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
Invoke-WebRequest -Uri "$Base/login" -WebSession $session -UseBasicParsing | Out-Null
|
||||
Invoke-WebRequest -Uri "$Base/login" -Method POST -WebSession $session -UseBasicParsing -Body @{
|
||||
username = $User; password = $Pass; next = '/calendar'
|
||||
} | Out-Null
|
||||
$cookie = ($session.Cookies.GetCookies($Base) | Where-Object { $_.Name -eq 'daili_sid' } | Select-Object -First 1)
|
||||
if (-not $cookie) { throw '登录失败,拿不到会话' }
|
||||
$token = $cookie.Value
|
||||
|
||||
# 2) 拿一个订阅 ID 用于「追加面板」截图
|
||||
$custID = ''
|
||||
$subID = ''
|
||||
$r = Invoke-WebRequest -Uri "$Base/customers?q=7001" -WebSession $session -UseBasicParsing
|
||||
if ($r.Content -match '/customers/(\d+)') {
|
||||
$custID = $matches[1]
|
||||
$r2 = Invoke-WebRequest -Uri "$Base/customers/$custID" -WebSession $session -UseBasicParsing
|
||||
if ($r2.Content -match 'id="sub-(\d+)"') { $subID = $matches[1] }
|
||||
}
|
||||
|
||||
# 3) 启动 Chrome
|
||||
$profile = Join-Path $root ('.chrome-profile-' + (Get-Date).ToString('HHmmss'))
|
||||
$chromeArgs = @(
|
||||
'--headless=new', '--disable-gpu', '--hide-scrollbars', '--no-sandbox',
|
||||
'--no-first-run', '--no-default-browser-check', '--disable-extensions',
|
||||
'--disable-crash-reporter', '--disable-breakpad',
|
||||
'--force-device-scale-factor=1', '--remote-debugging-port=9222',
|
||||
"--user-data-dir=$profile", '--window-size=1440,1000', 'about:blank'
|
||||
)
|
||||
$chromeProc = Start-Process -FilePath $chrome -ArgumentList $chromeArgs -PassThru
|
||||
$targets = $null
|
||||
for ($i = 0; $i -lt 20; $i++) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
try {
|
||||
$targets = Invoke-RestMethod 'http://127.0.0.1:9222/json'
|
||||
if ($targets) { break }
|
||||
} catch { }
|
||||
}
|
||||
if (-not $targets) {
|
||||
Stop-Process -Id $chromeProc.Id -Force -ErrorAction SilentlyContinue
|
||||
throw 'Chrome 调试端口未就绪'
|
||||
}
|
||||
$page = $targets | Where-Object { $_.type -eq 'page' } | Select-Object -First 1
|
||||
if (-not $page) { throw '拿不到调试目标' }
|
||||
|
||||
$script:ws = New-Object System.Net.WebSockets.ClientWebSocket
|
||||
$script:ws.ConnectAsync([Uri]$page.webSocketDebuggerUrl, [Threading.CancellationToken]::None).Wait()
|
||||
$script:msgId = 0
|
||||
|
||||
function Send-CDP {
|
||||
param([string]$Method, $Params)
|
||||
$script:msgId++
|
||||
$payload = @{ id = $script:msgId; method = $Method; params = $Params } | ConvertTo-Json -Depth 12 -Compress
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($payload)
|
||||
$seg = New-Object 'System.ArraySegment[byte]' -ArgumentList @(, $bytes)
|
||||
$script:ws.SendAsync($seg, [Net.WebSockets.WebSocketMessageType]::Text, $true, [Threading.CancellationToken]::None).Wait()
|
||||
while ($true) {
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
do {
|
||||
$buf = New-Object byte[] 131072
|
||||
$segR = New-Object 'System.ArraySegment[byte]' -ArgumentList @(, $buf)
|
||||
$res = $script:ws.ReceiveAsync($segR, [Threading.CancellationToken]::None).Result
|
||||
$ms.Write($buf, 0, $res.Count)
|
||||
} while (-not $res.EndOfMessage)
|
||||
$text = [Text.Encoding]::UTF8.GetString($ms.ToArray())
|
||||
$msg = $text | ConvertFrom-Json
|
||||
if ($msg.id -eq $script:msgId) { return $msg }
|
||||
}
|
||||
}
|
||||
|
||||
function Shoot {
|
||||
param([string]$Name, [string]$Path, [int]$Width = 1440, [int]$Height = 1000, [bool]$SetCookie = $true)
|
||||
Send-CDP 'Emulation.setDeviceMetricsOverride' @{
|
||||
width = $Width; height = $Height; deviceScaleFactor = 1; mobile = ($Width -lt 600)
|
||||
} | Out-Null
|
||||
if ($SetCookie) {
|
||||
Send-CDP 'Network.setCookie' @{
|
||||
name = 'daili_sid'; value = $token; domain = '127.0.0.1'; path = '/'; httpOnly = $true
|
||||
} | Out-Null
|
||||
}
|
||||
Send-CDP 'Page.navigate' @{ url = "$Base$Path" } | Out-Null
|
||||
Start-Sleep -Milliseconds 900
|
||||
$shot = Send-CDP 'Page.captureScreenshot' @{ format = 'png'; captureBeyondViewport = $false }
|
||||
$file = Join-Path $OutDir "$Name.png"
|
||||
[System.IO.File]::WriteAllBytes($file, [Convert]::FromBase64String($shot.result.data))
|
||||
Write-Host (" shot {0,-16} <- {1}{2}" -f $Name, $Base, $Path) -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
Send-CDP 'Network.enable' @{} | Out-Null
|
||||
Send-CDP 'Page.enable' @{} | Out-Null
|
||||
|
||||
Shoot 'login' '/login' 1440 900 $false
|
||||
Shoot 'calendar' '/calendar' 1440 1000
|
||||
Shoot 'customers' '/customers' 1440 900
|
||||
if ($custID) { Shoot 'customer-detail' "/customers/$custID" 1440 1100 }
|
||||
if ($custID -and $subID) {
|
||||
$today = (Get-Date).ToString('yyyy-MM-dd')
|
||||
Shoot 'extend-panel' ("/customers/{0}?extend={1}#extend" -f $custID, $subID) 1440 1200
|
||||
}
|
||||
Shoot 'subscriptions' '/subscriptions?state=all' 1440 900
|
||||
Shoot 'packages' '/packages' 1440 800
|
||||
Shoot 'package-form' '/packages/new' 1200 940
|
||||
Shoot 'customer-form' '/customers/new' 1000 900
|
||||
Shoot 'settings' '/settings' 1440 1000
|
||||
Shoot 'screen' '/screen' 1600 900
|
||||
Shoot 'calendar-mobile' '/calendar' 390 844
|
||||
|
||||
$script:ws.Dispose()
|
||||
Stop-Process -Id $chromeProc.Id -Force -ErrorAction SilentlyContinue
|
||||
Write-Host '截图完成' -ForegroundColor Green
|
||||
@@ -0,0 +1,215 @@
|
||||
# 端到端冒烟测试:真实 HTTP 走一遍主要流程
|
||||
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\smoke.ps1 -Base http://127.0.0.1:18080
|
||||
param(
|
||||
[string]$Base = 'http://127.0.0.1:18080',
|
||||
[string]$User = 'admin',
|
||||
[string]$Pass = 'daili123'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$script:fail = 0
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
|
||||
function Send {
|
||||
param([string]$Method, [string]$Path, $Body)
|
||||
$p = @{
|
||||
Uri = "$Base$Path"
|
||||
WebSession = $session
|
||||
UseBasicParsing = $true
|
||||
MaximumRedirection = 5
|
||||
Method = $Method
|
||||
}
|
||||
if ($null -ne $Body) { $p.Body = $Body }
|
||||
try {
|
||||
return Invoke-WebRequest @p
|
||||
} catch [System.Net.WebException] {
|
||||
$resp = $_.Exception.Response
|
||||
if ($null -eq $resp) { throw }
|
||||
$sr = New-Object System.IO.StreamReader($resp.GetResponseStream())
|
||||
$body = $sr.ReadToEnd()
|
||||
$sr.Close()
|
||||
return [pscustomobject]@{
|
||||
StatusCode = [int]$resp.StatusCode
|
||||
Content = $body
|
||||
BaseResponse = [pscustomobject]@{ ResponseUri = $resp.ResponseUri }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Check {
|
||||
param([bool]$Cond, [string]$Msg)
|
||||
if ($Cond) {
|
||||
Write-Host (" [ok] " + $Msg) -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host (" [FAIL] " + $Msg) -ForegroundColor Red
|
||||
$script:fail++
|
||||
}
|
||||
}
|
||||
|
||||
function Final-Path($resp) {
|
||||
if ($resp.PSObject.Properties.Name -contains 'BaseResponse' -and $resp.BaseResponse -and $resp.BaseResponse.ResponseUri) {
|
||||
return $resp.BaseResponse.ResponseUri.AbsolutePath
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
$today = $now.ToString('yyyy-MM-dd')
|
||||
$tail = Get-Random -Minimum 1000 -Maximum 9999
|
||||
Write-Host "`n=== 代打台冒烟测试 @ $Base ($today / 尾号 $tail) ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. 初始化管理员(或登录)
|
||||
$r = Send 'GET' '/setup'
|
||||
if ($r.Content -match '创建第一个管理员') {
|
||||
$r = Send 'POST' '/setup' @{ username = $User; display_name = '老板'; password = $Pass; password2 = $Pass }
|
||||
Check ($r.StatusCode -eq 200) '创建管理员并自动登录'
|
||||
Check ((Final-Path $r) -like '/packages*') '初始化后跳转到套餐页'
|
||||
} else {
|
||||
$r = Send 'POST' '/login' @{ username = $User; password = $Pass; next = '/calendar' }
|
||||
Check ($r.StatusCode -eq 200 -and (Final-Path $r) -like '/calendar*') '已存在管理员,登录成功'
|
||||
}
|
||||
|
||||
# 2. 套餐
|
||||
$r = Send 'GET' '/packages'
|
||||
Check ($r.StatusCode -eq 200) '套餐页可访问'
|
||||
$colors = @('#2F5D50', '#6B4E9B')
|
||||
$pkgs = @(
|
||||
@{ name = '包月日常'; unit = 'day'; default_amount = 30; price = 300; note = '每天日常 + 周常' },
|
||||
@{ name = '深渊十次'; unit = 'count'; default_amount = 10; price = 180; note = '按次结算' }
|
||||
)
|
||||
for ($i = 0; $i -lt $pkgs.Count; $i++) {
|
||||
$p = $pkgs[$i]
|
||||
$r = Send 'POST' '/packages' @{
|
||||
name = $p.name; unit = $p.unit; default_amount = "$($p.default_amount)"
|
||||
price = "$($p.price)"; color = $colors[$i]; note = $p.note; sort_order = "$($i + 1)"
|
||||
}
|
||||
Check ($r.Content -match '套餐已保存|套餐已创建|套餐') "提交套餐 $($p.name)"
|
||||
}
|
||||
$r = Send 'GET' '/packages'
|
||||
Check ($r.Content -match '包月日常' -and $r.Content -match '深渊十次') '套餐列表包含两个套餐'
|
||||
$r = Send 'GET' '/packages/1/edit'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match '包月日常') '套餐编辑页可打开'
|
||||
|
||||
# 3. 客户
|
||||
$r = Send 'POST' '/customers' @{
|
||||
tail = "$tail"; name = '老张'; game_name = '王者荣耀'; game_server = '微信区'
|
||||
contact = 'wx: laozhang'; tags = '老客户,包月'; source = '抖音'; note = '喜欢晚上打'
|
||||
}
|
||||
$custPath = Final-Path $r
|
||||
Check ($custPath -like '/customers/*') "新建客户成功 -> $custPath"
|
||||
$custID = ($custPath -split '/')[-1]
|
||||
|
||||
$r = Send 'GET' "/customers?q=$tail"
|
||||
Check ($r.Content -match '老张') "尾号 $tail 搜索命中老张"
|
||||
|
||||
$r = Send 'POST' '/customers' @{ tail = "$tail"; name = '老张' }
|
||||
Check ($r.StatusCode -eq 400 -and $r.Content -match '已存在') '重复客户被拒绝并提示'
|
||||
|
||||
# 4. 订阅(按日 + 按次)
|
||||
$r = Send 'POST' "/customers/$custID/subscriptions" @{ package_id = '1'; start_date = $today; days = '30'; note = '首单' }
|
||||
Check ($r.Content -match '已开通') '开通按日订阅'
|
||||
$r = Send 'POST' "/customers/$custID/subscriptions" @{ package_id = '1'; start_date = $today; days = '30' }
|
||||
Check ($r.Content -match '追加时长') '同一套餐重复订阅被拦截'
|
||||
|
||||
$r = Send 'POST' "/customers/$custID/subscriptions" @{
|
||||
package_id = '2'; start_date = $today; total_count = '10'; end_date = $now.AddDays(10).ToString('yyyy-MM-dd')
|
||||
}
|
||||
Check ($r.Content -match '已开通') '开通按次订阅'
|
||||
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
$subIDs = [regex]::Matches($r.Content, 'id="sub-(\d+)"') | ForEach-Object { [int]$_.Groups[1].Value }
|
||||
Check ($subIDs.Count -ge 2) "客户页出现 2 张订阅卡(实际 $($subIDs.Count))"
|
||||
$daySub = $subIDs[0]
|
||||
$cntSub = $subIDs[1]
|
||||
|
||||
# 5. 追加时长(日历手动选终止日:今天 +45 天)
|
||||
$newEnd = $now.AddDays(45)
|
||||
$oldEnd = $now.AddDays(29)
|
||||
$expectDays = ($newEnd.Date - $oldEnd.Date).Days
|
||||
$r = Send 'POST' "/subscriptions/$daySub/extend" @{
|
||||
new_end_date = $newEnd.ToString('yyyy-MM-dd'); note = '微信转账续费'; back = "/customers/$custID"
|
||||
}
|
||||
Check ($r.Content -match "已追加 $expectDays 天") "追加时长反算正确(期望 $expectDays 天)"
|
||||
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
Check ($r.Content -match '剩 46 天') '客户页剩余天数更新为 46 天'
|
||||
Check ($r.Content -match '微信转账续费') '追加记录已留痕(备注)'
|
||||
Check ($r.Content -match '追加天数') '追加记录标注类型'
|
||||
|
||||
# 6. 日历
|
||||
$r = Send 'GET' "/calendar?y=$($now.Year)&m=$($now.Month)&d=$today"
|
||||
Check ($r.StatusCode -eq 200) '日历页可打开'
|
||||
Check ($r.Content -match '老张') '日历当天出现该客户色签'
|
||||
Check ($r.Content -match '当日待打') '日历抽屉存在'
|
||||
|
||||
# 7. 打卡(按日)
|
||||
$r = Send 'POST' "/subscriptions/$daySub/checkin" @{ date = $today; status = 'done'; note = '打完'; back = "/calendar?d=$today" }
|
||||
Check ($r.Content -match '已记录') '按日打卡成功'
|
||||
|
||||
# 8. 按次打卡扣次
|
||||
$r = Send 'POST' "/subscriptions/$cntSub/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
Check ($r.Content -match '已记录') '按次打卡成功(应扣 1 次)'
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
Check ($r.Content -match '剩 9 / 10 次') '按次剩余次数扣减为 9'
|
||||
|
||||
# 9. 暂停 / 恢复
|
||||
$r = Send 'POST' "/subscriptions/$daySub/pause" @{ back = "/customers/$custID" }
|
||||
Check ($r.Content -match '已暂停') '暂停订阅'
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
Check ($r.Content -match '暂停中') '客户页显示暂停中'
|
||||
$r = Send 'POST' "/subscriptions/$daySub/resume" @{ back = "/customers/$custID" }
|
||||
Check ($r.Content -match '已恢复') '恢复订阅'
|
||||
|
||||
# 9.5 无有效期的按次订阅:靠客户页「今日打卡」扣次
|
||||
$r = Send 'POST' '/packages' @{
|
||||
name = '副本五次'; unit = 'count'; default_amount = '5'; price = '120'; color = '#41556B'; note = '无有效期'; sort_order = '3'
|
||||
}
|
||||
Check ($r.Content -match '套餐') '新建第三个套餐(按次、无有效期)'
|
||||
$r = Send 'GET' '/packages'
|
||||
$pkgIDs = [regex]::Matches($r.Content, '/packages/(\d+)/edit') | ForEach-Object { [int]$_.Groups[1].Value }
|
||||
$newPkg = ($pkgIDs | Measure-Object -Maximum).Maximum
|
||||
$r = Send 'POST' "/customers/$custID/subscriptions" @{ package_id = "$newPkg"; start_date = $today; total_count = '5' }
|
||||
Check ($r.Content -match '已开通') '开通无有效期的按次订阅'
|
||||
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
Check ($r.Content -match '今日打卡') '订阅卡片出现「今日打卡」按钮'
|
||||
$ids3 = [regex]::Matches($r.Content, 'id="sub-(\d+)"') | ForEach-Object { [int]$_.Groups[1].Value }
|
||||
$thirdSub = $ids3[-1]
|
||||
$r = Send 'POST' "/subscriptions/$thirdSub/checkin" @{ date = $today; status = 'done'; back = "/customers/$custID" }
|
||||
Check ($r.Content -match '已记录') '无有效期按次订阅打卡成功'
|
||||
$r = Send 'GET' "/customers/$custID"
|
||||
Check ($r.Content -match '剩 4 / 5 次') '打卡后次数扣减为 4'
|
||||
|
||||
# 10. 大屏
|
||||
$r = Send 'GET' '/screen'
|
||||
Check ($r.StatusCode -eq 200) '大屏可访问'
|
||||
Check ($r.Content -match '即将到期 TOP 10') '大屏含 TOP 榜'
|
||||
Check ($r.Content -match '到期热力') '大屏含热力月历'
|
||||
Check ($r.Content -match "$tail") '大屏显示该客户尾号'
|
||||
|
||||
# 11. 日志 / 设置 / 导出
|
||||
$r = Send 'GET' '/logs'
|
||||
Check ($r.Content -match '追加时长') '操作日志含追加记录'
|
||||
$r = Send 'GET' '/settings'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match '基础设置') '设置页可打开'
|
||||
$r = Send 'GET' '/export/customers.csv'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match '手机尾号') '客户 CSV 导出正常'
|
||||
$r = Send 'GET' '/export/subscriptions.csv'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match '剩余') '订阅 CSV 导出正常'
|
||||
$r = Send 'GET' '/export/logs.csv'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match '操作人') '日志 CSV 导出正常'
|
||||
|
||||
# 12. 静态资源与健康检查
|
||||
$r = Send 'GET' '/static/app.css'
|
||||
Check ($r.StatusCode -eq 200 -and $r.Content -match 'ticket') '静态样式可访问'
|
||||
$r = Send 'GET' '/healthz'
|
||||
Check ($r.Content -match 'ok') '健康检查正常'
|
||||
|
||||
Write-Host ""
|
||||
if ($script:fail -eq 0) {
|
||||
Write-Host "全部通过" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host ("失败 $script:fail 项") -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"""校验备份文件可被标准 SQLite 打开并含数据(Windows 自带 Python 或 Miniconda 均可运行)。"""
|
||||
import glob
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
pattern = os.path.join(sys.argv[1], "*.db")
|
||||
files = sorted(glob.glob(pattern), key=os.path.getmtime)
|
||||
if not files:
|
||||
print("FAIL: 没有找到备份文件")
|
||||
sys.exit(1)
|
||||
|
||||
path = files[-1]
|
||||
print("备份文件:", os.path.basename(path), os.path.getsize(path), "字节")
|
||||
con = sqlite3.connect(path)
|
||||
tables = [r[0] for r in con.execute(
|
||||
"select name from sqlite_master where type='table' order by name")]
|
||||
print("表:", ", ".join(tables))
|
||||
for t in ("users", "packages", "customers", "subscriptions", "sub_adjustments", "checkins", "op_logs"):
|
||||
if t in tables:
|
||||
n = con.execute("select count(*) from %s" % t).fetchone()[0]
|
||||
print(" %-16s %d 行" % (t, n))
|
||||
con.close()
|
||||
print("OK")
|
||||
Reference in New Issue
Block a user