superbrain-server 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/__pycache__/api.cpython-311.pyc +0 -0
  3. package/payload/__pycache__/main.cpython-311.pyc +0 -0
  4. package/payload/__pycache__/start.cpython-311.pyc +0 -0
  5. package/payload/start.py +70 -150
  6. package/payload/analyzers/__pycache__/__init__.cpython-311.pyc +0 -0
  7. package/payload/analyzers/__pycache__/audio_transcribe.cpython-311.pyc +0 -0
  8. package/payload/analyzers/__pycache__/caption.cpython-311.pyc +0 -0
  9. package/payload/analyzers/__pycache__/music_identifier.cpython-311.pyc +0 -0
  10. package/payload/analyzers/__pycache__/text_analyzer.cpython-311.pyc +0 -0
  11. package/payload/analyzers/__pycache__/visual_analyze.cpython-311.pyc +0 -0
  12. package/payload/analyzers/__pycache__/webpage_analyzer.cpython-311.pyc +0 -0
  13. package/payload/analyzers/__pycache__/youtube_analyzer.cpython-311.pyc +0 -0
  14. package/payload/config/backend_id.txt +0 -1
  15. package/payload/config/localtunnel.log +0 -86
  16. package/payload/core/__pycache__/__init__.cpython-311.pyc +0 -0
  17. package/payload/core/__pycache__/category_manager.cpython-311.pyc +0 -0
  18. package/payload/core/__pycache__/database.cpython-311.pyc +0 -0
  19. package/payload/core/__pycache__/link_checker.cpython-311.pyc +0 -0
  20. package/payload/core/__pycache__/model_router.cpython-311.pyc +0 -0
  21. package/payload/instagram/__pycache__/__init__.cpython-311.pyc +0 -0
  22. package/payload/instagram/__pycache__/instagram_downloader.cpython-311.pyc +0 -0
  23. package/payload/instagram/__pycache__/instagram_login.cpython-311.pyc +0 -0
  24. package/payload/test_backend.py +0 -241
  25. package/payload/tests/__init__.py +0 -0
  26. package/payload/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  27. package/payload/tests/__pycache__/test_api.cpython-311.pyc +0 -0
  28. package/payload/tests/__pycache__/test_db.cpython-311.pyc +0 -0
  29. package/payload/tests/__pycache__/test_sync_code.cpython-311.pyc +0 -0
  30. package/payload/tests/test_api.py +0 -17
  31. package/payload/tests/test_db.py +0 -22
  32. package/payload/tests/test_sync_code.py +0 -65
  33. package/payload/utils/__pycache__/__init__.cpython-311.pyc +0 -0
  34. package/payload/utils/__pycache__/db_stats.cpython-311.pyc +0 -0
  35. package/payload/utils/__pycache__/manage_token.cpython-311.pyc +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
package/payload/start.py CHANGED
@@ -22,6 +22,7 @@ import string
22
22
  import textwrap
23
23
  import time
24
24
  import importlib
25
+ import re
25
26
  from pathlib import Path
26
27
 
27
28
  # ── Paths ─────────────────────────────────────────────────────────────────────
@@ -157,6 +158,7 @@ def ensure_runtime_dependencies():
157
158
  ("fastapi", "fastapi"),
158
159
  ("uvicorn", "uvicorn"),
159
160
  ("multipart", "python-multipart"),
161
+ ("pyngrok", "pyngrok"),
160
162
  ("instaloader", "instaloader"),
161
163
  ("segno", "segno"),
162
164
  ]
@@ -662,64 +664,41 @@ def setup_whisper():
662
664
  # ══════════════════════════════════════════════════════════════════════════════
663
665
  # Step 6 — Remote Access / Port Forwarding
664
666
  # ══════════════════════════════════════════════════════════════════════════════
665
- LOCALTUNNEL_ENABLED = BASE_DIR / "config" / "localtunnel_enabled.txt"
666
- LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
667
+ NGROK_ENABLED = BASE_DIR / "config" / "ngrok_enabled.txt"
668
+ NGROK_TOKEN = BASE_DIR / "config" / "ngrok_token.txt"
667
669
 
668
670
  def setup_remote_access():
669
- h1("Step 6 of 7 — Remote Access (localtunnel / Port Forwarding)")
671
+ h1("Step 6 of 7 — Remote Access (ngrok)")
670
672
 
