From efa05f42bc53c44a352152b7c272bc0bda363070 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Mon, 18 Dec 2023 17:30:56 +0100 Subject: [PATCH] Improve security by isolating code execution more This commit enhances application security against potential attacks by isolating dependencies that access the host system (like file operations) from the renderer process. It narrows the exposed functionality to script execution only, adding an extra security layer. The changes allow secure and scalable API exposure, preparing for future functionalities such as desktop notifications for script errors (#264), improved script execution handling (#296), and creating restore points (#50) in a secure and repeatable way. Changes include: - Inject `CodeRunner` into Vue components via dependency injection. - Move `CodeRunner` to the application layer as an abstraction for better domain-driven design alignment. - Refactor `SystemOperations` and related interfaces, removing the `I` prefix. - Update architecture documentation for clarity. - Update return types in `NodeSystemOperations` to match the Node APIs. - Improve `WindowVariablesProvider` integration tests for better error context. - Centralize type checks with common functions like `isArray` and `isNumber`. - Change `CodeRunner` to use `os` parameter, ensuring correct window variable injection. - Streamline API exposure to the renderer process: - Automatically bind function contexts to prevent loss of original context. - Implement a way to create facades (wrapper/proxy objects) for increased security. --- SECURITY.md | 6 +- docs/architecture.md | 7 +- docs/presentation.md | 2 + img/architecture/app-ddd.drawio | 1 - img/architecture/app-ddd.drawio.png | Bin 0 -> 64572 bytes img/architecture/app-ddd.png | Bin 29625 -> 0 bytes src/TypeHelpers.ts | 32 +++++ src/application/CodeRunner.ts | 7 + src/application/Common/CustomError.ts | 7 +- src/application/Common/Enum.ts | 6 +- src/application/Parser/DocumentationParser.ts | 21 +-- .../Parser/NodeValidation/NodeValidator.ts | 3 +- .../Function/Call/FunctionCallParser.ts | 9 +- .../Function/SharedFunctionsParser.ts | 9 +- .../SystemOperations/NodeSystemOperations.ts | 21 ++- .../SystemOperations/SystemOperations.ts | 24 ++++ .../TemporaryFileCodeRunner.ts} | 23 +-- src/infrastructure/Entity/BaseEntity.ts | 3 +- .../EnvironmentVariablesValidator.ts | 5 +- src/infrastructure/Log/ElectronLogger.ts | 8 +- .../SystemOperations/ISystemOperations.ts | 24 ---- .../WindowInjectedSystemOperations.ts | 14 -- .../WindowVariables/WindowVariables.ts | 4 +- .../WindowVariablesValidator.ts | 29 ++-- .../bootstrapping/DependencyProvider.ts | 5 + .../Code/CodeButtons/CodeRunButton.vue | 21 ++- .../NodeCollection/TreeInputParser.ts | 3 +- .../components/Shared/Hooks/UseCodeRunner.ts | 9 ++ .../ContextBridging/ApiContextBridge.ts | 17 +++ .../ContextBridging/MethodContextBinder.ts | 52 +++++++ .../{ => ContextBridging}/NodeOsMapper.ts | 0 .../ContextBridging/RendererApiProvider.ts | 27 ++++ .../ContextBridging/SecureFacadeCreator.ts | 42 ++++++ .../preload/WindowVariablesProvider.ts | 18 --- src/presentation/electron/preload/index.ts | 8 +- src/presentation/injectionSymbols.ts | 2 + .../utils/text.ts | 4 +- .../RuntimeSanity/SanityChecks.spec.ts | 3 +- .../ContextBridging/ApiContextBridge.spec.ts | 15 ++ .../RendererApiProvider.spec.ts | 66 +++++++++ .../preload/WindowVariablesProvider.spec.ts | 35 ----- .../State/Code/Generation/CodeBuilder.spec.ts | 4 +- .../TemporaryFileCodeRunner.spec.ts} | 40 +++--- .../WindowVariablesValidator.spec.ts | 57 +++----- .../WindowInjectedSystemOperations.spec.ts | 46 ------ .../bootstrapping/DependencyProvider.spec.ts | 1 + .../Shared/Hooks/UseCodeRunner.spec.ts | 27 ++++ .../ContextBridging/ApiContextBridge.spec.ts | 96 +++++++++++++ .../MethodContextBinder.spec.ts | 132 ++++++++++++++++++ .../NodeOsMapper.spec.ts | 2 +- .../RendererApiProvider.spec.ts | 108 ++++++++++++++ .../SecureFacadeCreator.spec.ts | 99 +++++++++++++ .../preload/WindowVariablesProvider.spec.ts | 82 ----------- tests/unit/shared/Stubs/CodeRunnerStub.ts | 7 + tests/unit/shared/Stubs/CommandOpsStub.ts | 9 +- tests/unit/shared/Stubs/FileSystemOpsStub.ts | 6 +- tests/unit/shared/Stubs/LocationOpsStub.ts | 6 +- .../shared/Stubs/OperatingSystemOpsStub.ts | 6 +- .../unit/shared/Stubs/SystemOperationsStub.ts | 32 ++--- .../unit/shared/Stubs/WindowVariablesStub.ts | 10 +- 60 files changed, 939 insertions(+), 423 deletions(-) delete mode 100644 img/architecture/app-ddd.drawio create mode 100644 img/architecture/app-ddd.drawio.png delete mode 100644 img/architecture/app-ddd.png create mode 100644 src/application/CodeRunner.ts rename src/infrastructure/{ => CodeRunner}/SystemOperations/NodeSystemOperations.ts (65%) create mode 100644 src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts rename src/infrastructure/{CodeRunner.ts => CodeRunner/TemporaryFileCodeRunner.ts} (65%) delete mode 100644 src/infrastructure/SystemOperations/ISystemOperations.ts delete mode 100644 src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts create mode 100644 src/presentation/components/Shared/Hooks/UseCodeRunner.ts create mode 100644 src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts create mode 100644 src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts rename src/presentation/electron/preload/{ => ContextBridging}/NodeOsMapper.ts (100%) create mode 100644 src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts create mode 100644 src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts delete mode 100644 src/presentation/electron/preload/WindowVariablesProvider.ts create mode 100644 tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts create mode 100644 tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts delete mode 100644 tests/integration/presentation/electron/preload/WindowVariablesProvider.spec.ts rename tests/unit/infrastructure/{CodeRunner.spec.ts => CodeRunner/TemporaryFileCodeRunner.spec.ts} (87%) delete mode 100644 tests/unit/infrastructure/SystemOperations/WindowInjectedSystemOperations.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseCodeRunner.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/MethodContextBinder.spec.ts rename tests/unit/presentation/electron/preload/{ => ContextBridging}/NodeOsMapper.spec.ts (98%) create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts create mode 100644 tests/unit/presentation/electron/preload/ContextBridging/SecureFacadeCreator.spec.ts delete mode 100644 tests/unit/presentation/electron/preload/WindowVariablesProvider.spec.ts create mode 100644 tests/unit/shared/Stubs/CodeRunnerStub.ts diff --git a/SECURITY.md b/SECURITY.md index b06b9273..c324ffbf 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,9 +31,9 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay - **Content Security Policies (CSP):** privacy.sexy actively follows security guidelines from the Open Web Application Security Project (OWASP) at strictest level. This approach protects against attacks like Cross Site Scripting (XSS) and data injection. -- **Context Isolation:** - The desktop application isolates different code sections based on their access level. - This separation prevents attackers from introducing harmful code into the app, known as injection attacks. +- **Host System Access Control:** + The desktop application segregates code sections based on their access levels. + This provides a critical defense mechanism, prevents attackers from introducing harmful code into the app, known as injection attacks. ### Update Security and Integrity diff --git a/docs/architecture.md b/docs/architecture.md index 17fdf161..d0d6c881 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,13 +27,14 @@ Application uses highly decoupled models & services in different DDD layers: **Domain layer**: - Serves as the system's core and central truth. -- Facilitates communication between the application and presentation layers through the domain model. +- It should be independent of other layers and encapsulate the core business concepts. **Infrastructure layer**: -- Manages technical implementations without dependencies on other layers or domain knowledge. +- Provides technical implementations. +- Depends on the application and domain layers in terms of interfaces and contracts but should not include business logic. -![DDD + vue.js](./../img/architecture/app-ddd.png) +![DDD + vue.js](./../img/architecture/app-ddd.drawio.png) ### Application state diff --git a/docs/presentation.md b/docs/presentation.md index d5cdb281..1ab29b41 100644 --- a/docs/presentation.md +++ b/docs/presentation.md @@ -92,6 +92,8 @@ Shared components include: Desktop builds uses `electron-vite` to bundle the code, and `electron-builder` to build and publish the packages. +Host system access is strictly controlled. The [`preloader`](./../src/presentation/electron/preload/) isolates logic that interacts with the host system. These functionalities are then securely exposed to the renderer process (Vue application) using context-bridging. [`ApiContextBridge.ts`](./../src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts) handles the configuration of the exposed APIs, ensuring a secure bridge between the Electron and Vue layers. + ## Styles ### Style location diff --git a/img/architecture/app-ddd.drawio b/img/architecture/app-ddd.drawio deleted file mode 100644 index 4df829db..00000000 --- a/img/architecture/app-ddd.drawio +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/img/architecture/app-ddd.drawio.png b/img/architecture/app-ddd.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..e53e0db108ae34ac7f9ecc5f4ec8b1b6406d4220 GIT binary patch literal 64572 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

&*&P6V)za$$o z>$brY{iV(K_-DTV&MvD^bt+h=k0A{WI=tX2Hq!}mfCl&FON2kRHRyYcbZhdF2ej_K zk7Z=qw*5LToj*vS)VP^>t&NFQ3oVvYj*S?#>+e(*~5l=zpRW*^NGO%Jn(w7 zI|oYFPMKSG6LIL^Z@+J;X_`9(A`tr#qV}afwqxqbbs^IoT5+%Pv^4IB0w*DTas6*U zHO?D5Yfp6sI$XLix-#9-H5=p*>NLNxC$<0ov;d~EB~qC+-?*c(13?`azE`PxDeQBDS_m2w22%;eg%j1lX_8KiyoQ$rG#g zA3~D)xOic2`CX)Z?my-O+C%#BzDY}~6N$q9&;!Yy9ZoI(ae&)1Y?3afcORKlgd8}w znpm#>nC8&WYH8uXS1lF{^b-rXkfZ+s73ROQkCeY>H&x`=)HI8+x|fSwE~Cjh@r8o7 zLt+;rN(YaRu;NFQ!)!?BaNpeW1Q;YrKr+05-EbhMmU#KUUz$2aJG0G$ zA}EVZKzp~cA`FZU=<>zQ_i{JOoD{4a#u2f{!db-w4!iik6k|5`c?G)+2>xjwn+zdw zi_QK7^B%Wo6wC7>#L+?mcf0XSw;GTOmon}xbs%?bh(uul z#qO_%&T&+i{U#!zQqzNswx~<_1dcWGGxxo{d=xzUba)*8e zNskNx4GWL>D7x=Ws9&pNw|M;wAoV(I~7~~tPqi>3hk(H+(3#;Uf$y2_Vln; zrZgW}*tl0wN%jLo3w%)uMX%r15RfD zL^2v}($>7qkB9an|7raD=sNWO)q9c4^chPL?@-XPo=42ahT_;qMCSVH(}n8bEm5+( zWSoy70ogqKzL&YX6D~Kgb<8!0B@*=@2RuOyt@->@nYMf~?T+9>z@Xti!-LEWVT*S} zh^}6RlyX^f2P#L4Efmf|LVb5fTqjhK;)oHSO;v>agHQ;1)v{HIgIllT^JbZC<}^3% z?uck@9me5lNOTMapn)%uwp>v~(a>Ig82ah>N1J9a0;GxN8(av;`U4~#mnYJI)6KLoeMhsL7_-??c#}OV8|uAa zC}&||^_Bi|Lfv+@Hz@t`w{R|v3c51J7M6t+(YXT>0gH4^5k(SLftGWVvxZI#Hx6{e zc#-(seNb2BcOx- z{M++ng5XyVLWL;4 z8zTUwt$aDFUq`FWHhp^UfRbs7(i9RI_wL#M7h;^mQEUd(rl@u&Up#kob`{=>Xw4L4 z^i=PuOkm;i9RLRUJ_2x}Py4Orh()_8BBr<1?dz5sO?M-~E!L{tHeGpeWmXrx>y5D^hA-hIhHV(zGV=*xwd zBN~JS#VChlYbmLiNzd6W7P%LKq<38iu~BGlL;eV5AmhmC*T;(Jg*sa`Mn-07DGj8l zpdhActmiboU?>nvQvY(i?Lc{qGL zxwbewXaPe9Paf5dNsIJcp9q-iCKxVe^!V?njphl9M6I{N@v(+8(>m0ARi@5!k-Ma< zj|J9^EKjv^F)2W!D+>F%DeF1l^^$C zC*w%xMQ=a68~qefwJl)JYWWI(OLpLUbH#)Rc3x>9f^QhR^jaJf==s5Um!r<*`2W>& zwm0SGtZoj{NsHwZ`r5rtC6T343*sPX$+QT|KkJkiz4Nt}@<9SVK0f=I!FnkI>o)k6 z3SHxRXH_VrjY+a9Pxzu=p3id_-d{&g2qAUtcVk#@^92Cdmzc%^B-q%F*o@>{xi^+9Enr6P8s5BXr3u|+m6uW{ZJF_m9@L_sf)g{Ii{WpTrb~c5K&MP9U4f{5(9T$lRE) z3)&D*^bC=aj0Kh7b`Mietl*v8Cg{J{EIlfmL*o~tQCiEsJe;El2?>o3s1mLjuyOyj zLx(_6mtrmELiU7K#6Ka<)ZFYJ!SPUWI4hl`kRGv*bpud=MFIcU;nP-%-D6nq$}PLr z_^%QvmFM5pQ}OF7nk70jH}*JW&l_{JKI5bdPuKi6s5}+|Z<7262?=SI;J(ohqP7}` zOtq5&IWv027hsydekJluh z97PvF-|A!fun_SbU)t+~{GDv|q?pUH7gaf1APpM-8kL~dK=zOwCBj;ugHZs3%Vv?V zKCu*eK0Yra>D+)E>4v2$ zjoZw>-H88vM{lH`{*2Pv%D5&SUUCiigRF#fpC0X|vAq^>TNBe-o$|owzk4s(Ov6>R z-_%Pmgz8+H*LCv*?wTXL57AefmZB6$5!l%1H+u=4##wMuGjQODyG)j4C5iQc(NVXc z7txajSA<7V2IV5Y&ve#{fa<-UaKMO)M;J>Ay1M+$E6$~+VbK^K!Ebi{V&H4L*3pZ8s;@qrz6e<*zaETb)xE-p%ceJ(PmKp>Phmu=)8x zlO5v%)FOTd<(xkJ(xqv!kE8W1*x95@r5HA3;6pNclI)1f#%!Xy^wE8s6e;z-Jm5)+ zSqy^=0mJ%^6nx*N4g)|`!mHy;=YYprhzotw2H+l~n;|g7_FFkY*uSzf99s3F8h*_? zE;_nMa*6sm2c9qBn#KP1S9WNwzeJg9E}SCthohM+Qszf1yk8k5v zFrbQ*`{Q;yIJ1>5JWS+%1^G+{ZA1jBCmoeFZ&rsd^zJgihcyhw)?38UsCAV)ns%gE zm_Ay=XP4zdYDx|yM&_rflN@y(EG7tD@+~WinBBB*m%7u9rb@J<#*_e!ACy(N8#f<|f9BXt`PDM6#yguQ+8@D-BnzxV`(& zg+&wXs;ES%&xBxne3g?ilSyWL)+aqrw(;?Sq={EfJGTBn(Zxk0ZP+?|=nw?`_mKU_ zHs}$x=c%!O5pT@{nR|+^2jjmeXLPj6RjYA^!}kc2!S>0?9}uE(jz-Gc$cftoGdF1K zkCfz|$oTXTN+~{He&_i24etNJ!nCLwv459K_`=c(jW3Oe=HUF;YfL-hnc@h`zSNaY z0hTsg4gK?WrpzG66Mu}j>fBFD^}fXZBXJwj5+iQZ9&>+TDJ01VOdGDi&P;rRSF+fQ zdmP4>&meq;eYsKgC|^Tb+wYi**y57GF0|UgkUUBKIt#e9Je|M9ac!`11xpz|KELD7 zhzN-Qmsgw+Hlp-d`)xJG8Ail2Dy&|ZR?bnpvL0l@yo*h{FLwdI9(kes5YnUV^h%vc zzKmSE!Hy~r;{rIH5@8Jvntn2=ogZtVVqi~bet+>$(J^O|{0+F4Clog`&i=9V4N)EM z4I26OyR9$*=7=|7xYCo(r~GYmxv;3xK_#MkW8aAtAGOx6`9Z>K^z%KtmO>A!_meu+ z@s_CKdvz{tX>>PQui|@(v0l=;d9{R{P7CC@j8Zj5#}rtZWH@lhY!<8 zn5qH(YKcwPdVMw7j;6nuHx_LFm}!y~Jy?%-f}oCqYja4wI8GeCmr zkU%R%rJTbXOG5w3U_Ea$xDztFZLH5*A^7&L0G)$6rbp`S<`o8n0sOYBbTv=Ep;jbc zEGrH;;Rn9|_3ZUxd6$9yfsrR2urHo`x*o|=^+5o|a(7G68I#bz4u{fe(Ga3D^4qdI zvt$M**O2#FPer|aG3a`S<#$UevqGQ}zU#>yQ{y>(lIn)ATaZ}9vBl>;O`H9n8$#1f zJOE&`!^8nHV210Uy#1vc`M*!&xe))h+ycw!kiii!QG-+o4f1~X#JUG%+Sb*3j~$BN zHAaI)*j$F+-DnB%@Bm~WGT18RW^qEwhyN2_obb}mBPvw?R z0}oN&s=YJi)(CN#>BA&rkq4WQK~Zu8?Fe1&gu*!$qLcI1)AJIWgPynV9GlsCpevxW zd7=S}ZMr+i-6QR1wb;vMG1@A^_fz?WUV7ke!;RsiE)0Ffe1B!Q<2FkgjuY8T+b7hB z_wR0EZk{h0P`bL_=&1O=G`ukv^1ciDZ2a)o7wCckAC4G67}_G0F&jY-SCTSzu3<>f zBCy?x9)BV$;@244H47Wr%1Rq}r4c%OzAc1*a@_FC>l;9S@6yJ0(!97$h?xKDNb2vG z$xj1Ho?58yA|`I@zM`^`Z4^iuM(Ux}rtIpD3>dkl8&pH`y%X)tX69R#uIxub^bcp@ z0J7G@Hu0F4vPV4#D4)*rpS(Og$j88v3PYcLM7D%{acx;)Nx+UGOLOU=NY)JCn6%HI zYg7q3Ui^}KpIYnXwIDh#GWW?9Au(LEMmTxP_yy|pj1NN;B?#p;_jKp+O6UNz`ptzy zbSH2SjRyWcJh18@lhcK5TyIhVLeG&-wyR^XoEWh}=SKh^e!3wBy#fw8&UXB|TXFrW zo4!DeU8IwEGN_eVpPZ7s;%EBvV~UB4$aHCuJVa`9&F-GF43vFF%xvFq7}Tf@*h13; z({2xhQ=ZN3lE)?V=Q41ft-xz(e|N-8Z=A}P=)X@#p(jx}h{f9+6rcDcB&gpC%n8cf z=fYV&u=&c$vF26lKRm-{R`Zi#O|wH9CAgF-cnyHcYk<Y=ni)S>X908&|gyZ(OK>;rx^Y^jcQPJscDj-5!Hyvu1oYF}G$4F`te&xYR!GE0N zYJTA4XJr){La#X!C8VY~`1$z?^uMeUqD~ANN8?7{+Eq^@u?2kU9Y(dK><2*JD2gn) zV7^TZ4wSPl>baN`dUSYUS1kLSXQ|n6I~}xyM#?iA8Nuj-p^Ogy0+c-TR@X|djVs$x zwEe+S112m*Xp7P${Yw>o8LdEbpdhNMQsujJ^zR>ZZ&W9eJ%h(GlAYN+fL4UyHAg=J3lh=bB~q1_j5n@yyl#f=meH^4uKG)7wD_pHOV}DF1_`nhB4_L|rAh!@B)np`=36yArW$tI+xhdAeB4`3 z*PphM`*nMOY;<@2rUObR?7t?jhI4v)%IhI_x!Dl`gc<*bA5l?JDJDjku+}m*1Hmk* z%m$E+mMguru@-qVoM`V5Wlqg02?KE5s5WxUp?)a3(*!HAW=pE%-G!s-Qm)| zh7eVJ+SRL|VB*3l)ns${2hKv7vST;n5B_h(=AhBbT!9dxoPbbr0zKVvjLe?&UJ_`$099TjF5$?zs z7z03_rE0eRzYe6qr;A{1*mXUre~8tyiVD?w{tVHZJ`xZ2ZO#xgJ1EoH?0Anw6v!B8 zVAS`f`aa++_R{jZdRZGr2n%^4Go>Qp=Q<6b_7&=R@@QO!CIyOu?x%YQO9tY^G!)Sk z^cwmLp8?fcjo>I#IeNW+$wMQ;|L!S;AcO?8;COpRiFsQy6qT!rel@CFfCo!u2HFq_ z5Hp;@{ou+GF;=HROhc!9`~q}~J2neNFJc&uZ)cvm(2401`hB5RpY^*ZMFQ%`Mlj?v3Zb2&P1a^0(^#%zDYYD>ocU9z5%L9{e02ELH$J;|HW z=|Jc)10A+slD-%TiGg$8Aj^QqZ_HWW*tfCq2Vkp?03H#`T$ar%JOGF-zRb=W(tn+} zo1)|Ac=x*VT_*aP?CYuF%J|(p5}2+P(mLZYcJ~iR%E#tFK||-Zx%)I{*$NAJ^HVUW zse@GexAW%x@C0WYI@$hvq22EjRU|0vTeajXgg)#{pBgektE_-SE*>16K}V%~aCETq zqNM}um3`|AzLx{aMh#R>$^9XP>$IC)D?%DmkGT6ql3R? zrRoiD6V$q*%56)wUa;h6jCiN)r-ivh=Phr+j2=Q#XcBA|PY;=jD*IEb(TqUMX<54mS;}OvuF!McU0EILy?J(}r0HK`Oq5B|Jo(D$%)q z1wVrhP|S;Seo-gN2T)c3E4p|$x=Ae{c*A0*+0LL+cH^HsndjGM))ilw0G742wG3J* z0Gv+NN)P7T2dEb}Voxji9n|Vm5hP8P={q`_#B8I;cu!gzcMo`PttTYxFd&@)p& zZoEAWokw9}d^@;e09PEvyOwmrYPX0Z14+is%HwMPgG@hDMWU)fOqKPAQW$ z5qc|6R*_K-%l8q;x_`gkfHg{D>CMg_pgaDjWktI=FqyA9;t~tB{|&s_0v-%46q4@$ zk{-YdF56mlAN)j8x5P1=H~CH1%egF57zS;@M2fyy91QO)+dXjqkY)8PEW60}GZ%Nk%puJgXWd~6meKSO1-sun|gkGX|+3(!}1^Z0I~l*#*WFFY4c8EUv-HLc_v)t=DjA z?qpvAwf08ob%uW=51w61l#cDLBwTZH!WXR}5_jc(LmPrGWRp)ip(LSDuJkon;X{Ot z=?Gj$6-^kt;gr+BC-{i&m<_0HObRRMovOF_PE^p&qpT{2Bxnn2S&A+ z2NBLiRiw3s&N|1o-yKRk5g+FB^8-)T%d>jcTELt9GqB|o93R0maAB2<_FHm6B04%c zMqci}z@yGibmFihWi5lHzERp|gt4*CWsW??VCT|ThMV;l2h;T;X7_(iN2W2QxbOUz zZq|nHWwdAFM5%oVFy?Zm)c+4?`2gY6+z!&EsDRS8g87lc5a3FNQ&gDpZPDq;W7*@J zF55jbuREX96qR3~B?`p-&Tef5abn-lOy4$PFpBDI;PHdnk>W+na|EIkx9k-WU^3JRrCRiJl4 zE`&9)+8DGEZe%)N00R-97zke8AP~NXc^C@o6JdO7HB(_D;F{$$Sj2i%W4G%HKR^Sm z{C`*gK;kUFhj$MVz5Ity_hVPw5gm{|nJ4-agWFUfRkPfmf)fsaF*hvlUo~<>xyy_P zhxWI{ASe=38iqi=)&(LS>)LhjfFMO;`o4_RCf{p|Un6D$nChnlE=G@Uy;UE-K`c_< z;s+t9eotT3wex$VLbWCBcq8@RP#;f%Qd_Haw-VihhPYG~r&Gsd%J>PI4IADoG02D# z3l^~#2+X@v%m09*}yCDRLF?<&g??KS%Qu|vNckpZ?xA`rJ+Es8)w(ZVJ%O( zE|b-)F`M@TWNd5<&S0=wg)Az8tS5dbHIcC9|F`$Z4-^XIi)Z6>>wnL$^q!0kd7xu~ zBxLMGsn*0J6;>h|iZ_)@$zWj8H0)-*YyKYsn3^}EAQ(Ia6fcac1(rFq!mk{CD*(zp|i{bfv!mjqP&G|NZInnPw|fwyJ(dj z>-j?gdQS`@Zu4xXBq)D1?HwgxfhMS)HIwu0-5qg|2&VR?D~{|uPf*n>qSoB8Z(gG| zsf6s5IjE_bD5g#(Oed@|q&AjK-%pi!qx7tE+K3(Q1N0gj&5Lzss%^^iZlEyeez;=CgKr;k`_UQx(hZ?<++26l|d0n@Bg$-b}DQSS;ic zauUetBCop7$9@2NkblfU!8U>jE3}!fTf_x92Acekr|5d=gcw*9_6?Kk@Ql?R6aPs) zSN6)9*fW3+gNNi$T$oz7u%M{t<{iOORoT?mcs76Jzgr) z12D!5nh6+a3*qJ@Jiv&gj-@avY5->-^nc}3Ktz%d!@w`HoGy1j?vx4#u!nxBC0Rzf&jvd_k$2nVgIsHJ|wl0l^$^qNMd7 zfx>k!3y~{QR)k$K?rv2KuwsPOkDG{r(fStU*yGPyBE2i(GPq%f6|Q|O=%^Q8v=rpG z&%rZL6REDEH(n|Bz72Pe!b#95B-U1KnF^L zGxqQJi>cfeVW@f~gLla$I^8Unn*s=Yn77wa2u0>0!4OFH^4j{sO@lqLSx%Q_*iJv$ zF*nsf8>agS5BD6=XGoVcMn*o=_CxQKO^aW@Llt9NZ4IHaJ3U3ttS+`1F)#KtSQ2#r zy8l@J!B|6m&~P_ZZ`o2`v&0)hf~C-8h{``T$}M;Qzz891goT zHnLWK>*x@*IbZYwRX%zK24JG1qT|_octDv(HxPqK#fid1R*oYhBg5%-`#l&Q#~c83dAo;pt#>Cg@*GO>BQ z7RMev20UcM>d6fs7&DaZYJP(lYHn`aBZkbraST>Y_hKcE<-3KF`S$h59n3ufomd$@g(kk%Vse%6M8Y^5G@;h%Tu+0lmGgQeZ}x2E)-^P5!OggerR^3#MbK17(e z%-l{_=Kg}xE<<`aRaxy$Ak*EclbI9s``Mi9QNT5uqbzlh@V z;S38Rr%I>G|BpX;4oWQPH+JW9)zNigF^0>Ca8v7)vNBqT^0TF?e6zVCPM~WD@X=6B z17k{y!_oHfap^vvOf-k-FrM{-e?{$~=(wRmcPyO*x=K1&^ivR{W?#06oyTp_W z?go=JkW4I}9XY*Q1x`~hP$9n_c=C~_(sWEt|#{^XcZ%$`369$R+PD@Ko4Jep#ctk|99c5(V(Z1tLg^~|L4(pI~ ze*@0WOf4Uw{lX>%>yr#FnS%b{NB{bQ+4HH~wSE7hMO2Z+appKD%l3T!-Sb;HzQuFQ zN_maRdQ~;MMx*`Zk?O-@_HFD;Y>j^ZK5Kw+4;X6}CW20PcM-e>sh2KmfUakha(YDK zb5Tyk##t6-(dUibR>R+9;yck<^oo1#CDc8hN9NG5%1jW7dn*@-^-CbP9e<{4O%`65 zGbs@3%J5`|n0NHzUM{TLge1jNI;9mYclLhfJZ{JLr-;mP+**bHCE_&a9`KV8 zd_(=(UQD@O3j%bYo1XqQ%aoegtk@SBOiw%lyuA2?n}n&BY+=+|%P0FN?lkO_lp-E? z;6ugdkw-(!Nu7l*9+mG!D{95kXw-|70R1nxW+i@eE>AT_x==isn~h->>e~Tcd<9#Kf0^az*-}88s+hUisz>n!P#7)t#yuOjI=r^ugam?0VMs4KKEZdjv@-d?vocs9KT$K{4E0Vh6^<(=)r4J?JYo|GBZi1J zx-z2p`oBoF=y)ZPfNhNpTE=12D(-`sK&|qyEn`-vA7096hy|-U8W-w!;7RC+XzRzu zF_RMU2Y%I%MF!7KQOe*2nBAEtW3$ikd<%-x$t%}!LkzA)C!Y6Zs#z$18cOV0KL7Iv z8a=l9vHcXn@aUlD{2m9lK#-SE29MQu-~_)NdrGpK^fepiGm@`UPxWsh;=tngme~Q; z=iz|;ix|brAPgL=Qd!K>*pn;n3xY`z(ob=&Lta|a*!3{$&32rK9}NDfoM#2gC zV;lQ{_f;}y_2-}A$KCPlDz6ml?gn-|C@6lRFkZ>w3-@EUTFX$D>_(TIF*Ab|H?090 z8uT|W)DDC<${LD}X9MoTjAVSJZ@dS?YQqhQ;%%R}7_BhBzme{XuWbdW5WA_m34nlV zMy~$1HeM`YeURh)U-nZ7oA%G}q@^6)o8eBmtMQE6^UO=4uBfi}Ym8k51v~RnigZb( z?{SqfIzB!=p3yInDDLYOupe8QqRlv-Y#ahz5_V@lL#Vv#2q<6wywKD?FuN7Zmv*_g z6>XG$rS*)QCq-`P*JAD1@Y{=;=Zq-kGJ7B*#+WeR%9V(lSk`F_CAFksD8#-;Bom+H zaiB!4eW94^{BKsR;enJja=xJRUX zNp@-I&E!W1->ieZ9mO_zx&-vzbK)nm@t$eozBa-VUU0J2O7UpvWUr2QCp~FlJ-{{erIDl7!@T^)C+uqM>=5(lr?WZk?98(?LTHCvL%HhO%Mn&_W1_o-6Ak6=2i9JA` zas_f+mXKVwG^Kuz$*|SVJGH+wU5GVQ^#D-!E~F#QmT+V6?`TyGeBtA}-R`h>ij8(0 zOzmd>qou0dfrYYRiOTk$Kl2ainLJ-$9p$cK0*566-CFmO+VJ*0o(7-kl{x?FNIlH8 zOJcz@YfCXkRh3tyE)}ddL5;iAYLEKOL6P-cFaT@3+;V|Jf2ur4P~>F&N!p!1NZFB; zr3uH~|Ec%2jnUgNajvKz7?R2KdlC&kHH4ULkahR4L9##Lix7^ZvaQNwEsb5GTYp5a ztGIQ(hHh|sEr2d+6D80` zk!LV29rH%Ntf~IMN=%K7EM%qKL`vU$7-~Q`L;m=Ct4Rm?=fh+|Y-+{;`S3*EjFTr$ zkEiyXZ}`~BI5yrw*yw8|I(rMD#h9JQ=637^wE#~d|85!u!x0r3BoGf)P_8~!*{%Yw z0$MZ(lxiFlw)Pv7osH5;tFvZDF!fxK!_UySDkc2G`xjfEHv~*VlIinV$b{FrBWc@G zFoDPz8L5GXT&CgQ&;~KqMD^Bxy#AOtaijfRlT=~QTO3W})li=nkLgb=cYSkObzXD* z@rxd|@^PYR!78sHG*|BSA0g`iZYXn#?(n7hr5k_u4cHN2+JLwG&5fX`8~8bAyd4#T z1ez61cBXP**dP3cxE(ac{*&MPkBqlcEmlQ#5Q+g*O&faS5XQj)*~}y7>btmdEK}%hOUc%Q|M=OcDOKVd^9vjrq_km3Ty&@QBHP!Z>WVPlxltH zC5KhbyoN)aUTt$>EL0(VQG8$juf7t8)mRq}YlVMVtLo}a_59uo@L=NVM%t^?Ta7lY zFP}5itT*Q(>Fi+QgRs9)EVGqTs*fdhsTiA}_Tou=v)Ej1IfpkfACF-49ub)9Y%O1C z{IE!+|J39Z>0WiW@TL_K+PUH$6$8~LJ#Xuar0di>k?{#eD4oT+jBz7m3c{FsBU?!aJy**im zgp7gO-3su=1!PZ`SD90s`SNgP|4eTwa%m|A;0W_50frH7)pCIcV@%fhpojq;Btpvg ztnSfMGVReI2_yK&;o-iK5q+wm|NVU=>~09XR`C0|B`>cREZNK)H~ZB* zj=F`ko%P7y*~$&E=${b`;nzIoI(p5Ce?&4JsTsiJHRx%NAz0c$O{r0t%???QEhM+n zQ(evqX;Ya!esDV;%c0FJOv#5qu-M<(F)`s&=6Loai@L!poOYv2LJHvBtrTOg``o3u zZ;U}X70UarTjmJ(46fXnFuNdJ(aeX4K^63WyzZKncEq?~8LqRzX4IyFmJ@IsjbOoq ziA7!T>Bwm&Iy^PAXcb3mgZC0Wn^-K?pcxan^j6R(Pp>r+3>D)M^X-@vj*)|4&rW2h z5O*e-FT&3~5(V$U_s!ZRlv{1ASmUivwR4TXaqTMEqCgWqE)YUql zL)}dZSKDFWUfNRs?z-Si<{F=3^sgf{+5BJm$dj_)4nI$(P9wO$rud)DVig#CpJGTeJQEi5N-CpvbTL2$TYR`A*5wZl=XeL4bqX4irCvL>$L^uVHnObqO!% zu5L|j4fb;k4IV&>#Sj6`;qbJVGmQ(4?a?vDX5?Flt$5blnL_$Mt4+dh#=Ja0oNiA~ zjseQ+3lE{_psw(gb85*V;^~E}B5xek>)`}mvs<&?>TC)AiPpw;%FI*zu4325kA>Q- zsk&d!KbhuyjV`#`e&zi$JRc@x{aM)G^2Icn)eNhaHb8#)WjTn&uoIO+*%=a~r1m2a zTz{O~9Y1VFzPZ{H=C;n^_dw`x3EdkdEc{slxL&Shvu(j=qKb3_xmrK)OdI_D1bWcf zD!0E>6ofR8bI)SMs|w%u=Ssl}J3oSjp#=-IXsH^_ z!Y{2{p`aswkkRvC*>nz)y?gNbclGC~lu=Bdtw6qZtmfsVGUe~!`&hS4VRk^>3m;v! z!{q*+`qcv@>D+?f1*)f=5tZ=%O4?Jpj`>yu67jEn$>f4C4bt9$o#({9qje8ApFZ>j zSEYw#bdOT^c(;&~Ng2+8K$dj>ae^j*{yW~7u&_GpubG1Y4%{YdFt0N$PA(Hn#bld} zM)Jp;?JBn5dfH3mP+9-m{jX^DH1%rA$E=N<62j%q*&pv7wnYK=6QJ2dYfo@Ep@#=OUZP1pLY5i zK%;YDhX+I!&bWME4eQcgjJ1G|5jl6&+-6gD){T=Dyri!vX49I5n`alIK?b``|HhrR z@W{`9X`hzg|N6VFQp{teqw ze>LJDCc){T#F;i2^J(~R6C}>t4p1^Z7!*%F3(@Qi*6~vhY_F8rUedi0SHGZRB>f8h zF|7{&&QFU(H^lq>*AC2Xm@U>fF_7xJs3m)5ya-_RypG_hnB3Ff0?LICgpw%BALAokR40{E}WIK=xZMq%9hIjmV zzRLA+g|){rlJBV($D&V_ZhYic16W8N@G_H(bmY5F?w=;uo|PgoWZIUXSC7{IiQVg% zIr~9O#)aek7?P@PzC!E;|{qhHBxyZv+sKS)T9qfNg}`b{=4FY zy@qvbf>Jh)o&qtTq`eJKSG?+#d0dT{n+q%lb?%0Xnn0d9E%YNP>^Tx2*;aCDMBG@L z|JEBT8B!kx4n|m0HPl~?3`U;KjQDp4xmy~fDdQS4K|0&d6yqpjvL2$XFj^HxG94x& zum&g=6XYN6FP;23iCH-<-h0b^#mm^*lp)pds)k7(SE;;LvznqMiVhf+LGa$uk-;mo zSxMiLi4aesMYNq?-sqJ+eqnRkfbcqBb{A8|l(BbK9$^0V#lv`SuP5T7_)brL=n+)Y z7NedkLU|e7Xn%Jy-0TMRawU+z)IWlJAY4IZeX6WjJIr`Vpi-$9pGUG(in^3|`Ed8X zfl0g&Sf$rOd$W`%}g@vDOPvm_p)Jfgh>&x0AS%*Nww_&bNRqY$8Y`w<>2HKH6 z%grnK8p^{-*FPzH{M3{r2>D;XP6*VgI=fnewG`ke`-yYB)etipm zDq||&YOawv)~MR=((~K+(U$k)XOf8@=J#LAl;d1Y&@UhKgIvry$sdeN0)X*k(aObS zgPHmlm)C^MUbC~{nbR0jaf~Jdy9ErIsFAq)_ij>J^0dc=Oumq0BoY+=Pw@+sk6qE0 ziZqv5oMNnZ@i2wton6_+2)D5!efTNZGP|d+5m?XQKT{sZCB-pfu3L@lh6Lfzx#>Ti}tM7c2H zny=_S;6!*WTJ=^vQ)bEHj8|0V=v|gcUHe6*8~@1)q9C22bX`4cVuDHEJtdnX8S=!Q z8BQ`dV2PRIPdI1LEdXQVadxb~JbQrr`Sl^Ih7`e6k5=^9I$tZYN&0fHOLD zc8LAJrF3#7;si~vu!d{s4;rY7IboOmttt^nYkYm@4Gq;+S6C|%NK|6;a72*oX=6l% zK;*~s_N0`mBCL)}zz!vB_H0Y_uQcf`^BM`2G18aV%;>5bGh=-4dZxYY`$4!L4N5$) z1o6)53{M`VG2QyAqGcJKw%j+J-ZO%h;;`KIz;q6dD7liOq>s(XiQ)5j%H6u|RT<5ou&?#g1dE2a<81})BUwKi>F+5D~}wr2Yb zr<-wo5--YJcrZK2mc*1HQ?fKB^;A55uf$5_4drc#0uWuZv4@@r@d_;eL9`-kA65jK zgis{nQuvC+vR`Y-%3ayqx_*)=3?w>KNC89F3>?=dlv6ofwW#gVVk?uCU=%xH;rL*f zGv-ZCrN^7M0qTube}^$RX!+>DlFO`+-}pLLMH9Ni@+6ac-GoFTZNkJ@R1fXJLhc>o zQqo=kpi^N*a=ra#uXwzP*Obd_2+hXE z)=b@~t=s`o+8`(=hYU&t(Q6wAGDY{m&P@8X@C0RNoR9RGk+SgBM5hAeU2d4h#`+!; zRAkrnlZzHM*P4A-w;dV;lXz3YUr|vads5zLsNXSeXh_`o>lBln>b#Ad8HGiBGv?!> znKS~JrhALD%7p%ceI+L!JV8@yf6C#O1r%w0WqA<9%UPb;gHcdW#HLiOe6-(oJx+r0 zmeO48eYLo}8qT6gki>$52%{mZwkFCIK>J6u>WloOBO<0NuSLah(n_yvqn-V89v(!h z#7(Nj9he>XJvKW{Du?IEW6}vqSR&2-%8;Vv$?fo$u8iB%S7w^dB-fSO6vx)~7-LtU zcGXZ%02JP+Tx%J@)20U_uqpMo-e}!9NYSybzHn^N1I#X}KqEPYR2uOzh+e)@Ohqw2 zD{DFXM*|A&W2?D_h|~~Vbtn^DiHNq>-Wo~1ouk+jHm1#>!=S2=&=HmQ7y9G+RSu`T$%~ZIZH=Tw-H;pi`TG+u7?ID*53_mk!af8%}3a< zZ1G~({RWeusk{JRGQ~6407`a|+U)1uAyZ=FI=79IM)2eYkM8LG5Hc>If=NsE_kZwo z4PsF;T0BAwazJUGmvRKgPz-*^jP0BAxz3g=M&gM7kAM(ioYiWZaHmtj$GUq|*cd*N z^^3}+p?;muTde+}Au=+uzJxr0S|;%mh1ae#M&N%tX9aR{M3LXX@}^}~d|<Bbp4OdFWKxQxgu23Y-9p7&|54DH>hdT9gbe(mMAVmkO_cD zz{B@R-W&{1Zcd7G!~ptA=r7Iv$s82${mAL@NCydz?Te-D3L*>^vCLvTj@=Ht#U!Mq zG1T?DXnpnuqnb0FaLB$$HFekCYs)wt-5k=CMhlGL)zr6LNaq8D+Cd|hzg~k)f`_SI zkOF22l`PQTHxnf*iB_KXoGrKNu?B()tw%6WFoOO106{R-k9a8r`U6?FWS%B9vI!(3 zH9mB~Lgfyz;#BvbJmXsg?;`CA)t50}{fu-hH!*>NpNWU?nTE;>WF3y=V#Sc}J;^bU zrlmE~;VuAlQG91{N&JQ)KD?5xqT_|J<@3b{L>0BDtpf}5|D-xZW#c*=Gcz;%@;@{{ zjUu2XJc9$gdo94vky20$(td!_v##W?zm6$KX514$;b84Dt2-?_zry8D=n?SP^xqEh zL{*S7Ty>KkFs}2S(U(a5aI;+{ixYOFpuX#mW99U|hiAy_@IO;?W5Ti7@)6xsDM<>o zA&gMAN|-K@#N~Fzu#ReJ{ei0caK41LR9Cgde71w9PCiiR>e!#}GB0H8a07F*+~pyV z*=@i2Pp1whc&~WIe0v-6Dpn|yiC^`LmC$DwMraQK7KU!NFf%p!|F8fy0gwp2OFBML zQvzR^4iqF`onN1CH8xxPl^QVZGFjXMADbnLl=uSPn&8QTI=uTafwKHB;08Ikw5bUY z%wB70j1lKQ)@sv4u-a+l6hUiMg?vR5RojggTdQ*^HrmLM4klpoe*__5h+%(mWXP8( zO@o)5HHbi0zZO3Yox!nQs+aD;se>z{iOcpcNUGR0Bz2$GHIy;i7(+T+Z8%|7O9+p( zL{Mqeq3GF7OxJ#WlSt~#9IaYHeAYQAuB)wfA7YU5`gH5zg$jZM`0}`wIaz^^etIbN z3guX%VIX^Q3xpa)wKtk`7v(wU>m5pDu#k*7X^TE)G?}Qiy(M?v_HH}!9wI-`N`5}WFVoe|ol7eM!0uACC969+U*b=<27^FXEAVjLhveX)M&$iWGCDRUs{8WX z)>vJQ{Pl6&)!gKxQr}Trq1%u%C7|j4ayZi7g{ZYlWIgELFkj~n0kQc0ptx&Va{jY8 z@N+dV^v_rGzd_cl^+_e5OqDffx)e^ifM3N|bXJYgMCfVHm?IP9MnooU~%^$BSyheqtHh0cG-*)AA)NMaG52)pzQt6p>>UMa+jyl~2!kKi^tgRc!aClFi$o|@GVqvA zeh_elvjP^yG?SNrS%Ouq&3F@9mq-}vt9l$pgPo0|Rv zCY{U*Y#;lXd0(%KIj_rGmNzif68TtG8SDv0X)0G1A4hV9nO}2HpA?qT-M8rKm))a^ zx?Jq_nyta%2x~%X_!6&*jDh6jFB$FHD%{h{z+@ z<@8f~Xm#6TsAqSFGwO1!=`XhswOV-~CnrW8YIzV-r8i|5YT^}w7LrC|w*l@JG+Dlb zq%j_yZW;!LK_G*w#$X6BW>Vg2wH^k@9qc=t$gn$KW^TV7r3Iya60bq93l`N}Qt@hs zC1`EJn(y&9Qak7UvdY^q*5={!>S6VF{sTs{e$-96O27Hz3=X1#aK7upH79eXH0IMD zLbXBYECL}XKo6fiyN2CDS;v>%eqW+or)yK=m*(Www67>4gwN|qv8JUUiPlnBciW`5imqN{ z7jTSi%cnqKq5akN9hj(Z99Pd*fP6SfdDRUFOfKstRY%{X>)$iT7F?9;-Zh_!`xj~zUpzSnKO%k9ipFr$u^qRg{jj2wipJ|s=G0lK{j7R3ud_Q^2 z!e(_Y7uU-?1yvRj0>Rpf(d3)0H&bM&M_nOvJ1I;NB=t`4)&^>V>eVl_Sm_c*0s%eO zAtdO+nFo_W6Ix-bRH>}_7;FD)Sc-<07Otw}5tq>OT4?eiE6~J!FN)*(W{Aka&pga! z$LnchDCwc8>m%w8v_X|l^G_rM9426(E|N+EWf_}0anwvJ$Xv}12B$-{4*R)5#O)oL z8{!|9-NxF*$fQR5*vaeK!x<-ZNiAp7{Yq5tEk_&R{$S-hZ4af&K2cBOQpoBbL|cuD zs@i0!kNxMpmI%WGQ4kL;!1&2gNcFF6@z)04;$<;~UZ(QZdQh}hYbya$6NBzV8Y>il zV;pGCZMhv0Os>U@2TEX*+Y_hK?Zh!k-fAz93+)mBadbNH1l;IQi1^*N2V*vcWY(7) zHk+5injEUSRhJdG0{#%5rKHgkN3l}8BP`4-Vm}# zj!N*6&yWR2+w(@$dhus3xRQ6K9e7qZDQl@s*GF;yx*Ig7$NjONNC={zX1|24Shv{a zEh#H+ZNYMpG#-NtNljHHtBxOkA8FkhW7jJxcK~9*AR;xH9vE0eHt$xT#n>h>MB0j%l4nW0;5XrkB(Q zs49MaIoW5gU-J`Em7gzm>Amx!gSgtr3%KA)cT z<_o2(hK9y-jaC{`nVcvQnDo2G%-?6-Ll6m=^m@T#$*F69N{s%4rK^>acHhV& z3N7dJ@vx-6CY=mF@^B4Ye=u8lTw!HUz1#IsHg=}Cc#4@bJ&O5F}P{x=9!Zv+3 zUGlfS{qG|KUzg(HmlrYHG?{)g7#*UrK~8^A9hUWjX~aI}I*YK8c;w=Ry1_zv5`$0~ zzu$o+xZO0P7N|x-w8m_?5c^eDB>eXN3_ve zZc^y5P@!OI1w_9I&m#MBWicpMX-{}@B^)?|lGIj-&I03vU3)688;eU9W=+^KkNf>G zZkHbD&7j^agT9Ni^^ez>bH-QEuL#>rGG6HA|~ z75hL67AjitwpxKIKvPP#U=VBz)fU3;JO0BVrHUKJ{h@tPW z!c9x_{_1-&O-7CB?0bc^c90ikc+^?fjh(IKFezm8R=I}7`vn0s`~7`u)6n*u*RQF( z%;ud(A(~OZ4fWcAtu_xxn!~GZrMjvV98=UqX#+c9CgRyebMWc%gb={z^qtgd)&IlJ z8e`M>65Li6KAssG;V9w-W@Q7)4P2cpx$tFv%MTk(!UeO0vZPc$%7ot_=$6OK^7qCTy&MN>XoZL z;8(YE+aSSpx=Ax0=5(@xJv?At8bFyh^;r7cnc*78RP_^9ji=MtH3z%A!^utWZ#ZzW_-48)d7oOdt9>_n&Z-gS(~@FQjgh}pRq6b zx^hEqvj9z|tS&`R2SOqSO0QHlAm}@NMNf;&U?Wl(dvbTcUI={_l808Um~uLy&rYOL zC$;+Hw-ELER_+#xDv)$hM6g4^&X5QOZH`!(9haFdZL6N=yFfi2&wGj{GGZ57=-m-d zVZw{RIY!N!cDoOpAjHZ(rRrwSdyx3<3CLUvqv}Dsppxr!k}b0u5<%{cE=T%A`q-sE zI@?OYk~yk@>2DL*-j#+vsT~Y0P1`T>0k@?^&(5O3&mV-`-+rt6tY!8m*v{2#<<+gm z@;g#tAuD@&R&@h2Lyg;*F%>)Px`z%&58t;edOGIy?w&EcG1KWSDOnw+fhT$!O?F85 zYKbPgW$rz-gq7>9$uKawFnKc?pUcwH?p-kDOTjTb-MEHNWh?ca{hEXa*ot&rc0)lT z%axjxI;*>9W@vM-6ab{)!mjI1_Fga}xL;8(3HZoh}euFY0j{~*d5kTMHhr_F##z6vhJ@t(?xHrvBN zv!;3y=LDR^nKE%*HtaJ9^@bkvN4z?_(my-%LQRvmR3m}N8s_~sM~|M+aMH3!vN%<6X8c~N%HLR#c4o!CS{cIqkN)n__+ zu=d#}U(uSjfYQqPDb0z`>ap}_%Yk~>!KD6G;0*uWlbG16t&FIV7TpXcT(lreSl}m> z?Ao)n$>7W8EpPT;>|z7G0#CuB@k!*!OALSOz4$kS8(!|`tn$POPGAw)3=$hTb@G^Z`)Qt7xh6b8hZ5_0vcAjS>RQ9 z?uKM{?M7-fl?GUF?EOXTl9F-_)NxxMPRR)cD`#Q$yRYK%#joFLw&E7K5Uj4u0#XBC zTfWyz2#x_{&#O)>b9?f(v-Dg*0B5R=hEbL zf`RPtYxAs89FmDz{1tHii$K6GvkgQoe<9cC4ZZ$y84}j$qvAei1o$QV$s81!+{Nax z{6?#FrNvw*2g^pKixS7O$d6H`6zD^rG_vMs#Z1NW2aP__xgq(Ck@@8rJQ;b~9!nVk zLNy6Wb|#8clh*+zWlB339QBe;MIC6Qx%wxBPc@mz2WB4}4iT~6IAWTX>_1%eaQ3cY zOx3+((S^?Je8$z$VkkU#tmO7^OHQino&t)1s5*(su>N{GMqx3qR$*|(-WYYU<6|A@ z&5cd#0kddGs^bA%rGY)c!VnSi3dM1o{f8btFR7BdK1l*t`R8>0| za9#QIMjFBR*mo^A{c7*rLKbM;rfFH{u;@!^ zJDxjjFifkvNm(YpUQP~S&4x`_N@}H#4#7yt{NDwHgKgTrE>|@{Fo{^{W%UN2s@?A| zqdNmM;R=6Ves_3SsqJX9W*={q%K_Zp2bq@2)8)5dG2i zk~yILxxD_SV%)MS!<==6ZaUrF%D%SB;#E3e?A9duM?-Qb&>Ll+h3BS38$`wL0HMC! z4}++i)JV$R4i#%YrOISP;|h4uw3k$q8oEjiQFH`{Mo0PEuhRQET|F0G1A_yk3_qgV zu;Vq~PzVSv9IixmC&U>_fJv3&_VzY5BPF7Jo!6R?hN6@D?_LJ+50awIl-JTTi(9CX zu=9<9CD5{jeU^zK_|#8=j0d=ABv~^yKqy@X z#%kHz1N!oI5ap-MjjL3U8;FY1YWu}Lj_T6K{1rn;KUF<%lHahB^6IMo@qZN<)LdOT zpI~8Ngj7wj{R0B>W=xwkqDCb1tSCW>hJa&)TlPEMURZ> zG6@#X<<*xbw(+H9hLJK>b`CQIiUyJK(!k3z%j0Ky+GU0C7_^zMuE$HB-(+rZvq0 zNasK+BsX#y4+NH+N~&?Uy}cqe+Ydq3MAX}5Thd}^qmrZYd1bzxQb-0tC`GVnQev#o z`@>_#4R(UHYXGK@*%LjI#(Wz;T7W-oXpaL$c&kv7BnMHFi>M%iS1KxJi!9tLlB?k1 z(wtu9+@>DAsTqv6(gr&!9RwM%z;PbwWT9XyZ9curaUkl%Nb*WGx-9EDazeINQAWOKQPU2Az& z6}fUepMQ0LjuFt~DXQ1LyKp}(%|>uUTPWFI2$&Qxx;8L3bdl6I%z#EXZvMAtO|+j- zXzSao@~YDg$BeT&yP6KkiKLMAI|y84MWfhz-q~YlIIJ=G^yjjfw4=5<__+xhk$6B~ zZ$8EdI(ET;!Q0pHw{B69t4qBg(Xna8Je37F5j1;fF=)57{KOFGiqr3Wyc?@DOFx^0 zOQTPw%T){=Z9%Vup?>>qek-d?Q#L~2O;h8#y91%K$yLHS|Ec>jWy&~hv}ZV3C5qX` zyk*YO$@KIY^Y|R&^q2gSh8VOgULV+s-cW}HrV|x~{aqV+ibj-Qx&je%;YoIqMz291b`JJ!NE7$nUcT~Sdrr>n0kH@Msi_Ng9iLmgQE%=LvX zH+R?TNyfP=k90HEGz~AY(u|>s>h+n7faWuO{)Sm&_nGI#_lo_J|IC9cWI{x ze5>;Ow+JS!=Dk$bX2GT7&g}Tqkhgv@D$G=7$YeLH!B)z`GWy6zMp0$I@4L4dD%%yw z_?WX~ETle*w=6F!aCbL_Ur27Fcz`%4w@N&=LYzwL!}rSc$;mb2Qdnmf2>+ z`MiTAR|-yts^!6-=&j|mGe#~esynBcA`lf2f#qVay|sCAjDiY&$7InzmKfcxv~|qA z5h}Sbx7E66k|4>v0oI#y-o)3Rd~=sw4?p=Z&YeD2t@q&K<%=+6J$`7L|8bnk+r2XEhi{>)3yVA?DHSF63^9+q3(=}}^nwF2k!4wYOjJQJ!`1+O;S z%U^kQ=(2o|3KKI0i~=IC-0Z51pQkp}NR{)=Hsj+7k*Q^=O}j>Fm4O|Uw6X-A0;{e= z!F&bGCAJC)tmtZM+wXn6{(zh=zB<5M25Q{3tT*^*4+ z469D*cu$a9dKnk;j}+<6kGAZggC?UZk#d>23HDB8ZX`a-RfZ3Jge38kGZ3ke&$`NV(x~ZfCwxBdhv}7llrLT zA+MUPh5-qYt@SCdq;qXv_+T!18hLzq$U|mP)1OC7aFqutu&$ zQgr*^X5pzanN;CpORBG~Hem4$D^H=tC$LJs>;!_|R;japaz^ccdd40p=`!zD{{~4|NK{26;#fedAkujl3 z^=MJe_hAVID^H-s7qDD`Pj#J9S3VI~cdC|Ob_G`p&lKgCU5!x5wA+n#{d@C9Ec>}$ z2fA2!Kq?>t%MZHkAKy+6L5Ol91$vU5v8BX@C7Tl?+lEJK<9u#ibu%oXVdXBg_yAU2 z(5XxKs`;OvITcf9PUW(RzygZ&xwu+%wj{6gvQkRRE;+!gK)HGKoQIZ`*QKrazNkP= z3W&h+v$2t};khK0RPhABJ^~ievEj)~LPYD($e>uCSXvcWzJirI(DD{o-omP;dL_KHLP5PR+E@$Oc?esm;B6)(=KU2+C>fymlT&` z#Rn^H<#F{ea7~p|g&%>IR4T=S*B82Mn@bJw(?mDRbC6q-S(;mRRVo2nL{XA8CX4aY z*G4Tr0^X8P)tk;~4z+P`8tQ2|dB5P{{1YW19lobQrp6r<%5 z#Uo~$p?j!0x-2dr<3 z>cTm>I{Wjg!+emr+*FJkrB}_lWtR>y9nUo?T}1r9EO@q5@6@L|`?rv5>KTa%;I%He5z4##n8pu1cBK6cgHl zY7*QmBtjco=kj57z^X4;)iu*1@4E_xnm{aM_q%GFkT$qdHd*E?*5~17c}{tL`89{x zrdP>jsw3y3^z#4!1fxkrK~y%z@}t#m+n6CP^|>OWn8pVRh`{oJ$n{yp8*88LrBG^z zfyFSZ-P|Ea8KR5U#!I8M3BeKC7$~aTo?IVa?B3`TC{XaGjeO^|*o-LF=c7QMtt-%9 zw^x|UgXL1?NdTKH7X7BzMz8#p?>fcuLocr?Y821LmKqCCT2X*PeRgSa#SO}8Gs~p3?4*NX zw=oX$*14mW?c=#hu{coyK@<>yC5WtnQ|KE%O>WQ9Y!8(*Xcz-~*S+5#iIY)kvs$60 zgEe6al`II#Dzh+uYTaX#AM+=3>fIKdsTgI(A{1BT>5BFFcAcptSSpp}(~ur9*()|l z7|DUx9$0p&-VGHE5Ebx10TEaph}Mv^yu0=(t(lf~l~U54a$4EfZnt(|V2{u$!}KBQ zC^|wD6B4WrM~Eu2&i$~4tW$lrjqE#>n~I<_mY~>>UtVr3Wc21zEe&b0R4PAdWz2i* z4*Py3V?Q!`^eTOQ+bh;DD!_*VBCz;Sv4NM7vGLJh%Ax8=vC@tTnW_)VIyzV#mKIV< zZVys~T0_;*vJiEIR;vm_kRlW+c~Apa#COdzSu0RsDTCfrigH6yMVYD4T45=Z+Z|S^ zoKjqqNT`z*n`y5@Vn5-aOeZpif1&f8bz)7T0=`v11eS00Zcw#lY?{)7w%glEAa5s^ zE4l)XHa45BISYqEEe|$oRH1CJGE5n)3{$D(!O$pzp;l-?NdoH@W+6Mx!7?yf^e|X- zFj;lbnMzH1OR2fSR3uE+RJ)1FX=`|tk63Z49@J9ti zVEJRO1h`EZKTZpk%1tQ_yCa2`%TlF~w*%N@2g}6S8C#@GsxT?#YO6}7B>O35IlrI6AxP!cI<2@RT-a=%jqdlUtUg!xpJ7b64L4I1nIIOVPEsT{hTP!w{-D)>8HoFNHo0+yTR+-&l zRY@tSo}#6t5|%2GP{?H%CdTz zEqQz6i;;}O6e)2~5iG@qvkruVl7vz$9YNEwNQngDltdB?iA2jf5X7<$H4BFlkjPoq zA$PD2nFJEr0S7GsD3)a@NLUHW-k!QiB!pBFRw9u&SinJnVpy;YC82B*31tOXs|1u; zBB6{D3Kd{ky`6RFB&?)_VQfVX2~!LQTPC4UB5_DcSjtgMQ>vm@hJR25fNl8QB(_jg cz;_D#KRQ6d!{9jTA^-pY07*qoM6N<$f+d_d+W-In literal 0 HcmV?d00001 diff --git a/img/architecture/app-ddd.png b/img/architecture/app-ddd.png deleted file mode 100644 index 2deaa2e2ecc89bf79eb8b7d20de4ecf09cbe0e7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29625 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 uoFl6l5qFd3QlY(-#Jx#SK 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 diff --git a/src/TypeHelpers.ts b/src/TypeHelpers.ts index 562eb95c..c41522a1 100644 --- a/src/TypeHelpers.ts +++ b/src/TypeHelpers.ts @@ -14,3 +14,35 @@ export type ConstructorArguments = export type FunctionKeys = { [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never; }[keyof T]; + +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; +} + +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +export function isFunction(value: unknown): value is (...args: unknown[]) => unknown { + return typeof value === 'function'; +} + +export function isArray(value: unknown): value is Array { + return Array.isArray(value); +} + +export function isPlainObject( + variable: unknown, +): variable is object & Record { + return Boolean(variable) // the data type of null is an object + && typeof variable === 'object' + && !Array.isArray(variable); +} + +export function isNullOrUndefined(value: unknown): value is (null | undefined) { + return typeof value === 'undefined' || value === null; +} diff --git a/src/application/CodeRunner.ts b/src/application/CodeRunner.ts new file mode 100644 index 00000000..6c08a6d2 --- /dev/null +++ b/src/application/CodeRunner.ts @@ -0,0 +1,7 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; + +export interface CodeRunner { + runCode( + code: string, folderName: string, fileExtension: string, os: OperatingSystem, + ): Promise; +} diff --git a/src/application/Common/CustomError.ts b/src/application/Common/CustomError.ts index b219a945..8a31884d 100644 --- a/src/application/Common/CustomError.ts +++ b/src/application/Common/CustomError.ts @@ -1,3 +1,5 @@ +import { isFunction } from '@/TypeHelpers'; + /* Provides a unified and resilient way to extend errors across platforms. @@ -50,8 +52,3 @@ function ensureStackTrace(target: Error) { } captureStackTrace(target, target.constructor); } - -// eslint-disable-next-line @typescript-eslint/ban-types -function isFunction(func: unknown): func is Function { - return typeof func === 'function'; -} diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts index b7f37da7..74f42218 100644 --- a/src/application/Common/Enum.ts +++ b/src/application/Common/Enum.ts @@ -1,3 +1,5 @@ +import { isString } from '@/TypeHelpers'; + // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 export type EnumType = number | string; export type EnumVariable @@ -23,7 +25,7 @@ function parseEnumValue( if (!value) { throw new Error(`missing ${enumName}`); } - if (typeof value !== 'string') { + if (!isString(value)) { throw new Error(`unexpected type of ${enumName}: "${typeof value}"`); } const casedValue = getEnumNames(enumVariable) @@ -40,7 +42,7 @@ export function getEnumNames ): string[] { return Object .values(enumVariable) - .filter((enumMember) => typeof enumMember === 'string') as string[]; + .filter((enumMember): enumMember is string => isString(enumMember)); } export function getEnumValues( diff --git a/src/application/Parser/DocumentationParser.ts b/src/application/Parser/DocumentationParser.ts index 9d1b0ca3..2a177fb2 100644 --- a/src/application/Parser/DocumentationParser.ts +++ b/src/application/Parser/DocumentationParser.ts @@ -1,4 +1,5 @@ import type { DocumentableData, DocumentationData } from '@/application/collections/'; +import { isString, isArray } from '@/TypeHelpers'; export function parseDocs(documentable: DocumentableData): readonly string[] { const { docs } = documentable; @@ -14,11 +15,9 @@ function addDocs( docs: DocumentationData, container: DocumentationContainer, ): DocumentationContainer { - if (docs instanceof Array) { - if (docs.length > 0) { - container.addParts(docs); - } - } else if (typeof docs === 'string') { + if (isArray(docs)) { + docs.forEach((doc) => container.addPart(doc)); + } else if (isString(docs)) { container.addPart(docs); } else { throwInvalidType(); @@ -29,27 +28,21 @@ function addDocs( class DocumentationContainer { private readonly parts = new Array(); - public addPart(documentation: string) { + public addPart(documentation: unknown): void { if (!documentation) { throw Error('missing documentation'); } - if (typeof documentation !== 'string') { + if (!isString(documentation)) { throwInvalidType(); } this.parts.push(documentation); } - public addParts(parts: readonly string[]) { - for (const part of parts) { - this.addPart(part); - } - } - public getAll(): ReadonlyArray { return this.parts; } } -function throwInvalidType() { +function throwInvalidType(): never { throw new Error('docs field (documentation) must be an array of strings'); } diff --git a/src/application/Parser/NodeValidation/NodeValidator.ts b/src/application/Parser/NodeValidation/NodeValidator.ts index 4c154dbd..3d06932c 100644 --- a/src/application/Parser/NodeValidation/NodeValidator.ts +++ b/src/application/Parser/NodeValidation/NodeValidator.ts @@ -1,3 +1,4 @@ +import { isString } from '@/TypeHelpers'; import { INodeDataErrorContext, NodeDataError } from './NodeDataError'; import { NodeData } from './NodeData'; @@ -13,7 +14,7 @@ export class NodeValidator { 'missing name', ) .assert( - () => typeof nameValue === 'string', + () => isString(nameValue), `Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`, ); } diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts index 59c8382e..2e613041 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -1,4 +1,5 @@ import type { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from '@/application/collections/'; +import { isArray, isPlainObject } from '@/TypeHelpers'; import { FunctionCall } from './FunctionCall'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { FunctionCallArgument } from './Argument/FunctionCallArgument'; @@ -10,13 +11,13 @@ export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { } function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { - if (typeof calls !== 'object') { - throw new Error('called function(s) must be an object'); + if (!isPlainObject(calls) && !isArray(calls)) { + throw new Error('called function(s) must be an object or array'); } - if (calls instanceof Array) { + if (isArray(calls)) { return calls as FunctionCallData[]; } - const singleCall = calls; + const singleCall = calls as FunctionCallData; return [singleCall]; } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts index 5d978111..4a9af7cc 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts @@ -6,6 +6,7 @@ import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValida import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines'; import { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; +import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { SharedFunctionCollection } from './SharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection'; @@ -121,8 +122,11 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) { } function ensureExpectedParametersType(functions: readonly FunctionData[]) { + const hasValidParameters = ( + func: FunctionData, + ) => isNullOrUndefined(func.parameters) || isArrayOfObjects(func.parameters); const unexpectedFunctions = functions - .filter((func) => func.parameters && !isArrayOfObjects(func.parameters)); + .filter((func) => !hasValidParameters(func)); if (unexpectedFunctions.length) { const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`; throw new Error(errorMessage); @@ -130,8 +134,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) { } function isArrayOfObjects(value: unknown): boolean { - return Array.isArray(value) - && value.every((item) => typeof item === 'object'); + return isArray(value) && value.every((item) => isPlainObject(item)); } function printNames(holders: readonly FunctionData[]) { diff --git a/src/infrastructure/SystemOperations/NodeSystemOperations.ts b/src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts similarity index 65% rename from src/infrastructure/SystemOperations/NodeSystemOperations.ts rename to src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts index 6979ec61..88cbf747 100644 --- a/src/infrastructure/SystemOperations/NodeSystemOperations.ts +++ b/src/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations.ts @@ -1,10 +1,10 @@ -import { tmpdir } from 'os'; -import { join } from 'path'; -import { chmod, mkdir, writeFile } from 'fs/promises'; -import { exec } from 'child_process'; -import { ISystemOperations } from './ISystemOperations'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { chmod, mkdir, writeFile } from 'node:fs/promises'; +import { exec } from 'node:child_process'; +import { SystemOperations } from './SystemOperations'; -export function createNodeSystemOperations(): ISystemOperations { +export function createNodeSystemOperations(): SystemOperations { return { operatingSystem: { getTempDirectory: () => tmpdir(), @@ -33,7 +33,14 @@ export function createNodeSystemOperations(): ISystemOperations { ) => writeFile(filePath, data), }, command: { - execute: (command) => exec(command), + execute: (command) => new Promise((resolve, reject) => { + exec(command, (error) => { + if (error) { + reject(error); + } + resolve(); + }); + }), }, }; } diff --git a/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts b/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts new file mode 100644 index 00000000..0d2d9d1f --- /dev/null +++ b/src/infrastructure/CodeRunner/SystemOperations/SystemOperations.ts @@ -0,0 +1,24 @@ +export interface SystemOperations { + readonly operatingSystem: OperatingSystemOps; + readonly location: LocationOps; + readonly fileSystem: FileSystemOps; + readonly command: CommandOps; +} + +export interface OperatingSystemOps { + getTempDirectory(): string; +} + +export interface LocationOps { + combinePaths(...pathSegments: string[]): string; +} + +export interface CommandOps { + execute(command: string): Promise; +} + +export interface FileSystemOps { + setFilePermissions(filePath: string, mode: string | number): Promise; + createDirectory(directoryPath: string, isRecursive?: boolean): Promise; + writeToFile(filePath: string, data: string): Promise; +} diff --git a/src/infrastructure/CodeRunner.ts b/src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts similarity index 65% rename from src/infrastructure/CodeRunner.ts rename to src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts index c6135c7d..5a2714b3 100644 --- a/src/infrastructure/CodeRunner.ts +++ b/src/infrastructure/CodeRunner/TemporaryFileCodeRunner.ts @@ -1,18 +1,19 @@ -import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { getWindowInjectedSystemOperations } from './SystemOperations/WindowInjectedSystemOperations'; +import { CodeRunner } from '@/application/CodeRunner'; +import { SystemOperations } from './SystemOperations/SystemOperations'; +import { createNodeSystemOperations } from './SystemOperations/NodeSystemOperations'; -export class CodeRunner { +export class TemporaryFileCodeRunner implements CodeRunner { constructor( - private readonly system = getWindowInjectedSystemOperations(), - private readonly environment = RuntimeEnvironment.CurrentEnvironment, + private readonly system: SystemOperations = createNodeSystemOperations(), ) { } - public async runCode(code: string, folderName: string, fileExtension: string): Promise { - const { os } = this.environment; - if (os === undefined) { - throw new Error('Unidentified operating system'); - } + public async runCode( + code: string, + folderName: string, + fileExtension: string, + os: OperatingSystem, + ): Promise { const dir = this.system.location.combinePaths( this.system.operatingSystem.getTempDirectory(), folderName, @@ -22,7 +23,7 @@ export class CodeRunner { await this.system.fileSystem.writeToFile(filePath, code); await this.system.fileSystem.setFilePermissions(filePath, '755'); const command = getExecuteCommand(filePath, os); - this.system.command.execute(command); + await this.system.command.execute(command); } } diff --git a/src/infrastructure/Entity/BaseEntity.ts b/src/infrastructure/Entity/BaseEntity.ts index 90e1a6f7..5c3958cf 100644 --- a/src/infrastructure/Entity/BaseEntity.ts +++ b/src/infrastructure/Entity/BaseEntity.ts @@ -1,8 +1,9 @@ +import { isNumber } from '@/TypeHelpers'; import { IEntity } from './IEntity'; export abstract class BaseEntity implements IEntity { protected constructor(public id: TId) { - if (typeof id !== 'number' && !id) { + if (!isNumber(id) && !id) { throw new Error('Id cannot be null or empty'); } } diff --git a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts index 2f01af12..4b20ae17 100644 --- a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts +++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts @@ -1,3 +1,4 @@ +import { isBoolean, isFunction } from '@/TypeHelpers'; import { IEnvironmentVariables } from './IEnvironmentVariables'; /* Validation is externalized to keep the environment objects simple */ @@ -15,7 +16,7 @@ export function validateEnvironmentVariables(environment: IEnvironmentVariables) function getKeysMissingValues(keyValuePairs: Record): string[] { return Object.entries(keyValuePairs) .reduce((acc, [key, value]) => { - if (!value && typeof value !== 'boolean') { + if (!value && !isBoolean(value)) { acc.push(key); } return acc; @@ -38,7 +39,7 @@ function capturePropertyValues(instance: object): Record { // Capture getter properties from the instance's prototype for (const [key, descriptor] of Object.entries(descriptors)) { - if (typeof descriptor.get === 'function') { + if (isFunction(descriptor.get)) { obj[key] = descriptor.get.call(instance); } } diff --git a/src/infrastructure/Log/ElectronLogger.ts b/src/infrastructure/Log/ElectronLogger.ts index 7cbdfb14..c25f68e9 100644 --- a/src/infrastructure/Log/ElectronLogger.ts +++ b/src/infrastructure/Log/ElectronLogger.ts @@ -2,14 +2,8 @@ import log from 'electron-log/main'; import { Logger } from '@/application/Common/Log/Logger'; import type { LogFunctions } from 'electron-log'; -// Using plain-function rather than class so it can be used in Electron's context-bridging. export function createElectronLogger(logger: LogFunctions = log): Logger { - return { - info: (...params) => logger.info(...params), - debug: (...params) => logger.debug(...params), - warn: (...params) => logger.warn(...params), - error: (...params) => logger.error(...params), - }; + return logger; } export const ElectronLogger = createElectronLogger(); diff --git a/src/infrastructure/SystemOperations/ISystemOperations.ts b/src/infrastructure/SystemOperations/ISystemOperations.ts deleted file mode 100644 index 8915d0f5..00000000 --- a/src/infrastructure/SystemOperations/ISystemOperations.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface ISystemOperations { - readonly operatingSystem: IOperatingSystemOps; - readonly location: ILocationOps; - readonly fileSystem: IFileSystemOps; - readonly command: ICommandOps; -} - -export interface IOperatingSystemOps { - getTempDirectory(): string; -} - -export interface ILocationOps { - combinePaths(...pathSegments: string[]): string; -} - -export interface ICommandOps { - execute(command: string): void; -} - -export interface IFileSystemOps { - setFilePermissions(filePath: string, mode: string | number): Promise; - createDirectory(directoryPath: string, isRecursive?: boolean): Promise; - writeToFile(filePath: string, data: string): Promise; -} diff --git a/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts b/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts deleted file mode 100644 index b1be9800..00000000 --- a/src/infrastructure/SystemOperations/WindowInjectedSystemOperations.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WindowVariables } from '../WindowVariables/WindowVariables'; -import { ISystemOperations } from './ISystemOperations'; - -export function getWindowInjectedSystemOperations( - windowVariables: Partial = window, -): ISystemOperations { - if (!windowVariables) { - throw new Error('missing window'); - } - if (!windowVariables.system) { - throw new Error('missing system'); - } - return windowVariables.system; -} diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts index 4d96c27c..aa61f9a2 100644 --- a/src/infrastructure/WindowVariables/WindowVariables.ts +++ b/src/infrastructure/WindowVariables/WindowVariables.ts @@ -1,11 +1,11 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; -import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations'; import { Logger } from '@/application/Common/Log/Logger'; +import { CodeRunner } from '@/application/CodeRunner'; /* Primary entry point for platform-specific injections */ export interface WindowVariables { readonly isDesktop: boolean; - readonly system?: ISystemOperations; + readonly codeRunner?: CodeRunner; readonly os?: OperatingSystem; readonly log: Logger; } diff --git a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts index 8d2a98b7..a197fbff 100644 --- a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts +++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts @@ -1,12 +1,14 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; -import { PropertyKeys } from '@/TypeHelpers'; +import { + PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject, +} from '@/TypeHelpers'; import { WindowVariables } from './WindowVariables'; /** * Checks for consistency in runtime environment properties injected by Electron preloader. */ export function validateWindowVariables(variables: Partial) { - if (!isObject(variables)) { + if (!isPlainObject(variables)) { throw new Error('window is not an object'); } const errors = [...testEveryProperty(variables)]; @@ -21,7 +23,7 @@ function* testEveryProperty(variables: Partial): Iterable): boolean { if (!variables.isDesktop) { return true; } - return isObject(variables.log); + return isPlainObject(variables.log); } -function testSystem(variables: Partial): boolean { +function testCodeRunner(variables: Partial): boolean { if (!variables.isDesktop) { return true; } - return isObject(variables.system); + return isPlainObject(variables.codeRunner) + && isFunction(variables.codeRunner.runCode); } function testIsDesktop(isDesktop: unknown): boolean { @@ -65,17 +68,3 @@ function testIsDesktop(isDesktop: unknown): boolean { } return isBoolean(isDesktop); } - -function isNumber(variable: unknown): variable is number { - return typeof variable === 'number'; -} - -function isBoolean(variable: unknown): variable is boolean { - return typeof variable === 'boolean'; -} - -function isObject(variable: unknown): variable is object { - return Boolean(variable) // the data type of null is an object - && typeof variable === 'object' - && !Array.isArray(variable); -} diff --git a/src/presentation/bootstrapping/DependencyProvider.ts b/src/presentation/bootstrapping/DependencyProvider.ts index b3c70691..72124c9a 100644 --- a/src/presentation/bootstrapping/DependencyProvider.ts +++ b/src/presentation/bootstrapping/DependencyProvider.ts @@ -13,6 +13,7 @@ import { import { PropertyKeys } from '@/TypeHelpers'; import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState'; import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger'; +import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner'; export function provideDependencies( context: IApplicationContext, @@ -62,6 +63,10 @@ export function provideDependencies( InjectionKeys.useLogger, useLogger, ), + useCodeRunner: (di) => di.provide( + InjectionKeys.useCodeRunner, + useCodeRunner, + ), }; registerAll(Object.values(resolvers), api); } diff --git a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue index a3c579d8..3a481251 100644 --- a/src/presentation/components/Code/CodeButtons/CodeRunButton.vue +++ b/src/presentation/components/Code/CodeButtons/CodeRunButton.vue @@ -11,8 +11,6 @@ import { defineComponent, computed } from 'vue'; import { injectKey } from '@/presentation/injectionSymbols'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { CodeRunner } from '@/infrastructure/CodeRunner'; -import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext'; import IconButton from './IconButton.vue'; export default defineComponent({ @@ -22,11 +20,19 @@ export default defineComponent({ setup() { const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState); const { os, isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment); + const { codeRunner } = injectKey((keys) => keys.useCodeRunner); const canRun = computed(() => getCanRunState(currentState.value.os, isDesktop, os)); async function executeCode() { - await runCode(currentContext); + if (!codeRunner) { throw new Error('missing code runner'); } + if (os === undefined) { throw new Error('unidentified host operating system'); } + await codeRunner.runCode( + currentContext.state.code.current, + currentContext.app.info.name, + currentState.value.collection.scripting.fileExtension, + os, + ); } return { @@ -45,13 +51,4 @@ function getCanRunState( const isRunningOnSelectedOs = selectedOs === hostOs; return isDesktopVersion && isRunningOnSelectedOs; } - -async function runCode(context: IReadOnlyApplicationContext) { - const runner = new CodeRunner(); - await runner.runCode( - /* code: */ context.state.code.current, - /* appName: */ context.app.info.name, - /* fileExtension: */ context.state.collection.scripting.fileExtension, - ); -} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts index fe16b6bc..a42bf61d 100644 --- a/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts +++ b/src/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser.ts @@ -1,3 +1,4 @@ +import { isArray } from '@/TypeHelpers'; import { TreeInputNodeData } from '../../Bindings/TreeInputNodeData'; import { TreeNode } from '../../Node/TreeNode'; import { TreeNodeManager } from '../../Node/TreeNodeManager'; @@ -5,7 +6,7 @@ import { TreeNodeManager } from '../../Node/TreeNodeManager'; export function parseTreeInput( input: readonly TreeInputNodeData[], ): TreeNode[] { - if (!Array.isArray(input)) { + if (!isArray(input)) { throw new Error('input data must be an array'); } const nodes = input.map((nodeData) => createNode(nodeData)); diff --git a/src/presentation/components/Shared/Hooks/UseCodeRunner.ts b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts new file mode 100644 index 00000000..4c4a6f44 --- /dev/null +++ b/src/presentation/components/Shared/Hooks/UseCodeRunner.ts @@ -0,0 +1,9 @@ +import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; + +export function useCodeRunner( + window: WindowVariables = globalThis.window, +) { + return { + codeRunner: window.codeRunner, + }; +} diff --git a/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts new file mode 100644 index 00000000..f3bd78d2 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/ApiContextBridge.ts @@ -0,0 +1,17 @@ +import { contextBridge } from 'electron'; +import { bindObjectMethods } from './MethodContextBinder'; +import { provideWindowVariables } from './RendererApiProvider'; + +export function connectApisWithContextBridge( + bridgeConnector: BridgeConnector = contextBridge.exposeInMainWorld, + apiObject: object = provideWindowVariables(), + methodContextBinder: MethodContextBinder = bindObjectMethods, +) { + Object.entries(apiObject).forEach(([key, value]) => { + bridgeConnector(key, methodContextBinder(value)); + }); +} + +export type BridgeConnector = typeof contextBridge.exposeInMainWorld; + +export type MethodContextBinder = typeof bindObjectMethods; diff --git a/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts new file mode 100644 index 00000000..fb530774 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/MethodContextBinder.ts @@ -0,0 +1,52 @@ +import { + isArray, isFunction, isNullOrUndefined, isPlainObject, +} from '@/TypeHelpers'; + +/** + * Binds method contexts to their original object instances and recursively processes + * nested objects and arrays. This is particularly useful when exposing objects across + * different contexts in Electron, such as from the main process to the renderer process + * via the `contextBridge`. + * + * In Electron's context isolation environment, methods of objects passed through the + * `contextBridge` lose their original context (`this` binding). This function ensures that + * each method retains its binding to its original object, allowing it to work as intended + * when invoked from the renderer process. + * + * This approach decouples context isolation concerns from class implementations, enabling + * classes to operate normally without needing explicit binding or arrow functions to maintain + * the context. + */ +export function bindObjectMethods(obj: T): T { + if (isNullOrUndefined(obj)) { + return obj; + } + if (isPlainObject(obj)) { + bindMethodsOfObject(obj); + Object.values(obj).forEach((value) => { + if (!isNullOrUndefined(value) && !isFunction(value)) { + bindObjectMethods(value); + } + }); + } else if (isArray(obj)) { + obj.forEach((item) => bindObjectMethods(item)); + } + return obj; +} + +function bindMethodsOfObject(obj: T): T { + const prototype = Object.getPrototypeOf(obj); + if (!prototype) { + return obj; + } + Object.getOwnPropertyNames(prototype).forEach((property) => { + if (!prototype.hasOwnProperty.call(obj, property)) { + return; // Skip properties not directly on the prototype + } + const value = obj[property]; + if (isFunction(value)) { + (obj as object)[property] = value.bind(obj); + } + }); + return obj; +} diff --git a/src/presentation/electron/preload/NodeOsMapper.ts b/src/presentation/electron/preload/ContextBridging/NodeOsMapper.ts similarity index 100% rename from src/presentation/electron/preload/NodeOsMapper.ts rename to src/presentation/electron/preload/ContextBridging/NodeOsMapper.ts diff --git a/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts new file mode 100644 index 00000000..9f9ffcfb --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/RendererApiProvider.ts @@ -0,0 +1,27 @@ +import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { Logger } from '@/application/Common/Log/Logger'; +import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; +import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner'; +import { CodeRunner } from '@/application/CodeRunner'; +import { convertPlatformToOs } from './NodeOsMapper'; +import { createSecureFacade } from './SecureFacadeCreator'; + +export function provideWindowVariables( + createCodeRunner: CodeRunnerFactory = () => new TemporaryFileCodeRunner(), + createLogger: LoggerFactory = () => createElectronLogger(), + convertToOs = convertPlatformToOs, + createApiFacade: ApiFacadeFactory = createSecureFacade, +): WindowVariables { + return { + isDesktop: true, + log: createApiFacade(createLogger(), ['info', 'debug', 'warn', 'error']), + os: convertToOs(process.platform), + codeRunner: createApiFacade(createCodeRunner(), ['runCode']), + }; +} + +export type LoggerFactory = () => Logger; + +export type CodeRunnerFactory = () => CodeRunner; + +export type ApiFacadeFactory = typeof createSecureFacade; diff --git a/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts new file mode 100644 index 00000000..809e4099 --- /dev/null +++ b/src/presentation/electron/preload/ContextBridging/SecureFacadeCreator.ts @@ -0,0 +1,42 @@ +import { isFunction } from '@/TypeHelpers'; + +/** + * Creates a secure proxy for the specified object, exposing only the public properties + * of its interface. + * + * This approach prevents the full exposure of the object, thereby reducing the risk + * of unintended access or misuse. For instance, creating a facade for a class rather + * than exposing the class itself ensures that private members and dependencies + * (such as file access or internal state) remain encapsulated and inaccessible. + */ +export function createSecureFacade( + originalObject: T, + accessibleMembers: KeyTypeCombinations, +): T { + const facade: Partial = {}; + + accessibleMembers.forEach((key: keyof T) => { + const member = originalObject[key]; + if (isFunction(member)) { + facade[key] = ((...args: unknown[]) => { + return member.apply(originalObject, args); + }) as T[keyof T]; + } else { + facade[key] = member; + } + }); + + return facade as T; +} + +type PrependTuple = H extends unknown ? T extends unknown ? + ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never : never; +type RecursionDepthControl = [ + never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, +]; +type AllKeyCombinations = T extends unknown ? + PrependTuple extends infer X ? { + 0: [], 1: AllKeyCombinations + }[[X] extends [never] ? 0 : 1] : never> : + never; +type KeyTypeCombinations = AllKeyCombinations; diff --git a/src/presentation/electron/preload/WindowVariablesProvider.ts b/src/presentation/electron/preload/WindowVariablesProvider.ts deleted file mode 100644 index 3cc5808b..00000000 --- a/src/presentation/electron/preload/WindowVariablesProvider.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createNodeSystemOperations } from '@/infrastructure/SystemOperations/NodeSystemOperations'; -import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger'; -import { Logger } from '@/application/Common/Log/Logger'; -import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; -import { convertPlatformToOs } from './NodeOsMapper'; - -export function provideWindowVariables( - createSystem = createNodeSystemOperations, - createLogger: () => Logger = () => createElectronLogger(), - convertToOs = convertPlatformToOs, -): WindowVariables { - return { - system: createSystem(), - isDesktop: true, - log: createLogger(), - os: convertToOs(process.platform), - }; -} diff --git a/src/presentation/electron/preload/index.ts b/src/presentation/electron/preload/index.ts index 629f7d5d..f4aeb82f 100644 --- a/src/presentation/electron/preload/index.ts +++ b/src/presentation/electron/preload/index.ts @@ -1,9 +1,8 @@ // This file is used to securely expose Electron APIs to the application. -import { contextBridge } from 'electron'; import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; -import { provideWindowVariables } from './WindowVariablesProvider'; +import { connectApisWithContextBridge } from './ContextBridging/ApiContextBridge'; validateRuntimeSanity({ // Validate metadata as a preventive measure for fail-fast, @@ -14,10 +13,7 @@ validateRuntimeSanity({ validateWindowVariables: false, }); -const windowVariables = provideWindowVariables(); -Object.entries(windowVariables).forEach(([key, value]) => { - contextBridge.exposeInMainWorld(key, value); -}); +connectApisWithContextBridge(); // Do not remove [PRELOAD_INIT]; it's a marker used in tests. ElectronLogger.info('[PRELOAD_INIT] Preload script successfully initialized and executed.'); diff --git a/src/presentation/injectionSymbols.ts b/src/presentation/injectionSymbols.ts index 0e5fcf60..3d8cf46e 100644 --- a/src/presentation/injectionSymbols.ts +++ b/src/presentation/injectionSymbols.ts @@ -7,6 +7,7 @@ import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseC import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents'; import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState'; import type { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger'; +import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner'; export const InjectionKeys = { useCollectionState: defineTransientKey>('useCollectionState'), @@ -17,6 +18,7 @@ export const InjectionKeys = { useCurrentCode: defineTransientKey>('useCurrentCode'), useUserSelectionState: defineTransientKey>('useUserSelectionState'), useLogger: defineTransientKey>('useLogger'), + useCodeRunner: defineTransientKey>('useCodeRunner'), }; export interface InjectionKeyWithLifetime { diff --git a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts index 8176761b..9a6acf2b 100644 --- a/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts +++ b/tests/checks/desktop-runtime-errors/check-desktop-runtime-errors/utils/text.ts @@ -1,3 +1,5 @@ +import { isString } from '@/TypeHelpers'; + export function indentText( text: string, indentLevel = 1, @@ -21,7 +23,7 @@ export function filterEmpty(texts: readonly (string | undefined | null)[]): stri } function validateText(text: string): void { - if (typeof text !== 'string') { + if (!isString(text)) { throw new Error(`text is not a string. It is: ${typeof text}\n${text}`); } } diff --git a/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts index b10f7a69..b4fcd0ad 100644 --- a/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts +++ b/tests/integration/infrastructure/RuntimeSanity/SanityChecks.spec.ts @@ -1,6 +1,7 @@ import { describe } from 'vitest'; import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions'; import { validateRuntimeSanity } from '@/infrastructure/RuntimeSanity/SanityChecks'; +import { isBoolean } from '@/TypeHelpers'; describe('SanityChecks', () => { describe('validateRuntimeSanity', () => { @@ -42,7 +43,7 @@ function generateBooleanPermutations(object: T | undefined): T[] { const currentKey = keys[0]; const currentValue = object[currentKey]; - if (typeof currentValue !== 'boolean') { + if (!isBoolean(currentValue)) { return generateBooleanPermutations({ ...object, [currentKey]: currentValue, diff --git a/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts new file mode 100644 index 00000000..679aa7eb --- /dev/null +++ b/tests/integration/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts @@ -0,0 +1,15 @@ +import { it, describe, expect } from 'vitest'; +import { connectApisWithContextBridge } from '@/presentation/electron/preload/ContextBridging/ApiContextBridge'; + +describe('ApiContextBridge', () => { + describe('connectApisWithContextBridge', () => { + it('can provide keys and values', () => { + // arrange + const bridgeConnector = () => {}; + // act + const act = () => connectApisWithContextBridge(bridgeConnector); + // assert + expect(act).to.not.throw(); + }); + }); +}); diff --git a/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts new file mode 100644 index 00000000..29364b01 --- /dev/null +++ b/tests/integration/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts @@ -0,0 +1,66 @@ +import { it, describe, expect } from 'vitest'; +import { provideWindowVariables } from '@/presentation/electron/preload/ContextBridging/RendererApiProvider'; +import { + isArray, isBoolean, isFunction, isNullOrUndefined, isNumber, isPlainObject, isString, +} from '@/TypeHelpers'; + +describe('RendererApiProvider', () => { + describe('provideWindowVariables', () => { + describe('conforms to Electron\'s context bridging requirements', () => { + // https://www.electronjs.org/docs/latest/api/context-bridge + const variables = provideWindowVariables(); + Object.entries(variables).forEach(([key, value]) => { + it(`\`${key}\` conforms to allowed types for context bridging`, () => { + // act + const act = () => checkAllowedType(value); + // assert + expect(act).to.not.throw(); + }); + }); + }); + }); +}); + +function checkAllowedType(value: unknown): void { + if (isBasicType(value)) { + return; + } + if (isArray(value)) { + checkArrayElements(value); + return; + } + if (!isPlainObject(value)) { + throw new Error(`Type error: Expected a valid object, array, or primitive type, but received type '${typeof value}'.`); + } + if (isNullOrUndefined(value)) { + throw new Error('Type error: Value is null or undefined, which is not allowed.'); + } + checkObjectProperties(value); +} + +function isBasicType(value: unknown): boolean { + return isString(value) || isNumber(value) || isBoolean(value) || isFunction(value); +} + +function checkArrayElements(array: unknown[]): void { + array.forEach((item, index) => { + try { + checkAllowedType(item); + } catch (error) { + throw new Error(`Invalid array element at index ${index}: ${error.message}`); + } + }); +} + +function checkObjectProperties(obj: NonNullable): void { + if (Object.keys(obj).some((key) => !isString(key))) { + throw new Error('Type error: At least one object key is not a string, which violates the allowed types.'); + } + Object.entries(obj).forEach(([key, memberValue]) => { + try { + checkAllowedType(memberValue); + } catch (error) { + throw new Error(`Invalid object property '${key}': ${error.message}`); + } + }); +} diff --git a/tests/integration/presentation/electron/preload/WindowVariablesProvider.spec.ts b/tests/integration/presentation/electron/preload/WindowVariablesProvider.spec.ts deleted file mode 100644 index ecbd7873..00000000 --- a/tests/integration/presentation/electron/preload/WindowVariablesProvider.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { it, describe, expect } from 'vitest'; -import { provideWindowVariables } from '@/presentation/electron/preload/WindowVariablesProvider'; - -describe('WindowVariablesProvider', () => { - describe('provideWindowVariables', () => { - describe('conforms to Electron\'s context bridging requirements', () => { - // https://www.electronjs.org/docs/latest/api/context-bridge - const variables = provideWindowVariables(); - Object.entries(variables).forEach(([key, value]) => { - it(`\`${key}\` conforms to allowed types for context bridging`, () => { - expect(checkAllowedType(value)).to.equal(true); - }); - }); - }); - }); -}); - -function checkAllowedType(value: unknown) { - const type = typeof value; - if (['string', 'number', 'boolean', 'function'].includes(type)) { - return true; - } - if (Array.isArray(value)) { - return value.every(checkAllowedType); - } - if (type === 'object' && value !== null && value !== undefined) { - return ( - // Every key should be a string - Object.keys(value).every((key) => typeof key === 'string') - // Every value should be of allowed type - && Object.values(value).every(checkAllowedType) - ); - } - return false; -} diff --git a/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts index 36abebe5..74ee912b 100644 --- a/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts +++ b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts @@ -91,8 +91,8 @@ describe('CodeBuilder', () => { it('appendFunction', () => { // arrange const sut = new CodeBuilderConcrete(); - const functionName = 'function'; - const code = 'code'; + const functionName = 'expected-function-name'; + const code = 'expected-code'; // act sut.appendFunction(functionName, code); // assert diff --git a/tests/unit/infrastructure/CodeRunner.spec.ts b/tests/unit/infrastructure/CodeRunner/TemporaryFileCodeRunner.spec.ts similarity index 87% rename from tests/unit/infrastructure/CodeRunner.spec.ts rename to tests/unit/infrastructure/CodeRunner/TemporaryFileCodeRunner.spec.ts index 9a97f5d8..274bc005 100644 --- a/tests/unit/infrastructure/CodeRunner.spec.ts +++ b/tests/unit/infrastructure/CodeRunner/TemporaryFileCodeRunner.spec.ts @@ -1,17 +1,16 @@ import { describe, it, expect } from 'vitest'; -import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { CodeRunner } from '@/infrastructure/CodeRunner'; +import { FileSystemOps, SystemOperations } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; +import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner'; import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync'; import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub'; import { OperatingSystemOpsStub } from '@tests/unit/shared/Stubs/OperatingSystemOpsStub'; import { LocationOpsStub } from '@tests/unit/shared/Stubs/LocationOpsStub'; import { FileSystemOpsStub } from '@tests/unit/shared/Stubs/FileSystemOpsStub'; import { CommandOpsStub } from '@tests/unit/shared/Stubs/CommandOpsStub'; -import { IFileSystemOps, ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations'; import { FunctionKeys } from '@/TypeHelpers'; -describe('CodeRunner', () => { +describe('TemporaryFileCodeRunner', () => { describe('runCode', () => { it('creates temporary directory recursively', async () => { // arrange @@ -121,11 +120,11 @@ describe('CodeRunner', () => { describe('executes as expected', () => { // arrange const filePath = 'expected-file-path'; - interface IExecutionTestCase { + interface ExecutionTestCase { readonly givenOs: OperatingSystem; readonly expectedCommand: string; } - const testData: readonly IExecutionTestCase[] = [ + const testData: readonly ExecutionTestCase[] = [ { givenOs: OperatingSystem.Windows, expectedCommand: filePath, @@ -164,7 +163,7 @@ describe('CodeRunner', () => { } }); it('runs in expected order', async () => { // verifies correct `async`, `await` usage. - const expectedOrder: readonly FunctionKeys[] = [ + const expectedOrder: readonly FunctionKeys[] = [ 'createDirectory', 'writeToFile', 'setFilePermissions', @@ -186,7 +185,7 @@ describe('CodeRunner', () => { describe('throws with invalid OS', () => { const testScenarios: ReadonlyArray<{ readonly description: string; - readonly invalidOs: OperatingSystem | undefined; + readonly invalidOs: OperatingSystem; readonly expectedError: string; }> = [ (() => { @@ -197,11 +196,6 @@ describe('CodeRunner', () => { expectedError: `unsupported os: ${OperatingSystem[unsupportedOs]}`, }; })(), - { - description: 'unknown OS', - invalidOs: undefined, - expectedError: 'Unidentified operating system', - }, ]; testScenarios.forEach(({ description, invalidOs, expectedError }) => { it(description, async () => { @@ -225,19 +219,17 @@ class TestContext { private fileExtension = 'fileExtension'; - private os: OperatingSystem | undefined = OperatingSystem.Windows; + private os: OperatingSystem = OperatingSystem.Windows; - private systemOperations: ISystemOperations = new SystemOperationsStub(); + private systemOperations: SystemOperations = new SystemOperationsStub(); public async runCode(): Promise { - const environment = new RuntimeEnvironmentStub() - .withOs(this.os); - const runner = new CodeRunner(this.systemOperations, environment); - await runner.runCode(this.code, this.folderName, this.fileExtension); + const runner = new TemporaryFileCodeRunner(this.systemOperations); + await runner.runCode(this.code, this.folderName, this.fileExtension, this.os); } public withSystemOperations( - systemOperations: ISystemOperations, + systemOperations: SystemOperations, ): this { this.systemOperations = systemOperations; return this; @@ -250,22 +242,22 @@ class TestContext { return this.withSystemOperations(stub); } - public withOs(os: OperatingSystem | undefined) { + public withOs(os: OperatingSystem): this { this.os = os; return this; } - public withFolderName(folderName: string) { + public withFolderName(folderName: string): this { this.folderName = folderName; return this; } - public withCode(code: string) { + public withCode(code: string): this { this.code = code; return this; } - public withExtension(fileExtension: string) { + public withExtension(fileExtension: string): this { this.fileExtension = fileExtension; return this; } diff --git a/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts b/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts index 70af3f0d..7860d2af 100644 --- a/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts +++ b/tests/unit/infrastructure/RuntimeEnvironment/WindowVariablesValidator.spec.ts @@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest'; import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator'; import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub'; -import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests'; +import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub'; +import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub'; describe('WindowVariablesValidator', () => { describe('validateWindowVariables', () => { @@ -92,51 +92,35 @@ describe('WindowVariablesValidator', () => { }); describe('`isDesktop` property', () => { - it('throws an error when only isDesktop is provided and it is true without a system object', () => { + it('does not throw when true with valid services', () => { // arrange - const systemObject = undefined; - const expectedError = getExpectedError( - { - name: 'system', - object: systemObject, - }, - ); + const validCodeRunner = new CodeRunnerStub(); const input = new WindowVariablesStub() .withIsDesktop(true) - .withSystem(systemObject); - // act - const act = () => validateWindowVariables(input); - // assert - expect(act).to.throw(expectedError); - }); - - it('does not throw when isDesktop is true with a valid system object', () => { - // arrange - const validSystem = new SystemOperationsStub(); - const input = new WindowVariablesStub() - .withIsDesktop(true) - .withSystem(validSystem); + .withCodeRunner(validCodeRunner); // act const act = () => validateWindowVariables(input); // assert expect(act).to.not.throw(); }); - it('does not throw when isDesktop is false without a system object', () => { - // arrange - const absentSystem = undefined; - const input = new WindowVariablesStub() - .withIsDesktop(false) - .withSystem(absentSystem); - // act - const act = () => validateWindowVariables(input); - // assert - expect(act).to.not.throw(); + describe('does not throw when false without services', () => { + itEachAbsentObjectValue((absentValue) => { + // arrange + const absentCodeRunner = absentValue; + const input = new WindowVariablesStub() + .withIsDesktop(false) + .withCodeRunner(absentCodeRunner); + // act + const act = () => validateWindowVariables(input); + // assert + expect(act).to.not.throw(); + }, { excludeNull: true }); }); }); - describe('`system` property', () => { - expectObjectOnDesktop('system'); + describe('`codeRunner` property', () => { + expectObjectOnDesktop('codeRunner'); }); describe('`log` property', () => { @@ -158,6 +142,7 @@ function expectObjectOnDesktop(key: keyof WindowVariables) { describe('validates object type on desktop', () => { itEachInvalidObjectValue((invalidObjectValue) => { // arrange + const isOnDesktop = true; const invalidObject = invalidObjectValue as T; const expectedError = getExpectedError({ name: key, @@ -165,7 +150,7 @@ function expectObjectOnDesktop(key: keyof WindowVariables) { }); const input: WindowVariables = { ...new WindowVariablesStub(), - isDesktop: true, + isDesktop: isOnDesktop, [key]: invalidObject, }; // act diff --git a/tests/unit/infrastructure/SystemOperations/WindowInjectedSystemOperations.spec.ts b/tests/unit/infrastructure/SystemOperations/WindowInjectedSystemOperations.spec.ts deleted file mode 100644 index d1dd8095..00000000 --- a/tests/unit/infrastructure/SystemOperations/WindowInjectedSystemOperations.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getWindowInjectedSystemOperations } from '@/infrastructure/SystemOperations/WindowInjectedSystemOperations'; -import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; -import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests'; -import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub'; - -describe('WindowInjectedSystemOperations', () => { - describe('getWindowInjectedSystemOperations', () => { - describe('throws if window is absent', () => { - itEachAbsentObjectValue((absentValue) => { - // arrange - const expectedError = 'missing window'; - const window: WindowVariables = absentValue as never; - // act - const act = () => getWindowInjectedSystemOperations(window); - // assert - expect(act).to.throw(expectedError); - }, { excludeUndefined: true }); - }); - describe('throw if system is absent', () => { - itEachAbsentObjectValue((absentValue) => { - // arrange - const expectedError = 'missing system'; - const absentSystem = absentValue; - const window: Partial = { - system: absentSystem as never, - }; - // act - const act = () => getWindowInjectedSystemOperations(window); - // assert - expect(act).to.throw(expectedError); - }); - }); - it('returns from window', () => { - // arrange - const expectedValue = new SystemOperationsStub(); - const window: Partial = { - system: expectedValue, - }; - // act - const actualValue = getWindowInjectedSystemOperations(window); - // assert - expect(actualValue).to.equal(expectedValue); - }); - }); -}); diff --git a/tests/unit/presentation/bootstrapping/DependencyProvider.spec.ts b/tests/unit/presentation/bootstrapping/DependencyProvider.spec.ts index e64a5b21..45ea54f0 100644 --- a/tests/unit/presentation/bootstrapping/DependencyProvider.spec.ts +++ b/tests/unit/presentation/bootstrapping/DependencyProvider.spec.ts @@ -18,6 +18,7 @@ describe('DependencyProvider', () => { useCurrentCode: createTransientTests(), useUserSelectionState: createTransientTests(), useLogger: createTransientTests(), + useCodeRunner: createTransientTests(), }; Object.entries(testCases).forEach(([key, runTests]) => { const registeredKey = InjectionKeys[key].key; diff --git a/tests/unit/presentation/components/Shared/Hooks/UseCodeRunner.spec.ts b/tests/unit/presentation/components/Shared/Hooks/UseCodeRunner.spec.ts new file mode 100644 index 00000000..97af6e84 --- /dev/null +++ b/tests/unit/presentation/components/Shared/Hooks/UseCodeRunner.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner'; + +describe('UseCodeRunner', () => { + it('returns from the provided window object', () => { + // arrange + const mockCodeRunner = { run: () => {} }; + const mockWindow = { codeRunner: mockCodeRunner } as unknown as Window; + + // act + const { codeRunner } = useCodeRunner(mockWindow); + + // assert + expect(codeRunner).to.equal(mockCodeRunner); + }); + + it('returns undefined when not defined in the window object', () => { + // Arrange + const mockWindow = {} as unknown as Window; + + // Act + const { codeRunner } = useCodeRunner(mockWindow); + + // Assert + expect(codeRunner).toBeUndefined(); + }); +}); diff --git a/tests/unit/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts b/tests/unit/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts new file mode 100644 index 00000000..30df5747 --- /dev/null +++ b/tests/unit/presentation/electron/preload/ContextBridging/ApiContextBridge.spec.ts @@ -0,0 +1,96 @@ +import { it, describe, expect } from 'vitest'; +import { BridgeConnector, MethodContextBinder, connectApisWithContextBridge } from '@/presentation/electron/preload/ContextBridging/ApiContextBridge'; + +describe('ApiContextBridge', () => { + describe('connectApisWithContextBridge', () => { + it('connects properties as keys', () => { + // arrange + const context = new BridgeConnectorTestContext(); + const { exposedItems, bridgeConnector } = mockBridgeConnector(); + const expectedKeys = ['a', 'b']; + const api = { + [`${expectedKeys[0]}`]: () => {}, + [`${expectedKeys[1]}`]: () => {}, + }; + + // act + context + .withApiObject(api) + .withBridgeConnector(bridgeConnector) + .run(); + + // assert + expect(exposedItems).to.have.lengthOf(expectedKeys.length); + expect(exposedItems.map(([key]) => key)).to.have.members(expectedKeys); + }); + it('connects values after binding their context', () => { + // arrange + const context = new BridgeConnectorTestContext(); + const { exposedItems, bridgeConnector } = mockBridgeConnector(); + const rawValues = ['a', 'b']; + const api = { + first: rawValues[0], + second: rawValues[1], + }; + const boundValues = { + [`${rawValues[0]}`]: 'bound-a', + [`${rawValues[1]}`]: 'bound-b', + }; + const expectedValues = Object.values(boundValues); + const contextBinderMock: MethodContextBinder = (property) => { + return boundValues[property as string] as never; + }; + + // act + context + .withApiObject(api) + .withContextBinder(contextBinderMock) + .withBridgeConnector(bridgeConnector) + .run(); + + // assert + expect(exposedItems).to.have.lengthOf(rawValues.length); + expect(exposedItems.map(([,value]) => value)).to.have.members(expectedValues); + }); + }); +}); + +function mockBridgeConnector() { + const exposedItems = new Array<[string, unknown]>(); + const bridgeConnector: BridgeConnector = (key, api) => exposedItems.push([key, api]); + return { + exposedItems, + bridgeConnector, + }; +} + +class BridgeConnectorTestContext { + private bridgeConnector: BridgeConnector = () => {}; + + private apiObject: object = {}; + + private contextBinder: MethodContextBinder = (obj) => obj; + + public withBridgeConnector(bridgeConnector: BridgeConnector): this { + this.bridgeConnector = bridgeConnector; + return this; + } + + public withApiObject(apiObject: object): this { + this.apiObject = apiObject; + return this; + } + + public withContextBinder(contextBinder: MethodContextBinder): this { + this.contextBinder = contextBinder; + return this; + } + + public run() { + return connectApisWithContextBridge( + this.bridgeConnector, + this.apiObject, + this.contextBinder, + ); + } +} diff --git a/tests/unit/presentation/electron/preload/ContextBridging/MethodContextBinder.spec.ts b/tests/unit/presentation/electron/preload/ContextBridging/MethodContextBinder.spec.ts new file mode 100644 index 00000000..5c665e4e --- /dev/null +++ b/tests/unit/presentation/electron/preload/ContextBridging/MethodContextBinder.spec.ts @@ -0,0 +1,132 @@ +/* eslint-disable max-classes-per-file */ +import { describe, it, expect } from 'vitest'; +import { bindObjectMethods } from '@/presentation/electron/preload/ContextBridging/MethodContextBinder'; + +describe('MethodContextBinder', () => { + describe('bindObjectMethods', () => { + it('binds methods of an object to itself', () => { + // arrange + class TestClass { + constructor(public value: number) {} + + increment() { + this.value += 1; + } + } + const instance = new TestClass(0); + + // act + const boundInstance = bindObjectMethods(instance); + boundInstance.increment(); + + // assert + expect(boundInstance.value).toBe(1); + }); + + it('handles objects without prototype methods gracefully', () => { + // arrange + const object = Object.create(null); // object without prototype + object.value = 0; + // eslint-disable-next-line func-names + object.increment = function () { + this.value += 1; + }; + + // act + const boundObject = bindObjectMethods(object); + + // assert + expect(() => boundObject.increment()).not.toThrow(); + }); + + it('recursively binds methods in nested objects', () => { + // arrange + const nestedObject = { + child: { + value: 0, + increment() { + this.value += 1; + }, + }, + }; + + // act + const boundObject = bindObjectMethods(nestedObject); + boundObject.child.increment(); + + // assert + expect(boundObject.child.value).toBe(1); + }); + + it('recursively binds methods in arrays', () => { + // arrange + const array = [ + { + value: 0, + increment() { + this.value += 1; + }, + }, + ]; + + // act + const boundArray = bindObjectMethods(array); + boundArray[0].increment(); + + // assert + expect(boundArray[0].value).toBe(1); + }); + + describe('returns the same object if it is neither an object nor an array', () => { + const testScenarios: ReadonlyArray<{ + readonly description: string; + readonly value: unknown; + }> = [ + { + description: 'given primitive', + value: 42, + }, + { + description: 'null', + value: null, + }, + { + description: 'undefined', + value: undefined, + }, + ]; + testScenarios.forEach(({ description, value }) => { + it(description, () => { + // act + const boundValue = bindObjectMethods(value); + // assert + expect(boundValue).toBe(value); + }); + }); + }); + + it('skips binding inherited properties', () => { + // arrange + class ParentClass { + inheritedMethod() {} + } + class TestClass extends ParentClass { + constructor(public value: number) { + super(); + } + + increment() { + this.value += 1; + } + } + const instance = new TestClass(0); + const originalInheritedMethod = instance.inheritedMethod; + + // act + const boundInstance = bindObjectMethods(instance); + + // assert + expect(boundInstance.inheritedMethod).toBe(originalInheritedMethod); + }); + }); +}); diff --git a/tests/unit/presentation/electron/preload/NodeOsMapper.spec.ts b/tests/unit/presentation/electron/preload/ContextBridging/NodeOsMapper.spec.ts similarity index 98% rename from tests/unit/presentation/electron/preload/NodeOsMapper.spec.ts rename to tests/unit/presentation/electron/preload/ContextBridging/NodeOsMapper.spec.ts index d1686443..19c2f3b6 100644 --- a/tests/unit/presentation/electron/preload/NodeOsMapper.spec.ts +++ b/tests/unit/presentation/electron/preload/ContextBridging/NodeOsMapper.spec.ts @@ -1,6 +1,6 @@ import { describe } from 'vitest'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { convertPlatformToOs } from '@/presentation/electron/preload/NodeOsMapper'; +import { convertPlatformToOs } from '@/presentation/electron/preload/ContextBridging/NodeOsMapper'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; describe('NodeOsMapper', () => { diff --git a/tests/unit/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts b/tests/unit/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts new file mode 100644 index 00000000..2d9d0597 --- /dev/null +++ b/tests/unit/presentation/electron/preload/ContextBridging/RendererApiProvider.spec.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from 'vitest'; +import { ApiFacadeFactory, provideWindowVariables } from '@/presentation/electron/preload/ContextBridging/RendererApiProvider'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { Logger } from '@/application/Common/Log/Logger'; +import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub'; +import { CodeRunner } from '@/application/CodeRunner'; +import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub'; +import { PropertyKeys } from '@/TypeHelpers'; +import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; + +describe('RendererApiProvider', () => { + describe('provideWindowVariables', () => { + interface WindowVariableTestCase { + readonly description: string; + setupContext(context: RendererApiProviderTestContext): RendererApiProviderTestContext; + readonly expectedValue: unknown; + } + const testScenarios: Record>, WindowVariableTestCase> = { + isDesktop: { + description: 'returns true', + setupContext: (context) => context, + expectedValue: true, + }, + codeRunner: (() => { + const codeRunner = new CodeRunnerStub(); + const createFacadeMock: ApiFacadeFactory = (obj) => obj; + return { + description: 'encapsulates correctly', + setupContext: (context) => context + .withCodeRunner(codeRunner) + .withApiFacadeCreator(createFacadeMock), + expectedValue: codeRunner, + }; + })(), + os: (() => { + const operatingSystem = OperatingSystem.WindowsPhone; + return { + description: 'returns expected', + setupContext: (context) => context.withOs(operatingSystem), + expectedValue: operatingSystem, + }; + })(), + log: (() => { + const logger = new LoggerStub(); + const createFacadeMock: ApiFacadeFactory = (obj) => obj; + return { + description: 'encapsulates correctly', + setupContext: (context) => context + .withLogger(logger) + .withApiFacadeCreator(createFacadeMock), + expectedValue: logger, + }; + })(), + }; + Object.entries(testScenarios).forEach(( + [property, { description, setupContext, expectedValue }], + ) => { + it(`${property}: ${description}`, () => { + // arrange + const testContext = setupContext(new RendererApiProviderTestContext()); + // act + const variables = testContext.provideWindowVariables(); + // assert + const actualValue = variables[property]; + expect(actualValue).to.equal(expectedValue); + }); + }); + }); +}); + +class RendererApiProviderTestContext { + private codeRunner: CodeRunner = new CodeRunnerStub(); + + private os: OperatingSystem = OperatingSystem.Android; + + private log: Logger = new LoggerStub(); + + private apiFacadeCreator: ApiFacadeFactory = (obj) => obj; + + public withCodeRunner(codeRunner: CodeRunner): this { + this.codeRunner = codeRunner; + return this; + } + + public withOs(os: OperatingSystem): this { + this.os = os; + return this; + } + + public withLogger(log: Logger): this { + this.log = log; + return this; + } + + public withApiFacadeCreator(apiFacadeCreator: ApiFacadeFactory): this { + this.apiFacadeCreator = apiFacadeCreator; + return this; + } + + public provideWindowVariables() { + return provideWindowVariables( + () => this.codeRunner, + () => this.log, + () => this.os, + this.apiFacadeCreator, + ); + } +} diff --git a/tests/unit/presentation/electron/preload/ContextBridging/SecureFacadeCreator.spec.ts b/tests/unit/presentation/electron/preload/ContextBridging/SecureFacadeCreator.spec.ts new file mode 100644 index 00000000..09ffbd4b --- /dev/null +++ b/tests/unit/presentation/electron/preload/ContextBridging/SecureFacadeCreator.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from 'vitest'; +import { createSecureFacade } from '@/presentation/electron/preload/ContextBridging/SecureFacadeCreator'; + +describe('SecureFacadeCreator', () => { + describe('createSecureFacade', () => { + describe('methods', () => { + it('allows access to external methods', () => { + // arrange + let value = 0; + const testObject = { + increment: () => value++, + }; + const facade = createSecureFacade(testObject, ['increment']); + + // act + facade.increment(); + + // assert + expect(value).toBe(1); + }); + it('proxies external methods', () => { + // arrange + const testObject = { + method: () => {}, + }; + const facade = createSecureFacade(testObject, ['method']); + + // act + const actualMethod = facade.method; + + // assert + expect(testObject.method).not.equal(actualMethod); + expect(testObject.method).not.equal(actualMethod); + }); + it('does not expose internal methods', () => { + // arrange + interface External { + publicMethod(): void; + } + interface Internal { + privateMethod(): void; + } + const testObject: External & Internal = { + publicMethod: () => {}, + privateMethod: () => {}, + }; + const facade = createSecureFacade(testObject, ['publicMethod']); + + // act + facade.publicMethod(); + + // assert + expect((facade as unknown as Internal).privateMethod).toBeUndefined(); + }); + it('maintains original function context', () => { + // arrange + const testObject = { + value: 0, + increment() { this.value++; }, + }; + // act + const facade = createSecureFacade(testObject, ['increment', 'value']); + // assert + facade.increment(); + expect(testObject.value).toBe(1); + }); + }); + describe('properties', () => { + it('allows access to external properties', () => { + // arrange + const testObject = { a: 1 }; + // act + const facade = createSecureFacade(testObject, ['a']); + // assert + expect(facade.a).toBe(1); + }); + it('does not expose internal properties', () => { + // arrange + interface External { + readonly public: string; + } + interface Internal { + readonly private: string; + } + const testObject: External & Internal = { + public: '', + private: '', + }; + const facade = createSecureFacade(testObject, ['public']); + + // act + (() => facade.public)(); + + // assert + expect((facade as unknown as Internal).private).toBeUndefined(); + }); + }); + }); +}); diff --git a/tests/unit/presentation/electron/preload/WindowVariablesProvider.spec.ts b/tests/unit/presentation/electron/preload/WindowVariablesProvider.spec.ts deleted file mode 100644 index 82f86a2c..00000000 --- a/tests/unit/presentation/electron/preload/WindowVariablesProvider.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { provideWindowVariables } from '@/presentation/electron/preload/WindowVariablesProvider'; -import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub'; -import { OperatingSystem } from '@/domain/OperatingSystem'; -import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations'; -import { Logger } from '@/application/Common/Log/Logger'; -import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub'; - -describe('WindowVariablesProvider', () => { - describe('provideWindowVariables', () => { - it('returns expected `system`', () => { - // arrange - const expectedValue = new SystemOperationsStub(); - // act - const variables = new TestContext() - .withSystem(expectedValue) - .provideWindowVariables(); - // assert - expect(variables.system).to.equal(expectedValue); - }); - it('returns expected `os`', () => { - // arrange - const expectedValue = OperatingSystem.WindowsPhone; - // act - const variables = new TestContext() - .withOs(expectedValue) - .provideWindowVariables(); - // assert - expect(variables.os).to.equal(expectedValue); - }); - it('returns expected `log`', () => { - // arrange - const expectedValue = new LoggerStub(); - // act - const variables = new TestContext() - .withLogger(expectedValue) - .provideWindowVariables(); - // assert - expect(variables.log).to.equal(expectedValue); - }); - it('`isDesktop` is true', () => { - // arrange - const expectedValue = true; - // act - const variables = new TestContext() - .provideWindowVariables(); - // assert - expect(variables.isDesktop).to.equal(expectedValue); - }); - }); -}); - -class TestContext { - private system: ISystemOperations = new SystemOperationsStub(); - - private os: OperatingSystem = OperatingSystem.Android; - - private log: Logger = new LoggerStub(); - - public withSystem(system: ISystemOperations): this { - this.system = system; - return this; - } - - public withOs(os: OperatingSystem): this { - this.os = os; - return this; - } - - public withLogger(log: Logger): this { - this.log = log; - return this; - } - - public provideWindowVariables() { - return provideWindowVariables( - () => this.system, - () => this.log, - () => this.os, - ); - } -} diff --git a/tests/unit/shared/Stubs/CodeRunnerStub.ts b/tests/unit/shared/Stubs/CodeRunnerStub.ts new file mode 100644 index 00000000..cef0ceae --- /dev/null +++ b/tests/unit/shared/Stubs/CodeRunnerStub.ts @@ -0,0 +1,7 @@ +import { CodeRunner } from '@/application/CodeRunner'; + +export class CodeRunnerStub implements CodeRunner { + public runCode(): Promise { + return Promise.resolve(); + } +} diff --git a/tests/unit/shared/Stubs/CommandOpsStub.ts b/tests/unit/shared/Stubs/CommandOpsStub.ts index c2a3d74c..1059b9ca 100644 --- a/tests/unit/shared/Stubs/CommandOpsStub.ts +++ b/tests/unit/shared/Stubs/CommandOpsStub.ts @@ -1,13 +1,14 @@ -import { ICommandOps } from '@/infrastructure/SystemOperations/ISystemOperations'; +import { CommandOps } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; export class CommandOpsStub - extends StubWithObservableMethodCalls - implements ICommandOps { - public execute(command: string): void { + extends StubWithObservableMethodCalls + implements CommandOps { + public execute(command: string): Promise { this.registerMethodCall({ methodName: 'execute', args: [command], }); + return Promise.resolve(); } } diff --git a/tests/unit/shared/Stubs/FileSystemOpsStub.ts b/tests/unit/shared/Stubs/FileSystemOpsStub.ts index dccffa16..fc37ab5c 100644 --- a/tests/unit/shared/Stubs/FileSystemOpsStub.ts +++ b/tests/unit/shared/Stubs/FileSystemOpsStub.ts @@ -1,9 +1,9 @@ -import { IFileSystemOps } from '@/infrastructure/SystemOperations/ISystemOperations'; +import { FileSystemOps } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; export class FileSystemOpsStub - extends StubWithObservableMethodCalls - implements IFileSystemOps { + extends StubWithObservableMethodCalls + implements FileSystemOps { public setFilePermissions(filePath: string, mode: string | number): Promise { this.registerMethodCall({ methodName: 'setFilePermissions', diff --git a/tests/unit/shared/Stubs/LocationOpsStub.ts b/tests/unit/shared/Stubs/LocationOpsStub.ts index 0570879e..5c8b75bf 100644 --- a/tests/unit/shared/Stubs/LocationOpsStub.ts +++ b/tests/unit/shared/Stubs/LocationOpsStub.ts @@ -1,9 +1,9 @@ -import { ILocationOps } from '@/infrastructure/SystemOperations/ISystemOperations'; +import { LocationOps } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; export class LocationOpsStub - extends StubWithObservableMethodCalls - implements ILocationOps { + extends StubWithObservableMethodCalls + implements LocationOps { private sequence = new Array(); private scenarios = new Map(); diff --git a/tests/unit/shared/Stubs/OperatingSystemOpsStub.ts b/tests/unit/shared/Stubs/OperatingSystemOpsStub.ts index 418cc0a6..f7f8e3c6 100644 --- a/tests/unit/shared/Stubs/OperatingSystemOpsStub.ts +++ b/tests/unit/shared/Stubs/OperatingSystemOpsStub.ts @@ -1,9 +1,9 @@ -import { IOperatingSystemOps } from '@/infrastructure/SystemOperations/ISystemOperations'; +import { OperatingSystemOps } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; export class OperatingSystemOpsStub - extends StubWithObservableMethodCalls - implements IOperatingSystemOps { + extends StubWithObservableMethodCalls + implements OperatingSystemOps { private temporaryDirectory = '/stub-temp-dir/'; public withTemporaryDirectoryResult(directory: string): this { diff --git a/tests/unit/shared/Stubs/SystemOperationsStub.ts b/tests/unit/shared/Stubs/SystemOperationsStub.ts index fa83e5be..2b9ab3bc 100644 --- a/tests/unit/shared/Stubs/SystemOperationsStub.ts +++ b/tests/unit/shared/Stubs/SystemOperationsStub.ts @@ -1,40 +1,40 @@ -import { - ICommandOps, - IFileSystemOps, - IOperatingSystemOps, - ILocationOps, - ISystemOperations, -} from '@/infrastructure/SystemOperations/ISystemOperations'; +import type { + CommandOps, + FileSystemOps, + OperatingSystemOps, + LocationOps, + SystemOperations, +} from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; import { CommandOpsStub } from './CommandOpsStub'; import { FileSystemOpsStub } from './FileSystemOpsStub'; import { LocationOpsStub } from './LocationOpsStub'; import { OperatingSystemOpsStub } from './OperatingSystemOpsStub'; -export class SystemOperationsStub implements ISystemOperations { - public operatingSystem: IOperatingSystemOps = new OperatingSystemOpsStub(); +export class SystemOperationsStub implements SystemOperations { + public operatingSystem: OperatingSystemOps = new OperatingSystemOpsStub(); - public location: ILocationOps = new LocationOpsStub(); + public location: LocationOps = new LocationOpsStub(); - public fileSystem: IFileSystemOps = new FileSystemOpsStub(); + public fileSystem: FileSystemOps = new FileSystemOpsStub(); - public command: ICommandOps = new CommandOpsStub(); + public command: CommandOps = new CommandOpsStub(); - public withOperatingSystem(operatingSystemOps: IOperatingSystemOps): this { + public withOperatingSystem(operatingSystemOps: OperatingSystemOps): this { this.operatingSystem = operatingSystemOps; return this; } - public withLocation(location: ILocationOps): this { + public withLocation(location: LocationOps): this { this.location = location; return this; } - public withFileSystem(fileSystem: IFileSystemOps): this { + public withFileSystem(fileSystem: FileSystemOps): this { this.fileSystem = fileSystem; return this; } - public withCommand(command: ICommandOps): this { + public withCommand(command: CommandOps): this { this.command = command; return this; } diff --git a/tests/unit/shared/Stubs/WindowVariablesStub.ts b/tests/unit/shared/Stubs/WindowVariablesStub.ts index 78c543ac..88816beb 100644 --- a/tests/unit/shared/Stubs/WindowVariablesStub.ts +++ b/tests/unit/shared/Stubs/WindowVariablesStub.ts @@ -1,12 +1,12 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; import { Logger } from '@/application/Common/Log/Logger'; -import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations'; import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; -import { SystemOperationsStub } from './SystemOperationsStub'; +import { CodeRunner } from '@/application/CodeRunner'; import { LoggerStub } from './LoggerStub'; +import { CodeRunnerStub } from './CodeRunnerStub'; export class WindowVariablesStub implements WindowVariables { - public system?: ISystemOperations = new SystemOperationsStub(); + public codeRunner?: CodeRunner = new CodeRunnerStub(); public isDesktop = false; @@ -29,8 +29,8 @@ export class WindowVariablesStub implements WindowVariables { return this; } - public withSystem(value?: ISystemOperations): this { - this.system = value; + public withCodeRunner(value?: CodeRunner): this { + this.codeRunner = value; return this; } }