@@ -82,38 +82,88 @@ public void tearDown() throws Exception {
8282 }
8383
8484 @ Test
85- public void checkAuthorization_doesNotCacheNonCodegenMethods () throws Exception {
85+ public void checkAuthorization_deduplicatesSimultaneousNonCodegenMethods () throws Exception {
8686 ListenableFuture <Status > authResult1 = authState .checkAuthorization (NONCODEGEN_METHOD );
8787 assertThat (authResult1 .isDone ()).isFalse ();
8888
89+ ListenableFuture <Status > authResult2 = authState .checkAuthorization (NONCODEGEN_METHOD );
90+ assertThat (authResult2 .isDone ()).isFalse ();
91+
92+ // The fake policy checker was only invoked ONCE thanks to deduplication.
93+ // Completing that single underlying auth check satisfies both futures.
8994 fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .OK );
95+
9096 assertThat (authResult1 .get ()).isEqualTo (Status .OK );
97+ assertThat (authResult2 .get ()).isEqualTo (Status .OK );
9198 assertThat (fakePolicyChecker .statusesToSet ).isEmpty ();
9299
93100 // Because it's a non-codegen method, the auth result should not be cached.
94- ListenableFuture <Status > authResult2 = authState .checkAuthorization (NONCODEGEN_METHOD );
95- assertThat (authResult2 .isDone ()).isFalse ();
101+ ListenableFuture <Status > authResult3 = authState .checkAuthorization (NONCODEGEN_METHOD );
102+ assertThat (authResult3 .isDone ()).isFalse ();
96103
97104 fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .PERMISSION_DENIED );
98- assertThat (authResult2 .get ()).isEqualTo (Status .PERMISSION_DENIED );
105+ assertThat (authResult3 .get ()).isEqualTo (Status .PERMISSION_DENIED );
99106 }
100107
101108 @ Test
102- public void checkAuthorization_cachesCodegenMethods () throws Exception {
109+ public void checkAuthorization_cachesSimultaneousCodegenMethods () throws Exception {
103110 ListenableFuture <Status > authResult1 = authState .checkAuthorization (CODEGEN_METHOD );
104111 assertThat (authResult1 .isDone ()).isFalse ();
105112
113+ ListenableFuture <Status > authResult2 = authState .checkAuthorization (CODEGEN_METHOD );
114+ assertThat (authResult2 .isDone ()).isFalse ();
115+
116+ // The fake policy checker was only invoked ONCE thanks to deduplication.
117+ // Completing that single underlying auth check satisfies both futures.
106118 fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .OK );
119+
107120 assertThat (authResult1 .get ()).isEqualTo (Status .OK );
121+ assertThat (authResult2 .get ()).isEqualTo (Status .OK );
108122 assertThat (fakePolicyChecker .statusesToSet ).isEmpty ();
109123
110124 // Because it's a codegen method, the auth result should be cached for the life of the object.
125+ ListenableFuture <Status > authResult3 = authState .checkAuthorization (CODEGEN_METHOD );
126+ assertThat (authResult3 .isDone ()).isTrue ();
127+ assertThat (authResult3 .get ()).isEqualTo (Status .OK );
128+ assertThat (fakePolicyChecker .statusesToSet ).isEmpty ();
129+ }
130+
131+ @ Test
132+ public void checkAuthorization_cachesPermissionDeniedForCodegenMethods () throws Exception {
133+ ListenableFuture <Status > authResult1 = authState .checkAuthorization (CODEGEN_METHOD );
134+ assertThat (authResult1 .isDone ()).isFalse ();
135+
136+ fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .PERMISSION_DENIED );
137+
138+ assertThat (authResult1 .get ()).isEqualTo (Status .PERMISSION_DENIED );
139+ assertThat (fakePolicyChecker .statusesToSet ).isEmpty ();
140+
141+ // Because it's a codegen method, the non-OK status should be cached for the life of the object.
111142 ListenableFuture <Status > authResult2 = authState .checkAuthorization (CODEGEN_METHOD );
112143 assertThat (authResult2 .isDone ()).isTrue ();
113- assertThat (authResult2 .get ()).isEqualTo (Status .OK );
144+ assertThat (authResult2 .get ()).isEqualTo (Status .PERMISSION_DENIED );
114145 assertThat (fakePolicyChecker .statusesToSet ).isEmpty ();
115146 }
116147
148+ @ Test
149+ public void checkAuthorization_cancellation_doesNotPropagateToUnderlyingCheck () throws Exception {
150+ ListenableFuture <Status > authResult1 = authState .checkAuthorization (CODEGEN_METHOD );
151+ ListenableFuture <Status > authResult2 = authState .checkAuthorization (CODEGEN_METHOD );
152+
153+ // Cancel the first future.
154+ authResult1 .cancel (true );
155+
156+ assertThat (authResult1 .isCancelled ()).isTrue ();
157+ // The second future should NOT be cancelled, because the cancellation shouldn't propagate
158+ // to the underlying shared future.
159+ assertThat (authResult2 .isCancelled ()).isFalse ();
160+
161+ // Completing the underlying auth check satisfies the non-cancelled future.
162+ fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .OK );
163+
164+ assertThat (authResult2 .get ()).isEqualTo (Status .OK );
165+ }
166+
117167 @ Test
118168 public void checkAuthorization_failedFuture_notCached () throws Exception {
119169 ListenableFuture <Status > authResult1 = authState .checkAuthorization (CODEGEN_METHOD );
@@ -133,11 +183,37 @@ public void checkAuthorization_failedFuture_notCached() throws Exception {
133183 assertThat (authResult2 .get ()).isEqualTo (Status .OK );
134184 }
135185
186+
187+
188+ @ Test
189+ public void checkAuthorization_synchronousException_doesNotLeaveStrandedFuture ()
190+ throws Exception {
191+ IllegalStateException syncException = new IllegalStateException ("ouch" );
192+ fakePolicyChecker .syncExceptionsToThrow .add (syncException );
193+
194+ // The synchronous exception is safely returned as a failed future.
195+ ListenableFuture <Status > authResult1 = authState .checkAuthorization (CODEGEN_METHOD );
196+ ExecutionException ee = assertThrows (ExecutionException .class , authResult1 ::get );
197+ assertThat (ee ).hasCauseThat ().isSameInstanceAs (syncException );
198+
199+ // Ensure the failed check can be retried.
200+ ListenableFuture <Status > authResult2 = authState .checkAuthorization (CODEGEN_METHOD );
201+ assertThat (authResult2 .isDone ()).isFalse ();
202+
203+ fakePolicyChecker .takeNextAuthRequestOrDie ().set (Status .OK );
204+ assertThat (authResult2 .get ()).isEqualTo (Status .OK );
205+ }
206+
136207 private static final class FakeServerPolicyChecker implements ServerPolicyChecker {
208+ final LinkedBlockingQueue <RuntimeException > syncExceptionsToThrow = new LinkedBlockingQueue <>();
137209 final LinkedBlockingQueue <SettableFuture <Status >> statusesToSet = new LinkedBlockingQueue <>();
138210
139211 @ Override
140212 public ListenableFuture <Status > checkAuthorizationForServiceAsync (int uid , String serviceName ) {
213+ RuntimeException syncException = syncExceptionsToThrow .poll ();
214+ if (syncException != null ) {
215+ throw syncException ;
216+ }
141217 SettableFuture <Status > pendingResult = SettableFuture .create ();
142218 statusesToSet .add (pendingResult );
143219 return pendingResult ;
0 commit comments