671
673
  print(f"""
672
- The SuperBrain backend runs on {BOLD}port 5000{RESET} on your machine.
673
- Your phone needs to reach this port over the internet.
674
-
675
- You have two options:
676
-
677
- {BOLD}Option A — localtunnel (easiest + free){RESET}
678
- localtunnel creates a public HTTPS URL that tunnels to your local port 5000.
679
- No account required.
680
- Official site: {CYAN}https://theboroer.github.io/localtunnel-www/{RESET}
681
-
682
- {BOLD}Option B — Your own port forwarding (advanced){RESET}
683
- Forward {BOLD}TCP port 5000{RESET} on your router to your machine's local IP.
684
- Then use {BOLD}http://<your-public-ip>:5000{RESET} in the mobile app.
685
- Steps:
686
- 1. Find your machine's local IP → ip addr (Linux) / ipconfig (Windows)
687
- 2. Log into your router admin panel (usually http://192.168.1.1)
688
- 3. Add a port forwarding rule: External 5000 → Internal <your-local-IP>:5000
689
- 4. Use your public IP (check https://ipify.org) in the mobile app.
690
- {YELLOW}Note: dynamic public IPs change on router restart — consider a DDNS service.{RESET}
691
-
692
- {DIM}You can also run only on your local WiFi — both phone and PC must be on
693
- the same network. Use your PC's local IP (e.g. 192.168.x.x) in the app.{RESET}
674
+ The SuperBrain backend runs locally. Your phone needs to reach it over the internet.
675
+ We recommend {BOLD}ngrok{RESET} for a secure tunnel.
676
+
677
+ Requires a free account from: {CYAN}https://dashboard.ngrok.com/signup{RESET}
678
+ Get your Authtoken at: {CYAN}https://dashboard.ngrok.com/get-started/your-authtoken{RESET}
694
679
  """)
695
680
 
696
- choice = ask_yn("Enable localtunnel on startup?", default=True)
681
+ choice = ask_yn("Enable ngrok on startup?", default=True)
697
682
  if not choice:
698
- LOCALTUNNEL_ENABLED.unlink(missing_ok=True)
699
- warn("Skipping localtunnel. Use either your own port forwarding or local WiFi.")
700
- info("Remember: set the correct server URL in the mobile app Settings.")
683
+ NGROK_ENABLED.unlink(missing_ok=True)
684
+ warn("Skipping ngrok. Local WiFi only.")
701
685
  return
702
686
 
703
- if not shutil.which("npx"):
704
- print(f"""
705
- {YELLOW}npx is not installed / not on PATH.{RESET}
706
-
707
- Install it:
708
- Linux → {CYAN}Install Node.js (includes npm + npx){RESET}
709
- macOS → {CYAN}brew install node{RESET}
710
- Windows → Install Node.js LTS from {CYAN}https://nodejs.org/{RESET}
711
-
712
- After installing, re-run {BOLD}python start.py{RESET}.
713
- """)
714
- warn("Skipping localtunnel setup.")
715
- return
716
-
717
- ok("npx binary found")
718
- LOCALTUNNEL_ENABLED.parent.mkdir(parents=True, exist_ok=True)
719
- LOCALTUNNEL_ENABLED.write_text("enabled")
720
- ok("localtunnel auto-start enabled")
721
- nl()
722
- info("localtunnel will be started automatically every time you run start.py.")
687
+ ok("ngrok auto-start enabled")
688
+ NGROK_ENABLED.parent.mkdir(parents=True, exist_ok=True)
689
+ NGROK_ENABLED.write_text("enabled")
690
+
691
+ existing_token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else ""
692
+ print(f"\n {YELLOW}Please paste your ngrok Authtoken.{RESET}")
693
+ if existing_token:
694
+ print(f" {DIM}(Leave blank to keep existing token){RESET}")
695
+
696
+ auth_token = ask("Authtoken", default=existing_token, paste=True)
697
+ if auth_token.strip():
698
+ NGROK_TOKEN.write_text(auth_token.strip())
699
+ ok("ngrok token saved.")
700
+ else:
701
+ warn("No ngrok token provided. ngrok may disconnect. To fix, re-run setup.")
723
702
 
