Skip to content

Commit a03649c

Browse files
authored
Support authorizedCollections option for listCollections helpers (#1270)
JAVA-4353
1 parent 104f7da commit a03649c

File tree

48 files changed

+1773
-85
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1773
-85
lines changed

config/detekt/baseline.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
<ID>LongMethod:KotlinSerializerCodecTest.kt$KotlinSerializerCodecTest$@Test fun testDataClassOptionalBsonValues()</ID>
1010
<ID>MaxLineLength:MapReduceFlow.kt$MapReduceFlow$*</ID>
1111
<ID>MaxLineLength:MapReduceIterable.kt$MapReduceIterable$*</ID>
12+
<ID>MaxLineLength:ListCollectionsFlow.kt$ListCollectionsFlow$*</ID>
13+
<ID>MaxLineLength:ListCollectionsIterable.kt$ListCollectionsIterable$*</ID>
14+
<ID>MaxLineLength:ListCollectionNamesIterable.kt$ListCollectionNamesIterable$*</ID>
15+
<ID>MaxLineLength:ListCollectionNamesFlow.kt$ListCollectionNamesFlow$*</ID>
1216
<ID>SwallowedException:MockitoHelper.kt$MockitoHelper.DeepReflectionEqMatcher$e: Throwable</ID>
1317
<ID>TooManyFunctions:ClientSession.kt$ClientSession : jClientSession</ID>
1418
<ID>TooManyFunctions:FindFlow.kt$FindFlow&lt;T : Any> : Flow</ID>

driver-core/src/main/com/mongodb/internal/operation/AsyncOperations.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,11 @@ public AsyncWriteOperation<Void> dropIndex(final Bson keys, final DropIndexOptio
307307
}
308308

309309
public <TResult> AsyncReadOperation<AsyncBatchCursor<TResult>> listCollections(final String databaseName, final Class<TResult> resultClass,
310-
final Bson filter, final boolean collectionNamesOnly,
310+
final Bson filter, final boolean collectionNamesOnly, final boolean authorizedCollections,
311311
final Integer batchSize, final long maxTimeMS,
312312
final BsonValue comment) {
313-
return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, batchSize, maxTimeMS, comment);
313+
return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, authorizedCollections,
314+
batchSize, maxTimeMS, comment);
314315
}
315316

