import test from "node:test" import assert from "node:assert/strict" import { saveUserSteps, type UserSaveCheckpoint, type UserSaveApi } from "./user-save" import { ApiClientError } from "@/shared/api/http-client" import type { AppUser, AppUserForm, InterfaceBinding } from "./users" const user:AppUser={id:"u1",name:"Тест",login:"test@example.test",email:"test@example.test",role:"viewer",active:true,avatar:"Т",last:"—",sections:[],servers:[],bindings:[]} const binding=(name:string):InterfaceBinding=>({id:"draft-"+name,userId:"",serverId:"1",serverName:"Сервер",serverSite:"",serverCountry:"RU",interfaceName:name,interfaceType:"ether",comment:""}) const form:AppUserForm={...user,bindings:[binding("ether1"),binding("ether2")]} function api(overrides:Partial={}):UserSaveApi{return{create:async()=>({...user}),update:async()=>({...user}),read:async()=>({...user}),removeBinding:async()=>{},addBinding:async(_,b)=>({...b,id:"saved-"+b.interfaceName,userId:"u1"}),...overrides}} test("частичные привязки: повтор читает сервер, не создаёт второй профиль и не дублирует привязку",async()=>{ const state:UserSaveCheckpoint={user:null},calls:string[]=[];let fail=true,remote:AppUser={...user} const service=api({create:async()=>{calls.push("create");return remote},read:async()=>{calls.push("read");return remote},update:async()=>{calls.push("patch");return remote},addBinding:async(_,b)=>{calls.push("add:"+b.interfaceName);if(b.interfaceName==="ether2"&&fail)throw new ApiClientError("conflict",409);const saved={...b,id:"saved-"+b.interfaceName,userId:"u1"};remote={...remote,bindings:[...remote.bindings,saved]};return saved}}) await assert.rejects(saveUserSteps(form,state,service,()=>true,()=>{}),/conflict/);assert.equal(state.user?.bindings.length,1);assert.equal(state.bindingsNeedRead,true) fail=false;const saved=await saveUserSteps(form,state,service,()=>true,()=>{});assert.equal(saved?.bindings.length,2);assert.deepEqual(calls,["create","add:ether1","add:ether2","read","patch","add:ether2"]) }) test("потерянный ответ удаления: read подтверждает удаление, повтор не отправляет второй DELETE",async()=>{ const b={...binding("ether1"),id:"old",userId:"u1"};let remote:AppUser={...user,bindings:[b]},deletes=0 const state:UserSaveCheckpoint={user:remote},service=api({update:async()=>remote,read:async()=>remote,removeBinding:async()=>{deletes++;remote={...remote,bindings:[]};throw new Error("socket closed")}}) await assert.rejects(saveUserSteps({...form,bindings:[]},state,service,()=>true,()=>{}),/socket/);await saveUserSteps({...form,bindings:[]},state,service,()=>true,()=>{});assert.equal(deletes,1);assert.equal(state.user?.bindings.length,0) }) test("неизвестный исход создания запрещает повторный POST; 403 допускает исправление",async()=>{ for(const known of [false,true]){let creates=0;const state:UserSaveCheckpoint={user:null};const service=api({create:async()=>{creates++;throw new ApiClientError("fail",known?403:500)}}) await assert.rejects(saveUserSteps(form,state,service,()=>true,()=>{}));assert.equal(!!state.createUncertain,!known);await assert.rejects(saveUserSteps(form,state,service,()=>true,()=>{}));assert.equal(creates,known?2:1)} }) test("поздний профиль прежнего контекста не начинает следующие сетевые шаги",async()=>{ let active=true,adds=0,changes=0;const state:UserSaveCheckpoint={user:null},service=api({create:async()=>{active=false;return user},addBinding:async(_,b)=>{adds++;return b}}) assert.equal(await saveUserSteps(form,state,service,()=>active,()=>{changes++}),null);assert.equal(adds,0);assert.equal(changes,0);assert.equal(state.user,null) }) test("изменённый профиль после частичного шага PATCH идёт по подтверждённому id",async()=>{ const state:UserSaveCheckpoint={user,profileSignature:"previous"};let id="" const service=api({update:async(userId,f)=>{id=userId;return{...user,name:f.name}}}) const saved=await saveUserSteps({...form,name:"Новое имя",bindings:[]},state,service,()=>true,()=>{});assert.equal(id,"u1");assert.equal(saved?.name,"Новое имя") })