diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx
index d9afe8db5a..420fe6ecf4 100644
--- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx
+++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx
@@ -23,11 +23,9 @@ import {
ErrorPanel,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
-import { useApi } from '@backstage/core-plugin-api';
import React from 'react';
-import { azureDevOpsApiRef } from '../../api';
-import useAsync from 'react-use/lib/useAsync';
-import { getAnnotationValuesFromEntity } from '../../utils';
+
+import { useReadme } from '../../hooks';
const useStyles = makeStyles(theme => ({
readMe: {
@@ -85,16 +83,9 @@ const ReadmeCardError = ({ error }: ErrorProps) => {
export const ReadmeCard = (props: Props) => {
const classes = useStyles();
- const api = useApi(azureDevOpsApiRef);
const { entity } = useEntity();
- const { project, repo } = getAnnotationValuesFromEntity(entity);
- const { loading, error, value } = useAsync(async () => {
- if (repo) {
- return await api.getReadme({ project, repo });
- }
- return undefined;
- }, [api, project, repo, entity]);
+ const { loading, error, item: value } = useReadme(entity);
if (loading) {
return ;
diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts
index 0b745ba586..ab1a3c9998 100644
--- a/plugins/azure-devops/src/hooks/index.ts
+++ b/plugins/azure-devops/src/hooks/index.ts
@@ -20,3 +20,4 @@ export * from './usePullRequests';
export * from './useRepoBuilds';
export * from './useUserEmail';
export * from './useUserTeamIds';
+export * from './useReadme';
diff --git a/plugins/azure-devops/src/hooks/useReadme.test.tsx b/plugins/azure-devops/src/hooks/useReadme.test.tsx
new file mode 100644
index 0000000000..ffc33c136b
--- /dev/null
+++ b/plugins/azure-devops/src/hooks/useReadme.test.tsx
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { renderHook, waitFor } from '@testing-library/react';
+import { Entity } from '@backstage/catalog-model';
+import { Readme } from '@backstage/plugin-azure-devops-common';
+import { TestApiProvider } from '@backstage/test-utils';
+import { AzureDevOpsApi, azureDevOpsApiRef } from '../api';
+import { useReadme } from './useReadme';
+
+describe('useReadme', () => {
+ const azureDevOpsApiMock = {
+ getReadme: jest.fn(),
+ };
+ const azureDevOpsApi =
+ azureDevOpsApiMock as Partial as AzureDevOpsApi;
+
+ const Wrapper = (props: { children?: React.ReactNode }) => (
+
+ {props.children}
+
+ );
+
+ it('should provide a Readme', async () => {
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ namespace: 'default',
+ name: 'project-repo',
+ annotations: {
+ 'dev.azure.com/project-repo': 'projectName/repoName',
+ },
+ },
+ };
+ const readme: Readme = {
+ url: 'https://dev.azure.com/org/project/repo',
+ content: 'This is some fake README content',
+ };
+ azureDevOpsApiMock.getReadme.mockResolvedValue({
+ item: readme,
+ });
+ const { result } = renderHook(() => useReadme(entity), {
+ wrapper: Wrapper,
+ });
+
+ expect(result.current.loading).toEqual(true);
+
+ await waitFor(() => {
+ expect(result.current.item).toEqual({
+ item: readme,
+ });
+ });
+ });
+
+ it('should return throw when annotation missing', async () => {
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ namespace: 'default',
+ name: 'project-repo',
+ },
+ };
+
+ expect(() =>
+ renderHook(() => useReadme(entity), {
+ wrapper: Wrapper,
+ }),
+ ).toThrow('Value for annotation "dev.azure.com/project" was not found');
+ });
+
+ it('should return throw when annotation invalid', async () => {
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ namespace: 'default',
+ name: 'project-repo',
+ annotations: {
+ 'dev.azure.com/project-repo': 'fake',
+ },
+ },
+ };
+
+ expect(() =>
+ renderHook(() => useReadme(entity), {
+ wrapper: Wrapper,
+ }),
+ ).toThrow(
+ 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"',
+ );
+ });
+});
diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts
new file mode 100644
index 0000000000..60b2e21990
--- /dev/null
+++ b/plugins/azure-devops/src/hooks/useReadme.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Readme } from '@backstage/plugin-azure-devops-common';
+
+import { Entity } from '@backstage/catalog-model';
+import { azureDevOpsApiRef } from '../api';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { getAnnotationValuesFromEntity } from '../utils';
+
+export function useReadme(entity: Entity): {
+ item?: Readme;
+ loading: boolean;
+ error?: Error;
+} {
+ const api = useApi(azureDevOpsApiRef);
+ const { project, repo } = getAnnotationValuesFromEntity(entity);
+
+ const { value, loading, error } = useAsync(async () => {
+ return await api.getReadme({ project, repo: repo as string });
+ }, [api, project, repo]);
+
+ return {
+ item: value,
+ loading,
+ error,
+ };
+}