From bd44dc3183bb775c443eb9da7c160a7e66726cbc Mon Sep 17 00:00:00 2001 From: Tobias Reisinger Date: Sun, 19 Nov 2023 18:54:27 +0100 Subject: [PATCH] Add much stuff for rewrite --- .drone.yml | 38 ---- Cargo.lock | Bin 49884 -> 49208 bytes Cargo.toml | 22 ++- README.md | 2 +- build.rs | 3 +- emgauwa-core.conf | 17 -- emgauwa-core.toml | 8 + migrations/2021-10-13-000000_init/up.sql | 2 - sql/cache.sql | 10 -- sql/migration_0.sql | 83 --------- sql/migration_1.sql | 28 --- src/db.rs | 53 +++++- src/db/errors.rs | 2 + src/db/model_utils.rs | 60 +++++-- src/db/models.rs | 19 +- src/db/schedules.rs | 36 ++-- src/db/tag.rs | 16 +- src/handlers/v1/schedules.rs | 12 +- src/main.rs | 35 ++-- src/settings.rs | 66 +++++++ src/types.rs | 118 +----------- src/types/emgauwa_uid.rs | 122 +++++++++++++ tests/controller.testing.ini | 65 ------- tests/core.testing.ini | 16 -- tests/run_tests.sh | 67 ------- tests/tavern_tests/0.0.get_all.tavern.yaml | 23 --- .../1.0.controllers_basic.tavern.yaml | 116 ------------ .../1.1.controller_relays_basic.tavern.yaml | 42 ----- .../1.2.controllers_bad.tavern.yaml | 99 ---------- .../2.0.schedules_basic.tavern.yaml | 89 --------- .../2.1.schedules_protected.tavern.yaml | 74 -------- .../2.2.schedules_bad.tavern.yaml | 169 ------------------ tests/tavern_tests/3.0.tags.tavern.yaml | 108 ----------- tests/tavern_utils/validate_controller.py | 45 ----- tests/tavern_utils/validate_relay.py | 68 ------- tests/tavern_utils/validate_schedule.py | 97 ---------- tests/tavern_utils/validate_tag.py | 34 ---- 37 files changed, 374 insertions(+), 1490 deletions(-) delete mode 100644 .drone.yml delete mode 100644 emgauwa-core.conf create mode 100644 emgauwa-core.toml delete mode 100644 sql/cache.sql delete mode 100644 sql/migration_0.sql delete mode 100644 sql/migration_1.sql create mode 100644 src/settings.rs create mode 100644 src/types/emgauwa_uid.rs delete mode 100644 tests/controller.testing.ini delete mode 100644 tests/core.testing.ini delete mode 100755 tests/run_tests.sh delete mode 100644 tests/tavern_tests/0.0.get_all.tavern.yaml delete mode 100644 tests/tavern_tests/1.0.controllers_basic.tavern.yaml delete mode 100644 tests/tavern_tests/1.1.controller_relays_basic.tavern.yaml delete mode 100644 tests/tavern_tests/1.2.controllers_bad.tavern.yaml delete mode 100644 tests/tavern_tests/2.0.schedules_basic.tavern.yaml delete mode 100644 tests/tavern_tests/2.1.schedules_protected.tavern.yaml delete mode 100644 tests/tavern_tests/2.2.schedules_bad.tavern.yaml delete mode 100644 tests/tavern_tests/3.0.tags.tavern.yaml delete mode 100644 tests/tavern_utils/validate_controller.py delete mode 100644 tests/tavern_utils/validate_relay.py delete mode 100644 tests/tavern_utils/validate_schedule.py delete mode 100644 tests/tavern_utils/validate_tag.py diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 8a07dd3..0000000 --- a/.drone.yml +++ /dev/null @@ -1,38 +0,0 @@ -kind: pipeline -name: default - -workspace: - path: /drone/src - -steps: -- name: build - image: registry.serguzim.me/emgauwa/builder:rust - volumes: - - name: docker-socket - path: /var/run/docker.sock - pull: always - commands: - - cross build --release --target arm-unknown-linux-musleabihf - - ls -lh ./target/arm-unknown-linux-musleabihf/release/emgauwa-core - -- name: gitea_release - image: plugins/gitea-release - settings: - api_key: - from_secret: gitea_token - base_url: https://git.serguzim.me - title: ${DRONE_TAG} - when: - event: tag - -trigger: - ref: - include: - - refs/heads/main - - refs/heads/testing - - refs/tags/** - -volumes: -- name: docker-socket - host: - path: /var/run/docker.sock diff --git a/Cargo.lock b/Cargo.lock index ce30f6a4267e05ba0dfeb2b149fb7d7435ccf100..5229765836b3d80ba33b49ea8141784c1ebdd9b2 100644 GIT binary patch literal 49208 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 TE1lRzL980#wLxP4{N?`xE0yA* literal 49884 zcmciLS#KO!k|p5x{1rm>V-1zQFAWSZz&y?SycHUPuTpeMq>4*sseb)@77G~>a;KC9 z=X5t(WH88#>-Sz;jvYJpAMgI}r`^N5dG~pG=ilz>@$O6axEs39pTE8PUmvI2>AriM z#&`X{N8<>{_DSfx$VA8Uj4Nj9(Vt|8Schu`1S1Fzb5bV_sOrn zJls9q5BoRXPak&=kN4mH_UZBQ>%)Kk_S?tZ_VksjPjRarzo-y&ZPbgTMIqU*7%N?;hvR-N%Pt|3`fD&42w}^1ggu z?Z2O&9-r=~2kX0^E?#)p-TuCR;q%?c{l8y#w>Mw!@BVul9{qmtK5OVmyL~XZAkX{Bfpr-Q;zbCQX&kY1wzpn0He?4QV!aLz;NLZChsT zG-gTC54L@pydf&#&3XyU)(^j~@U2 z&z>LU?0xn=-S{Y%L*KMzGj>H@*J(B9bMBZt0#ub)oB@Ts%3!>4?H?R0WZFFYTY`JbP< zhfn*D_v!F=o)1ub(cM2BKH%Q)eCZBHYxj89?SCb#(-YHo^LVlqpSyp3`|$7>hW7A| zqb2yV`*Qe0Uni#T_VMPllKs2R?L_KQAI!vkcRTKX%u2F)@z~vx@$h%{Uw!!Rhr8RC zAAESa|GfXZ)8Y3XK6N+g;m4o5e*eXz&%qQqy|wROwK!k8;r{O7!r&zD)A#wthrTa{ zt{J8z>9eZt=4NoL`>`tOaZ1K2osz09`f94uv?+(7?#ecs=ccLate-|#V)4-bbwoLQ1tc#(Vi@YeaWy`wvO^^4k#D0695AVtPeSdUbe-xJvCbi#N8@aT|=V8d& zG_$|yIA#4X7gILZSu$o_l?+)^m08ypY28*$*`?EzmhF_xeLnV?W4rEPjwklTK3y5D z3oCOn1xxng**rXa5ib29iiy8@a@(&y!SG{OhbeA!vGHLl$|9*J*P_mbj`wYfzU!*0 z9?CJ#rkS4}hH-4ha!UHrg=(j{>8g4z#<{PP=C8PlpLWwf^+Znx^UE;;^6$;lc(CTT zPhUO^U!M-&Je*9{!`<-v^ccqZhYvmii1TY4Prlmt$tOvf6?r+9P1=|J)c7(@o!~z2 zhsmKGnr=>;v8l(=BOe5$b7PyPl4ozSzF$1Qr$_gE@XkHLbL9By`o*XboqxCft?zG9 zXL&nxc~diHeO*jb)iqsPjOj2n;_0;Lvu?<%IV)W1L=aP#PUc*-=~TDrvKc!>eBa&Q z?~dLv>ia%qJRPf_JS=C1;rzTrezLWH6DPNGQjAF=bSrueV9e_xPx4Ic+$U9)Iu~K< z%6?|{^Qy@_%eEV`VMwNYIX}6Mh)?I|+?cI}A{Vzzg!6mEQ=jh+!T8yO`N4gA6r7(w zySGS2!mzcya5t~gVJ^!g@5;ngn^KublT=OXT%}W6O+(jqbzUY@mdoq9p{vt!UcPIG z!*fVZUtGm&`N!$$9&hKX*D&7m|KY(*ZIWfu>$0lyj2o@nx@BURo?=eQw(hc~Yx1OP zhjggtX=wT^&uZbtoK}6=EgGTa2lL$>JY{K@asE|ozmTGxpUu+~ea@IqFFYH-(}i9% zgaYA%cX)Lx{+t|u7p&q}cl&*|u{V(Pq+`*gbvD&=*U6f*tVp{ondbrsQFeX8NDf)e z1_*7&qJ&G-bw|b6B z1cnz$ReXQ@?G!Xh$$k{MoIkSj^BR7CYqU%bQq6Upv=Wbef-Oid=BW&unwL#Bwn+o~ zDW;-m`1xrXs`HGW=*l()X2b5>0Im30RoD(AY$#!7TKbYHd||`S9&_xOu$qc9&UUpaA9OVP%o0%~aRbI9E-YmYtwzoMMAOwOm^+FdFNw z=t@!cFcfUr9Zj^*TtEw~Q2=$GB&P1o=C*7x(SKbYS?cemZw9poo} z-@dZriUh;__Bn)wCV4uJbu(p6?JL<*Sq($O7R`k{&nsDNHRKIwd7NrFSCLN@thwom zst~>`-$}ma+<-<%x^+0O&X7(;%X0Qg3xh4{hsed-J}J_CY*$#Tkz1v&@ET5st zzXC|E1Zx2u-ze&wMQQ=RZEZr`sVto#ZgQ+NufdA$U!N8UbDy(1Q_=7ZiNvd}rCmdn zw%kD*Nc%Kr4bQiD=8nMk<)=@#FLSk0$a(69Qbs+DT`$0&`R-Er1<=#i)3jp$N9w_(ItUgziX_XJz?CwuVTSWO{+#{gSk)h4n zq+|mcq4PMvP?Boy(MsVBBgQp^Ogw$oi?~WZbvQ0bodmcF3 zysS&le{2faX2mzRQrBcI`l8FBLRni+9LR#j-dAFG!%cU8-+fy*&HcWRa`W)*GPw+c zzIDV@6x`3$L87fiOf-z@q{Gm1I$2(IQ!)14)PT6ePGY9oksQ*#FUO|px@mCK7M3|2 z>mdh+v}Ov9)uQvm_!cWn2L6Y8QPFbMRffwU<#Bs(-B1FV2MElZx5}GRwXV+I_yeuzLCXX7eN4%;}T?)$@)4NxFJ~ETvgdDKWTa zLlha)u1ULeo{~mds;JTe@c;%@(f1X9x@`6MnLDMZ%R84!Shn`JsSnw-L&Ps32KzDZHv-Ib{#8d1-;GdxQM*(N#QM`8J_2(RL!xeyc4O2^6jY|69tw!|q9Ho~8HWeH3X!sQ*NdyS z&Y0?U$fQbYplwklm22BeWve_Lpv+FzJak=WHG+dwAeslOpij$c6dg>{xE2u{LdJ`a z9|>RFq)mA{E<)Llss?SPV9*r)e?M|FNjAC9ZuHm#Gbe?UyeKNsOj(E!I!G`>)?}RC z;>BM0_-}EvaEqj;h5S=q2Egx!M8G)ZgD=u7nI>S0vYXO%nfJvs_p{nZTK8inc~4fu zG5Pr2?WI=;6ew*se)v_*(p1d=?rDasYpdK@$=YTR3ah#{tm^Dk$+cY-_X8%R*qAWE zoib0&$S=6~=VVib0aNP{vKHvVyI^+khj=&kek?-I5b6G0x%ERM_`S$9J z7MrIGv($U;>{eN|NuR6Ji}Yp%R|ExR318wwm0mBE^V_UXO3~_E0hUxNs!6GO`H0@# z%5@$;3=*I3*0A+8t1`iy!azPXS=fY3h0@tn46fR2?BRPe^WG&x9EPGTD@hV#83l!; z=#n+~>&V?XH*%~*v3Z?4Dn4V~XK9nQ{ZI!mC*UkP(Y3O-@Edg5m1W#18_;J-mR4Rv zh!T3EP%IwQMGnq2hv8!rr{L;Qyhlvg)LwSkIJZ@zM%YwspDFH_DmraG zhZ&msvd9?EsZYTs5-Gp~0Hw)t1&GN{bD_=hVf^|et=tnDug>E3{I<4Knkpo&l~Y$T z@;T6NmO^mLMU7FIN&qDhGbB?@x#76dj8{-b!Ca^q!F8*%5@!$hL++A3Tq2n18<6(` zw5(Lxrbfn4b?uy~^h#gbVM3rRfeJ-tce17^(h0M+ZMnO825PljM7ms(yTWf)7I_Zp zTpq)=eV=X})E04|#?fwp>?Vnyup&$PekxOxB}oP#7haOi*=X~+A_b%5WuDf22nb*q zi)PbbY-0F~b?7#@o5P0_S`_Kw!FfQd*RSb_+lv%<{&XT+*&1Z(vnI{UiS<{t7Pj=R zrm}06v8!2^Q%I#i)HFv0gmk5pf=pt#~kpm3%nt`N5%bybu5Q zynCGTJw6ccdV1Q8(B6s}S`AXK{?hfc**acTvnT}+PCR6XFXC81`jT!}o)1c=CUGx@ zyodd$M)fUEBz=-UB{GU}Ip$bDCO>f;K784IL@h^Uzf1+=WNht+LHKZ#21e{IsLQb9 zHNzClN05J4R_;yNj@eLRZ;2kcX@mynqm`t14vQz_<@0}tczp5Wfis;c?W*yMb| zS_#TF%(kC6j6%xk$Su0)5tsG$kBd())+#qYqIE3<>_K89N@|fN>=-kZ)MeErJXeFX z+NN_As9&c%THg<6MtQLIe3Kq-`W4@g&cl#1`VV*{`8-_;0u3^ z3gL=PI45IVuG!z%Iv4G52)m~3_{2Wk10+vKn+>hz6D*p2y<5PT4tba~G+j5e3L>M^?9eVAw&;Y~@9Mwl3D3^i>FuzGq=NX0MJFPK2G(gCv9qx#O=R3l`&uV@7&{;;qLR@ zD};w*H_7>P2saT})269hsu0{XTGGJrHW-&d#aJPw!`~QGkCp4V#G;;CIJRRB#mq`| zCUx=|>bep^Uzd7cQlEG{&*03L-Rmrd4PitjernbmdW-71+K=>L2P9bUEgGb@O05zYFPYo-UxHOn_GvqB}_%O z8YTWYxV{jIS-{d}$`F~;E`W2VXQ7WhO?k=yS;o99#R(GLWrRYfCfwl=-7laOYrFO| ze0tb*H~-q*%ctJlo@HQl)!eGpht^G1E_OheQbbZ4?16`95>r;T0C=G2G%EHg=)#nQ zMq<6jty*xN7p&6xVnN5`=BF0FSSV#dHoeE<_|DnHTFO9vfwY6P^G<3HYgZ+$6OW-8 z5+{31Yt*w0c7vwH_9t`Qt%V#%PR4ipmu`InRRU2~108|`GsETJCaTeE6p@CN4HQ19 zYo(noN5=`P%nvL0v`7aPhR)&!J~K*whPZib0y+K8cXZ&#?&09wPVab~f_-||iE4a) z_X8-{{0Ww0r@+*BTHxWd(tI;W+FG$EGE<%uT4u3;d-p-vQc)cngO?wWEQO-($?H$> zx@+wTpCIv8)hgfbZPP?=e2KX3GmV{##7cZq75JQa%)nBUv4ER~8@DgeDGM4v&K z)Va*{2TRx|-fzDOtVvWeSobjb2Q^YZU8*mjE&&K~n@M^z;imNZbqZ}L`YMGNX>?Dh zNv-rFg<60R`YN!z`J~hT>a{~k@so_-)BW5ZF4jYnA0uR5Jggu+c2XNatttbg)i_KO)}e*RIXzmD4%g)AQe0cSFyV zX#n+0JHbt0ulug*XsSN^=%p{>B{5sNKR36~w^P6Nt4%G(R`tux+P+8=IaxL8Mee2y zOqr&+R6EJW9Gw-s-zwRo)9>l?cCo-SM7!zng+##s-esWj9Kv34Uzb?kxGd!>7APIvmpz8JH*T*f4R_)Jou!e4(V zpYJaF*@7hl!nMSnwFG^2}@x$%;NQlpfL}A$OEGFg||eFe}$L?l=0AN^V`0<=Fe& zb01}^McmPwU~G(c^cu*D+KiG)?jbHa(30`mRk{WxlS271VZvn*NUwQ~>#F%! zYFjNgbY5~?h0;f>cN9Ka+v<~G()pUT3U0pD9FGJ@;Z*DOj6?va;+dtoQ#J+163xIl z&I|l+(3WJQgI&#t;jVLt2(0<2=L*940{Ga?E3N2wi0YbKT3x1UA?WKtf<2^0>S811M*mM7rA zN7`s*Dqvu*$4ie)iO;PO7rr#%NGxYZulvT`S7n-cciC?ncXsQ|n^Dk+60s{vHi7HyUoE!i9J)vC=kFyh)WuXd5L*rBUKxf;ui1hi_}g~3Pv!-vkcjle~h2ss>H4b zL3_t?xx;Tj&Z|?gjBDvlYu>>bZb2f2@(7-MK@I?DLces5T4@GwYl6Ka{@XtcgYGvDc%d{adPdeh{W*E{Y8~63BTdFqxfaMn)I?MQ z+)AWBJzPEaLJ{(Gt&Y$pI^Uzw3O14`ABFIVzL0QnG9>-48hM2- zI9O7|S-bDNx`8^dKMR=GV^B-s&j=(LQY zDmu@E?=}G%WDQzKBYd7{8UW_0-!UsH=@r43V0AWf_7(BK&sX)FMuHOVGNg6rdDN<>^x5(<_#c5|Z1M(=n+dmR|ilGOBBaWHfCb%qiF0BKnQ35Q%8$gpcIr4Du0R9t$9d1@LfT z10Rs39a1p*37w7QqT}L*?d!Auo?h8rN$&hf>?uWChgo|KWZLdBlL1dw17wRM%FdV0 zrY;H1WNQ5yQ1yS4!!A^0@ahsVV_>uq;V<94Gur3n8n$oRlv=5gPTGm}+&hIh33f~o zx#_ePC^hQiMgc;E*5}ezXi>SMThb*Wd`Ep5lkE0taj|_b5Xnkn657I-YpmX9o8w?O zsqSD&3!&HRr@?>|BOx$_W5aet2Xl1wbpQuolhhHKvO?*OHm49Ud(B@5zQLo%VlOF> z3okvld>vqt_??T4G+uayu}`#UPw3D6-?}|0D?bZro^roEn<0|H^+&vyZN7fYTQ}5o zx=l3Fpvi|2A%0n*%e4`pMz^mHkEx5|%}mEZZ-8V8i?JhSQiZy>-C4GOFHh?E!5rkM z*BATe)q&bRFVR!kB#rpnn3CYY(+x89p$Yx4KD<=k4Wv*=R|zWiydR4A`&@C<87qNY zJWt=9gy;SKKIebPJ{&=e)m`8J_T`oXn88DvmZ0;NNEEM~p=41_4)d>;t5zGb$qqol z_t|Mdq@tCOBiaWg{RwRwG^1`Ai91HZXE*D4@1GyVtJ-+yq6U|bfZyLb8PFnzSFK4O zPouy%RJ`HU1z<)96zsmH(kQ;GdQe1!wo>Tq8oFk>oT!+MD;D&O)OR7tuyX9r-gCKN zC6TQc+;##OIVTcn$LGuL8rA$vtf4)AZcYFCZ;4vY1a3b^yoIo)oG#uOLhH-s0j?c)ZbmakCsEDM}OCk1%B4^zd zF_gviEsH&_?B?Cq?jKLnjkdR^>uh%W<$@^jd<|a`tHH}dLIV!jP~Zvu6QyU`v!N3d zQ7yqOOieXA`9=}cX(8-N5Oc0pe!gXH_4c`{Dk-UEw~CGwUxFYOo!U`cRX0WgP*V)O z9Y8Eo#U3T5CXJP>LpH98unaex>NR_>d;(75o3o{TIFd?MHuAf7UOtItbKFHu5*sc> zhJv<;L^CH6fGHcC9bYvwMY1y!-Xy$(&?M@2BzZ zpC5?c#n`faz;ZE>59}?N)#6B+gk%Z8 zjgpz{1$?TkMu;uSNt0UP+UdXU(~i%+by%x2C3L?e8EVm?BZdB^8m5-1D-HZKnnW|z zfgQ4Di5_nES@=%kP;HD>)8b}74I=@d{j8_8L;b?0y^6Hsy8O(tr( zvaEqt1-St%Cd!mv%|M|@ev0neo0w@mFR1fiQ+UF1|J^ws_|%Lqiari~`-{tte!Hzh zX&wblS3s4abgJ#S%9gg*MvL;7L|aXtLYgoG0k8p!gy5qqO|#17UM}1(uKMulSEIm? z`XGMz1m0r*8F4ti(3&=}D?B^%A!~`w8JecV2%rbL)Zpmz>=B7UxM!7Pint-Nxa`k1 zcmP43zuNvc;4ft4@uKWgn$e)ftz1ZCQ6drcSFf>SfTtli(nzD{Mwd4k_$FAd!Zu|p$2)DlvB{=sjA9E~)Q zeO`ozVjYK;g>&Z^*^^n(fD##UyD-{uMx(j&SdhuL%RB4lkkdwml8?k4W z4n-PmQaD=#KInje$7lL$Eyp+E?DFx7f@{0*jI17dT`t#w3AlBoLSr780P>5ewg&0d z9yA`RW>4@clONRryX;0MA~7dwi*?#qwYaqQ2zTnRea#v5WUoFS#W(DW2p zVIj*sR3ua>8j@S;6*>{McVTt91&lz@0xBB<_b_(mhYN8D&JI;Ln0)Jdftwhm7p9eb zhgq;Xoz*U5lqt56&d*-aYZ7EorDL}=nb65017ul_BdM(A*zw4wCRnfzNB-l<7>=9i z{+_(US@(o=ftdyB(9qw6c_wB@dNoK^MiWo$ z>7oHUv`}7tc=mT|^P9UX!6xa-nWQxwO(L#0IY@0JpJgJ5Pby^5^F+yj60XH%TK1JE z^-3noq{Yi9U?tYw&kQ(UUrb8bI(?C=_l}zeNX!%p)}FVRH%D!A7stKctOwH zRsb32%YSvr@q{iHyggGK*N4L*$RE>IO?RNKf<#i;Xk}lP3U^{(6-Ek#yC-aIE(Fo* z=ccMIF2}qJ$Kkbw+WgC98cVXV^DvH4@l=RJG>>JKzJ}zhJJ1*r!54N~9h6e@fD$FM zN^VGx(j0Tp%^%E{%cnoHkr(jH^(r^FUG$OYsH<@!Fj0ZWWyP#0JIe36a}*nKb{*-~ zRtj}_P|k5Q$wSRp4{JUz=j~^LBWt_39}#xGx@cQIW|(yuN4^se(~ z5aN>A)?uk3CbBW;cmWPalUEKyOmitG9KxEkK_?$4V1V=4&;imQf$SAs9?^ebROE_p zJxqDuQ}eFw;`UtuC<@Xn=vgKz6Dc4~rV`4~0Vj#7T%Rgop^vNINH;a(5XnJi&?8`+ zDuJWLeSGesiak6JcGmapTqX5t(c8yF5;r0mh+IZSv~XYAF%(S^b$h%?jSu+q;gn-| zeM47}EJLIn(Jc_*S|XiTkOF#`zKB>4EyS-J&{r0^eZ!zeM<}jJhN%Z`T}%+5DF`m> zqawu=Nxa=#!>Sc$15l&^6;fxjDyCR#8AWB$^|@QS)SVv!PMryP=!=Z zK{k{Dp;T;I{3NC^kEb<_IGMWMCpP;}bw|#qr~|!Lp9%$I(-KH(tgT>w7%!gxTgarE zTqA`Iz1 zh0$9*WrxFfwb1>++!leDoik!@iQ=D73(kOEO?JwtxG^K!) z#3z*8brjcBo+*Dv%B+~ZctF2KW$Tp_Y0}We_Jrl5>a%eaEj7TTGZD&Ap%5fSE78=a zCIWtBJpe`p;25kVZ$|2>Lp4CNfS4Kv1ASUt+Chr-S(|mmv|qgI(n7w)N>%oVy0Pxq zch;$38)X2RJ35}2L2~<1zc=k{0Ckd6Ffgi=AfqK=XebsfuJjTFh%kkktzE?}drg`B zOsn>K;nmh9Fxn4T2s#7sFo4X6dz4pA*EE;Fcs;8pMXQ9Ztc}u)dZUnyT>|xJLYs43 zE`c4JqGi7P^M1L<`6jRKwYk81m0i+5=gU4A=WVYn0gsT}*6>2viu9~J7aPN%BV02r zNNV{Mu{8D=UV=1du!Ory7XyF-lhZM{xZBqlE@K0LBZ@09Oyg6s1&d?v-TB zXT|)Nab=*8ME&0^SzVDpFY}?%NtJ$=Gp{4ew|199vFq{xZ{PQ%J4wY*@-wi?y~WbxhP3+K@(T|d_5bS zy8^o%jsB0uD?L1pA*6q~$2c`x$9`(g;+&`*!~_=ylDikQqCa8CxKx<8(f!1F(0FwT zRo`&81chK#1tE8A0l+xTod}zMJ+&qeB=qT~Wx)28IP-b1+(kUSf6T9jpvzDE^66v-r*CUl zFeg>vbU12J)0&SoN=aC!sy;5%_fYYyYTj9XUug!V7(y(bmJu~tRbrQN@ys2kX~&xX zy$2P~@MUzwLj)mz`2n8J<>eEEns59Bv#6A(WXJuZwr3KytVXpb+zY2p<%zl|Q5_kn zF>>s)j8!HRb63W+S{=9&`O5i6k90FcNH_H=F&_-U0v*Nxax+R0FSLm2I5$&^w5!Wt z*qsVZO904FH=qzynO{K%h%j|IEM4F7lZei%W%QRB375;?o?IPj;v{%m1y~q(496v3 zp{gNz(aqh}x*SjkrkG}43OMZ_P{img?EtvOv*OEwy*+r&lXBil$oBYY$2xpZ6c9RW z?Nk(M@Eg=0P4?OYB{7Z`|97yS((zOrP>c(t1jROnf)UHn3|tk3)<=xm#YH}U{+r#G zufd7_(K-+ByWG$>Sgf(Y7{Wp!ku?bz^ZY8N@c8Lo>hksO?t0=|vw4FqSs(yEzLXpqqAZEVr49AaV7>!nWAL`{ z39Mj zG@KEJP>VFR4y9^hr>Ja~BMi(?0Evpp@skdam|wkcOaEe^yC3Otxl+W4tz+D1nKo3E zGNnGesIcnbpim8kN6jBA$Up^zrtoD4Gl8vXUQ?&2j82*fiIk#>@H}lm_we!Do`QX%BHa;PWL6f$Bh8cRQ`X`j3oKAKGo~Beem-Jp)xule$r^47F>}ZA?xlhvzX!nSK=IzC44M=kguf{5c!^ zVXXGJyZv^prxtvMt)1VjZ#Co=VgL^5ri5d+UIH$AwGw9l+0t=n)@#M2ucIPIvPBP@yikGzo?#-10vdXwJ(Zm>Zz9G* zmrRM4{Z!l!QA1HH@oyFpnu8?p<%ehcwvM6^#`Bm*BIepVD>PKRDb;J4y!Zs~7dMq) znwoG(N{m{vLE^aEs-;cRF5ABEgp-d02c89!&zi8ie|`JFDn9OlCU<5j&&Vo={%rh} z-QB+LJl=QBy+5$?4p-x034q++jf2MiPutSF-{ZgdxVwxb_BhkVrgX2ah(C%Y0xgp( zb2ynh8AJ0jCulD@sk?O1+5T`FzSFHfNd&z1bWd8f(>6kj2; zevizNe_Azg9j9i2 zEzyPInJk+zBP~THF!XfPK2@ZNd>cP&wezw8J3IgH_5i^7M*Dj8?BgTdjJFTQ9Nax# z>yV`F=8Xf?Mn=O^qWu;*$DAqr0SqKX8$52uoc+Lk0l86sCM}T?g*2_rgV0wK()f#J zqmy0@S~j~~)a4hdTbGkF<1H_e^a_C?L>w<(9v9yquTpSJ_iuPELhNtRb85c-E==$K zO=_k7{ow;Yu>8Iom(X;HE_PDnF6)JygEvOU$AglGLjwtjZrekm;vHW;rG>z)as-0isH-a z=Rfwv>}9eq*N|^-WN0(tmr+4L1eE4f&UvUltM>Tc=&5qfwT=c2cQW|RK)m4C1tMga z;Oc1T6DWD9?0vZyXKL%PffxlfJEa@8lA0+tc_;|Q*kPC?JR^u9FuMWdYN92s-Cjx( z4J%rf`gz%8p7N^I3v*iR#aM{*wWjZz&9g9*93YM2`XGF8iHHyIjS>3?lbXPy0pIkW zGw?YulEgRO6RxftjB;LXn&sN%Icv%~U&336SL;@iGrQ%p(eZHt_bRQy$ib`Dx8q*} zzf=4j>Yvbkv};vJ&Rv9)gO=Bpz^R;nbcxmG$N=yRtuGQ`D)^Y{8jXk%R%i;y!n{Qc zr_rX1d_i6#2(==$0LHZ)yFJZvU&bQy=Hzfs3Xx&&ztg=~KblXP>&_oWko&hzGc7T6 zUQC6ICI__|vyNHsQ71}54@1Nj?F}(G0PLn;(=a%QaR+k5NWl|W3{2h|zPr}{YP`e?J)R7a~(k%RPr4VO6^2%DguP&x%S+H36_T6NsPei`mR zZ#H_a8NW_2JID@SZYNs6*852oKoz{xE@&zRPBo%6$pogf7mRB+r~a&d3Gm<$M4B-g z$gLzNg6k37iIsbKiF#hHqTSv(YjHHi*R-pQ~Q z62hTPMjk~&mHk}fyDP?D>mk1~2W{J97_u^>iW5 z2t6nPTpgjT9AOQn7y<}1F=6UI$s;;n)1jKMo;zKB{LZe^FjRYh>F$A*;D`YL;Jx(zM?5KvqbUO7--u55QyiMRdq zW;q)DQ4zC%Xc<4y3g`e~!a2joVKHQ*FqQ%uFvrYLbs~5LK_4MU%&=bRr$5qmc4jc% zd>w9lJKp{IaP#r@&`xpu@jsjDLsfkLhLwo__*;h&HkU_o`zpBf4)#pDuji+TK2wT} ztPY(thbU+*z~I7beaq}Q>zrK z%hDjrR7s_5V3R5I5=vbX!P{2wRu9x9Dn!$#7d?v6owpoB?)EEMsHbgD4n(GkY3@Lb zUY4y>6)V~NKb+K<`l=d0Jpd8Ca5Pow0R=hZq5p^U`bZ0DnY?m1Rw|9JGB92}vTwb0 z1gSVXV2`3ZKZmrV=%pe+z)^)7TtX~>3b{lq>OVqiHEM-v+@+kd(CY98Yl;zGm!F;l zz{VCv?ODMh;u!takRBpog5{WcuL)M~34DMfGBzZ}uPNhz5hlkmcm*<@VHunI*GL?<{q@86l~HGDT%ar?of}xEL4@KVV@rl1fd4CZCugLhMw;Q4SRy9!w!(`75G|&%3oc Hj_m&doUM+X diff --git a/Cargo.toml b/Cargo.toml index f56d4c6..bc91bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,15 +10,23 @@ authors = ["Tobias Reisinger "] #panic = 'abort' [dependencies] -actix-web = "3" -chrono = { version = "0.4", features = ["serde"] } -diesel = { version = "1.4", features = ["sqlite", "uuid"] } -diesel_migrations = "1.4" +actix-web = "4.4" + +diesel = { version = "2.1", features = ["uuid", "sqlite"] } +diesel_migrations = "2.1" + dotenv = "0.15" -env_logger = "0.9.0" +config = "0.13" +lazy_static = { version = "1.4.0", features = [] } + +simple_logger = "4.2" +log = "0.4" + +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.5", features = ["serde", "v4"] } + serde = "1.0" serde_json = "1.0" serde_derive = "1.0" + libsqlite3-sys = { version = "*", features = ["bundled"] } -uuid = { version = "0.8", features = ["serde", "v4"] } -wiringpi = { git = "https://github.com/jvandervelden/rust-wiringpi.git " } diff --git a/README.md b/README.md index 5a43b28..e6a5938 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -[![Build Status](https://ci.serguzim.me/api/badges/emgauwa/core/status.svg)](https://ci.serguzim.me/emgauwa/core) \ No newline at end of file +[![Build Status](https://ci.serguzim.me/api/badges/emgauwa/core/status.svg)](https://ci.serguzim.me/emgauwa/core) diff --git a/build.rs b/build.rs index e2ad321..827c260 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,3 @@ fn main() { - #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] - println!("cargo:rustc-link-lib=dylib=wiringPi"); + println!("cargo:rerun-if-changed=migrations"); } diff --git a/emgauwa-core.conf b/emgauwa-core.conf deleted file mode 100644 index d433d74..0000000 --- a/emgauwa-core.conf +++ /dev/null @@ -1,17 +0,0 @@ -database = "emgauwa-core.sqlite" -content-dir = "/usr/share/webapps/emgauwa" - -[not-found] -file = "404.html" -content = "404 - NOT FOUND" -content-type = "text/plain" - -[bind] -http = "127.0.0.1:5000" -mqtt = "127.0.0.1:1883" - -[logging] -level = "debug" -file = "stdout" - -# vim: set ft=toml: diff --git a/emgauwa-core.toml b/emgauwa-core.toml new file mode 100644 index 0000000..2d0d9b2 --- /dev/null +++ b/emgauwa-core.toml @@ -0,0 +1,8 @@ +port = 5000 +host = "127.0.0.1" + +database = "sqlite://emgauwa-core.sqlite" + +[logging] +level = "DEBUG" +file = "stdout" diff --git a/migrations/2021-10-13-000000_init/up.sql b/migrations/2021-10-13-000000_init/up.sql index 62fe3aa..e963801 100644 --- a/migrations/2021-10-13-000000_init/up.sql +++ b/migrations/2021-10-13-000000_init/up.sql @@ -61,8 +61,6 @@ CREATE TABLE schedules BLOB NOT NULL ); -INSERT INTO schedules (uid, name, periods) VALUES (x'00', 'off', x''); -INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', x'00000000'); CREATE TABLE tags ( diff --git a/sql/cache.sql b/sql/cache.sql deleted file mode 100644 index 3e4ce21..0000000 --- a/sql/cache.sql +++ /dev/null @@ -1,10 +0,0 @@ --- a key-value table used for the json-cache - -CREATE TABLE cache ( - key STRING - PRIMARY KEY, - value TEXT - NOT NULL, - expiration INT - DEFAULT 0 -); diff --git a/sql/migration_0.sql b/sql/migration_0.sql deleted file mode 100644 index 939b907..0000000 --- a/sql/migration_0.sql +++ /dev/null @@ -1,83 +0,0 @@ --- base migration - -CREATE TABLE controllers -( - id INTEGER - PRIMARY KEY - AUTOINCREMENT, - uid BLOB - NOT NULL - UNIQUE, - name VARCHAR(128), - ip VARCHAR(16), - port INTEGER, - relay_count INTEGER, - active BOOLEAN - NOT NULL -); - -CREATE TABLE relays -( - id INTEGER - PRIMARY KEY - AUTOINCREMENT, - name VARCHAR(128), - number INTEGER - NOT NULL, - controller_id INTEGER - NOT NULL - REFERENCES controllers (id) - ON DELETE CASCADE -); - -CREATE TABLE schedules -( - id INTEGER - PRIMARY KEY - AUTOINCREMENT, - uid BLOB - NOT NULL - UNIQUE, - name VARCHAR(128), - periods BLOB -); - -CREATE TABLE tags -( - id INTEGER - PRIMARY KEY - AUTOINCREMENT, - tag VARCHAR(128) - NOT NULL - UNIQUE -); - -CREATE TABLE junction_tag -( - tag_id INTEGER - NOT NULL - REFERENCES tags (id) - ON DELETE CASCADE, - relay_id INTEGER - REFERENCES relays (id) - ON DELETE CASCADE, - schedule_id INTEGER - REFERENCES schedules (id) - ON DELETE CASCADE -); - -CREATE TABLE junction_relay_schedule -( - weekday SMALLINT - NOT NULL, - relay_id INTEGER - REFERENCES relays (id) - ON DELETE CASCADE, - schedule_id INTEGER - DEFAULT 1 - REFERENCES schedules (id) - ON DELETE SET DEFAULT -); - -INSERT INTO schedules (uid, name, periods) VALUES (x'6f666600000000000000000000000000', 'off', x'00'); -INSERT INTO schedules (uid, name, periods) VALUES (x'6f6e0000000000000000000000000000', 'on', x'010000009F05'); diff --git a/sql/migration_1.sql b/sql/migration_1.sql deleted file mode 100644 index 5078763..0000000 --- a/sql/migration_1.sql +++ /dev/null @@ -1,28 +0,0 @@ --- migration to add macros - -CREATE TABLE macros -( - id INTEGER - PRIMARY KEY - AUTOINCREMENT, - uid BLOB - NOT NULL - UNIQUE, - name VARCHAR(128) -); - -CREATE TABLE macro_actions -( - macro_id INTEGER - NOT NULL - REFERENCES macros (id) - ON DELETE CASCADE, - relay_id INTEGER - REFERENCES relays (id) - ON DELETE CASCADE, - schedule_id INTEGER - REFERENCES schedules (id) - ON DELETE CASCADE, - weekday SMALLINT - NOT NULL -); diff --git a/src/db.rs b/src/db.rs index e47c7e8..970a207 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,8 +1,13 @@ use std::env; +use crate::db::errors::DatabaseError; +use crate::db::model_utils::Period; +use crate::db::models::{NewSchedule, Periods}; +use crate::types::EmgauwaUid; use diesel::prelude::*; -use diesel_migrations::embed_migrations; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use dotenv::dotenv; +use log::{info, trace}; pub mod errors; pub mod models; @@ -12,7 +17,7 @@ pub mod tag; mod model_utils; -embed_migrations!("migrations"); +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); fn get_connection() -> SqliteConnection { dotenv().ok(); @@ -23,6 +28,46 @@ fn get_connection() -> SqliteConnection { } pub fn run_migrations() { - let connection = get_connection(); - embedded_migrations::run(&connection).expect("Failed to run migrations."); + info!("Running migrations"); + let mut connection = get_connection(); + connection + .run_pending_migrations(MIGRATIONS) + .expect("Failed to run migrations."); +} + +fn init_schedule(schedule: &NewSchedule) -> Result<(), DatabaseError> { + trace!("Initializing schedule {:?}", schedule.name); + match schedules::get_schedule_by_uid(schedule.uid.clone()) { + Ok(_) => Ok(()), + Err(err) => match err { + DatabaseError::NotFound => { + trace!("Schedule {:?} not found, inserting", schedule.name); + let mut connection = get_connection(); + diesel::insert_into(schema::schedules::table) + .values(schedule) + .execute(&mut connection) + .map(|_| ()) + .map_err(DatabaseError::InsertError) + } + _ => Err(err), + }, + } +} + +pub fn init(db: &str) { + run_migrations(); + + init_schedule(&NewSchedule { + uid: &EmgauwaUid::Off, + name: "Off", + periods: &Periods(vec![]), + }) + .expect("Error initializing schedule Off"); + + init_schedule(&NewSchedule { + uid: &EmgauwaUid::On, + name: "On", + periods: &Periods(vec![Period::new_on()]), + }) + .expect("Error initializing schedule On"); } diff --git a/src/db/errors.rs b/src/db/errors.rs index 6ccb199..af36c6d 100644 --- a/src/db/errors.rs +++ b/src/db/errors.rs @@ -11,6 +11,7 @@ pub enum DatabaseError { NotFound, Protected, UpdateError(diesel::result::Error), + Unknown, } impl DatabaseError { @@ -47,6 +48,7 @@ impl From<&DatabaseError> for String { DatabaseError::DeleteError => String::from("error on deleting from database"), DatabaseError::Protected => String::from("model is protected"), DatabaseError::UpdateError(_) => String::from("error on updating the model"), + DatabaseError::Unknown => String::from("unknown error"), } } } diff --git a/src/db/model_utils.rs b/src/db/model_utils.rs index 4e1313a..0d15068 100644 --- a/src/db/model_utils.rs +++ b/src/db/model_utils.rs @@ -1,16 +1,14 @@ use crate::db::models::Periods; use chrono::{NaiveTime, Timelike}; -use diesel::backend::Backend; use diesel::deserialize::FromSql; use diesel::serialize::{IsNull, Output, ToSql}; use diesel::sql_types::Binary; use diesel::sqlite::Sqlite; use diesel::{deserialize, serialize}; use serde::{Deserialize, Serialize}; -use std::io::Write; #[derive(Debug, Serialize, Deserialize, AsExpression, FromSqlRow, PartialEq, Clone)] -#[sql_type = "Binary"] +#[diesel(sql_type = Binary)] pub struct Period { #[serde(with = "period_format")] pub start: NaiveTime, @@ -41,23 +39,51 @@ mod period_format { } } -impl ToSql for Periods { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - for period in self.0.iter() { - out.write_all(&[ - period.start.hour() as u8, - period.start.minute() as u8, - period.end.hour() as u8, - period.end.minute() as u8, - ])?; +impl Period { + pub fn new(start: NaiveTime, end: NaiveTime) -> Self { + Period { start, end } + } + + pub fn new_on() -> Self { + Period { + start: NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + end: NaiveTime::from_hms_opt(0, 0, 0).unwrap(), } + } +} + +impl ToSql for Periods +where + Vec: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { + let periods_u8: Vec = self + .0 + .iter() + .flat_map(|period| { + let vec = vec![ + period.start.hour() as u8, + period.start.minute() as u8, + period.end.hour() as u8, + period.end.minute() as u8, + ]; + vec + }) + .collect(); + + out.set_value(periods_u8); + Ok(IsNull::No) } } -impl FromSql for Periods { - fn from_sql(bytes: Option<&::RawValue>) -> deserialize::Result { - let blob = bytes.unwrap().read_blob(); +impl FromSql for Periods +where + DB: diesel::backend::Backend, + Vec: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let blob: Vec = Vec::from_sql(bytes).unwrap(); let mut vec = Vec::new(); for i in (3..blob.len()).step_by(4) { @@ -66,8 +92,8 @@ impl FromSql for Periods { let end_val_h: u32 = blob[i - 1] as u32; let end_val_m: u32 = blob[i] as u32; vec.push(Period { - start: NaiveTime::from_hms(start_val_h, start_val_m, 0), - end: NaiveTime::from_hms(end_val_h, end_val_m, 0), + start: NaiveTime::from_hms_opt(start_val_h, start_val_m, 0).unwrap(), + end: NaiveTime::from_hms_opt(end_val_h, end_val_m, 0).unwrap(), }); } Ok(Periods(vec)) diff --git a/src/db/models.rs b/src/db/models.rs index d75fc3d..3c80774 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -23,7 +23,7 @@ pub struct Schedule { } #[derive(Insertable)] -#[table_name = "schedules"] +#[diesel(table_name = crate::db::schema::schedules)] pub struct NewSchedule<'a> { pub uid: &'a EmgauwaUid, pub name: &'a str, @@ -31,26 +31,27 @@ pub struct NewSchedule<'a> { } #[derive(Debug, Serialize, Deserialize, AsExpression, FromSqlRow, PartialEq, Clone)] -#[sql_type = "Binary"] -pub struct Periods(pub(crate) Vec); +#[diesel(sql_type = Binary)] +pub struct Periods(pub Vec); #[derive(Debug, Serialize, Identifiable, Queryable, Clone)] +#[diesel(table_name = crate::db::schema::tags)] pub struct Tag { pub id: i32, pub tag: String, } #[derive(Insertable)] -#[table_name = "tags"] +#[diesel(table_name = crate::db::schema::tags)] pub struct NewTag<'a> { pub tag: &'a str, } #[derive(Queryable, Associations, Identifiable)] -#[belongs_to(Relay)] -#[belongs_to(Schedule)] -#[belongs_to(Tag)] -#[table_name = "junction_tag"] +#[diesel(belongs_to(Relay))] +#[diesel(belongs_to(Schedule))] +#[diesel(belongs_to(Tag))] +#[diesel(table_name = crate::db::schema::junction_tag)] pub struct JunctionTag { pub id: i32, pub tag_id: i32, @@ -59,7 +60,7 @@ pub struct JunctionTag { } #[derive(Insertable)] -#[table_name = "junction_tag"] +#[diesel(table_name = crate::db::schema::junction_tag)] pub struct NewJunctionTag { pub tag_id: i32, pub relay_id: Option, diff --git a/src/db/schedules.rs b/src/db/schedules.rs index 9495391..01f13f5 100644 --- a/src/db/schedules.rs +++ b/src/db/schedules.rs @@ -13,37 +13,37 @@ use crate::db::tag::{create_junction_tag, create_tag}; use crate::db::{get_connection, schema}; pub fn get_schedule_tags(schedule: &Schedule) -> Vec { - let connection = get_connection(); + let mut connection = get_connection(); JunctionTag::belonging_to(schedule) .inner_join(schema::tags::dsl::tags) .select(schema::tags::tag) - .load::(&connection) + .load::(&mut connection) .expect("Error loading tags") } pub fn get_schedules() -> Vec { - let connection = get_connection(); + let mut connection = get_connection(); schedules - .load::(&connection) + .load::(&mut connection) .expect("Error loading schedules") } pub fn get_schedule_by_uid(filter_uid: EmgauwaUid) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let result = schedules .filter(schema::schedules::uid.eq(filter_uid)) - .first::(&connection) + .first::(&mut connection) .or(Err(DatabaseError::NotFound))?; Ok(result) } pub fn get_schedules_by_tag(tag: &Tag) -> Vec { - let connection = get_connection(); + let mut connection = get_connection(); JunctionTag::belonging_to(tag) .inner_join(schedules) .select(schema::schedules::all_columns) - .load::(&connection) + .load::(&mut connection) .expect("Error loading tags") } @@ -54,9 +54,9 @@ pub fn delete_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<(), DatabaseErro EmgauwaUid::Any(_) => Ok(filter_uid), }?; - let connection = get_connection(); + let mut connection = get_connection(); match diesel::delete(schedules.filter(schema::schedules::uid.eq(filter_uid))) - .execute(&connection) + .execute(&mut connection) { Ok(rows) => { if rows != 0 { @@ -70,7 +70,7 @@ pub fn delete_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<(), DatabaseErro } pub fn create_schedule(new_name: &str, new_periods: &Periods) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let new_schedule = NewSchedule { uid: &EmgauwaUid::default(), @@ -80,12 +80,12 @@ pub fn create_schedule(new_name: &str, new_periods: &Periods) -> Result(&connection) + .get_result::(&mut connection) .or(Err(DatabaseError::InsertGetError))?; Ok(result) @@ -96,7 +96,7 @@ pub fn update_schedule( new_name: &str, new_periods: &Periods, ) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let new_periods = match schedule.uid { EmgauwaUid::Off | EmgauwaUid::On => schedule.periods.borrow(), @@ -108,21 +108,21 @@ pub fn update_schedule( schema::schedules::name.eq(new_name), schema::schedules::periods.eq(new_periods), )) - .execute(&connection) + .execute(&mut connection) .map_err(DatabaseError::UpdateError)?; get_schedule_by_uid(schedule.uid.clone()) } pub fn set_schedule_tags(schedule: &Schedule, new_tags: &[String]) -> Result<(), DatabaseError> { - let connection = get_connection(); + let mut connection = get_connection(); diesel::delete(junction_tag.filter(schema::junction_tag::schedule_id.eq(schedule.id))) - .execute(&connection) + .execute(&mut connection) .or(Err(DatabaseError::DeleteError))?; let mut database_tags: Vec = tags .filter(schema::tags::tag.eq_any(new_tags)) - .load::(&connection) + .load::(&mut connection) .expect("Error loading tags"); // create missing tags diff --git a/src/db/tag.rs b/src/db/tag.rs index 4934b92..c31df9a 100644 --- a/src/db/tag.rs +++ b/src/db/tag.rs @@ -8,29 +8,29 @@ use crate::db::schema::tags::dsl::tags; use crate::db::{get_connection, schema}; pub fn create_tag(new_tag: &str) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let new_tag = NewTag { tag: new_tag }; diesel::insert_into(tags) .values(&new_tag) - .execute(&connection) + .execute(&mut connection) .map_err(DatabaseError::InsertError)?; let result = tags .find(sql("last_insert_rowid()")) - .get_result::(&connection) + .get_result::(&mut connection) .or(Err(DatabaseError::InsertGetError))?; Ok(result) } pub fn get_tag(target_tag: &str) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let result = tags .filter(schema::tags::tag.eq(target_tag)) - .first::(&connection) + .first::(&mut connection) .or(Err(DatabaseError::NotFound))?; Ok(result) @@ -41,7 +41,7 @@ pub fn create_junction_tag( target_relay: Option<&Relay>, target_schedule: Option<&Schedule>, ) -> Result { - let connection = get_connection(); + let mut connection = get_connection(); let new_junction_tag = NewJunctionTag { relay_id: target_relay.map(|r| r.id), @@ -51,12 +51,12 @@ pub fn create_junction_tag( diesel::insert_into(junction_tag) .values(&new_junction_tag) - .execute(&connection) + .execute(&mut connection) .map_err(DatabaseError::InsertError)?; let result = junction_tag .find(sql("last_insert_rowid()")) - .get_result::(&connection) + .get_result::(&mut connection) .or(Err(DatabaseError::InsertGetError))?; Ok(result) diff --git a/src/handlers/v1/schedules.rs b/src/handlers/v1/schedules.rs index d71dac7..c6ee513 100644 --- a/src/handlers/v1/schedules.rs +++ b/src/handlers/v1/schedules.rs @@ -28,7 +28,8 @@ pub async fn index() -> impl Responder { } #[get("/api/v1/schedules/tag/{tag}")] -pub async fn tagged(web::Path((tag,)): web::Path<(String,)>) -> impl Responder { +pub async fn tagged(path: web::Path<(String,)>) -> impl Responder { + let (tag,) = path.into_inner(); let tag_db = get_tag(&tag); if tag_db.is_err() { return HttpResponse::from(tag_db.unwrap_err()); @@ -42,7 +43,8 @@ pub async fn tagged(web::Path((tag,)): web::Path<(String,)>) -> impl Responder { } #[get("/api/v1/schedules/{schedule_id}")] -pub async fn show(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Responder { +pub async fn show(path: web::Path<(String,)>) -> impl Responder { + let (schedule_uid,) = path.into_inner(); let emgauwa_uid = EmgauwaUid::try_from(schedule_uid.as_str()).or(Err(HandlerError::BadUid)); match emgauwa_uid { @@ -108,9 +110,10 @@ pub async fn add_list(data: web::Json>) -> impl Responder { #[put("/api/v1/schedules/{schedule_id}")] pub async fn update( - web::Path((schedule_uid,)): web::Path<(String,)>, + path: web::Path<(String,)>, data: web::Json, ) -> impl Responder { + let (schedule_uid,) = path.into_inner(); let emgauwa_uid = EmgauwaUid::try_from(schedule_uid.as_str()).or(Err(HandlerError::BadUid)); if emgauwa_uid.is_err() { return HttpResponse::from(emgauwa_uid.unwrap_err()); @@ -138,7 +141,8 @@ pub async fn update( } #[delete("/api/v1/schedules/{schedule_id}")] -pub async fn delete(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Responder { +pub async fn delete(path: web::Path<(String,)>) -> impl Responder { + let (schedule_uid,) = path.into_inner(); let emgauwa_uid = EmgauwaUid::try_from(schedule_uid.as_str()).or(Err(HandlerError::BadUid)); match emgauwa_uid { diff --git a/src/main.rs b/src/main.rs index 29a54af..0aa72eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,45 @@ #[macro_use] extern crate diesel; -#[macro_use] extern crate diesel_migrations; -extern crate core; extern crate dotenv; -use actix_web::middleware::normalize::TrailingSlash; +use actix_web::middleware::TrailingSlash; use actix_web::{middleware, web, App, HttpServer}; -use env_logger::{Builder, Env}; -use wiringpi::pin::Value::High; +use log::{trace, LevelFilter}; +use simple_logger::SimpleLogger; +use std::fmt::format; +use std::str::FromStr; mod db; mod handlers; mod return_models; +mod settings; mod types; mod utils; #[actix_web::main] async fn main() -> std::io::Result<()> { - db::run_migrations(); + settings::init(); + let settings = settings::get(); - Builder::from_env(Env::default().default_filter_or("info")).init(); + let log_level: LevelFilter = log::LevelFilter::from_str(&settings.logging.level) + .unwrap_or_else(|_| panic!("Error parsing log level.")); + trace!("Log level set to {:?}", log_level); - let pi = wiringpi::setup(); + SimpleLogger::new() + .with_level(log_level) + .init() + .unwrap_or_else(|_| panic!("Error initializing logger.")); - //Use WiringPi pin 0 as output - let pin = pi.output_pin(0); - pin.digital_write(High); + db::init(&settings.database); HttpServer::new(|| { App::new() .wrap( middleware::DefaultHeaders::new() - .header("Access-Control-Allow-Origin", "*") - .header("Access-Control-Allow-Headers", "*") - .header("Access-Control-Allow-Methods", "*"), + .add(("Access-Control-Allow-Origin", "*")) + .add(("Access-Control-Allow-Headers", "*")) + .add(("Access-Control-Allow-Methods", "*")), ) .wrap(middleware::Logger::default()) .wrap(middleware::NormalizePath::new(TrailingSlash::Trim)) @@ -47,7 +52,7 @@ async fn main() -> std::io::Result<()> { .service(handlers::v1::schedules::update) .service(handlers::v1::schedules::delete) }) - .bind("127.0.0.1:5000")? + .bind(format!("{}:{}", settings.host, settings.port))? .run() .await } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..fbb3d6c --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,66 @@ +use config::Config; +use lazy_static::lazy_static; +use serde_derive::Deserialize; +use std::sync::RwLock; + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +#[allow(unused)] +pub struct Logging { + pub level: String, + pub file: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +#[allow(unused)] +pub struct Settings { + pub database: String, + pub port: u16, + pub host: String, + pub logging: Logging, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + database: String::from("sqlite://emgauwa-core.sqlite"), + port: 5000, + host: String::from("127.0.0.1"), + logging: Logging::default(), + } + } +} + +impl Default for Logging { + fn default() -> Self { + Logging { + level: String::from("info"), + file: String::from("stdout"), + } + } +} + +lazy_static! { + static ref SETTINGS: RwLock = RwLock::new(Settings::default()); +} + +pub fn init() { + let settings = Config::builder() + .add_source(config::File::with_name("emgauwa-core")) + .add_source( + config::Environment::with_prefix("EMGAUWA") + .prefix_separator("_") + .separator("__"), + ) + .build() + .unwrap() + .try_deserialize::() + .unwrap_or_else(|_| panic!("Error reading settings.")); + + *SETTINGS.write().unwrap() = settings; +} + +pub fn get() -> Settings { + SETTINGS.read().unwrap().clone() +} diff --git a/src/types.rs b/src/types.rs index 69eca71..9fb50b5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,124 +1,12 @@ -use std::convert::TryFrom; -use std::fmt::{Debug, Formatter}; -use std::io::Write; -use std::str::FromStr; - -use diesel::backend::Backend; -use diesel::deserialize::FromSql; -use diesel::serialize::{IsNull, Output, ToSql}; use diesel::sql_types::Binary; -use diesel::sqlite::Sqlite; -use diesel::{deserialize, serialize}; -use serde::{Serialize, Serializer}; use uuid::Uuid; +pub mod emgauwa_uid; + #[derive(AsExpression, FromSqlRow, PartialEq, Clone)] -#[sql_type = "Binary"] +#[diesel(sql_type = Binary)] pub enum EmgauwaUid { Off, On, Any(Uuid), } - -impl EmgauwaUid { - const OFF_STR: &'static str = "off"; - const ON_STR: &'static str = "on"; - const OFF_U8: u8 = 0; - const ON_U8: u8 = 1; - const OFF_U128: u128 = 0; - const ON_U128: u128 = 1; -} - -impl Default for EmgauwaUid { - fn default() -> Self { - EmgauwaUid::Any(Uuid::new_v4()) - } -} - -impl Debug for EmgauwaUid { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - EmgauwaUid::Off => EmgauwaUid::OFF_STR.fmt(f), - EmgauwaUid::On => EmgauwaUid::ON_STR.fmt(f), - EmgauwaUid::Any(value) => value.fmt(f), - } - } -} - -impl ToSql for EmgauwaUid { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match self { - EmgauwaUid::Off => out.write_all(&[EmgauwaUid::OFF_U8])?, - EmgauwaUid::On => out.write_all(&[EmgauwaUid::ON_U8])?, - EmgauwaUid::Any(value) => out.write_all(value.as_bytes())?, - } - Ok(IsNull::No) - } -} - -impl FromSql for EmgauwaUid { - fn from_sql(bytes: Option<&::RawValue>) -> deserialize::Result { - match bytes { - None => Ok(EmgauwaUid::default()), - Some(value) => match value.read_blob() { - [EmgauwaUid::OFF_U8] => Ok(EmgauwaUid::Off), - [EmgauwaUid::ON_U8] => Ok(EmgauwaUid::On), - value_bytes => Ok(EmgauwaUid::Any(Uuid::from_slice(value_bytes).unwrap())), - }, - } - } -} - -impl Serialize for EmgauwaUid { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - String::from(self).serialize(serializer) - } -} - -impl From for EmgauwaUid { - fn from(uid: Uuid) -> EmgauwaUid { - match uid.as_u128() { - EmgauwaUid::OFF_U128 => EmgauwaUid::Off, - EmgauwaUid::ON_U128 => EmgauwaUid::On, - _ => EmgauwaUid::Any(uid), - } - } -} - -impl TryFrom<&str> for EmgauwaUid { - type Error = uuid::Error; - - fn try_from(value: &str) -> Result { - match value { - EmgauwaUid::OFF_STR => Ok(EmgauwaUid::Off), - EmgauwaUid::ON_STR => Ok(EmgauwaUid::On), - any => match Uuid::from_str(any) { - Ok(uuid) => Ok(EmgauwaUid::Any(uuid)), - Err(err) => Err(err), - }, - } - } -} - -impl From<&EmgauwaUid> for Uuid { - fn from(emgauwa_uid: &EmgauwaUid) -> Uuid { - match emgauwa_uid { - EmgauwaUid::Off => uuid::Uuid::from_u128(EmgauwaUid::OFF_U128), - EmgauwaUid::On => uuid::Uuid::from_u128(EmgauwaUid::ON_U128), - EmgauwaUid::Any(value) => *value, - } - } -} - -impl From<&EmgauwaUid> for String { - fn from(emgauwa_uid: &EmgauwaUid) -> String { - match emgauwa_uid { - EmgauwaUid::Off => String::from(EmgauwaUid::OFF_STR), - EmgauwaUid::On => String::from(EmgauwaUid::ON_STR), - EmgauwaUid::Any(value) => value.to_hyphenated().to_string(), - } - } -} diff --git a/src/types/emgauwa_uid.rs b/src/types/emgauwa_uid.rs new file mode 100644 index 0000000..7183f0d --- /dev/null +++ b/src/types/emgauwa_uid.rs @@ -0,0 +1,122 @@ +use std::convert::TryFrom; +use std::fmt::{Debug, Formatter}; +use std::str::FromStr; + +use crate::types::EmgauwaUid; +use diesel::backend::Backend; +use diesel::deserialize::FromSql; +use diesel::serialize::{IsNull, Output, ToSql}; +use diesel::sql_types::Binary; +use diesel::{deserialize, serialize}; +use serde::{Serialize, Serializer}; +use uuid::Uuid; + +impl EmgauwaUid { + const OFF_STR: &'static str = "off"; + const ON_STR: &'static str = "on"; + const OFF_U8: u8 = 0; + const ON_U8: u8 = 1; + const OFF_U128: u128 = 0; + const ON_U128: u128 = 1; +} + +impl Default for EmgauwaUid { + fn default() -> Self { + EmgauwaUid::Any(Uuid::new_v4()) + } +} + +impl Debug for EmgauwaUid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EmgauwaUid::Off => EmgauwaUid::OFF_STR.fmt(f), + EmgauwaUid::On => EmgauwaUid::ON_STR.fmt(f), + EmgauwaUid::Any(value) => value.fmt(f), + } + } +} + +impl ToSql for EmgauwaUid +where + DB: Backend, + [u8]: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result { + match self { + EmgauwaUid::Off => [EmgauwaUid::OFF_U8].to_sql(out)?, + EmgauwaUid::On => [EmgauwaUid::ON_U8].to_sql(out)?, + EmgauwaUid::Any(value) => value.as_bytes().to_sql(out)?, + }; + Ok(IsNull::No) + } +} + +impl FromSql for EmgauwaUid +where + DB: Backend, + Vec: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let blob: Vec = FromSql::::from_sql(bytes)?; + + match blob.as_slice() { + [EmgauwaUid::OFF_U8] => Ok(EmgauwaUid::Off), + [EmgauwaUid::ON_U8] => Ok(EmgauwaUid::On), + value_bytes => Ok(EmgauwaUid::Any(Uuid::from_slice(value_bytes).unwrap())), + } + } +} + +impl Serialize for EmgauwaUid { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + String::from(self).serialize(serializer) + } +} + +impl From for EmgauwaUid { + fn from(uid: Uuid) -> EmgauwaUid { + match uid.as_u128() { + EmgauwaUid::OFF_U128 => EmgauwaUid::Off, + EmgauwaUid::ON_U128 => EmgauwaUid::On, + _ => EmgauwaUid::Any(uid), + } + } +} + +impl TryFrom<&str> for EmgauwaUid { + type Error = uuid::Error; + + fn try_from(value: &str) -> Result { + match value { + EmgauwaUid::OFF_STR => Ok(EmgauwaUid::Off), + EmgauwaUid::ON_STR => Ok(EmgauwaUid::On), + any => match Uuid::from_str(any) { + Ok(uuid) => Ok(EmgauwaUid::Any(uuid)), + Err(err) => Err(err), + }, + } + } +} + +impl From<&EmgauwaUid> for Uuid { + fn from(emgauwa_uid: &EmgauwaUid) -> Uuid { + match emgauwa_uid { + EmgauwaUid::Off => uuid::Uuid::from_u128(EmgauwaUid::OFF_U128), + EmgauwaUid::On => uuid::Uuid::from_u128(EmgauwaUid::ON_U128), + EmgauwaUid::Any(value) => *value, + } + } +} + +impl From<&EmgauwaUid> for String { + fn from(emgauwa_uid: &EmgauwaUid) -> String { + match emgauwa_uid { + EmgauwaUid::Off => String::from(EmgauwaUid::OFF_STR), + EmgauwaUid::On => String::from(EmgauwaUid::ON_STR), + EmgauwaUid::Any(value) => value.as_hyphenated().to_string(), + } + } +} diff --git a/tests/controller.testing.ini b/tests/controller.testing.ini deleted file mode 100644 index d3577f6..0000000 --- a/tests/controller.testing.ini +++ /dev/null @@ -1,65 +0,0 @@ -[controller] -name = new emgauwa device - -: 4422 for testing; 4421 for dev-env; 4420 for testing-env; 4419 for prod-env -discovery-port = 4422 -: 1886 for testing; 1885 for dev-env; 1884 for testing-env; 1883 for prod-env -mqtt-port = 1886 -mqtt-host = localhost - -relay-count = 10 -database = controller.sqlite -log-level = debug -log-file = stdout - -[relay-0] -driver = piface -pin = 0 -inverted = 0 - -[relay-1] -driver = piface -pin = 1 -inverted = 0 - -[relay-2] -driver = gpio -pin = 5 -inverted = 1 - -[relay-3] -driver = gpio -pin = 4 -inverted = 1 - -[relay-4] -driver = gpio -pin = 3 -inverted = 1 - -[relay-5] -driver = gpio -pin = 2 -inverted = 1 - -[relay-6] -driver = gpio -pin = 1 -inverted = 1 -pulse-duration = 3 - -[relay-7] -driver = gpio -pin = 0 -inverted = 1 -pulse-duration = 3 - -[relay-8] -driver = gpio -pin = 16 -inverted = 1 - -[relay-9] -driver = gpio -pin = 15 -inverted = 1 diff --git a/tests/core.testing.ini b/tests/core.testing.ini deleted file mode 100644 index c3710a1..0000000 --- a/tests/core.testing.ini +++ /dev/null @@ -1,16 +0,0 @@ -[core] -server-port = 5000 -database = core.sqlite -content-dir = /usr/share/webapps/emgauwa -not-found-file = 404.html -not-found-file-mime = text/html -not-found-content = 404 - NOT FOUND -not-found-content-type = text/plain - -: 4422 for testing; 4421 for dev-env; 4420 for testing-env; 4419 for prod-env -discovery-port = 4422 -: 1886 for testing; 1885 for dev-env; 1884 for testing-env; 1883 for prod-env -mqtt-port = 1886 - -log-level = debug -log-file = stdout diff --git a/tests/run_tests.sh b/tests/run_tests.sh deleted file mode 100755 index be0101e..0000000 --- a/tests/run_tests.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env sh - -source_dir=$PWD/tests -working_dir=$source_dir/testing_latest -working_bak=$source_dir/testing_bak - -rm -rf "$working_bak" -[ -d "$working_dir" ] && mv "$working_dir" "$working_bak" - -mkdir -p "$working_dir" - - -cp "${1:-"target/debug/emgauwa-core"}" "$working_dir/core" - -cd "$working_dir" || exit - -#target_branch=$(git rev-parse --abbrev-ref HEAD) - -#if [ -z "$EMGAUWA_CONTROLLER_EXE" ] -#then -# git clone --quiet ssh://git@git.serguzim.me:3022/emgauwa/controller.git controller || exit -# cd ./controller || exit -# -# git checkout dev >/dev/null 2>&1 -# git checkout "$target_branch" >/dev/null 2>&1 -# git checkout "$2" >/dev/null 2>&1 -# -# echo "Building controller on branch $(git rev-parse --abbrev-ref HEAD)" -# mkdir build -# cd build || exit -# -# cmake -DWIRING_PI_DEBUG=on .. >/dev/null -# make >/dev/null -# EMGAUWA_CONTROLLER_EXE=./controller -#fi - -#echo "Emgauwa controller: $($EMGAUWA_CONTROLLER_EXE --version)" - -#$EMGAUWA_CONTROLLER_EXE start -c "$source_dir/controller.testing.ini" >"$working_dir/controller.log" 2>&1 & -#controller_id=$! - -cd "$working_dir" || exit - -EMGAUWA_CORE_EXE="$working_dir/core" -cp "$source_dir/core.testing.ini" "$working_dir/core.ini" - -$EMGAUWA_CORE_EXE start >>"$working_dir/core.log" 2>&1 & -core_id=$! - - -# wait for start -if [ -x "$(command -v wait-for-it)" ] -then - wait-for-it localhost:5000 -t 15 -else - echo "waiting 5 seconds for server" - sleep 5; -fi - -export PYTHONPATH=$PYTHONPATH:$source_dir/tavern_utils -tavern-ci --disable-warnings "$source_dir/tavern_tests" -test_result=$? - -#kill $controller_id -kill $core_id - -exit $test_result diff --git a/tests/tavern_tests/0.0.get_all.tavern.yaml b/tests/tavern_tests/0.0.get_all.tavern.yaml deleted file mode 100644 index 9d28bf0..0000000 --- a/tests/tavern_tests/0.0.get_all.tavern.yaml +++ /dev/null @@ -1,23 +0,0 @@ -test_name: "[get_all] Test basic get all requests" - -stages: -- name: "[get_all] get all schedules" - request: - url: "http://localhost:5000/api/v1/schedules/" - method: GET - response: - status_code: 200 - -- name: "[get_all] get all relays" - request: - url: "http://localhost:5000/api/v1/relays/" - method: GET - response: - status_code: 200 - -- name: "[get_all] get all controllers" - request: - url: "http://localhost:5000/api/v1/controllers/" - method: GET - response: - status_code: 200 diff --git a/tests/tavern_tests/1.0.controllers_basic.tavern.yaml b/tests/tavern_tests/1.0.controllers_basic.tavern.yaml deleted file mode 100644 index a48ea17..0000000 --- a/tests/tavern_tests/1.0.controllers_basic.tavern.yaml +++ /dev/null @@ -1,116 +0,0 @@ -test_name: Test basic controller functions - -stages: -- name: "[controllers_basic] discover controllers" - request: - method: POST - url: "http://localhost:5000/api/v1/controllers/discover/" - response: - status_code: 200 - verify_response_with: - function: validate_controller:multiple - save: - json: - returned_name: "[0].name" - returned_id: "[0].id" - returned_ip: "[0].ip" - -- name: "[controllers_basic] get controller, check name" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - response: - status_code: 200 - verify_response_with: - function: validate_controller:single - function: validate_controller:check_id - extra_kwargs: - id: "{returned_id}" - function: validate_controller:check_name - extra_kwargs: - name: "{returned_name}" - -- name: "[controllers_basic] put controller, check name" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - json: - name: "renamed_controller" - response: - status_code: 200 - verify_response_with: - function: validate_controller:single - function: validate_controller:check_id - extra_kwargs: - id: "{returned_id}" - function: validate_controller:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" - save: - json: - changed_name: "name" - -#- name: "[controllers_basic] put controller, check name and ip" -# request: -# method: PUT -# url: "http://localhost:5000/api/v1/controllers/{returned_id}" -# json: -# ip: "203.0.113.17" -# response: -# status_code: 200 -# verify_response_with: -# function: validate_controller:single -# function: validate_controller:check_id -# extra_kwargs: -# id: "{returned_id}" -# function: validate_controller:check_ip -# extra_kwargs: -# ip: "{tavern.request_vars.json.ip}" -# save: -# json: -# changed_ip: "ip" - -- name: "[controllers_basic] delete controller" - request: - method: DELETE - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - response: - status_code: 200 - -- name: "[controllers_basic] get controller, expect 404" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - response: - status_code: 404 - -- name: "[controllers_basic] discover controllers again" - request: - method: POST - url: "http://localhost:5000/api/v1/controllers/discover/" - response: - status_code: 200 - verify_response_with: - function: validate_controller:multiple - function: validate_controller:find - extra_kwargs: - id: "{returned_id}" - name: "{changed_name}" - -- name: "[controllers_basic] get controller again, check name" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - response: - status_code: 200 - verify_response_with: - function: validate_controller:single - function: validate_controller:check_id - extra_kwargs: - id: "{returned_id}" - function: validate_controller:check_name - extra_kwargs: - name: "{changed_name}" - function: validate_controller:check_ip - extra_kwargs: - ip: "{returned_ip}" diff --git a/tests/tavern_tests/1.1.controller_relays_basic.tavern.yaml b/tests/tavern_tests/1.1.controller_relays_basic.tavern.yaml deleted file mode 100644 index f0d1750..0000000 --- a/tests/tavern_tests/1.1.controller_relays_basic.tavern.yaml +++ /dev/null @@ -1,42 +0,0 @@ -test_name: Test basic controller relays functions - -stages: -- name: "[controller_relays_basic] get controllers" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/" - response: - status_code: 200 - verify_response_with: - function: validate_controller:multiple - save: - json: - returned_id: "[0].id" - returned_relay_count: "[0].relay_count" - -- name: "[controller_relays_basic] get controller relays, check length" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays" - response: - status_code: 200 - verify_response_with: - function: validate_relay:multiple - function: validate_relay:relay_count - extra_kwargs: - relay_count: !int "{returned_relay_count:d}" - -- name: "[controller_relays_basic] get controller relays, check length" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays/5" - response: - status_code: 200 - verify_response_with: - function: validate_relay:single - function: validate_relay:check_controller_id - extra_kwargs: - name: "{returned_id}" - function: validate_relay:check_number - extra_kwargs: - number: 5 diff --git a/tests/tavern_tests/1.2.controllers_bad.tavern.yaml b/tests/tavern_tests/1.2.controllers_bad.tavern.yaml deleted file mode 100644 index cf7e663..0000000 --- a/tests/tavern_tests/1.2.controllers_bad.tavern.yaml +++ /dev/null @@ -1,99 +0,0 @@ -test_name: Test bad controller functions - -stages: -- name: "[controllers_bad] get controller with bad id" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/this_id_is_invalid" - response: - status_code: 400 - -- name: "[controllers_bad] put controller with bad id" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/this_id_is_invalid" - json: - name: "unknown_controller" - response: - status_code: 400 - -- name: "[controllers_bad] delete controller with bad id" - request: - method: DELETE - url: "http://localhost:5000/api/v1/controllers/this_id_is_invalid" - json: - name: "unknown_controller" - response: - status_code: 400 - -- name: "[controllers_bad] get controller with unknown id" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/00000000-0000-0000-0000-000000000000" - response: - status_code: 404 - -- name: "[controllers_bad] put controller with unknown id" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/00000000-0000-0000-0000-000000000000" - json: - name: "unknown_controller" - response: - status_code: 404 - -- name: "[controllers_bad] delete controller with unknown id" - request: - method: DELETE - url: "http://localhost:5000/api/v1/controllers/00000000-0000-0000-0000-000000000000" - json: - name: "unknown_controller" - response: - status_code: 404 - -- name: "[controllers_bad] get controllers to save valid id" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/" - response: - status_code: 200 - verify_response_with: - function: validate_controller:multiple - save: - json: - returned_id: "[0].id" - -- name: "[controllers_bad] put controller with bad body (invalid name)" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - json: - name: NULL - response: - status_code: 400 - -- name: "[controllers_bad] put controller with bad body (invalid ip)" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - json: - ip: 123 - response: - status_code: 400 - -- name: "[controllers_bad] put controller with bad body (invalid IPv4)" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - json: - ip: "10.0.0.300" - response: - status_code: 400 - -- name: "[controllers_bad] put controller with bad body (no json)" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}" - data: "not jsonbut html" - response: - status_code: 400 diff --git a/tests/tavern_tests/2.0.schedules_basic.tavern.yaml b/tests/tavern_tests/2.0.schedules_basic.tavern.yaml deleted file mode 100644 index 121013b..0000000 --- a/tests/tavern_tests/2.0.schedules_basic.tavern.yaml +++ /dev/null @@ -1,89 +0,0 @@ -test_name: Test basic schedule requests - -stages: -- name: "[schedules_basic] Make sure we get any response" - request: - url: "http://localhost:5000/api/v1/schedules/" - method: GET - response: - status_code: 200 - verify_response_with: - function: validate_schedule:multiple - -- name: "[schedules_basic] post schedule with no periods, expect it to be echoed back" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "same as off" - periods: [] - tags: [] - response: - status_code: 201 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" - function: validate_schedule:check_periods - extra_kwargs: - periods: "{tavern.request_vars.json.periods}" - -- name: "[schedules_basic] post schedule, expect it to be echoed back" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "hello" - periods: - - start: "00:10" - end: "00:20" - - start: "00:30" - end: "00:40" - - start: "00:50" - end: "01:00" - tags: [] - response: - status_code: 201 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" - function: validate_schedule:check_periods - extra_kwargs: - periods: "{tavern.request_vars.json.periods}" - save: - json: - returned_name: "name" - returned_id: "id" - returned_periods: "periods" - -- name: "[schedules_basic] get schedule, check name and some periods" - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/{returned_id}" - response: - status_code: 200 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:check_name - extra_kwargs: - name: "{returned_name}" - function: validate_schedule:check_periods - extra_kwargs: - periods: "{returned_periods}" - -- name: "[schedules_basic] delete schedule" - request: - method: DELETE - url: "http://localhost:5000/api/v1/schedules/{returned_id}" - response: - status_code: 200 - -- name: "[schedules_basic] get deleted schedule, expect 404" - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/{returned_id}" - response: - status_code: 404 diff --git a/tests/tavern_tests/2.1.schedules_protected.tavern.yaml b/tests/tavern_tests/2.1.schedules_protected.tavern.yaml deleted file mode 100644 index c29c0a6..0000000 --- a/tests/tavern_tests/2.1.schedules_protected.tavern.yaml +++ /dev/null @@ -1,74 +0,0 @@ -test_name: Test protected schedules requests - -stages: -- name: "[schedules_protected] delete protected off schedule; expect forbidden/fail" - request: - method: DELETE - url: "http://localhost:5000/api/v1/schedules/off" - response: - status_code: 403 - -- name: "[schedules_protected] get protected off schedule" - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/off" - response: - status_code: 200 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:compare_off - -- name: "[schedules_protected] overwrite protected off schedule" - request: - method: PUT - url: "http://localhost:5000/api/v1/schedules/off" - json: - name: "turned_off" - periods: - - start: "00:10" - end: "00:20" - tags: [] - response: - status_code: 200 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:compare_off - function: validate_schedule:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" - -- name: "[schedules_protected] delete protected on schedule; expect forbidden/fail" - request: - method: DELETE - url: "http://localhost:5000/api/v1/schedules/on" - response: - status_code: 403 - -- name: get protected on schedule - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/on" - response: - status_code: 200 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:compare_on - -- name: "[schedules_protected] overwrite protected on schedule" - request: - method: PUT - url: "http://localhost:5000/api/v1/schedules/on" - json: - name: "turned_on" - periods: - - start: "16:10" - end: "17:20" - tags: [] - response: - status_code: 200 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:compare_on - function: validate_schedule:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" diff --git a/tests/tavern_tests/2.2.schedules_bad.tavern.yaml b/tests/tavern_tests/2.2.schedules_bad.tavern.yaml deleted file mode 100644 index 2e34410..0000000 --- a/tests/tavern_tests/2.2.schedules_bad.tavern.yaml +++ /dev/null @@ -1,169 +0,0 @@ -test_name: Test bad schedule requests - -stages: -- name: "[schedules_bad] get schedule with bad id" - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/this_id_is_invalid" - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad body (no json)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - data: "not jsonbut html" - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad body (no name)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - periods: - - start: "00:10" - end: "00:20" - - start: "00:30" - end: "00:40" - - start: "00:50" - end: "01:00" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad body (name as number)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: 42 - periods: - - start: "00:10" - end: "00:20" - - start: "00:30" - end: "00:40" - - start: "00:50" - end: "01:00" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad period (no start)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am invalid" - periods: - - end: "00:20" - - start: "00:30" - end: "00:40" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad period (no end)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am invalid" - periods: - - start: "00:20" - - start: "00:30" - end: "00:40" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad period (invalid start)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am invalid" - periods: - - start: "hello" - end: "00:20" - - start: "00:30" - end: "00:40" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad period (invalid end)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am invalid" - periods: - - start: "12:10" - end: 1215 - - start: "00:30" - end: "00:40" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad period (invalid end 2)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am invalid" - periods: - - start: "12:10" - end: "25:90" - - start: "00:30" - end: "00:40" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad periods (invalid list)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "i am nvalid" - periods: "not a list" - tags: [] - response: - status_code: 400 - -- name: "[schedules_bad] post schedule with bad tags (one invalid)" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "hello" - periods: - - start: "00:10" - end: "00:20" - - start: "00:30" - end: "00:40" - - start: "00:50" - end: "01:00" - tags: - - "valid_tag" - - 123 - response: - status_code: 400 - -- name: "[schedules_bad] post schedule without tags" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "hello" - periods: - - start: "00:10" - end: "00:20" - - start: "00:30" - end: "00:40" - - start: "00:50" - end: "01:00" - response: - status_code: 400 diff --git a/tests/tavern_tests/3.0.tags.tavern.yaml b/tests/tavern_tests/3.0.tags.tavern.yaml deleted file mode 100644 index d4ef67f..0000000 --- a/tests/tavern_tests/3.0.tags.tavern.yaml +++ /dev/null @@ -1,108 +0,0 @@ -test_name: "[tags] Test tagging of schedules and relays" - -stages: -- name: "[tags] post schedule, expect it to be echoed back by tag" - request: - method: POST - url: "http://localhost:5000/api/v1/schedules/" - json: - name: "test tagging schedule" - periods: - - start: "00:50" - end: "01:00" - tags: - - "test_tag_1" - response: - status_code: 201 - verify_response_with: - function: validate_schedule:single - function: validate_schedule:check_name - extra_kwargs: - name: "{tavern.request_vars.json.name}" - function: validate_schedule:check_periods - extra_kwargs: - periods: "{tavern.request_vars.json.periods}" - function: validate_schedule:check_tag - extra_kwargs: - tag: "{tavern.request_vars.json.tags[0]}" - save: - json: - returned_name: "name" - returned_id: "id" - returned_periods: "periods" - -- name: "[tags] get schedule, check name and some periods" - request: - method: GET - url: "http://localhost:5000/api/v1/schedules/tag/test_tag_1" - response: - status_code: 200 - verify_response_with: - function: validate_schedule:multiple - function: validate_schedule:find - extra_kwargs: - id: "{returned_id}" - name: "{returned_name}" - periods: "{returned_periods}" - -- name: "[tags] get controllers" - request: - method: GET - url: "http://localhost:5000/api/v1/controllers/" - response: - status_code: 200 - verify_response_with: - function: validate_controller:multiple - save: - json: - returned_id: "[0].id" - -- name: "[tags] set relay tag" - request: - method: PUT - url: "http://localhost:5000/api/v1/controllers/{returned_id}/relays/3" - json: - tags: - - "test_tag_1" - response: - status_code: 200 - verify_response_with: - function: validate_relay:single - function: validate_relay:check_controller_id - extra_kwargs: - name: "{returned_id}" - function: validate_relay:check_number - extra_kwargs: - number: 3 - function: validate_relay:check_tag - extra_kwargs: - tag: "{tavern.request_vars.json.tags[0]}" - save: - json: - returned_name: "name" - returned_number: "number" - returned_tag: "tags[0]" - -- name: "[tags] get relay, check name and number" - request: - method: GET - url: "http://localhost:5000/api/v1/relays/tag/{returned_tag}" - response: - status_code: 200 - verify_response_with: - function: validate_relay:multiple - function: validate_relay:find - extra_kwargs: - name: "{returned_name}" - number: !int "{returned_number:d}" - controller_id: "{returned_id}" - tag: "{returned_tag}" - -- name: "[tags] get tags" - request: - method: GET - url: "http://localhost:5000/api/v1/tags/" - response: - status_code: 200 - verify_response_with: - function: validate_tag:multiple diff --git a/tests/tavern_utils/validate_controller.py b/tests/tavern_utils/validate_controller.py deleted file mode 100644 index db0fb25..0000000 --- a/tests/tavern_utils/validate_controller.py +++ /dev/null @@ -1,45 +0,0 @@ -import json -import validate_relay - -def _verify_single(controller): - assert isinstance(controller.get("id"), str), "controller id is not a string" - assert isinstance(controller.get("name"), str), "controller name is not a string" - assert isinstance(controller.get("relay_count"), int), "controller relay_count is not an integer" - - assert isinstance(controller.get("relays"), list), "controller relays is not a list" - assert len(controller.get("relays")) == controller.get("relay_count"), "controller relay have a length unequal to relay_count" - for relay in controller.get("relays"): - assert isinstance(relay, dict), "controller relays contain a relay which is not a dict" - validate_relay._verify_single(relay) - assert relay.get("controller_id") == controller.get("id") - -def single(response): - _verify_single(response.json()) - -def multiple(response): - assert isinstance(response.json(), list), "response is not a list" - for controller in response.json(): - _verify_single(controller) - -def check_id(response, id): - assert response.json().get("id") == id, "controller id check failed" - -def check_name(response, name): - assert response.json().get("name") == name, "controller name check failed" - -def check_ip(response, ip): - assert response.json().get("ip") == ip, "controller ip check failed" - -def find(response, id=None, name=None): - print(response.json()) - for controller in response.json(): - if id != None and id != controller.get("id"): - print(controller.get("id")) - continue - - if name != None and name != controller.get("name"): - print(controller.get("name")) - continue - return - assert False, "controller not found in list" - diff --git a/tests/tavern_utils/validate_relay.py b/tests/tavern_utils/validate_relay.py deleted file mode 100644 index 39f3811..0000000 --- a/tests/tavern_utils/validate_relay.py +++ /dev/null @@ -1,68 +0,0 @@ -import json -import validate_schedule - -def _verify_single(relay): - assert isinstance(relay.get("number"), int), "relay number is not an integer" - assert isinstance(relay.get("name"), str), "relay name is not a string" - assert isinstance(relay.get("controller_id"), str), "relay controller_id is not a string" - - assert isinstance(relay.get("active_schedule"), dict), "relay active_schedule is not a dict" - validate_schedule._verify_single(relay.get("active_schedule")) - - assert isinstance(relay.get("schedules"), list), "relay schedules is not a list" - assert len(relay.get("schedules")) == 7, "relay schedule have a length unequal to 7" - for schedule in relay.get("schedules"): - assert isinstance(relay, dict), "relay schedules contain a schedule which is not a dict" - validate_schedule._verify_single(schedule) - - assert isinstance(relay.get("tags"), list), "relay tags is not a list" - for tag in relay.get("tags"): - assert isinstance(tag, str), "relay tags contain a tag which is not a string" - -def single(response): - _verify_single(response.json()) - -def multiple(response): - assert isinstance(response.json(), list), "response is not a list" - for relay in response.json(): - _verify_single(relay) - -def relay_count(response, relay_count): - assert len(response.json()) == relay_count, "response has invalid length" - -def check_number(response, number): - assert response.json().get("number") == number, "relay number check failed" - -def check_name(response, name): - assert response.json().get("name") == name, "relay name check failed" - -def check_controller_id(response, controller_id): - assert response.json().get("controller_id") == controller_id, "relay controller_id check failed" - -def check_tag(response, tag): - for response_tag in response.json().get("tags"): - if response_tag == tag: - return - assert False, "tag not found in relay," - -def find(response, name=None, number=None, controller_id=None, tag=None): - print(response.json()) - for relay in response.json(): - if number != None and number != relay.get("number"): - continue - - if name != None and name != relay.get("name"): - continue - - if controller_id != None and controller_id != relay.get("controller_id"): - continue - - if tag != None: - found_in_response = False - for response_tag in relay.get("tags"): - if response_tag == tag: - found_in_response = True - if not found_in_response: - continue - return - assert False, "relay not found in list" diff --git a/tests/tavern_utils/validate_schedule.py b/tests/tavern_utils/validate_schedule.py deleted file mode 100644 index eb92b9e..0000000 --- a/tests/tavern_utils/validate_schedule.py +++ /dev/null @@ -1,97 +0,0 @@ -import json - -def _verify_single(schedule): - assert isinstance(schedule.get("id"), str), "schedule ID is not a string" - assert isinstance(schedule.get("name"), str), "schedule name is not a string" - - assert isinstance(schedule.get("periods"), list), "schedule periods is not a list" - for period in schedule.get("periods"): - assert isinstance(period, dict), "schedule periods contain a periods which is not a dict" - assert isinstance(period.get("start"), str), "schedule periods contain a periods with start not being a string" - assert isinstance(period.get("end"), str), "schedule periods contain a periods with end not being a string" - - assert isinstance(schedule.get("tags"), list), "schedule tags is not a list" - for tag in schedule.get("tags"): - assert isinstance(tag, str), "schedule tags contain a tag which is not a string" - -def single(response): - _verify_single(response.json()) - -def multiple(response): - assert isinstance(response.json(), list), "response is not a list" - for schedule in response.json(): - _verify_single(schedule) - -def check_name(response, name): - response_name = response.json().get("name") - assert response_name == name, f"schedule name check failed (expected: '{name}'; actual: '{response_name}')" - -def check_id(response, id): - assert response.json().get("id") == id, "schedule id check failed" - -def check_periods(response, periods): - periods_json = json.loads(periods.replace("'", "\"")) - assert len(periods_json) == len(response.json().get("periods")), "periods in response and request have different lengths" - for request_period in periods_json: - found_in_response = False - for response_period in response.json().get("periods"): - if response_period.get("start") != request_period.get("start"): - continue - if response_period.get("end") != request_period.get("end"): - continue - found_in_response = True - if not found_in_response: - print(request_period) - assert False, "a period from the request was missing from the response" - -def check_tag(response, tag): - for response_tag in response.json().get("tags"): - if response_tag == tag: - return - assert False, "tag not found in schedule," - -def compare_off(response): - assert response.json().get("id") == "off", "schedule off did not return id off" - assert len(response.json().get("periods")) == 0, "schedule off has periods" - -def compare_on(response): - assert response.json().get("id") == "on", "schedule on did not return id on" - assert len(response.json().get("periods")) == 1, "schedule on has unexpected amount of periods" - assert response.json().get("periods")[0].get("start") == "00:00", "Schedule on has unexpected start" - assert response.json().get("periods")[0].get("end") == "00:00", "Schedule on has unexpected start" - -def find(response, id=None, name=None, periods=None, tag=None): - if periods != None: - periods_json = json.loads(periods.replace("'", "\"")) - for schedule in response.json(): - if id != None and id != schedule.get("id"): - print(schedule.get("id")) - continue - - if name != None and name != schedule.get("name"): - print(schedule.get("name")) - continue - - if periods != None: - if len(periods_json) != len(schedule.get("periods")): - continue - for request_period in periods_json: - found_in_response = False - for response_period in schedule.get("periods"): - if response_period.get("start") != request_period.get("start"): - continue - if response_period.get("end") != request_period.get("end"): - continue - found_in_response = True - if not found_in_response: - continue - - if tag != None: - found_in_response = False - for response_tag in schedule.get("tags"): - if response_tag == tag: - found_in_response = True - if not found_in_response: - continue - return - assert False, "schedule not found in list" diff --git a/tests/tavern_utils/validate_tag.py b/tests/tavern_utils/validate_tag.py deleted file mode 100644 index 1e907e5..0000000 --- a/tests/tavern_utils/validate_tag.py +++ /dev/null @@ -1,34 +0,0 @@ -import json - -def _verify_single(tag): - assert isinstance(tag, str), "tag is not a string" - -def single(response): - _verify_single(response.json()) - -def multiple(response): - assert isinstance(response.json(), list), "response is not a list" - for tag in response.json(): - _verify_single(tag) - -#def find(response, name=None, number=None, controller_id=None, tag=None): -# print(response.json()) -# for tag in response.json(): -# if number != None and number != tag.get("number"): -# continue -# -# if name != None and name != tag.get("name"): -# continue -# -# if controller_id != None and controller_id != tag.get("controller_id"): -# continue -# -# if tag != None: -# found_in_response = False -# for response_tag in tag.get("tags"): -# if response_tag == tag: -# found_in_response = True -# if not found_in_response: -# continue -# return -# assert False, "tag not found in list"