1 /*
2  * Copyright 2020 The Android Open Source Project
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 
17 package androidx.appsearch.testutil;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import androidx.annotation.RestrictTo;
23 import androidx.appsearch.app.AppSearchBatchResult;
24 import androidx.appsearch.app.AppSearchSession;
25 import androidx.appsearch.app.GenericDocument;
26 import androidx.appsearch.app.GetByDocumentIdRequest;
27 import androidx.appsearch.app.SearchResult;
28 import androidx.appsearch.app.SearchResults;
29 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
30 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
31 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
32 import androidx.appsearch.testutil.flags.DeviceFlagsValueProvider;
33 
34 import org.jspecify.annotations.NonNull;
35 import org.junit.rules.RuleChain;
36 
37 import java.security.MessageDigest;
38 import java.security.NoSuchAlgorithmException;
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.concurrent.Future;
43 import java.util.concurrent.ThreadLocalRandom;
44 
45 /**
46  * Class with helper functions for testing for AppSearch.
47  *
48  * @exportToFramework:hide
49  */
50 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
51 public class AppSearchTestUtils {
52     /** Checks batch result. */
checkIsBatchResultSuccess( @onNull Future<AppSearchBatchResult<K, V>> future)53     public static <K, V> @NonNull AppSearchBatchResult<K, V> checkIsBatchResultSuccess(
54             @NonNull Future<AppSearchBatchResult<K, V>> future) throws Exception {
55         AppSearchBatchResult<K, V> result = future.get();
56         assertWithMessage("AppSearchBatchResult not successful: " + result)
57                 .that(result.isSuccess()).isTrue();
58         return result;
59     }
60 
61     /** Gets documents from ids. */
doGet( @onNull AppSearchSession session, @NonNull String namespace, String @NonNull ... ids)62     public static @NonNull List<GenericDocument> doGet(
63             @NonNull AppSearchSession session, @NonNull String namespace, String @NonNull ... ids)
64             throws Exception {
65         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
66                 session.getByDocumentIdAsync(
67                         new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
68         assertThat(result.getSuccesses()).hasSize(ids.length);
69         assertThat(result.getFailures()).isEmpty();
70         List<GenericDocument> list = new ArrayList<>(ids.length);
71         for (String id : ids) {
72             list.add(result.getSuccesses().get(id));
73         }
74         return list;
75     }
76 
77     /** Gets documents from {@link GetByDocumentIdRequest}. */
doGet( @onNull AppSearchSession session, @NonNull GetByDocumentIdRequest request)78     public static @NonNull List<GenericDocument> doGet(
79             @NonNull AppSearchSession session, @NonNull GetByDocumentIdRequest request)
80             throws Exception {
81         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
82                 session.getByDocumentIdAsync(request));
83         Set<String> ids = request.getIds();
84         assertThat(result.getSuccesses()).hasSize(ids.size());
85         assertThat(result.getFailures()).isEmpty();
86         List<GenericDocument> list = new ArrayList<>(ids.size());
87         for (String id : ids) {
88             list.add(result.getSuccesses().get(id));
89         }
90         return list;
91     }
92 
93     /** Extracts documents from {@link SearchResults}. */
convertSearchResultsToDocuments( @onNull SearchResults searchResults)94     public static @NonNull List<GenericDocument> convertSearchResultsToDocuments(
95             @NonNull SearchResults searchResults)
96             throws Exception {
97         List<SearchResult> results = retrieveAllSearchResults(searchResults);
98         List<GenericDocument> documents = new ArrayList<>(results.size());
99         for (SearchResult result : results) {
100             documents.add(result.getGenericDocument());
101         }
102         return documents;
103     }
104 
105     /** Extracts all {@link SearchResult} from {@link SearchResults}. */
retrieveAllSearchResults( @onNull SearchResults searchResults)106     public static @NonNull List<SearchResult> retrieveAllSearchResults(
107             @NonNull SearchResults searchResults) throws Exception {
108         List<SearchResult> page = searchResults.getNextPageAsync().get();
109         List<SearchResult> results = new ArrayList<>();
110         while (!page.isEmpty()) {
111             results.addAll(page);
112             page = searchResults.getNextPageAsync().get();
113         }
114         return results;
115     }
116 
117     /**
118      * Creates a mock {@link VisibilityChecker} where schema is searchable if prefixedSchema is
119      * one of the provided set of visiblePrefixedSchemas and caller does not have system access.
120      *
121      * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
122      * @return Mocked {@link VisibilityChecker} instance.
123      */
createMockVisibilityChecker( @onNull Set<String> visiblePrefixedSchemas)124     public static @NonNull VisibilityChecker createMockVisibilityChecker(
125             @NonNull Set<String> visiblePrefixedSchemas) {
126         return new VisibilityChecker() {
127             @Override
128             public boolean isSchemaSearchableByCaller(
129                     @NonNull CallerAccess callerAccess,
130                     @NonNull String packageName,
131                     @NonNull String prefixedSchema,
132                     @NonNull VisibilityStore visibilityStore) {
133                 return visiblePrefixedSchemas.contains(prefixedSchema);
134             }
135 
136             @Override
137             public boolean doesCallerHaveSystemAccess(@NonNull String s) {
138                 return false;
139             }
140         };
141     }
142 
143     /**
144      * Creates a mock {@link VisibilityChecker}, where it can be configured if schema is searchable
145      * by caller and caller does not have system access.
146      *
147      * @param isSchemaSearchableByCaller Schema visibility for caller.
148      * @return Mocked {@link VisibilityChecker} instance.
149      */
150     public static @NonNull VisibilityChecker createMockVisibilityChecker(
151             boolean isSchemaSearchableByCaller) {
152         return new VisibilityChecker() {
153             @Override
154             public boolean isSchemaSearchableByCaller(
155                     @NonNull CallerAccess callerAccess,
156                     @NonNull String packageName,
157                     @NonNull String prefixedSchema,
158                     @NonNull VisibilityStore visibilityStore) {
159                 return isSchemaSearchableByCaller;
160             }
161 
162             @Override
163             public boolean doesCallerHaveSystemAccess(@NonNull String s) {
164                 return false;
165             }
166         };
167     }
168 
169     /** Generate an array contains random bytes for the given length.     */
170     public static byte @NonNull [] generateRandomBytes(int length) {
171         byte[] bytes = new byte[length];
172         ThreadLocalRandom.current().nextBytes(bytes);
173         return bytes;
174     }
175 
176     /** Calculate the sha-256 digest for the given data.     */
177     public static byte @NonNull [] calculateDigest(byte @NonNull [] data)
178             throws NoSuchAlgorithmException {
179         MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
180         messageDigest.update(data);
181         return messageDigest.digest();
182     }
183 
184     /** Returns a rule chain of common test rules for tests. */
185     public static @NonNull RuleChain createCommonTestRules() {
186         // We don't have another common test rule yet, but we can add one later with .around() here
187         return RuleChain.outerRule(DeviceFlagsValueProvider.createCheckFlagsRule());
188     }
189 
190     private AppSearchTestUtils() {
191     }
192 }
193