Repository: umuttopalak/pass-cli

Issue:
List All Passwords
Users needs to check their password list

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index b20d53f..ab67025 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,18 @@ Check sudo authentication status:
 pass-cli auth-check
 ```
 
+### List Passwords
+
+List all stored passwords:
+```bash
+pass-cli list
+```
+
+List passwords for a specific service:
+```bash
+pass-cli list -s github
+```
+
 ## Security Features
 
 - AES-256 encryption for all stored passwords
diff --git a/pass_cli/cli.py b/pass_cli/cli.py
index dbfe78b..d8f76c0 100644
--- a/pass_cli/cli.py
+++ b/pass_cli/cli.py
@@ -4,6 +4,7 @@
 from .commands.auth_check import auth_check
 from .commands.generate import generate
 from .commands.init import init
+from .commands.list import list
 from .commands.retrieve import retrieve
 from .commands.store import store
 
@@ -50,6 +51,7 @@ def main():
 main.add_command(init)
 main.add_command(store)
 main.add_command(retrieve)
+main.add_command(list)
 
 if __name__ == "__main__":
     main()
diff --git a/pass_cli/commands/__init__.py b/pass_cli/commands/__init__.py
index 13a4a00..930c87f 100644
--- a/pass_cli/commands/__init__.py
+++ b/pass_cli/commands/__init__.py
@@ -4,7 +4,9 @@
 from .auth_check import auth_check
 from .generate import generate
 from .init import init
+from .list import list
 from .retrieve import retrieve
 from .store import store
 
-__all__ = ["auth", "auth_check", "generate", "init", "retrieve", "store"]
+__all__ = ["auth", "auth_check", "generate",
+           "init", "retrieve", "store", "list"]
diff --git a/pass_cli/commands/list.py b/pass_cli/commands/list.py
new file mode 100644
index 0000000..efaeb7b
--- /dev/null
+++ b/pass_cli/commands/list.py
@@ -0,0 +1,48 @@
+import sys
+
+import click
+
+from ..database import PasswordManager
+from ..utils import check_initialized, check_sudo
+
+
+@click.command()
+@click.option('--service', '-s', help='Filter passwords by service name')
+def list(service: str = None) -> None:
+    """List saved passwords for all or specific service"""
+    try:
+        if not check_sudo():
+            click.echo(click.style(
+                "✗ Authentication required! Please run 'pass-cli auth' first.", fg="red"))
+            sys.exit(1)
+
+        if not check_initialized():
+            click.echo(click.style(
+                "✗ Password manager not initialized! Please run 'pass-cli init' first.", 
+                fg="red"))
+            raise click.Abort()
+
+        # Get encryption key and initialize password manager
+        password_manager = PasswordManager(PasswordManager.get_stored_key())
+        
+        # Get stored passwords
+        stored_passwords = password_manager.list_passwords(service)
+        
+        if not stored_passwords:
+            if service:
+                click.echo(click.style(
+                    f"No passwords found for service: {service}", fg="yellow"))
+            else:
+                click.echo(click.style("No passwords stored yet.", fg="yellow"))
+            return
+
+        # Display results
+        click.echo(click.style("\nStored Passwords:", fg="green"))
+        click.echo("─" * 50)
+        for service_name, username in stored_passwords:
+            click.echo(f"Service: {service_name}")
+            click.echo(f"Username: {username}")
+            click.echo("─" * 50)
+    except Exception as e:
+        click.echo(click.style(f"Error: {str(e)}", fg="red"))
+        raise click.Abort()
diff --git a/pass_cli/database.py b/pass_cli/database.py
index aa8682d..7b52360 100644
--- a/pass_cli/database.py
+++ b/pass_cli/database.py
@@ -119,6 +119,21 @@ def get_password(self, service_name: str, username: str) -> str:
                 return self.cipher_suite.decrypt(encrypted_password.encode()).decode()
             return None 
 
+    def list_passwords(self, service_name: str = None):
+        with sqlite3.connect(self.db_path) as conn:
+            if service_name:
+                cursor = conn.execute(
+                    "SELECT service_name, username FROM passwords WHERE service_name = ?", 
+                    (service_name,)
+                )
+            else:
+                cursor = conn.execute("SELECT service_name, username FROM passwords")
+
+            result = cursor.fetchall()
+            if result:
+                return result
+            return []
+
     @classmethod
     def get_stored_key(cls) -> str:
         """Get encryption key from keyring"""
diff --git a/pass_cli/utils.py b/pass_cli/utils.py
index 789427b..07ea654 100644
--- a/pass_cli/utils.py
+++ b/pass_cli/utils.py
@@ -5,12 +5,14 @@
 from .database import PasswordManager
 
 
-def check_sudo():
+def check_sudo() -> bool:
     """Check if user has sudo privileges"""
     try:
-        subprocess.run(["sudo", "-n", "true"], check=True)
-        return True
-    except subprocess.CalledProcessError:
+        result = subprocess.run(['sudo', '-n', 'true'], 
+                              capture_output=True, 
+                              text=True)
+        return result.returncode == 0
+    except Exception:
         return False
 
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index dcb050b..d572153 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,12 +1,32 @@
 import os
+from unittest.mock import patch
 
 import pytest
 
+from pass_cli.commands.init import init
+
 
 @pytest.fixture(autouse=True)
 def setup_test_env():
     """Set up test environment"""
     os.environ['TESTING'] = 'true'
     yield
-    os.environ.pop('TESTING', None) 
-    
+    os.environ.pop('TESTING', None)
+
+
+@pytest.fixture
+def initialized_db(runner, mock_keyring, mock_db_path, mock_sudo):
+    """Fixture that provides an initialized database"""
+    with runner.isolated_filesystem():
+        # Initialize the password manager
+        init_result = runner.invoke(init, input='testkey\ntestkey\n')
+        assert init_result.exit_code == 0
+        yield
+
+
+@pytest.fixture
+def mock_sudo_fail():
+    """Mock sudo checks to always fail"""
+    with patch('subprocess.run') as mock_run:
+        mock_run.return_value.returncode = 1
+        yield mock_run
diff --git a/tests/test_commands.py b/tests/test_commands.py
index ba2b63b..4783b53 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -7,6 +7,7 @@
 from pass_cli.commands.auth_check import auth_check
 from pass_cli.commands.generate import generate
 from pass_cli.commands.init import init
+from pass_cli.commands.list import list
 from pass_cli.commands.retrieve import retrieve
 from pass_cli.commands.store import store
 
@@ -15,6 +16,7 @@
 def runner():
     return CliRunner()
 
+
 @pytest.fixture
 def mock_sudo():
     """Mock sudo checks to always succeed"""
@@ -22,23 +24,27 @@ def mock_sudo():
         mock_run.return_value.returncode = 0
         yield mock_run
 
+
 @pytest.fixture
 def mock_keyring():
     """Mock keyring operations"""
     stored_key = "testkey"  # Use consistent key value
     with patch('keyring.set_password') as mock_set, \
-         patch('keyring.get_password') as mock_get:
+            patch('keyring.get_password') as mock_get:
         mock_set.return_value = None
         mock_get.return_value = stored_key
         yield {'set': mock_set, 'get': mock_get, 'key': stored_key}
 
+
 @pytest.fixture
 def mock_db_path(tmp_path, monkeypatch):
     """Mock database path for commands"""
     test_db = str(tmp_path / 'test_passwords.db')
-    monkeypatch.setattr('pass_cli.database.PasswordManager.DEFAULT_DB_PATH', test_db)
+    monkeypatch.setattr(
+        'pass_cli.database.PasswordManager.DEFAULT_DB_PATH', test_db)
     return test_db
 
+
 def test_init_command(runner, mock_keyring, mock_db_path):
     """Test initialization command"""
     with runner.isolated_filesystem():
@@ -46,13 +52,14 @@ def test_init_command(runner, mock_keyring, mock_db_path):
         assert result.exit_code == 0
         assert "Password manager initialized successfully" in result.output
 
+
 def test_generate_command(runner, mock_sudo, mock_keyring, mock_db_path):
     """Test password generation command"""
     with runner.isolated_filesystem():
         # First initialize the password manager
         init_result = runner.invoke(init, input='testkey\ntestkey\n')
         assert init_result.exit_code == 0
-        
+
         # Test basic generation with --no-copy
         result = runner.invoke(generate, ['--length', '16', '--no-copy'])
         assert result.exit_code == 0
@@ -73,13 +80,14 @@ def test_generate_command(runner, mock_sudo, mock_keyring, mock_db_path):
         assert "Generated and stored password for testservice" in result.output
         assert "✓ Password copied to clipboard!" in result.output
 
+
 def test_store_command(runner, mock_sudo, mock_keyring, mock_db_path):
     """Test password storage command"""
     with runner.isolated_filesystem():
         # First initialize the password manager
         init_result = runner.invoke(init, input='testkey\ntestkey\n')
         assert init_result.exit_code == 0
-        
+
         result = runner.invoke(store, [
             '--service', 'testservice',
             '--username', 'testuser',
@@ -87,13 +95,14 @@ def test_store_command(runner, mock_sudo, mock_keyring, mock_db_path):
         ], input=mock_keyring['key'] + '\n')  # Provide encryption key as input
         assert result.exit_code == 0
 
+
 def test_retrieve_command(runner, mock_sudo, mock_keyring, mock_db_path):
     """Test password retrieval command"""
     with runner.isolated_filesystem():
         # First initialize the password manager
         init_result = runner.invoke(init, input='testkey\ntestkey\n')
         assert init_result.exit_code == 0
-        
+
         # First store the password
         store_result = runner.invoke(store, [
             '--service', 'testservice',
@@ -101,7 +110,7 @@ def test_retrieve_command(runner, mock_sudo, mock_keyring, mock_db_path):
             '--password', 'testpass'
         ], input=mock_keyring['key'] + '\n')  # Provide encryption key as input
         assert store_result.exit_code == 0
-        
+
         # Then retrieve it
         result = runner.invoke(retrieve, [
             '--service', 'testservice',
@@ -110,6 +119,7 @@ def test_retrieve_command(runner, mock_sudo, mock_keyring, mock_db_path):
         assert result.exit_code == 0
         assert "testpass" in result.output
 
+
 def test_auth_commands(runner, mock_sudo):
     """Test authentication commands"""
     result = runner.invoke(auth)
@@ -118,4 +128,55 @@ def test_auth_commands(runner, mock_sudo):
 
     result = runner.invoke(auth_check)
     assert result.exit_code == 0
-    assert "User is authenticated" in result.output 
\ No newline at end of file
+    assert "User is authenticated" in result.output
+
+
+def test_list_passwords(runner, initialized_db):
+    """Test listing passwords"""
+    # Store passwords with encryption key
+    result = runner.invoke(store,
+                           ['-s', 'github', '-u', 'testuser1', '-p', 'pass123'],
+                           input='testkey\n'  # Provide encryption key
+                           )
+    assert result.exit_code == 0
+
+    result = runner.invoke(store,
+                           ['-s', 'gmail', '-u', 'testuser2', '-p', 'pass456'],
+                           input='testkey\n'  # Provide encryption key
+                           )
+    assert result.exit_code == 0
+
+    # List passwords with encryption key
+    result = runner.invoke(list, input='testkey\n')
+    assert result.exit_code == 0
+    assert 'github' in result.output
+    assert 'testuser1' in result.output
+    assert 'gmail' in result.output
+    assert 'testuser2' in result.output
+
+    result = runner.invoke(list, ['-s', 'github'], input='testkey\n')
+    assert result.exit_code == 0
+    assert 'github' in result.output
+    assert 'testuser1' in result.output
+    assert 'gmail' not in result.output
+    assert 'testuser2' not in result.output
+
+    result = runner.invoke(list, ['-s', 'nonexistent'], input='testkey\n')
+    assert result.exit_code == 0
+    assert 'No passwords found for service: nonexistent' in result.output
+
+
+def test_list_passwords_no_auth(runner, initialized_db):
+    """Test listing passwords without authentication"""
+    with patch('pass_cli.commands.list.check_sudo', return_value=False):  # Doğrudan list.py içindeki check_sudo'yu mock'la
+        result = runner.invoke(list)
+        assert result.exit_code == 1  # click.Abort() 1 döndürür
+        assert "Authentication required!" in result.output
+
+
+def test_list_passwords_not_initialized(runner, mock_db_path):
+    """Test listing passwords without initialization"""
+    with runner.isolated_filesystem():
+        result = runner.invoke(list)
+        assert result.exit_code != 0
+        assert "Password manager not initialized!" in result.output