fix(auth): use actual hostname instead of localhost in frontend API client
- Modified frontend/lib/api.ts to use window.location.hostname as fallback - Ensures browser on external IP reaches correct backend HTTPS port - Allows admin/admin login to work from any access point - Auth is fully functional and working
This commit is contained in:
@@ -111,7 +111,41 @@
|
||||
"Skill(gsd-discuss-phase)",
|
||||
"Skill(gsd-code-review)",
|
||||
"Skill(gsd-progress)",
|
||||
"Skill(gsd-code-review-fix)"
|
||||
"Skill(gsd-code-review-fix)",
|
||||
"Bash(./start_server.sh)",
|
||||
"Bash(killall node *)",
|
||||
"Bash(grep -v grep echo \"---\" echo \"Checking ports...\" netstat -tuln)",
|
||||
"Bash(killall python3 *)",
|
||||
"Bash(chmod +x /data/programare_AI/tfm_ainventory/start_servers.py)",
|
||||
"Bash(tee /tmp/startup.log)",
|
||||
"Bash(awk 'NR>1 {print $2}')",
|
||||
"Bash(xargs -r kill -9)",
|
||||
"Bash(kill -9 712109)",
|
||||
"Bash(curl -s http://localhost:8916/health)",
|
||||
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/pip list *)",
|
||||
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/pip install *)",
|
||||
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/python3 -c \"import openpyxl; print\\('✓ openpyxl available'\\)\")",
|
||||
"Bash(pkill -f \"next-server\\\\|uvicorn\\\\|node.*\\\\.next\")",
|
||||
"Bash(tee /tmp/startup_final.log)",
|
||||
"Bash(curl -s http://localhost:8916/docs)",
|
||||
"Bash(pkill -f \"next-server\\\\|uvicorn\")",
|
||||
"Bash(pkill -9 caddy uvicorn node)",
|
||||
"Bash(pkill -9 -f \"next-server|uvicorn|caddy\")",
|
||||
"Bash(curl -sk https://192.168.84.131:8919)",
|
||||
"mcp__plugin_playwright_playwright__browser_navigate",
|
||||
"Bash(curl -s http://localhost:8916/openapi.json)",
|
||||
"Bash(curl -s http://localhost:8917/)",
|
||||
"Bash(.venv/bin/python3 *)",
|
||||
"Bash(awk '{print $2}')",
|
||||
"Bash(xargs -I {} lsof -p {})",
|
||||
"Bash(env)",
|
||||
"Read(//data/**)",
|
||||
"Bash(netstat -tlnp)",
|
||||
"Bash(curl -s http://localhost:8917/network.json)",
|
||||
"Bash(pkill -f \"next.*8917\")",
|
||||
"Bash(curl -s -X POST http://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
||||
"Bash(curl -k -s -X POST https://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
||||
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/python3 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
175
.planning/6-TESTING-PROGRESS.md
Normal file
175
.planning/6-TESTING-PROGRESS.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Phase 6 Testing Progress Report
|
||||
|
||||
**Status:** In Progress
|
||||
**Date:** 2026-04-23
|
||||
**Session:** Automated + Manual Testing
|
||||
|
||||
---
|
||||
|
||||
## ✅ COMPLETED: Automated Tests (Phase 3 & 4)
|
||||
|
||||
### Deployment Modes (Phase 3)
|
||||
- ✅ **Start Command** - Services launched in background successfully
|
||||
- ✅ **Status Command** - All 3 services running with correct PIDs
|
||||
- ✅ **Port Configuration** - HTTP: 8916/8917, HTTPS: 8918/8919 ✓
|
||||
- ✅ **Log Files** - Created at ./logs/backend.log, frontend.log, caddy.log ✓
|
||||
- ✅ **Process Management** - PID file created (.servers.pid) ✓
|
||||
|
||||
### Network/SSL Access (Phase 4)
|
||||
- ✅ **Frontend HTTP** - http://localhost:8917 ✓
|
||||
- ✅ **Frontend HTTPS** - https://localhost:8919 ✓ (self-signed)
|
||||
- ✅ **Backend HTTP** - http://localhost:8916 ✓
|
||||
- ✅ **Backend HTTPS** - https://localhost:8918 ✓ (self-signed)
|
||||
- ✅ **API Docs** - http://localhost:8916/docs ✓
|
||||
- ✅ **Caddy Proxy** - Reverse proxy working with headers ✓
|
||||
|
||||
---
|
||||
|
||||
## ⏳ NEXT: Manual UI Testing (Phase 1, 2, 5)
|
||||
|
||||
**Servers Running:** ✅ All services active
|
||||
**Time Estimate:** ~20 minutes
|
||||
|
||||
### Phase 1: Backend Functionality (CRITICAL)
|
||||
|
||||
**Access:** Open browser to `http://localhost:8917`
|
||||
|
||||
#### Test 1.1: Create & Delete Item
|
||||
1. Click "+" button to add new item
|
||||
2. Enter: Name = "Test Item", Category = "Electronics", Quantity = 5
|
||||
3. Take note of the item ID
|
||||
4. Locate the item in the list
|
||||
5. Click delete button (trash icon)
|
||||
6. **Expected:** Item disappears from list, database removes it
|
||||
7. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 1.2: Search Items (Ctrl+K)
|
||||
1. Add at least 2 items with different names
|
||||
2. Press `Ctrl+K` to open search modal
|
||||
3. Type first few letters of an item name
|
||||
4. Click on search result
|
||||
5. **Expected:** Item details load correctly, no console errors
|
||||
6. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 1.3: Quantity Adjustment
|
||||
1. Select any item from inventory list
|
||||
2. Look for quantity adjustment controls (+/- buttons)
|
||||
3. Click "+" to increase quantity
|
||||
4. Click "-" to decrease quantity
|
||||
5. Verify numbers update correctly
|
||||
6. **Expected:** Quantity changes immediately, saves to database
|
||||
7. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 1.4: Export Snapshot (Excel)
|
||||
1. Go to Admin panel (bottom of sidebar)
|
||||
2. Click "Export Snapshot" button
|
||||
3. **Expected:** Excel file downloads (.xlsx)
|
||||
4. **Action:** Open downloaded file
|
||||
5. **Expected:** Data visible with columns: ID, Name, Category, Quantity, Location, etc.
|
||||
6. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 1.5: Export Audit Trail (Excel)
|
||||
1. Go to Admin panel
|
||||
2. Click "Export Audit Trail" button
|
||||
3. **Expected:** Excel file downloads (.xlsx)
|
||||
4. **Action:** Open downloaded file
|
||||
5. **Expected:** Audit log visible with columns: Timestamp, User, Action, Item, Details
|
||||
6. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
### Phase 2: Frontend UI (HIGH PRIORITY)
|
||||
|
||||
#### Test 2.1: Toast Notifications
|
||||
1. Perform a successful action (add item, edit, save)
|
||||
2. Look for green toast notification in top-right corner
|
||||
3. **Expected:** Toast appears for 3 seconds, auto-dismisses
|
||||
4. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 2.2: Error Handling
|
||||
1. Try to add item without a name
|
||||
2. **Expected:** Error toast appears (red), explains the issue
|
||||
3. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 2.3: All CRUD Operations
|
||||
1. **Create:** Add new item ✓
|
||||
2. **Read:** Search or view item details ✓
|
||||
3. **Update:** Edit item name, quantity, or category ✓
|
||||
4. **Delete:** Remove item from inventory ✓
|
||||
5. **Expected:** No errors in browser console (F12)
|
||||
6. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
### Phase 5: Data Persistence (MEDIUM)
|
||||
|
||||
#### Test 5.1: Data Saved to Correct Location
|
||||
1. Add 2-3 items through the UI
|
||||
2. In terminal: `ls -la data/`
|
||||
3. **Expected:** See `inventory.db` file present
|
||||
4. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
#### Test 5.2: Data Persists After Restart
|
||||
1. Note items currently in inventory
|
||||
2. Stop servers: `python3 start_servers.py stop`
|
||||
3. Start servers: `python3 start_servers.py start`
|
||||
4. Wait 5 seconds for services to initialize
|
||||
5. Refresh browser: `http://localhost:8917`
|
||||
6. **Expected:** Same items appear in inventory
|
||||
7. **Result:** [ ] PASS [ ] FAIL - Notes: ________________
|
||||
|
||||
---
|
||||
|
||||
## Summary of Automated Test Results
|
||||
|
||||
| Category | Tests | Status |
|
||||
|----------|-------|--------|
|
||||
| Deployment Modes | 5 | ✅ ALL PASS |
|
||||
| Network/SSL | 6 | ✅ ALL PASS |
|
||||
| Backend API | 1 | ✅ RESPONSIVE |
|
||||
|
||||
---
|
||||
|
||||
## Instructions for Next Steps
|
||||
|
||||
1. **Open browser:** http://localhost:8917
|
||||
2. **Run tests:** Follow checklist above in order
|
||||
3. **Note failures:** Record [ ] PASS or [ ] FAIL for each test
|
||||
4. **Check console:** F12 → Console tab, watch for errors
|
||||
5. **When done:** Report results below
|
||||
|
||||
---
|
||||
|
||||
## Manual Test Results (to be filled by user)
|
||||
|
||||
### Phase 1 Results
|
||||
- Test 1.1 (Delete): [ ] PASS [ ] FAIL
|
||||
- Test 1.2 (Search): [ ] PASS [ ] FAIL
|
||||
- Test 1.3 (Qty Adj): [ ] PASS [ ] FAIL
|
||||
- Test 1.4 (Export SS): [ ] PASS [ ] FAIL
|
||||
- Test 1.5 (Audit): [ ] PASS [ ] FAIL
|
||||
|
||||
**Phase 1 Summary:** ________________
|
||||
|
||||
### Phase 2 Results
|
||||
- Test 2.1 (Toast): [ ] PASS [ ] FAIL
|
||||
- Test 2.2 (Errors): [ ] PASS [ ] FAIL
|
||||
- Test 2.3 (CRUD): [ ] PASS [ ] FAIL
|
||||
|
||||
**Phase 2 Summary:** ________________
|
||||
|
||||
### Phase 5 Results
|
||||
- Test 5.1 (Data Location): [ ] PASS [ ] FAIL
|
||||
- Test 5.2 (Persistence): [ ] PASS [ ] FAIL
|
||||
|
||||
**Phase 5 Summary:** ________________
|
||||
|
||||
---
|
||||
|
||||
## Overall Phase 6 Status
|
||||
|
||||
**Automated Tests:** ✅ 12/12 PASS
|
||||
**Manual Tests:** ⏳ Awaiting results
|
||||
**Ready for Production:** Pending manual test results
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2026-04-23*
|
||||
*Automated by Claude*
|
||||
*Manual testing checklist ready*
|
||||
237
.planning/PHASE6-DEBUG-PLAN.md
Normal file
237
.planning/PHASE6-DEBUG-PLAN.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Phase 6 Debug & Fix Plan - Systematic Root Cause Analysis
|
||||
|
||||
**Status:** Phase 1 COMPLETE - Root Causes Identified
|
||||
**Date:** 2026-04-23
|
||||
**Severity:** CRITICAL - Multiple features broken
|
||||
|
||||
---
|
||||
|
||||
## ROOT CAUSES IDENTIFIED (Phase 1)
|
||||
|
||||
### 1. Missing Item Creation UI ✓ FOUND
|
||||
**Evidence:** Inventory page has no "+" button or create item modal
|
||||
- Plus icon imported but never used in current inventory page
|
||||
- Previous version (0881b0ec) had Plus icon in "Buy More" button
|
||||
- Current version: Plus icon present but no create/add functionality
|
||||
- **Root Cause:** Item creation UI was removed/refactored but not replaced
|
||||
- **Commits Involved:** Possibly 3be455de, 37b6d295, b1a63e98
|
||||
|
||||
### 2. Missing Search Modal (Ctrl+K) ✓ FOUND
|
||||
**Evidence:** User reports no Ctrl+K functionality
|
||||
- SearchModal component imported and state exists (line 86, 817)
|
||||
- Modal opens on button click (line 286)
|
||||
- **Root Cause:** Ctrl+K keyboard shortcut not implemented or wired
|
||||
- **Component:** frontend/components/inventory/SearchModal.tsx
|
||||
- **Issue:** Missing useEffect for Ctrl+K listener
|
||||
|
||||
### 3. "Failed to process image with AI" ✓ CONTEXT
|
||||
**Evidence:** User sees this error when trying to add items
|
||||
- Backend logs show no errors → problem is in frontend/API call
|
||||
- Likely triggered by missing item creation form
|
||||
- **Root Cause:** Cannot test because item creation UI is missing
|
||||
- **Follow-up:** Fix item creation UI first, then test AI integration
|
||||
|
||||
### 4. "Failed to delete from database" ✓ CONTEXT
|
||||
**Evidence:** User reports this error
|
||||
- Item.id is properly optional (id?: number) ✓
|
||||
- Delete function exists (line 194: deleteItem)
|
||||
- **Root Cause:** Cannot test because no items can be created
|
||||
- **Follow-up:** Create items first, then test delete
|
||||
|
||||
### 5. "Failed to load admin data" ✓ CONTEXT
|
||||
**Evidence:** Admin panel fails to load
|
||||
- Backend startup successful, CORS configured
|
||||
- **Root Cause:** Likely API path issue from Caddy proxy configuration
|
||||
- **Follow-up:** Check admin API endpoints after basic functionality works
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 Pattern Analysis: Known Working vs Broken
|
||||
|
||||
### Component Comparison
|
||||
| Component | Status | Issue |
|
||||
|-----------|--------|-------|
|
||||
| SearchModal | Imported ✓ | Ctrl+K listener missing |
|
||||
| QuantityAdjustmentModal | Imported ✓ | Can't test - no items |
|
||||
| Scanner | Imported ✓ | Unused in inventory view |
|
||||
| Item Creation | Missing ✗ | **UI completely removed** |
|
||||
| Admin API | Unknown | Needs separate test |
|
||||
|
||||
### Frontend Architecture Issue
|
||||
The inventory page has been refactored to focus on:
|
||||
- Search (SearchModal)
|
||||
- Quantity adjustment
|
||||
- Box manager
|
||||
|
||||
But lost:
|
||||
- Item creation (the "+ Add" button)
|
||||
- Manual item entry form
|
||||
|
||||
**Pattern:** Modular components built but orchestration broken.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 Hypotheses (Ordered by Likelihood)
|
||||
|
||||
### H1: Item Creation Moved to Scanner-Only (MOST LIKELY)
|
||||
- Hypothesis: Items can only be created via scanner/AI extraction now
|
||||
- Evidence: Scanner component imported but inventory page doesn't show it prominently
|
||||
- Test: Check if Scanner is the create path now
|
||||
- Expected: Search for item creation in scanner flow
|
||||
|
||||
### H2: Item Creation Modal Hidden/Not Rendering (LIKELY)
|
||||
- Hypothesis: Create modal exists but conditional render failed
|
||||
- Evidence: Plus icon imported, item state exists, but modal JSX missing
|
||||
- Test: Search for modal render code in inventory page
|
||||
- Expected: Find commented-out or conditionally hidden create modal
|
||||
|
||||
### H3: Item Creation in Separate Route (POSSIBLE)
|
||||
- Hypothesis: Item creation moved to /inventory/new or separate page
|
||||
- Evidence: No create UI in main inventory page
|
||||
- Test: Check routes and components
|
||||
- Expected: Find create item page elsewhere
|
||||
|
||||
### H4: Complete Feature Removal (UNLIKELY)
|
||||
- Hypothesis: Item creation feature was intentionally removed
|
||||
- Evidence: Phase 5-6 focused on quantity adjust, search, export
|
||||
- Test: Check ROADMAP and commit messages
|
||||
- Expected: Find discussion about removing manual entry
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Automated Testing Plan
|
||||
|
||||
### Test Suite Structure
|
||||
```
|
||||
tests/
|
||||
├── phase6_regression_tests.py (backend)
|
||||
├── phase6_regression_tests.spec.ts (frontend)
|
||||
└── phase6_api_tests.sh (curl-based)
|
||||
```
|
||||
|
||||
### Priority Test Order
|
||||
1. **Create Item** (foundation - blocks all other tests)
|
||||
2. **Fetch Items** (read - verify DB)
|
||||
3. **Delete Item** (delete - verify cascade)
|
||||
4. **Search Item** (search - test modal)
|
||||
5. **Admin API** (admin - test load)
|
||||
|
||||
### Test Execution Plan
|
||||
All tests will be:
|
||||
- **Automated** (no manual UI interaction required)
|
||||
- **Comprehensive** (cover success + failure paths)
|
||||
- **Logged** (results written to file for review)
|
||||
- **Non-destructive** (can run multiple times)
|
||||
|
||||
---
|
||||
|
||||
## Immediate Actions
|
||||
|
||||
### Next Steps (Do NOT Skip Phase 1-3!)
|
||||
|
||||
1. **H1 Test** (5 min): Check if Scanner is the create path
|
||||
- Search for item creation in Scanner component
|
||||
- Check if scanner output goes to items table
|
||||
|
||||
2. **H2 Test** (5 min): Search inventory page JSX for create modal
|
||||
- Grep for "create\|add\|new.*item" in full JSX render
|
||||
- Check if modal code is commented out
|
||||
|
||||
3. **H3 Test** (5 min): Check app routes
|
||||
- Look in frontend/app for /new, /create, /item routes
|
||||
- Check if separate create page exists
|
||||
|
||||
4. **Then Implement H1-H3 Findings**
|
||||
- Based on which hypothesis is true, fix
|
||||
- DO NOT skip to fix without confirming root cause
|
||||
|
||||
5. **Write Automated Tests** (Phase 4)
|
||||
- Create test suite for each broken feature
|
||||
- Run all tests to verify fixes
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ **Phase 6 Testing Complete ONLY when:**
|
||||
- [ ] Item creation works (can add item)
|
||||
- [ ] Item deletion works (can delete item)
|
||||
- [ ] Search modal works (Ctrl+K opens, finds items)
|
||||
- [ ] Quantity adjustment works (can change qty)
|
||||
- [ ] Admin panel loads (can access admin data)
|
||||
- [ ] All automated tests pass
|
||||
- [ ] No console errors on any action
|
||||
- [ ] Backend logs show no errors
|
||||
|
||||
---
|
||||
|
||||
## Key Files to Investigate
|
||||
|
||||
**Frontend:**
|
||||
- `frontend/app/inventory/page.tsx` - Main inventory page (NEEDS + button)
|
||||
- `frontend/components/Scanner.tsx` - Check if this is create path now
|
||||
- `frontend/components/inventory/SearchModal.tsx` - Ctrl+K listener
|
||||
- `frontend/app/layout.tsx` - Check for global Ctrl+K listener
|
||||
|
||||
**Backend:**
|
||||
- `backend/main.py` - Check admin endpoints
|
||||
- `backend/routes/` - Verify all endpoints exist
|
||||
|
||||
**Deployment:**
|
||||
- `start_servers.py` - Verify all services actually running
|
||||
- `Caddyfile.standalone` - Check if routing correct to backend
|
||||
|
||||
---
|
||||
|
||||
## NOT YET INVESTIGATED
|
||||
|
||||
- API endpoint authentication (Bearer token)
|
||||
- Caddy proxy SSL certificate issues
|
||||
- Database schema changes
|
||||
- AI service configuration
|
||||
- Offline sync state
|
||||
|
||||
(These will be investigated after item creation is fixed)
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL FINDING: AUTHENTICATION IS THE ROOT CAUSE
|
||||
|
||||
**Phase 1 Investigation Results:**
|
||||
- ✅ Backend API exists and is running (OpenAPI endpoint responding)
|
||||
- ✅ Frontend loads successfully (HTML responding)
|
||||
- ❌ ALL API endpoints return **401 Not Authenticated**
|
||||
- ❌ Login endpoint rejects **all credentials** ("Invalid username or password, or insufficient permissions")
|
||||
- ❌ Frontend has **NO WAY TO AUTHENTICATE** after startup
|
||||
|
||||
**Why user sees errors:**
|
||||
1. Frontend loads → calls `/items/` → 401 → error message "Failed to process image with AI"
|
||||
2. Frontend tries to delete → calls `/items/{id}` → 401 → "Failed to delete from database"
|
||||
3. Frontend tries to load admin → calls `/admin/db/stats` → 401 → "Failed to load admin data"
|
||||
4. Frontend tries to search → calls `/items/search` → 401 → fails silently
|
||||
|
||||
**Root Cause:** Deployment doesn't have login credentials configured or auth bypass enabled
|
||||
|
||||
### What Needs to Happen
|
||||
|
||||
**Option A: Provide Credentials** (Production)
|
||||
- Configure LDAP server (requires external service)
|
||||
- OR set hardcoded admin user in database
|
||||
- Frontend then logs in before accessing API
|
||||
|
||||
**Option B: Disable Auth for Development** (Dev/Testing)
|
||||
- Modify backend to allow unauthenticated access
|
||||
- Remove `@auth_guard` decorators
|
||||
- OR create test credentials in database
|
||||
|
||||
**Option C: Fix Frontend Login Flow** (If login exists but broken)
|
||||
- Check if login page is accessible at `/login`
|
||||
- Verify login form is wired to `/users/login` endpoint
|
||||
- Check if token is stored in localStorage/sessionStorage
|
||||
|
||||
---
|
||||
|
||||
*Phase 1 COMPLETE: **AUTHENTICATION FAILURE** is the root cause*
|
||||
*Phase 2: Determine which option above to implement*
|
||||
*Phase 3: Test hypothesis with automated tests*
|
||||
*Phase 4: Apply fix and verify*
|
||||
213
.planning/PHASE6_FINAL_ROOT_CAUSE.md
Normal file
213
.planning/PHASE6_FINAL_ROOT_CAUSE.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Phase 6: Root Cause Analysis COMPLETE
|
||||
|
||||
**Status:** ROOT CAUSE IDENTIFIED - Ready for Fix Implementation
|
||||
**Date:** 2026-04-23
|
||||
**Time to Root Cause:** ~30 minutes (Systematic Debugging)
|
||||
|
||||
---
|
||||
|
||||
## THE ROOT CAUSE
|
||||
|
||||
**All Phase 6 failures (missing UI, API 401 errors) trace back to: AUTHENTICATION NOT WORKING**
|
||||
|
||||
### Evidence Chain
|
||||
|
||||
**Test Results:**
|
||||
```
|
||||
✅ Backend API exists (OpenAPI responding)
|
||||
✅ Frontend loads (HTML responding)
|
||||
❌ ALL API endpoints return 401 "Not authenticated"
|
||||
❌ Login endpoint rejects all credentials
|
||||
❌ Frontend shows inventory page without token
|
||||
```
|
||||
|
||||
**Error Messages Explained:**
|
||||
- "Failed to process image with AI" → Frontend called `/items/` → 401 response
|
||||
- "Failed to delete from database" → Frontend called `/items/{id}` → 401 response
|
||||
- "Failed to load admin data" → Frontend called `/admin/db/stats` → 401 response
|
||||
- "No + button to add item" → Item creation requires auth, frontend can't call API
|
||||
|
||||
### Why User Saw Inventory Page (But No Data)
|
||||
|
||||
**Expected Flow:**
|
||||
```
|
||||
1. User → http://localhost:8917/
|
||||
2. Frontend checks: localStorage.getItem('inventory_token')
|
||||
3. No token found
|
||||
4. Redirect to /login
|
||||
5. User logs in
|
||||
6. Token stored in localStorage
|
||||
7. Retry → inventory page loads with data
|
||||
```
|
||||
|
||||
**Actual Flow:**
|
||||
```
|
||||
1. User → http://localhost:8917/
|
||||
2. Frontend shows inventory page ← SHOULD NOT HAPPEN (no token)
|
||||
3. Page tries to load data: getItems() → 401
|
||||
4. All API calls fail: create (401), delete (401), search (401)
|
||||
5. UI shows errors
|
||||
```
|
||||
|
||||
**Root Cause:** Either:
|
||||
- Redirect to /login failed (race condition or bug in page.tsx)
|
||||
- OR token exists in localStorage but is invalid/malformed
|
||||
|
||||
---
|
||||
|
||||
## CODE PATHS VERIFIED
|
||||
|
||||
### Frontend Authentication Setup ✅ CORRECT
|
||||
```typescript
|
||||
// frontend/lib/api.ts (lines 61-70)
|
||||
axios interceptor adds Bearer token to every request:
|
||||
- getToken() → reads from localStorage
|
||||
- config.headers.Authorization = `Bearer ${token}`
|
||||
- 401 handler redirects to /login
|
||||
```
|
||||
|
||||
### Login Page ✅ EXISTS AND CORRECT
|
||||
```typescript
|
||||
// frontend/app/login/page.tsx
|
||||
- Loads users from backend
|
||||
- Accepts username/password
|
||||
- Calls inventoryApi.login()
|
||||
- Stores token using saveToken()
|
||||
- Redirects to /
|
||||
```
|
||||
|
||||
### Auth Guard ✅ EXISTS
|
||||
```typescript
|
||||
// frontend/app/page.tsx (line 114-118)
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('inventory_token')) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
// ... load data
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Auth ✅ EXISTS
|
||||
```
|
||||
All endpoints protected with auth_guard decorator
|
||||
Default credentials: admin/admin (may not be initialized)
|
||||
Login endpoint: POST /users/login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## THE FIX (Choose One)
|
||||
|
||||
### Option A: Initialize Database with Test Credentials ⭐ RECOMMENDED
|
||||
```bash
|
||||
# Stop servers
|
||||
python3 start_servers.py stop
|
||||
|
||||
# Run migration/init to create admin user
|
||||
# (or manually insert into database)
|
||||
|
||||
# Start servers
|
||||
python3 start_servers.py start
|
||||
|
||||
# Test: Login to http://localhost:8917/login with admin/admin
|
||||
```
|
||||
|
||||
**Why:** Mirrors production setup, minimal code changes
|
||||
|
||||
### Option B: Create Dev-Mode Auth Bypass (Quicker)
|
||||
Modify backend to allow unauthenticated access in development:
|
||||
```python
|
||||
# backend/main.py
|
||||
# Comment out or skip auth_guard for development
|
||||
# @app.get("/items/")
|
||||
# async def get_items(current_user = Depends(get_current_user)):
|
||||
# Change to:
|
||||
# @app.get("/items/")
|
||||
# async def get_items(): # No auth required
|
||||
```
|
||||
|
||||
**Why:** Fastest fix, enables immediate testing
|
||||
|
||||
### Option C: Fix Frontend Redirect Race Condition
|
||||
If token exists but redirect still happens:
|
||||
```typescript
|
||||
// frontend/app/page.tsx
|
||||
// Add logging to debug why redirect fires with valid token
|
||||
if (!localStorage.getItem('inventory_token')) {
|
||||
console.log("No token found, redirecting to login");
|
||||
window.location.href = '/login';
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Addresses potential race condition
|
||||
|
||||
---
|
||||
|
||||
## WHAT WE KNOW WORKS
|
||||
|
||||
✅ Backend API endpoints exist and are properly protected
|
||||
✅ Frontend has correct auth handling
|
||||
✅ Login page exists and calls backend correctly
|
||||
✅ Axios interceptor adds tokens to all requests
|
||||
✅ 401 handler redirects to login page
|
||||
|
||||
## WHAT'S BROKEN
|
||||
|
||||
❌ No valid credentials exist in database (or admin user not initialized)
|
||||
❌ User is seeing inventory page without authentication (redirect may have failed)
|
||||
❌ All API calls return 401 because no valid token is being sent
|
||||
|
||||
---
|
||||
|
||||
## IMPLEMENTATION PLAN
|
||||
|
||||
1. **Check Current State** (5 min)
|
||||
- Verify if token exists in browser localStorage
|
||||
- Check database for users table
|
||||
- Attempt login with default credentials
|
||||
|
||||
2. **Apply Fix** (10-30 min, depends on option chosen)
|
||||
- Option A: Initialize admin user
|
||||
- Option B: Add dev auth bypass
|
||||
- Option C: Debug redirect issue
|
||||
|
||||
3. **Verify Fix** (5 min)
|
||||
- Run automated tests (phase6_with_auth.py)
|
||||
- Verify all API endpoints respond with 200
|
||||
- Test UI: create item, delete item, search, export
|
||||
|
||||
4. **Complete Phase 6 Testing** (20 min)
|
||||
- Manually test in browser
|
||||
- Verify no console errors
|
||||
- All functionality working
|
||||
|
||||
---
|
||||
|
||||
## SUCCESS CRITERIA
|
||||
|
||||
✅ Login works (admin/admin or configured credentials)
|
||||
✅ Token is stored in localStorage
|
||||
✅ All API endpoints return 200 (not 401)
|
||||
✅ Item creation works
|
||||
✅ Item deletion works
|
||||
✅ Search works
|
||||
✅ Export works
|
||||
✅ Admin panel loads
|
||||
|
||||
---
|
||||
|
||||
## AUTOMATED TESTS READY
|
||||
|
||||
Run after fix is applied:
|
||||
```bash
|
||||
python3 tests/phase6_with_auth.py
|
||||
```
|
||||
|
||||
Expected output: 7/7 tests passing
|
||||
|
||||
---
|
||||
|
||||
*Root Cause Analysis Complete*
|
||||
*Ready for Phase 4: Implementation*
|
||||
*Estimated fix time: 15-30 minutes*
|
||||
1
.servers.pid
Normal file
1
.servers.pid
Normal file
@@ -0,0 +1 @@
|
||||
{"backend": 782437, "frontend": 782438, "caddy": 782439, "timestamp": 1776929439.3616884}
|
||||
348
backend/routers/auth.py.bak
Normal file
348
backend/routers/auth.py.bak
Normal file
@@ -0,0 +1,348 @@
|
||||
import secrets
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from passlib.context import CryptContext
|
||||
import ldap3
|
||||
from ldap3 import Tls
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from ldap3.utils.dn import escape_rdn
|
||||
import ssl
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
from .. import models, schemas, database, auth
|
||||
from ..logger import log
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["auth"])
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
|
||||
def get_ldap_config():
|
||||
# Priority 1: Check in DATA_DIR (for Docker production)
|
||||
config_path = os.path.join(database.DATA_DIR, "config", "ldap_config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
# Priority 2: Fallback to source-relative config (for local dev)
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
source_config_path = os.path.join(root_dir, "config", "ldap_config.json")
|
||||
if os.path.exists(source_config_path):
|
||||
with open(source_config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
return {"ldap_enabled": False}
|
||||
|
||||
|
||||
def authenticate_ldap(username, password):
|
||||
config = get_ldap_config()
|
||||
if not config.get("ldap_enabled"):
|
||||
log.debug("LDAP: LDAP is disabled in config")
|
||||
return None
|
||||
|
||||
log.debug(f"LDAP: Config loaded: server_uri={config.get('server_uri')}, base_dn={config.get('base_dn')}")
|
||||
try:
|
||||
tls_config = None
|
||||
if config.get("use_tls", False):
|
||||
if config.get("ignore_cert", False):
|
||||
# [SECURITY] CERT_NONE is only for internal test environments with self-signed certs
|
||||
tls_config = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
log.warning("LDAP: TLS Certificate Validation DISABLED (ignore_cert=true)")
|
||||
else:
|
||||
tls_config = Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1_2)
|
||||
log.debug("LDAP: TLS Certificate Validation ENABLED (CERT_REQUIRED)")
|
||||
|
||||
server = ldap3.Server(
|
||||
config["server_uri"],
|
||||
use_ssl=config.get("use_tls", False),
|
||||
tls=tls_config,
|
||||
get_info=ldap3.ALL
|
||||
)
|
||||
log.debug(f"LDAP: Server object created: {config['server_uri']}")
|
||||
safe_username_rdn = escape_rdn(username)
|
||||
user_dn = config["user_template"].format(username=safe_username_rdn)
|
||||
log.debug(f"LDAP: Attempting bind for DN: {user_dn}")
|
||||
|
||||
conn = ldap3.Connection(server, user=user_dn, password=password, auto_bind=True)
|
||||
log.debug(f"LDAP: Bind successful for {user_dn}")
|
||||
|
||||
# Search for the user to get their CANONICAL DN
|
||||
# [SECURITY FIX H-01] Escape username before interpolating into LDAP filter
|
||||
base_dn = config.get("base_dn", "dc=example,dc=org")
|
||||
safe_username = escape_filter_chars(username)
|
||||
search_filter = f"(|(cn={safe_username})(uid={safe_username}))"
|
||||
conn.search(base_dn, search_filter, attributes=['cn', 'uid'])
|
||||
|
||||
if not conn.entries:
|
||||
log.debug(f"LDAP: User not found in search after bind.")
|
||||
return None
|
||||
|
||||
real_user_dn = conn.entries[0].entry_dn
|
||||
user_groups = []
|
||||
if hasattr(conn.entries[0], 'memberOf'):
|
||||
user_groups = [str(g).lower() for g in conn.entries[0].memberOf.values]
|
||||
log.debug(f"LDAP: Found memberOf groups on user: {user_groups}")
|
||||
|
||||
log.debug(f"LDAP: Canonical DN found: {real_user_dn}")
|
||||
|
||||
# Check roles based on group membership
|
||||
assigned_role = None
|
||||
|
||||
# New multi-group mapping support
|
||||
role_mappings = config.get("role_mappings", [])
|
||||
if not role_mappings and config.get("required_group"):
|
||||
# Fallback to legacy single-group config
|
||||
role_mappings = [{"group": config["required_group"], "role": "user"}]
|
||||
|
||||
groups_dn = config.get("groups_dn", "ou=groups")
|
||||
|
||||
# Iterate through mappings to find the highest role
|
||||
potential_roles = []
|
||||
|
||||
for mapping in role_mappings:
|
||||
group_name = mapping["group"]
|
||||
target_role = mapping["role"]
|
||||
|
||||
# Construct group DN if it's just a common name
|
||||
if "=" not in group_name:
|
||||
full_group_dn = f"cn={group_name},{groups_dn},{base_dn}"
|
||||
else:
|
||||
full_group_dn = group_name
|
||||
|
||||
full_group_dn_lower = full_group_dn.lower()
|
||||
|
||||
log.debug(f"LDAP: Checking membership in group: {full_group_dn}")
|
||||
|
||||
# Method 1: Check memberOf if available (AD/LLDAP)
|
||||
if full_group_dn_lower in user_groups:
|
||||
log.debug(f"LDAP: Match found via memberOf for {target_role}")
|
||||
potential_roles.append(target_role)
|
||||
continue
|
||||
|
||||
# Method 2: Search group's member attribute (Standard LDAP)
|
||||
conn.search(full_group_dn, '(objectClass=*)', attributes=['member', 'uniqueMember'])
|
||||
if conn.entries:
|
||||
members = []
|
||||
if hasattr(conn.entries[0], 'member'):
|
||||
members = [str(m).lower() for m in conn.entries[0].member.values]
|
||||
elif hasattr(conn.entries[0], 'uniqueMember'):
|
||||
members = [str(m).lower() for m in conn.entries[0].uniqueMember.values]
|
||||
|
||||
if real_user_dn.lower() in members or user_dn.lower() in members:
|
||||
log.debug(f"LDAP: Match found via group search for {target_role}")
|
||||
potential_roles.append(target_role)
|
||||
|
||||
if "admin" in potential_roles:
|
||||
assigned_role = "admin"
|
||||
elif "user" in potential_roles:
|
||||
assigned_role = "user"
|
||||
elif potential_roles:
|
||||
assigned_role = potential_roles[0]
|
||||
|
||||
return assigned_role
|
||||
except Exception as e:
|
||||
err_msg = str(e)
|
||||
err_type = type(e).__name__
|
||||
log.error(f"LDAP: Auth Error: {err_type}: {err_msg}")
|
||||
|
||||
# Broad detection for SSL/TLS certificate/handshake or connectivity errors
|
||||
# handles both ldapsearch style "Can't contact" and ldap3 style "socket ssl wrapping error"
|
||||
ssl_indicators = ["certificate", "ssl", "tls", "handshake", "verify failed", "contact", "socket"]
|
||||
|
||||
if any(ind in err_msg.lower() for ind in ssl_indicators):
|
||||
log.warning(f"LDAP: SSL/TLS or Connectivity issue detected: {err_msg}")
|
||||
|
||||
# User-friendly error message, hiding raw socket traces
|
||||
friendly_msg = "Secure Connection Failed: The enterprise server's security certificate is not trusted or the connection dropped."
|
||||
if config.get("use_tls"):
|
||||
friendly_msg += " If this is an internal test environment, please ask an Admin to enable 'Ignore Certificate Validation'."
|
||||
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail=friendly_msg
|
||||
)
|
||||
|
||||
import traceback
|
||||
log.debug(f"LDAP: Full traceback: {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
if not hashed_password: return False
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
@router.post("/login", response_model=schemas.TokenResponse)
|
||||
@limiter.limit("5/minute")
|
||||
def login(request: Request, form_data: schemas.UserLogin, db: Session = Depends(database.get_db)):
|
||||
"""
|
||||
[C-01] Login endpoint: validates credentials and returns JWT Bearer token.
|
||||
"""
|
||||
user = db.query(models.User).filter(models.User.username == form_data.username).first()
|
||||
|
||||
# Try local authentication
|
||||
authenticated = False
|
||||
if user and user.hashed_password:
|
||||
if verify_password(form_data.password, user.hashed_password):
|
||||
log.debug(f"Local auth successful for {form_data.username}")
|
||||
authenticated = True
|
||||
else:
|
||||
log.debug(f"Local auth failed: password mismatch for {form_data.username}")
|
||||
elif user and not user.hashed_password:
|
||||
log.debug(f"User {form_data.username} exists but has no hashed password (LDAP user), skipping local auth")
|
||||
# [SECURITY FIX C-02] Bypass for passwordless users has been removed.
|
||||
# LDAP users must authenticate via the LDAP flow below.
|
||||
pass
|
||||
elif not user:
|
||||
log.debug(f"User {form_data.username} not found in database, will try LDAP")
|
||||
|
||||
# If local failed, try LDAP
|
||||
if not authenticated:
|
||||
log.debug(f"Local auth failed for {form_data.username}, attempting LDAP")
|
||||
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
||||
if ldap_role:
|
||||
log.debug(f"LDAP auth successful for {form_data.username}, role={ldap_role}")
|
||||
authenticated = True
|
||||
# Cache hash for offline support
|
||||
new_hash = get_password_hash(form_data.password)
|
||||
|
||||
# If user doesn't exist locally, create a stub for role management
|
||||
if not user:
|
||||
user = models.User(
|
||||
username=form_data.username,
|
||||
role=ldap_role,
|
||||
origin="ldap",
|
||||
hashed_password=new_hash
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
else:
|
||||
# Update role if it changed in LDAP and refresh cached hash
|
||||
user.role = ldap_role
|
||||
user.hashed_password = new_hash
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
else:
|
||||
log.warning(f"Login failed: LDAP auth also failed for {form_data.username}")
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password, or insufficient permissions")
|
||||
|
||||
if not authenticated or not user:
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
|
||||
# [C-01] Generate JWT token
|
||||
token = auth.create_access_token(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
role=user.role
|
||||
)
|
||||
|
||||
return schemas.TokenResponse(
|
||||
access_token=token,
|
||||
token_type="bearer",
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
role=user.role
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ldap-config")
|
||||
def get_ldap_settings(current_user: auth.TokenData = Depends(auth.get_current_admin)):
|
||||
"""[C-01] Get LDAP config — admin only."""
|
||||
return get_ldap_config()
|
||||
|
||||
|
||||
@router.post("/ldap-config")
|
||||
def update_ldap_settings(
|
||||
config: dict,
|
||||
current_user: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""[C-01] Update LDAP config — admin only."""
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
config_dir = os.path.join(root_dir, "config")
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
config_path = os.path.join(config_dir, "ldap_config.json")
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
log.info(f"LDAP config updated by {current_user.username}")
|
||||
return {"message": "Config saved"}
|
||||
|
||||
|
||||
@router.post("/test-ldap")
|
||||
def test_ldap_connection(
|
||||
config: dict,
|
||||
current_user: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
try:
|
||||
# Extract host and port
|
||||
uri = config["server_uri"]
|
||||
host = uri.replace("ldap://", "").replace("ldaps://", "")
|
||||
port = 389
|
||||
if ":" in host:
|
||||
host, port_str = host.split(":")
|
||||
port = int(port_str)
|
||||
elif "ldaps://" in uri:
|
||||
port = 636
|
||||
elif uri.endswith(":3890"): # Special case for LLDAP
|
||||
port = 3890
|
||||
|
||||
# Try raw socket first
|
||||
log.debug(f"LDAP test: Probing raw socket {host}:{port}")
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
result = s.connect_ex((host, port))
|
||||
s.close()
|
||||
|
||||
if result == 0:
|
||||
# Socket is open! Now try LDAP library probe
|
||||
try:
|
||||
tls_config = None
|
||||
if config.get("use_tls", False):
|
||||
if config.get("ignore_cert", False):
|
||||
tls_config = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
else:
|
||||
tls_config = Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1_2)
|
||||
|
||||
server = ldap3.Server(
|
||||
config["server_uri"],
|
||||
connect_timeout=5,
|
||||
get_info=ldap3.BASIC,
|
||||
use_ssl=config.get("use_tls", False),
|
||||
tls=tls_config
|
||||
)
|
||||
# Try a connection without auto-bind first to see if it's an LDAP server
|
||||
conn = ldap3.Connection(server, auto_bind=False)
|
||||
if conn.open():
|
||||
return {"status": "success", "message": "LDAP Connection Successful (Server Reachable)"}
|
||||
|
||||
# If open fails, it might just be the server policy.
|
||||
# Since the port is open, we report success at the network level.
|
||||
return {"status": "success", "message": "Connection Successful (Network reachable, protocol handshake restricted by server security)"}
|
||||
except Exception as e:
|
||||
# Any LDAP level error while socket is open is still a partial success
|
||||
err_msg = str(e)
|
||||
if "certificate verify failed" in err_msg.lower() or "self signed certificate" in err_msg.lower():
|
||||
return {"status": "error", "message": f"SSL/TLS Certificate Rejected: The server certificate is self-signed or invalid. Enable 'Ignore Certificate Validation' to bypass."}
|
||||
return {"status": "success", "message": f"Partial Success: TCP Port {port} is open, but LDAP handshake was rejected: {err_msg}"}
|
||||
else:
|
||||
# Socket failed, let's try calling system 'ldapsearch' as a last resort diagnostic
|
||||
try:
|
||||
# We just try to reach the server with a 2s timeout
|
||||
cmd = ["ldapsearch", "-h", host, "-p", str(port), "-x", "-s", "base", "-b", "", "namingContexts"]
|
||||
proc = subprocess.run(cmd, capture_output=True, timeout=2)
|
||||
if proc.returncode == 0 or b"namingContexts" in proc.stdout:
|
||||
return {"status": "error", "message": f"SYSTEM CAN CONNECT, BUT PYTHON IS BLOCKED. Check Mac Firewall settings for Python."}
|
||||
except:
|
||||
pass
|
||||
return {"status": "error", "message": f"TCP Port {port} is closed or unreachable (Error code: {result}). Check firewall on {host}."}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Network Error: {str(e)}"}
|
||||
0
data/.gitkeep
Normal file → Executable file
0
data/.gitkeep
Normal file → Executable file
@@ -22,8 +22,9 @@ export const getNetworkConfig = async () => {
|
||||
return cachedConfig;
|
||||
} catch (e) {
|
||||
console.warn("Network config not found, using compiled defaults.");
|
||||
// Defaults matching the initial reserve ports in case network.json is missing
|
||||
return { SERVER_IP: 'localhost', BACKEND_PORT: 8916, BACKEND_SSL_PORT: 8918 };
|
||||
// Use actual hostname from browser + default ports
|
||||
const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
||||
return { SERVER_IP: hostname, BACKEND_PORT: 8916, BACKEND_SSL_PORT: 8918 };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
68
init_admin.py
Normal file
68
init_admin.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Initialize admin user with simple credentials for Phase 6 testing
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add backend to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
||||
|
||||
from passlib.context import CryptContext
|
||||
from backend.database import SessionLocal
|
||||
from backend import models
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def init_admin():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
username = "admin"
|
||||
password = "admin"
|
||||
|
||||
# Check if user exists
|
||||
user = db.query(models.User).filter(models.User.username == username).first()
|
||||
|
||||
if user:
|
||||
# Update existing user
|
||||
user.hashed_password = pwd_context.hash(password)
|
||||
user.role = "admin"
|
||||
user.origin = "local"
|
||||
db.commit()
|
||||
print(f"✅ Updated admin user: {username}")
|
||||
else:
|
||||
# Create new admin user
|
||||
hashed_password = pwd_context.hash(password)
|
||||
new_user = models.User(
|
||||
username=username,
|
||||
role="admin",
|
||||
origin="local",
|
||||
hashed_password=hashed_password
|
||||
)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
print(f"✅ Created admin user: {username}")
|
||||
|
||||
print(f" Username: {username}")
|
||||
print(f" Password: {password}")
|
||||
print(f" Role: admin")
|
||||
print(f"\n✅ Admin user ready. Try logging in:")
|
||||
print(f" URL: http://localhost:8917/login")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print(" Initialize Admin User")
|
||||
print("=" * 60)
|
||||
success = init_admin()
|
||||
sys.exit(0 if success else 1)
|
||||
0
logs/.gitkeep
Normal file → Executable file
0
logs/.gitkeep
Normal file → Executable file
43
tests/PHASE6_TEST_RESULTS.json
Normal file
43
tests/PHASE6_TEST_RESULTS.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"passed": [
|
||||
{
|
||||
"name": "Frontend Loads",
|
||||
"details": "Frontend HTML responding",
|
||||
"timestamp": "2026-04-23T09:52:00.028035"
|
||||
}
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"name": "Backend Health",
|
||||
"details": "Status 404",
|
||||
"timestamp": "2026-04-23T09:52:00.022268"
|
||||
},
|
||||
{
|
||||
"name": "Create Item",
|
||||
"details": "Status 404: {\"detail\":\"Not Found\"}",
|
||||
"timestamp": "2026-04-23T09:52:00.030370"
|
||||
},
|
||||
{
|
||||
"name": "Read Items",
|
||||
"details": "Status 404",
|
||||
"timestamp": "2026-04-23T09:52:00.032219"
|
||||
},
|
||||
{
|
||||
"name": "Search Items",
|
||||
"details": "Endpoint /api/items/search not found",
|
||||
"timestamp": "2026-04-23T09:52:00.033978"
|
||||
},
|
||||
{
|
||||
"name": "Admin Stats",
|
||||
"details": "Endpoint /api/admin/stats not found",
|
||||
"timestamp": "2026-04-23T09:52:00.035652"
|
||||
},
|
||||
{
|
||||
"name": "Export Snapshot",
|
||||
"details": "Endpoint not found",
|
||||
"timestamp": "2026-04-23T09:52:00.037389"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"timestamp": "2026-04-23T09:52:00.015498"
|
||||
}
|
||||
43
tests/PHASE6_TEST_RESULTS_CORRECTED.json
Normal file
43
tests/PHASE6_TEST_RESULTS_CORRECTED.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"passed": [
|
||||
{
|
||||
"name": "Backend Health",
|
||||
"details": "OpenAPI responding",
|
||||
"timestamp": "2026-04-23T09:53:06.593585"
|
||||
},
|
||||
{
|
||||
"name": "Frontend Loads",
|
||||
"details": "Frontend HTML responding",
|
||||
"timestamp": "2026-04-23T09:53:06.597945"
|
||||
}
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"name": "Create Item",
|
||||
"details": "Status 401: {\"detail\":\"Not authenticated\"}",
|
||||
"timestamp": "2026-04-23T09:53:06.605426"
|
||||
},
|
||||
{
|
||||
"name": "Read Items",
|
||||
"details": "Status 401",
|
||||
"timestamp": "2026-04-23T09:53:06.607900"
|
||||
},
|
||||
{
|
||||
"name": "Search Items",
|
||||
"details": "Status 401",
|
||||
"timestamp": "2026-04-23T09:53:06.610796"
|
||||
},
|
||||
{
|
||||
"name": "Admin Stats",
|
||||
"details": "Status 401",
|
||||
"timestamp": "2026-04-23T09:53:06.613239"
|
||||
},
|
||||
{
|
||||
"name": "Export Snapshot",
|
||||
"details": "Status 401",
|
||||
"timestamp": "2026-04-23T09:53:06.615119"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"timestamp": "2026-04-23T09:53:06.589338"
|
||||
}
|
||||
38
tests/PHASE6_TEST_RESULTS_WITH_AUTH.json
Normal file
38
tests/PHASE6_TEST_RESULTS_WITH_AUTH.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"passed": [
|
||||
{
|
||||
"name": "Login",
|
||||
"details": "Got auth token: eyJhbGciOiJIUzI1NiIs...",
|
||||
"timestamp": "2026-04-23T10:09:59.506977"
|
||||
},
|
||||
{
|
||||
"name": "Read Items",
|
||||
"details": "Found 1 items",
|
||||
"timestamp": "2026-04-23T10:09:59.540606"
|
||||
},
|
||||
{
|
||||
"name": "Search Items",
|
||||
"details": "Search returned 1 results",
|
||||
"timestamp": "2026-04-23T10:09:59.545718"
|
||||
},
|
||||
{
|
||||
"name": "Admin Stats",
|
||||
"details": "Stats loaded",
|
||||
"timestamp": "2026-04-23T10:09:59.548954"
|
||||
},
|
||||
{
|
||||
"name": "Export Snapshot",
|
||||
"details": "Exported 176128 bytes",
|
||||
"timestamp": "2026-04-23T10:09:59.552782"
|
||||
}
|
||||
],
|
||||
"failed": [
|
||||
{
|
||||
"name": "Create Item",
|
||||
"details": "Status 201: {\"name\":\"Test Item with Auth\",\"category\":\"Electronics\",\"category_id\":null,\"type\":null,\"barcode\":\"TES",
|
||||
"timestamp": "2026-04-23T10:09:59.535630"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"timestamp": "2026-04-23T10:09:59.498960"
|
||||
}
|
||||
304
tests/phase6_regression_tests.py
Normal file
304
tests/phase6_regression_tests.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Phase 6 Regression Tests - Automated API Tests
|
||||
Tests all broken features without manual UI interaction
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8916"
|
||||
FRONTEND_URL = "http://localhost:8917"
|
||||
|
||||
# Test results tracker
|
||||
results = {
|
||||
"passed": [],
|
||||
"failed": [],
|
||||
"errors": [],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def log_result(test_name, passed, details=""):
|
||||
"""Log test result"""
|
||||
result = {
|
||||
"name": test_name,
|
||||
"details": details,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if passed:
|
||||
results["passed"].append(result)
|
||||
print(f"✅ PASS: {test_name}")
|
||||
if details:
|
||||
print(f" {details}")
|
||||
else:
|
||||
results["failed"].append(result)
|
||||
print(f"❌ FAIL: {test_name}")
|
||||
if details:
|
||||
print(f" {details}")
|
||||
return passed
|
||||
|
||||
def log_error(test_name, error):
|
||||
"""Log exception"""
|
||||
results["errors"].append({
|
||||
"name": test_name,
|
||||
"error": str(error),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
print(f"⚠️ ERROR: {test_name}")
|
||||
print(f" {str(error)}")
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 1: CREATE ITEM (Foundation - blocks all other tests)
|
||||
# ============================================================================
|
||||
|
||||
def test_create_item():
|
||||
"""Test creating a new item via API"""
|
||||
print("\n--- TEST 1: CREATE ITEM ---")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"barcode": "TEST-001",
|
||||
"name": "Test Item",
|
||||
"category": "Electronics",
|
||||
"quantity": 5,
|
||||
"min_quantity": 1
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/items/",
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
item_id = data.get("id")
|
||||
log_result("Create Item", True, f"Created item ID {item_id}")
|
||||
return item_id
|
||||
else:
|
||||
log_result("Create Item", False, f"Status {response.status_code}: {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
log_error("Create Item", e)
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# TEST 2: READ ITEMS (Verify DB)
|
||||
# ============================================================================
|
||||
|
||||
def test_read_items():
|
||||
"""Test fetching items from API"""
|
||||
print("\n--- TEST 2: READ ITEMS ---")
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/items/")
|
||||
|
||||
if response.status_code == 200:
|
||||
items = response.json()
|
||||
log_result("Read Items", True, f"Found {len(items)} items")
|
||||
return items
|
||||
else:
|
||||
log_result("Read Items", False, f"Status {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
log_error("Read Items", e)
|
||||
return []
|
||||
|
||||
# ============================================================================
|
||||
# TEST 3: DELETE ITEM (Test cascade)
|
||||
# ============================================================================
|
||||
|
||||
def test_delete_item(item_id):
|
||||
"""Test deleting an item"""
|
||||
print("\n--- TEST 3: DELETE ITEM ---")
|
||||
|
||||
if not item_id:
|
||||
log_result("Delete Item", False, "No item ID provided (create test failed)")
|
||||
return False
|
||||
|
||||
try:
|
||||
response = requests.delete(f"{BASE_URL}/items/{item_id}")
|
||||
|
||||
if response.status_code == 200:
|
||||
log_result("Delete Item", True, f"Deleted item ID {item_id}")
|
||||
return True
|
||||
else:
|
||||
log_result("Delete Item", False, f"Status {response.status_code}: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Delete Item", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 4: SEARCH API (Test backend search)
|
||||
# ============================================================================
|
||||
|
||||
def test_search_items():
|
||||
"""Test searching items via API"""
|
||||
print("\n--- TEST 4: SEARCH ITEMS ---")
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}/items/search",
|
||||
params={"q": "test"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
results_list = response.json()
|
||||
log_result("Search Items", True, f"Search returned {len(results_list)} results")
|
||||
return True
|
||||
elif response.status_code == 404:
|
||||
log_result("Search Items", False, "Endpoint /api/items/search not found")
|
||||
return False
|
||||
else:
|
||||
log_result("Search Items", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Search Items", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 5: ADMIN API (Test admin data)
|
||||
# ============================================================================
|
||||
|
||||
def test_admin_stats():
|
||||
"""Test admin stats endpoint"""
|
||||
print("\n--- TEST 5: ADMIN STATS ---")
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/stats")
|
||||
|
||||
if response.status_code == 200:
|
||||
stats = response.json()
|
||||
log_result("Admin Stats", True, f"Stats: {stats}")
|
||||
return True
|
||||
elif response.status_code == 404:
|
||||
log_result("Admin Stats", False, "Endpoint /api/admin/stats not found")
|
||||
return False
|
||||
else:
|
||||
log_result("Admin Stats", False, f"Status {response.status_code}: {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Admin Stats", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 6: EXPORT ENDPOINT (Test Excel export)
|
||||
# ============================================================================
|
||||
|
||||
def test_export_snapshot():
|
||||
"""Test export snapshot endpoint"""
|
||||
print("\n--- TEST 6: EXPORT SNAPSHOT ---")
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/export")
|
||||
|
||||
if response.status_code == 200:
|
||||
# Check if response is binary (Excel file)
|
||||
if response.headers.get("content-type", "").startswith("application/vnd"):
|
||||
log_result("Export Snapshot", True, f"Exported {len(response.content)} bytes")
|
||||
return True
|
||||
else:
|
||||
log_result("Export Snapshot", False, "Response not Excel file")
|
||||
return False
|
||||
elif response.status_code == 404:
|
||||
log_result("Export Snapshot", False, "Endpoint not found")
|
||||
return False
|
||||
else:
|
||||
log_result("Export Snapshot", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Export Snapshot", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 7: BACKEND HEALTH
|
||||
# ============================================================================
|
||||
|
||||
def test_backend_health():
|
||||
"""Test backend health endpoint"""
|
||||
print("\n--- TEST 7: BACKEND HEALTH ---")
|
||||
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health")
|
||||
|
||||
if response.status_code == 200:
|
||||
log_result("Backend Health", True, "Backend responding")
|
||||
return True
|
||||
else:
|
||||
log_result("Backend Health", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Backend Health", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 8: FRONTEND LOADS
|
||||
# ============================================================================
|
||||
|
||||
def test_frontend_loads():
|
||||
"""Test frontend HTML loads"""
|
||||
print("\n--- TEST 8: FRONTEND LOADS ---")
|
||||
|
||||
try:
|
||||
response = requests.get(FRONTEND_URL)
|
||||
|
||||
if response.status_code == 200 and "Inventory" in response.text:
|
||||
log_result("Frontend Loads", True, "Frontend HTML responding")
|
||||
return True
|
||||
else:
|
||||
log_result("Frontend Loads", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_error("Frontend Loads", e)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# MAIN TEST EXECUTION
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print(" PHASE 6 REGRESSION TEST SUITE - Automated API Tests")
|
||||
print("=" * 70)
|
||||
print(f"Backend: {BASE_URL}")
|
||||
print(f"Frontend: {FRONTEND_URL}")
|
||||
print(f"Time: {datetime.now().isoformat()}")
|
||||
print("=" * 70)
|
||||
|
||||
# Run tests in order
|
||||
print("\n[PHASE A: Infrastructure Tests]")
|
||||
test_backend_health()
|
||||
test_frontend_loads()
|
||||
|
||||
print("\n[PHASE B: Core CRUD Operations]")
|
||||
item_id = test_create_item()
|
||||
items = test_read_items()
|
||||
if item_id:
|
||||
test_delete_item(item_id)
|
||||
|
||||
print("\n[PHASE C: Feature Tests]")
|
||||
test_search_items()
|
||||
test_admin_stats()
|
||||
test_export_snapshot()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 70)
|
||||
print(" TEST SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f"✅ Passed: {len(results['passed'])}")
|
||||
print(f"❌ Failed: {len(results['failed'])}")
|
||||
print(f"⚠️ Errors: {len(results['errors'])}")
|
||||
print("=" * 70)
|
||||
|
||||
# Write results to file
|
||||
with open("tests/PHASE6_TEST_RESULTS.json", "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
print(f"\nResults saved to: tests/PHASE6_TEST_RESULTS.json")
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if len(results["failed"]) == 0 and len(results["errors"]) == 0 else 1)
|
||||
207
tests/phase6_regression_tests_corrected.py
Normal file
207
tests/phase6_regression_tests_corrected.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Phase 6 Regression Tests - Automated API Tests (CORRECTED PATHS)
|
||||
Tests all broken features without manual UI interaction
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8916"
|
||||
FRONTEND_URL = "http://localhost:8917"
|
||||
|
||||
results = {"passed": [], "failed": [], "errors": [], "timestamp": datetime.now().isoformat()}
|
||||
|
||||
def log_result(test_name, passed, details=""):
|
||||
result = {"name": test_name, "details": details, "timestamp": datetime.now().isoformat()}
|
||||
if passed:
|
||||
results["passed"].append(result)
|
||||
print(f"✅ PASS: {test_name}")
|
||||
else:
|
||||
results["failed"].append(result)
|
||||
print(f"❌ FAIL: {test_name}")
|
||||
if details:
|
||||
print(f" {details}")
|
||||
return passed
|
||||
|
||||
def log_error(test_name, error):
|
||||
results["errors"].append({"name": test_name, "error": str(error), "timestamp": datetime.now().isoformat()})
|
||||
print(f"⚠️ ERROR: {test_name}: {error}")
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# TEST 1: CREATE ITEM
|
||||
# ============================================================================
|
||||
def test_create_item():
|
||||
print("\n--- TEST 1: CREATE ITEM ---")
|
||||
try:
|
||||
payload = {
|
||||
"barcode": "TEST-001",
|
||||
"name": "Test Item",
|
||||
"category": "Electronics",
|
||||
"quantity": 5,
|
||||
"min_quantity": 1
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/items/", json=payload)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
item_id = data.get("id")
|
||||
log_result("Create Item", True, f"Created item ID {item_id}")
|
||||
return item_id
|
||||
else:
|
||||
log_result("Create Item", False, f"Status {response.status_code}: {response.text[:100]}")
|
||||
return None
|
||||
except Exception as e:
|
||||
return log_error("Create Item", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 2: READ ITEMS
|
||||
# ============================================================================
|
||||
def test_read_items():
|
||||
print("\n--- TEST 2: READ ITEMS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/items/")
|
||||
if response.status_code == 200:
|
||||
items = response.json()
|
||||
log_result("Read Items", True, f"Found {len(items)} items")
|
||||
return items
|
||||
else:
|
||||
log_result("Read Items", False, f"Status {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
log_error("Read Items", e)
|
||||
return []
|
||||
|
||||
# ============================================================================
|
||||
# TEST 3: DELETE ITEM
|
||||
# ============================================================================
|
||||
def test_delete_item(item_id):
|
||||
print("\n--- TEST 3: DELETE ITEM ---")
|
||||
if not item_id:
|
||||
log_result("Delete Item", False, "No item ID (create failed)")
|
||||
return False
|
||||
try:
|
||||
response = requests.delete(f"{BASE_URL}/items/{item_id}")
|
||||
if response.status_code == 200:
|
||||
log_result("Delete Item", True, f"Deleted item ID {item_id}")
|
||||
return True
|
||||
else:
|
||||
log_result("Delete Item", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Delete Item", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 4: SEARCH ITEMS
|
||||
# ============================================================================
|
||||
def test_search_items():
|
||||
print("\n--- TEST 4: SEARCH ITEMS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/items/search", params={"q": "test"})
|
||||
if response.status_code == 200:
|
||||
results_list = response.json()
|
||||
log_result("Search Items", True, f"Search returned {len(results_list)} results")
|
||||
return True
|
||||
else:
|
||||
log_result("Search Items", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Search Items", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 5: ADMIN STATS
|
||||
# ============================================================================
|
||||
def test_admin_stats():
|
||||
print("\n--- TEST 5: ADMIN STATS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/stats")
|
||||
if response.status_code == 200:
|
||||
log_result("Admin Stats", True, "Stats loaded")
|
||||
return True
|
||||
else:
|
||||
log_result("Admin Stats", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Admin Stats", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 6: EXPORT SNAPSHOT
|
||||
# ============================================================================
|
||||
def test_export_snapshot():
|
||||
print("\n--- TEST 6: EXPORT SNAPSHOT ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/export")
|
||||
if response.status_code == 200:
|
||||
log_result("Export Snapshot", True, f"Exported {len(response.content)} bytes")
|
||||
return True
|
||||
else:
|
||||
log_result("Export Snapshot", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Export Snapshot", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 7: BACKEND HEALTH
|
||||
# ============================================================================
|
||||
def test_backend_health():
|
||||
print("\n--- TEST 7: BACKEND HEALTH (OpenAPI) ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/openapi.json")
|
||||
if response.status_code == 200:
|
||||
log_result("Backend Health", True, "OpenAPI responding")
|
||||
return True
|
||||
else:
|
||||
log_result("Backend Health", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Backend Health", e)
|
||||
|
||||
# ============================================================================
|
||||
# TEST 8: FRONTEND LOADS
|
||||
# ============================================================================
|
||||
def test_frontend_loads():
|
||||
print("\n--- TEST 8: FRONTEND LOADS ---")
|
||||
try:
|
||||
response = requests.get(FRONTEND_URL)
|
||||
if response.status_code == 200 and "Inventory" in response.text:
|
||||
log_result("Frontend Loads", True, "Frontend HTML responding")
|
||||
return True
|
||||
else:
|
||||
log_result("Frontend Loads", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
return log_error("Frontend Loads", e)
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print(" PHASE 6 REGRESSION TEST SUITE (CORRECTED)")
|
||||
print("=" * 70)
|
||||
|
||||
print("\n[Infrastructure Tests]")
|
||||
test_backend_health()
|
||||
test_frontend_loads()
|
||||
|
||||
print("\n[Core CRUD Operations]")
|
||||
item_id = test_create_item()
|
||||
items = test_read_items()
|
||||
if item_id:
|
||||
test_delete_item(item_id)
|
||||
|
||||
print("\n[Feature Tests]")
|
||||
test_search_items()
|
||||
test_admin_stats()
|
||||
test_export_snapshot()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"✅ Passed: {len(results['passed'])} | ❌ Failed: {len(results['failed'])} | ⚠️ Errors: {len(results['errors'])}")
|
||||
print("=" * 70)
|
||||
|
||||
with open("tests/PHASE6_TEST_RESULTS_CORRECTED.json", "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
print(f"\nResults: tests/PHASE6_TEST_RESULTS_CORRECTED.json")
|
||||
sys.exit(0 if len(results["failed"]) == 0 else 1)
|
||||
199
tests/phase6_with_auth.py
Normal file
199
tests/phase6_with_auth.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Phase 6 Regression Tests WITH AUTHENTICATION
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8916"
|
||||
FRONTEND_URL = "http://localhost:8917"
|
||||
|
||||
results = {"passed": [], "failed": [], "errors": [], "timestamp": datetime.now().isoformat()}
|
||||
auth_token = None
|
||||
|
||||
def log_result(test_name, passed, details=""):
|
||||
result = {"name": test_name, "details": details, "timestamp": datetime.now().isoformat()}
|
||||
if passed:
|
||||
results["passed"].append(result)
|
||||
print(f"✅ PASS: {test_name}")
|
||||
else:
|
||||
results["failed"].append(result)
|
||||
print(f"❌ FAIL: {test_name}")
|
||||
if details:
|
||||
print(f" {details}")
|
||||
return passed
|
||||
|
||||
# ============================================================================
|
||||
# AUTHENTICATION
|
||||
# ============================================================================
|
||||
def test_login():
|
||||
print("\n--- STEP 0: AUTHENTICATE ---")
|
||||
global auth_token
|
||||
|
||||
# Try default admin login (LDAP disabled or fallback)
|
||||
try:
|
||||
payload = {"username": "admin", "password": "admin"}
|
||||
response = requests.post(f"{BASE_URL}/users/login", json=payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
auth_token = data.get("access_token")
|
||||
log_result("Login", True, f"Got auth token: {auth_token[:20]}...")
|
||||
return auth_token
|
||||
else:
|
||||
log_result("Login", False, f"Status {response.status_code}: {response.text[:100]}")
|
||||
# Try without auth (check if endpoints allow public access)
|
||||
log_result("Login", False, "Will try endpoints without auth")
|
||||
return None
|
||||
except Exception as e:
|
||||
log_result("Login", False, str(e))
|
||||
return None
|
||||
|
||||
def get_headers():
|
||||
"""Get request headers with auth if available"""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if auth_token:
|
||||
headers["Authorization"] = f"Bearer {auth_token}"
|
||||
return headers
|
||||
|
||||
# ============================================================================
|
||||
# TESTS WITH AUTH
|
||||
# ============================================================================
|
||||
def test_create_item():
|
||||
print("\n--- TEST 1: CREATE ITEM ---")
|
||||
try:
|
||||
payload = {
|
||||
"barcode": "TEST-002",
|
||||
"name": "Test Item with Auth",
|
||||
"category": "Electronics",
|
||||
"quantity": 5,
|
||||
"min_quantity": 1
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/items/", json=payload, headers=get_headers())
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
item_id = data.get("id")
|
||||
log_result("Create Item", True, f"Created item ID {item_id}")
|
||||
return item_id
|
||||
else:
|
||||
log_result("Create Item", False, f"Status {response.status_code}: {response.text[:100]}")
|
||||
return None
|
||||
except Exception as e:
|
||||
log_result("Create Item", False, str(e))
|
||||
return None
|
||||
|
||||
def test_read_items():
|
||||
print("\n--- TEST 2: READ ITEMS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/items/", headers=get_headers())
|
||||
|
||||
if response.status_code == 200:
|
||||
items = response.json()
|
||||
log_result("Read Items", True, f"Found {len(items)} items")
|
||||
return items
|
||||
else:
|
||||
log_result("Read Items", False, f"Status {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
log_result("Read Items", False, str(e))
|
||||
return []
|
||||
|
||||
def test_delete_item(item_id):
|
||||
print("\n--- TEST 3: DELETE ITEM ---")
|
||||
if not item_id:
|
||||
log_result("Delete Item", False, "No item ID")
|
||||
return False
|
||||
try:
|
||||
response = requests.delete(f"{BASE_URL}/items/{item_id}", headers=get_headers())
|
||||
if response.status_code == 200:
|
||||
log_result("Delete Item", True, f"Deleted item ID {item_id}")
|
||||
return True
|
||||
else:
|
||||
log_result("Delete Item", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_result("Delete Item", False, str(e))
|
||||
return False
|
||||
|
||||
def test_search_items():
|
||||
print("\n--- TEST 4: SEARCH ITEMS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/items/search", params={"q": "test"}, headers=get_headers())
|
||||
if response.status_code == 200:
|
||||
results_list = response.json()
|
||||
log_result("Search Items", True, f"Search returned {len(results_list)} results")
|
||||
return True
|
||||
else:
|
||||
log_result("Search Items", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_result("Search Items", False, str(e))
|
||||
return False
|
||||
|
||||
def test_admin_stats():
|
||||
print("\n--- TEST 5: ADMIN STATS ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/stats", headers=get_headers())
|
||||
if response.status_code == 200:
|
||||
log_result("Admin Stats", True, "Stats loaded")
|
||||
return True
|
||||
else:
|
||||
log_result("Admin Stats", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_result("Admin Stats", False, str(e))
|
||||
return False
|
||||
|
||||
def test_export_snapshot():
|
||||
print("\n--- TEST 6: EXPORT SNAPSHOT ---")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/admin/db/export", headers=get_headers())
|
||||
if response.status_code == 200:
|
||||
log_result("Export Snapshot", True, f"Exported {len(response.content)} bytes")
|
||||
return True
|
||||
else:
|
||||
log_result("Export Snapshot", False, f"Status {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_result("Export Snapshot", False, str(e))
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print(" PHASE 6 REGRESSION TEST SUITE - WITH AUTHENTICATION")
|
||||
print("=" * 70)
|
||||
|
||||
auth_token = test_login()
|
||||
|
||||
if not auth_token:
|
||||
print("\n⚠️ NO AUTH TOKEN - Trying endpoints anyway (may all fail with 401)")
|
||||
|
||||
print("\n[Core CRUD Operations]")
|
||||
item_id = test_create_item()
|
||||
items = test_read_items()
|
||||
if item_id:
|
||||
test_delete_item(item_id)
|
||||
|
||||
print("\n[Feature Tests]")
|
||||
test_search_items()
|
||||
test_admin_stats()
|
||||
test_export_snapshot()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
passed = len(results['passed'])
|
||||
failed = len(results['failed'])
|
||||
total = passed + failed
|
||||
print(f"Results: {passed}/{total} passed")
|
||||
print(f"✅ {passed} | ❌ {failed}")
|
||||
print("=" * 70)
|
||||
|
||||
with open("tests/PHASE6_TEST_RESULTS_WITH_AUTH.json", "w") as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
print(f"Details: tests/PHASE6_TEST_RESULTS_WITH_AUTH.json")
|
||||
Reference in New Issue
Block a user