test_validate.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337
  1. # Regression tests for interactive setup wizard.
  2. # Classification: keep tests here when they verify validate_* and related checks that accept or reject final env/security/runtime configuration values.
  3. from __future__ import annotations
  4. import subprocess
  5. from pathlib import Path
  6. import pytest
  7. from tests.setup._helpers import (
  8. REPO_ROOT,
  9. parse_lines,
  10. run_bash,
  11. run_bash_lines,
  12. write_text_lines,
  13. )
  14. pytestmark = pytest.mark.offline
  15. def test_validate_env_file_rejects_missing_ssl_files(tmp_path: Path) -> None:
  16. """Validation should fail when SSL is enabled with missing cert/key paths."""
  17. env_file = tmp_path / ".env"
  18. env_file.write_text(
  19. "\n".join(
  20. [
  21. "SSL=true",
  22. "SSL_CERTFILE=/missing/cert.pem",
  23. "SSL_KEYFILE=/missing/key.pem",
  24. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  25. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  26. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  27. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  28. ]
  29. )
  30. + "\n",
  31. encoding="utf-8",
  32. )
  33. result = subprocess.run(
  34. [
  35. "bash",
  36. "-lc",
  37. f"""
  38. source "{REPO_ROOT}/scripts/setup/setup.sh"
  39. REPO_ROOT="{tmp_path}"
  40. reset_state
  41. validate_env_file
  42. """,
  43. ],
  44. cwd=REPO_ROOT,
  45. capture_output=True,
  46. text=True,
  47. check=False,
  48. )
  49. assert result.returncode == 1
  50. assert "Invalid SSL_CERTFILE" in result.stderr
  51. assert "Invalid SSL_KEYFILE" in result.stderr
  52. def test_validate_env_file_rejects_container_ssl_paths_for_host_target(
  53. tmp_path: Path,
  54. ) -> None:
  55. """host-target .env must not accept /app/data/certs/* even when the staged file exists."""
  56. (tmp_path / "data" / "certs").mkdir(parents=True)
  57. (tmp_path / "data" / "certs" / "cert.pem").write_text("cert", encoding="utf-8")
  58. (tmp_path / "data" / "certs" / "key.pem").write_text("key", encoding="utf-8")
  59. env_file = tmp_path / ".env"
  60. env_file.write_text(
  61. "\n".join(
  62. [
  63. "SSL=true",
  64. "SSL_CERTFILE=/app/data/certs/cert.pem",
  65. "SSL_KEYFILE=/app/data/certs/key.pem",
  66. "LIGHTRAG_RUNTIME_TARGET=host",
  67. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  68. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  69. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  70. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  71. ]
  72. )
  73. + "\n",
  74. encoding="utf-8",
  75. )
  76. result = subprocess.run(
  77. [
  78. "bash",
  79. "-lc",
  80. f"""
  81. source "{REPO_ROOT}/scripts/setup/setup.sh"
  82. REPO_ROOT="{tmp_path}"
  83. reset_state
  84. validate_env_file
  85. """,
  86. ],
  87. cwd=REPO_ROOT,
  88. capture_output=True,
  89. text=True,
  90. check=False,
  91. )
  92. assert result.returncode == 1
  93. assert "Invalid SSL_CERTFILE" in result.stderr
  94. assert "Invalid SSL_KEYFILE" in result.stderr
  95. def test_validate_env_file_rejects_container_ssl_paths_for_default_host_target(
  96. tmp_path: Path,
  97. ) -> None:
  98. """Omitting LIGHTRAG_RUNTIME_TARGET defaults to host; container paths must still be rejected."""
  99. (tmp_path / "data" / "certs").mkdir(parents=True)
  100. (tmp_path / "data" / "certs" / "cert.pem").write_text("cert", encoding="utf-8")
  101. (tmp_path / "data" / "certs" / "key.pem").write_text("key", encoding="utf-8")
  102. env_file = tmp_path / ".env"
  103. env_file.write_text(
  104. "\n".join(
  105. [
  106. "SSL=true",
  107. "SSL_CERTFILE=/app/data/certs/cert.pem",
  108. "SSL_KEYFILE=/app/data/certs/key.pem",
  109. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  110. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  111. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  112. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  113. ]
  114. )
  115. + "\n",
  116. encoding="utf-8",
  117. )
  118. result = subprocess.run(
  119. [
  120. "bash",
  121. "-lc",
  122. f"""
  123. source "{REPO_ROOT}/scripts/setup/setup.sh"
  124. REPO_ROOT="{tmp_path}"
  125. reset_state
  126. validate_env_file
  127. """,
  128. ],
  129. cwd=REPO_ROOT,
  130. capture_output=True,
  131. text=True,
  132. check=False,
  133. )
  134. assert result.returncode == 1
  135. assert "Invalid SSL_CERTFILE" in result.stderr
  136. assert "Invalid SSL_KEYFILE" in result.stderr
  137. def test_validate_env_file_accepts_container_ssl_paths_for_compose_target(
  138. tmp_path: Path,
  139. ) -> None:
  140. """compose-target .env may use /app/data/certs/* when the staged files exist."""
  141. (tmp_path / "data" / "certs").mkdir(parents=True)
  142. (tmp_path / "data" / "certs" / "cert.pem").write_text("cert", encoding="utf-8")
  143. (tmp_path / "data" / "certs" / "key.pem").write_text("key", encoding="utf-8")
  144. env_file = tmp_path / ".env"
  145. env_file.write_text(
  146. "\n".join(
  147. [
  148. "SSL=true",
  149. "SSL_CERTFILE=/app/data/certs/cert.pem",
  150. "SSL_KEYFILE=/app/data/certs/key.pem",
  151. "LIGHTRAG_RUNTIME_TARGET=compose",
  152. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  153. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  154. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  155. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  156. ]
  157. )
  158. + "\n",
  159. encoding="utf-8",
  160. )
  161. result = subprocess.run(
  162. [
  163. "bash",
  164. "-lc",
  165. f"""
  166. source "{REPO_ROOT}/scripts/setup/setup.sh"
  167. REPO_ROOT="{tmp_path}"
  168. reset_state
  169. validate_env_file
  170. """,
  171. ],
  172. cwd=REPO_ROOT,
  173. capture_output=True,
  174. text=True,
  175. check=False,
  176. )
  177. assert result.returncode == 0
  178. def test_validate_sensitive_env_literals_rejects_interpolation_syntax() -> None:
  179. """Sensitive values should reject `${...}` so default dotenv interpolation stays safe."""
  180. output = run_bash(f"""
  181. set -euo pipefail
  182. source "{REPO_ROOT}/scripts/setup/setup.sh"
  183. reset_state
  184. ENV_VALUES[TOKEN_SECRET]='${{JWT_SECRET}}'
  185. ENV_VALUES[LIGHTRAG_API_KEY]='plain$token'
  186. ENV_VALUES[WEBUI_DESCRIPTION]='${{ALLOWED_MACRO}}'
  187. if validate_sensitive_env_literals; then
  188. printf 'VALID=yes\\n'
  189. else
  190. printf 'VALID=no\\n'
  191. fi
  192. """)
  193. values = parse_lines(output)
  194. assert values["VALID"] == "no"
  195. def test_validate_env_file_allows_predictable_auth_passwords_and_leaves_them_to_audit(
  196. tmp_path: Path,
  197. ) -> None:
  198. """validate_env_file should allow risky-but-runnable auth settings."""
  199. write_text_lines(
  200. tmp_path / ".env",
  201. [
  202. "AUTH_ACCOUNTS=admin:Passw0rd!",
  203. "TOKEN_SECRET=jwt-secret",
  204. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  205. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  206. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  207. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  208. ],
  209. )
  210. write_text_lines(tmp_path / "env.example", ["LLM_BINDING=openai"])
  211. result = subprocess.run(
  212. [
  213. "bash",
  214. "--norc",
  215. "--noprofile",
  216. "-c",
  217. f"""
  218. source "{REPO_ROOT}/scripts/setup/setup.sh"
  219. REPO_ROOT="{tmp_path}"
  220. reset_state
  221. if validate_env_file; then
  222. printf 'VALID=yes\\n'
  223. else
  224. printf 'VALID=no\\n'
  225. fi
  226. """,
  227. ],
  228. cwd=REPO_ROOT,
  229. capture_output=True,
  230. text=True,
  231. check=False,
  232. )
  233. values = parse_lines(result.stdout)
  234. assert values["VALID"] == "yes"
  235. audit_result = subprocess.run(
  236. [
  237. "bash",
  238. "--norc",
  239. "--noprofile",
  240. "-c",
  241. f"""
  242. source "{REPO_ROOT}/scripts/setup/setup.sh"
  243. REPO_ROOT="{tmp_path}"
  244. security_check_env_file
  245. """,
  246. ],
  247. cwd=REPO_ROOT,
  248. capture_output=True,
  249. text=True,
  250. check=False,
  251. )
  252. assert audit_result.returncode == 1
  253. assert "AUTH_ACCOUNTS uses a predictable password prefix." in audit_result.stdout
  254. def test_validate_uri_accepts_neo4j_self_signed_tls_scheme() -> None:
  255. """Neo4j self-signed TLS URIs should pass validation."""
  256. output = run_bash(f"""
  257. set -euo pipefail
  258. source "{REPO_ROOT}/scripts/setup/setup.sh"
  259. if validate_uri "neo4j+ssc://db.example.com:7687" neo4j; then
  260. printf 'VALID=yes\\n'
  261. else
  262. printf 'VALID=no\\n'
  263. fi
  264. """)
  265. values = parse_lines(output)
  266. assert values["VALID"] == "yes"
  267. def test_validate_security_config_rejects_malformed_auth_accounts() -> None:
  268. """Security validation should reject auth entries the API cannot parse."""
  269. output = run_bash(f"""
  270. set -euo pipefail
  271. source "{REPO_ROOT}/scripts/setup/setup.sh"
  272. reset_state
  273. if validate_security_config "admin" "token-secret" "" no "/health"; then
  274. printf 'MISSING_COLON=yes\\n'
  275. else
  276. printf 'MISSING_COLON=no\\n'
  277. fi
  278. if validate_security_config "admin:secret," "token-secret" "" no "/health"; then
  279. printf 'TRAILING_COMMA=yes\\n'
  280. else
  281. printf 'TRAILING_COMMA=no\\n'
  282. fi
  283. if validate_security_config "admin:secret,reader:hunter2" "token-secret" "" no "/health"; then
  284. printf 'VALID_FORMAT=yes\\n'
  285. else
  286. printf 'VALID_FORMAT=no\\n'
  287. fi
  288. if validate_security_config 'admin:{{bcrypt}}$2b$12$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuu' "token-secret" "" no "/health"; then
  289. printf 'BCRYPT_FORMAT=yes\\n'
  290. else
  291. printf 'BCRYPT_FORMAT=no\\n'
  292. fi
  293. if validate_security_config "admin:admin123!" "token-secret" "" no "/health"; then
  294. printf 'ADMIN_PREFIX=yes\\n'
  295. else
  296. printf 'ADMIN_PREFIX=no\\n'
  297. fi
  298. if validate_security_config "admin:Passw0rd!" "token-secret" "" no "/health"; then
  299. printf 'PASS_PREFIX=yes\\n'
  300. else
  301. printf 'PASS_PREFIX=no\\n'
  302. fi
  303. """)
  304. values = parse_lines(output)
  305. assert values["MISSING_COLON"] == "no"
  306. assert values["TRAILING_COMMA"] == "no"
  307. assert values["VALID_FORMAT"] == "yes"
  308. assert values["BCRYPT_FORMAT"] == "yes"
  309. assert values["ADMIN_PREFIX"] == "no"
  310. assert values["PASS_PREFIX"] == "no"
  311. def test_validate_env_file_handles_supported_and_unsupported_uri_schemes(
  312. tmp_path: Path,
  313. ) -> None:
  314. """validate_env_file should reject malformed schemes and allow supported TLS variants."""
  315. cases = {
  316. "invalid-neo4j-scheme": (
  317. [
  318. "LIGHTRAG_GRAPH_STORAGE=Neo4JStorage",
  319. "NEO4J_URI=http://localhost:7687",
  320. "NEO4J_USERNAME=neo4j",
  321. "NEO4J_PASSWORD=secret",
  322. ],
  323. "no",
  324. "Invalid NEO4J_URI",
  325. ),
  326. "invalid-redis-scheme": (
  327. ["LIGHTRAG_KV_STORAGE=RedisKVStorage", "REDIS_URI=tcp://localhost:6379"],
  328. "no",
  329. "Invalid REDIS_URI",
  330. ),
  331. "valid-rediss-scheme": (
  332. ["LIGHTRAG_KV_STORAGE=RedisKVStorage", "REDIS_URI=rediss://localhost:6380"],
  333. "yes",
  334. "",
  335. ),
  336. }
  337. for case_name, (extra_lines, expected_valid, expected_stderr) in cases.items():
  338. case_dir = tmp_path / case_name
  339. case_dir.mkdir()
  340. write_text_lines(
  341. case_dir / ".env",
  342. [
  343. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  344. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  345. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  346. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  347. *extra_lines,
  348. ],
  349. )
  350. write_text_lines(case_dir / "env.example", ["LLM_BINDING=openai"])
  351. result = subprocess.run(
  352. [
  353. "bash",
  354. "--norc",
  355. "--noprofile",
  356. "-c",
  357. f"""
  358. source "{REPO_ROOT}/scripts/setup/setup.sh"
  359. REPO_ROOT="{case_dir}"
  360. reset_state
  361. if validate_env_file; then
  362. printf 'VALID=yes\\n'
  363. else
  364. printf 'VALID=no\\n'
  365. fi
  366. """,
  367. ],
  368. cwd=REPO_ROOT,
  369. capture_output=True,
  370. text=True,
  371. check=False,
  372. )
  373. values = parse_lines(result.stdout)
  374. assert values["VALID"] == expected_valid
  375. if expected_stderr:
  376. assert expected_stderr in result.stderr
  377. def test_validate_env_file_rejects_invalid_runtime_target(tmp_path: Path) -> None:
  378. """validate_env_file should reject unsupported LIGHTRAG_RUNTIME_TARGET values."""
  379. write_text_lines(
  380. tmp_path / ".env",
  381. [
  382. "LIGHTRAG_RUNTIME_TARGET=laptop",
  383. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  384. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  385. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  386. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  387. ],
  388. )
  389. write_text_lines(tmp_path / "env.example", ["LLM_BINDING=openai"])
  390. result = subprocess.run(
  391. [
  392. "bash",
  393. "--norc",
  394. "--noprofile",
  395. "-c",
  396. f"""
  397. source "{REPO_ROOT}/scripts/setup/setup.sh"
  398. REPO_ROOT="{tmp_path}"
  399. if validate_env_file; then
  400. printf 'VALID=yes\\n'
  401. else
  402. printf 'VALID=no\\n'
  403. fi
  404. """,
  405. ],
  406. cwd=REPO_ROOT,
  407. capture_output=True,
  408. text=True,
  409. check=False,
  410. )
  411. values = parse_lines(result.stdout)
  412. assert values["VALID"] == "no"
  413. assert "Invalid LIGHTRAG_RUNTIME_TARGET" in result.stderr
  414. def test_validate_required_variables_requires_opensearch_basic_auth() -> None:
  415. """OpenSearch storages should require both OPENSEARCH_USER and OPENSEARCH_PASSWORD."""
  416. values = run_bash_lines(f"""
  417. set -euo pipefail
  418. source "{REPO_ROOT}/scripts/setup/setup.sh"
  419. reset_state
  420. ENV_VALUES[LIGHTRAG_KV_STORAGE]="OpenSearchKVStorage"
  421. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="OpenSearchVectorDBStorage"
  422. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="OpenSearchGraphStorage"
  423. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="OpenSearchDocStatusStorage"
  424. ENV_VALUES[OPENSEARCH_HOSTS]="localhost:9200"
  425. if validate_required_variables "${{ENV_VALUES[LIGHTRAG_KV_STORAGE]}}" "${{ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]}}" "${{ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]}}" "${{ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]}}"; then
  426. printf 'VALID=yes\\n'
  427. else
  428. printf 'VALID=no\\n'
  429. fi
  430. """)
  431. assert values["VALID"] == "no"
  432. def test_validate_env_file_rejects_invalid_opensearch_index_settings(
  433. tmp_path: Path,
  434. ) -> None:
  435. """validate_env_file should reject invalid OpenSearch shard and replica counts."""
  436. write_text_lines(
  437. tmp_path / ".env",
  438. [
  439. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  440. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  441. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  442. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  443. "OPENSEARCH_HOSTS=localhost:9200",
  444. "OPENSEARCH_USER=admin",
  445. "OPENSEARCH_PASSWORD=StrongPass1!",
  446. "OPENSEARCH_NUMBER_OF_SHARDS=abc",
  447. "OPENSEARCH_NUMBER_OF_REPLICAS=-1",
  448. ],
  449. )
  450. write_text_lines(tmp_path / "env.example", ["LLM_BINDING=openai"])
  451. result = subprocess.run(
  452. [
  453. "bash",
  454. "--norc",
  455. "--noprofile",
  456. "-c",
  457. f"""
  458. source "{REPO_ROOT}/scripts/setup/setup.sh"
  459. REPO_ROOT="{tmp_path}"
  460. if validate_env_file; then
  461. printf 'VALID=yes\\n'
  462. else
  463. printf 'VALID=no\\n'
  464. fi
  465. """,
  466. ],
  467. cwd=REPO_ROOT,
  468. capture_output=True,
  469. text=True,
  470. check=False,
  471. )
  472. values = parse_lines(result.stdout)
  473. assert values["VALID"] == "no"
  474. assert "OPENSEARCH_NUMBER_OF_SHARDS must be a positive integer." in result.stderr
  475. def test_validate_env_file_rejects_blank_opensearch_index_settings(
  476. tmp_path: Path,
  477. ) -> None:
  478. """validate_env_file should reject blank OpenSearch shard and replica counts."""
  479. write_text_lines(
  480. tmp_path / ".env",
  481. [
  482. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  483. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  484. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  485. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  486. "OPENSEARCH_HOSTS=localhost:9200",
  487. "OPENSEARCH_USER=admin",
  488. "OPENSEARCH_PASSWORD=StrongPass1!",
  489. "OPENSEARCH_NUMBER_OF_SHARDS=",
  490. "OPENSEARCH_NUMBER_OF_REPLICAS=",
  491. ],
  492. )
  493. write_text_lines(tmp_path / "env.example", ["LLM_BINDING=openai"])
  494. result = subprocess.run(
  495. [
  496. "bash",
  497. "--norc",
  498. "--noprofile",
  499. "-c",
  500. f"""
  501. source "{REPO_ROOT}/scripts/setup/setup.sh"
  502. REPO_ROOT="{tmp_path}"
  503. if validate_env_file; then
  504. printf 'VALID=yes\\n'
  505. else
  506. printf 'VALID=no\\n'
  507. fi
  508. """,
  509. ],
  510. cwd=REPO_ROOT,
  511. capture_output=True,
  512. text=True,
  513. check=False,
  514. )
  515. values = parse_lines(result.stdout)
  516. assert values["VALID"] == "no"
  517. assert "OPENSEARCH_NUMBER_OF_SHARDS must be a positive integer." in result.stderr
  518. def test_validate_env_file_rejects_mongo_vector_storage_without_atlas_capable_uri(
  519. tmp_path: Path,
  520. ) -> None:
  521. """validate_env_file must reject MongoVectorDBStorage when the URI is not an Atlas cluster and no Atlas Local marker is set."""
  522. env_file = tmp_path / ".env"
  523. env_file.write_text(
  524. "\n".join(
  525. [
  526. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  527. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  528. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  529. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  530. "MONGO_URI=mongodb://localhost:27017",
  531. ]
  532. )
  533. + "\n",
  534. encoding="utf-8",
  535. )
  536. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  537. result = subprocess.run(
  538. [
  539. "bash",
  540. "--norc",
  541. "--noprofile",
  542. "-c",
  543. f"""
  544. source "{REPO_ROOT}/scripts/setup/setup.sh"
  545. REPO_ROOT="{tmp_path}"
  546. reset_state
  547. if validate_env_file; then
  548. printf 'VALID=yes\\n'
  549. else
  550. printf 'VALID=no\\n'
  551. fi
  552. """,
  553. ],
  554. cwd=REPO_ROOT,
  555. capture_output=True,
  556. text=True,
  557. check=False,
  558. )
  559. values = parse_lines(result.stdout)
  560. assert values["VALID"] == "no"
  561. assert "MongoVectorDBStorage requires an Atlas-capable MongoDB URI" in result.stderr
  562. def test_validate_env_file_allows_mongo_vector_storage_with_wizard_managed_atlas_local(
  563. tmp_path: Path,
  564. ) -> None:
  565. """validate_env_file should allow MongoVectorDBStorage with the bundled Atlas Local deployment."""
  566. env_file = tmp_path / ".env"
  567. env_file.write_text(
  568. "\n".join(
  569. [
  570. "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
  571. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  572. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  573. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  574. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  575. "MONGO_URI=mongodb://localhost:27017/?directConnection=true",
  576. "MONGO_DATABASE=LightRAG",
  577. ]
  578. )
  579. + "\n",
  580. encoding="utf-8",
  581. )
  582. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  583. result = subprocess.run(
  584. [
  585. "bash",
  586. "--norc",
  587. "--noprofile",
  588. "-c",
  589. f"""
  590. source "{REPO_ROOT}/scripts/setup/setup.sh"
  591. REPO_ROOT="{tmp_path}"
  592. reset_state
  593. if validate_env_file; then
  594. printf 'VALID=yes\\n'
  595. else
  596. printf 'VALID=no\\n'
  597. fi
  598. """,
  599. ],
  600. cwd=REPO_ROOT,
  601. capture_output=True,
  602. text=True,
  603. check=False,
  604. )
  605. values = parse_lines(result.stdout)
  606. assert values["VALID"] == "yes"
  607. def test_validate_env_file_allows_external_atlas_local_for_mongo_vector_storage(
  608. tmp_path: Path,
  609. ) -> None:
  610. """validate_env_file should allow Atlas Local URIs outside the wizard-managed docker path."""
  611. env_file = tmp_path / ".env"
  612. env_file.write_text(
  613. "\n".join(
  614. [
  615. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  616. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  617. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  618. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  619. "MONGO_URI=mongodb://atlas-local.example.com:27017/LightRAG?replicaSet=rs0&directConnection=true",
  620. "MONGO_DATABASE=LightRAG",
  621. ]
  622. )
  623. + "\n",
  624. encoding="utf-8",
  625. )
  626. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  627. result = subprocess.run(
  628. [
  629. "bash",
  630. "--norc",
  631. "--noprofile",
  632. "-c",
  633. f"""
  634. source "{REPO_ROOT}/scripts/setup/setup.sh"
  635. REPO_ROOT="{tmp_path}"
  636. reset_state
  637. if validate_env_file; then
  638. printf 'VALID=yes\\n'
  639. else
  640. printf 'VALID=no\\n'
  641. fi
  642. """,
  643. ],
  644. cwd=REPO_ROOT,
  645. capture_output=True,
  646. text=True,
  647. check=False,
  648. )
  649. values = parse_lines(result.stdout)
  650. assert values["VALID"] == "yes"
  651. def test_validate_env_file_rejects_remote_mongo_uri_with_docker_marker(
  652. tmp_path: Path,
  653. ) -> None:
  654. """validate_env_file should reject remote mongodb:// URIs when the docker marker is set."""
  655. env_file = tmp_path / ".env"
  656. env_file.write_text(
  657. "\n".join(
  658. [
  659. "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
  660. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  661. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  662. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  663. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  664. "MONGO_URI=mongodb://mongo.example.com:27017/?directConnection=true",
  665. "MONGO_DATABASE=LightRAG",
  666. ]
  667. )
  668. + "\n",
  669. encoding="utf-8",
  670. )
  671. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  672. result = subprocess.run(
  673. [
  674. "bash",
  675. "--norc",
  676. "--noprofile",
  677. "-c",
  678. f"""
  679. source "{REPO_ROOT}/scripts/setup/setup.sh"
  680. REPO_ROOT="{tmp_path}"
  681. reset_state
  682. if validate_env_file; then
  683. printf 'VALID=yes\\n'
  684. else
  685. printf 'VALID=no\\n'
  686. fi
  687. """,
  688. ],
  689. cwd=REPO_ROOT,
  690. capture_output=True,
  691. text=True,
  692. check=False,
  693. )
  694. values = parse_lines(result.stdout)
  695. assert values["VALID"] == "no"
  696. assert (
  697. "MongoVectorDBStorage requires the bundled Atlas Local endpoint"
  698. in result.stderr
  699. )
  700. def test_validate_env_file_rejects_stale_local_mongo_uri_without_direct_connection(
  701. tmp_path: Path,
  702. ) -> None:
  703. """validate_env_file should reject the old local MongoDB URI format when the docker marker is set."""
  704. env_file = tmp_path / ".env"
  705. env_file.write_text(
  706. "\n".join(
  707. [
  708. "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
  709. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  710. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  711. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  712. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  713. "MONGO_URI=mongodb://localhost:27017/",
  714. "MONGO_DATABASE=LightRAG",
  715. ]
  716. )
  717. + "\n",
  718. encoding="utf-8",
  719. )
  720. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  721. result = subprocess.run(
  722. [
  723. "bash",
  724. "--norc",
  725. "--noprofile",
  726. "-c",
  727. f"""
  728. source "{REPO_ROOT}/scripts/setup/setup.sh"
  729. REPO_ROOT="{tmp_path}"
  730. reset_state
  731. if validate_env_file; then
  732. printf 'VALID=yes\\n'
  733. else
  734. printf 'VALID=no\\n'
  735. fi
  736. """,
  737. ],
  738. cwd=REPO_ROOT,
  739. capture_output=True,
  740. text=True,
  741. check=False,
  742. )
  743. values = parse_lines(result.stdout)
  744. assert values["VALID"] == "no"
  745. assert (
  746. "MongoVectorDBStorage requires the bundled Atlas Local endpoint"
  747. in result.stderr
  748. )
  749. def test_validate_env_file_rejects_wrong_local_mongo_port_with_docker_marker(
  750. tmp_path: Path,
  751. ) -> None:
  752. """validate_env_file should reject local MongoDB URIs that do not use the managed Atlas Local port."""
  753. env_file = tmp_path / ".env"
  754. env_file.write_text(
  755. "\n".join(
  756. [
  757. "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
  758. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  759. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  760. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  761. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  762. "MONGO_URI=mongodb://localhost:9999/?directConnection=true",
  763. "MONGO_DATABASE=LightRAG",
  764. ]
  765. )
  766. + "\n",
  767. encoding="utf-8",
  768. )
  769. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  770. result = subprocess.run(
  771. [
  772. "bash",
  773. "--norc",
  774. "--noprofile",
  775. "-c",
  776. f"""
  777. source "{REPO_ROOT}/scripts/setup/setup.sh"
  778. REPO_ROOT="{tmp_path}"
  779. reset_state
  780. if validate_env_file; then
  781. printf 'VALID=yes\\n'
  782. else
  783. printf 'VALID=no\\n'
  784. fi
  785. """,
  786. ],
  787. cwd=REPO_ROOT,
  788. capture_output=True,
  789. text=True,
  790. check=False,
  791. )
  792. values = parse_lines(result.stdout)
  793. assert values["VALID"] == "no"
  794. assert (
  795. "MongoVectorDBStorage requires the bundled Atlas Local endpoint"
  796. in result.stderr
  797. )
  798. def test_validate_env_file_rejects_empty_opensearch_hosts(tmp_path: Path) -> None:
  799. """validate_env_file should reject an explicitly empty OPENSEARCH_HOSTS setting."""
  800. env_file = tmp_path / ".env"
  801. env_file.write_text(
  802. "\n".join(
  803. [
  804. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  805. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  806. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  807. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  808. "OPENSEARCH_HOSTS=",
  809. ]
  810. )
  811. + "\n",
  812. encoding="utf-8",
  813. )
  814. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  815. result = subprocess.run(
  816. [
  817. "bash",
  818. "--norc",
  819. "--noprofile",
  820. "-c",
  821. f"""
  822. source "{REPO_ROOT}/scripts/setup/setup.sh"
  823. REPO_ROOT="{tmp_path}"
  824. reset_state
  825. if validate_env_file; then
  826. printf 'VALID=yes\\n'
  827. else
  828. printf 'VALID=no\\n'
  829. fi
  830. """,
  831. ],
  832. cwd=REPO_ROOT,
  833. capture_output=True,
  834. text=True,
  835. check=False,
  836. )
  837. values = parse_lines(result.stdout)
  838. assert values["VALID"] == "no"
  839. assert "Empty OPENSEARCH_HOSTS" in result.stderr
  840. def test_validate_env_file_rejects_whitespace_only_opensearch_hosts(
  841. tmp_path: Path,
  842. ) -> None:
  843. """validate_env_file should reject OpenSearch host lists with only blank entries."""
  844. env_file = tmp_path / ".env"
  845. env_file.write_text(
  846. "\n".join(
  847. [
  848. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  849. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  850. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  851. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  852. "OPENSEARCH_HOSTS= , ",
  853. "OPENSEARCH_USER=admin",
  854. "OPENSEARCH_PASSWORD=StrongPass1!",
  855. ]
  856. )
  857. + "\n",
  858. encoding="utf-8",
  859. )
  860. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  861. result = subprocess.run(
  862. [
  863. "bash",
  864. "--norc",
  865. "--noprofile",
  866. "-c",
  867. f"""
  868. source "{REPO_ROOT}/scripts/setup/setup.sh"
  869. REPO_ROOT="{tmp_path}"
  870. reset_state
  871. if validate_env_file; then
  872. printf 'VALID=yes\\n'
  873. else
  874. printf 'VALID=no\\n'
  875. fi
  876. """,
  877. ],
  878. cwd=REPO_ROOT,
  879. capture_output=True,
  880. text=True,
  881. check=False,
  882. )
  883. values = parse_lines(result.stdout)
  884. assert values["VALID"] == "no"
  885. assert "OPENSEARCH_HOSTS must not contain empty host entries." in result.stderr
  886. def test_validate_env_file_rejects_docker_opensearch_without_password(
  887. tmp_path: Path,
  888. ) -> None:
  889. """validate_env_file should reject bundled OpenSearch when auth is incomplete."""
  890. env_file = tmp_path / ".env"
  891. env_file.write_text(
  892. "\n".join(
  893. [
  894. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  895. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  896. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  897. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  898. "LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT=docker",
  899. "OPENSEARCH_HOSTS=localhost:9200",
  900. "OPENSEARCH_USER=admin",
  901. ]
  902. )
  903. + "\n",
  904. encoding="utf-8",
  905. )
  906. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  907. result = subprocess.run(
  908. [
  909. "bash",
  910. "--norc",
  911. "--noprofile",
  912. "-c",
  913. f"""
  914. source "{REPO_ROOT}/scripts/setup/setup.sh"
  915. REPO_ROOT="{tmp_path}"
  916. reset_state
  917. if validate_env_file; then
  918. printf 'VALID=yes\\n'
  919. else
  920. printf 'VALID=no\\n'
  921. fi
  922. """,
  923. ],
  924. cwd=REPO_ROOT,
  925. capture_output=True,
  926. text=True,
  927. check=False,
  928. )
  929. values = parse_lines(result.stdout)
  930. assert values["VALID"] == "no"
  931. assert (
  932. "Bundled OpenSearch requires OPENSEARCH_USER and OPENSEARCH_PASSWORD"
  933. in result.stderr
  934. )
  935. def test_validate_env_file_rejects_weak_docker_opensearch_password(
  936. tmp_path: Path,
  937. ) -> None:
  938. """validate_env_file should reject bundled OpenSearch passwords the image will refuse."""
  939. env_file = tmp_path / ".env"
  940. env_file.write_text(
  941. "\n".join(
  942. [
  943. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  944. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  945. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  946. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  947. "LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT=docker",
  948. "OPENSEARCH_HOSTS=localhost:9200",
  949. "OPENSEARCH_USER=admin",
  950. "OPENSEARCH_PASSWORD=weakpass",
  951. ]
  952. )
  953. + "\n",
  954. encoding="utf-8",
  955. )
  956. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  957. result = subprocess.run(
  958. [
  959. "bash",
  960. "--norc",
  961. "--noprofile",
  962. "-c",
  963. f"""
  964. source "{REPO_ROOT}/scripts/setup/setup.sh"
  965. REPO_ROOT="{tmp_path}"
  966. reset_state
  967. if validate_env_file; then
  968. printf 'VALID=yes\\n'
  969. else
  970. printf 'VALID=no\\n'
  971. fi
  972. """,
  973. ],
  974. cwd=REPO_ROOT,
  975. capture_output=True,
  976. text=True,
  977. check=False,
  978. )
  979. values = parse_lines(result.stdout)
  980. assert values["VALID"] == "no"
  981. assert "OpenSearch requires a strong OPENSEARCH_PASSWORD" in result.stderr
  982. def test_validate_env_file_rejects_weak_host_opensearch_password(
  983. tmp_path: Path,
  984. ) -> None:
  985. """validate_env_file should reject weak OpenSearch passwords even for host deployments."""
  986. env_file = tmp_path / ".env"
  987. env_file.write_text(
  988. "\n".join(
  989. [
  990. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  991. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  992. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  993. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  994. "OPENSEARCH_HOSTS=localhost:9200",
  995. "OPENSEARCH_USER=admin",
  996. "OPENSEARCH_PASSWORD=weakpass",
  997. ]
  998. )
  999. + "\n",
  1000. encoding="utf-8",
  1001. )
  1002. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1003. result = subprocess.run(
  1004. [
  1005. "bash",
  1006. "--norc",
  1007. "--noprofile",
  1008. "-c",
  1009. f"""
  1010. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1011. REPO_ROOT="{tmp_path}"
  1012. reset_state
  1013. if validate_env_file; then
  1014. printf 'VALID=yes\\n'
  1015. else
  1016. printf 'VALID=no\\n'
  1017. fi
  1018. """,
  1019. ],
  1020. cwd=REPO_ROOT,
  1021. capture_output=True,
  1022. text=True,
  1023. check=False,
  1024. )
  1025. values = parse_lines(result.stdout)
  1026. assert values["VALID"] == "no"
  1027. assert "OpenSearch requires a strong OPENSEARCH_PASSWORD" in result.stderr
  1028. def test_validate_env_file_rejects_unauthenticated_host_opensearch(
  1029. tmp_path: Path,
  1030. ) -> None:
  1031. """validate_env_file should reject host-mode OpenSearch with no auth fields."""
  1032. env_file = tmp_path / ".env"
  1033. env_file.write_text(
  1034. "\n".join(
  1035. [
  1036. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  1037. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  1038. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  1039. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  1040. "OPENSEARCH_HOSTS=localhost:9200",
  1041. ]
  1042. )
  1043. + "\n",
  1044. encoding="utf-8",
  1045. )
  1046. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1047. result = subprocess.run(
  1048. [
  1049. "bash",
  1050. "--norc",
  1051. "--noprofile",
  1052. "-c",
  1053. f"""
  1054. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1055. REPO_ROOT="{tmp_path}"
  1056. reset_state
  1057. if validate_env_file; then
  1058. printf 'VALID=yes\\n'
  1059. else
  1060. printf 'VALID=no\\n'
  1061. fi
  1062. """,
  1063. ],
  1064. cwd=REPO_ROOT,
  1065. capture_output=True,
  1066. text=True,
  1067. check=False,
  1068. )
  1069. values = parse_lines(result.stdout)
  1070. assert values["VALID"] == "no"
  1071. assert "OPENSEARCH_USER" in result.stderr
  1072. assert "OPENSEARCH_PASSWORD" in result.stderr
  1073. def test_validate_env_file_rejects_partial_host_opensearch_auth(tmp_path: Path) -> None:
  1074. """validate_env_file should reject host-mode OpenSearch when only one auth field is set."""
  1075. env_file = tmp_path / ".env"
  1076. env_file.write_text(
  1077. "\n".join(
  1078. [
  1079. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  1080. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  1081. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  1082. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  1083. "OPENSEARCH_HOSTS=localhost:9200",
  1084. "OPENSEARCH_USER=admin",
  1085. ]
  1086. )
  1087. + "\n",
  1088. encoding="utf-8",
  1089. )
  1090. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1091. result = subprocess.run(
  1092. [
  1093. "bash",
  1094. "--norc",
  1095. "--noprofile",
  1096. "-c",
  1097. f"""
  1098. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1099. REPO_ROOT="{tmp_path}"
  1100. reset_state
  1101. if validate_env_file; then
  1102. printf 'VALID=yes\\n'
  1103. else
  1104. printf 'VALID=no\\n'
  1105. fi
  1106. """,
  1107. ],
  1108. cwd=REPO_ROOT,
  1109. capture_output=True,
  1110. text=True,
  1111. check=False,
  1112. )
  1113. values = parse_lines(result.stdout)
  1114. assert values["VALID"] == "no"
  1115. assert "OPENSEARCH_PASSWORD" in result.stderr
  1116. def test_validate_env_file_rejects_opensearch_hosts_with_uri_scheme(
  1117. tmp_path: Path,
  1118. ) -> None:
  1119. """validate_env_file should require OPENSEARCH_HOSTS to stay as host:port entries."""
  1120. env_file = tmp_path / ".env"
  1121. env_file.write_text(
  1122. "\n".join(
  1123. [
  1124. "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
  1125. "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
  1126. "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
  1127. "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
  1128. "OPENSEARCH_HOSTS=https://localhost:9200",
  1129. "OPENSEARCH_USER=admin",
  1130. "OPENSEARCH_PASSWORD=StrongPass1!",
  1131. ]
  1132. )
  1133. + "\n",
  1134. encoding="utf-8",
  1135. )
  1136. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1137. result = subprocess.run(
  1138. [
  1139. "bash",
  1140. "--norc",
  1141. "--noprofile",
  1142. "-c",
  1143. f"""
  1144. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1145. REPO_ROOT="{tmp_path}"
  1146. reset_state
  1147. if validate_env_file; then
  1148. printf 'VALID=yes\\n'
  1149. else
  1150. printf 'VALID=no\\n'
  1151. fi
  1152. """,
  1153. ],
  1154. cwd=REPO_ROOT,
  1155. capture_output=True,
  1156. text=True,
  1157. check=False,
  1158. )
  1159. values = parse_lines(result.stdout)
  1160. assert values["VALID"] == "no"
  1161. assert (
  1162. "OPENSEARCH_HOSTS must use bare host:port entries, not URLs." in result.stderr
  1163. )
  1164. def test_validate_env_file_ignores_invalid_unused_storage_settings(
  1165. tmp_path: Path,
  1166. ) -> None:
  1167. """validate_env_file should ignore malformed settings for backends not selected by storage."""
  1168. env_file = tmp_path / ".env"
  1169. env_file.write_text(
  1170. "\n".join(
  1171. [
  1172. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  1173. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  1174. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  1175. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  1176. "NEO4J_URI=http://localhost:7687",
  1177. "MONGO_URI=not-a-mongo-uri",
  1178. "REDIS_URI=tcp://localhost:6379",
  1179. "MILVUS_URI=tcp://localhost:19530",
  1180. "QDRANT_URL=tcp://localhost:6333",
  1181. "MEMGRAPH_URI=http://localhost:7687",
  1182. "POSTGRES_PORT=99999",
  1183. ]
  1184. )
  1185. + "\n",
  1186. encoding="utf-8",
  1187. )
  1188. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1189. result = subprocess.run(
  1190. [
  1191. "bash",
  1192. "--norc",
  1193. "--noprofile",
  1194. "-c",
  1195. f"""
  1196. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1197. REPO_ROOT="{tmp_path}"
  1198. reset_state
  1199. if validate_env_file; then
  1200. printf 'VALID=yes\\n'
  1201. else
  1202. printf 'VALID=no\\n'
  1203. fi
  1204. """,
  1205. ],
  1206. cwd=REPO_ROOT,
  1207. capture_output=True,
  1208. text=True,
  1209. check=False,
  1210. )
  1211. values = parse_lines(result.stdout)
  1212. assert values["VALID"] == "yes"
  1213. assert "Invalid NEO4J_URI" not in result.stderr
  1214. assert "Invalid MONGO_URI" not in result.stderr
  1215. assert "Invalid REDIS_URI" not in result.stderr
  1216. assert "Invalid MILVUS_URI" not in result.stderr
  1217. assert "Invalid QDRANT_URL" not in result.stderr
  1218. assert "Invalid MEMGRAPH_URI" not in result.stderr
  1219. assert "Invalid POSTGRES_PORT" not in result.stderr
  1220. def test_validate_env_file_allows_empty_opensearch_hosts_when_unused(
  1221. tmp_path: Path,
  1222. ) -> None:
  1223. """validate_env_file should ignore blank OpenSearch hosts when no OpenSearch storage is selected."""
  1224. env_file = tmp_path / ".env"
  1225. env_file.write_text(
  1226. "\n".join(
  1227. [
  1228. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  1229. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  1230. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  1231. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  1232. "OPENSEARCH_HOSTS=",
  1233. ]
  1234. )
  1235. + "\n",
  1236. encoding="utf-8",
  1237. )
  1238. (tmp_path / "env.example").write_text("LLM_BINDING=openai\n", encoding="utf-8")
  1239. result = subprocess.run(
  1240. [
  1241. "bash",
  1242. "--norc",
  1243. "--noprofile",
  1244. "-c",
  1245. f"""
  1246. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1247. REPO_ROOT="{tmp_path}"
  1248. reset_state
  1249. if validate_env_file; then
  1250. printf 'VALID=yes\\n'
  1251. else
  1252. printf 'VALID=no\\n'
  1253. fi
  1254. """,
  1255. ],
  1256. cwd=REPO_ROOT,
  1257. capture_output=True,
  1258. text=True,
  1259. check=False,
  1260. )
  1261. values = parse_lines(result.stdout)
  1262. assert values["VALID"] == "yes"
  1263. assert "Empty OPENSEARCH_HOSTS" not in result.stderr