1 /**
2  * Orthanc - A Lightweight, RESTful DICOM Store
3  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4  * Department, University Hospital of Liege, Belgium
5  * Copyright (C) 2017-2021 Osimis S.A., Belgium
6  *
7  * This program is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * In addition, as a special exception, the copyright holders of this
13  * program give permission to link the code of its release with the
14  * OpenSSL project's "OpenSSL" library (or with modified versions of it
15  * that use the same license as the "OpenSSL" library), and distribute
16  * the linked executables. You must obey the GNU General Public License
17  * in all respects for all of the code used other than "OpenSSL". If you
18  * modify file(s) with this exception, you may extend this exception to
19  * your version of the file(s), but you are not obligated to do so. If
20  * you do not wish to do so, delete this exception statement from your
21  * version. If you delete this exception statement from all source files
22  * in the program, then also delete it here.
23  *
24  * This program is distributed in the hope that it will be useful, but
25  * WITHOUT ANY WARRANTY; without even the implied warranty of
26  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
27  * General Public License for more details.
28  *
29  * You should have received a copy of the GNU General Public License
30  * along with this program. If not, see <http://www.gnu.org/licenses/>.
31  **/
32 
33 
34 #include "PrecompiledHeadersUnitTests.h"
35 #include <gtest/gtest.h>
36 
37 #include "../../OrthancFramework/Sources/Compatibility.h"
38 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
39 #include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
40 #include "../../OrthancFramework/Sources/Logging.h"
41 #include "../../OrthancFramework/Sources/SerializationToolbox.h"
42 
43 #include "../Sources/Database/SQLiteDatabaseWrapper.h"
44 #include "../Sources/ServerContext.h"
45 #include "../Sources/ServerJobs/LuaJobManager.h"
46 #include "../Sources/ServerJobs/OrthancJobUnserializer.h"
47 
48 #include "../Sources/ServerJobs/Operations/DeleteResourceOperation.h"
49 #include "../Sources/ServerJobs/Operations/DicomInstanceOperationValue.h"
50 #include "../Sources/ServerJobs/Operations/ModifyInstanceOperation.h"
51 #include "../Sources/ServerJobs/Operations/StorePeerOperation.h"
52 #include "../Sources/ServerJobs/Operations/StoreScuOperation.h"
53 #include "../Sources/ServerJobs/Operations/SystemCallOperation.h"
54 
55 #include "../Sources/ServerJobs/ArchiveJob.h"
56 #include "../Sources/ServerJobs/DicomModalityStoreJob.h"
57 #include "../Sources/ServerJobs/DicomMoveScuJob.h"
58 #include "../Sources/ServerJobs/MergeStudyJob.h"
59 #include "../Sources/ServerJobs/OrthancPeerStoreJob.h"
60 #include "../Sources/ServerJobs/ResourceModificationJob.h"
61 #include "../Sources/ServerJobs/SplitStudyJob.h"
62 
63 
64 using namespace Orthanc;
65 
66 namespace
67 {
68   class DummyJob : public IJob
69   {
70   private:
71     bool         fails_;
72     unsigned int count_;
73     unsigned int steps_;
74 
75   public:
DummyJob()76     DummyJob() :
77       fails_(false),
78       count_(0),
79       steps_(4)
80     {
81     }
82 
DummyJob(bool fails)83     explicit DummyJob(bool fails) :
84       fails_(fails),
85       count_(0),
86       steps_(4)
87     {
88     }
89 
Start()90     virtual void Start() ORTHANC_OVERRIDE
91     {
92     }
93 
Reset()94     virtual void Reset() ORTHANC_OVERRIDE
95     {
96     }
97 
Step(const std::string & jobId)98     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
99     {
100       if (fails_)
101       {
102         return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
103       }
104       else if (count_ == steps_ - 1)
105       {
106         return JobStepResult::Success();
107       }
108       else
109       {
110         count_++;
111         return JobStepResult::Continue();
112       }
113     }
114 
Stop(JobStopReason reason)115     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
116     {
117     }
118 
GetProgress()119     virtual float GetProgress() ORTHANC_OVERRIDE
120     {
121       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
122     }
123 
GetJobType(std::string & type)124     virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
125     {
126       type = "DummyJob";
127     }
128 
Serialize(Json::Value & value)129     virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
130     {
131       value = Json::objectValue;
132       value["Type"] = "DummyJob";
133       return true;
134     }
135 
GetPublicContent(Json::Value & value)136     virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
137     {
138       value["hello"] = "world";
139     }
140 
GetOutput(std::string & output,MimeType & mime,const std::string & key)141     virtual bool GetOutput(std::string& output,
142                            MimeType& mime,
143                            const std::string& key) ORTHANC_OVERRIDE
144     {
145       return false;
146     }
147   };
148 
149 
150   class DummyInstancesJob : public SetOfInstancesJob
151   {
152   private:
153     bool   trailingStepDone_;
154 
155   protected:
HandleInstance(const std::string & instance)156     virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
157     {
158       return (instance != "nope");
159     }
160 
HandleTrailingStep()161     virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
162     {
163       if (HasTrailingStep())
164       {
165         if (trailingStepDone_)
166         {
167           throw OrthancException(ErrorCode_InternalError);
168         }
169         else
170         {
171           trailingStepDone_ = true;
172           return true;
173         }
174       }
175       else
176       {
177         throw OrthancException(ErrorCode_InternalError);
178       }
179     }
180 
181   public:
DummyInstancesJob()182     DummyInstancesJob() :
183       trailingStepDone_(false)
184     {
185     }
186 
DummyInstancesJob(const Json::Value & value)187     DummyInstancesJob(const Json::Value& value) :
188       SetOfInstancesJob(value)
189     {
190       if (HasTrailingStep())
191       {
192         trailingStepDone_ = (GetPosition() == GetCommandsCount());
193       }
194       else
195       {
196         trailingStepDone_ = false;
197       }
198     }
199 
IsTrailingStepDone() const200     bool IsTrailingStepDone() const
201     {
202       return trailingStepDone_;
203     }
204 
Stop(JobStopReason reason)205     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
206     {
207     }
208 
GetJobType(std::string & s)209     virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
210     {
211       s = "DummyInstancesJob";
212     }
213   };
214 
215 
216   class DummyUnserializer : public GenericJobUnserializer
217   {
218   public:
UnserializeJob(const Json::Value & value)219     virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
220     {
221       if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
222       {
223         return new DummyInstancesJob(value);
224       }
225       else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
226       {
227         return new DummyJob;
228       }
229       else
230       {
231         return GenericJobUnserializer::UnserializeJob(value);
232       }
233     }
234   };
235 
236 
237   class DynamicInteger : public IDynamicObject
238   {
239   private:
240     int value_;
241     std::set<int>& target_;
242 
243   public:
DynamicInteger(int value,std::set<int> & target)244     DynamicInteger(int value, std::set<int>& target) :
245       value_(value), target_(target)
246     {
247     }
248 
GetValue() const249     int GetValue() const
250     {
251       return value_;
252     }
253   };
254 }
255 
256 
TEST(JobsEngine,DISABLED_Lua)257 TEST(JobsEngine, DISABLED_Lua)
258 {
259   JobsEngine engine(10);
260   engine.SetThreadSleep(10);
261   engine.SetWorkersCount(2);
262   engine.Start();
263 
264   LuaJobManager lua;
265   lua.SetMaxOperationsPerJob(5);
266   lua.SetTrailingOperationTimeout(200);
267 
268   for (size_t i = 0; i < 30; i++)
269   {
270     boost::this_thread::sleep(boost::posix_time::milliseconds(150));
271 
272     LuaJobManager::Lock lock(lua, engine);
273     size_t a = lock.AddLogOperation();
274     size_t b = lock.AddLogOperation();
275     size_t c = lock.AddSystemCallOperation("echo");
276     lock.AddStringInput(a, boost::lexical_cast<std::string>(i));
277     lock.AddNullInput(a);
278     lock.Connect(a, b);
279     lock.Connect(a, c);
280   }
281 
282   boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
283 
284   engine.Stop();
285 }
286 
287 
CheckSameJson(const Json::Value & a,const Json::Value & b)288 static bool CheckSameJson(const Json::Value& a,
289                           const Json::Value& b)
290 {
291   std::string s = a.toStyledString();
292   std::string t = b.toStyledString();
293 
294   if (s == t)
295   {
296     return true;
297   }
298   else
299   {
300     LOG(ERROR) << "Expected serialization: " << s;
301     LOG(ERROR) << "Actual serialization: " << t;
302     return false;
303   }
304 }
305 
306 
CheckIdempotentSetOfInstances(IJobUnserializer & unserializer,SetOfInstancesJob & job)307 static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
308                                           SetOfInstancesJob& job)
309 {
310   Json::Value a = 42;
311 
312   if (!job.Serialize(a))
313   {
314     return false;
315   }
316   else
317   {
318     std::unique_ptr<SetOfInstancesJob> unserialized
319       (dynamic_cast<SetOfInstancesJob*>(unserializer.UnserializeJob(a)));
320 
321     Json::Value b = 43;
322     if (unserialized->Serialize(b))
323     {
324       return (CheckSameJson(a, b) &&
325               job.HasTrailingStep() == unserialized->HasTrailingStep() &&
326               job.GetPosition() == unserialized->GetPosition() &&
327               job.GetInstancesCount() == unserialized->GetInstancesCount() &&
328               job.GetCommandsCount() == unserialized->GetCommandsCount());
329     }
330     else
331     {
332       return false;
333     }
334   }
335 }
336 
337 
CheckIdempotentSerialization(IJobUnserializer & unserializer,IJobOperation & operation)338 static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
339                                          IJobOperation& operation)
340 {
341   Json::Value a = 42;
342   operation.Serialize(a);
343 
344   std::unique_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
345 
346   Json::Value b = 43;
347   unserialized->Serialize(b);
348 
349   return CheckSameJson(a, b);
350 }
351 
352 
CheckIdempotentSerialization(IJobUnserializer & unserializer,IJobOperationValue & value)353 static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
354                                          IJobOperationValue& value)
355 {
356   Json::Value a = 42;
357   value.Serialize(a);
358 
359   std::unique_ptr<IJobOperationValue> unserialized(unserializer.UnserializeValue(a));
360 
361   Json::Value b = 43;
362   unserialized->Serialize(b);
363 
364   return CheckSameJson(a, b);
365 }
366 
367 
TEST(JobsSerialization,GenericOperations)368 TEST(JobsSerialization, GenericOperations)
369 {
370   DummyUnserializer unserializer;
371   Json::Value s;
372 
373   {
374     LogJobOperation operation;
375 
376     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
377     operation.Serialize(s);
378   }
379 
380   ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
381   ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
382 
383   {
384     std::unique_ptr<IJobOperation> operation;
385     operation.reset(unserializer.UnserializeOperation(s));
386 
387     // Make sure that we have indeed unserialized a log operation
388     Json::Value dummy;
389     ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy), std::bad_cast);
390     dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy);
391   }
392 }
393 
394 
TEST(JobsSerialization,DicomInstanceOrigin)395 TEST(JobsSerialization, DicomInstanceOrigin)
396 {
397   Json::Value s;
398   std::string t;
399 
400   {
401     DicomInstanceOrigin origin;
402 
403     s = 42;
404     origin.Serialize(s);
405   }
406 
407   {
408     DicomInstanceOrigin origin(s);
409     ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin());
410     ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
411     ASSERT_FALSE(origin.LookupRemoteIp(t));
412     ASSERT_FALSE(origin.LookupRemoteAet(t));
413     ASSERT_FALSE(origin.LookupCalledAet(t));
414     ASSERT_FALSE(origin.LookupHttpUsername(t));
415   }
416 
417   {
418     DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called"));
419 
420     s = 42;
421     origin.Serialize(s);
422   }
423 
424   {
425     DicomInstanceOrigin origin(s);
426     ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin());
427     ASSERT_EQ("aet", std::string(origin.GetRemoteAetC()));
428     ASSERT_TRUE(origin.LookupRemoteIp(t));   ASSERT_EQ("host", t);
429     ASSERT_TRUE(origin.LookupRemoteAet(t));  ASSERT_EQ("aet", t);
430     ASSERT_TRUE(origin.LookupCalledAet(t));  ASSERT_EQ("called", t);
431     ASSERT_FALSE(origin.LookupHttpUsername(t));
432   }
433 
434   {
435     DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username"));
436 
437     s = 42;
438     origin.Serialize(s);
439   }
440 
441   {
442     DicomInstanceOrigin origin(s);
443     ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin());
444     ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
445     ASSERT_TRUE(origin.LookupRemoteIp(t));     ASSERT_EQ("host", t);
446     ASSERT_FALSE(origin.LookupRemoteAet(t));
447     ASSERT_FALSE(origin.LookupCalledAet(t));
448     ASSERT_TRUE(origin.LookupHttpUsername(t)); ASSERT_EQ("username", t);
449   }
450 
451   {
452     DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua());
453 
454     s = 42;
455     origin.Serialize(s);
456   }
457 
458   {
459     DicomInstanceOrigin origin(s);
460     ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin());
461     ASSERT_FALSE(origin.LookupRemoteIp(t));
462     ASSERT_FALSE(origin.LookupRemoteAet(t));
463     ASSERT_FALSE(origin.LookupCalledAet(t));
464     ASSERT_FALSE(origin.LookupHttpUsername(t));
465   }
466 
467   {
468     DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins());
469 
470     s = 42;
471     origin.Serialize(s);
472   }
473 
474   {
475     DicomInstanceOrigin origin(s);
476     ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin());
477     ASSERT_FALSE(origin.LookupRemoteIp(t));
478     ASSERT_FALSE(origin.LookupRemoteAet(t));
479     ASSERT_FALSE(origin.LookupCalledAet(t));
480     ASSERT_FALSE(origin.LookupHttpUsername(t));
481   }
482 
483   {
484     DicomInstanceOrigin origin(DicomInstanceOrigin::FromWebDav());
485 
486     s = 42;
487     origin.Serialize(s);
488   }
489 
490   {
491     DicomInstanceOrigin origin(s);
492     ASSERT_EQ(RequestOrigin_WebDav, origin.GetRequestOrigin());
493     ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
494     ASSERT_FALSE(origin.LookupRemoteIp(t));
495     ASSERT_FALSE(origin.LookupRemoteAet(t));
496     ASSERT_FALSE(origin.LookupCalledAet(t));
497     ASSERT_FALSE(origin.LookupHttpUsername(t));
498   }
499 }
500 
501 
502 namespace
503 {
504   class OrthancJobsSerialization : public testing::Test
505   {
506   private:
507     MemoryStorageArea              storage_;
508     SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
509     std::unique_ptr<ServerContext>   context_;
510 
511   public:
OrthancJobsSerialization()512     OrthancJobsSerialization()
513     {
514       db_.Open();
515       context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
516       context_->SetupJobsEngine(true, false);
517     }
518 
~OrthancJobsSerialization()519     virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
520     {
521       context_->Stop();
522       context_.reset(NULL);
523       db_.Close();
524     }
525 
GetContext()526     ServerContext& GetContext()
527     {
528       return *context_;
529     }
530 
CreateInstance(std::string & id)531     bool CreateInstance(std::string& id)
532     {
533       // Create a sample DICOM file
534       ParsedDicomFile dicom(true);
535       dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
536                     false, DicomReplaceMode_InsertIfAbsent, "");
537 
538       std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
539 
540       return (context_->Store(id, *toStore, StoreInstanceMode_Default) == StoreStatus_Success);
541     }
542   };
543 }
544 
545 
TEST_F(OrthancJobsSerialization,Values)546 TEST_F(OrthancJobsSerialization, Values)
547 {
548   std::string id;
549   ASSERT_TRUE(CreateInstance(id));
550 
551   Json::Value s;
552   OrthancJobUnserializer unserializer(GetContext());
553 
554   {
555     DicomInstanceOperationValue instance(GetContext(), id);
556 
557     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance));
558     instance.Serialize(s);
559   }
560 
561   std::unique_ptr<IJobOperationValue> value;
562   value.reset(unserializer.UnserializeValue(s));
563   ASSERT_EQ(IJobOperationValue::Type_DicomInstance, value->GetType());
564   ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId());
565 
566   {
567     std::string content;
568     dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content);
569 
570     ParsedDicomFile dicom(content);
571     ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME));
572     ASSERT_EQ("JODOGNE", content);
573   }
574 }
575 
576 
TEST_F(OrthancJobsSerialization,Operations)577 TEST_F(OrthancJobsSerialization, Operations)
578 {
579   std::string id;
580   ASSERT_TRUE(CreateInstance(id));
581 
582   Json::Value s;
583   OrthancJobUnserializer unserializer(GetContext());
584 
585   // DeleteResourceOperation
586 
587   {
588     DeleteResourceOperation operation(GetContext());
589 
590     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
591     operation.Serialize(s);
592   }
593 
594   std::unique_ptr<IJobOperation> operation;
595 
596   {
597     operation.reset(unserializer.UnserializeOperation(s));
598 
599     Json::Value dummy;
600     ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy), std::bad_cast);
601     dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy);
602   }
603 
604   // StorePeerOperation
605 
606   {
607     WebServiceParameters peer;
608     peer.SetUrl("http://localhost/");
609     peer.SetCredentials("username", "password");
610     peer.SetPkcs11Enabled(true);
611 
612     StorePeerOperation operation(peer);
613 
614     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
615     operation.Serialize(s);
616   }
617 
618   {
619     operation.reset(unserializer.UnserializeOperation(s));
620 
621     const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation);
622     ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
623     ASSERT_EQ("username", tmp.GetPeer().GetUsername());
624     ASSERT_EQ("password", tmp.GetPeer().GetPassword());
625     ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
626   }
627 
628   // StoreScuOperation
629 
630   {
631     TimeoutDicomConnectionManager luaManager;
632 
633     {
634       RemoteModalityParameters modality;
635       modality.SetApplicationEntityTitle("REMOTE");
636       modality.SetHost("192.168.1.1");
637       modality.SetPortNumber(1000);
638       modality.SetManufacturer(ModalityManufacturer_GE);
639 
640       StoreScuOperation operation(GetContext(), luaManager, "TEST", modality);
641 
642       ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
643       operation.Serialize(s);
644     }
645 
646     {
647       operation.reset(unserializer.UnserializeOperation(s));
648 
649       const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
650       ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
651       ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
652       ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
653       ASSERT_EQ(ModalityManufacturer_GE, tmp.GetRemoteModality().GetManufacturer());
654       ASSERT_EQ("TEST", tmp.GetLocalAet());
655     }
656   }
657 
658   // SystemCallOperation
659 
660   {
661     SystemCallOperation operation(std::string("echo"));
662     operation.AddPreArgument("a");
663     operation.AddPreArgument("b");
664     operation.AddPostArgument("c");
665 
666     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
667     operation.Serialize(s);
668   }
669 
670   {
671     operation.reset(unserializer.UnserializeOperation(s));
672 
673     const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation);
674     ASSERT_EQ("echo", tmp.GetCommand());
675     ASSERT_EQ(2u, tmp.GetPreArgumentsCount());
676     ASSERT_EQ(1u, tmp.GetPostArgumentsCount());
677     ASSERT_EQ("a", tmp.GetPreArgument(0));
678     ASSERT_EQ("b", tmp.GetPreArgument(1));
679     ASSERT_EQ("c", tmp.GetPostArgument(0));
680   }
681 
682   // ModifyInstanceOperation
683 
684   {
685     std::unique_ptr<DicomModification> modification(new DicomModification);
686     modification->SetupAnonymization(DicomVersion_2008);
687 
688     ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
689 
690     ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
691     operation.Serialize(s);
692   }
693 
694   {
695     operation.reset(unserializer.UnserializeOperation(s));
696 
697     const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation);
698     ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin());
699     ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
700   }
701 }
702 
703 
TEST_F(OrthancJobsSerialization,Jobs)704 TEST_F(OrthancJobsSerialization, Jobs)
705 {
706   Json::Value s;
707 
708   // ArchiveJob
709 
710   {
711     ArchiveJob job(GetContext(), false, false);
712     ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
713   }
714 
715   // DicomModalityStoreJob
716 
717   OrthancJobUnserializer unserializer(GetContext());
718 
719   {
720     RemoteModalityParameters modality;
721     modality.SetApplicationEntityTitle("REMOTE");
722     modality.SetHost("192.168.1.1");
723     modality.SetPortNumber(1000);
724     modality.SetManufacturer(ModalityManufacturer_GE);
725 
726     DicomModalityStoreJob job(GetContext());
727     job.SetLocalAet("LOCAL");
728     job.SetRemoteModality(modality);
729     job.SetMoveOriginator("MOVESCU", 42);
730 
731     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
732     ASSERT_TRUE(job.Serialize(s));
733   }
734 
735   {
736     std::unique_ptr<IJob> job;
737     job.reset(unserializer.UnserializeJob(s));
738 
739     DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
740     ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
741     ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
742     ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
743     ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
744     ASSERT_EQ(ModalityManufacturer_GE, tmp.GetParameters().GetRemoteModality().GetManufacturer());
745     ASSERT_TRUE(tmp.HasMoveOriginator());
746     ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
747     ASSERT_EQ(42, tmp.GetMoveOriginatorId());
748   }
749 
750   // OrthancPeerStoreJob
751 
752   {
753     WebServiceParameters peer;
754     peer.SetUrl("http://localhost/");
755     peer.SetCredentials("username", "password");
756     peer.SetPkcs11Enabled(true);
757 
758     OrthancPeerStoreJob job(GetContext());
759     job.SetPeer(peer);
760 
761     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
762     ASSERT_TRUE(job.Serialize(s));
763   }
764 
765   {
766     std::unique_ptr<IJob> job;
767     job.reset(unserializer.UnserializeJob(s));
768 
769     OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
770     ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
771     ASSERT_EQ("username", tmp.GetPeer().GetUsername());
772     ASSERT_EQ("password", tmp.GetPeer().GetPassword());
773     ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
774     ASSERT_FALSE(tmp.IsTranscode());
775     ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
776   }
777 
778   {
779     OrthancPeerStoreJob job(GetContext());
780     ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
781     job.SetTranscode("1.2.840.10008.1.2.4.50");
782 
783     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
784     ASSERT_TRUE(job.Serialize(s));
785   }
786 
787   {
788     std::unique_ptr<IJob> job;
789     job.reset(unserializer.UnserializeJob(s));
790 
791     OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
792     ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
793     ASSERT_EQ("", tmp.GetPeer().GetUsername());
794     ASSERT_EQ("", tmp.GetPeer().GetPassword());
795     ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
796     ASSERT_TRUE(tmp.IsTranscode());
797     ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
798   }
799 
800   // ResourceModificationJob
801 
802   {
803     std::unique_ptr<DicomModification> modification(new DicomModification);
804     modification->SetupAnonymization(DicomVersion_2008);
805 
806     ResourceModificationJob job(GetContext());
807     job.SetModification(modification.release(), ResourceType_Patient, true);
808     job.SetOrigin(DicomInstanceOrigin::FromLua());
809 
810     job.AddTrailingStep();  // Necessary since 1.7.0
811     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
812     ASSERT_TRUE(job.Serialize(s));
813   }
814 
815   {
816     std::unique_ptr<IJob> job;
817     job.reset(unserializer.UnserializeJob(s));
818 
819     ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
820     ASSERT_TRUE(tmp.IsAnonymization());
821     ASSERT_FALSE(tmp.IsTranscode());
822     ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
823     ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
824     ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
825   }
826 
827   {
828     ResourceModificationJob job(GetContext());
829     ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
830     job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
831 
832     job.AddTrailingStep();  // Necessary since 1.7.0
833     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
834     ASSERT_TRUE(job.Serialize(s));
835   }
836 
837   {
838     std::unique_ptr<IJob> job;
839     job.reset(unserializer.UnserializeJob(s));
840 
841     ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
842     ASSERT_FALSE(tmp.IsAnonymization());
843     ASSERT_TRUE(tmp.IsTranscode());
844     ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
845     ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
846   }
847 
848   // SplitStudyJob
849 
850   std::string instance;
851   ASSERT_TRUE(CreateInstance(instance));
852 
853   std::string study, series;
854 
855   {
856     ServerContext::DicomCacheLocker lock(GetContext(), instance);
857     study = lock.GetDicom().GetHasher().HashStudy();
858     series = lock.GetDicom().GetHasher().HashSeries();
859   }
860 
861   {
862     std::list<std::string> tmp;
863     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
864     ASSERT_EQ(1u, tmp.size());
865     ASSERT_EQ(study, tmp.front());
866     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
867     ASSERT_EQ(1u, tmp.size());
868     ASSERT_EQ(series, tmp.front());
869   }
870 
871   std::string study2;
872 
873   {
874     std::string a, b;
875 
876     {
877       ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
878 
879       SplitStudyJob job(GetContext(), study);
880       job.SetKeepSource(true);
881       job.AddSourceSeries(series);
882       ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
883       job.SetOrigin(DicomInstanceOrigin::FromLua());
884       job.Replace(DICOM_TAG_PATIENT_NAME, "hello");
885       job.Remove(DICOM_TAG_PATIENT_BIRTH_DATE);
886       ASSERT_THROW(job.Replace(DICOM_TAG_SERIES_DESCRIPTION, "nope"), OrthancException);
887       ASSERT_THROW(job.Remove(DICOM_TAG_SERIES_DESCRIPTION), OrthancException);
888 
889       ASSERT_TRUE(job.GetTargetStudy().empty());
890       a = job.GetTargetStudyUid();
891       ASSERT_TRUE(job.LookupTargetSeriesUid(b, series));
892 
893       job.AddTrailingStep();
894       job.Start();
895       ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
896       ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
897 
898       study2 = job.GetTargetStudy();
899       ASSERT_FALSE(study2.empty());
900 
901       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
902       ASSERT_TRUE(job.Serialize(s));
903     }
904 
905     {
906       std::unique_ptr<IJob> job;
907       job.reset(unserializer.UnserializeJob(s));
908 
909       SplitStudyJob& tmp = dynamic_cast<SplitStudyJob&>(*job);
910       ASSERT_TRUE(tmp.IsKeepSource());
911       ASSERT_EQ(study, tmp.GetSourceStudy());
912       ASSERT_EQ(a, tmp.GetTargetStudyUid());
913       ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
914 
915       std::string s;
916       ASSERT_EQ(study2, tmp.GetTargetStudy());
917       ASSERT_FALSE(tmp.LookupTargetSeriesUid(s, "nope"));
918       ASSERT_TRUE(tmp.LookupTargetSeriesUid(s, series));
919       ASSERT_EQ(b, s);
920 
921       ASSERT_FALSE(tmp.LookupReplacement(s, DICOM_TAG_STUDY_DESCRIPTION));
922       ASSERT_TRUE(tmp.LookupReplacement(s, DICOM_TAG_PATIENT_NAME));
923       ASSERT_EQ("hello", s);
924       ASSERT_FALSE(tmp.IsRemoved(DICOM_TAG_PATIENT_NAME));
925       ASSERT_TRUE(tmp.IsRemoved(DICOM_TAG_PATIENT_BIRTH_DATE));
926     }
927   }
928 
929   {
930     std::list<std::string> tmp;
931     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
932     ASSERT_EQ(2u, tmp.size());
933     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
934     ASSERT_EQ(2u, tmp.size());
935   }
936 
937   // MergeStudyJob
938 
939   {
940     ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
941 
942     MergeStudyJob job(GetContext(), study);
943     job.SetKeepSource(true);
944     job.AddSource(study2);
945     ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
946     ASSERT_THROW(job.AddSourceStudy("nope"), OrthancException);
947     ASSERT_THROW(job.AddSource("nope"), OrthancException);
948     job.SetOrigin(DicomInstanceOrigin::FromLua());
949 
950     ASSERT_EQ(job.GetTargetStudy(), study);
951 
952     job.AddTrailingStep();
953     job.Start();
954     ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
955     ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
956 
957     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
958     ASSERT_TRUE(job.Serialize(s));
959   }
960 
961   {
962     std::list<std::string> tmp;
963     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
964     ASSERT_EQ(2u, tmp.size());
965     GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
966     ASSERT_EQ(3u, tmp.size());
967   }
968 
969   {
970     std::unique_ptr<IJob> job;
971     job.reset(unserializer.UnserializeJob(s));
972 
973     MergeStudyJob& tmp = dynamic_cast<MergeStudyJob&>(*job);
974     ASSERT_TRUE(tmp.IsKeepSource());
975     ASSERT_EQ(study, tmp.GetTargetStudy());
976     ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
977   }
978 }
979 
980 
981 
TEST_F(OrthancJobsSerialization,DicomAssociationParameters)982 TEST_F(OrthancJobsSerialization, DicomAssociationParameters)
983 {
984   Json::Value v;
985 
986   {
987     v = Json::objectValue;
988     DicomAssociationParameters p;
989     p.SerializeJob(v);
990   }
991 
992   {
993     DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
994     ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle());
995     ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle());
996     ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber());
997     ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
998     ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost());
999     ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout());
1000   }
1001 
1002   {
1003     v = Json::objectValue;
1004     DicomAssociationParameters p;
1005     p.SetLocalApplicationEntityTitle("HELLO");
1006     p.SetRemoteApplicationEntityTitle("WORLD");
1007     p.SetRemotePort(42);
1008     p.SetRemoteHost("MY_HOST");
1009     p.SetTimeout(43);
1010     p.SerializeJob(v);
1011   }
1012 
1013   {
1014     DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
1015     ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle());
1016     ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle());
1017     ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber());
1018     ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
1019     ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost());
1020     ASSERT_EQ(43u, p.GetTimeout());
1021   }
1022 
1023   {
1024     DicomModalityStoreJob job(GetContext());
1025     job.Serialize(v);
1026   }
1027 
1028   {
1029     OrthancJobUnserializer unserializer(GetContext());
1030     std::unique_ptr<DicomModalityStoreJob> job(
1031       dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
1032     ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
1033     ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
1034     ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
1035     ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
1036     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
1037     ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
1038     ASSERT_FALSE(job->HasMoveOriginator());
1039     ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException);
1040     ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException);
1041     ASSERT_FALSE(job->HasStorageCommitment());
1042   }
1043 
1044   {
1045     RemoteModalityParameters r;
1046     r.SetApplicationEntityTitle("HELLO");
1047     r.SetPortNumber(42);
1048     r.SetHost("MY_HOST");
1049 
1050     DicomModalityStoreJob job(GetContext());
1051     job.SetLocalAet("WORLD");
1052     job.SetRemoteModality(r);
1053     job.SetTimeout(43);
1054     job.SetMoveOriginator("ORIGINATOR", 100);
1055     job.EnableStorageCommitment(true);
1056     job.Serialize(v);
1057   }
1058 
1059   {
1060     OrthancJobUnserializer unserializer(GetContext());
1061     std::unique_ptr<DicomModalityStoreJob> job(
1062       dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
1063     ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
1064     ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
1065     ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
1066     ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
1067     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
1068     ASSERT_EQ(43u, job->GetParameters().GetTimeout());
1069     ASSERT_TRUE(job->HasMoveOriginator());
1070     ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet());
1071     ASSERT_EQ(100, job->GetMoveOriginatorId());
1072     ASSERT_TRUE(job->HasStorageCommitment());
1073   }
1074 
1075   {
1076     DicomMoveScuJob job(GetContext());
1077     job.Serialize(v);
1078   }
1079 
1080   {
1081     OrthancJobUnserializer unserializer(GetContext());
1082     std::unique_ptr<DicomMoveScuJob> job(
1083       dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
1084     ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
1085     ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
1086     ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
1087     ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
1088     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
1089     ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
1090   }
1091 
1092   {
1093     RemoteModalityParameters r;
1094     r.SetApplicationEntityTitle("HELLO");
1095     r.SetPortNumber(42);
1096     r.SetHost("MY_HOST");
1097 
1098     DicomMoveScuJob job(GetContext());
1099     job.SetLocalAet("WORLD");
1100     job.SetRemoteModality(r);
1101     job.SetTimeout(43);
1102     job.Serialize(v);
1103   }
1104 
1105   {
1106     OrthancJobUnserializer unserializer(GetContext());
1107     std::unique_ptr<DicomMoveScuJob> job(
1108       dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
1109     ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
1110     ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
1111     ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
1112     ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
1113     ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
1114     ASSERT_EQ(43u, job->GetParameters().GetTimeout());
1115   }
1116 }
1117