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  });