724
703
  # ══════════════════════════════════════════════════════════════════════════════
725
704
  # Step 6 — Access Token & Database
@@ -754,87 +733,21 @@ def setup_token_and_db():
754
733
  # ══════════════════════════════════════════════════════════════════════════════
755
734
  # Launch Backend
756
735
  # ══════════════════════════════════════════════════════════════════════════════
757
- def _extract_localtunnel_url(text: str) -> str | None:
758
- """Extract first localtunnel public URL from text."""
759
- import re
760
- m = re.search(r"https://[\w.-]+\.loca\.lt\b", text)
761
- return m.group(0) if m else None
762
-
763
-
764
- def _find_localtunnel_url_from_log() -> str | None:
765
- """Read local tunnel log and return detected public URL if available."""
736
+ def _start_ngrok(port: int) -> str | None:
766
737
  try:
767
- if not LOCALTUNNEL_LOG.exists():
768
- return None
769
- text = LOCALTUNNEL_LOG.read_text(encoding="utf-8", errors="ignore")
770
- return _extract_localtunnel_url(text)
771
- except Exception:
772
- return None
773
-
774
-
775
- def _stop_localtunnel_processes():
776
- """Stop existing localtunnel processes so only one tunnel remains active."""
777
- try:
778
- if IS_WINDOWS:
779
- script = (
780
- "Get-CimInstance Win32_Process "
781
- "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
782
- "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
783
- )
784
- subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
785
- else:
786
- subprocess.run(["pkill", "-f", "localtunnel"], check=False)
787
- except Exception:
788
- pass
789
-
790
-
791
- def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
792
- """Start localtunnel in the background and wait for the public URL."""
793
- import time as _time
794
-
795
- npx_exec = shutil.which("npx") or shutil.which("npx.cmd")
796
- if not npx_exec:
797
- return None
798
-
799
- # Clean stale localtunnel processes.
800
- _stop_localtunnel_processes()
801
- _time.sleep(0.8)
802
-
803
- info("Starting localtunnel in background …")
804
- try:
805
- LOCALTUNNEL_LOG.parent.mkdir(parents=True, exist_ok=True)
806
- LOCALTUNNEL_LOG.write_text("")
807
-
808
- log_handle = open(LOCALTUNNEL_LOG, "a", encoding="utf-8", buffering=1)
809
- kwargs = {
810
- "start_new_session": True,
811
- "stdout": log_handle,
812
- "stderr": subprocess.STDOUT,
813
- "text": True,
814
- }
815
- if IS_WINDOWS and npx_exec.lower().endswith(".cmd"):
816
- cmd = ["cmd", "/c", npx_exec, "-y", "localtunnel", "--port", str(port)]
817
- else:
818
- cmd = [npx_exec, "-y", "localtunnel", "--port", str(port)]
819
- subprocess.Popen(cmd, **kwargs)
738
+ import pyngrok
739
+ from pyngrok import ngrok
740
+
741
+ token = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else None
742
+ if token:
743
+ ngrok.set_auth_token(token)
744
+
745
+ tunnel = ngrok.connect(port, bind_tls=True)
746
+ return tunnel.public_url
820
747
  except Exception as e:
821
- warn(f"Could not start localtunnel: {e}")
748
+ warn(f"Failed to start ngrok: {e}")
822
749
  return None
823
750
 
824
- # Poll log output until URL is emitted.
825
- deadline = _time.time() + timeout
826
- while _time.time() < deadline:
827
- _time.sleep(1)
828
- url = _find_localtunnel_url_from_log()
829
- if url:
830
- ok(f"localtunnel active → {GREEN}{BOLD}{url}{RESET}")
831
- return url
832
-
833
- warn("localtunnel started but URL is not available yet.")
834
- info(f"Check tunnel logs in: {LOCALTUNNEL_LOG}")
835
- return None
836
-
837
-
838
751
  def _get_windows_pids_on_port(port: int) -> list[int]:
839
752
  """Return listener PIDs on Windows using Get-NetTCPConnection when available."""
840
753
  pids: set[int] = set()
@@ -1202,26 +1115,27 @@ def launch_backend():
1202
1115
  token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "—"
1203
1116
  local_ip = _detect_local_ip()
1204
1117
 
