View Javadoc
1   package emissary.directory;
2   
3   import emissary.core.DataObjectFactory;
4   import emissary.core.Form;
5   import emissary.core.HDMobileAgent;
6   import emissary.core.IBaseDataObject;
7   import emissary.place.IServiceProviderPlace;
8   import emissary.place.sample.DelayPlace;
9   import emissary.test.core.junit5.UnitTest;
10  import emissary.util.io.ResourceReader;
11  
12  import org.junit.jupiter.api.AfterEach;
13  import org.junit.jupiter.api.BeforeEach;
14  import org.junit.jupiter.api.Test;
15  
16  import java.io.IOException;
17  import java.util.ArrayList;
18  import java.util.List;
19  import javax.annotation.Nullable;
20  
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  
25  class RoutingAlgorithmTest extends UnitTest {
26      private MyDirectoryPlace dir;
27      private IBaseDataObject payload;
28      private MyMobileAgent agent;
29      private final List<DirectoryEntry> unknowns = createUnknownEntries();
30      private final List<DirectoryEntry> transforms = createTransformEntries();
31      private final List<DirectoryEntry> analyzers = createAnalyzeEntries();
32  
33      // String unknownDataId = "UNKNOWN::ID";
34      // String transformDataId = "XFORM::TRANSFORM";
35      // String analyzeDataId = "ANALYZE::ANALYZE";
36  
37      @Override
38      @BeforeEach
39      public void setUp() throws Exception {
40          super.setUp();
41  
42          this.payload = DataObjectFactory.getInstance();
43          this.payload.setFilename("testpayload");
44          this.dir = new MyDirectoryPlace("http://example.com:8001/MyDirectoryPlace");
45          this.agent = new MyMobileAgent();
46      }
47  
48      @Override
49      @AfterEach
50      public void tearDown() throws Exception {
51          super.tearDown();
52          this.dir.clearAllEntries();
53          this.dir.shutDown();
54          this.agent.killAgent();
55      }
56  
57      private static List<DirectoryEntry> createUnknownEntries() {
58          final List<DirectoryEntry> unknowns = new ArrayList<>();
59          unknowns.add(new DirectoryEntry("UNKNOWN.s1.ID.http://example.com:8001/U$5050"));
60          unknowns.add(new DirectoryEntry("UNKNOWN.s2.ID.http://example.com:8001/U$5060"));
61          unknowns.add(new DirectoryEntry("UNKNOWN.s3.ID.http://example.com:8001/U$6050"));
62          unknowns.add(new DirectoryEntry("UNKNOWN.s4.ID.http://example.com:8001/U$7050"));
63          return unknowns;
64      }
65  
66      private static List<DirectoryEntry> createTransformEntries() {
67          final List<DirectoryEntry> transforms = new ArrayList<>();
68          transforms.add(new DirectoryEntry("XFORM.s1.TRANSFORM.http://example.com:8001/T$5050"));
69          transforms.add(new DirectoryEntry("XFORM.s2.TRANSFORM.http://example.com:8001/T$5060"));
70          transforms.add(new DirectoryEntry("XFORM.s3.TRANSFORM.http://example.com:8001/T$6050"));
71          transforms.add(new DirectoryEntry("XFORM.s4.TRANSFORM.http://example.com:8001/T$7050"));
72          return transforms;
73      }
74  
75      private static List<DirectoryEntry> createAnalyzeEntries() {
76          final List<DirectoryEntry> analyzers = new ArrayList<>();
77          analyzers.add(new DirectoryEntry("ANALYZE.s1.ANALYZE.http://example.com:8001/A$5050"));
78          analyzers.add(new DirectoryEntry("ANALYZE.s2.ANALYZE.http://example.com:8001/A$5060"));
79          analyzers.add(new DirectoryEntry("ANALYZE.s3.ANALYZE.http://example.com:8001/A$6050"));
80          analyzers.add(new DirectoryEntry("ANALYZE.s4.ANALYZE.http://example.com:8001/A$7050"));
81          return analyzers;
82      }
83  
84      private void loadAllTestEntries() {
85          this.dir.addTestEntries(this.unknowns);
86          this.dir.addTestEntries(this.transforms);
87          this.dir.addTestEntries(this.analyzers);
88      }
89  
90      @Test
91      void testFindsBuriedCurrentFormAndPullsToTop() {
92          this.dir.addTestEntries(this.unknowns);
93          this.payload.pushCurrentForm("UNKNOWN");
94          this.payload.pushCurrentForm("FOO");
95          this.payload.pushCurrentForm("BAR");
96          final int oldSize = this.payload.currentFormSize();
97          final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
98          assertEquals(this.unknowns.get(0).getKey(), result.getKey(), "Next keys returns lowest cost");
99          assertEquals(oldSize, this.payload.currentFormSize(), "Form stack must have same size");
100         assertEquals("UNKNOWN", this.payload.currentForm(), "Payload form used pulled to top");
101     }
102 
103     @Test
104     void testIdsInOrderWithNullLastPlace() {
105         this.dir.addTestEntries(this.unknowns);
106         this.payload.pushCurrentForm("UNKNOWN");
107         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
108         assertEquals(this.unknowns.get(0).getKey(), result.getKey(), "Next keys returns lowest cost");
109     }
110 
111     @Test
112     void testIdsInOrderWithCheaperLastPlace() {
113         this.dir.addTestEntries(this.unknowns);
114         this.payload.pushCurrentForm("UNKNOWN");
115         this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
116         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
117         assertEquals(this.unknowns.get(1).getKey(), result.getKey(), "Next keys should return next in cost/quality order");
118     }
119 
120     @Test
121     void testIdsInOrderGettingLastPlaceOnList() {
122         this.dir.addTestEntries(this.unknowns);
123         this.payload.pushCurrentForm("UNKNOWN");
124         this.payload.appendTransformHistory(this.unknowns.get(this.unknowns.size() - 2).getFullKey());
125         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
126         assertEquals(this.unknowns.get(this.unknowns.size() - 1).getKey(), result.getKey(), "Next keys returns next cost");
127     }
128 
129     @Test
130     void testIdsInOrderGettingNoResultAtEndOfList() {
131         this.dir.addTestEntries(this.unknowns);
132         this.payload.pushCurrentForm("UNKNOWN");
133         this.payload.appendTransformHistory(this.unknowns.get(this.unknowns.size() - 1).getFullKey());
134         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
135         assertNull(result, "Result must be null with all unknown entries loaded not " + result);
136     }
137 
138     @Test
139     void testHighestIdWithAllEntriesLoaded() {
140         loadAllTestEntries();
141         this.unknowns.get(this.unknowns.size() - 1);
142         this.payload.pushCurrentForm("UNKNOWN");
143         this.payload.appendTransformHistory(this.unknowns.get(this.unknowns.size() - 1).getFullKey());
144         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
145         assertNull(result, "Result must be null with all entries loaded not " + result);
146     }
147 
148     @Test
149     void testBackToIdPhaseAfterTransform() {
150         final DirectoryEntry lastPlace = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
151         this.dir.addTestEntry(new DirectoryEntry("FOO.s1.ID.http://example.com:8001/I$7050"));
152         this.dir.addTestEntry(lastPlace);
153         this.dir.addTestEntry(new DirectoryEntry("FOO.s3.ANALYZE.http://example.com:8001/A$7050"));
154 
155         this.payload.pushCurrentForm("FOO");
156         this.payload.appendTransformHistory(lastPlace.getFullKey());
157         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
158         assertEquals("ID", result.getServiceType(), "Should go to ID place after transform");
159     }
160 
161     @Test
162     void testNotBackToIdPhaseAfterAnalyze() {
163         final DirectoryEntry lastPlace = new DirectoryEntry("FOO.s3.ANALYZE.http://example.com:8001/T$7050");
164         this.dir.addTestEntry(new DirectoryEntry("FOO.s1.ID.http://example.com:8001/I$7050"));
165         this.dir.addTestEntry(new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/A$7050"));
166         this.dir.addTestEntry(lastPlace);
167 
168         this.payload.pushCurrentForm("FOO");
169         this.payload.appendTransformHistory(lastPlace.getFullKey());
170         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
171         assertNull(result, "Should not return to ID place after analyze");
172     }
173 
174     @Test
175     void testBackToIdPhaseAfterCoordinate() {
176         final DirectoryEntry lastPlace = new DirectoryEntry("UNKNOWN.s1.ID.http://example.com:8001/I$1010");
177         this.dir.addTestEntry(lastPlace);
178         this.dir.addTestEntry(new DirectoryEntry("UNKNOWN.s3.ID.http://example.com:8001/A$3030"));
179         this.dir.addTestEntry(new DirectoryEntry("UNKNOWN.s4.ANALYZE.http://example.com:8001/A$4040"));
180 
181         // after appending a key w/ coordinated=true to the transform history, we should be in the ID phase
182         this.payload.pushCurrentForm("UNKNOWN");
183         this.payload.appendTransformHistory("UNKNOWN.s1.ID.http://example.com:8001/I$1010");
184         this.payload.appendTransformHistory("UNKNOWN.s2.ANALYZE.http://example.com:8001/T$2020", true);
185         assertEquals("ID", this.agent.getNextKeyAccess(this.dir, this.payload).getServiceType(), "Should go to ID place after coordinate");
186 
187         // after appending a key w/ coordinated=false to the transform history, we should be in the ANALYZE phase
188         this.payload.clearTransformHistory();
189         this.payload.appendTransformHistory("UNKNOWN.s1.ID.http://example.com:8001/I$1010");
190         this.payload.appendTransformHistory("UNKNOWN.s2.ANALYZE.http://example.com:8001/T$2020", false);
191         assertEquals("ANALYZE", this.agent.getNextKeyAccess(this.dir, this.payload).getServiceType(), "Should not return to ID place after analyze");
192     }
193 
194     @Test
195     void testCheckTransformProxyWithTwoFormsDoesNotRepeat() {
196         loadAllTestEntries();
197 
198         // Same place, two service proxy values
199         final DirectoryEntry foo = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
200         final DirectoryEntry bar = new DirectoryEntry("BAR.s2.TRANSFORM.http://example.com:8001/T$7050");
201         this.dir.addTestEntry(foo);
202         this.dir.addTestEntry(bar);
203 
204         this.payload.pushCurrentForm("FOO");
205         this.payload.pushCurrentForm("BAR");
206         this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
207         this.payload.appendTransformHistory(foo.getFullKey());
208 
209         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
210         assertNull(result, "No place remains without looping error but we got " + result + " on " + this.payload.getAllCurrentForms() + " lastPlace="
211                 + this.payload.getLastPlaceVisited());
212     }
213 
214     @Test
215     void testCannotProceedPastMaxItinerarySteps() {
216         loadAllTestEntries();
217         this.payload.pushCurrentForm("UNKNOWN");
218         for (int i = 0; i <= this.agent.getMaxItinerarySteps(); i++) {
219             this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
220         }
221         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
222         assertNull(result, "Must not proceed past MAX steps but we got " + result);
223     }
224 
225     @Test
226     void testCannotProceedPastMaxItineraryStepsWithSetValue() {
227         loadAllTestEntries();
228         this.payload.pushCurrentForm("UNKNOWN");
229         this.agent.setMaxItinerarySteps(10);
230         for (int i = 0; i <= this.agent.getMaxItinerarySteps(); i++) {
231             this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
232         }
233         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
234         assertNull(result, "Must not proceed past MAX steps but we got " + result);
235     }
236 
237     @Test
238     void testContractWithNextKeysPlace() {
239         loadAllTestEntries();
240         this.payload.pushCurrentForm("UNKNOWN");
241         final DirectoryEntry result = this.agent.getNextKeyAccess(null, this.payload);
242         assertNull(result, "Must not return result with null place");
243     }
244 
245     @Test
246     void testContractWithNextKeysPayload() {
247         loadAllTestEntries();
248         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, null);
249         assertNull(result, "Must not return result with null payload");
250     }
251 
252     @Test
253     void testPayloadWithNoCurrentForm() {
254         loadAllTestEntries();
255         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
256         assertNull(result, "Must not return result when payload has no current form");
257     }
258 
259     @Test
260     void testPayloadWithDoneForm() {
261         loadAllTestEntries();
262         this.dir.addTestEntry(new DirectoryEntry("DONE.d1.IO.http://example.com:8001/D$5050"));
263         this.payload.pushCurrentForm(Form.DONE);
264         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
265         assertNull(result, "Must not return result when payload has DONE form");
266     }
267 
268     @Test
269     void testErrorHandlingPopsCurrentForms() {
270         loadAllTestEntries();
271         final DirectoryEntry eplace = new DirectoryEntry("ERROR.e1.IO.http://example.com:8001/E$5050");
272         this.dir.addTestEntry(eplace);
273         this.payload.pushCurrentForm("FOO");
274         this.payload.pushCurrentForm("BAR");
275         this.payload.pushCurrentForm("BAZ");
276         this.payload.pushCurrentForm(Form.ERROR);
277         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
278         assertEquals(eplace.getKey(), result.getKey(), "Routing to error handling place should occur");
279         assertEquals(1, this.payload.currentFormSize(), "Routing to error handling place removes other forms");
280     }
281 
282     @Test
283     void testErrorHandlingErrorPopsAllCurrentForms() {
284         loadAllTestEntries();
285         final DirectoryEntry eplace = new DirectoryEntry("ERROR.e1.IO.http://example.com:8001/E$5050");
286         this.dir.addTestEntry(eplace);
287         this.payload.pushCurrentForm(Form.ERROR);
288         this.payload.pushCurrentForm(Form.ERROR);
289         this.payload.pushCurrentForm(Form.ERROR);
290         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
291         assertEquals(0, this.payload.currentFormSize(), "Error in error handling place removes all forms");
292         assertNull(result, "Error in Error handling must not re-route to error handler but we got " + result);
293     }
294 
295     @Test
296     void testNoRepeatWhenTransformAddsFormButDoesNotRemoveOwnProxyAndIsntEvenOnTop() {
297         loadAllTestEntries();
298         final DirectoryEntry foo = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
299         final DirectoryEntry bar = new DirectoryEntry("BAR.s2.ANALYZE.http://example.com:8001/A$1050");
300         this.dir.addTestEntry(foo);
301         this.dir.addTestEntry(bar);
302 
303         this.payload.pushCurrentForm("BAR");
304         this.payload.pushCurrentForm("FOO");
305         this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
306         this.payload.appendTransformHistory(foo.getFullKey());
307 
308         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
309         assertNotNull(result, "Must move along to analyze with current form of xform still on stack");
310         assertEquals(bar.getKey(), result.getKey(), "Must move along to analyze even with current form of xform place still on stack");
311     }
312 
313     @Test
314     void testVisitedPlaceNoRepeatListForAnalyzeStage() {
315         loadAllTestEntries();
316         final DirectoryEntry foo = new DirectoryEntry("FOO.s2.ANALYZE.http://example.com:8001/A$7050");
317         final DirectoryEntry bar = new DirectoryEntry("BAR.s2.ANALYZE.http://example.com:8001/A$1050");
318         this.dir.addTestEntry(foo);
319         this.dir.addTestEntry(bar);
320 
321         this.payload.pushCurrentForm("BAR");
322         this.payload.pushCurrentForm("FOO");
323         this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
324         this.payload.appendTransformHistory(foo.getFullKey());
325 
326         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
327         assertNull(result, "Must not repeat analyze place even with two forms but we got " + result);
328     }
329 
330     @Test
331     void testNoRepeatWhenTransformAddsFormButDoesNotRemoveOwnProxy() {
332         loadAllTestEntries();
333         final DirectoryEntry foo = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
334         final DirectoryEntry bar = new DirectoryEntry("BAR.s2.ANALYZE.http://example.com:8001/A$1050");
335         this.dir.addTestEntry(foo);
336         this.dir.addTestEntry(bar);
337 
338         this.payload.pushCurrentForm("FOO");
339         this.payload.pushCurrentForm("BAR");
340         this.payload.appendTransformHistory(this.unknowns.get(0).getFullKey());
341         this.payload.appendTransformHistory(foo.getFullKey());
342 
343         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
344         assertNotNull(result, "Must move along to analyze with current form of xform still on stack");
345         assertEquals(bar.getKey(), result.getKey(), "Must move along to analyze even with current form of xform place still on stack");
346     }
347 
348     @Test
349     void testNextKeyFromQueue() {
350         loadAllTestEntries();
351         final DirectoryEntry foo = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
352         this.agent.addEntryToQueue(foo);
353         final int sz = this.agent.queueSize();
354         this.payload.pushCurrentForm("BAR");
355         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
356         assertEquals(foo.getKey(), result.getKey(), "Next key must be from queue when queue non-empty");
357         assertEquals(sz - 1, this.agent.queueSize(), "Queue should drain by one");
358     }
359 
360     @Test
361     void testCompleteKeyRouting() {
362         loadAllTestEntries();
363         final DirectoryEntry foo1 = new DirectoryEntry("FOO.s2.TRANSFORM.http://example.com:8001/T$7050");
364         final DirectoryEntry foo2 = new DirectoryEntry("FOO.s3.TRANSFORM.http://example.com:9999/T$7050");
365         this.dir.addTestEntry(foo1);
366         this.payload.pushCurrentForm(foo2.getKey());
367         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
368         assertEquals(foo2.getKey(), result.getKey(), "Routing must take place to fully qualified key");
369     }
370 
371     @Test
372     void testWildCardProxyHonorsDenyList() throws IOException {
373         loadAllTestEntries();
374 
375         this.payload.pushCurrentForm("MYFORM");
376 
377         // create our place and add it to the directory. This place proxies for "*" but explicitly denies "MYFORM"
378         DelayPlace deniedWildcardPlace = new DelayPlace(new ResourceReader().getConfigDataName(DelayPlace.class).replace("/main/", "/test/"));
379         this.dir.addTestEntry(deniedWildcardPlace.getDirectoryEntry());
380 
381         // Add another entry that proxies for "MYFORM".
382         // Doesn't need an actual place, but does need a higher expense than deniedWildcardPlace
383         DirectoryEntry expected = new DirectoryEntry("MYFORM.s4.ANALYZE.http://example.com:8001/A$9999");
384         this.dir.addTestEntry(expected);
385 
386         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
387         assertEquals(expected, result, "After a denied entry, should get next matching entry for the same stage");
388     }
389 
390     @Test
391     void testWildCardProxyWithDeniedEntry() throws IOException {
392         loadAllTestEntries();
393 
394         this.payload.pushCurrentForm("OTHERFORM");
395 
396         // create our place and add it to the directory. This place proxies for "*" but explicitly denies "MYFORM"
397         DelayPlace deniedWildcardPlace = new DelayPlace(new ResourceReader().getConfigDataName(DelayPlace.class).replace("/main/", "/test/"));
398         this.dir.addTestEntry(deniedWildcardPlace.getDirectoryEntry());
399 
400         // OTHERFORM is not denied so we expect non-null result
401         final DirectoryEntry result = this.agent.getNextKeyAccess(this.dir, this.payload);
402         assertEquals(deniedWildcardPlace.getKey(), result.getKey(), "Should get matching entry for wildcard place");
403 
404         this.payload.pushCurrentForm("MYFORM");
405 
406         // MYFORM is denied so null should be returned
407         final DirectoryEntry nullResult = this.agent.getNextKeyAccess(this.dir, this.payload);
408         assertNull(nullResult, "MYFORM should be denied");
409     }
410 
411     /**
412      * Extend directory place to allow us to access the entryMap
413      */
414     class MyDirectoryPlace extends DirectoryPlace {
415         public MyDirectoryPlace(final String placeLoc) throws IOException {
416             super(new ResourceReader().getConfigDataAsStream(thisPackage + ".MyDirectoryPlace.cfg"), placeLoc, new EmissaryNode());
417         }
418 
419         public void clearAllEntries() {
420             entryMap.clear();
421         }
422 
423         public void addTestEntry(final DirectoryEntry newEntry) {
424             addEntry(newEntry);
425         }
426 
427         public void addTestEntries(final List<DirectoryEntry> newEntryList) {
428             addEntries(newEntryList);
429         }
430     }
431 
432     private static final class MyMobileAgent extends HDMobileAgent {
433         /**
434          * provide uid for serialization
435          */
436         private static final long serialVersionUID = 6667669555504467253L;
437 
438         public DirectoryEntry getNextKeyAccess(@Nullable final IServiceProviderPlace place, @Nullable final IBaseDataObject payload) {
439             return getNextKey(place, payload);
440         }
441 
442         public void addEntryToQueue(final DirectoryEntry entry) {
443             nextKeyQueue.add(entry);
444         }
445 
446         public int queueSize() {
447             return nextKeyQueue.size();
448         }
449     }
450 }