go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/service_workers/prefetch/prefetch.test.ts (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { queryAuthState, setAuthStateCache } from '@/common/api/auth_state'; 16 import { BUILD_FIELD_MASK, BuildsService } from '@/common/services/buildbucket'; 17 import { 18 getInvIdFromBuildId, 19 getInvIdFromBuildNum, 20 RESULT_LIMIT, 21 ResultDb, 22 } from '@/common/services/resultdb'; 23 import { PrpcClientExt } from '@/generic_libs/tools/prpc_client_ext'; 24 25 import { Prefetcher } from './prefetch'; 26 27 describe('Prefetcher', () => { 28 let fetchStub: jest.MockedFunction<typeof fetch>; 29 let respondWithStub: jest.MockedFunction< 30 (_res: Response | ReturnType<typeof fetch>) => void 31 >; 32 let prefetcher: Prefetcher; 33 34 // Helps generate fetch requests that are identical to the ones generated 35 // by the pRPC Clients. 36 let fetchInterceptor: jest.MockedFunction<typeof fetch>; 37 let buildsService: BuildsService; 38 let resultdb: ResultDb; 39 40 beforeEach(async () => { 41 jest.useFakeTimers(); 42 await setAuthStateCache({ 43 accessToken: 'access-token', 44 identity: 'user:user-id', 45 }); 46 47 fetchStub = jest.fn(fetch); 48 respondWithStub = jest.fn( 49 (_res: Response | ReturnType<typeof fetch>) => {}, 50 ); 51 prefetcher = new Prefetcher(SETTINGS, fetchStub); 52 53 fetchInterceptor = jest.fn(fetch); 54 fetchInterceptor.mockResolvedValue(new Response('')); 55 buildsService = new BuildsService( 56 new PrpcClientExt( 57 { host: SETTINGS.buildbucket.host, fetchImpl: fetchInterceptor }, 58 () => 'access-token', 59 ), 60 ); 61 resultdb = new ResultDb( 62 new PrpcClientExt( 63 { host: SETTINGS.resultdb.host, fetchImpl: fetchInterceptor }, 64 () => 'access-token', 65 ), 66 ); 67 }); 68 69 afterEach(() => { 70 jest.useRealTimers(); 71 }); 72 73 test('prefetches build page resources', async () => { 74 const authResponse = new Response( 75 JSON.stringify({ accessToken: 'access-token', identity: 'user:user-id' }), 76 ); 77 const buildResponse = new Response(JSON.stringify({})); 78 const invResponse = new Response(JSON.stringify({})); 79 const testVariantsResponse = new Response(JSON.stringify({})); 80 81 fetchStub.mockImplementation(async (input, init) => { 82 const req = new Request(input, init); 83 switch (req.url) { 84 case self.origin + '/auth/openid/state': 85 return authResponse; 86 case `https://${SETTINGS.buildbucket.host}/prpc/buildbucket.v2.Builds/GetBuild`: 87 return buildResponse; 88 case `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetInvocation`: 89 return invResponse; 90 case `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/QueryTestVariants`: 91 return testVariantsResponse; 92 default: 93 throw new Error('unexpected request URL'); 94 } 95 }); 96 97 const invName = 98 'invocations/' + 99 (await getInvIdFromBuildNum( 100 { 101 project: 'chromium', 102 bucket: 'ci', 103 builder: 'Win7 Tests (1)', 104 }, 105 116372, 106 )); 107 108 await prefetcher.prefetchResources( 109 '/ui/p/chromium/builders/ci/Win7%20Tests%20(1)/116372', 110 ); 111 112 await jest.advanceTimersByTimeAsync(100); 113 await jest.advanceTimersByTimeAsync(100); 114 115 const requestedUrls = fetchStub.mock.calls.map( 116 (c) => new Request(...c).url, 117 ); 118 expect(requestedUrls.length).toStrictEqual(4); 119 expect(requestedUrls).toEqual( 120 expect.arrayContaining([ 121 self.origin + '/auth/openid/state', 122 `https://${SETTINGS.buildbucket.host}/prpc/buildbucket.v2.Builds/GetBuild`, 123 `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetInvocation`, 124 `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/QueryTestVariants`, 125 ]), 126 ); 127 128 // Generate a fetch request. 129 await queryAuthState(fetchInterceptor).catch((_e) => {}); 130 let cacheHit = prefetcher.respondWithPrefetched({ 131 request: new Request(...fetchInterceptor.mock.calls[0]), 132 respondWith: respondWithStub, 133 } as Partial<FetchEvent> as FetchEvent); 134 // Check whether the auth state was prefetched. 135 expect(cacheHit).toBeTruthy(); 136 let cachedRes = await respondWithStub.mock.calls[0][0]; 137 138 expect(cachedRes).toStrictEqual(authResponse); 139 expect(fetchStub.mock.calls.length).toStrictEqual(4); 140 141 // Generate a fetch request. 142 await buildsService 143 .getBuild({ 144 builder: { 145 project: 'chromium', 146 bucket: 'ci', 147 builder: 'Win7 Tests (1)', 148 }, 149 buildNumber: 116372, 150 fields: BUILD_FIELD_MASK, 151 }) 152 .catch((_e) => {}); 153 // Check whether the build was prefetched. 154 cacheHit = prefetcher.respondWithPrefetched({ 155 request: new Request(...fetchInterceptor.mock.calls[1]), 156 respondWith: respondWithStub, 157 } as Partial<FetchEvent> as FetchEvent); 158 cachedRes = await respondWithStub.mock.calls[1][0]; 159 160 expect(cacheHit).toBeTruthy(); 161 expect(cachedRes).toStrictEqual(buildResponse); 162 expect(fetchStub.mock.calls.length).toStrictEqual(4); 163 164 // Generate a fetch request. 165 await resultdb.getInvocation({ name: invName }).catch((_e) => {}); 166 // Check whether the invocation was prefetched. 167 cacheHit = prefetcher.respondWithPrefetched({ 168 request: new Request(...fetchInterceptor.mock.calls[2]), 169 respondWith: respondWithStub, 170 } as Partial<FetchEvent> as FetchEvent); 171 cachedRes = await respondWithStub.mock.calls[2][0]; 172 173 expect(cacheHit).toBeTruthy(); 174 expect(cachedRes).toStrictEqual(invResponse); 175 expect(fetchStub.mock.calls.length).toStrictEqual(4); 176 177 // Generate a fetch request. 178 await resultdb 179 .queryTestVariants({ invocations: [invName], resultLimit: RESULT_LIMIT }) 180 .catch((_e) => {}); 181 // Check whether the test variants was prefetched. 182 cacheHit = prefetcher.respondWithPrefetched({ 183 request: new Request(...fetchInterceptor.mock.calls[3]), 184 respondWith: respondWithStub, 185 } as Partial<FetchEvent> as FetchEvent); 186 cachedRes = await respondWithStub.mock.calls[3][0]; 187 188 expect(cacheHit).toBeTruthy(); 189 expect(cachedRes).toStrictEqual(testVariantsResponse); 190 expect(fetchStub.mock.calls.length).toStrictEqual(4); 191 }); 192 193 test('prefetches build page resources when visiting a short build page url', async () => { 194 const authResponse = new Response( 195 JSON.stringify({ accessToken: 'access-token', identity: 'user:user-id' }), 196 ); 197 const buildResponse = new Response(JSON.stringify({})); 198 const invResponse = new Response(JSON.stringify({})); 199 const testVariantsResponse = new Response(JSON.stringify({})); 200 201 fetchStub.mockImplementation(async (input, init) => { 202 const req = new Request(input, init); 203 switch (req.url) { 204 case self.origin + '/auth/openid/state': 205 return authResponse; 206 case `https://${SETTINGS.buildbucket.host}/prpc/buildbucket.v2.Builds/GetBuild`: 207 return buildResponse; 208 case `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetInvocation`: 209 return invResponse; 210 case `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/QueryTestVariants`: 211 return testVariantsResponse; 212 default: 213 throw new Error('unexpected request URL'); 214 } 215 }); 216 217 const invName = 'invocations/' + getInvIdFromBuildId('123456789'); 218 219 await prefetcher.prefetchResources('/ui/b/123456789'); 220 221 await jest.advanceTimersByTimeAsync(100); 222 223 const requestedUrls = fetchStub.mock.calls.map( 224 (c) => new Request(...c).url, 225 ); 226 expect(requestedUrls.length).toStrictEqual(4); 227 expect(requestedUrls).toEqual( 228 expect.arrayContaining([ 229 self.origin + '/auth/openid/state', 230 `https://${SETTINGS.buildbucket.host}/prpc/buildbucket.v2.Builds/GetBuild`, 231 `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetInvocation`, 232 `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/QueryTestVariants`, 233 ]), 234 ); 235 236 // Generate a fetch request. 237 await queryAuthState(fetchInterceptor).catch((_e) => {}); 238 // Check whether the auth state was prefetched. 239 let cacheHit = prefetcher.respondWithPrefetched({ 240 request: new Request(...fetchInterceptor.mock.calls[0]), 241 respondWith: respondWithStub, 242 } as Partial<FetchEvent> as FetchEvent); 243 let cachedRes = await respondWithStub.mock.calls[0][0]; 244 245 expect(cacheHit).toBeTruthy(); 246 expect(cachedRes).toStrictEqual(authResponse); 247 expect(fetchStub.mock.calls.length).toStrictEqual(4); 248 249 // Generate a fetch request. 250 await buildsService 251 .getBuild({ 252 id: '123456789', 253 fields: BUILD_FIELD_MASK, 254 }) 255 .catch((_e) => {}); 256 // Check whether the build was prefetched. 257 cacheHit = prefetcher.respondWithPrefetched({ 258 request: new Request(...fetchInterceptor.mock.calls[1]), 259 respondWith: respondWithStub, 260 } as Partial<FetchEvent> as FetchEvent); 261 cachedRes = await respondWithStub.mock.calls[1][0]; 262 263 expect(cacheHit).toBeTruthy(); 264 expect(cachedRes).toStrictEqual(buildResponse); 265 expect(fetchStub.mock.calls.length).toStrictEqual(4); 266 267 // Generate a fetch request. 268 await resultdb.getInvocation({ name: invName }).catch((_e) => {}); 269 // Check whether the invocation was prefetched. 270 cacheHit = prefetcher.respondWithPrefetched({ 271 request: new Request(...fetchInterceptor.mock.calls[2]), 272 respondWith: respondWithStub, 273 } as Partial<FetchEvent> as FetchEvent); 274 cachedRes = await respondWithStub.mock.calls[2][0]; 275 276 expect(cacheHit).toBeTruthy(); 277 expect(cachedRes).toStrictEqual(invResponse); 278 expect(fetchStub.mock.calls.length).toStrictEqual(4); 279 280 // Generate a fetch request. 281 await resultdb 282 .queryTestVariants({ invocations: [invName], resultLimit: RESULT_LIMIT }) 283 .catch((_e) => {}); 284 // Check whether the test variants was prefetched. 285 cacheHit = prefetcher.respondWithPrefetched({ 286 request: new Request(...fetchInterceptor.mock.calls[3]), 287 respondWith: respondWithStub, 288 } as Partial<FetchEvent> as FetchEvent); 289 cachedRes = await respondWithStub.mock.calls[3][0]; 290 291 expect(cacheHit).toBeTruthy(); 292 expect(cachedRes).toStrictEqual(testVariantsResponse); 293 expect(fetchStub.mock.calls.length).toStrictEqual(4); 294 }); 295 296 test('prefetches artifact page resources', async () => { 297 const authResponse = new Response( 298 JSON.stringify({ accessToken: 'access-token', identity: 'user:user-id' }), 299 ); 300 const artifactResponse = new Response( 301 JSON.stringify({ 302 name: 'invocations/inv-id/tests/test-id/results/result-id/artifacts/artifact-id', 303 artifactId: 'artifact-id', 304 }), 305 ); 306 307 fetchStub.mockImplementation(async (input, init) => { 308 const req = new Request(input, init); 309 switch (req.url) { 310 case self.origin + '/auth/openid/state': 311 return authResponse; 312 case `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetArtifact`: 313 return artifactResponse; 314 default: 315 throw new Error('unexpected request URL'); 316 } 317 }); 318 319 await prefetcher.prefetchResources( 320 '/ui/artifact/raw/invocations/inv-id/tests/test-id/results/result-id/artifacts/artifact-id', 321 ); 322 323 await jest.advanceTimersByTimeAsync(100); 324 325 const requestedUrls = fetchStub.mock.calls.map( 326 (c) => new Request(...c).url, 327 ); 328 expect(requestedUrls.length).toStrictEqual(2); 329 expect(requestedUrls).toEqual( 330 expect.arrayContaining([ 331 self.origin + '/auth/openid/state', 332 `https://${SETTINGS.resultdb.host}/prpc/luci.resultdb.v1.ResultDB/GetArtifact`, 333 ]), 334 ); 335 336 // Generate a fetch request. 337 await queryAuthState(fetchInterceptor).catch((_e) => {}); 338 // Check whether the auth state was prefetched. 339 let cacheHit = prefetcher.respondWithPrefetched({ 340 request: new Request(...fetchInterceptor.mock.calls[0]), 341 respondWith: respondWithStub, 342 } as Partial<FetchEvent> as FetchEvent); 343 let cachedRes = await respondWithStub.mock.calls[0][0]; 344 345 expect(cacheHit).toBeTruthy(); 346 expect(cachedRes).toStrictEqual(authResponse); 347 expect(fetchStub.mock.calls.length).toStrictEqual(2); 348 349 // Generate a fetch request. 350 await resultdb 351 .getArtifact({ 352 name: 'invocations/inv-id/tests/test-id/results/result-id/artifacts/artifact-id', 353 }) 354 .catch((_e) => {}); 355 // Check whether the artifact was prefetched. 356 cacheHit = prefetcher.respondWithPrefetched({ 357 request: new Request(...fetchInterceptor.mock.calls[1]), 358 respondWith: respondWithStub, 359 } as Partial<FetchEvent> as FetchEvent); 360 cachedRes = await respondWithStub.mock.calls[1][0]; 361 362 expect(cacheHit).toBeTruthy(); 363 expect(fetchStub.mock.calls.length).toStrictEqual(2); 364 expect(cachedRes).toStrictEqual(artifactResponse); 365 }); 366 });