1205
- localtunnel_enabled = bool(shutil.which("npx") or shutil.which("npx.cmd"))
1206
-
1207
- localtunnel_url: str | None = None
1208
- if localtunnel_enabled:
1209
- localtunnel_url = _start_localtunnel(PORT)
1210
- else:
1211
- localtunnel_url = _find_localtunnel_url_from_log()
1212
-
1213
- if localtunnel_url:
1214
- tunnel_line = f" Public URL → {GREEN}{BOLD}{localtunnel_url}{RESET} {DIM}(localtunnel){RESET}"
1215
- tunnel_hint = f" · public → {GREEN}{localtunnel_url}{RESET}"
1216
- elif localtunnel_enabled:
1217
- tunnel_line = f" Public URL → {YELLOW}(starting — URL pending, check localtunnel.log){RESET}"
1218
- tunnel_hint = f" · public → run: {DIM}npx localtunnel --port {PORT}{RESET}"
1118
+ # ngrok startup
1119
+ public_url: str | None = None
1120
+ if NGROK_ENABLED.exists():
1121
+ info("Starting ngrok in background...")
1122
+ public_url = _start_ngrok(PORT)
1123
+
1124
+ tunnel_line = ""
1125
+ tunnel_hint = ""
1126
+
1127
+ if public_url:
1128
+ tunnel_line = f" Public URL → {GREEN}{BOLD}{public_url}{RESET} {DIM}(ngrok){RESET}"
1129
+ tunnel_hint = f" · public → {GREEN}{public_url}{RESET}"
1130
+ ok(f"ngrok active → {GREEN}{BOLD}{public_url}{RESET}")
1131
+ elif NGROK_ENABLED.exists():
1132
+ tunnel_line = f" Public URL → {YELLOW}(failed to start ngrok){RESET}"
1133
+ tunnel_hint = f" · public → run manually: {DIM}ngrok http {PORT}{RESET}"
1219
1134
  else:
1220
- tunnel_line = ""
1221
- tunnel_hint = f" · public → install Node.js first, then run: {DIM}npx localtunnel --port {PORT}{RESET}"
1135
+ tunnel_hint = f" · public → enable ngrok via {DIM}python start.py --reset{RESET}"
1222
1136
 
1223
1137
  # ── Generate and display QR code ──────────────────────────────────────────
1224
- qr_url = localtunnel_url if localtunnel_url else f"http://{local_ip}:{PORT}"
1138
+ qr_url = public_url if public_url else f"http://{local_ip}:{PORT}"
1225
1139
  _display_connect_qr(qr_url, token)
1226
1140
 
