Repository: rucio/rucio

Issue:
Verify the value of `expires_at` for bad/temporary unavailable replicas
Motivation
----------
When a replica is declared as `TEMPORARY UNAVAILABLE`, a value should be provided in `expires_at`. When it is declared as `BAD`, the argument should not be populated (`None`). While the CLI complies to these constraints, the user could provide wrong arguments in the API/Python client.

Modification
------------
Add all necessary constraints.


Discussion:

Resolving patch:
diff --git a/lib/rucio/core/replica.py b/lib/rucio/core/replica.py
--- a/lib/rucio/core/replica.py
+++ b/lib/rucio/core/replica.py
@@ -3116,6 +3116,11 @@ def add_bad_pfns(pfns, account, state, reason=None, expires_at=None, session=Non
     else:
         rep_state = state
 
+    if rep_state == BadPFNStatus.TEMPORARY_UNAVAILABLE and expires_at is None:
+        raise exception.InputValidationError("When adding a TEMPORARY UNAVAILABLE pfn the expires_at value should be set.")
+    elif rep_state == BadPFNStatus.BAD and expires_at is not None:
+        raise exception.InputValidationError("When adding a BAD pfn the expires_at value shouldn't be set.")
+
     pfns = clean_surls(pfns)
     for pfn in pfns:
         new_pfn = models.BadPFNs(path=str(pfn), account=account, state=rep_state, reason=reason, expires_at=expires_at)


Test patch:
diff --git a/lib/rucio/tests/test_replica.py b/lib/rucio/tests/test_replica.py
--- a/lib/rucio/tests/test_replica.py
+++ b/lib/rucio/tests/test_replica.py
@@ -50,7 +50,7 @@
 from rucio.client.ruleclient import RuleClient
 from rucio.common.exception import (DataIdentifierNotFound, AccessDenied, RucioException,
                                     ReplicaIsLocked, ReplicaNotFound, ScopeNotFound,
-                                    DatabaseException)
+                                    DatabaseException, InputValidationError)
 from rucio.common.utils import generate_uuid, clean_surls, parse_response
 from rucio.core.config import set as cconfig_set
 from rucio.core.did import add_did, attach_dids, get_did, set_status, list_files, get_did_atime
@@ -875,6 +875,27 @@ def test_client_declare_bad_pfns(rse_factory, mock_scope, replica_client):
         assert bad_pfns[pfn][1] == reason_str
 
 
+def test_client_add_temporary_pfns_input_validation_error(rse_factory, mock_scope, replica_client):
+    """ REPLICA (CLIENT): Try to add temporary unavailable PFNs without the expired at value, this should fail."""
+    rse, rse_id = rse_factory.make_posix_rse()
+    nbfiles = 5
+    # Adding replicas to deterministic RSE
+    files = [{'scope': mock_scope.external, 'name': 'file_%s' % generate_uuid(), 'bytes': 1, 'adler32': '0cc737eb', 'meta': {'events': 10}} for _ in range(nbfiles)]
+    replica_client.add_replicas(rse=rse, files=files)
+
+    # Listing replicas on deterministic RSE
+    list_rep = []
+    for replica in replica_client.list_replicas(dids=[{'scope': f['scope'], 'name': f['name']} for f in files], schemes=['file'], all_states=True):
+        pfn = list(replica['pfns'].keys())[0]
+        list_rep.append(pfn)
+
+    with pytest.raises(InputValidationError):
+        replica_client.add_bad_pfns(pfns=list_rep, reason="Some weird reason", state='TEMPORARY_UNAVAILABLE', expires_at=None)
+
+    with pytest.raises(InputValidationError):
+        replica_client.add_bad_pfns(pfns=list_rep, reason="Some weird reason", state='BAD', expires_at=(datetime.utcnow() + timedelta(seconds=10)).isoformat())
+
+
 def test_client_set_tombstone(rse_factory, mock_scope, root_account, replica_client):
     """ REPLICA (CLIENT): set tombstone on replica """
     # Set tombstone on one replica