Repository: rucio/rucio
Issue: Error declaring bad file replicas

Motivation
----------

```
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/rucio/web/rest/flaskapi/v1/common.py", line 66, in dispatch_request
    return super(ErrorHandlingMethodView, self).dispatch_request(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/flask/views.py", line 163, in dispatch_request
    return meth(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/rucio/web/rest/flaskapi/v1/replicas.py", line 851, in post
    vo=request.environ.get('vo'),
  File "/usr/local/lib/python3.6/site-packages/rucio/api/replica.py", line 405, in add_bad_pfns
    if datetime.datetime.utcnow() <= expires_at and expires_at > datetime.datetime.utcnow() + datetime.timedelta(days=30):
TypeError: '<=' not supported between instances of 'datetime.datetime' and 'NoneType'
```

This appears to be a regression from #4962.

Modification
------------




Changed files:
lib/rucio/api/replica.py, lib/rucio/tests/test_replica.py

diff --git a/lib/rucio/api/replica.py b/lib/rucio/api/replica.py
index 8acd2ed61..9a2b4f2d1 100644
--- a/lib/rucio/api/replica.py
+++ b/lib/rucio/api/replica.py
@@ -402,7 +402,7 @@ def add_bad_pfns(pfns, issuer, state, reason=None, expires_at=None, vo='def'):
     if not permission.has_permission(issuer=issuer, vo=vo, action='add_bad_pfns', kwargs=kwargs):
         raise exception.AccessDenied('Account %s can not declare bad PFNs' % (issuer))
 
-    if datetime.datetime.utcnow() <= expires_at and expires_at > datetime.datetime.utcnow() + datetime.timedelta(days=30):
+    if expires_at and datetime.datetime.utcnow() <= expires_at and expires_at > datetime.datetime.utcnow() + datetime.timedelta(days=30):
         raise exception.InputValidationError('The given duration of %s days exceeds the maximum duration of 30 days.' % (expires_at - datetime.datetime.utcnow()).days)
 
     issuer = InternalAccount(issuer, vo=vo)
diff --git a/lib/rucio/tests/test_replica.py b/lib/rucio/tests/test_replica.py
index 77b683ac2..461a4b769 100644
--- a/lib/rucio/tests/test_replica.py
+++ b/lib/rucio/tests/test_replica.py
@@ -845,6 +845,36 @@ def test_client_add_temporary_unavailable_pfns(rse_factory, mock_scope, replica_
         assert list(rep.keys())[0] == ReplicaState.AVAILABLE
 
 
+def test_client_declare_bad_pfns(rse_factory, mock_scope, replica_client):
+    """ REPLICA (CLIENT): Client should be able to declare bad pfns."""
+    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)
+
+    # Submit bad PFNs
+    reason_str = generate_uuid()
+    replica_client.add_bad_pfns(pfns=list_rep, reason=str(reason_str), state='BAD', expires_at=None)
+    result = get_bad_pfns(limit=10000, thread=None, total_threads=None, session=None)
+    print("bad pfns:", result)
+    bad_pfns = {}
+    for res in result:
+        bad_pfns[res['pfn']] = (res['state'], res['reason'], res['expires_at'])
+
+    for pfn in list_rep:
+        pfn = str(clean_surls([pfn])[0])
+        assert pfn in bad_pfns
+        assert bad_pfns[pfn][0] == BadPFNStatus.BAD
+        assert bad_pfns[pfn][1] == reason_str
+
+
 def test_client_set_tombstone(rse_factory, mock_scope, root_account, replica_client):
     """ REPLICA (CLIENT): set tombstone on replica """
     # Set tombstone on one replica