pwquality_test.go 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package pwquality
  15. import (
  16. "strings"
  17. "testing"
  18. )
  19. func TestParseConfig(t *testing.T) {
  20. tests := []struct {
  21. name string
  22. content string
  23. expected *Config
  24. }{
  25. {
  26. name: "basic config",
  27. content: `minlen = 8
  28. dcredit = -1
  29. ucredit = -1
  30. lcredit = -1
  31. ocredit = -1
  32. minclass = 3`,
  33. expected: &Config{
  34. Minlen: 8,
  35. Dcredit: -1,
  36. Ucredit: -1,
  37. Lcredit: -1,
  38. Ocredit: -1,
  39. Minclass: 3,
  40. },
  41. },
  42. {
  43. name: "config with comments",
  44. content: `# This is a comment
  45. minlen = 12
  46. # Another comment
  47. dcredit = -2
  48. ucredit = 0
  49. lcredit = -1
  50. ocredit = -1`,
  51. expected: &Config{
  52. Minlen: 12,
  53. Dcredit: -2,
  54. Ucredit: 0,
  55. Lcredit: -1,
  56. Ocredit: -1,
  57. Minclass: 0,
  58. },
  59. },
  60. {
  61. name: "empty config",
  62. content: `# Empty config file
  63. # No settings`,
  64. expected: &Config{
  65. Minlen: 0,
  66. Dcredit: 0,
  67. Ucredit: 0,
  68. Lcredit: 0,
  69. Ocredit: 0,
  70. Minclass: 0,
  71. },
  72. },
  73. {
  74. name: "config with spaces",
  75. content: ` minlen = 10
  76. dcredit = -1
  77. ucredit = -1`,
  78. expected: &Config{
  79. Minlen: 10,
  80. Dcredit: -1,
  81. Ucredit: -1,
  82. Lcredit: 0,
  83. Ocredit: 0,
  84. Minclass: 0,
  85. },
  86. },
  87. {
  88. name: "positive credit values",
  89. content: `minlen = 8
  90. dcredit = 1
  91. ucredit = 1
  92. lcredit = 1`,
  93. expected: &Config{
  94. Minlen: 8,
  95. Dcredit: 1,
  96. Ucredit: 1,
  97. Lcredit: 1,
  98. Ocredit: 0,
  99. Minclass: 0,
  100. },
  101. },
  102. {
  103. name: "config with maxrepeat",
  104. content: `minlen = 8
  105. maxrepeat = 3`,
  106. expected: &Config{
  107. Minlen: 8,
  108. Maxrepeat: 3,
  109. },
  110. },
  111. {
  112. name: "config with maxclassrepeat",
  113. content: `minlen = 8
  114. maxclassrepeat = 2`,
  115. expected: &Config{
  116. Minlen: 8,
  117. Maxclassrepeat: 2,
  118. },
  119. },
  120. {
  121. name: "config with maxsequence",
  122. content: `minlen = 8
  123. maxsequence = 3`,
  124. expected: &Config{
  125. Minlen: 8,
  126. Maxsequence: 3,
  127. },
  128. },
  129. {
  130. name: "config with all new options",
  131. content: `minlen = 8
  132. maxrepeat = 3
  133. maxclassrepeat = 2
  134. maxsequence = 3`,
  135. expected: &Config{
  136. Minlen: 8,
  137. Maxrepeat: 3,
  138. Maxclassrepeat: 2,
  139. Maxsequence: 3,
  140. },
  141. },
  142. {
  143. name: "invalid value - non-numeric",
  144. content: `minlen = abc
  145. dcredit = -1`,
  146. expected: &Config{
  147. Minlen: 0, // 无效值应该被忽略
  148. Dcredit: -1,
  149. },
  150. },
  151. {
  152. name: "line without equals sign",
  153. content: `minlen 8
  154. dcredit = -1`,
  155. expected: &Config{
  156. Minlen: 0, // 没有等号的行应该被忽略
  157. Dcredit: -1,
  158. },
  159. },
  160. {
  161. name: "multiple equals signs",
  162. content: `minlen = 8 = 10
  163. dcredit = -1`,
  164. expected: &Config{
  165. Minlen: 0, // " 8 = 10" 不是有效数字,会被忽略
  166. Dcredit: -1,
  167. },
  168. },
  169. {
  170. name: "unknown config key",
  171. content: `minlen = 8
  172. unknown_key = 10
  173. dcredit = -1`,
  174. expected: &Config{
  175. Minlen: 8,
  176. Dcredit: -1,
  177. },
  178. },
  179. {
  180. name: "empty value",
  181. content: `minlen =
  182. dcredit = -1`,
  183. expected: &Config{
  184. Minlen: 0, // 空值应该被忽略
  185. Dcredit: -1,
  186. },
  187. },
  188. {
  189. name: "config with enforcing",
  190. content: `minlen = 8
  191. enforcing = 1`,
  192. expected: &Config{
  193. Minlen: 8,
  194. Enforcing: 1,
  195. },
  196. },
  197. {
  198. name: "config with enforcing disabled",
  199. content: `minlen = 8
  200. enforcing = 0`,
  201. expected: &Config{
  202. Minlen: 8,
  203. Enforcing: 0,
  204. },
  205. },
  206. {
  207. name: "config with enforce_for_root",
  208. content: `minlen = 8
  209. enforce_for_root = 1`,
  210. expected: &Config{
  211. Minlen: 8,
  212. EnforceForRoot: 1,
  213. },
  214. },
  215. {
  216. name: "config with both enforcing parameters",
  217. content: `minlen = 8
  218. enforcing = 1
  219. enforce_for_root = 1`,
  220. expected: &Config{
  221. Minlen: 8,
  222. Enforcing: 1,
  223. EnforceForRoot: 1,
  224. },
  225. },
  226. {
  227. name: "config with enforce_for_root as flag (no equals sign)",
  228. content: `minlen = 8
  229. enforce_for_root`,
  230. expected: &Config{
  231. Minlen: 8,
  232. EnforceForRoot: 1, // 独立标志形式,应设置为 1
  233. },
  234. },
  235. }
  236. for _, tt := range tests {
  237. t.Run(tt.name, func(t *testing.T) {
  238. config := ParseConfig([]byte(tt.content))
  239. if config.Minlen != tt.expected.Minlen {
  240. t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen)
  241. }
  242. if config.Dcredit != tt.expected.Dcredit {
  243. t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit)
  244. }
  245. if config.Ucredit != tt.expected.Ucredit {
  246. t.Errorf("Ucredit = %d, want %d", config.Ucredit, tt.expected.Ucredit)
  247. }
  248. if config.Lcredit != tt.expected.Lcredit {
  249. t.Errorf("Lcredit = %d, want %d", config.Lcredit, tt.expected.Lcredit)
  250. }
  251. if config.Ocredit != tt.expected.Ocredit {
  252. t.Errorf("Ocredit = %d, want %d", config.Ocredit, tt.expected.Ocredit)
  253. }
  254. if config.Minclass != tt.expected.Minclass {
  255. t.Errorf("Minclass = %d, want %d", config.Minclass, tt.expected.Minclass)
  256. }
  257. if config.Maxrepeat != tt.expected.Maxrepeat {
  258. t.Errorf("Maxrepeat = %d, want %d", config.Maxrepeat, tt.expected.Maxrepeat)
  259. }
  260. if config.Maxclassrepeat != tt.expected.Maxclassrepeat {
  261. t.Errorf("Maxclassrepeat = %d, want %d", config.Maxclassrepeat, tt.expected.Maxclassrepeat)
  262. }
  263. if config.Maxsequence != tt.expected.Maxsequence {
  264. t.Errorf("Maxsequence = %d, want %d", config.Maxsequence, tt.expected.Maxsequence)
  265. }
  266. })
  267. }
  268. }
  269. func TestParseConfig_EdgeCases(t *testing.T) {
  270. tests := []struct {
  271. name string
  272. content string
  273. expected *Config
  274. }{
  275. {
  276. name: "empty content",
  277. content: "",
  278. expected: &Config{},
  279. },
  280. {
  281. name: "only newlines",
  282. content: `
  283. `,
  284. expected: &Config{},
  285. },
  286. {
  287. name: "mixed valid and invalid",
  288. content: `minlen = 8
  289. invalid_line
  290. dcredit = -1
  291. another_invalid = line`,
  292. expected: &Config{
  293. Minlen: 8,
  294. Dcredit: -1,
  295. },
  296. },
  297. }
  298. for _, tt := range tests {
  299. t.Run(tt.name, func(t *testing.T) {
  300. config := ParseConfig([]byte(tt.content))
  301. if config.Minlen != tt.expected.Minlen {
  302. t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen)
  303. }
  304. if config.Dcredit != tt.expected.Dcredit {
  305. t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit)
  306. }
  307. })
  308. }
  309. }
  310. func TestConfig_Validate(t *testing.T) {
  311. tests := []struct {
  312. name string
  313. config *Config
  314. password string
  315. wantError bool
  316. errorMsg string
  317. }{
  318. {
  319. name: "nil config should pass",
  320. config: nil,
  321. password: "anypassword",
  322. wantError: false,
  323. },
  324. {
  325. name: "minlen check - too short",
  326. config: &Config{Minlen: 8},
  327. password: "short",
  328. wantError: true,
  329. errorMsg: "effective length",
  330. },
  331. {
  332. name: "minlen check - pass",
  333. config: &Config{Minlen: 8},
  334. password: "longpassword",
  335. wantError: false,
  336. },
  337. {
  338. name: "dcredit negative - require at least 1 digit",
  339. config: &Config{Dcredit: -1},
  340. password: "nodigits",
  341. wantError: true,
  342. errorMsg: "password requires at least 1 digit(s)",
  343. },
  344. {
  345. name: "dcredit negative - pass with digit",
  346. config: &Config{Dcredit: -1},
  347. password: "pass1word",
  348. wantError: false,
  349. },
  350. {
  351. name: "dcredit negative - require 2 digits",
  352. config: &Config{Dcredit: -2},
  353. password: "pass1word",
  354. wantError: true,
  355. errorMsg: "password requires at least 2 digit(s)",
  356. },
  357. {
  358. name: "dcredit negative - pass with 2 digits",
  359. config: &Config{Dcredit: -2},
  360. password: "pass12word",
  361. wantError: false,
  362. },
  363. {
  364. name: "ucredit negative - require uppercase",
  365. config: &Config{Ucredit: -1},
  366. password: "nouppercase",
  367. wantError: true,
  368. errorMsg: "password requires at least 1 uppercase letter(s)",
  369. },
  370. {
  371. name: "ucredit negative - pass with uppercase",
  372. config: &Config{Ucredit: -1},
  373. password: "passWord",
  374. wantError: false,
  375. },
  376. {
  377. name: "lcredit negative - require lowercase",
  378. config: &Config{Lcredit: -1},
  379. password: "NOLOWERCASE",
  380. wantError: true,
  381. errorMsg: "password requires at least 1 lowercase letter(s)",
  382. },
  383. {
  384. name: "lcredit negative - pass with lowercase",
  385. config: &Config{Lcredit: -1},
  386. password: "PASSWORDw",
  387. wantError: false,
  388. },
  389. {
  390. name: "ocredit negative - require special char",
  391. config: &Config{Ocredit: -1},
  392. password: "nospecialchar",
  393. wantError: true,
  394. errorMsg: "password requires at least 1 special character(s)",
  395. },
  396. {
  397. name: "ocredit negative - pass with special char",
  398. config: &Config{Ocredit: -1},
  399. password: "password@",
  400. wantError: false,
  401. },
  402. {
  403. name: "minclass - require 3 classes",
  404. config: &Config{Minclass: 3},
  405. password: "onlylowercase",
  406. wantError: true,
  407. errorMsg: "requires at least 3 character class(es)",
  408. },
  409. {
  410. name: "minclass - pass with 3 classes",
  411. config: &Config{Minclass: 3},
  412. password: "Pass1word",
  413. wantError: false,
  414. },
  415. {
  416. name: "minclass - pass with 4 classes",
  417. config: &Config{Minclass: 3},
  418. password: "Pass1@word",
  419. wantError: false,
  420. },
  421. {
  422. name: "complex config - all requirements",
  423. config: &Config{
  424. Minlen: 8,
  425. Dcredit: -1,
  426. Ucredit: -1,
  427. Lcredit: -1,
  428. Ocredit: -1,
  429. Minclass: 3,
  430. },
  431. password: "Pass1@word",
  432. wantError: false,
  433. },
  434. {
  435. name: "complex config - missing digit",
  436. config: &Config{
  437. Minlen: 8,
  438. Dcredit: -1,
  439. Ucredit: -1,
  440. Lcredit: -1,
  441. Ocredit: -1,
  442. Minclass: 3,
  443. },
  444. password: "Pass@word",
  445. wantError: true,
  446. errorMsg: "password requires at least 1 digit(s)",
  447. },
  448. {
  449. name: "complex config - missing uppercase",
  450. config: &Config{
  451. Minlen: 8,
  452. Dcredit: -1,
  453. Ucredit: -1,
  454. Lcredit: -1,
  455. Ocredit: -1,
  456. Minclass: 3,
  457. },
  458. password: "pass1@word",
  459. wantError: true,
  460. errorMsg: "password requires at least 1 uppercase letter(s)",
  461. },
  462. {
  463. name: "complex config - missing lowercase",
  464. config: &Config{
  465. Minlen: 8,
  466. Dcredit: -1,
  467. Ucredit: -1,
  468. Lcredit: -1,
  469. Ocredit: -1,
  470. Minclass: 3,
  471. },
  472. password: "PASS1@WORD",
  473. wantError: true,
  474. errorMsg: "password requires at least 1 lowercase letter(s)",
  475. },
  476. {
  477. name: "complex config - missing special char",
  478. config: &Config{
  479. Minlen: 8,
  480. Dcredit: -1,
  481. Ucredit: -1,
  482. Lcredit: -1,
  483. Ocredit: -1,
  484. Minclass: 3,
  485. },
  486. password: "Pass1word",
  487. wantError: true,
  488. errorMsg: "password requires at least 1 special character(s)",
  489. },
  490. {
  491. name: "complex config - too short",
  492. config: &Config{
  493. Minlen: 12,
  494. Dcredit: -1,
  495. Ucredit: -1,
  496. Lcredit: -1,
  497. Ocredit: -1,
  498. Minclass: 3,
  499. },
  500. password: "Pass1@wor",
  501. wantError: true,
  502. errorMsg: "effective length",
  503. },
  504. {
  505. name: "positive credit - dcredit",
  506. config: &Config{
  507. Minlen: 8,
  508. Dcredit: 1,
  509. },
  510. password: "password",
  511. wantError: true,
  512. errorMsg: "password should contain at least one digit",
  513. },
  514. {
  515. name: "positive credit - pass with digit",
  516. config: &Config{
  517. Minlen: 8,
  518. Dcredit: 1,
  519. },
  520. password: "pass1word",
  521. wantError: false,
  522. },
  523. {
  524. name: "real world example - strong password",
  525. config: &Config{
  526. Minlen: 8,
  527. Dcredit: -1,
  528. Ucredit: -1,
  529. Lcredit: -1,
  530. Ocredit: -1,
  531. Minclass: 3,
  532. },
  533. password: "MyP@ssw0rd",
  534. wantError: false,
  535. },
  536. {
  537. name: "real world example - weak password",
  538. config: &Config{
  539. Minlen: 8,
  540. Dcredit: -1,
  541. Ucredit: -1,
  542. Lcredit: -1,
  543. Ocredit: -1,
  544. Minclass: 3,
  545. },
  546. password: "password",
  547. wantError: true,
  548. },
  549. {
  550. name: "maxrepeat - too many repeated characters",
  551. config: &Config{
  552. Minlen: 8,
  553. Maxrepeat: 2,
  554. },
  555. password: "Passaaa1",
  556. wantError: true,
  557. errorMsg: "more than 2 consecutive repeated characters",
  558. },
  559. {
  560. name: "maxrepeat - pass with repeated characters within limit",
  561. config: &Config{
  562. Minlen: 8,
  563. Maxrepeat: 3,
  564. },
  565. password: "Passaa12",
  566. wantError: false,
  567. },
  568. {
  569. name: "maxclassrepeat - too many consecutive digits",
  570. config: &Config{
  571. Minlen: 8,
  572. Maxclassrepeat: 2,
  573. },
  574. password: "Pass1111",
  575. wantError: true,
  576. errorMsg: "more than 2 consecutive characters of the same class",
  577. },
  578. {
  579. name: "maxclassrepeat - pass with consecutive digits within limit",
  580. config: &Config{
  581. Minlen: 8,
  582. Maxclassrepeat: 3,
  583. },
  584. password: "Pass111@",
  585. wantError: false,
  586. },
  587. {
  588. name: "maxsequence - ascending sequence too long",
  589. config: &Config{
  590. Minlen: 8,
  591. Maxsequence: 3,
  592. },
  593. password: "Pass1234",
  594. wantError: true,
  595. errorMsg: "sequence of more than 3 consecutive characters",
  596. },
  597. {
  598. name: "maxsequence - descending sequence too long",
  599. config: &Config{
  600. Minlen: 8,
  601. Maxsequence: 3,
  602. },
  603. password: "Pass4321",
  604. wantError: true,
  605. errorMsg: "sequence of more than 3 consecutive characters",
  606. },
  607. {
  608. name: "maxsequence - pass with sequence within limit",
  609. config: &Config{
  610. Minlen: 8,
  611. Maxsequence: 3,
  612. },
  613. password: "Pass123@",
  614. wantError: false,
  615. },
  616. {
  617. name: "maxsequence - pass with no sequence",
  618. config: &Config{
  619. Minlen: 8,
  620. Maxsequence: 3,
  621. },
  622. password: "Pass1@word",
  623. wantError: false,
  624. },
  625. {
  626. name: "complex config with all new options",
  627. config: &Config{
  628. Minlen: 8,
  629. Dcredit: -1,
  630. Ucredit: -1,
  631. Lcredit: -1,
  632. Ocredit: -1,
  633. Maxrepeat: 2,
  634. Maxclassrepeat: 2,
  635. Maxsequence: 3,
  636. },
  637. password: "P1@w0rD2",
  638. wantError: false,
  639. },
  640. {
  641. name: "complex config - fails maxrepeat",
  642. config: &Config{
  643. Minlen: 8,
  644. Maxrepeat: 2,
  645. },
  646. password: "Passaaa1",
  647. wantError: true,
  648. errorMsg: "more than 2 consecutive repeated characters",
  649. },
  650. {
  651. name: "empty password",
  652. config: &Config{
  653. Minlen: 8,
  654. },
  655. password: "",
  656. wantError: true,
  657. errorMsg: "effective length",
  658. },
  659. {
  660. name: "maxrepeat boundary - exactly at limit",
  661. config: &Config{
  662. Minlen: 8,
  663. Maxrepeat: 3,
  664. },
  665. password: "Passaaa1",
  666. wantError: false, // 3个重复字符,正好在限制内
  667. },
  668. {
  669. name: "maxrepeat boundary - one over limit",
  670. config: &Config{
  671. Minlen: 8,
  672. Maxrepeat: 2,
  673. },
  674. password: "Passaaa1",
  675. wantError: true,
  676. errorMsg: "more than 2 consecutive repeated characters",
  677. },
  678. {
  679. name: "maxclassrepeat boundary - exactly at limit",
  680. config: &Config{
  681. Minlen: 8,
  682. Maxclassrepeat: 3,
  683. },
  684. password: "Pass111@",
  685. wantError: false, // 3个连续数字,正好在限制内
  686. },
  687. {
  688. name: "maxsequence boundary - exactly at limit",
  689. config: &Config{
  690. Minlen: 8,
  691. Maxsequence: 3,
  692. },
  693. password: "Pass123@",
  694. wantError: false, // 3个连续字符,正好在限制内
  695. },
  696. {
  697. name: "maxsequence boundary - one over limit",
  698. config: &Config{
  699. Minlen: 8,
  700. Maxsequence: 2,
  701. },
  702. password: "Pass123@",
  703. wantError: true,
  704. errorMsg: "sequence of more than 2 consecutive characters",
  705. },
  706. {
  707. name: "positive credit - effective length calculation",
  708. config: &Config{
  709. Minlen: 10,
  710. Dcredit: 2, // 每个数字可以减少2个长度要求
  711. },
  712. password: "Pass123", // 7个字符 + 3个数字*2 = 13,应该通过
  713. wantError: false,
  714. },
  715. {
  716. name: "positive credit - effective length too short",
  717. config: &Config{
  718. Minlen: 10,
  719. Dcredit: 1, // 每个数字可以减少1个长度要求
  720. },
  721. password: "Pass12", // 6个字符 + 2个数字*1 = 8,小于10,应该失败
  722. wantError: true,
  723. errorMsg: "effective length",
  724. },
  725. {
  726. name: "maxsequence - ascending at start",
  727. config: &Config{
  728. Minlen: 8,
  729. Maxsequence: 3,
  730. },
  731. password: "123Pass@",
  732. wantError: false, // 3个连续字符在开头,正好在限制内
  733. },
  734. {
  735. name: "maxsequence - descending at end",
  736. config: &Config{
  737. Minlen: 8,
  738. Maxsequence: 3,
  739. },
  740. password: "Pass@321",
  741. wantError: false, // 3个连续字符在结尾,正好在限制内
  742. },
  743. {
  744. name: "positive credit - ucredit",
  745. config: &Config{
  746. Minlen: 8,
  747. Ucredit: 1,
  748. },
  749. password: "password",
  750. wantError: true,
  751. errorMsg: "password should contain at least one uppercase letter",
  752. },
  753. {
  754. name: "positive credit - ucredit pass",
  755. config: &Config{
  756. Minlen: 8,
  757. Ucredit: 1,
  758. },
  759. password: "passWord",
  760. wantError: false,
  761. },
  762. {
  763. name: "positive credit - lcredit",
  764. config: &Config{
  765. Minlen: 8,
  766. Lcredit: 1,
  767. },
  768. password: "PASSWORD",
  769. wantError: true,
  770. errorMsg: "password should contain at least one lowercase letter",
  771. },
  772. {
  773. name: "positive credit - lcredit pass",
  774. config: &Config{
  775. Minlen: 8,
  776. Lcredit: 1,
  777. },
  778. password: "PASSWORDw",
  779. wantError: false,
  780. },
  781. {
  782. name: "positive credit - ocredit",
  783. config: &Config{
  784. Minlen: 8,
  785. Ocredit: 1,
  786. },
  787. password: "password",
  788. wantError: true,
  789. errorMsg: "password should contain at least one special character",
  790. },
  791. {
  792. name: "positive credit - ocredit pass",
  793. config: &Config{
  794. Minlen: 8,
  795. Ocredit: 1,
  796. },
  797. password: "password@",
  798. wantError: false,
  799. },
  800. {
  801. name: "positive credit - multiple credits effective length",
  802. config: &Config{
  803. Minlen: 10,
  804. Dcredit: 2,
  805. Ucredit: 1,
  806. Lcredit: 1,
  807. Ocredit: 1,
  808. },
  809. password: "Pass123@", // 8 + 3*2 + 1*1 + 3*1 + 1*1 = 8+6+1+3+1 = 19,应该通过
  810. wantError: false,
  811. },
  812. {
  813. name: "maxrepeat - single character password",
  814. config: &Config{
  815. Minlen: 1,
  816. Maxrepeat: 2,
  817. },
  818. password: "a",
  819. wantError: false, // 单个字符,没有重复
  820. },
  821. {
  822. name: "maxrepeat - no repeated characters",
  823. config: &Config{
  824. Minlen: 8,
  825. Maxrepeat: 2,
  826. },
  827. password: "Passw0rd",
  828. wantError: false, // 没有重复字符
  829. },
  830. {
  831. name: "maxclassrepeat - single character",
  832. config: &Config{
  833. Minlen: 1,
  834. Maxclassrepeat: 2,
  835. },
  836. password: "1",
  837. wantError: false, // 单个字符,没有同类重复
  838. },
  839. {
  840. name: "maxclassrepeat - no consecutive same class",
  841. config: &Config{
  842. Minlen: 8,
  843. Maxclassrepeat: 2,
  844. },
  845. password: "P1a2s3w4",
  846. wantError: false, // 没有连续同类字符
  847. },
  848. {
  849. name: "maxsequence - short password less than maxsequence",
  850. config: &Config{
  851. Minlen: 2,
  852. Maxsequence: 3,
  853. },
  854. password: "12", // 长度小于 maxsequence+1,不会触发检查
  855. wantError: false,
  856. },
  857. {
  858. name: "maxsequence - exactly maxsequence+1 length with sequence",
  859. config: &Config{
  860. Minlen: 4,
  861. Maxsequence: 3,
  862. },
  863. password: "1234", // 正好4个字符,包含4个连续字符序列
  864. wantError: true,
  865. errorMsg: "sequence of more than 3 consecutive characters",
  866. },
  867. {
  868. name: "maxsequence - ascending sequence in middle",
  869. config: &Config{
  870. Minlen: 8,
  871. Maxsequence: 2,
  872. },
  873. password: "Pa123ss@",
  874. wantError: true,
  875. errorMsg: "sequence of more than 2 consecutive characters",
  876. },
  877. {
  878. name: "maxsequence - descending sequence in middle",
  879. config: &Config{
  880. Minlen: 8,
  881. Maxsequence: 2,
  882. },
  883. password: "Pa321ss@",
  884. wantError: true,
  885. errorMsg: "sequence of more than 2 consecutive characters",
  886. },
  887. {
  888. name: "maxsequence - mixed ascending and descending",
  889. config: &Config{
  890. Minlen: 8,
  891. Maxsequence: 3,
  892. },
  893. password: "Pass1234@", // 包含4个连续字符序列
  894. wantError: true,
  895. errorMsg: "sequence of more than 3 consecutive characters",
  896. },
  897. {
  898. name: "minclass - exactly required classes",
  899. config: &Config{
  900. Minlen: 8,
  901. Minclass: 2,
  902. },
  903. password: "Password", // 只有大写和小写,2个类
  904. wantError: false,
  905. },
  906. {
  907. name: "minclass - one class short",
  908. config: &Config{
  909. Minlen: 8,
  910. Minclass: 2,
  911. },
  912. password: "password", // 只有小写,1个类
  913. wantError: true,
  914. errorMsg: "requires at least 2 character class(es)",
  915. },
  916. {
  917. name: "maxrepeat - exactly at limit with multiple repeats",
  918. config: &Config{
  919. Minlen: 7,
  920. Maxrepeat: 2,
  921. },
  922. password: "Passaa1", // 2个a重复,正好在限制内
  923. wantError: false,
  924. },
  925. {
  926. name: "maxclassrepeat - exactly at limit",
  927. config: &Config{
  928. Minlen: 7,
  929. Maxclassrepeat: 3, // 允许3个连续同类字符
  930. },
  931. password: "Pass111@", // 3个连续数字,正好在限制内
  932. wantError: false,
  933. },
  934. {
  935. name: "maxclassrepeat - different classes",
  936. config: &Config{
  937. Minlen: 8,
  938. Maxclassrepeat: 3, // 允许3个连续同类字符
  939. },
  940. password: "PassAA11", // AA和11都是2个连续同类字符,都在限制内
  941. wantError: false,
  942. },
  943. {
  944. name: "maxclassrepeat - uppercase consecutive",
  945. config: &Config{
  946. Minlen: 8,
  947. Maxclassrepeat: 2,
  948. },
  949. password: "PassAAA1",
  950. wantError: true,
  951. errorMsg: "more than 2 consecutive characters of the same class",
  952. },
  953. {
  954. name: "maxclassrepeat - lowercase consecutive",
  955. config: &Config{
  956. Minlen: 8,
  957. Maxclassrepeat: 2,
  958. },
  959. password: "Paaass1@",
  960. wantError: true,
  961. errorMsg: "more than 2 consecutive characters of the same class",
  962. },
  963. {
  964. name: "maxclassrepeat - special consecutive",
  965. config: &Config{
  966. Minlen: 8,
  967. Maxclassrepeat: 2,
  968. },
  969. password: "Pass1@@@",
  970. wantError: true,
  971. errorMsg: "more than 2 consecutive characters of the same class",
  972. },
  973. {
  974. name: "usercheck - password contains username",
  975. config: &Config{
  976. Minlen: 8,
  977. Usercheck: 1,
  978. },
  979. password: "user1234", // 密码包含用户名 "user"
  980. wantError: true,
  981. errorMsg: "password contains the username",
  982. },
  983. {
  984. name: "usercheck - password contains reversed username",
  985. config: &Config{
  986. Minlen: 8,
  987. Usercheck: 1,
  988. },
  989. password: "resu1234", // 密码包含反向用户名 "resu" (user 的反向)
  990. wantError: true,
  991. errorMsg: "password contains the reversed username",
  992. },
  993. {
  994. name: "usercheck - password does not contain username",
  995. config: &Config{
  996. Minlen: 8,
  997. Usercheck: 1,
  998. },
  999. password: "Pass1234", // 密码不包含用户名
  1000. wantError: false,
  1001. },
  1002. {
  1003. name: "usercheck - case insensitive",
  1004. config: &Config{
  1005. Minlen: 8,
  1006. Usercheck: 1,
  1007. },
  1008. password: "USER1234", // 密码包含大写用户名
  1009. wantError: true,
  1010. errorMsg: "password contains the username",
  1011. },
  1012. {
  1013. name: "usercheck - disabled",
  1014. config: &Config{
  1015. Minlen: 8,
  1016. Usercheck: 0, // 不检查用户名
  1017. },
  1018. password: "user1234", // 即使密码包含用户名,也不应该报错
  1019. wantError: false,
  1020. },
  1021. {
  1022. name: "usercheck - empty username",
  1023. config: &Config{
  1024. Minlen: 8,
  1025. Usercheck: 1,
  1026. },
  1027. password: "anypassword", // 用户名为空,不应该检查
  1028. wantError: false,
  1029. },
  1030. {
  1031. name: "root user with enforce_for_root = 1 - should validate",
  1032. config: &Config{
  1033. Minlen: 8,
  1034. EnforceForRoot: 1, // 对 root 强制执行
  1035. Enforcing: 1, // 强制执行
  1036. },
  1037. password: "short", // root 用户也需要验证密码强度
  1038. wantError: true,
  1039. errorMsg: "effective length",
  1040. },
  1041. {
  1042. name: "non-root user with enforce_for_root = 0 - should validate",
  1043. config: &Config{
  1044. Minlen: 8,
  1045. EnforceForRoot: 0, // 不对 root 强制执行,但普通用户需要验证
  1046. Enforcing: 1, // 强制执行(对普通用户)
  1047. },
  1048. password: "short", // 普通用户需要验证密码强度
  1049. wantError: true,
  1050. errorMsg: "effective length",
  1051. },
  1052. }
  1053. for _, tt := range tests {
  1054. t.Run(tt.name, func(t *testing.T) {
  1055. // 对于 usercheck 测试,使用 "user" 作为用户名
  1056. // 对于 root 用户测试,使用 "root" 作为用户名
  1057. // 对于 non-root 用户测试,使用 "testuser" 作为用户名
  1058. // 注意:需要先检查 "non-root user",因为 "non-root user" 包含 "root user"
  1059. username := ""
  1060. if strings.Contains(tt.name, "usercheck") {
  1061. username = "user"
  1062. } else if strings.Contains(tt.name, "non-root user") {
  1063. username = "testuser"
  1064. } else if strings.Contains(tt.name, "root user") {
  1065. username = "root"
  1066. }
  1067. // 如果 config 不为 nil 且未设置 Enforcing,默认设置为 1(强制执行)
  1068. // 但如果是 enforcing=0 的测试用例,不要修改
  1069. // 对于 root 用户测试,如果 enforce_for_root=0,enforcing 可能为 0,不要修改
  1070. // 对于 non-root 用户测试,如果已经设置了 Enforcing=1,不要修改
  1071. // 注意:只对 Enforcing=0 的测试用例进行修改,且排除特殊测试用例
  1072. if tt.config != nil && tt.config.Enforcing == 0 && tt.name != "nil config should pass" &&
  1073. !strings.Contains(tt.name, "enforcing = 0") &&
  1074. !strings.Contains(tt.name, "root user with enforce_for_root = 0") &&
  1075. !strings.Contains(tt.name, "non-root user") {
  1076. // 只对非特殊测试用例且 Enforcing=0 的情况设置为 1
  1077. tt.config.Enforcing = 1
  1078. }
  1079. err := tt.config.Validate(tt.password, username)
  1080. if tt.wantError {
  1081. if err == nil {
  1082. t.Errorf("Validate() expected error but got nil")
  1083. } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
  1084. t.Errorf("Validate() error = %v, want error containing %q", err, tt.errorMsg)
  1085. }
  1086. } else {
  1087. if err != nil {
  1088. t.Errorf("Validate() unexpected error = %v", err)
  1089. }
  1090. }
  1091. })
  1092. }
  1093. }
  1094. func TestConfig_Validate_CharacterClasses(t *testing.T) {
  1095. config := &Config{
  1096. Minclass: 4,
  1097. Enforcing: 1, // 强制执行
  1098. }
  1099. tests := []struct {
  1100. name string
  1101. password string
  1102. wantError bool
  1103. }{
  1104. {"all 4 classes", "Pass1@word", false},
  1105. {"3 classes - missing special", "Pass1word", true},
  1106. {"3 classes - missing digit", "Pass@word", true},
  1107. {"3 classes - missing uppercase", "pass1@word", true},
  1108. {"3 classes - missing lowercase", "PASS1@WORD", true},
  1109. {"2 classes", "password", true},
  1110. {"1 class", "PASSWORD", true},
  1111. }
  1112. for _, tt := range tests {
  1113. t.Run(tt.name, func(t *testing.T) {
  1114. err := config.Validate(tt.password, "")
  1115. if tt.wantError {
  1116. if err == nil {
  1117. t.Errorf("Validate() expected error but got nil for password %q", tt.password)
  1118. }
  1119. } else {
  1120. if err != nil {
  1121. t.Errorf("Validate() unexpected error = %v for password %q", err, tt.password)
  1122. }
  1123. }
  1124. })
  1125. }
  1126. }
  1127. func TestParsePAMConfig(t *testing.T) {
  1128. tests := []struct {
  1129. name string
  1130. content string
  1131. expected *Config
  1132. }{
  1133. {
  1134. name: "pam_pwquality config",
  1135. content: `# PAM configuration
  1136. auth required pam_unix.so
  1137. password requisite pam_pwquality.so retry=3 minlen=8 dcredit=-1 ucredit=-1 lcredit=-1 ocredit=-1 minclass=3
  1138. password required pam_unix.so sha512 shadow nullok try_first_pass use_authtok`,
  1139. expected: &Config{
  1140. Minlen: 8,
  1141. Dcredit: -1,
  1142. Ucredit: -1,
  1143. Lcredit: -1,
  1144. Ocredit: -1,
  1145. Minclass: 3,
  1146. },
  1147. },
  1148. {
  1149. name: "pam_cracklib config",
  1150. content: `# PAM configuration
  1151. password requisite pam_cracklib.so retry=3 minlen=12 dcredit=-2 ucredit=-1 lcredit=-1 ocredit=-1
  1152. password required pam_unix.so`,
  1153. expected: &Config{
  1154. Minlen: 12,
  1155. Dcredit: -2,
  1156. Ucredit: -1,
  1157. Lcredit: -1,
  1158. Ocredit: -1,
  1159. Minclass: 0,
  1160. },
  1161. },
  1162. {
  1163. name: "PAM config with comments",
  1164. content: `# This is a comment
  1165. password requisite pam_pwquality.so minlen=10 dcredit=-1
  1166. # Another comment`,
  1167. expected: &Config{
  1168. Minlen: 10,
  1169. Dcredit: -1,
  1170. Ucredit: 0,
  1171. Lcredit: 0,
  1172. Ocredit: 0,
  1173. Minclass: 0,
  1174. },
  1175. },
  1176. {
  1177. name: "PAM config without password module",
  1178. content: `# No password module
  1179. auth required pam_unix.so`,
  1180. expected: &Config{
  1181. Minlen: 0,
  1182. Dcredit: 0,
  1183. Ucredit: 0,
  1184. Lcredit: 0,
  1185. Ocredit: 0,
  1186. Minclass: 0,
  1187. },
  1188. },
  1189. {
  1190. name: "multiple password lines - use first",
  1191. content: `password requisite pam_pwquality.so minlen=8 dcredit=-1
  1192. password required pam_unix.so
  1193. password optional pam_gnome_keyring.so`,
  1194. expected: &Config{
  1195. Minlen: 8,
  1196. Dcredit: -1,
  1197. Ucredit: 0,
  1198. Lcredit: 0,
  1199. Ocredit: 0,
  1200. Minclass: 0,
  1201. },
  1202. },
  1203. {
  1204. name: "PAM config with new options",
  1205. content: `password requisite pam_pwquality.so minlen=8 maxrepeat=3 maxclassrepeat=2 maxsequence=3`,
  1206. expected: &Config{
  1207. Minlen: 8,
  1208. Maxrepeat: 3,
  1209. Maxclassrepeat: 2,
  1210. Maxsequence: 3,
  1211. },
  1212. },
  1213. {
  1214. name: "PAM config with invalid value",
  1215. content: `password requisite pam_pwquality.so minlen=abc dcredit=-1`,
  1216. expected: &Config{
  1217. Minlen: 0, // 无效值应该被忽略
  1218. Dcredit: -1,
  1219. },
  1220. },
  1221. {
  1222. name: "PAM config - password line without pam module",
  1223. content: `password required pam_unix.so
  1224. auth required pam_unix.so`,
  1225. expected: &Config{
  1226. Minlen: 0,
  1227. Dcredit: 0,
  1228. Ucredit: 0,
  1229. Lcredit: 0,
  1230. Ocredit: 0,
  1231. Minclass: 0,
  1232. },
  1233. },
  1234. {
  1235. name: "PAM config - pam module without password",
  1236. content: `auth required pam_pwquality.so minlen=8
  1237. account required pam_unix.so`,
  1238. expected: &Config{
  1239. Minlen: 0, // 不是 password 行,应该被忽略
  1240. Dcredit: 0,
  1241. },
  1242. },
  1243. {
  1244. name: "PAM config - parameter without equals",
  1245. content: `password requisite pam_pwquality.so minlen dcredit=-1`,
  1246. expected: &Config{
  1247. Minlen: 0, // 没有等号的参数应该被忽略
  1248. Dcredit: -1,
  1249. },
  1250. },
  1251. {
  1252. name: "PAM config - multiple equals in parameter",
  1253. content: `password requisite pam_pwquality.so minlen=8=10 dcredit=-1`,
  1254. expected: &Config{
  1255. Minlen: 0, // "8=10" 不是有效数字,会被忽略
  1256. Dcredit: -1,
  1257. },
  1258. },
  1259. {
  1260. name: "PAM config with enforcing",
  1261. content: `password requisite pam_pwquality.so minlen=8 enforcing=1`,
  1262. expected: &Config{
  1263. Minlen: 8,
  1264. Enforcing: 1,
  1265. },
  1266. },
  1267. {
  1268. name: "PAM config with enforce_for_root",
  1269. content: `password requisite pam_pwquality.so minlen=8 enforce_for_root=1`,
  1270. expected: &Config{
  1271. Minlen: 8,
  1272. EnforceForRoot: 1,
  1273. },
  1274. },
  1275. {
  1276. name: "PAM config with both enforcing parameters",
  1277. content: `password requisite pam_pwquality.so minlen=8 enforcing=0 enforce_for_root=1`,
  1278. expected: &Config{
  1279. Minlen: 8,
  1280. Enforcing: 0,
  1281. EnforceForRoot: 1,
  1282. },
  1283. },
  1284. {
  1285. name: "PAM config with enforce_for_root as flag (no value)",
  1286. content: `password requisite pam_pwquality.so minlen=8 enforce_for_root`,
  1287. expected: &Config{
  1288. Minlen: 8,
  1289. EnforceForRoot: 1, // 独立标志形式,应设置为 1
  1290. },
  1291. },
  1292. {
  1293. name: "PAM config with usercheck",
  1294. content: `password requisite pam_pwquality.so minlen=8 usercheck=1`,
  1295. expected: &Config{
  1296. Minlen: 8,
  1297. Usercheck: 1,
  1298. },
  1299. },
  1300. }
  1301. for _, tt := range tests {
  1302. t.Run(tt.name, func(t *testing.T) {
  1303. config := ParsePAMConfig([]byte(tt.content), nil)
  1304. if config.Minlen != tt.expected.Minlen {
  1305. t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen)
  1306. }
  1307. if config.Dcredit != tt.expected.Dcredit {
  1308. t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit)
  1309. }
  1310. if config.Ucredit != tt.expected.Ucredit {
  1311. t.Errorf("Ucredit = %d, want %d", config.Ucredit, tt.expected.Ucredit)
  1312. }
  1313. if config.Lcredit != tt.expected.Lcredit {
  1314. t.Errorf("Lcredit = %d, want %d", config.Lcredit, tt.expected.Lcredit)
  1315. }
  1316. if config.Ocredit != tt.expected.Ocredit {
  1317. t.Errorf("Ocredit = %d, want %d", config.Ocredit, tt.expected.Ocredit)
  1318. }
  1319. if config.Minclass != tt.expected.Minclass {
  1320. t.Errorf("Minclass = %d, want %d", config.Minclass, tt.expected.Minclass)
  1321. }
  1322. if config.Maxrepeat != tt.expected.Maxrepeat {
  1323. t.Errorf("Maxrepeat = %d, want %d", config.Maxrepeat, tt.expected.Maxrepeat)
  1324. }
  1325. if config.Maxclassrepeat != tt.expected.Maxclassrepeat {
  1326. t.Errorf("Maxclassrepeat = %d, want %d", config.Maxclassrepeat, tt.expected.Maxclassrepeat)
  1327. }
  1328. if config.Maxsequence != tt.expected.Maxsequence {
  1329. t.Errorf("Maxsequence = %d, want %d", config.Maxsequence, tt.expected.Maxsequence)
  1330. }
  1331. })
  1332. }
  1333. }
  1334. func TestConfig_HasAnyPolicy(t *testing.T) {
  1335. tests := []struct {
  1336. name string
  1337. config *Config
  1338. expected bool
  1339. }{
  1340. {
  1341. name: "nil config",
  1342. config: nil,
  1343. expected: false,
  1344. },
  1345. {
  1346. name: "empty config",
  1347. config: &Config{},
  1348. expected: false,
  1349. },
  1350. {
  1351. name: "only minlen",
  1352. config: &Config{Minlen: 8},
  1353. expected: true,
  1354. },
  1355. {
  1356. name: "only dcredit",
  1357. config: &Config{Dcredit: -1},
  1358. expected: true,
  1359. },
  1360. {
  1361. name: "only ucredit",
  1362. config: &Config{Ucredit: -1},
  1363. expected: true,
  1364. },
  1365. {
  1366. name: "only lcredit",
  1367. config: &Config{Lcredit: -1},
  1368. expected: true,
  1369. },
  1370. {
  1371. name: "only ocredit",
  1372. config: &Config{Ocredit: -1},
  1373. expected: true,
  1374. },
  1375. {
  1376. name: "only minclass",
  1377. config: &Config{Minclass: 3},
  1378. expected: true,
  1379. },
  1380. {
  1381. name: "all fields set",
  1382. config: &Config{Minlen: 8, Dcredit: -1, Ucredit: -1, Lcredit: -1, Ocredit: -1, Minclass: 3},
  1383. expected: true,
  1384. },
  1385. {
  1386. name: "only lcredit and ocredit",
  1387. config: &Config{Lcredit: -1, Ocredit: -1},
  1388. expected: true,
  1389. },
  1390. {
  1391. name: "only maxrepeat",
  1392. config: &Config{Maxrepeat: 3},
  1393. expected: true,
  1394. },
  1395. {
  1396. name: "only maxclassrepeat",
  1397. config: &Config{Maxclassrepeat: 2},
  1398. expected: true,
  1399. },
  1400. {
  1401. name: "only maxsequence",
  1402. config: &Config{Maxsequence: 3},
  1403. expected: true,
  1404. },
  1405. {
  1406. name: "all new options",
  1407. config: &Config{Maxrepeat: 3, Maxclassrepeat: 2, Maxsequence: 3},
  1408. expected: true,
  1409. },
  1410. }
  1411. for _, tt := range tests {
  1412. t.Run(tt.name, func(t *testing.T) {
  1413. got := tt.config.HasAnyPolicy()
  1414. if got != tt.expected {
  1415. t.Errorf("HasAnyPolicy() = %v, want %v", got, tt.expected)
  1416. }
  1417. })
  1418. }
  1419. }
  1420. func TestConfig_IsEnforcing(t *testing.T) {
  1421. tests := []struct {
  1422. name string
  1423. config *Config
  1424. expected bool
  1425. }{
  1426. {
  1427. name: "nil config - default enforcing",
  1428. config: nil,
  1429. expected: true, // 默认强制执行
  1430. },
  1431. {
  1432. name: "enforcing = 1",
  1433. config: &Config{Enforcing: 1},
  1434. expected: true,
  1435. },
  1436. {
  1437. name: "enforcing = 0",
  1438. config: &Config{Enforcing: 0},
  1439. expected: false,
  1440. },
  1441. {
  1442. name: "enforcing = 2",
  1443. config: &Config{Enforcing: 2},
  1444. expected: true, // 非0值都视为强制执行
  1445. },
  1446. {
  1447. name: "default config - enforcing not set",
  1448. config: &Config{Enforcing: 1}, // 默认值为1
  1449. expected: true, // 默认值为1,强制执行
  1450. },
  1451. }
  1452. for _, tt := range tests {
  1453. t.Run(tt.name, func(t *testing.T) {
  1454. got := tt.config.IsEnforcing()
  1455. if got != tt.expected {
  1456. t.Errorf("IsEnforcing() = %v, want %v", got, tt.expected)
  1457. }
  1458. })
  1459. }
  1460. }
  1461. func TestConfig_IsEnforcingForRoot(t *testing.T) {
  1462. tests := []struct {
  1463. name string
  1464. config *Config
  1465. expected bool
  1466. }{
  1467. {
  1468. name: "nil config - default not enforcing for root",
  1469. config: nil,
  1470. expected: false, // 默认不对 root 强制执行
  1471. },
  1472. {
  1473. name: "enforce_for_root = 1",
  1474. config: &Config{EnforceForRoot: 1},
  1475. expected: true,
  1476. },
  1477. {
  1478. name: "enforce_for_root = 0",
  1479. config: &Config{EnforceForRoot: 0},
  1480. expected: false,
  1481. },
  1482. {
  1483. name: "enforce_for_root = 2",
  1484. config: &Config{EnforceForRoot: 2},
  1485. expected: false, // 只有1才表示强制执行
  1486. },
  1487. {
  1488. name: "default config - enforce_for_root not set",
  1489. config: &Config{},
  1490. expected: false, // 默认值为0,不对 root 强制执行
  1491. },
  1492. }
  1493. for _, tt := range tests {
  1494. t.Run(tt.name, func(t *testing.T) {
  1495. got := tt.config.IsEnforcingForRoot()
  1496. if got != tt.expected {
  1497. t.Errorf("IsEnforcingForRoot() = %v, want %v", got, tt.expected)
  1498. }
  1499. })
  1500. }
  1501. }
  1502. func TestConfig_GeneratePassword(t *testing.T) {
  1503. tests := []struct {
  1504. name string
  1505. config *Config
  1506. passwordGenerator func(int) string
  1507. wantError bool // 生成的密码是否应该通过验证
  1508. description string
  1509. }{
  1510. {
  1511. name: "nil config - should return empty or default password",
  1512. config: nil,
  1513. passwordGenerator: func(length int) string {
  1514. return "defaultpassword"
  1515. },
  1516. wantError: false,
  1517. description: "nil 配置应该返回默认长度的密码",
  1518. },
  1519. {
  1520. name: "no policy config - should return default password",
  1521. config: &Config{},
  1522. passwordGenerator: func(length int) string {
  1523. return "defaultpassword"
  1524. },
  1525. wantError: false,
  1526. description: "没有策略的配置应该返回默认长度的密码",
  1527. },
  1528. {
  1529. name: "minlen only - generate valid password",
  1530. config: &Config{
  1531. Minlen: 8,
  1532. },
  1533. passwordGenerator: func(length int) string {
  1534. // 第一次返回不符合要求的密码,第二次返回符合要求的密码
  1535. if length == 8 {
  1536. return "short" // 太短
  1537. }
  1538. return "longpassword" // 符合要求
  1539. },
  1540. wantError: false,
  1541. description: "只有最小长度要求,应该生成符合要求的密码",
  1542. },
  1543. {
  1544. name: "dcredit requirement - generate password with digits",
  1545. config: &Config{
  1546. Minlen: 8,
  1547. Dcredit: -1, // 至少需要1个数字
  1548. },
  1549. passwordGenerator: func(length int) string {
  1550. // 第一次返回没有数字的密码,第二次返回有数字的密码
  1551. if length == 8 {
  1552. return "nodigits" // 没有数字
  1553. }
  1554. return "pass1word" // 有数字,符合要求
  1555. },
  1556. wantError: false,
  1557. description: "需要数字字符,应该生成包含数字的密码",
  1558. },
  1559. {
  1560. name: "ucredit requirement - generate password with uppercase",
  1561. config: &Config{
  1562. Minlen: 8,
  1563. Ucredit: -1, // 至少需要1个大写字母
  1564. },
  1565. passwordGenerator: func(length int) string {
  1566. // 第一次返回没有大写字母的密码,第二次返回有大写字母的密码
  1567. if length == 8 {
  1568. return "nouppercase" // 没有大写字母
  1569. }
  1570. return "passWord" // 有大写字母,符合要求
  1571. },
  1572. wantError: false,
  1573. description: "需要大写字母,应该生成包含大写字母的密码",
  1574. },
  1575. {
  1576. name: "lcredit requirement - generate password with lowercase",
  1577. config: &Config{
  1578. Minlen: 8,
  1579. Lcredit: -1, // 至少需要1个小写字母
  1580. },
  1581. passwordGenerator: func(length int) string {
  1582. // 第一次返回没有小写字母的密码,第二次返回有小写字母的密码
  1583. if length == 8 {
  1584. return "NOLOWERCASE" // 没有小写字母
  1585. }
  1586. return "PASSWORDw" // 有小写字母,符合要求
  1587. },
  1588. wantError: false,
  1589. description: "需要小写字母,应该生成包含小写字母的密码",
  1590. },
  1591. {
  1592. name: "ocredit requirement - generate password with special char",
  1593. config: &Config{
  1594. Minlen: 8,
  1595. Ocredit: -1, // 至少需要1个特殊字符
  1596. },
  1597. passwordGenerator: func(length int) string {
  1598. // 第一次返回没有特殊字符的密码,第二次返回有特殊字符的密码
  1599. if length == 8 {
  1600. return "nospecial" // 没有特殊字符
  1601. }
  1602. return "pass@word" // 有特殊字符,符合要求
  1603. },
  1604. wantError: false,
  1605. description: "需要特殊字符,应该生成包含特殊字符的密码",
  1606. },
  1607. {
  1608. name: "complex requirements - multiple retries",
  1609. config: &Config{
  1610. Minlen: 12,
  1611. Dcredit: -1,
  1612. Ucredit: -1,
  1613. Lcredit: -1,
  1614. Ocredit: -1,
  1615. Minclass: 3,
  1616. },
  1617. passwordGenerator: func(length int) string {
  1618. // 模拟多次尝试:第一次太短,第二次缺少数字,第三次缺少大写,第四次符合要求
  1619. switch length {
  1620. case 12:
  1621. return "short" // 太短
  1622. case 13:
  1623. return "nouppercase1@" // 缺少大写字母
  1624. case 14:
  1625. return "nolowercase1@A" // 缺少小写字母
  1626. default:
  1627. return "ValidPass12@" // 符合所有要求:12个字符,包含数字、大写、小写、特殊字符
  1628. }
  1629. },
  1630. wantError: false,
  1631. description: "复杂要求,需要多次重试才能生成符合要求的密码",
  1632. },
  1633. {
  1634. name: "nil passwordGenerator - should return empty",
  1635. config: &Config{
  1636. Minlen: 8,
  1637. },
  1638. passwordGenerator: nil,
  1639. wantError: false,
  1640. description: "passwordGenerator 为 nil 时应该返回空字符串",
  1641. },
  1642. }
  1643. for _, tt := range tests {
  1644. t.Run(tt.name, func(t *testing.T) {
  1645. password := tt.config.GeneratePassword(tt.passwordGenerator)
  1646. if tt.passwordGenerator == nil {
  1647. // 如果 passwordGenerator 为 nil,应该返回空字符串
  1648. if password != "" {
  1649. t.Errorf("GeneratePassword() with nil generator = %q, want empty string", password)
  1650. }
  1651. return
  1652. }
  1653. if password == "" {
  1654. t.Errorf("GeneratePassword() returned empty string, want non-empty password")
  1655. return
  1656. }
  1657. // 验证生成的密码是否符合配置要求
  1658. if tt.config != nil && tt.config.HasAnyPolicy() {
  1659. err := tt.config.Validate(password, "")
  1660. if tt.wantError {
  1661. if err == nil {
  1662. t.Errorf("GeneratePassword() generated password %q should fail validation but passed", password)
  1663. }
  1664. } else {
  1665. if err != nil {
  1666. t.Errorf("GeneratePassword() generated password %q failed validation: %v", password, err)
  1667. }
  1668. }
  1669. }
  1670. })
  1671. }
  1672. }
  1673. func TestConfig_GeneratePassword_RetryMechanism(t *testing.T) {
  1674. // 测试重试机制:确保在多次尝试后能生成符合要求的密码
  1675. attemptCount := 0
  1676. config := &Config{
  1677. Minlen: 10,
  1678. Dcredit: -2, // 需要至少2个数字
  1679. Ucredit: -1, // 需要至少1个大写字母
  1680. Lcredit: -1, // 需要至少1个小写字母
  1681. Ocredit: -1, // 需要至少1个特殊字符
  1682. Enforcing: 1, // 强制执行
  1683. }
  1684. passwordGenerator := func(length int) string {
  1685. attemptCount++
  1686. // 前几次生成不符合要求的密码
  1687. switch attemptCount {
  1688. case 1:
  1689. return "short" // 太短
  1690. case 2:
  1691. return "nouppercase12@" // 缺少大写字母
  1692. case 3:
  1693. return "NOLOWERCASE12@" // 缺少小写字母
  1694. case 4:
  1695. return "NoSpecial12" // 缺少特殊字符
  1696. case 5:
  1697. return "ValidPass12@" // 符合所有要求
  1698. default:
  1699. return "ValidPass12@" // 后续都返回符合要求的密码
  1700. }
  1701. }
  1702. password := config.GeneratePassword(passwordGenerator)
  1703. if password == "" {
  1704. t.Fatal("GeneratePassword() returned empty string")
  1705. }
  1706. // 验证密码符合要求
  1707. err := config.Validate(password, "")
  1708. if err != nil {
  1709. t.Errorf("GeneratePassword() generated password %q failed validation: %v", password, err)
  1710. }
  1711. // 验证确实进行了多次尝试(至少尝试了4次)
  1712. if attemptCount < 4 {
  1713. t.Errorf("Expected at least 4 attempts, got %d", attemptCount)
  1714. }
  1715. }
  1716. func TestConfig_GeneratePassword_LengthCalculation(t *testing.T) {
  1717. // 测试密码长度计算逻辑
  1718. tests := []struct {
  1719. name string
  1720. config *Config
  1721. expectedMinLen int
  1722. description string
  1723. }{
  1724. {
  1725. name: "minlen only",
  1726. config: &Config{
  1727. Minlen: 8,
  1728. },
  1729. expectedMinLen: 8,
  1730. description: "只有最小长度要求",
  1731. },
  1732. {
  1733. name: "minlen with credit requirements",
  1734. config: &Config{
  1735. Minlen: 8,
  1736. Dcredit: -2, // 需要2个数字
  1737. Ucredit: -1, // 需要1个大写字母
  1738. },
  1739. expectedMinLen: 8, // 至少是 minlen 和 requiredChars 的较大值
  1740. description: "最小长度和 credit 要求",
  1741. },
  1742. {
  1743. name: "minlen with minclass",
  1744. config: &Config{
  1745. Minlen: 8,
  1746. Minclass: 3,
  1747. },
  1748. expectedMinLen: 8,
  1749. description: "最小长度和最小字符类要求",
  1750. },
  1751. }
  1752. for _, tt := range tests {
  1753. t.Run(tt.name, func(t *testing.T) {
  1754. callCount := 0
  1755. passwordGenerator := func(length int) string {
  1756. callCount++
  1757. // 第一次调用时记录长度
  1758. if callCount == 1 {
  1759. if length < tt.expectedMinLen {
  1760. t.Errorf("First password generation length = %d, want at least %d", length, tt.expectedMinLen)
  1761. }
  1762. }
  1763. // 根据配置返回符合要求的密码
  1764. if tt.config.Dcredit < 0 && -tt.config.Dcredit >= 2 {
  1765. // 需要至少2个数字
  1766. return "ValidPass12@"
  1767. }
  1768. return "ValidPass1@"
  1769. }
  1770. password := tt.config.GeneratePassword(passwordGenerator)
  1771. if password == "" {
  1772. t.Errorf("GeneratePassword() returned empty string")
  1773. }
  1774. err := tt.config.Validate(password, "")
  1775. if err != nil {
  1776. t.Errorf("Generated password failed validation: %v", err)
  1777. }
  1778. })
  1779. }
  1780. }
  1781. func TestConfig_GeneratePassword_EdgeCases(t *testing.T) {
  1782. // 测试 GeneratePassword 的边界情况
  1783. tests := []struct {
  1784. name string
  1785. config *Config
  1786. passwordGenerator func(int) string
  1787. description string
  1788. }{
  1789. {
  1790. name: "minclass = 1 should not add buffer",
  1791. config: &Config{
  1792. Minlen: 8,
  1793. Minclass: 1, // minclass = 1,不应该添加缓冲
  1794. Dcredit: -1,
  1795. },
  1796. passwordGenerator: func(length int) string {
  1797. return "Pass1word"
  1798. },
  1799. description: "minclass = 1 时不应该添加额外的长度缓冲",
  1800. },
  1801. {
  1802. name: "requiredChars > minLength",
  1803. config: &Config{
  1804. Minlen: 5,
  1805. Dcredit: -3, // 需要3个数字
  1806. Ucredit: -2, // 需要2个大写字母
  1807. },
  1808. passwordGenerator: func(length int) string {
  1809. // requiredChars = 5,应该使用5而不是minLength
  1810. if length < 5 {
  1811. t.Errorf("Expected length >= 5, got %d", length)
  1812. }
  1813. return "PASS123"
  1814. },
  1815. description: "当 requiredChars > minLength 时,应该使用 requiredChars",
  1816. },
  1817. {
  1818. name: "minLength = 0 with credit requirements",
  1819. config: &Config{
  1820. Minlen: 0, // 没有设置最小长度
  1821. Dcredit: -2, // 需要2个数字
  1822. },
  1823. passwordGenerator: func(length int) string {
  1824. // 应该使用默认的8,或者 requiredChars 的较大值
  1825. if length < 2 {
  1826. t.Errorf("Expected length >= 2, got %d", length)
  1827. }
  1828. return "Pass12"
  1829. },
  1830. description: "minLength = 0 时应该使用默认值8",
  1831. },
  1832. {
  1833. name: "maxAttempts reached - should return longer password",
  1834. config: &Config{
  1835. Minlen: 8,
  1836. Maxrepeat: 1, // 非常严格的限制
  1837. },
  1838. passwordGenerator: func(length int) string {
  1839. // 总是返回不符合要求的密码(包含重复字符)
  1840. return strings.Repeat("a", length) // 全部是重复字符
  1841. },
  1842. description: "当达到最大尝试次数时,应该返回一个较长的密码",
  1843. },
  1844. {
  1845. name: "minclass > 1 should add buffer",
  1846. config: &Config{
  1847. Minlen: 8,
  1848. Minclass: 3, // minclass > 1,应该添加缓冲
  1849. Dcredit: -1,
  1850. },
  1851. passwordGenerator: func(length int) string {
  1852. // 应该包含 minclass 的缓冲
  1853. if length < 8+3 {
  1854. t.Errorf("Expected length >= 11 (8 + 3), got %d", length)
  1855. }
  1856. return "Pass1@word"
  1857. },
  1858. description: "minclass > 1 时应该添加额外的长度缓冲",
  1859. },
  1860. {
  1861. name: "requiredChars = 0",
  1862. config: &Config{
  1863. Minlen: 8,
  1864. // 没有 credit 要求
  1865. },
  1866. passwordGenerator: func(length int) string {
  1867. if length != 8 {
  1868. t.Errorf("Expected length = 8, got %d", length)
  1869. }
  1870. return "password"
  1871. },
  1872. description: "没有 credit 要求时,应该使用 minLength",
  1873. },
  1874. {
  1875. name: "all positive credits",
  1876. config: &Config{
  1877. Minlen: 8,
  1878. Dcredit: 1,
  1879. Ucredit: 1,
  1880. Lcredit: 1,
  1881. Ocredit: 1,
  1882. },
  1883. passwordGenerator: func(length int) string {
  1884. return "Pass1@word"
  1885. },
  1886. description: "所有 credit 都是正数时,应该正常生成密码",
  1887. },
  1888. }
  1889. for _, tt := range tests {
  1890. t.Run(tt.name, func(t *testing.T) {
  1891. password := tt.config.GeneratePassword(tt.passwordGenerator)
  1892. if password == "" && tt.passwordGenerator != nil {
  1893. t.Errorf("GeneratePassword() returned empty string")
  1894. }
  1895. // 验证生成的密码(如果可能)
  1896. if password != "" && tt.config.HasAnyPolicy() {
  1897. err := tt.config.Validate(password, "")
  1898. // 对于 maxAttempts 测试,密码可能不符合要求
  1899. if tt.name != "maxAttempts reached - should return longer password" {
  1900. if err != nil {
  1901. t.Errorf("Generated password failed validation: %v", err)
  1902. }
  1903. }
  1904. }
  1905. })
  1906. }
  1907. }
  1908. func TestConfig_GeneratePassword_MaxAttempts(t *testing.T) {
  1909. // 专门测试达到最大尝试次数的情况
  1910. attemptCount := 0
  1911. config := &Config{
  1912. Minlen: 8,
  1913. Maxrepeat: 1, // 非常严格的限制,几乎不可能满足
  1914. Enforcing: 1, // 强制执行
  1915. }
  1916. passwordGenerator := func(length int) string {
  1917. attemptCount++
  1918. // 总是返回不符合要求的密码(全部是重复字符)
  1919. return strings.Repeat("a", length)
  1920. }
  1921. password := config.GeneratePassword(passwordGenerator)
  1922. // 应该返回一个密码(即使不符合要求)
  1923. if password == "" {
  1924. t.Error("GeneratePassword() should return a password even after max attempts")
  1925. }
  1926. // 应该进行了多次尝试
  1927. if attemptCount < 10 {
  1928. t.Errorf("Expected at least 10 attempts, got %d", attemptCount)
  1929. }
  1930. // 密码长度应该增加了
  1931. if len(password) < 8 {
  1932. t.Errorf("Expected password length >= 8 after retries, got %d", len(password))
  1933. }
  1934. }