From f4440364e8d51a1c0ce2ec77c9f815a9711ca359 Mon Sep 17 00:00:00 2001
From: sindrekroknes <143387557+sindrekroknes@users.noreply.github.com>
Date: Tue, 9 Jul 2024 11:43:29 +0200
Subject: [PATCH] fix(azure): getRawFile not handling 404 for Azure DevOps
 (#30066)

---
 .../azure/__snapshots__/index.spec.ts.snap    |  1 +
 lib/modules/platform/azure/index.spec.ts      | 93 ++++++++++---------
 lib/modules/platform/azure/index.ts           | 25 +++--
 3 files changed, 61 insertions(+), 58 deletions(-)

diff --git a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap
index 1475060660..bc735f9bbd 100644
--- a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap
@@ -147,6 +147,7 @@ exports[`modules/platform/azure/index getJsonFile() supports fetch from another
     undefined,
     undefined,
     undefined,
+    true,
   ],
 ]
 `;
diff --git a/lib/modules/platform/azure/index.spec.ts b/lib/modules/platform/azure/index.spec.ts
index 934cb93e8c..a30bdfce5f 100644
--- a/lib/modules/platform/azure/index.spec.ts
+++ b/lib/modules/platform/azure/index.spec.ts
@@ -1886,17 +1886,26 @@ describe('modules/platform/azure/index', () => {
     it('returns file content', async () => {
       const data = { foo: 'bar' };
       azureApi.gitApi.mockImplementationOnce(
-        () =>
-          ({
-            getItemContent: jest.fn(() =>
-              Promise.resolve(Readable.from(JSON.stringify(data))),
-            ),
-          }) as any,
+        jest.fn().mockImplementationOnce(() => ({
+          getItem: jest.fn(() =>
+            Promise.resolve({ content: JSON.stringify(data) }),
+          ),
+        })),
       );
       const res = await azure.getJsonFile('file.json');
       expect(res).toEqual(data);
     });
 
+    it('returns null when file not found', async () => {
+      azureApi.gitApi.mockImplementationOnce(
+        jest.fn().mockImplementationOnce(() => ({
+          getItem: jest.fn(() => Promise.resolve(null)),
+        })),
+      );
+      const res = await azure.getJsonFile('file.json');
+      expect(res).toBeNull();
+    });
+
     it('returns file content in json5 format', async () => {
       const json5Data = `
         {
@@ -1905,12 +1914,9 @@ describe('modules/platform/azure/index', () => {
         }
       `;
       azureApi.gitApi.mockImplementationOnce(
-        () =>
-          ({
-            getItemContent: jest.fn(() =>
-              Promise.resolve(Readable.from(json5Data)),
-            ),
-          }) as any,
+        jest.fn().mockImplementationOnce(() => ({
+          getItem: jest.fn(() => Promise.resolve({ content: json5Data })),
+        })),
       );
       const res = await azure.getJsonFile('file.json5');
       expect(res).toEqual({ foo: 'bar' });
@@ -1918,58 +1924,55 @@ describe('modules/platform/azure/index', () => {
 
     it('returns file content from branch or tag', async () => {
       const data = { foo: 'bar' };
-      azureApi.gitApi.mockImplementationOnce(
-        () =>
-          ({
-            getItemContent: jest.fn(() =>
-              Promise.resolve(Readable.from(JSON.stringify(data))),
-            ),
-          }) as any,
+      azureApi.gitApi.mockResolvedValueOnce(
+        partial<IGitApi>({
+          getItem: jest.fn(() =>
+            Promise.resolve({ content: JSON.stringify(data) }),
+          ),
+        }),
       );
       const res = await azure.getJsonFile('file.json', undefined, 'dev');
       expect(res).toEqual(data);
     });
 
     it('throws on malformed JSON', async () => {
-      azureApi.gitApi.mockImplementationOnce(
-        () =>
-          ({
-            getItemContent: jest.fn(() =>
-              Promise.resolve(Readable.from('!@#')),
-            ),
-          }) as any,
+      azureApi.gitApi.mockResolvedValueOnce(
+        partial<IGitApi>({
+          getItemContent: jest.fn(() => Promise.resolve(Readable.from('!@#'))),
+        }),
       );
       await expect(azure.getJsonFile('file.json')).rejects.toThrow();
     });
 
     it('throws on errors', async () => {
-      azureApi.gitApi.mockImplementationOnce(
-        () =>
-          ({
-            getItemContent: jest.fn(() => {
-              throw new Error('some error');
-            }),
-          }) as any,
+      azureApi.gitApi.mockResolvedValueOnce(
+        partial<IGitApi>({
+          getItemContent: jest.fn(() => {
+            throw new Error('some error');
+          }),
+        }),
       );
       await expect(azure.getJsonFile('file.json')).rejects.toThrow();
     });
 
     it('supports fetch from another repo', async () => {
       const data = { foo: 'bar' };
-      const gitApiMock = {
-        getItemContent: jest.fn(() =>
-          Promise.resolve(Readable.from(JSON.stringify(data))),
-        ),
-        getRepositories: jest.fn(() =>
-          Promise.resolve([
-            { id: '123456', name: 'bar', project: { name: 'foo' } },
-          ]),
-        ),
-      };
-      azureApi.gitApi.mockImplementationOnce(() => gitApiMock as any);
+      const getItemFn = jest
+        .fn()
+        .mockResolvedValueOnce({ content: JSON.stringify(data) });
+      azureApi.gitApi.mockResolvedValueOnce(
+        partial<IGitApi>({
+          getItem: getItemFn,
+          getRepositories: jest
+            .fn()
+            .mockResolvedValue([
+              { id: '123456', name: 'bar', project: { name: 'foo' } },
+            ]),
+        }),
+      );
       const res = await azure.getJsonFile('file.json', 'foo/bar');
       expect(res).toEqual(data);
-      expect(gitApiMock.getItemContent.mock.calls).toMatchSnapshot();
+      expect(getItemFn.mock.calls).toMatchSnapshot();
     });
 
     it('returns null', async () => {
diff --git a/lib/modules/platform/azure/index.ts b/lib/modules/platform/azure/index.ts
index f866e0dcdf..36a2313632 100644
--- a/lib/modules/platform/azure/index.ts
+++ b/lib/modules/platform/azure/index.ts
@@ -22,7 +22,6 @@ import * as git from '../../../util/git';
 import * as hostRules from '../../../util/host-rules';
 import { regEx } from '../../../util/regex';
 import { sanitize } from '../../../util/sanitize';
-import { streamToString } from '../../../util/streams';
 import { ensureTrailingSlash } from '../../../util/url';
 import type {
   BranchStatusConfig,
@@ -146,20 +145,20 @@ export async function getRawFile(
       version: branchOrTag,
     } satisfies GitVersionDescriptor;
 
-    const buf = await azureApiGit.getItemContent(
-      repoId,
-      fileName,
-      undefined,
-      undefined,
-      undefined,
-      undefined,
-      undefined,
-      undefined,
-      branchOrTag ? versionDescriptor : undefined,
+    const item = await azureApiGit.getItem(
+      repoId, // repositoryId
+      fileName, // path
+      undefined, // project
+      undefined, // scopePath
+      undefined, // recursionLevel
+      undefined, // includeContentMetadata
+      undefined, // latestProcessedChange
+      undefined, // download
+      branchOrTag ? versionDescriptor : undefined, // versionDescriptor
+      true, // includeContent
     );
 
-    const str = await streamToString(buf);
-    return str;
+    return item?.content ?? null;
   } catch (err) /* istanbul ignore next */ {
     if (
       err.message?.includes('<title>Azure DevOps Services Unavailable</title>')
-- 
GitLab