1227
1141
  print(f"""
@@ -1274,15 +1188,21 @@ def launch_backend_status():
1274
1188
  if TOKEN_FILE.exists():
1275
1189
  token = TOKEN_FILE.read_text(encoding="utf-8").strip()
1276
1190
 
1191
+ # Fetch ngrok status via API
1277
1192
  url = "NOT_FOUND"
1278
- log_file = BASE_DIR / "config" / "localtunnel.log"
1279
- if log_file.exists():
1280
- match = re.search(r"your url is: (https://[^\s]+)", log_file.read_text(encoding="utf-8"))
1281
- if match:
1282
- url = match.group(1)
1283
-
1193
+ try:
1194
+ import urllib.request, json
1195
+ req = urllib.request.urlopen("http://127.0.0.1:4040/api/tunnels", timeout=2)
1196
+ data = json.loads(req.read())
1197
+ for tunnel in data.get("tunnels", []):
1198
+ if tunnel.get("proto") == "https":
1199
+ url = tunnel.get("public_url")
1200
+ break
1201
+ except Exception:
1202
+ pass
1203
+
1284
1204
  if url == "NOT_FOUND":
1285
- warn("Could not find a running localtunnel URL in config/localtunnel.log.")
1205
+ warn("Could not find a running ngrok URL. Is the server running?")
1286
1206
  nl()
1287
1207
  print(" Wait 5 seconds, or run 'superbrain-server' to start the server.")
1288
1208
  return
@@ -1 +0,0 @@
1
- 3f491f84-184a-40ba-9660-b882b881c4d5
@@ -1,86 +0,0 @@
1
-
2
- ===============================================================================
3
- Welcome to localhost.run!
4
-
5
- Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
6
-
7
- To set up and manage custom domains go to https://admin.localhost.run/
8
-
9
- More details on custom domains (and how to enable subdomains of your custom
10
- domain) at https://localhost.run/docs/custom-domains
11
-
12
- If you get a permission denied error check the faq for how to connect with a key or
13
- create a free tunnel without a key at [http://localhost:3000/docs/faq#generating-an-ssh-key].
14
-
15
- To explore using localhost.run visit the documentation site:
16
- https://localhost.run/docs/
17
-
18
- ===============================================================================
19
-
20
- ** your connection id is 78ac0b8b-a8f4-4185-9da5-a446d15e032f, please mention it if you send me a message about an issue. **
21
-
22
- authenticated as anonymous user
23
- 14ebb2ac2ccab3.lhr.life tunneled with tls termination, https://14ebb2ac2ccab3.lhr.life
24
- create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
25
- Open your tunnel address on your mobile with this QR:
26
-
27
-                            
28
-                            
29
-                            
30
-                            
31
-                            
32
-                            
33
-                            
34
-                            
35
-                            
36
-                            
37
-                            
38
-                            
39
-                            
40
-                            
41
-                            
42
-                            
43
-                            
44
-                            
45
-                            
46
-                            
47
-                            
48
-                            
49
-                            
50
-                            
51
-                            
52
-                            
53
-                            
54
-
55
- c3f3097e2d129a.lhr.life tunneled with tls termination, https://c3f3097e2d129a.lhr.life
56
- create an account and add your key for a longer lasting domain name. see https://localhost.run/docs/forever-free/ for more information.
57
- Open your tunnel address on your mobile with this QR:
58
-
59
-                            
60
-                            
61
-                            
62
-                            
63
-                            
64
-                            
65
-                            
66
-                            
67
-                            
68
-                            
69
-                            
70
-                            
71
-                            
72
-                            
73
-                            
74
-                            
75
-                            
76
-                            
77
-                            
78
-                            
79
-                            
80
-                            
81
-                            
82
-                            
83
-                            
84
-                            
85
-                            
86
-
@@ -1,241 +0,0 @@
1
- """
2
- SuperBrain - Comprehensive Backend Test Suite
3
- Tests all API endpoints against the running backend.
4
- """
5
- import requests
6
- import sys
7
- import json
8
- import time
9
-
10
- # Config
11
- BASE_URL = "http://localhost:5000"
12
- TOKEN = "4XVTLWWV"
13
- HEADERS = {"X-API-Key": TOKEN, "Content-Type": "application/json"}
14
-
15
- TEST_YT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
16
-
17
- passed = 0
18
- failed = 0
19
- skipped = 0
20
-
21
- def test(name, condition, detail=""):
22
- global passed, failed
23
- if condition:
24
- passed += 1
25
- print(f" [PASS] {name}")
26
- else:
27
- failed += 1
28
- print(f" [FAIL] {name} -- {detail}")
29
-
30
- def skip(name, reason):
31
- global skipped
32
- skipped += 1
33
- print(f" [SKIP] {name} -- {reason}")
34
-
35
- # Phase 1: Health & Auth
36
- print("\n--- Phase 1: Health & Auth ---")
37
-
38
- try:
39
- r = requests.get(f"{BASE_URL}/status", timeout=5)
40
- test("1.1 GET /status", r.status_code == 200 and r.json().get("status") == "online", f"status={r.status_code}")
41
- except Exception as e:
42
- test("1.1 GET /status", False, str(e))
43
-
44
- try:
45
- r = requests.get(f"{BASE_URL}/health", headers=HEADERS, timeout=5)
46
- data = r.json()
47
- test("1.2 GET /health (auth)", r.status_code == 200 and "database" in data, f"status={r.status_code}")
48
- except Exception as e:
49
- test("1.2 GET /health", False, str(e))
50
-
51
- try:
52
- r = requests.get(f"{BASE_URL}/health", headers={"X-API-Key": "WRONGTKN"}, timeout=5)
53
- test("1.3 Auth rejection (wrong)", r.status_code == 401, f"status={r.status_code}")
54
- except Exception as e:
55
- test("1.3 Auth rejection", False, str(e))
56
-
57
- try:
58
- r = requests.get(f"{BASE_URL}/health", timeout=5)
59
- test("1.4 Auth rejection (none)", r.status_code in [401, 403, 422], f"status={r.status_code}")
60
- except Exception as e:
61
- test("1.4 Auth rejection (none)", False, str(e))
62
-
63
- # Phase 2: Read Endpoints
64
- print("\n--- Phase 2: Read Endpoints ---")
65
-
66
- try:
67
- r = requests.get(f"{BASE_URL}/recent?limit=10", headers=HEADERS, timeout=10)
68
- data = r.json()
69
- test("2.1 GET /recent", r.status_code == 200 and "data" in data, f"status={r.status_code}")
70
- print(f" found {len(data.get('data', []))} posts")
71
- except Exception as e:
72
- test("2.1 GET /recent", False, str(e))
73
-
74
- try:
75
- r = requests.get(f"{BASE_URL}/categories", headers=HEADERS, timeout=10)
76
- data = r.json()
77
- test("2.2 GET /categories", r.status_code == 200 and "categories" in data, f"status={r.status_code}")
78
- print(f" found {len(data.get('categories', []))} categories")
79
- except Exception as e:
80
- test("2.2 GET /categories", False, str(e))
81
-
82
- try:
83
- r = requests.get(f"{BASE_URL}/queue-status", headers=HEADERS, timeout=10)
84
- data = r.json()
85
- test("2.3 GET /queue-status", r.status_code == 200 and "processing_count" in data, f"keys={list(data.keys())}")
86
- print(f" processing={data.get('processing_count',0)}, queue={data.get('queue_count',0)}, retry={data.get('retry_count',0)}")
87
- except Exception as e:
88
- test("2.3 GET /queue-status", False, str(e))
89
-
90
- try:
91
- r = requests.get(f"{BASE_URL}/stats", headers=HEADERS, timeout=10)
92
- test("2.4 GET /stats", r.status_code == 200, f"status={r.status_code}")
93
- except Exception as e:
94
- test("2.4 GET /stats", False, str(e))
95
-
96
- try:
97
- r = requests.get(f"{BASE_URL}/search?tags=travel&limit=5", headers=HEADERS, timeout=10)
98
- test("2.5 GET /search?tags=travel", r.status_code == 200, f"status={r.status_code}")
99
- except Exception as e:
100
- test("2.5 GET /search", False, str(e))
101
-
102
- try:
103
- r = requests.get(f"{BASE_URL}/queue/retry", headers=HEADERS, timeout=10)
104
- test("2.6 GET /queue/retry", r.status_code == 200, f"status={r.status_code}")
105
- except Exception as e:
106
- test("2.6 GET /queue/retry", False, str(e))
107
-
108
- # Phase 3: Collections CRUD
109
- print("\n--- Phase 3: Collections CRUD ---")
110
-
111
- test_collection_id = None
112
-
113
- try:
114
- r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
115
- data = r.json()
116
- test("3.1 GET /collections", r.status_code == 200, f"status={r.status_code}")
117
- print(f" found {len(data)} collections")
118
- except Exception as e:
119
- test("3.1 GET /collections", False, str(e))
120
-
121
- try:
122
- r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": "test-id", "name": "Test Collection", "icon": "test"}, timeout=10)
123
- data = r.json()
124
- test("3.2 POST /collections (create)", r.status_code == 200 and data.get("id"), f"status={r.status_code}")
125
- test_collection_id = data.get("id")
126
- print(f" created ID: {test_collection_id}")
127
- except Exception as e:
128
- test("3.2 POST /collections", False, str(e))
129
-
130
- if test_collection_id:
131
- try:
132
- r = requests.post(f"{BASE_URL}/collections", headers=HEADERS, json={"id": test_collection_id, "name": "Updated Test", "icon": "ok"}, timeout=10)
133
- test("3.3 POST /collections (update)", r.status_code == 200, f"status={r.status_code}")
134
- except Exception as e:
135
- test("3.3 POST /collections (update)", False, str(e))
136
-
137
- try:
138
- r = requests.delete(f"{BASE_URL}/collections/{test_collection_id}", headers=HEADERS, timeout=10)
139
- test("3.4 DELETE /collections", r.status_code == 200, f"status={r.status_code}")
140
- except Exception as e:
141
- test("3.4 DELETE /collections", False, str(e))
142
- else:
143
- skip("3.3 PUT /collections", "no collection created")
144
- skip("3.4 DELETE /collections", "no collection created")
145
-
146
- try:
147
- r = requests.get(f"{BASE_URL}/collections", headers=HEADERS, timeout=10)
148
- data = r.json()
149
- collections = data.get("data", [])
150
- wl = next((c for c in collections if c.get("name") == "Watch Later"), None)
151
- if wl:
152
- r = requests.delete(f"{BASE_URL}/collections/{wl['id']}", headers=HEADERS, timeout=10)
153
- test("3.5 Watch Later protection", r.status_code in [400, 403], f"status={r.status_code} (should be blocked)")
154
- else:
155
- skip("3.5 Watch Later protection", "no Watch Later found")
156
- except Exception as e:
157
- test("3.5 Watch Later protection", False, str(e))
158
-
159
- # Phase 4: Settings
160
- print("\n--- Phase 4: Settings ---")
161
-
162
- try:
163
- r = requests.get(f"{BASE_URL}/settings/ai-providers", headers=HEADERS, timeout=10)
164
- test("4.1 GET /settings/ai-providers", r.status_code == 200, f"status={r.status_code}")
165
- except Exception as e:
166
- test("4.1 GET /settings/ai-providers", False, str(e))
167
-
168
- try:
169
- r = requests.post(f"{BASE_URL}/settings/ai-providers", headers=HEADERS,
170
- json={"provider": "groq", "api_key": "gsk_test"}, timeout=10)
171
- test("4.2 POST ai-providers (Groq)", r.status_code == 200, f"status={r.status_code}")
172
- except Exception as e:
173
- test("4.2 POST ai-providers", False, str(e))
174
-
175
- try:
176
- r = requests.get(f"{BASE_URL}/settings/instagram", headers=HEADERS, timeout=10)
177
- test("4.3 GET /settings/instagram", r.status_code == 200, f"status={r.status_code}")
178
- except Exception as e:
179
- test("4.3 GET /settings/instagram", False, str(e))
180
-
181
- try:
182
- r = requests.post(f"{BASE_URL}/settings/instagram", headers=HEADERS,
183
- json={"username": "testuser", "password": "testpass"}, timeout=10)
184
- test("4.4 POST /settings/instagram", r.status_code == 200, f"status={r.status_code}")
185
- except Exception as e:
186
- test("4.4 POST /settings/instagram", False, str(e))
187
-
188
- # Phase 5: Export/Import
189
- print("\n--- Phase 5: Export/Import ---")
190
-
191
- try:
192
- r = requests.get(f"{BASE_URL}/export", headers=HEADERS, timeout=15)
193
- test("5.1 GET /export", r.status_code == 200, f"status={r.status_code}")
194
- ed = r.json()
195
- print(f" exported {len(ed.get('posts', []))} posts, {len(ed.get('collections', []))} collections")
196
- except Exception as e:
197
- test("5.1 GET /export", False, str(e))
198
-
199
- try:
200
- r = requests.post(f"{BASE_URL}/import", headers=HEADERS,
201
- json={"posts": [], "collections": [], "mode": "merge"}, timeout=15)
202
- test("5.2 POST /import (merge, empty)", r.status_code == 200, f"status={r.status_code}")
203
- except Exception as e:
204
- test("5.2 POST /import", False, str(e))
205
-
206
- # Phase 6: Analysis
207
- print("\n--- Phase 6: Analysis (YouTube) ---")
208
-
209
- try:
210
- print(" submitting YouTube URL... (timeout 90s)")
211
- r = requests.post(f"{BASE_URL}/analyze", headers=HEADERS,
212
- json={"url": TEST_YT_URL}, timeout=90)
213
- data = r.json()
214
- if r.status_code == 200:
215
- test("6.1 POST /analyze (YouTube)", data.get("success") == True, f"success={data.get('success')}")
216
- if data.get("data"):
217
- d = data["data"]
218
- print(f" title: {d.get('title', 'N/A')[:60]}")
219
- print(f" category: {d.get('category', 'N/A')}")
220
- print(f" tags: {str(d.get('tags', [])[:5])}")
221
- print(f" cached: {data.get('cached', False)}")
222
- elif r.status_code == 202:
223
- skip("6.1 POST /analyze (YouTube)", "queued for retry (quota)")
224
- elif r.status_code == 503:
225
- skip("6.1 POST /analyze (YouTube)", "503 server busy")
226
- else:
227
- test("6.1 POST /analyze (YouTube)", False, f"status={r.status_code}")
228
- except requests.exceptions.Timeout:
229
- skip("6.1 POST /analyze (YouTube)", "timed out (90s)")
230
- except Exception as e:
231
- test("6.1 POST /analyze (YouTube)", False, str(e))
232
-
233
- # Summary
234
- print("\n" + "=" * 50)
235
- print(f" PASSED: {passed}")
236
- print(f" FAILED: {failed}")
237
- print(f" SKIPPED: {skipped}")
238
- print(f" TOTAL: {passed + failed + skipped}")
239
- print("=" * 50)
240
-
241
- sys.exit(1 if failed > 0 else 0)
File without changes
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env python3
2
- import requests
3
- import json
4
-
5
- # Read token
6
- with open('token.txt', 'r') as f:
7
- token = f.read().strip()
8
-
9
- # Test the /recent endpoint
10
- response = requests.get(
11
- 'http://localhost:5000/recent?limit=10',
12
- headers={'X-API-Key': token}
13
- )
14
-
15
- print(f"Status Code: {response.status_code}")
16
- print(f"\nResponse:")
17
- print(json.dumps(response.json(), indent=2))
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env python3
2
- import sys
3
- from pathlib import Path
4
-
5
- # Ensure backend root is in sys.path
6
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
7
-
8
- from core.database import get_db
9
-
10
- db = get_db()
11
- posts = db.get_recent(5)
12
- print(f'Found {len(posts)} posts in database')
13
- print()
14
-
15
- for p in posts:
16
- print(f"Shortcode: {p.get('shortcode', 'N/A')}")
17
- print(f"Title: {p.get('title', 'N/A')}")
18
- print(f"Username: {p.get('username', 'N/A')}")
19
- print(f"URL: {p.get('url', 'N/A')}")
20
- print(f"Category: {p.get('category', 'N/A')}")
21
- print(f"Analyzed: {p.get('analyzed_at', 'N/A')}")
22
- print("-" * 60)
@@ -1,65 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Legacy filename retained for compatibility.
4
- This script now validates API key authentication behavior.
5
- """
6
-
7
- import sys
8
- from pathlib import Path
9
-
10
- from fastapi.testclient import TestClient
11
-
12
- # Ensure backend root is in sys.path
13
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
14
-
15
- from api import app, API_TOKEN, generate_api_token # noqa: E402
16
-
17
-
18
- client = TestClient(app)
19
-
20
-
21
- def test_generate_api_token_shape():
22
- token = generate_api_token()
23
- assert isinstance(token, str)
24
- assert len(token) == 8
25
- assert token.isalnum()
26
-
27
-
28
- def test_ping_works_without_auth():
29
- response = client.get('/ping')
30
- assert response.status_code == 200
31
-
32
-
33
- def test_queue_status_rejects_invalid_api_key():
34
- response = client.get('/queue-status', headers={'X-API-Key': 'INVALID_TOKEN'})
35
- assert response.status_code == 401
36
-
37
-
38
- def test_queue_status_accepts_valid_api_key():
39
- response = client.get('/queue-status', headers={'X-API-Key': API_TOKEN})
40
- assert response.status_code == 200
41
-
42
-
43
- def test_connect_endpoint_is_deprecated():
44
- response = client.post('/connect', json={'api_key': API_TOKEN})
45
- assert response.status_code == 410
46
-
47
-
48
- if __name__ == '__main__':
49
- print('Running API key auth tests...')
50
- test_generate_api_token_shape()
51
- print('PASS: API token generation shape')
52
-
53
- test_ping_works_without_auth()
54
- print('PASS: /ping unauthenticated access')
55
-
56
- test_queue_status_rejects_invalid_api_key()
57
- print('PASS: /queue-status rejects invalid API key')
58
-
59
- test_queue_status_accepts_valid_api_key()
60
- print('PASS: /queue-status accepts valid API key')
61
-
62
- test_connect_endpoint_is_deprecated()
63
- print('PASS: /connect deprecated behavior')
64
-
65
- print('\nAll API key auth tests passed!')