# HG changeset patch # User Sebastien Jodogne # Date 1618586033 -7200 # Node ID 227d9a932467644daee0fc51b68d9667ece28045 # Parent 5c2472f008eb31dda23a455b4515db5963c967ac testing revisions in metadata diff -r 5c2472f008eb -r 227d9a932467 GenerateConfigurationForTests.py --- a/GenerateConfigurationForTests.py Mon Mar 08 16:59:25 2021 +0100 +++ b/GenerateConfigurationForTests.py Fri Apr 16 17:13:53 2021 +0200 @@ -149,6 +149,7 @@ config['WebDavDeleteAllowed'] = True config['WebDavUploadAllowed'] = True config['StorageCompression'] = args.compression +config['CheckRevisions'] = True del config['KeepAlive'] diff -r 5c2472f008eb -r 227d9a932467 Tests/Tests.py --- a/Tests/Tests.py Mon Mar 08 16:59:25 2021 +0100 +++ b/Tests/Tests.py Fri Apr 16 17:13:53 2021 +0200 @@ -1126,15 +1126,26 @@ self.assertEqual(DoGet(_REMOTE, '/instances/%s/metadata/SopClassUid' % i), '1.2.840.10008.5.1.4.1.1.4') # Play with custom metadata - DoPut(_REMOTE, '/patients/%s/metadata/5555' % p, 'coucou') + (headers, body) = DoPutRaw(_REMOTE, '/patients/%s/metadata/5555' % p, 'coucou') + self.assertEqual('200', headers['status']) + self.assertEqual('', body) + self.assertEqual('"0"', headers['etag']) + m = DoGet(_REMOTE, '/patients/%s/metadata' % p) self.assertEqual(2, len(m)) self.assertTrue('LastUpdate' in m) self.assertTrue('5555' in m) self.assertEqual('coucou', DoGet(_REMOTE, '/patients/%s/metadata/5555' % p)) - DoPut(_REMOTE, '/patients/%s/metadata/5555' % p, 'hello') - self.assertEqual('hello', DoGet(_REMOTE, '/patients/%s/metadata/5555' % p)) - DoDelete(_REMOTE, '/patients/%s/metadata/5555' % p) + DoPut(_REMOTE, '/patients/%s/metadata/5555' % p, 'hello', headers = { + 'If-Match' : headers['etag'] + }) + + (headers, body) = DoGetRaw(_REMOTE, '/patients/%s/metadata/5555' % p) + self.assertEqual('200', headers['status']) + self.assertEqual('hello', body) + DoDelete(_REMOTE, '/patients/%s/metadata/5555' % p, headers = { + 'If-Match' : headers['etag'] + }) m = DoGet(_REMOTE, '/patients/%s/metadata' % p) self.assertEqual(1, len(m)) self.assertTrue('LastUpdate' in m) @@ -4641,7 +4652,10 @@ # changes of type "UpdatedMetadata" i = DoGet(_REMOTE, '/instances') [0] DoPut(_REMOTE, '/instances/%s/metadata/4000' % i, 'hello', 'text/plain') - self.assertEqual('hello', DoGet(_REMOTE, '/instances/%s/metadata/4000' % i)) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/4000' % i) + self.assertEqual('200', headers['status']) + self.assertEqual('hello', body) c = DoGet(_REMOTE, '/changes?last') self.assertEqual(1, len(c['Changes'])) @@ -4649,7 +4663,9 @@ self.assertEqual(seq + 5, c['Last']) self.assertEqual('UpdatedMetadata', c['Changes'][0]['ChangeType']) - DoDelete(_REMOTE, '/instances/%s/metadata/4000' % i) + DoDelete(_REMOTE, '/instances/%s/metadata/4000' % i, headers = { + 'If-Match' : headers['etag'] + }) c = DoGet(_REMOTE, '/changes?last') self.assertEqual(1, len(c['Changes'])) self.assertTrue(c['Done']) @@ -6556,3 +6572,141 @@ instance = DoGet(_REMOTE, '/instances/%s' % a) self.assertEqual(tags['0008,0018'], instance['MainDicomTags']['SOPInstanceUID']) + + def test_revisions(self): + # This test fails on Orthanc <= 1.9.1 (support for revisions + # was introduced in 1.9.2), or if configuration option + # "CheckRevisions" is "False". Conventions for HTTP headers + # related to revisions mimic CouchDB: + # https://docs.couchdb.org/en/stable/api/document/common.html + i = UploadInstance(_REMOTE, 'DummyCT.dcm') ['ID'] + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i) + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + self.assertEqual('1.2.840.10008.1.2.4.70', body) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i, headers = { + 'If-None-Match' : '"0"' + }) + self.assertEqual('304', headers['status']) # Not modified + self.assertEqual('"0"', headers['etag']) + self.assertEqual('', body) # Body must be empty on 304 status + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i, headers = { + 'If-None-Match' : '"1"' + }) + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + self.assertEqual('1.2.840.10008.1.2.4.70', body) + + (headers, body) = DoDeleteRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i) + self.assertEqual('403', headers['status']) # Forbidden (system metadata) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i, 'hello') + self.assertEqual('403', headers['status']) # Forbidden (system metadata) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello') + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/1024' % i) + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + self.assertEqual('hello', body) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-None-Match' : '"0"' + }) + self.assertEqual('304', headers['status']) + self.assertEqual('"0"', headers['etag']) + self.assertEqual('', body) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-None-Match' : '"1"' + }) + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + self.assertEqual('hello', body) + self.assertEqual('hello', DoGet(_REMOTE, '/instances/%s/metadata/1024' % i)) + + (headers, body) = DoDeleteRaw(_REMOTE, '/instances/%s/metadata/1024' % i) + self.assertEqual('409', headers['status']) # No revision given, but "CheckRevisions" is True + + (headers, body) = DoDeleteRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-Match' : '45' + }) + self.assertEqual('409', headers['status']) # Conflict, as bad revision + + (headers, body) = DoDeleteRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-Match' : '0' + }) + self.assertEqual('200', headers['status']) + + (headers, body) = DoDeleteRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-Match' : '0' + }) + self.assertEqual('404', headers['status']) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/1024' % i) + self.assertEqual('404', headers['status']) + + self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/%s/metadata/1024' % i)) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello') + self.assertEqual('200', headers['status']) + self.assertEqual('"0"', headers['etag']) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello') + self.assertEqual('409', headers['status']) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/TransferSyntax' % i, headers = { + 'If-None-Match' : '"0"' + }) + self.assertEqual('304', headers['status']) # Not modified + self.assertEqual('"0"', headers['etag']) + self.assertEqual('', body) # Body must be empty on 304 status + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello', headers = { + 'If-Match' : '0' + }) + self.assertEqual('200', headers['status']) + self.assertEqual('"1"', headers['etag']) + self.assertEqual('', body) + + (headers, body) = DoGetRaw(_REMOTE, '/instances/%s/metadata/1024' % i, headers = { + 'If-None-Match' : headers['etag'] + }) + + if headers['status'] == '200': + print("Your database backend doesn't store revisions") + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello2', headers = { + 'If-Match' : '1' + }) + self.assertEqual('409', headers['status']) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello2', headers = { + 'If-Match' : '0' + }) + self.assertEqual('200', headers['status']) + self.assertEqual('"1"', headers['etag']) + self.assertEqual('', body) + + elif headers['status'] == '304': # Revisions are supported + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello2', headers = { + 'If-Match' : '0' + }) + self.assertEqual('409', headers['status']) + + (headers, body) = DoPutRaw(_REMOTE, '/instances/%s/metadata/1024' % i, 'hello2', headers = { + 'If-Match' : '1' + }) + self.assertEqual('200', headers['status']) + self.assertEqual('"2"', headers['etag']) + self.assertEqual('', body) + + else: + raise Exception('Internal error') + + self.assertEqual('hello2', DoGet(_REMOTE, '/instances/%s/metadata/1024' % i)) + diff -r 5c2472f008eb -r 227d9a932467 Tests/Toolbox.py --- a/Tests/Toolbox.py Mon Mar 08 16:59:25 2021 +0100 +++ b/Tests/Toolbox.py Fri Apr 16 17:13:53 2021 +0200 @@ -141,28 +141,43 @@ resp, content = http.request(orthanc['Url'] + uri, method, body = body, headers = headers) + return (resp, content) + +def DoDeleteRaw(orthanc, uri, headers = {}): + http = httplib2.Http() + http.follow_redirects = False + _SetupCredentials(orthanc, http) + + resp, content = http.request(orthanc['Url'] + uri, 'DELETE', headers = headers) + return (resp, content) + +def DoDelete(orthanc, uri, headers = {}): + (resp, content) = DoDeleteRaw(orthanc, uri, headers) + if not (resp.status in [ 200 ]): + raise Exception(resp.status, resp) + else: + return _DecodeJson(content) + +def DoPutRaw(orthanc, uri, data = {}, contentType = '', headers = {}): + return _DoPutOrPost(orthanc, uri, 'PUT', data, contentType, headers) + +def DoPut(orthanc, uri, data = {}, contentType = '', headers = {}): + (resp, content) = DoPutRaw(orthanc, uri, data, contentType, headers) if not (resp.status in [ 200, 201, 302 ]): raise Exception(resp.status, resp) else: return _DecodeJson(content) -def DoDelete(orthanc, uri): - http = httplib2.Http() - http.follow_redirects = False - _SetupCredentials(orthanc, http) - - resp, content = http.request(orthanc['Url'] + uri, 'DELETE') - if not (resp.status in [ 200 ]): +def DoPostRaw(orthanc, uri, data = {}, contentType = '', headers = {}): + return _DoPutOrPost(orthanc, uri, 'POST', data, contentType, headers) + +def DoPost(orthanc, uri, data = {}, contentType = '', headers = {}): + (resp, content) = DoPostRaw(orthanc, uri, data, contentType, headers) + if not (resp.status in [ 200, 201, 302 ]): raise Exception(resp.status, resp) else: return _DecodeJson(content) -def DoPut(orthanc, uri, data = {}, contentType = ''): - return _DoPutOrPost(orthanc, uri, 'PUT', data, contentType, {}) - -def DoPost(orthanc, uri, data = {}, contentType = '', headers = {}): - return _DoPutOrPost(orthanc, uri, 'POST', data, contentType, headers) - def GetDatabasePath(filename): return os.path.join(os.path.dirname(__file__), '..', 'Database', filename)