$BASE = "http://127.0.0.1:5080" $pass = 0; $fail = 0 function Jget([string]$Url) { $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 return ($r.Content | ConvertFrom-Json).data } function Jpost([string]$Url, [string]$Body) { $r = Invoke-WebRequest -Uri $Url -Method POST -ContentType "application/json" -Body $Body -UseBasicParsing -TimeoutSec 5 return ($r.Content | ConvertFrom-Json).data } function Jput([string]$Url, [string]$Body) { $r = Invoke-WebRequest -Uri $Url -Method PUT -ContentType "application/json" -Body $Body -UseBasicParsing -TimeoutSec 5 return ($r.Content | ConvertFrom-Json).data } function Jdel([string]$Url) { Invoke-WebRequest -Uri $Url -Method DELETE -UseBasicParsing -TimeoutSec 5 | Out-Null } function Step([string]$Name, [scriptblock]$A) { try { $r = & $A Write-Host " PASS $Name" -ForegroundColor Green $script:pass++ return $r } catch { Write-Host " FAIL $Name :: $($_.Exception.Message)" -ForegroundColor Red $script:fail++ } } Write-Host "===== E2E 完整链路 =====" -ForegroundColor Cyan # 1) Root $root = Step "Create root category" { Jpost "$BASE/api/categories" '{"parentId":0,"name":"e2e-root","icon":"star","sort":99}' } if ($root) { Write-Host " id=$($root.id) parentId=$($root.parentId)" -ForegroundColor DarkGray } # 2) Child $child = Step "Create child category" { Jpost "$BASE/api/categories" ('{"parentId":' + $root.id + ',"name":"e2e-child","icon":"link","sort":1}') } if ($child) { Write-Host " id=$($child.id) parentId=$($child.parentId)" -ForegroundColor DarkGray } # 3) Update child $upd = Step "Update child category" { Jput "$BASE/api/categories/$($child.id)" ('{"parentId":' + $root.id + ',"name":"e2e-child-renamed","icon":"link","sort":2}') } if ($upd) { Write-Host " name=$($upd.name) sort=$($upd.sort)" -ForegroundColor DarkGray } # 4) Create bookmark $bm = Step "Create bookmark" { Jpost "$BASE/api/bookmarks" ('{"categoryId":' + $child.id + ',"title":"e2e-bookmark","url":"https://e2e.com","description":"e2e","icon":"link","iconType":"lucide","iconUrl":null,"sort":0}') } if ($bm) { Write-Host " id=$($bm.id) categoryId=$($bm.categoryId)" -ForegroundColor DarkGray } # 5) Update bookmark $bmUpd = Step "Update bookmark" { Jput "$BASE/api/bookmarks/$($bm.id)" ('{"categoryId":' + $child.id + ',"title":"e2e-renamed","url":"https://e2e.com","description":"new","icon":"link","iconType":"lucide","iconUrl":null,"sort":1}') } if ($bmUpd) { Write-Host " title=$($bmUpd.title) sort=$($bmUpd.sort)" -ForegroundColor DarkGray } # 6) Settings $set = Step "Update settings" { Jput "$BASE/api/settings" '{"themeMode":"light","accentColor":"#00b894","backgroundImage":"wp5","backgroundType":"preset"}' } if ($set) { Write-Host " theme=$($set.themeMode) accent=$($set.accentColor) bg=$($set.backgroundImage)" -ForegroundColor DarkGray } # 7) Upload PNG (multipart) $png = [System.IO.Path]::GetTempFileName() + ".png" $bytes = [byte[]](0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A,0x00,0x00,0x00,0x0D,0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x08,0x06,0x00,0x00,0x00,0x1F,0x15,0xC4,0x89,0x00,0x00,0x00,0x0A,0x49,0x44,0x41,0x54,0x78,0x9C,0x63,0x00,0x01,0x00,0x00,0x05,0x00,0x01,0x0D,0x0A,0x2D,0xB4,0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,0xAE,0x42,0x60,0x82) [System.IO.File]::WriteAllBytes($png, $bytes) $up = Step "Upload PNG (multipart)" { Add-Type -AssemblyName System.Net.Http $uri = "http://127.0.0.1:5080/api/upload" $form = [System.Net.Http.MultipartFormDataContent]::new() $fs = [System.IO.File]::OpenRead($png) $content = [System.Net.Http.StreamContent]::new($fs) $content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("image/png") $form.Add($content, "file", [System.IO.Path]::GetFileName($png)) $client = [System.Net.Http.HttpClient]::new() try { $r = $client.PostAsync($uri, $form).GetAwaiter().GetResult() if ($r.IsSuccessStatusCode) { $body = $r.Content.ReadAsStringAsync().GetAwaiter().GetResult() | ConvertFrom-Json return $body.data } else { throw "status=" + $r.StatusCode } } finally { $fs.Close() $form.Dispose() $client.Dispose() } } if ($up) { Write-Host " url=$($up.url)" -ForegroundColor DarkGray } Remove-Item $png -ErrorAction SilentlyContinue # 8) Default engine $eng = Step "Set default search engine" { Jput "$BASE/api/search-engines/3/default" "" } if ($eng) { Write-Host " default=$($eng.name) isDefault=$($eng.isDefault)" -ForegroundColor DarkGray } # 9) Sync $sync = Step "Sync changes" { Jget "$BASE/api/sync/changes?since=2026-07-04T00:00:00Z" } if ($sync) { Write-Host " changes=$($sync.changes.Count) snapshot(cats=$($sync.snapshot.categories.Count), bookmarks=$($sync.snapshot.bookmarks.Count))" -ForegroundColor DarkGray } # 10) Tree $tree = Step "Get category tree" { Jget "$BASE/api/categories" } if ($tree) { Write-Host " top-level=$($tree.Count)" -ForegroundColor DarkGray } # 11) Delete root with children should be 400 $deny = Step "Delete root with children (expect 400)" { $got = "unknown" try { Invoke-WebRequest -Uri "$BASE/api/categories/$($root.id)" -Method DELETE -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop | Out-Null $got = "unexpected-200" } catch { $resp = $_.Exception.Response $code = 0 if ($resp) { $code = [int]$resp.StatusCode } $got = "rejected-" + $code } if ($got -eq "rejected-400") { return $got } else { throw $got } } # 12) Cleanup Step "Cleanup: delete bookmark" { Jdel "$BASE/api/bookmarks/$($bm.id)" } Step "Cleanup: delete child" { Jdel "$BASE/api/categories/$($child.id)" } Step "Cleanup: delete root" { Jdel "$BASE/api/categories/$($root.id)" } Step "Reset: default engine" { Jput "$BASE/api/search-engines/1/default" "" | Out-Null } Step "Reset: settings" { Jput "$BASE/api/settings" '{"themeMode":"dark","accentColor":"#6c5ce7","backgroundImage":"wp1","backgroundType":"preset"}' | Out-Null } Write-Host "" Write-Host "===== TOTAL: pass=$pass fail=$fail =====" -ForegroundColor $(if ($fail -eq 0) { "Green" } else { "Red" })