Skip to content

Commit

Permalink
feat: backend support for atlas publication lists (#411) (#413)
Browse files Browse the repository at this point in the history
* feat: backend support for atlas publication lists (#411)

* fix: allow undefined `dois` in schema test (#411)

* test: update tests to include atlas publications (#411)
  • Loading branch information
hunterckx authored Aug 14, 2024
1 parent 95ee0fd commit 09c3293
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 81 deletions.
85 changes: 56 additions & 29 deletions __tests__/api-atlases-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@ import {
ATLAS_STATUS,
HCAAtlasTrackerAtlas,
HCAAtlasTrackerDBAtlas,
PublicationInfo,
} from "../app/apis/catalog/hca-atlas-tracker/common/entities";
import { NewAtlasData } from "../app/apis/catalog/hca-atlas-tracker/common/schema";
import { METHOD } from "../app/common/entities";
import { endPgPool, query } from "../app/services/database";
import createHandler from "../pages/api/atlases/create";
import {
DOI_NONEXISTENT,
DOI_PREPRINT_NO_JOURNAL,
PUBLICATION_PREPRINT_NO_JOURNAL,
STAKEHOLDER_ANALOGOUS_ROLES,
USER_CONTENT_ADMIN,
USER_UNREGISTERED,
} from "../testing/constants";
import { resetDatabase } from "../testing/db-utils";
import { TestUser } from "../testing/entities";
import { testApiRole, withConsoleErrorHiding } from "../testing/utils";
import {
expectDbAtlasToMatchApi,
testApiRole,
withConsoleErrorHiding,
} from "../testing/utils";

jest.mock("../app/services/user-profile");
jest.mock("../app/utils/crossref/crossref-api");
Expand All @@ -28,6 +36,7 @@ const NEW_ATLAS_DATA: NewAtlasData = {
cellxgeneAtlasCollection: "7a223dd3-a422-4f4b-a437-90b9a3b00ba8",
codeLinks: [{ url: "https://example.com/new-atlas-foo" }],
description: "foo bar baz baz foo bar",
dois: [DOI_PREPRINT_NO_JOURNAL],
highlights: "bar foo baz baz baz foo",
integrationLead: [],
network: "eye",
Expand Down Expand Up @@ -90,6 +99,15 @@ const NEW_ATLAS_WITHOUT_DESCRIPTION: NewAtlasData = {
wave: "2",
};

const NEW_ATLAS_WITH_NONEXISTENT_PUBLICATION: NewAtlasData = {
dois: [DOI_NONEXISTENT],
integrationLead: [],
network: "lung",
shortName: "test6",
version: "2.3",
wave: "1",
};

beforeAll(async () => {
await resetDatabase();
});
Expand Down Expand Up @@ -308,28 +326,52 @@ describe("/api/atlases/create", () => {
).toEqual(400);
});

it("returns error 400 when dois are non-unique", async () => {
expect(
(
await doCreateTest(
USER_CONTENT_ADMIN,
{
...NEW_ATLAS_DATA,
dois: ["10.123/foo", "https://doi.org/10.123/foo"],
},
true
)
)._getStatusCode()
).toEqual(400);
});

it("creates and returns atlas entry with no integration leads", async () => {
await testSuccessfulCreate(NEW_ATLAS_DATA);
await testSuccessfulCreate(NEW_ATLAS_DATA, [
PUBLICATION_PREPRINT_NO_JOURNAL,
]);
});

it("creates and returns atlas entry with specified integration lead", async () => {
await testSuccessfulCreate(NEW_ATLAS_WITH_IL_DATA);
await testSuccessfulCreate(NEW_ATLAS_WITH_IL_DATA, []);
});

it("creates and returns atlas entry with multiple integration leads", async () => {
await testSuccessfulCreate(NEW_ATLAS_WITH_MULTIPLE_ILS);
await testSuccessfulCreate(NEW_ATLAS_WITH_MULTIPLE_ILS, []);
});

it("creates and returns atlas entry with target completion", async () => {
await testSuccessfulCreate(NEW_ATLAS_WITH_TARGET_COMPLETION);
await testSuccessfulCreate(NEW_ATLAS_WITH_TARGET_COMPLETION, []);
});

it("creates and returns atlas entry without description", async () => {
await testSuccessfulCreate(NEW_ATLAS_WITHOUT_DESCRIPTION);
await testSuccessfulCreate(NEW_ATLAS_WITHOUT_DESCRIPTION, []);
});

it("creates and returns atlas entry with nonexistent publication", async () => {
await testSuccessfulCreate(NEW_ATLAS_WITH_NONEXISTENT_PUBLICATION, [null]);
});
});

async function testSuccessfulCreate(atlasData: NewAtlasData): Promise<void> {
async function testSuccessfulCreate(
atlasData: NewAtlasData,
expectedPublicationsInfo: (PublicationInfo | null)[]
): Promise<void> {
const res = await doCreateTest(USER_CONTENT_ADMIN, atlasData);
expect(res._getStatusCode()).toEqual(201);
const newAtlas: HCAAtlasTrackerAtlas = res._getJSONData();
Expand All @@ -353,33 +395,18 @@ async function testSuccessfulCreate(atlasData: NewAtlasData): Promise<void> {
atlasData.integrationLead
);
expect(newAtlasFromDb.overview.network).toEqual(atlasData.network);
expect(newAtlasFromDb.overview.publications.map((p) => p.doi)).toEqual(
atlasData.dois ?? []
);
expect(
newAtlasFromDb.overview.publications.map((p) => p.publication)
).toEqual(expectedPublicationsInfo);
expect(newAtlasFromDb.overview.shortName).toEqual(atlasData.shortName);
expect(newAtlasFromDb.overview.version).toEqual(atlasData.version);
expect(newAtlasFromDb.overview.wave).toEqual(atlasData.wave);
expect(newAtlasFromDb.overview.taskCount).toEqual(0);
expect(newAtlasFromDb.overview.completedTaskCount).toEqual(0);
expectAtlasPropertiesToMatch(newAtlasFromDb, newAtlas);
}

function expectAtlasPropertiesToMatch(
dbAtlas: HCAAtlasTrackerDBAtlas,
apiAtlas: HCAAtlasTrackerAtlas
): void {
expect(dbAtlas.overview.network).toEqual(apiAtlas.bioNetwork);
expect(dbAtlas.overview.completedTaskCount).toEqual(
apiAtlas.completedTaskCount
);
expect(dbAtlas.id).toEqual(apiAtlas.id);
expect(dbAtlas.overview.integrationLead).toEqual(apiAtlas.integrationLead);
expect(dbAtlas.overview.shortName).toEqual(apiAtlas.shortName);
expect(dbAtlas.source_studies).toHaveLength(apiAtlas.sourceStudyCount);
expect(dbAtlas.status).toEqual(apiAtlas.status);
expect(dbAtlas.target_completion?.toISOString() ?? null).toEqual(
apiAtlas.targetCompletion
);
expect(dbAtlas.overview.taskCount).toEqual(apiAtlas.taskCount);
expect(dbAtlas.overview.version).toEqual(apiAtlas.version);
expect(dbAtlas.overview.wave).toEqual(apiAtlas.wave);
expectDbAtlasToMatchApi(newAtlasFromDb, newAtlas);
}

async function doCreateTest(
Expand Down
77 changes: 38 additions & 39 deletions __tests__/api-atlases-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import httpMocks from "node-mocks-http";
import {
HCAAtlasTrackerAtlas,
HCAAtlasTrackerDBAtlas,
PublicationInfo,
} from "../app/apis/catalog/hca-atlas-tracker/common/entities";
import { AtlasEditData } from "../app/apis/catalog/hca-atlas-tracker/common/schema";
import { METHOD } from "../app/common/entities";
Expand All @@ -13,6 +14,10 @@ import {
ATLAS_PUBLIC,
ATLAS_WITH_IL,
ATLAS_WITH_MISC_SOURCE_STUDIES,
DOI_JOURNAL_WITH_PREPRINT_COUNTERPART,
DOI_PREPRINT_WITH_JOURNAL_COUNTERPART,
PUBLICATION_JOURNAL_WITH_PREPRINT_COUNTERPART,
PUBLICATION_PREPRINT_WITH_JOURNAL_COUNTERPART,
STAKEHOLDER_ANALOGOUS_ROLES,
USER_CONTENT_ADMIN,
USER_UNREGISTERED,
Expand All @@ -21,6 +26,7 @@ import { resetDatabase } from "../testing/db-utils";
import { TestAtlas, TestUser } from "../testing/entities";
import {
expectApiAtlasToMatchTest,
expectDbAtlasToMatchApi,
makeTestAtlasOverview,
testApiRole,
withConsoleErrorHiding,
Expand Down Expand Up @@ -83,14 +89,18 @@ const ATLAS_DRAFT_EDIT: AtlasEditData = {
};

const ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION_OR_CELLXGENE: AtlasEditData = {
codeLinks: ATLAS_DRAFT.codeLinks,
description: ATLAS_DRAFT.description,
highlights: ATLAS_DRAFT.highlights,
integrationLead: ATLAS_DRAFT.integrationLead,
network: ATLAS_DRAFT.network,
shortName: ATLAS_DRAFT.shortName,
version: ATLAS_DRAFT.version,
wave: ATLAS_DRAFT.wave,
codeLinks: ATLAS_PUBLIC.codeLinks,
description: ATLAS_PUBLIC.description,
dois: [
DOI_JOURNAL_WITH_PREPRINT_COUNTERPART,
DOI_PREPRINT_WITH_JOURNAL_COUNTERPART,
],
highlights: ATLAS_PUBLIC.highlights,
integrationLead: ATLAS_PUBLIC.integrationLead,
network: ATLAS_PUBLIC.network,
shortName: ATLAS_PUBLIC.shortName,
version: ATLAS_PUBLIC.version,
wave: ATLAS_PUBLIC.wave,
};

const ATLAS_WITH_MISC_SOURCE_STUDIES_EDIT: AtlasEditData = {
Expand Down Expand Up @@ -381,31 +391,36 @@ describe(TEST_ROUTE, () => {
});

it("PUT updates and returns atlas entry", async () => {
await testSuccessfulEdit(ATLAS_PUBLIC, ATLAS_PUBLIC_EDIT, 0);
await testSuccessfulEdit(ATLAS_PUBLIC, ATLAS_PUBLIC_EDIT, 0, []);
});

it("PUT updates and returns atlas entry with integration lead set to empty array", async () => {
await testSuccessfulEdit(ATLAS_WITH_IL, ATLAS_WITH_IL_EDIT, 0);
await testSuccessfulEdit(ATLAS_WITH_IL, ATLAS_WITH_IL_EDIT, 0, []);
});

it("PUT updates and returns atlas entry with multiple integration leads", async () => {
await testSuccessfulEdit(ATLAS_DRAFT, ATLAS_DRAFT_EDIT, 2);
await testSuccessfulEdit(ATLAS_DRAFT, ATLAS_DRAFT_EDIT, 2, []);
});

it("PUT updates and returns atlas entry with target completion and CELLxGENE collection removed", async () => {
const updatedAtlas = await testSuccessfulEdit(
ATLAS_PUBLIC,
ATLAS_PUBLIC_EDIT_NO_TARGET_COMPLETION_OR_CELLXGENE,
0
0,
[
PUBLICATION_JOURNAL_WITH_PREPRINT_COUNTERPART,
PUBLICATION_PREPRINT_WITH_JOURNAL_COUNTERPART,
]
);
expect(updatedAtlas.target_completion).toBeNull();
});

it("PUT updates and returns atlas entry with description, code links, and highlights removed", async () => {
it("PUT updates and returns atlas entry with description, code links, highlights, and publications removed", async () => {
const updatedAtlas = await testSuccessfulEdit(
ATLAS_WITH_MISC_SOURCE_STUDIES,
ATLAS_WITH_MISC_SOURCE_STUDIES_EDIT,
1
1,
[]
);
expect(updatedAtlas.overview.description).toEqual("");
});
Expand All @@ -414,7 +429,8 @@ describe(TEST_ROUTE, () => {
async function testSuccessfulEdit(
testAtlas: TestAtlas,
editData: AtlasEditData,
expectedComponentAtlasCount: number
expectedComponentAtlasCount: number,
expectedPublicationsInfo: PublicationInfo[]
): Promise<HCAAtlasTrackerDBAtlas> {
const res = await doAtlasRequest(
testAtlas.id,
Expand All @@ -439,6 +455,12 @@ async function testSuccessfulEdit(
);
expect(updatedOverview.codeLinks).toEqual(editData.codeLinks ?? []);
expect(updatedOverview.description).toEqual(editData.description ?? "");
expect(updatedOverview.publications.map((p) => p.doi)).toEqual(
editData.dois ?? []
);
expect(updatedOverview.publications.map((p) => p.publication)).toEqual(
expectedPublicationsInfo
);
expect(updatedOverview.highlights).toEqual(editData.highlights ?? "");
expect(updatedOverview.integrationLead).toEqual(editData.integrationLead);
expect(updatedOverview.network).toEqual(editData.network);
Expand All @@ -450,7 +472,7 @@ async function testSuccessfulEdit(
editData.targetCompletion ?? null
);

expectAtlasPropertiesToMatch(
expectDbAtlasToMatchApi(
updatedAtlasFromDb,
updatedAtlas,
expectedComponentAtlasCount
Expand All @@ -465,29 +487,6 @@ async function testSuccessfulEdit(
return updatedAtlasFromDb;
}

function expectAtlasPropertiesToMatch(
dbAtlas: HCAAtlasTrackerDBAtlas,
apiAtlas: HCAAtlasTrackerAtlas,
expectedComponentAtlasCount: number
): void {
expect(dbAtlas.overview.network).toEqual(apiAtlas.bioNetwork);
expect(dbAtlas.overview.completedTaskCount).toEqual(
apiAtlas.completedTaskCount
);
expect(dbAtlas.id).toEqual(apiAtlas.id);
expect(dbAtlas.overview.integrationLead).toEqual(apiAtlas.integrationLead);
expect(dbAtlas.overview.shortName).toEqual(apiAtlas.shortName);
expect(dbAtlas.source_studies).toHaveLength(apiAtlas.sourceStudyCount);
expect(dbAtlas.status).toEqual(apiAtlas.status);
expect(dbAtlas.target_completion?.toISOString() ?? null).toEqual(
apiAtlas.targetCompletion
);
expect(dbAtlas.overview.taskCount).toEqual(apiAtlas.taskCount);
expect(dbAtlas.overview.version).toEqual(apiAtlas.version);
expect(dbAtlas.overview.wave).toEqual(apiAtlas.wave);
expect(apiAtlas.componentAtlasCount).toEqual(expectedComponentAtlasCount);
}

async function doAtlasRequest(
atlasId: string,
user?: TestUser,
Expand Down
14 changes: 8 additions & 6 deletions app/apis/catalog/hca-atlas-tracker/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export interface HCAAtlasTrackerListAtlas {
integrationLeadEmail: IntegrationLead["email"][];
integrationLeadName: IntegrationLead["name"][];
name: string;
publicationDoi: string;
publicationPubString: string;
publications: DoiPublicationInfo[];
shortName: string;
sourceStudyCount: number;
status: ATLAS_STATUS;
Expand All @@ -38,10 +37,7 @@ export interface HCAAtlasTrackerAtlas {
highlights: string;
id: string;
integrationLead: IntegrationLead[];
publication: {
doi: string;
pubString: string;
};
publications: DoiPublicationInfo[];
shortName: string;
sourceStudyCount: number;
status: ATLAS_STATUS;
Expand Down Expand Up @@ -230,6 +226,7 @@ export interface HCAAtlasTrackerDBAtlasOverview {
highlights: string;
integrationLead: IntegrationLead[];
network: NetworkKey;
publications: DoiPublicationInfo[];
shortName: string;
taskCount: number;
version: string;
Expand Down Expand Up @@ -439,6 +436,11 @@ export type NetworkKey = (typeof NETWORK_KEYS)[number];

export type Wave = (typeof WAVES)[number];

export interface DoiPublicationInfo {
doi: string;
publication: PublicationInfo | null;
}

export interface PublicationInfo {
authors: Author[];
hasPreprintDoi: string | null;
Expand Down
10 changes: 9 additions & 1 deletion app/apis/catalog/hca-atlas-tracker/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Schema,
string,
} from "yup";
import { isDoi } from "../../../../utils/doi";
import { isDoi, normalizeDoi } from "../../../../utils/doi";
import { NETWORK_KEYS, WAVES } from "./constants";
import { ROLE } from "./entities";

Expand All @@ -24,6 +24,14 @@ export const newAtlasSchema = object({
}).required()
),
description: string().max(10000),
dois: array()
.of(string().required())
.test("dois-unique", "DOIs must be unique", (value) => {
return (
!Array.isArray(value) ||
new Set(value.map(normalizeDoi)).size === value.length
);
}),
highlights: string().max(10000),
integrationLead: array()
.of(
Expand Down
8 changes: 2 additions & 6 deletions app/apis/catalog/hca-atlas-tracker/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ export function atlasInputMapper(
integrationLeadEmail: apiAtlas.integrationLead.map(({ email }) => email),
integrationLeadName: apiAtlas.integrationLead.map(({ name }) => name),
name: getAtlasName(apiAtlas),
publicationDoi: apiAtlas.publication.doi,
publicationPubString: apiAtlas.publication.pubString,
publications: apiAtlas.publications,
shortName: apiAtlas.shortName,
sourceStudyCount: apiAtlas.sourceStudyCount,
status: apiAtlas.status,
Expand All @@ -79,10 +78,7 @@ export function dbAtlasToApiAtlas(
highlights: dbAtlas.overview.highlights,
id: dbAtlas.id,
integrationLead: dbAtlas.overview.integrationLead,
publication: {
doi: "",
pubString: "",
},
publications: dbAtlas.overview.publications,
shortName: dbAtlas.overview.shortName,
sourceStudyCount: dbAtlas.source_studies.length,
status: dbAtlas.status,
Expand Down
Loading

0 comments on commit 09c3293

Please sign in to comment.