316317
public <TResult> AsyncReadOperation<AsyncBatchCursor<TResult>> listDatabases(final Class<TResult> resultClass, final Bson filter,

driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.mongodb.MongoCommandException;
2020
import com.mongodb.MongoNamespace;
21+
import com.mongodb.internal.VisibleForTesting;
2122
import com.mongodb.internal.async.AsyncBatchCursor;
2223
import com.mongodb.internal.async.SingleResultCallback;
2324
import com.mongodb.internal.async.function.AsyncCallbackSupplier;
@@ -37,6 +38,7 @@
3738
import java.util.function.Supplier;
3839

3940
import static com.mongodb.assertions.Assertions.notNull;
41+
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
4042
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
4143
import static com.mongodb.internal.operation.AsyncOperationHelper.CommandReadTransformerAsync;
4244
import static com.mongodb.internal.operation.AsyncOperationHelper.createReadCommandAndExecuteAsync;
@@ -49,6 +51,7 @@
4951
import static com.mongodb.internal.operation.CommandOperationHelper.rethrowIfNotNamespaceError;
5052
import static com.mongodb.internal.operation.CursorHelper.getCursorDocumentFromBatchSize;
5153
import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull;
54+
import static com.mongodb.internal.operation.DocumentHelper.putIfTrue;
5255
import static com.mongodb.internal.operation.OperationHelper.LOGGER;
5356
import static com.mongodb.internal.operation.OperationHelper.canRetryRead;
5457
import static com.mongodb.internal.operation.SingleBatchCursor.createEmptySingleBatchCursor;
@@ -62,6 +65,8 @@
6265
* An operation that provides a cursor allowing iteration through the metadata of all the collections in a database. This operation
6366
* ensures that the value of the {@code name} field of each returned document is the simple name of the collection rather than the full
6467
* namespace.
68+
* <p>
69+
* See <a href="https://docs.mongodb.com/manual/reference/command/listCollections/">{@code listCollections}</a></p>.
6570
*
6671
* <p>This class is not part of the public API and may be removed or changed at any time</p>
6772
*/
@@ -73,6 +78,7 @@ public class ListCollectionsOperation<T> implements AsyncReadOperation<AsyncBatc
7378
private int batchSize;
7479
private long maxTimeMS;
7580
private boolean nameOnly;
81+
private boolean authorizedCollections;
7682
private BsonValue comment;
7783

7884
public ListCollectionsOperation(final String databaseName, final Decoder<T> decoder) {
@@ -137,6 +143,20 @@ public ListCollectionsOperation<T> comment(@Nullable final BsonValue comment) {
137143
return this;
138144
}
139145

146+
public ListCollectionsOperation<T> authorizedCollections(final boolean authorizedCollections) {
147+
this.authorizedCollections = authorizedCollections;
148+
return this;
149+
}
150+
151+
/**
152+
* This method is used by tests via the reflection API. See
153+
* {@code com.mongodb.reactivestreams.client.internal.TestHelper.assertOperationIsTheSameAs}.
154+
*/
155+
@VisibleForTesting(otherwise = PRIVATE)
156+
public boolean isAuthorizedCollections() {
157+
return authorizedCollections;
158+
}
159+
140160
@Override
141161
public BatchCursor<T> execute(final ReadBinding binding) {
142162
RetryState retryState = initialRetryState(retryReads);
@@ -206,6 +226,7 @@ private BsonDocument getCommand() {
206226
if (nameOnly) {
207227
command.append("nameOnly", BsonBoolean.TRUE);
208228
}
229+
putIfTrue(command, "authorizedCollections", authorizedCollections);
209230
if (maxTimeMS > 0) {
210231
command.put("maxTimeMS", new BsonInt64(maxTimeMS));
211232
}

driver-core/src/main/com/mongodb/internal/operation/Operations.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,12 +686,14 @@ DropIndexOperation dropIndex(final Bson keys, final DropIndexOptions dropIndexOp
686686

687687
<TResult> ListCollectionsOperation<TResult> listCollections(final String databaseName, final Class<TResult> resultClass,
688688
final Bson filter, final boolean collectionNamesOnly,
689+
final boolean authorizedCollections,
689690
@Nullable final Integer batchSize, final long maxTimeMS,
690691
final BsonValue comment) {
691692
return new ListCollectionsOperation<>(databaseName, codecRegistry.get(resultClass))
692693
.retryReads(retryReads)
693694
.filter(toBsonDocument(filter))
694695
.nameOnly(collectionNamesOnly)
696+
.authorizedCollections(authorizedCollections)
695697
.batchSize(batchSize == null ? 0 : batchSize)
696698
.maxTime(maxTimeMS, MILLISECONDS)
697699
.comment(comment);

driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,11 @@ public WriteOperation<Void> dropIndex(final Bson keys, final DropIndexOptions op
283283

284284
public <TResult> ReadOperation<BatchCursor<TResult>> listCollections(final String databaseName, final Class<TResult> resultClass,
285285
final Bson filter, final boolean collectionNamesOnly,
286+
final boolean authorizedCollections,
286287
@Nullable final Integer batchSize, final long maxTimeMS,
287288
final BsonValue comment) {
288-
return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, batchSize, maxTimeMS, comment);
289+
return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, authorizedCollections,
290+
batchSize, maxTimeMS, comment);
289291
}
290292

291293
public <TResult> ReadOperation<BatchCursor<TResult>> listDatabases(final Class<TResult> resultClass, final Bson filter,

driver-core/src/test/functional/com/mongodb/internal/operation/ListCollectionsOperationSpecification.groovy

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,28 @@ class ListCollectionsOperationSpecification extends OperationFunctionalSpecifica
190190
collection.size() == 2
191191
}
192192

193+
@IgnoreIf({ serverVersionLessThan(4, 0) })
194+
def 'should only get collection names when nameOnly and authorizedCollections are requested'() {
195+
given:
196+
def operation = new ListCollectionsOperation(databaseName, new DocumentCodec())
197+
.nameOnly(true)
198+
.authorizedCollections(true)
199+
getCollectionHelper().create('collection6', new CreateCollectionOptions())
200+
201+
when:
202+
def cursor = operation.execute(getBinding())
203+
def collection = cursor.next()[0]
204+
205+
then:
206+
collection.size() == 2
207+
}
208+
193209
@IgnoreIf({ serverVersionLessThan(3, 4) || serverVersionAtLeast(4, 0) })
194210
def 'should only get all field names when nameOnly is requested on server versions that do not support nameOnly'() {
195211
given:
196212
def operation = new ListCollectionsOperation(databaseName, new DocumentCodec())
197213
.nameOnly(true)
198-
getCollectionHelper().create('collection6', new CreateCollectionOptions())
214+
getCollectionHelper().create('collection7', new CreateCollectionOptions())
199215

200216
when:
201217
def cursor = operation.execute(getBinding())
@@ -205,6 +221,21 @@ class ListCollectionsOperationSpecification extends OperationFunctionalSpecifica
205221
collection.size() > 2
206222
}
207223

224+
@IgnoreIf({ serverVersionLessThan(4, 0) })
225+
def 'should get all fields when authorizedCollections is requested and nameOnly is not requested'() {
226+
given:
227+
def operation = new ListCollectionsOperation(databaseName, new DocumentCodec())
228+
.nameOnly(false)
229+
.authorizedCollections(true)
230+
getCollectionHelper().create('collection8', new CreateCollectionOptions())
231+
232+
when:
233+
def cursor = operation.execute(getBinding())
234+
def collection = cursor.next()[0]
235+
236+
then:
237+
collection.size() > 2
238+
}
208239

209240
def 'should return collection names if a collection exists asynchronously'() {
210241
given:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal.mockito;
17+
18+
import com.mongodb.lang.Nullable;
19+
import org.mockito.invocation.InvocationOnMock;
20+
import org.mockito.stubbing.Answer;
21+
22+
import java.util.function.Consumer;
23+
24+
import static com.mongodb.assertions.Assertions.fail;
25+
import static java.lang.String.format;
26+
27+
/**
28+
* @see MongoMockito#mock(Class, Consumer)
29+
*/
30+
final class InsufficientStubbingDetector implements Answer<Void> {
31+
private boolean enabled;
32+
33+
InsufficientStubbingDetector() {
34+
}
35+
36+
@Nullable
37+
@Override
38+
public Void answer(final InvocationOnMock invocation) throws AssertionError {
39+
if (enabled) {
40+
throw fail(format("Insufficient stubbing. Unexpected invocation %s on the object %s.", invocation, invocation.getMock()));
41+
}
42+
return null;
43+
}
44+
45+
void enable() {
46+
enabled = true;
47+
}
48+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal.mockito;
17+
18+
import com.mongodb.internal.binding.ReadBinding;
19+
import com.mongodb.internal.connection.OperationContext;
20+
import com.mongodb.internal.diagnostics.logging.Logger;
21+
import com.mongodb.internal.diagnostics.logging.Loggers;
22+
import com.mongodb.internal.operation.ListCollectionsOperation;
23+
import org.bson.BsonDocument;
24+
import org.bson.codecs.BsonDocumentCodec;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
import org.mockito.Mockito;
28+
import org.mockito.internal.stubbing.answers.ThrowsException;
29+
30+
import static org.junit.jupiter.api.Assertions.assertThrows;
31+
import static org.mockito.Mockito.when;
32+
33+
final class InsufficientStubbingDetectorDemoTest {
34+
private static final Logger LOGGER = Loggers.getLogger(InsufficientStubbingDetectorDemoTest.class.getSimpleName());
35+
36+
private ListCollectionsOperation<BsonDocument> operation;
37+
38+
@BeforeEach
39+
void beforeEach() {
40+
operation = new ListCollectionsOperation<>("db", new BsonDocumentCodec());
41+
}
42+
43+
@Test
44+
void mockObjectWithDefaultAnswer() {
45+
ReadBinding binding = Mockito.mock(ReadBinding.class);
46+
LOGGER.info("", assertThrows(NullPointerException.class, () -> operation.execute(binding)));
47+
}
48+
49+
@Test
50+
void mockObjectWithThrowsException() {
51+
ReadBinding binding = Mockito.mock(ReadBinding.class,
52+
new ThrowsException(new AssertionError("Insufficient stubbing for " + ReadBinding.class)));
53+
LOGGER.info("", assertThrows(AssertionError.class, () -> operation.execute(binding)));
54+
}
55+
56+
@Test
57+
void mockObjectWithInsufficientStubbingDetector() {
58+
ReadBinding binding = MongoMockito.mock(ReadBinding.class);
59+
LOGGER.info("", assertThrows(AssertionError.class, () -> operation.execute(binding)));
60+
}
61+
62+
@Test
63+
void stubbingWithThrowsException() {
64+
ReadBinding binding = Mockito.mock(ReadBinding.class,
65+
new ThrowsException(new AssertionError("Unfortunately, you cannot do stubbing")));
66+
assertThrows(AssertionError.class, () -> when(binding.getOperationContext()).thenReturn(new OperationContext()));
67+
}
68+
69+
@Test
70+
void stubbingWithInsufficientStubbingDetector() {
71+
MongoMockito.mock(ReadBinding.class, bindingMock ->
72+
when(bindingMock.getOperationContext()).thenReturn(new OperationContext())
73+
);
74+
}
75+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal.mockito;
17+
18+
import com.mongodb.lang.Nullable;
19+
import org.mockito.Answers;
20+
import org.mockito.Mockito;
21+
import org.mockito.internal.stubbing.answers.ThrowsException;
22+
import org.mockito.stubbing.OngoingStubbing;
23+
24+
import java.util.function.Consumer;
25+
26+
import static org.mockito.Mockito.when;
27+
import static org.mockito.Mockito.withSettings;
28+
29+
/**
30+
* Complements {@link Mockito}.
31+
*/
32+
public final class MongoMockito {
33+
/**
34+
* Is equivalent to calling {@link #mock(Class, Consumer)} with a {@code null} {@code tuner}.
35+
*/
36+
public static <T> T mock(final Class<T> classToMock) {
37+
return mock(classToMock, null);
38+
}
39+
40+
/**
41+
* This method is similar to {@link Mockito#mock(Class)} but changes the default behavior of the methods of a mock object
42+
* such that insufficient stubbing is detected and reported. By default, Mockito uses {@link Answers#RETURNS_DEFAULTS}.
43+
* While this answer has potential to save users some stubbing work, the provided convenience may not be worth the cost:
44+
* if the default result (often {@code null} for reference types) is insufficient,
45+
* one likely gets an unhelpful {@link NullPointerException}
46+
* (see {@link InsufficientStubbingDetectorDemoTest#mockObjectWithDefaultAnswer()}),
47+
* or a silent incorrect behavior with no clear indication of the mock object method that caused the problem.
48+
* Furthermore, a working test that uses mock objects may be unwittingly broken when refactoring production code.
49+
* While this particular issue is inherent to tests that use mock objects,
50+
* broken tests not indicating clearly what is wrong make matters worse.
51+
* <p>
52+
* Mockito has {@link ThrowsException},
53+
* and at first glance it may seem like using it may help detecting insufficient stubbing.
54+
* It can point us to a line where the insufficiently stubbed method was called at, but it cannot tell us the name of that method
55+
* (see {@link InsufficientStubbingDetectorDemoTest#mockObjectWithThrowsException()}).
56+
* Moreover, a mock object created with {@link ThrowsException} as its default answer cannot be stubbed:
57+
* stubbing requires calling methods of the mock object, but they all complete abruptly
58+
* (see {@link InsufficientStubbingDetectorDemoTest#stubbingWithThrowsException()}).
59+
* Therefore, {@link ThrowsException} is not suitable for detecting insufficient stubbing.</p>
60+
* <p>
61+
* This method overcomes both of the aforementioned limitations by using {@link InsufficientStubbingDetector} as the default answer
62+
* (see {@link InsufficientStubbingDetectorDemoTest#mockObjectWithInsufficientStubbingDetector()},
63+
* {@link InsufficientStubbingDetectorDemoTest#stubbingWithInsufficientStubbingDetector()}).
64+
* Note also that for convenience, {@link InsufficientStubbingDetector} stubs the {@link Object#toString()} method by using
65+
* {@link OngoingStubbing#thenCallRealMethod()}, unless this stubbing is overwritten by the {@code tuner}.</p>
66+
*/
67+
public static <T> T mock(final Class<T> classToMock, @Nullable final Consumer<T> tuner) {
68+
final InsufficientStubbingDetector insufficientStubbingDetector = new InsufficientStubbingDetector();
69+
final T mock = Mockito.mock(classToMock, withSettings().defaultAnswer(insufficientStubbingDetector));
70+
when(mock.toString()).thenCallRealMethod();
71+
if (tuner != null) {
72+
tuner.accept(mock);
73+
}
74+
insufficientStubbingDetector.enable();
75+
return mock;
76+
}
77+
78+
private MongoMockito() {
79+
}
80+
}

0 commit comments

Comments
 (0)