// (C) 2007-2019 GoodData Corporation import { catalog, ILoadCatalogRequest } from "../catalog"; import * as request from "supertest"; import * as HttpStatusCodes from "http-status-codes"; import { createEndpoint } from "../../../utils/tests"; import * as errors from "../../errors/errors"; import { ISchema } from "../../../schema/model/Schema"; const schema: ISchema = { project: { title: "My title", }, groups: [ { metrics: [ { title: "Beer", expression: "Bar", }, { title: "Amount", expression: "Foo", }, ], dateDataSets: [{ title: "Activity" }], }, { attributes: [ { title: "Customer", elements: ["John Doe", "Jane Doe"], }, { title: "Product", elements: ["Product A", "Product B"], }, ], facts: [ { title: "Amount", values: [1, 2, 3], }, ], }, { attributes: [ { title: "Country", elements: ["Motherland Росси́я", "United States of Money"], }, ], meta: { type: "csv", identifier: "my.country.dataset", }, }, ], }; const app = createEndpoint(catalog, schema); const LOAD_CATALOG_URI = "/gdc/internal/projects/mockproject/loadCatalog"; const FACT_AMOUNT = { fact: { content: { expr: [ { data: "/gdc/md/mockproject/obj/fact.amount_data", type: "col", }, ], }, meta: { category: "fact", title: "Amount", identifier: "fact.amount", uri: "/gdc/md/mockproject/obj/fact.amount", }, }, }; const METRIC_FOO = { metric: { content: { expression: "Foo", format: "#,##0.00", }, meta: { category: "metric", identifier: "amount", title: "Amount", uri: "/gdc/md/mockproject/obj/amount", }, }, }; const METRIC_BAR = { metric: { content: { expression: "Bar", format: "#,##0.00", }, meta: { category: "metric", identifier: "beer", title: "Beer", uri: "/gdc/md/mockproject/obj/beer", }, }, }; const ATTRIBUTE_CUSTOMER = { attribute: { content: { displayForms: [ { content: { expression: "[/gdc/md/mockproject/obj/123]", formOf: "/gdc/md/mockproject/obj/attr.customer", }, links: { elements: "/gdc/md/mockproject/obj/attr.customer/elements", }, meta: { category: "attributeDisplayForm", identifier: "attr.customer.df", title: "Customer", uri: "/gdc/md/mockproject/obj/attr.customer.df", }, }, ], }, meta: { category: "attribute", title: "Customer", identifier: "attr.customer", uri: "/gdc/md/mockproject/obj/attr.customer", }, }, }; const ATTRIBUTE_PRODUCT = { attribute: { content: { displayForms: [ { content: { expression: "[/gdc/md/mockproject/obj/123]", formOf: "/gdc/md/mockproject/obj/attr.product", }, links: { elements: "/gdc/md/mockproject/obj/attr.product/elements", }, meta: { category: "attributeDisplayForm", identifier: "attr.product.df", title: "Product", uri: "/gdc/md/mockproject/obj/attr.product.df", }, }, ], }, meta: { category: "attribute", title: "Product", identifier: "attr.product", uri: "/gdc/md/mockproject/obj/attr.product", }, }, }; const ATTRIBUTE_COUNTRY = { attribute: { content: { displayForms: [ { content: { expression: "[/gdc/md/mockproject/obj/123]", formOf: "/gdc/md/mockproject/obj/attr.country", }, links: { elements: "/gdc/md/mockproject/obj/attr.country/elements", }, meta: { category: "attributeDisplayForm", identifier: "attr.country.df", title: "Country", uri: "/gdc/md/mockproject/obj/attr.country.df", }, }, ], }, meta: { category: "attribute", title: "Country", identifier: "attr.country", uri: "/gdc/md/mockproject/obj/attr.country", }, }, }; it("should return metrics", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["metric"], }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_FOO, METRIC_BAR], totals: { unavailable: 0, available: 2, }, paging: { offset: 0, count: 2, }, }, }); }); }); it("should return fact", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: { types: ["fact"], }, }) .expect(HttpStatusCodes.OK) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [FACT_AMOUNT], totals: { unavailable: 0, available: 1, }, paging: { offset: 0, count: 1, }, }, }); }); }); it("should return attributes", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: { types: ["attribute"], }, }) .expect(HttpStatusCodes.OK) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [ATTRIBUTE_CUSTOMER, ATTRIBUTE_PRODUCT], totals: { unavailable: 0, available: 2, }, paging: { offset: 0, count: 2, }, }, }); }); }); it("should apply filter (search query)", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: { types: ["metric", "attribute", "fact"], filter: "amo", }, }) .expect(HttpStatusCodes.OK) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_FOO, FACT_AMOUNT], totals: { unavailable: 0, available: 2, }, paging: { offset: 0, count: 2, }, }, }); }); }); it("should return metrics", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["metric"], paging: { offset: 1, count: 1, }, }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_BAR], totals: { unavailable: 0, available: 2, }, paging: { offset: 1, count: 1, }, }, }); }); }); it("should return only available items", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["metric", "fact", "attribute"], bucketItems: ["/gdc/md/mockproject/obj/attr.customer"], }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [FACT_AMOUNT, ATTRIBUTE_CUSTOMER, ATTRIBUTE_PRODUCT], totals: { unavailable: 2, available: 3, }, paging: { offset: 0, count: 3, }, }, }); }); }); it("should return only available items if queried by date attribute", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["metric", "fact", "attribute"], bucketItems: ["/gdc/md/mockproject/obj/attr.activity.year"], }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_FOO, METRIC_BAR], totals: { unavailable: 3, available: 2, }, paging: { offset: 0, count: 2, }, }, }); }); }); it("number of unavailable items should respect the type and search filters", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { filter: "cust", types: ["attribute"], bucketItems: ["/gdc/md/mockproject/obj/attr.customer"], }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [ATTRIBUTE_CUSTOMER], totals: { unavailable: 0, available: 1, }, paging: { offset: 0, count: 1, }, }, }); }); }); it("should return only available items parsed from MAQL", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["metric", "attribute", "fact"], bucketItems: ["SELECT COUNT([/gdc/md/mockproject/obj/fact.amount])"], }, }) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [FACT_AMOUNT, ATTRIBUTE_CUSTOMER, ATTRIBUTE_PRODUCT], totals: { unavailable: 2, available: 3, }, paging: { offset: 0, count: 3, }, }, }); }); }); it("should return items from CSV dataset", () => { const catalogRequest: ILoadCatalogRequest = { catalogRequest: { types: ["attribute", "metric", "fact"], requiredDataSets: { type: "CUSTOM", customIdentifiers: ["my.country.dataset"], }, paging: { offset: 0, limit: 50, }, }, }; return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send(catalogRequest) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [ATTRIBUTE_COUNTRY], totals: { unavailable: 0, available: 1, }, paging: { offset: 0, count: 1, }, }, }); }); }); describe("validation", () => { it("should return ok if types are not sent in request", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: {}, }) .expect(200) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_FOO, FACT_AMOUNT, METRIC_BAR, ATTRIBUTE_CUSTOMER, ATTRIBUTE_PRODUCT], paging: { count: 5, offset: 0, }, totals: { available: 5, unavailable: 0, }, }, }); }); }); it("should return bad request if types are not array", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: { types: "metrics", }, }) .expect(400) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestTypes()); }); }); it("should process request normally if duplicate types are present", () => { return request(app) .post(LOAD_CATALOG_URI) .send({ catalogRequest: { types: ["metric", "metric", "metric", "metric"], }, }) .expect(200) .then(res => { expect(res.body).toEqual({ catalogResponse: { catalog: [METRIC_FOO, METRIC_BAR], totals: { unavailable: 0, available: 2, }, paging: { offset: 0, count: 2, }, }, }); }); }); }); it("should validate more than one root element", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ a: ["b"], c: ["d"], e: ["f"], }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.moreThanOneRootElement()); }); }); it("should validate invalid element key", () => { const validKey = "catalogRequest"; const invalidKey = "invalidCatalogRequestElementKey"; const invalidMessage = {}; invalidMessage[invalidKey] = {}; return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send(invalidMessage) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidElementKey(validKey, invalidKey)); }); }); it("should validate invalid catalog request structure (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: [] }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestStructureType("ARRAY")); }); }); it("should validate invalid catalog request structure (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: "" }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestStructureType("")); }); }); it("should validate invalid catalog request types", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: ["a", "b", "c"] } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestTypes()); }); }); it("should validate invalid catalog request types structure (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: "" } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestTypesStructure("")); }); }); it("should validate invalid catalog request types structure (object)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { types: {} } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestTypesStructure("HASH")); }); }); it("should validate invalid catalog request paging structure (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: "" } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestPagingStructure("")); }); }); it("should validate invalid catalog request paging structure (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: [] } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestPagingStructure("ARRAY")); }); }); it("should validate invalid catalog request paging", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { a: "b", c: "d" } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestPaging()); }); }); it("should validate invalid catalog request paging offset type (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: "a", limit: 1 } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/offset")); }); }); it("should validate invalid catalog request paging limit type (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: 1, limit: "a" } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/limit")); }); }); it("should validate invalid catalog request paging offset type (object)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: {}, limit: 1 } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/offset")); }); }); it("should validate invalid catalog request paging limit type (object)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: 1, limit: {} } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/limit")); }); }); it("should validate invalid catalog request paging offset type (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: [], limit: 1 } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/offset")); }); }); it("should validate invalid catalog request paging limit type (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { paging: { offset: 1, limit: [] } } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("paging/limit")); }); }); it("should validate invalid catalog request filter type (object)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { filter: {} } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("filter")); }); }); it("should validate invalid catalog request filter type (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { filter: [] } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestScalarType("filter")); }); }); it("should validate invalid catalog request requiredDataSets type (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { requiredDataSets: "" } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual( errors.invalidCatalogRequestScalarType("requiredDataSets/DataSetsSelector"), ); }); }); it("should validate invalid catalog request requiredDataSets type (array)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { requiredDataSets: [] } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual( errors.invalidCatalogRequestScalarType("requiredDataSets/DataSetsSelector"), ); }); }); it("should validate invalid catalog request bucketItems structure (object)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { bucketItems: {} } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestBucketItemsStructure("HASH")); }); }); it("should validate invalid catalog request bucketItems structure (string)", () => { return request(app) .post(LOAD_CATALOG_URI) .set("Content-Type", "application/json") .send({ catalogRequest: { bucketItems: "" } }) .expect(HttpStatusCodes.BAD_REQUEST) .then(res => { expect(res.body).toEqual(errors.invalidCatalogRequestBucketItemsStructure("")); }); });