|
5 | 5 | Note: These tests rely on conftest.py for Pinecone/OpenAI/Redis mocking. |
6 | 6 | """ |
7 | 7 | import pytest |
8 | | -from unittest.mock import patch, MagicMock |
| 8 | +from unittest.mock import patch, MagicMock, AsyncMock |
9 | 9 | from datetime import datetime, timezone, timedelta |
10 | 10 | import json |
11 | 11 |
|
@@ -679,3 +679,203 @@ def test_partial_job_includes_partial_info(self, mock_job_class, client): |
679 | 679 | data = response.json() |
680 | 680 | assert data["partial"] is True |
681 | 681 | assert data["max_files"] == 200 |
| 682 | + |
| 683 | + |
| 684 | + |
| 685 | +# ============================================================================= |
| 686 | +# Issue #128: Search User-Indexed Repos Tests |
| 687 | +# ============================================================================= |
| 688 | + |
| 689 | +class TestSearchUserRepos: |
| 690 | + """Tests for searching user-indexed repositories.""" |
| 691 | + |
| 692 | + @patch('routes.playground._get_limiter') |
| 693 | + @patch('routes.playground.indexer') |
| 694 | + def test_search_with_repo_id_user_owns(self, mock_indexer, mock_get_limiter, client): |
| 695 | + """User can search their own indexed repo via repo_id.""" |
| 696 | + mock_limiter = MagicMock() |
| 697 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 698 | + allowed=True, |
| 699 | + remaining=99, |
| 700 | + limit=100, |
| 701 | + session_token="test_session_123" |
| 702 | + ) |
| 703 | + # Session owns this repo |
| 704 | + mock_limiter.get_session_data.return_value = MagicMock( |
| 705 | + indexed_repo={ |
| 706 | + "repo_id": "repo_user_abc123", |
| 707 | + "github_url": "https://github.com/user/repo", |
| 708 | + "name": "repo", |
| 709 | + "file_count": 50, |
| 710 | + "indexed_at": "2024-01-01T00:00:00Z", |
| 711 | + "expires_at": "2099-01-02T00:00:00Z" # Far future |
| 712 | + } |
| 713 | + ) |
| 714 | + mock_get_limiter.return_value = mock_limiter |
| 715 | + mock_indexer.semantic_search = AsyncMock(return_value=[ |
| 716 | + {"file": "test.py", "score": 0.9} |
| 717 | + ]) |
| 718 | + |
| 719 | + response = client.post( |
| 720 | + "/api/v1/playground/search", |
| 721 | + json={"query": "test function", "repo_id": "repo_user_abc123"} |
| 722 | + ) |
| 723 | + |
| 724 | + assert response.status_code == 200 |
| 725 | + data = response.json() |
| 726 | + assert data["count"] == 1 |
| 727 | + |
| 728 | + @patch('routes.playground._get_limiter') |
| 729 | + def test_search_repo_id_not_owned_returns_403(self, mock_get_limiter, client): |
| 730 | + """Searching repo_id user doesn't own returns 403.""" |
| 731 | + mock_limiter = MagicMock() |
| 732 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 733 | + allowed=True, |
| 734 | + remaining=99, |
| 735 | + limit=100, |
| 736 | + session_token="test_session_123" |
| 737 | + ) |
| 738 | + # Session owns different repo |
| 739 | + mock_limiter.get_session_data.return_value = MagicMock( |
| 740 | + indexed_repo={ |
| 741 | + "repo_id": "repo_OTHER_xyz", |
| 742 | + "github_url": "https://github.com/other/repo", |
| 743 | + "name": "other-repo", |
| 744 | + "file_count": 50, |
| 745 | + "indexed_at": "2024-01-01T00:00:00Z", |
| 746 | + "expires_at": "2099-01-02T00:00:00Z" |
| 747 | + } |
| 748 | + ) |
| 749 | + mock_get_limiter.return_value = mock_limiter |
| 750 | + |
| 751 | + response = client.post( |
| 752 | + "/api/v1/playground/search", |
| 753 | + json={"query": "test", "repo_id": "repo_user_abc123"} |
| 754 | + ) |
| 755 | + |
| 756 | + assert response.status_code == 403 |
| 757 | + data = response.json() |
| 758 | + assert data["detail"]["error"] == "access_denied" |
| 759 | + |
| 760 | + @patch('routes.playground._get_limiter') |
| 761 | + def test_search_repo_id_no_session_repo_returns_403(self, mock_get_limiter, client): |
| 762 | + """Searching repo_id when session has no indexed repo returns 403.""" |
| 763 | + mock_limiter = MagicMock() |
| 764 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 765 | + allowed=True, |
| 766 | + remaining=99, |
| 767 | + limit=100, |
| 768 | + session_token="test_session_123" |
| 769 | + ) |
| 770 | + # Session has no indexed repo |
| 771 | + mock_limiter.get_session_data.return_value = MagicMock(indexed_repo=None) |
| 772 | + mock_get_limiter.return_value = mock_limiter |
| 773 | + |
| 774 | + response = client.post( |
| 775 | + "/api/v1/playground/search", |
| 776 | + json={"query": "test", "repo_id": "repo_user_abc123"} |
| 777 | + ) |
| 778 | + |
| 779 | + assert response.status_code == 403 |
| 780 | + |
| 781 | + @patch('routes.playground._get_limiter') |
| 782 | + def test_search_expired_repo_returns_410(self, mock_get_limiter, client): |
| 783 | + """Searching expired repo returns 410 with can_reindex hint.""" |
| 784 | + mock_limiter = MagicMock() |
| 785 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 786 | + allowed=True, |
| 787 | + remaining=99, |
| 788 | + limit=100, |
| 789 | + session_token="test_session_123" |
| 790 | + ) |
| 791 | + # Session owns repo but it's expired |
| 792 | + mock_limiter.get_session_data.return_value = MagicMock( |
| 793 | + indexed_repo={ |
| 794 | + "repo_id": "repo_user_abc123", |
| 795 | + "github_url": "https://github.com/user/repo", |
| 796 | + "name": "repo", |
| 797 | + "file_count": 50, |
| 798 | + "indexed_at": "2024-01-01T00:00:00Z", |
| 799 | + "expires_at": "2024-01-01T00:00:01Z" # Already expired |
| 800 | + } |
| 801 | + ) |
| 802 | + mock_get_limiter.return_value = mock_limiter |
| 803 | + |
| 804 | + response = client.post( |
| 805 | + "/api/v1/playground/search", |
| 806 | + json={"query": "test", "repo_id": "repo_user_abc123"} |
| 807 | + ) |
| 808 | + |
| 809 | + assert response.status_code == 410 |
| 810 | + data = response.json() |
| 811 | + assert data["detail"]["error"] == "repo_expired" |
| 812 | + assert data["detail"]["can_reindex"] is True |
| 813 | + |
| 814 | + @patch('routes.playground._get_limiter') |
| 815 | + @patch('routes.playground.indexer') |
| 816 | + def test_search_demo_repo_via_repo_id_allowed(self, mock_indexer, mock_get_limiter, client): |
| 817 | + """Demo repos can be accessed via repo_id without ownership check.""" |
| 818 | + mock_limiter = MagicMock() |
| 819 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 820 | + allowed=True, |
| 821 | + remaining=99, |
| 822 | + limit=100, |
| 823 | + session_token="test_session_123" |
| 824 | + ) |
| 825 | + mock_get_limiter.return_value = mock_limiter |
| 826 | + mock_indexer.semantic_search = AsyncMock(return_value=[]) |
| 827 | + |
| 828 | + # Use the flask demo repo ID |
| 829 | + from routes.playground import DEMO_REPO_IDS |
| 830 | + flask_repo_id = DEMO_REPO_IDS.get("flask") |
| 831 | + |
| 832 | + if flask_repo_id: |
| 833 | + response = client.post( |
| 834 | + "/api/v1/playground/search", |
| 835 | + json={"query": "route handler", "repo_id": flask_repo_id} |
| 836 | + ) |
| 837 | + assert response.status_code == 200 |
| 838 | + |
| 839 | + @patch('routes.playground._get_limiter') |
| 840 | + @patch('routes.playground.indexer') |
| 841 | + def test_search_backward_compat_demo_repo(self, mock_indexer, mock_get_limiter, client): |
| 842 | + """Backward compat: demo_repo parameter still works.""" |
| 843 | + mock_limiter = MagicMock() |
| 844 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 845 | + allowed=True, |
| 846 | + remaining=99, |
| 847 | + limit=100, |
| 848 | + session_token=None |
| 849 | + ) |
| 850 | + mock_get_limiter.return_value = mock_limiter |
| 851 | + mock_indexer.semantic_search = AsyncMock(return_value=[]) |
| 852 | + |
| 853 | + response = client.post( |
| 854 | + "/api/v1/playground/search", |
| 855 | + json={"query": "test", "demo_repo": "flask"} |
| 856 | + ) |
| 857 | + |
| 858 | + # Should work (200) or 404 if flask not indexed - but not 4xx auth error |
| 859 | + assert response.status_code in [200, 404] |
| 860 | + |
| 861 | + @patch('routes.playground._get_limiter') |
| 862 | + @patch('routes.playground.indexer') |
| 863 | + def test_search_default_to_flask_when_no_repo_specified(self, mock_indexer, mock_get_limiter, client): |
| 864 | + """When neither repo_id nor demo_repo provided, defaults to flask.""" |
| 865 | + mock_limiter = MagicMock() |
| 866 | + mock_limiter.check_and_record.return_value = MagicMock( |
| 867 | + allowed=True, |
| 868 | + remaining=99, |
| 869 | + limit=100, |
| 870 | + session_token=None |
| 871 | + ) |
| 872 | + mock_get_limiter.return_value = mock_limiter |
| 873 | + mock_indexer.semantic_search = AsyncMock(return_value=[]) |
| 874 | + |
| 875 | + response = client.post( |
| 876 | + "/api/v1/playground/search", |
| 877 | + json={"query": "test"} # No repo_id or demo_repo |
| 878 | + ) |
| 879 | + |
| 880 | + # Should default to flask |
| 881 | + assert response.status_code in [200, 404] |
0 commit comments