Repository: canonical/charmcraft

Issue:
Provide a better error description and suggest solution for the "double login" case



Craft Store does not allow to "login" if it finds credentials in the keyring. Currently the received error is confusing:

```
craft-store error: Credentials found for 'charmcraft' on 'api.charmhub.io'.    
```

Furthermore, when the user reads "credentials found" they move on and issue another store related command that may also fail because the credentials are expired! so they try to login again and ... (loop)

We should provide a better error and possible resolution:

```
Cannot login because credentials were found in your system (which may be no longer valid, though)
Recommended resolution: Please logout before login again 
```

(and the original error should appear in the logs only)


Comments:

Patch:
diff --git a/charmcraft/commands/store/store.py b/charmcraft/commands/store/store.py
index dd6fe74..6dc7f06 100644
--- a/charmcraft/commands/store/store.py
+++ b/charmcraft/commands/store/store.py
@@ -25,6 +25,7 @@ from functools import wraps
 import craft_store
 from craft_cli import emit, CraftError
 from craft_store import attenuations, endpoints
+from craft_store.errors import CredentialsAlreadyAvailable
 from dateutil import parser
 
 from charmcraft.commands.store.client import Client, ALTERNATE_AUTH_ENV_VAR
@@ -207,7 +208,15 @@ class Store:
         if packages:
             kwargs["packages"] = packages
 
-        return self._client.login(**kwargs)
+        try:
+            credentials = self._client.login(**kwargs)
+        except CredentialsAlreadyAvailable as exc:
+            raise CraftError(
+                "Cannot login because credentials were found in your system "
+                "(which may be no longer valid, though).",
+                resolution="Please logout first, then login again.",
+            ) from exc
+        return credentials
 
     def logout(self):
         """Logout from the store.


Test patch:
diff --git a/tests/commands/test_store_api.py b/tests/commands/test_store_api.py
index c46ac46..00ff878 100644
--- a/tests/commands/test_store_api.py
+++ b/tests/commands/test_store_api.py
@@ -26,7 +26,12 @@ from dateutil import parser
 from craft_cli import CraftError
 from craft_store import attenuations
 from craft_store.endpoints import Package
-from craft_store.errors import NetworkError, CredentialsUnavailable, StoreServerError
+from craft_store.errors import (
+    CredentialsAlreadyAvailable,
+    CredentialsUnavailable,
+    NetworkError,
+    StoreServerError,
+)
 
 from charmcraft.commands.store.client import Client
 from charmcraft.utils import ResourceOption
@@ -296,6 +301,24 @@ def test_login(client_mock, config):
     assert result == acquired_credentials
 
 
+def test_login_having_credentials(client_mock, config):
+    """Login attempt when already having credentials.."""
+    # client raises a specific exception for this case
+    original_exception = CredentialsAlreadyAvailable("app", "host")
+    client_mock.login.side_effect = original_exception
+
+    store = Store(config.charmhub)
+    with pytest.raises(CraftError) as cm:
+        store.login()
+    error = cm.value
+    assert str(error) == (
+        "Cannot login because credentials were found in your system (which may be "
+        "no longer valid, though)."
+    )
+    assert error.resolution == "Please logout first, then login again."
+    assert error.__cause__ is original_exception
+
+
 def test_login_attenuating_ttl(client_mock, config):
     """Login with specific TTL restrictions."""
     store = Store(config.charmhub)
diff --git a/tests/spread/store/resources/task.yaml b/tests/spread/store/resources/task.yaml
index 881bae3..cbcba78 100644
--- a/tests/spread/store/resources/task.yaml
+++ b/tests/spread/store/resources/task.yaml
@@ -35,7 +35,7 @@ prepare: |
   echo "Super complex file resource" > resourcefile.txt
 
   # an oci image resource
-  docker pull hellow-world@sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
+  docker pull hello-world@sha256:18a657d0cc1c7d0678a3fbea8b7eb4918bba25968d3e1b0adebfa71caddbc346
 
 restore: |
   pushd charm