PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB`Mini Shell

HOME


Mini Shell 1.0
DIR:/lib/python2.7/site-packages/passlib/tests/
Upload File :
Current File : //lib/python2.7/site-packages/passlib/tests/test_totp.py
"""passlib.tests -- test passlib.totp"""
#=============================================================================
# imports
#=============================================================================
# core
import datetime
from functools import partial
import logging; log = logging.getLogger(__name__)
import sys
import time as _time
# site
# pkg
from passlib import exc
from passlib.utils.compat import unicode, u
from passlib.tests.utils import TestCase, time_call
# subject
from passlib import totp as totp_module
from passlib.totp import TOTP, AppWallet, AES_SUPPORT
# local
__all__ = [
    "EngineTest",
]

#=============================================================================
# helpers
#=============================================================================

# XXX: python 3 changed what error base64.b16decode() throws, from TypeError to base64.Error().
#      it wasn't until 3.3 that base32decode() also got changed.
#      really should normalize this in the code to a single BinaryDecodeError,
#      predicting this cross-version is getting unmanagable.
Base32DecodeError = Base16DecodeError = TypeError
if sys.version_info >= (3,0):
    from binascii import Error as Base16DecodeError
if sys.version_info >= (3,3):
    from binascii import Error as Base32DecodeError

PASS1 = "abcdef"
PASS2 = b"\x00\xFF"
KEY1 = '4AOGGDBBQSYHNTUZ'
KEY1_RAW = b'\xe0\x1cc\x0c!\x84\xb0v\xce\x99'
KEY2_RAW = b'\xee]\xcb9\x870\x06 D\xc8y/\xa54&\xe4\x9c\x13\xc2\x18'
KEY3 = 'S3JDVB7QD2R7JPXX' # used in docstrings
KEY4 = 'JBSWY3DPEHPK3PXP' # from google keyuri spec
KEY4_RAW = b'Hello!\xde\xad\xbe\xef'

# NOTE: for randtime() below,
#       * want at least 7 bits on fractional side, to test fractional times to at least 0.01s precision
#       * want at least 32 bits on integer side, to test for 32-bit epoch issues.
#       most systems *should* have 53 bit mantissa, leaving plenty of room on both ends,
#       so using (1<<37) as scale, to allocate 16 bits on fractional side, but generate reasonable # of > 1<<32 times.
#       sanity check that we're above 44 ensures minimum requirements (44 - 37 int = 7 frac)
assert sys.float_info.radix == 2, "unexpected float_info.radix"
assert sys.float_info.mant_dig >= 44, "double precision unexpectedly small"

def _get_max_time_t():
    """
    helper to calc max_time_t constant (see below)
    """
    value = 1 << 30  # even for 32 bit systems will handle this
    year = 0
    while True:
        next_value = value << 1
        try:
            next_year = datetime.datetime.utcfromtimestamp(next_value-1).year
        except (ValueError, OSError, OverflowError):
            # utcfromtimestamp() may throw any of the following:
            #
            # * year out of range for datetime:
            #   py < 3.6 throws ValueError.
            #   (py 3.6.0 returns odd value instead, see workaround below)
            #
            # * int out of range for host's gmtime/localtime:
            #   py2 throws ValueError, py3 throws OSError.
            #
            # * int out of range for host's time_t:
            #   py2 throws ValueError, py3 throws OverflowError.
            #
            break

        # Workaround for python 3.6.0 issue --
        # Instead of throwing ValueError if year out of range for datetime,
        # Python 3.6 will do some weird behavior that masks high bits
        # e.g. (1<<40) -> year 36812, but (1<<41) -> year 6118.
        # (Appears to be bug http://bugs.python.org/issue29100)
        # This check stops at largest non-wrapping bit size.
        if next_year < year:
            break

        value = next_value

    # 'value-1' is maximum.
    value -= 1

    # check for crazy case where we're beyond what datetime supports
    # (caused by bug 29100 again). compare to max value that datetime
    # module supports -- datetime.datetime(9999, 12, 31, 23, 59, 59, 999999)
    max_datetime_timestamp = 253402318800
    return min(value, max_datetime_timestamp)

#: Rough approximation of max value acceptable by hosts's time_t.
#: This is frequently ~2**37 on 64 bit, and ~2**31 on 32 bit systems.
max_time_t = _get_max_time_t()

def to_b32_size(raw_size):
    return (raw_size * 8 + 4) // 5

#=============================================================================
# wallet
#=============================================================================
class AppWalletTest(TestCase):
    descriptionPrefix = "passlib.totp.AppWallet"

    #=============================================================================
    # constructor
    #=============================================================================

    def test_secrets_types(self):
        """constructor -- 'secrets' param -- input types"""

        # no secrets
        wallet = AppWallet()
        self.assertEqual(wallet._secrets, {})
        self.assertFalse(wallet.has_secrets)

        # dict
        ref = {"1": b"aaa", "2": b"bbb"}
        wallet = AppWallet(ref)
        self.assertEqual(wallet._secrets, ref)
        self.assertTrue(wallet.has_secrets)

        # # list
        # wallet = AppWallet(list(ref.items()))
        # self.assertEqual(wallet._secrets, ref)

        # # iter
        # wallet = AppWallet(iter(ref.items()))
        # self.assertEqual(wallet._secrets, ref)

        # "tag:value" string
        wallet = AppWallet("\n 1: aaa\n# comment\n \n2: bbb   ")
        self.assertEqual(wallet._secrets, ref)

        # ensure ":" allowed in secret
        wallet = AppWallet("1: aaa: bbb \n# comment\n \n2: bbb   ")
        self.assertEqual(wallet._secrets, {"1": b"aaa: bbb", "2": b"bbb"})

        # json dict
        wallet = AppWallet('{"1":"aaa","2":"bbb"}')
        self.assertEqual(wallet._secrets, ref)

        # # json list
        # wallet = AppWallet('[["1","aaa"],["2","bbb"]]')
        # self.assertEqual(wallet._secrets, ref)

        # invalid type
        self.assertRaises(TypeError, AppWallet, 123)

        # invalid json obj
        self.assertRaises(TypeError, AppWallet, "[123]")

        # # invalid list items
        # self.assertRaises(ValueError, AppWallet, ["1", b"aaa"])

        # forbid empty secret
        self.assertRaises(ValueError, AppWallet, {"1": "aaa", "2": ""})

    def test_secrets_tags(self):
        """constructor -- 'secrets' param -- tag/value normalization"""

        # test reference
        ref = {"1": b"aaa", "02": b"bbb", "C": b"ccc"}
        wallet = AppWallet(ref)
        self.assertEqual(wallet._secrets, ref)

        # accept unicode
        wallet = AppWallet({u("1"): b"aaa", u("02"): b"bbb", u("C"): b"ccc"})
        self.assertEqual(wallet._secrets, ref)

        # normalize int tags
        wallet = AppWallet({1: b"aaa", "02": b"bbb", "C": b"ccc"})
        self.assertEqual(wallet._secrets, ref)

        # forbid non-str/int tags
        self.assertRaises(TypeError, AppWallet, {(1,): "aaa"})

        # accept valid tags
        wallet = AppWallet({"1-2_3.4": b"aaa"})

        # forbid invalid tags
        self.assertRaises(ValueError, AppWallet, {"-abc": "aaa"})
        self.assertRaises(ValueError, AppWallet, {"ab*$": "aaa"})

        # coerce value to bytes
        wallet = AppWallet({"1": u("aaa"), "02": "bbb", "C": b"ccc"})
        self.assertEqual(wallet._secrets, ref)

        # forbid invalid value types
        self.assertRaises(TypeError, AppWallet, {"1": 123})
        self.assertRaises(TypeError, AppWallet, {"1": None})
        self.assertRaises(TypeError, AppWallet, {"1": []})

    # TODO: test secrets_path

    def test_default_tag(self):
        """constructor -- 'default_tag' param"""

        # should sort numerically
        wallet = AppWallet({"1": "one", "02": "two"})
        self.assertEqual(wallet.default_tag, "02")
        self.assertEqual(wallet.get_secret(wallet.default_tag), b"two")

        # should sort alphabetically if non-digit present
        wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"})
        self.assertEqual(wallet.default_tag, "A")
        self.assertEqual(wallet.get_secret(wallet.default_tag), b"aaa")

        # should use honor custom tag
        wallet = AppWallet({"1": "one", "02": "two", "A": "aaa"}, default_tag="1")
        self.assertEqual(wallet.default_tag, "1")
        self.assertEqual(wallet.get_secret(wallet.default_tag), b"one")

        # throw error on unknown value
        self.assertRaises(KeyError, AppWallet, {"1": "one", "02": "two", "A": "aaa"},
                          default_tag="B")

        # should be empty
        wallet = AppWallet()
        self.assertEqual(wallet.default_tag, None)
        self.assertRaises(KeyError, wallet.get_secret, None)

    # TODO: test 'cost' param

    #=============================================================================
    # encrypt_key() & decrypt_key() helpers
    #=============================================================================
    def require_aes_support(self, canary=None):
        if AES_SUPPORT:
            canary and canary()
        else:
            canary and self.assertRaises(RuntimeError, canary)
            raise self.skipTest("'cryptography' package not installed")

    def test_decrypt_key(self):
        """.decrypt_key()"""

        wallet = AppWallet({"1": PASS1, "2": PASS2})

        # check for support
        CIPHER1 = dict(v=1, c=13, s='6D7N7W53O7HHS37NLUFQ',
                       k='MHCTEGSNPFN5CGBJ', t='1')
        self.require_aes_support(canary=partial(wallet.decrypt_key, CIPHER1))

        # reference key
        self.assertEqual(wallet.decrypt_key(CIPHER1)[0], KEY1_RAW)

        # different salt used to encrypt same raw key
        CIPHER2 = dict(v=1, c=13, s='SPZJ54Y6IPUD2BYA4C6A',
                       k='ZGDXXTVQOWYLC2AU', t='1')
        self.assertEqual(wallet.decrypt_key(CIPHER2)[0], KEY1_RAW)

        # different sized key, password, and cost
        CIPHER3 = dict(v=1, c=8, s='FCCTARTIJWE7CPQHUDKA',
                       k='D2DRS32YESGHHINWFFCELKN7Z6NAHM4M', t='2')
        self.assertEqual(wallet.decrypt_key(CIPHER3)[0], KEY2_RAW)

        # wrong password should silently result in wrong key
        temp = CIPHER1.copy()
        temp.update(t='2')
        self.assertEqual(wallet.decrypt_key(temp)[0], b'\xafD6.F7\xeb\x19\x05Q')

        # missing tag should throw error
        temp = CIPHER1.copy()
        temp.update(t='3')
        self.assertRaises(KeyError, wallet.decrypt_key, temp)

        # unknown version should throw error
        temp = CIPHER1.copy()
        temp.update(v=999)
        self.assertRaises(ValueError, wallet.decrypt_key, temp)

    def test_decrypt_key_needs_recrypt(self):
        """.decrypt_key() -- needs_recrypt flag"""
        self.require_aes_support()

        wallet = AppWallet({"1": PASS1, "2": PASS2}, encrypt_cost=13)

        # ref should be accepted
        ref = dict(v=1, c=13, s='AAAA', k='AAAA', t='2')
        self.assertFalse(wallet.decrypt_key(ref)[1])

        # wrong cost
        temp = ref.copy()
        temp.update(c=8)
        self.assertTrue(wallet.decrypt_key(temp)[1])

        # wrong tag
        temp = ref.copy()
        temp.update(t="1")
        self.assertTrue(wallet.decrypt_key(temp)[1])

        # XXX: should this check salt_size?

    def assertSaneResult(self, result, wallet, key, tag="1",
                         needs_recrypt=False):
        """check encrypt_key() result has expected format"""

        self.assertEqual(set(result), set(["v", "t", "c", "s", "k"]))

        self.assertEqual(result['v'], 1)
        self.assertEqual(result['t'], tag)
        self.assertEqual(result['c'], wallet.encrypt_cost)

        self.assertEqual(len(result['s']), to_b32_size(wallet.salt_size))
        self.assertEqual(len(result['k']), to_b32_size(len(key)))

        result_key, result_needs_recrypt = wallet.decrypt_key(result)
        self.assertEqual(result_key, key)
        self.assertEqual(result_needs_recrypt, needs_recrypt)

    def test_encrypt_key(self):
        """.encrypt_key()"""

        # check for support
        wallet = AppWallet({"1": PASS1}, encrypt_cost=5)
        self.require_aes_support(canary=partial(wallet.encrypt_key, KEY1_RAW))

        # basic behavior
        result = wallet.encrypt_key(KEY1_RAW)
        self.assertSaneResult(result, wallet, KEY1_RAW)

        # creates new salt each time
        other = wallet.encrypt_key(KEY1_RAW)
        self.assertSaneResult(result, wallet, KEY1_RAW)
        self.assertNotEqual(other['s'], result['s'])
        self.assertNotEqual(other['k'], result['k'])

        # honors custom cost
        wallet2 = AppWallet({"1": PASS1}, encrypt_cost=6)
        result = wallet2.encrypt_key(KEY1_RAW)
        self.assertSaneResult(result, wallet2, KEY1_RAW)

        # honors default tag
        wallet2 = AppWallet({"1": PASS1, "2": PASS2})
        result = wallet2.encrypt_key(KEY1_RAW)
        self.assertSaneResult(result, wallet2, KEY1_RAW, tag="2")

        # honor salt size
        wallet2 = AppWallet({"1": PASS1})
        wallet2.salt_size = 64
        result = wallet2.encrypt_key(KEY1_RAW)
        self.assertSaneResult(result, wallet2, KEY1_RAW)

        # larger key
        result = wallet.encrypt_key(KEY2_RAW)
        self.assertSaneResult(result, wallet, KEY2_RAW)

        # border case: empty key
        # XXX: might want to allow this, but documenting behavior for now
        self.assertRaises(ValueError, wallet.encrypt_key, b"")

    def test_encrypt_cost_timing(self):
        """verify cost parameter via timing"""
        self.require_aes_support()

        # time default cost
        wallet = AppWallet({"1": "aaa"})
        wallet.encrypt_cost -= 2
        delta, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0)

        # this should take (2**3=8) times as long
        wallet.encrypt_cost += 3
        delta2, _ = time_call(partial(wallet.encrypt_key, KEY1_RAW), maxtime=0)

        self.assertAlmostEqual(delta2, delta*8, delta=(delta*8)*0.5)

    #=============================================================================
    # eoc
    #=============================================================================

#=============================================================================
# common OTP code
#=============================================================================

#: used as base value for RFC test vector keys
RFC_KEY_BYTES_20 = "12345678901234567890".encode("ascii")
RFC_KEY_BYTES_32 = (RFC_KEY_BYTES_20*2)[:32]
RFC_KEY_BYTES_64 = (RFC_KEY_BYTES_20*4)[:64]

# TODO: this class is separate from TotpTest due to historical issue,
#       when there was a base class, and a separate HOTP class.
#       these test case classes should probably be combined.
class TotpTest(TestCase):
    """
    common code shared by TotpTest & HotpTest
    """
    #=============================================================================
    # class attrs
    #=============================================================================

    descriptionPrefix = "passlib.totp.TOTP"

    #=============================================================================
    # setup
    #=============================================================================
    def setUp(self):
        super(TotpTest, self).setUp()

        # clear norm_hash_name() cache so 'unknown hash' warnings get emitted each time
        from passlib.crypto.digest import lookup_hash
        lookup_hash.clear_cache()

        # monkeypatch module's rng to be deterministic
        self.patchAttr(totp_module, "rng", self.getRandom())

    #=============================================================================
    # general helpers
    #=============================================================================
    def randtime(self):
        """
        helper to generate random epoch time
        :returns float: epoch time
        """
        return self.getRandom().random() * max_time_t

    def randotp(self, cls=None, **kwds):
        """
        helper which generates a random TOTP instance.
        """
        rng = self.getRandom()
        if "key" not in kwds:
            kwds['new'] = True
        kwds.setdefault("digits", rng.randint(6, 10))
        kwds.setdefault("alg", rng.choice(["sha1", "sha256", "sha512"]))
        kwds.setdefault("period", rng.randint(10, 120))
        return (cls or TOTP)(**kwds)

    def test_randotp(self):
        """
        internal test -- randotp()
        """
        otp1 = self.randotp()
        otp2 = self.randotp()

        self.assertNotEqual(otp1.key, otp2.key, "key not randomized:")

        # NOTE: has (1/5)**10 odds of failure
        for _ in range(10):
            if otp1.digits != otp2.digits:
                break
            otp2 = self.randotp()
        else:
            self.fail("digits not randomized")

        # NOTE: has (1/3)**10 odds of failure
        for _ in range(10):
            if otp1.alg != otp2.alg:
                break
            otp2 = self.randotp()
        else:
            self.fail("alg not randomized")

    #=============================================================================
    # reference vector helpers
    #=============================================================================

    #: default options used by test vectors (unless otherwise stated)
    vector_defaults = dict(format="base32", alg="sha1", period=30, digits=8)

    #: various TOTP test vectors,
    #: each element in list has format [options, (time, token <, int(expires)>), ...]
    vectors = [

        #-------------------------------------------------------------------------
        # passlib test vectors
        #-------------------------------------------------------------------------

        # 10 byte key, 6 digits
        [dict(key="ACDEFGHJKL234567", digits=6),
            # test fencepost to make sure we're rounding right
            (1412873399, '221105'), # == 29 mod 30
            (1412873400, '178491'), # == 0 mod 30
            (1412873401, '178491'), # == 1 mod 30
            (1412873429, '178491'), # == 29 mod 30
            (1412873430, '915114'), # == 0 mod 30
        ],

        # 10 byte key, 8 digits
        [dict(key="ACDEFGHJKL234567", digits=8),
            # should be same as 6 digits (above), but w/ 2 more digits on left side of token.
            (1412873399, '20221105'), # == 29 mod 30
            (1412873400, '86178491'), # == 0 mod 30
            (1412873401, '86178491'), # == 1 mod 30
            (1412873429, '86178491'), # == 29 mod 30
            (1412873430, '03915114'), # == 0 mod 30
        ],

        # sanity check on key used in docstrings
        [dict(key="S3JD-VB7Q-D2R7-JPXX", digits=6),
            (1419622709, '000492'),
            (1419622739, '897212'),
        ],

        #-------------------------------------------------------------------------
        # reference vectors taken from http://tools.ietf.org/html/rfc6238, appendix B
        # NOTE: while appendix B states same key used for all tests, the reference
        #       code in the appendix repeats the key up to the alg's block size,
        #       and uses *that* as the secret... so that's what we're doing here.
        #-------------------------------------------------------------------------

        # sha1 test vectors
        [dict(key=RFC_KEY_BYTES_20, format="raw", alg="sha1"),
            (59, '94287082'),
            (1111111109, '07081804'),
            (1111111111, '14050471'),
            (1234567890, '89005924'),
            (2000000000, '69279037'),
            (20000000000, '65353130'),
        ],

        # sha256 test vectors
        [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256"),
            (59, '46119246'),
            (1111111109, '68084774'),
            (1111111111, '67062674'),
            (1234567890, '91819424'),
            (2000000000, '90698825'),
            (20000000000, '77737706'),
        ],

        # sha512 test vectors
        [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512"),
            (59, '90693936'),
            (1111111109, '25091201'),
            (1111111111, '99943326'),
            (1234567890, '93441116'),
            (2000000000, '38618901'),
            (20000000000, '47863826'),
        ],

        #-------------------------------------------------------------------------
        # other test vectors
        #-------------------------------------------------------------------------

        # generated at http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript
        [dict(key="JBSWY3DPEHPK3PXP", digits=6), (1409192430, '727248'), (1419890990, '122419')],
        [dict(key="JBSWY3DPEHPK3PXP", digits=9, period=41), (1419891152, '662331049')],

        # found in https://github.com/eloquent/otis/blob/develop/test/suite/Totp/Value/TotpValueGeneratorTest.php, line 45
        [dict(key=RFC_KEY_BYTES_20, format="raw", period=60), (1111111111, '19360094')],
        [dict(key=RFC_KEY_BYTES_32, format="raw", alg="sha256", period=60), (1111111111, '40857319')],
        [dict(key=RFC_KEY_BYTES_64, format="raw", alg="sha512", period=60), (1111111111, '37023009')],

    ]

    def iter_test_vectors(self):
        """
        helper to iterate over test vectors.
        yields ``(totp, time, token, expires, prefix)`` tuples.
        """
        from passlib.totp import TOTP
        for row in self.vectors:
            kwds = self.vector_defaults.copy()
            kwds.update(row[0])
            for entry in row[1:]:
                if len(entry) == 3:
                    time, token, expires = entry
                else:
                    time, token = entry
                    expires = None
                # NOTE: not re-using otp between calls so that stateful methods
                #       (like .match) don't have problems.
                log.debug("test vector: %r time=%r token=%r expires=%r", kwds, time, token, expires)
                otp = TOTP(**kwds)
                prefix = "alg=%r time=%r token=%r: " % (otp.alg, time, token)
                yield otp, time, token, expires, prefix

    #=============================================================================
    # constructor tests
    #=============================================================================
    def test_ctor_w_new(self):
        """constructor -- 'new'  parameter"""

        # exactly one of 'key' or 'new' is required
        self.assertRaises(TypeError, TOTP)
        self.assertRaises(TypeError, TOTP, key='4aoggdbbqsyhntuz', new=True)

        # generates new key
        otp = TOTP(new=True)
        otp2 = TOTP(new=True)
        self.assertNotEqual(otp.key, otp2.key)

    def test_ctor_w_size(self):
        """constructor -- 'size'  parameter"""

        # should default to digest size, per RFC
        self.assertEqual(len(TOTP(new=True, alg="sha1").key), 20)
        self.assertEqual(len(TOTP(new=True, alg="sha256").key), 32)
        self.assertEqual(len(TOTP(new=True, alg="sha512").key), 64)

        # explicit key size
        self.assertEqual(len(TOTP(new=True, size=10).key), 10)
        self.assertEqual(len(TOTP(new=True, size=16).key), 16)

        # for new=True, maximum size enforced (based on alg)
        self.assertRaises(ValueError, TOTP, new=True, size=21, alg="sha1")

        # for new=True, minimum size enforced
        self.assertRaises(ValueError, TOTP, new=True, size=9)

        # for existing key, minimum size is only warned about
        with self.assertWarningList([
                dict(category=exc.PasslibSecurityWarning, message_re=".*for security purposes, secret key must be.*")
                ]):
            _ = TOTP('0A'*9, 'hex')

    def test_ctor_w_key_and_format(self):
        """constructor -- 'key' and 'format' parameters"""

        # handle base32 encoding (the default)
        self.assertEqual(TOTP(KEY1).key, KEY1_RAW)

            # .. w/ lower case
        self.assertEqual(TOTP(KEY1.lower()).key, KEY1_RAW)

            # .. w/ spaces (e.g. user-entered data)
        self.assertEqual(TOTP(' 4aog gdbb qsyh ntuz ').key, KEY1_RAW)

            # .. w/ invalid char
        self.assertRaises(Base32DecodeError, TOTP, 'ao!ggdbbqsyhntuz')

        # handle hex encoding
        self.assertEqual(TOTP('e01c630c2184b076ce99', 'hex').key, KEY1_RAW)

            # .. w/ invalid char
        self.assertRaises(Base16DecodeError, TOTP, 'X01c630c2184b076ce99', 'hex')

        # handle raw bytes
        self.assertEqual(TOTP(KEY1_RAW, "raw").key, KEY1_RAW)

    def test_ctor_w_alg(self):
        """constructor -- 'alg' parameter"""

        # normalize hash names
        self.assertEqual(TOTP(KEY1, alg="SHA-256").alg, "sha256")
        self.assertEqual(TOTP(KEY1, alg="SHA256").alg, "sha256")

        # invalid alg
        self.assertRaises(ValueError, TOTP, KEY1, alg="SHA-333")

    def test_ctor_w_digits(self):
        """constructor -- 'digits' parameter"""
        self.assertRaises(ValueError, TOTP, KEY1, digits=5)
        self.assertEqual(TOTP(KEY1, digits=6).digits, 6)  # min value
        self.assertEqual(TOTP(KEY1, digits=10).digits, 10)  # max value
        self.assertRaises(ValueError, TOTP, KEY1, digits=11)

    def test_ctor_w_period(self):
        """constructor -- 'period' parameter"""

        # default
        self.assertEqual(TOTP(KEY1).period, 30)

        # explicit value
        self.assertEqual(TOTP(KEY1, period=63).period, 63)

        # reject wrong type
        self.assertRaises(TypeError, TOTP, KEY1, period=1.5)
        self.assertRaises(TypeError, TOTP, KEY1, period='abc')

        # reject non-positive values
        self.assertRaises(ValueError, TOTP, KEY1, period=0)
        self.assertRaises(ValueError, TOTP, KEY1, period=-1)

    def test_ctor_w_label(self):
        """constructor -- 'label' parameter"""
        self.assertEqual(TOTP(KEY1).label, None)
        self.assertEqual(TOTP(KEY1, label="foo@bar").label, "foo@bar")
        self.assertRaises(ValueError, TOTP, KEY1, label="foo:bar")

    def test_ctor_w_issuer(self):
        """constructor -- 'issuer' parameter"""
        self.assertEqual(TOTP(KEY1).issuer, None)
        self.assertEqual(TOTP(KEY1, issuer="foo.com").issuer, "foo.com")
        self.assertRaises(ValueError, TOTP, KEY1, issuer="foo.com:bar")

    #=============================================================================
    # using() tests
    #=============================================================================

    # TODO: test using() w/ 'digits', 'alg', 'issue', 'wallet', **wallet_kwds

    def test_using_w_period(self):
        """using() -- 'period' parameter"""

        # default
        self.assertEqual(TOTP(KEY1).period, 30)

        # explicit value
        self.assertEqual(TOTP.using(period=63)(KEY1).period, 63)

        # reject wrong type
        self.assertRaises(TypeError, TOTP.using, period=1.5)
        self.assertRaises(TypeError, TOTP.using, period='abc')

        # reject non-positive values
        self.assertRaises(ValueError, TOTP.using, period=0)
        self.assertRaises(ValueError, TOTP.using, period=-1)

    def test_using_w_now(self):
        """using -- 'now' parameter"""

        # NOTE: reading time w/ normalize_time() to make sure custom .now actually has effect.

        # default -- time.time
        otp = self.randotp()
        self.assertIs(otp.now, _time.time)
        self.assertAlmostEqual(otp.normalize_time(None), int(_time.time()))

        # custom function
        counter = [123.12]
        def now():
            counter[0] += 1
            return counter[0]
        otp = self.randotp(cls=TOTP.using(now=now))
        # NOTE: TOTP() constructor invokes this as part of test, using up counter values 124 & 125
        self.assertEqual(otp.normalize_time(None), 126)
        self.assertEqual(otp.normalize_time(None), 127)

        # require callable
        self.assertRaises(TypeError, TOTP.using, now=123)

        # require returns int/float
        msg_re = r"now\(\) function must return non-negative"
        self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: 'abc')

        # require returns non-negative value
        self.assertRaisesRegex(AssertionError, msg_re, TOTP.using, now=lambda: -1)

    #=============================================================================
    # internal method tests
    #=============================================================================

    def test_normalize_token_instance(self, otp=None):
        """normalize_token() -- instance method"""
        if otp is None:
            otp = self.randotp(digits=7)

        # unicode & bytes
        self.assertEqual(otp.normalize_token(u('1234567')), '1234567')
        self.assertEqual(otp.normalize_token(b'1234567'), '1234567')

        # int
        self.assertEqual(otp.normalize_token(1234567), '1234567')

        # int which needs 0 padding
        self.assertEqual(otp.normalize_token(234567), '0234567')

        # reject wrong types (float, None)
        self.assertRaises(TypeError, otp.normalize_token, 1234567.0)
        self.assertRaises(TypeError, otp.normalize_token, None)

        # too few digits
        self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '123456')

        # too many digits
        self.assertRaises(exc.MalformedTokenError, otp.normalize_token, '01234567')
        self.assertRaises(exc.MalformedTokenError, otp.normalize_token, 12345678)

    def test_normalize_token_class(self):
        """normalize_token() -- class method"""
        self.test_normalize_token_instance(otp=TOTP.using(digits=7))

    def test_normalize_time(self):
        """normalize_time()"""
        TotpFactory = TOTP.using()
        otp = self.randotp(TotpFactory)

        for _ in range(10):
            time = self.randtime()
            tint = int(time)

            self.assertEqual(otp.normalize_time(time), tint)
            self.assertEqual(otp.normalize_time(tint + 0.5), tint)

            self.assertEqual(otp.normalize_time(tint), tint)

            dt = datetime.datetime.utcfromtimestamp(time)
            self.assertEqual(otp.normalize_time(dt), tint)

            orig = TotpFactory.now
            try:
                TotpFactory.now = staticmethod(lambda: time)
                self.assertEqual(otp.normalize_time(None), tint)
            finally:
                TotpFactory.now = orig

        self.assertRaises(TypeError, otp.normalize_time, '1234')

    #=============================================================================
    # key attr tests
    #=============================================================================

    def test_key_attrs(self):
        """pretty_key() and .key attributes"""
        rng = self.getRandom()

        # test key attrs
        otp = TOTP(KEY1_RAW, "raw")
        self.assertEqual(otp.key, KEY1_RAW)
        self.assertEqual(otp.hex_key, 'e01c630c2184b076ce99')
        self.assertEqual(otp.base32_key, KEY1)

        # test pretty_key()
        self.assertEqual(otp.pretty_key(), '4AOG-GDBB-QSYH-NTUZ')
        self.assertEqual(otp.pretty_key(sep=" "), '4AOG GDBB QSYH NTUZ')
        self.assertEqual(otp.pretty_key(sep=False), KEY1)
        self.assertEqual(otp.pretty_key(format="hex"), 'e01c-630c-2184-b076-ce99')

        # quick fuzz test: make attr access works for random key & random size
        otp = TOTP(new=True, size=rng.randint(10, 20))
        _ = otp.hex_key
        _ = otp.base32_key
        _ = otp.pretty_key()

    #=============================================================================
    # generate() tests
    #=============================================================================
    def test_totp_token(self):
        """generate() -- TotpToken() class"""
        from passlib.totp import TOTP, TotpToken

        # test known set of values
        otp = TOTP('s3jdvb7qd2r7jpxx')
        result = otp.generate(1419622739)
        self.assertIsInstance(result, TotpToken)
        self.assertEqual(result.token, '897212')
        self.assertEqual(result.counter, 47320757)
        ##self.assertEqual(result.start_time, 1419622710)
        self.assertEqual(result.expire_time, 1419622740)
        self.assertEqual(result, ('897212', 1419622740))
        self.assertEqual(len(result), 2)
        self.assertEqual(result[0], '897212')
        self.assertEqual(result[1], 1419622740)
        self.assertRaises(IndexError, result.__getitem__, -3)
        self.assertRaises(IndexError, result.__getitem__, 2)
        self.assertTrue(result)

        # time dependant bits...
        otp.now = lambda : 1419622739.5
        self.assertEqual(result.remaining, 0.5)
        self.assertTrue(result.valid)

        otp.now = lambda : 1419622741
        self.assertEqual(result.remaining, 0)
        self.assertFalse(result.valid)

        # same time -- shouldn't return same object, but should be equal
        result2 = otp.generate(1419622739)
        self.assertIsNot(result2, result)
        self.assertEqual(result2, result)

        # diff time in period -- shouldn't return same object, but should be equal
        result3 = otp.generate(1419622711)
        self.assertIsNot(result3, result)
        self.assertEqual(result3, result)

        # shouldn't be equal
        result4 = otp.generate(1419622999)
        self.assertNotEqual(result4, result)

    def test_generate(self):
        """generate()"""
        from passlib.totp import TOTP

        # generate token
        otp = TOTP(new=True)
        time = self.randtime()
        result = otp.generate(time)
        token = result.token
        self.assertIsInstance(token, unicode)
        start_time = result.counter * 30

        # should generate same token for next 29s
        self.assertEqual(otp.generate(start_time + 29).token, token)

        # and new one at 30s
        self.assertNotEqual(otp.generate(start_time + 30).token, token)

        # verify round-trip conversion of datetime
        dt = datetime.datetime.utcfromtimestamp(time)
        self.assertEqual(int(otp.normalize_time(dt)), int(time))

        # handle datetime object
        self.assertEqual(otp.generate(dt).token, token)

        # omitting value should use current time
        otp2 = TOTP.using(now=lambda: time)(key=otp.base32_key)
        self.assertEqual(otp2.generate().token, token)

        # reject invalid time
        self.assertRaises(ValueError, otp.generate, -1)

    def test_generate_w_reference_vectors(self):
        """generate() -- reference vectors"""
        for otp, time, token, expires, prefix in self.iter_test_vectors():
            # should output correct token for specified time
            result = otp.generate(time)
            self.assertEqual(result.token, token, msg=prefix)
            self.assertEqual(result.counter, time // otp.period, msg=prefix)
            if expires:
                self.assertEqual(result.expire_time, expires)

    #=============================================================================
    # TotpMatch() tests
    #=============================================================================

    def assertTotpMatch(self, match, time, skipped=0, period=30, window=30, msg=''):
        from passlib.totp import TotpMatch

        # test type
        self.assertIsInstance(match, TotpMatch)

        # totp sanity check
        self.assertIsInstance(match.totp, TOTP)
        self.assertEqual(match.totp.period, period)

        # test attrs
        self.assertEqual(match.time, time, msg=msg + " matched time:")
        expected = time // period
        counter = expected + skipped
        self.assertEqual(match.counter, counter, msg=msg + " matched counter:")
        self.assertEqual(match.expected_counter, expected, msg=msg + " expected counter:")
        self.assertEqual(match.skipped, skipped, msg=msg + " skipped:")
        self.assertEqual(match.cache_seconds, period + window)
        expire_time = (counter + 1) * period
        self.assertEqual(match.expire_time, expire_time)
        self.assertEqual(match.cache_time, expire_time + window)

        # test tuple
        self.assertEqual(len(match), 2)
        self.assertEqual(match, (counter, time))
        self.assertRaises(IndexError, match.__getitem__, -3)
        self.assertEqual(match[0], counter)
        self.assertEqual(match[1], time)
        self.assertRaises(IndexError, match.__getitem__, 2)

        # test bool
        self.assertTrue(match)

    def test_totp_match_w_valid_token(self):
        """match() -- valid TotpMatch object"""
        time = 141230981
        token = '781501'
        otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3)
        result = otp.match(token, time)
        self.assertTotpMatch(result, time=time, skipped=0)

    def test_totp_match_w_older_token(self):
        """match() -- valid TotpMatch object with future token"""
        from passlib.totp import TotpMatch

        time = 141230981
        token = '781501'
        otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3)
        result = otp.match(token, time - 30)
        self.assertTotpMatch(result, time=time - 30, skipped=1)

    def test_totp_match_w_new_token(self):
        """match() -- valid TotpMatch object with past token"""
        time = 141230981
        token = '781501'
        otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3)
        result = otp.match(token, time + 30)
        self.assertTotpMatch(result, time=time + 30, skipped=-1)

    def test_totp_match_w_invalid_token(self):
        """match() -- invalid TotpMatch object"""
        time = 141230981
        token = '781501'
        otp = TOTP.using(now=lambda: time + 24 * 3600)(KEY3)
        self.assertRaises(exc.InvalidTokenError, otp.match, token, time + 60)

    #=============================================================================
    # match() tests
    #=============================================================================

    def assertVerifyMatches(self, expect_skipped, token, time,  # *
                            otp, gen_time=None, **kwds):
        """helper to test otp.match() output is correct"""
        # NOTE: TotpMatch return type tested more throughly above ^^^
        msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \
              (otp.base32_key, otp.alg, otp.period, token, gen_time, time)
        result = otp.match(token, time, **kwds)
        self.assertTotpMatch(result,
                             time=otp.normalize_time(time),
                             period=otp.period,
                             window=kwds.get("window", 30),
                             skipped=expect_skipped,
                             msg=msg)

    def assertVerifyRaises(self, exc_class, token, time,  # *
                          otp, gen_time=None,
                          **kwds):
        """helper to test otp.match() throws correct error"""
        # NOTE: TotpMatch return type tested more throughly above ^^^
        msg = "key=%r alg=%r period=%r token=%r gen_time=%r time=%r:" % \
              (otp.base32_key, otp.alg, otp.period, token, gen_time, time)
        return self.assertRaises(exc_class, otp.match, token, time,
                                 __msg__=msg, **kwds)

    def test_match_w_window(self):
        """match() -- 'time' and 'window' parameters"""

        # init generator & helper
        otp = self.randotp()
        period = otp.period
        time = self.randtime()
        token = otp.generate(time).token
        common = dict(otp=otp, gen_time=time)
        assertMatches = partial(self.assertVerifyMatches, **common)
        assertRaises = partial(self.assertVerifyRaises, **common)

        #-------------------------------
        # basic validation, and 'window' parameter
        #-------------------------------

        # validate against previous counter (passes if window >= period)
        assertRaises(exc.InvalidTokenError, token, time - period, window=0)
        assertMatches(+1, token, time - period, window=period)
        assertMatches(+1, token, time - period, window=2 * period)

        # validate against current counter
        assertMatches(0, token, time, window=0)

        # validate against next counter (passes if window >= period)
        assertRaises(exc.InvalidTokenError, token, time + period, window=0)
        assertMatches(-1, token, time + period, window=period)
        assertMatches(-1, token, time + period, window=2 * period)

        # validate against two time steps later (should never pass)
        assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=0)
        assertRaises(exc.InvalidTokenError, token, time + 2 * period, window=period)
        assertMatches(-2, token, time + 2 * period, window=2 * period)

        # TODO: test window values that aren't multiples of period
        #       (esp ensure counter rounding works correctly)

        #-------------------------------
        # time normalization
        #-------------------------------

        # handle datetimes
        dt = datetime.datetime.utcfromtimestamp(time)
        assertMatches(0, token, dt, window=0)

        # reject invalid time
        assertRaises(ValueError, token, -1)

    def test_match_w_skew(self):
        """match() -- 'skew' parameters"""
        # init generator & helper
        otp = self.randotp()
        period = otp.period
        time = self.randtime()
        common = dict(otp=otp, gen_time=time)
        assertMatches = partial(self.assertVerifyMatches, **common)
        assertRaises = partial(self.assertVerifyRaises, **common)

        # assume client is running far behind server / has excessive transmission delay
        skew = 3 * period
        behind_token = otp.generate(time - skew).token
        assertRaises(exc.InvalidTokenError, behind_token, time, window=0)
        assertMatches(-3, behind_token, time, window=0, skew=-skew)

        # assume client is running far ahead of server
        ahead_token = otp.generate(time + skew).token
        assertRaises(exc.InvalidTokenError, ahead_token, time, window=0)
        assertMatches(+3, ahead_token, time, window=0, skew=skew)

        # TODO: test skew + larger window

    def test_match_w_reuse(self):
        """match() -- 'reuse' and 'last_counter' parameters"""

        # init generator & helper
        otp = self.randotp()
        period = otp.period
        time = self.randtime()
        tdata = otp.generate(time)
        token = tdata.token
        counter = tdata.counter
        expire_time = tdata.expire_time
        common = dict(otp=otp, gen_time=time)
        assertMatches = partial(self.assertVerifyMatches, **common)
        assertRaises = partial(self.assertVerifyRaises, **common)

        # last counter unset --
        # previous period's token should count as valid
        assertMatches(-1, token, time + period, window=period)

        # last counter set 2 periods ago --
        # previous period's token should count as valid
        assertMatches(-1, token, time + period, last_counter=counter-1,
                      window=period)

        # last counter set 2 periods ago --
        # 2 periods ago's token should NOT count as valid
        assertRaises(exc.InvalidTokenError, token, time + 2 * period,
                     last_counter=counter, window=period)

        # last counter set 1 period ago --
        # previous period's token should now be rejected as 'used'
        err = assertRaises(exc.UsedTokenError, token, time + period,
                           last_counter=counter, window=period)
        self.assertEqual(err.expire_time, expire_time)

        # last counter set to current period --
        # current period's token should be rejected
        err = assertRaises(exc.UsedTokenError, token, time,
                           last_counter=counter, window=0)
        self.assertEqual(err.expire_time, expire_time)

    def test_match_w_token_normalization(self):
        """match() -- token normalization"""
        # setup test helper
        otp = TOTP('otxl2f5cctbprpzx')
        match = otp.match
        time = 1412889861

        # separators / spaces should be stripped (orig token '332136')
        self.assertTrue(match('    3 32-136  ', time))

        # ascii bytes
        self.assertTrue(match(b'332136', time))

        # too few digits
        self.assertRaises(exc.MalformedTokenError, match, '12345', time)

        # invalid char
        self.assertRaises(exc.MalformedTokenError, match, '12345X', time)

        # leading zeros count towards size
        self.assertRaises(exc.MalformedTokenError, match, '0123456', time)

    def test_match_w_reference_vectors(self):
        """match() -- reference vectors"""
        for otp, time, token, expires, msg in self.iter_test_vectors():
            # create wrapper
            match = otp.match

            # token should match against time
            result = match(token, time)
            self.assertTrue(result)
            self.assertEqual(result.counter, time // otp.period, msg=msg)

            # should NOT match against another time
            self.assertRaises(exc.InvalidTokenError, match, token, time + 100, window=0)

    #=============================================================================
    # verify() tests
    #=============================================================================
    def test_verify(self):
        """verify()"""
        # NOTE: since this is thin wrapper around .from_source() and .match(),
        #       just testing basic behavior here.

        from passlib.totp import TOTP

        time = 1412889861
        TotpFactory = TOTP.using(now=lambda: time)

        # successful match
        source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx')
        match = TotpFactory.verify('332136', source1)
        self.assertTotpMatch(match, time=time)

        # failed match
        source1 = dict(v=1, type="totp", key='otxl2f5cctbprpzx')
        self.assertRaises(exc.InvalidTokenError, TotpFactory.verify, '332155', source1)

        # bad source
        source1 = dict(v=1, type="totp")
        self.assertRaises(ValueError, TotpFactory.verify, '332155', source1)

        # successful match -- json source
        source1json = '{"v": 1, "type": "totp", "key": "otxl2f5cctbprpzx"}'
        match = TotpFactory.verify('332136', source1json)
        self.assertTotpMatch(match, time=time)

        # successful match -- URI
        source1uri = 'otpauth://totp/Label?secret=otxl2f5cctbprpzx'
        match = TotpFactory.verify('332136', source1uri)
        self.assertTotpMatch(match, time=time)

    #=============================================================================
    # serialization frontend tests
    #=============================================================================
    def test_from_source(self):
        """from_source()"""
        from passlib.totp import TOTP
        from_source = TOTP.from_source

        # uri (unicode)
        otp = from_source(u("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                            "issuer=Example"))
        self.assertEqual(otp.key, KEY4_RAW)

        # uri (bytes)
        otp = from_source(b"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                          b"issuer=Example")
        self.assertEqual(otp.key, KEY4_RAW)

        # dict
        otp = from_source(dict(v=1, type="totp", key=KEY4))
        self.assertEqual(otp.key, KEY4_RAW)

        # json (unicode)
        otp = from_source(u('{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}'))
        self.assertEqual(otp.key, KEY4_RAW)

        # json (bytes)
        otp = from_source(b'{"v": 1, "type": "totp", "key": "JBSWY3DPEHPK3PXP"}')
        self.assertEqual(otp.key, KEY4_RAW)

        # TOTP object -- return unchanged
        self.assertIs(from_source(otp), otp)

        # TOTP object w/ different wallet -- return new one.
        wallet1 = AppWallet()
        otp1 = TOTP.using(wallet=wallet1).from_source(otp)
        self.assertIsNot(otp1, otp)
        self.assertEqual(otp1.to_dict(), otp.to_dict())

        # TOTP object w/ same wallet -- return original
        otp2 = TOTP.using(wallet=wallet1).from_source(otp1)
        self.assertIs(otp2, otp1)

        # random string
        self.assertRaises(ValueError, from_source, u("foo"))
        self.assertRaises(ValueError, from_source, b"foo")

    #=============================================================================
    # uri serialization tests
    #=============================================================================
    def test_from_uri(self):
        """from_uri()"""
        from passlib.totp import TOTP
        from_uri = TOTP.from_uri

        # URIs from https://code.google.com/p/google-authenticator/wiki/KeyUriFormat

        #--------------------------------------------------------------------------------
        # canonical uri
        #--------------------------------------------------------------------------------
        otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                       "issuer=Example")
        self.assertIsInstance(otp, TOTP)
        self.assertEqual(otp.key, KEY4_RAW)
        self.assertEqual(otp.label, "alice@google.com")
        self.assertEqual(otp.issuer, "Example")
        self.assertEqual(otp.alg, "sha1") # default
        self.assertEqual(otp.period, 30) # default
        self.assertEqual(otp.digits, 6) # default

        #--------------------------------------------------------------------------------
        # secret param
        #--------------------------------------------------------------------------------

        # secret case insensitive
        otp = from_uri("otpauth://totp/Example:alice@google.com?secret=jbswy3dpehpk3pxp&"
                       "issuer=Example")
        self.assertEqual(otp.key, KEY4_RAW)

        # missing secret
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?digits=6")

        # undecodable secret
        self.assertRaises(Base32DecodeError, from_uri, "otpauth://totp/Example:alice@google.com?"
                                                       "secret=JBSWY3DPEHP@3PXP")

        #--------------------------------------------------------------------------------
        # label param
        #--------------------------------------------------------------------------------

        # w/ encoded space
        otp = from_uri("otpauth://totp/Provider1:Alice%20Smith?secret=JBSWY3DPEHPK3PXP&"
                       "issuer=Provider1")
        self.assertEqual(otp.label, "Alice Smith")
        self.assertEqual(otp.issuer, "Provider1")

        # w/ encoded space and colon
        # (note url has leading space before 'alice') -- taken from KeyURI spec
        otp = from_uri("otpauth://totp/Big%20Corporation%3A%20alice@bigco.com?"
                       "secret=JBSWY3DPEHPK3PXP")
        self.assertEqual(otp.label, "alice@bigco.com")
        self.assertEqual(otp.issuer, "Big Corporation")

        #--------------------------------------------------------------------------------
        # issuer param / prefix
        #--------------------------------------------------------------------------------

        # 'new style' issuer only
        otp = from_uri("otpauth://totp/alice@bigco.com?secret=JBSWY3DPEHPK3PXP&issuer=Big%20Corporation")
        self.assertEqual(otp.label, "alice@bigco.com")
        self.assertEqual(otp.issuer, "Big Corporation")

        # new-vs-old issuer mismatch
        self.assertRaises(ValueError, TOTP.from_uri,
                          "otpauth://totp/Provider1:alice?secret=JBSWY3DPEHPK3PXP&issuer=Provider2")

        #--------------------------------------------------------------------------------
        # algorithm param
        #--------------------------------------------------------------------------------

        # custom alg
        otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256")
        self.assertEqual(otp.alg, "sha256")

        # unknown alg
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?"
                                                "secret=JBSWY3DPEHPK3PXP&algorithm=SHA333")

        #--------------------------------------------------------------------------------
        # digit param
        #--------------------------------------------------------------------------------

        # custom digits
        otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=8")
        self.assertEqual(otp.digits, 8)

        # digits out of range / invalid
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=A")
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=%20")
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&digits=15")

        #--------------------------------------------------------------------------------
        # period param
        #--------------------------------------------------------------------------------

        # custom period
        otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&period=63")
        self.assertEqual(otp.period, 63)

        # reject period < 1
        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?"
                                                "secret=JBSWY3DPEHPK3PXP&period=0")

        self.assertRaises(ValueError, from_uri, "otpauth://totp/Example:alice@google.com?"
                                                "secret=JBSWY3DPEHPK3PXP&period=-1")

        #--------------------------------------------------------------------------------
        # unrecognized param
        #--------------------------------------------------------------------------------

        # should issue warning, but otherwise ignore extra param
        with self.assertWarningList([
            dict(category=exc.PasslibRuntimeWarning, message_re="unexpected parameters encountered")
        ]):
            otp = from_uri("otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                           "foo=bar&period=63")
        self.assertEqual(otp.base32_key, KEY4)
        self.assertEqual(otp.period, 63)

    def test_to_uri(self):
        """to_uri()"""

        #-------------------------------------------------------------------------
        # label & issuer parameters
        #-------------------------------------------------------------------------

        # with label & issuer
        otp = TOTP(KEY4, alg="sha1", digits=6, period=30)
        self.assertEqual(otp.to_uri("alice@google.com", "Example Org"),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                         "issuer=Example%20Org")

        # label is required
        self.assertRaises(ValueError, otp.to_uri, None, "Example Org")

        # with label only
        self.assertEqual(otp.to_uri("alice@google.com"),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP")

        # with default label from constructor
        otp.label = "alice@google.com"
        self.assertEqual(otp.to_uri(),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP")

        # with default label & default issuer from constructor
        otp.issuer = "Example Org"
        self.assertEqual(otp.to_uri(),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP"
                         "&issuer=Example%20Org")

        # reject invalid label
        self.assertRaises(ValueError, otp.to_uri, "label:with:semicolons")

        # reject invalid issue
        self.assertRaises(ValueError, otp.to_uri, "alice@google.com", "issuer:with:semicolons")

        #-------------------------------------------------------------------------
        # algorithm parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, alg="sha256").to_uri("alice@google.com"),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                         "algorithm=SHA256")

        #-------------------------------------------------------------------------
        # digits parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, digits=8).to_uri("alice@google.com"),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                         "digits=8")

        #-------------------------------------------------------------------------
        # period parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, period=63).to_uri("alice@google.com"),
                         "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP&"
                         "period=63")

    #=============================================================================
    # dict serialization tests
    #=============================================================================
    def test_from_dict(self):
        """from_dict()"""
        from passlib.totp import TOTP
        from_dict = TOTP.from_dict

        #--------------------------------------------------------------------------------
        # canonical simple example
        #--------------------------------------------------------------------------------
        otp = from_dict(dict(v=1, type="totp", key=KEY4, label="alice@google.com", issuer="Example"))
        self.assertIsInstance(otp, TOTP)
        self.assertEqual(otp.key, KEY4_RAW)
        self.assertEqual(otp.label, "alice@google.com")
        self.assertEqual(otp.issuer, "Example")
        self.assertEqual(otp.alg, "sha1")  # default
        self.assertEqual(otp.period, 30)  # default
        self.assertEqual(otp.digits, 6)  # default

        #--------------------------------------------------------------------------------
        # metadata
        #--------------------------------------------------------------------------------

        # missing version
        self.assertRaises(ValueError, from_dict, dict(type="totp", key=KEY4))

        # invalid version
        self.assertRaises(ValueError, from_dict, dict(v=0, type="totp", key=KEY4))
        self.assertRaises(ValueError, from_dict, dict(v=999, type="totp", key=KEY4))

        # missing type
        self.assertRaises(ValueError, from_dict, dict(v=1, key=KEY4))

        #--------------------------------------------------------------------------------
        # secret param
        #--------------------------------------------------------------------------------

        # secret case insensitive
        otp = from_dict(dict(v=1, type="totp", key=KEY4.lower(), label="alice@google.com", issuer="Example"))
        self.assertEqual(otp.key, KEY4_RAW)

        # missing secret
        self.assertRaises(ValueError, from_dict, dict(v=1, type="totp"))

        # undecodable secret
        self.assertRaises(Base32DecodeError, from_dict,
                          dict(v=1, type="totp", key="JBSWY3DPEHP@3PXP"))

        #--------------------------------------------------------------------------------
        # label & issuer params
        #--------------------------------------------------------------------------------

        otp = from_dict(dict(v=1, type="totp", key=KEY4, label="Alice Smith", issuer="Provider1"))
        self.assertEqual(otp.label, "Alice Smith")
        self.assertEqual(otp.issuer, "Provider1")

        #--------------------------------------------------------------------------------
        # algorithm param
        #--------------------------------------------------------------------------------

        # custom alg
        otp = from_dict(dict(v=1, type="totp", key=KEY4, alg="sha256"))
        self.assertEqual(otp.alg, "sha256")

        # unknown alg
        self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, alg="sha333"))

        #--------------------------------------------------------------------------------
        # digit param
        #--------------------------------------------------------------------------------

        # custom digits
        otp = from_dict(dict(v=1, type="totp", key=KEY4, digits=8))
        self.assertEqual(otp.digits, 8)

        # digits out of range / invalid
        self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, digits="A"))
        self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, digits=15))

        #--------------------------------------------------------------------------------
        # period param
        #--------------------------------------------------------------------------------

        # custom period
        otp = from_dict(dict(v=1, type="totp", key=KEY4, period=63))
        self.assertEqual(otp.period, 63)

        # reject period < 1
        self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=0))
        self.assertRaises(ValueError, from_dict, dict(v=1, type="totp", key=KEY4, period=-1))

        #--------------------------------------------------------------------------------
        # unrecognized param
        #--------------------------------------------------------------------------------
        self.assertRaises(TypeError, from_dict, dict(v=1, type="totp", key=KEY4, INVALID=123))

    def test_to_dict(self):
        """to_dict()"""

        #-------------------------------------------------------------------------
        # label & issuer parameters
        #-------------------------------------------------------------------------

        # without label or issuer
        otp = TOTP(KEY4, alg="sha1", digits=6, period=30)
        self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4))

        # with label & issuer from constructor
        otp = TOTP(KEY4, alg="sha1", digits=6, period=30,
                   label="alice@google.com", issuer="Example Org")
        self.assertEqual(otp.to_dict(),
                         dict(v=1, type="totp", key=KEY4,
                              label="alice@google.com", issuer="Example Org"))

        # with label only
        otp = TOTP(KEY4, alg="sha1", digits=6, period=30,
                   label="alice@google.com")
        self.assertEqual(otp.to_dict(),
                         dict(v=1, type="totp", key=KEY4,
                              label="alice@google.com"))

        # with issuer only
        otp = TOTP(KEY4, alg="sha1", digits=6, period=30,
                   issuer="Example Org")
        self.assertEqual(otp.to_dict(),
                         dict(v=1, type="totp", key=KEY4,
                              issuer="Example Org"))

        # don't serialize default issuer
        TotpFactory = TOTP.using(issuer="Example Org")
        otp = TotpFactory(KEY4)
        self.assertEqual(otp.to_dict(), dict(v=1, type="totp", key=KEY4))

        # don't serialize default issuer *even if explicitly set*
        otp = TotpFactory(KEY4, issuer="Example Org")
        self.assertEqual(otp.to_dict(),  dict(v=1, type="totp", key=KEY4))

        #-------------------------------------------------------------------------
        # algorithm parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, alg="sha256").to_dict(),
                         dict(v=1, type="totp", key=KEY4, alg="sha256"))

        #-------------------------------------------------------------------------
        # digits parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, digits=8).to_dict(),
                         dict(v=1, type="totp", key=KEY4, digits=8))

        #-------------------------------------------------------------------------
        # period parameter
        #-------------------------------------------------------------------------
        self.assertEqual(TOTP(KEY4, period=63).to_dict(),
                         dict(v=1, type="totp", key=KEY4, period=63))

    # TODO: to_dict()
    #           with encrypt=False
    #           with encrypt="auto" + wallet + secrets
    #           with encrypt="auto" + wallet + no secrets
    #           with encrypt="auto" + no wallet
    #           with encrypt=True + wallet + secrets
    #           with encrypt=True + wallet + no secrets
    #           with encrypt=True + no wallet
    #           that 'changed' is set for old versions, and old encryption tags.

    #=============================================================================
    # json serialization tests
    #=============================================================================

    # TODO: from_json() / to_json().
    #       (skipped for right now cause just wrapper for from_dict/to_dict)

    #=============================================================================
    # eoc
    #=============================================================================

#=============================================================================
# eof
#=============================================================================