diff --git a/tests/resolvers/flows.spec.ts b/tests/resolvers/flows.spec.ts new file mode 100644 index 00000000..39fc64e7 --- /dev/null +++ b/tests/resolvers/flows.spec.ts @@ -0,0 +1,450 @@ +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; +import { type GraphQLResponse } from 'apollo-server-types'; +import type { + Flow, + FlowSearchResult, +} from '../../src/domain-services/flows/graphql/types'; +import ContextProvider from '../testContext'; +const defaultPageSize = 10; + +const defaultSortField = '"flow.updatedAt"'; +const defaultSortOrder = '"DESC"'; + +type SearchFlowGQLResponse = { + searchFlows: FlowSearchResult; +}; + +function buildSimpleQuery( + limit: number | null, + sortField: string, + sortOrder: string, + pending: boolean = false +) { + const query = `query { + searchFlows( + limit: ${limit} + sortField: ${sortField} + sortOrder: ${sortOrder} + pending: ${pending} + flowFilters: { activeStatus: ${!pending} } + ) { + total + flows { + id + versionID + updatedAt + amountUSD + activeStatus + } + + prevPageCursor + + hasNextPage + + nextPageCursor + + hasPreviousPage + + pageSize + } + } + `; + + return query; +} + +function buildFullQuery( + limit: number, + sortField: string, + sortOrder: string, + pending: boolean = false +) { + const fullQuery = `query { + searchFlows( + limit: ${limit} + sortField: ${sortField} + sortOrder: ${sortOrder} + pending: ${pending} + flowFilters: { activeStatus: ${!pending} } + ) { + total + flows { + id + updatedAt + amountUSD + versionID + activeStatus + restricted + exchangeRate + flowDate + newMoney + decisionDate + categories { + id + name + group + createdAt + updatedAt + description + parentID + code + includeTotals + categoryRef { + objectID + versionID + objectType + categoryID + updatedAt + } + } + + organizations { + id + name + direction + abbreviation + } + + destinationOrganizations { + id + name + direction + abbreviation + } + + sourceOrganizations { + id + name + direction + abbreviation + } + + plans { + id + name + direction + } + + usageYears { + year + direction + } + childIDs + parentIDs + origAmount + origCurrency + locations { + id + name + direction + } + externalReferences { + systemID + flowID + externalRecordID + externalRecordDate + versionID + createdAt + updatedAt + } + reportDetails { + id + flowID + versionID + contactInfo + refCode + organizationID + channel + source + date + verified + updatedAt + createdAt + sourceID + } + parkedParentSource { + orgName + organization + } + } + + prevPageCursor + + hasNextPage + + nextPageCursor + + hasPreviousPage + + pageSize + } + } + `; + + return fullQuery; +} + +describe('Query should return Flow search', () => { + beforeAll(async () => { + const models = ContextProvider.Instance.models; + + const activeFlowsProt = []; + const pendingFlowsProt = []; + + // Create 20 active and pending flows + for (let i = 0; i < 20; i++) { + const flow = { + amountUSD: 10_000, + updatedAt: new Date(), + flowDate: new Date(), + origCurrency: 'USD', + origAmount: 10_000, + }; + + activeFlowsProt.push({ + ...flow, + activeStatus: true, + }); + + pendingFlowsProt.push({ + ...flow, + activeStatus: false, + }); + } + const activeFlows = await models.flow.createMany(activeFlowsProt); + const pendingFlows = await models.flow.createMany(pendingFlowsProt); + + // Create category group + const categoryGroup = { + name: 'Flow Status', + type: 'flowStatus' as const, + }; + + await models.categoryGroup.create(categoryGroup); + + // Create categories + const categoriesProt = [ + { + id: createBrandedValue(136), + name: 'Not Pending', + group: 'flowStatus' as const, + code: 'not-pending', + }, + { + id: createBrandedValue(45), + name: 'Pending', + group: 'flowStatus' as const, + code: 'pending', + }, + ]; + + await models.category.createMany(categoriesProt); + + // Asign categories to flows + const activeFlowRelationCategory = activeFlows.map((flow) => { + return { + objectID: flow.id, + objectType: 'flow' as 'plan', + categoryID: createBrandedValue(136), + }; + }); + + const pendingFlowRelationCategory = pendingFlows.map((flow) => { + return { + objectID: flow.id, + objectType: 'flow' as 'plan', + categoryID: createBrandedValue(45), + }; + }); + + await models.categoryRef.createMany(activeFlowRelationCategory); + await models.categoryRef.createMany(pendingFlowRelationCategory); + }); + + afterAll(async () => { + const connection = ContextProvider.Instance.conn; + await connection.table('flow').del(); + await connection.table('category').del(); + await connection.table('categoryRef').del(); + await connection.table('categoryGroup').del(); + }); + + test('All data should be returned (full query) [pending = false]', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildFullQuery( + defaultPageSize, + defaultSortField, + defaultSortOrder, + false + ), + }); + + validateSearchFlowResponse(response); + + const data = response.data as SearchFlowGQLResponse; + + validateSearchFlowResponseData(data); + + const searchFlowsResponse: FlowSearchResult = data.searchFlows; + const flows = searchFlowsResponse.flows; + + validateFlowResponseFullQuery(flows); + }); + + test('All data should be returned (simpleQuery) [pending = false]', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildSimpleQuery( + defaultPageSize, + defaultSortField, + defaultSortOrder, + false + ), + }); + + validateSearchFlowResponse(response); + + const data = response.data as SearchFlowGQLResponse; + + validateSearchFlowResponseData(data); + + const searchFlowsResponse: FlowSearchResult = data.searchFlows; + const flows = searchFlowsResponse.flows; + + validateFlowResponseSimpleQuery(flows); + }); + + test('All data should be returned (full query) [pending = true]', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildFullQuery( + defaultPageSize, + defaultSortField, + defaultSortOrder, + true + ), + }); + + validateSearchFlowResponse(response); + + const data = response.data as SearchFlowGQLResponse; + + validateSearchFlowResponseData(data); + + const searchFlowsResponse: FlowSearchResult = data.searchFlows; + const flows = searchFlowsResponse.flows; + validateFlowResponseFullQuery(flows); + }); + + test('All data should be returned (simpleQuery) [pending = true]', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildSimpleQuery( + defaultPageSize, + defaultSortField, + defaultSortOrder, + true + ), + }); + + validateSearchFlowResponse(response); + + const data = response.data as SearchFlowGQLResponse; + + validateSearchFlowResponseData(data); + + const searchFlowsResponse: FlowSearchResult = data.searchFlows; + const flows = searchFlowsResponse.flows; + validateFlowResponseSimpleQuery(flows); + }); + + function validateSearchFlowResponse(response: GraphQLResponse) { + expect(response).toBeDefined(); + expect(response.errors).toBeUndefined(); + expect(response.data).toBeDefined(); + } + + function validateSearchFlowResponseData(data: SearchFlowGQLResponse) { + expect(data.searchFlows).toBeDefined(); + + const searchFlowsResponse: FlowSearchResult = data.searchFlows; + expect(searchFlowsResponse.pageSize).toBe(defaultPageSize); + expect(searchFlowsResponse.hasPreviousPage).toBeDefined(); + expect(searchFlowsResponse.hasNextPage).toBeDefined(); + expect(searchFlowsResponse.nextPageCursor).toBeDefined(); + expect(searchFlowsResponse.prevPageCursor).toBeDefined(); + expect(searchFlowsResponse.total).toBeDefined(); + expect(searchFlowsResponse.flows).toBeDefined(); + } + + function validateFlowResponseFullQuery(flows: Flow[]) { + expect(flows.length).toBeLessThanOrEqual(defaultPageSize); + expect(flows.length).toBeGreaterThan(0); + + // We can get at least the first + const flow = flows[0]; + + expect(flow.id).toBeDefined(); + expect(flow.updatedAt).toBeDefined(); + expect(flow.amountUSD).toBeDefined(); + expect(flow.categories).toBeDefined(); + expect(flow.categories.length).toBeGreaterThan(0); + expect(flow.organizations).toBeDefined(); + expect(flow.locations).toBeDefined(); + expect(flow.plans).toBeDefined(); + expect(flow.usageYears).toBeDefined(); + } + + function validateFlowResponseSimpleQuery(flows: Flow[]) { + expect(flows.length).toBeLessThanOrEqual(defaultPageSize); + expect(flows.length).toBeGreaterThan(0); + // We can get at least the first + const flow = flows[0]; + + expect(flow.id).toBeDefined(); + expect(flow.updatedAt).toBeDefined(); + expect(flow.amountUSD).toBeDefined(); + + expect(flow.categories).toBeUndefined(); + expect(flow.organizations).toBeUndefined(); + expect(flow.locations).toBeUndefined(); + expect(flow.plans).toBeUndefined(); + expect(flow.usageYears).toBeUndefined(); + } +}); + +describe('GraphQL does not return data but error', () => { + test('Should return error when invalid sort field', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildSimpleQuery(defaultPageSize, 'invalid', defaultSortOrder), + }); + + validateGraphQLResponseError(response); + }); + + test('Should return error when invalid sort order', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildSimpleQuery(defaultPageSize, defaultSortField, 'invalid'), + }); + + validateGraphQLResponseError(response); + }); + + test('Should return error when no limit is provided', async () => { + const response = + await ContextProvider.Instance.apolloTestServer.executeOperation({ + query: buildSimpleQuery(null, defaultSortField, defaultSortOrder), + }); + + validateGraphQLResponseError(response); + }); + + function validateGraphQLResponseError(response: GraphQLResponse) { + expect(response).toBeDefined(); + expect(response.errors).toBeDefined(); + expect(response.data).toBeUndefined(); + } +}); diff --git a/tests/resolvers/software-info.spec.ts b/tests/resolvers/software-info.spec.ts index 004c7586..2b75336e 100644 --- a/tests/resolvers/software-info.spec.ts +++ b/tests/resolvers/software-info.spec.ts @@ -39,11 +39,11 @@ const testSoftwareInfo = }; describe('Query should return Software info', () => { - it('All data should be returned', testSoftwareInfo(true, true, true)); + test('All data should be returned', testSoftwareInfo(true, true, true)); - it('Only version should be returned', testSoftwareInfo(true, false, false)); + test('Only version should be returned', testSoftwareInfo(true, false, false)); - it('Only title should be returned', testSoftwareInfo(false, true, false)); + test('Only title should be returned', testSoftwareInfo(false, true, false)); - it('Only status should be returned', testSoftwareInfo(false, false, true)); + test('Only status should be returned', testSoftwareInfo(false, false, true)); }); diff --git a/tests/services/flow-search-service.spec.ts b/tests/services/flow-search-service.spec.ts new file mode 100644 index 00000000..59da8788 --- /dev/null +++ b/tests/services/flow-search-service.spec.ts @@ -0,0 +1,29 @@ +import { SearchFlowsFilters } from '../../src/domain-services/flows/graphql/args'; +import { prepareFlowConditions } from '../../src/domain-services/flows/strategy/impl/utils'; + +describe('FlowSearchService', () => { + describe('PrepareFlowConditions', () => { + test('should prepare flow conditions with all filters set to undefined', () => { + const flowFilters = new SearchFlowsFilters(); + + const result = prepareFlowConditions(flowFilters); + + expect(result).toEqual({}); + }); + + test('should prepare flow conditions with some filters having falsy values', () => { + const flowFilters = new SearchFlowsFilters(); + flowFilters.id = []; + flowFilters.activeStatus = false; + flowFilters.amountUSD = 0; + + const result = prepareFlowConditions(flowFilters); + + expect(result).toEqual({ + id: [], + activeStatus: false, + amountUSD: 0, + }); + }); + }); +}); diff --git a/tests/services/flow-service.spec.ts b/tests/services/flow-service.spec.ts new file mode 100644 index 00000000..4f3638ad --- /dev/null +++ b/tests/services/flow-service.spec.ts @@ -0,0 +1,167 @@ +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; +import { type EntityDirection } from '../../src/domain-services/base-types'; +import { FlowObjectService } from '../../src/domain-services/flow-object/flow-object-service'; +import { type FlowObjectType } from '../../src/domain-services/flow-object/model'; +import { FlowService } from '../../src/domain-services/flows/flow-service'; +import { type FlowOrderByWithSubEntity } from '../../src/domain-services/flows/model'; +import { buildOrderBy } from '../../src/domain-services/flows/strategy/impl/utils'; +import ContextProvider from '../testContext'; + +const context = ContextProvider.Instance; + +describe('Test flow service', () => { + const externalReferences = [ + { + systemID: 'CERF' as const, + flowID: createBrandedValue(1), + versionID: 1, + externalRecordID: '-1234', + externalRecordDate: new Date(), + }, + { + systemID: 'EDRIS' as const, + flowID: createBrandedValue(3), + versionID: 1, + externalRecordID: '829634', + externalRecordDate: new Date(), + }, + { + systemID: 'OCT' as const, + flowID: createBrandedValue(2), + versionID: 2, + externalRecordID: '1234', + externalRecordDate: new Date(), + }, + ]; + + const organizations = [ + { name: 'AAAA', abbreviation: 'A' }, + { name: 'CCCC', abbreviation: 'C' }, + { name: 'ZZZZ', abbreviation: 'Z' }, + ]; + const flowObjectsOrganizations = [ + { + flowID: createBrandedValue(1), + objectID: createBrandedValue(1), + versionID: 1, + objectType: 'organization' as FlowObjectType, + refDirection: 'source' as EntityDirection, + }, + { + flowID: createBrandedValue(1), + objectID: createBrandedValue(2), + versionID: 1, + objectType: 'organization' as FlowObjectType, + refDirection: 'destination' as EntityDirection, + }, + { + flowID: createBrandedValue(2), + objectID: createBrandedValue(2), + versionID: 1, + objectType: 'organization' as FlowObjectType, + refDirection: 'source' as EntityDirection, + }, + { + flowID: createBrandedValue(2), + objectID: createBrandedValue(3), + versionID: 1, + objectType: 'organization' as FlowObjectType, + refDirection: 'destination' as EntityDirection, + }, + ]; + beforeAll(async () => { + // Create externalReferences + await context.models.externalReference.createMany(externalReferences); + + // Create organizations + const createdOrganization = + await context.models.organization.createMany(organizations); + + // Update flowObjects with organization IDs + flowObjectsOrganizations[0].objectID = createdOrganization[0].id; + flowObjectsOrganizations[1].objectID = createdOrganization[1].id; + flowObjectsOrganizations[2].objectID = createdOrganization[1].id; + flowObjectsOrganizations[3].objectID = createdOrganization[2].id; + + // Create flowObjects + await context.models.flowObject.createMany(flowObjectsOrganizations); + }); + + afterAll(async () => { + // Delete externalReference + await context.conn.table('externalReference').del(); + }); + describe('Test getFlowIDsFromEntity', () => { + const flowService = new FlowService(new FlowObjectService()); + + it("Case 1.1: if entity is 'externalReference' and order 'asc'", async () => { + const orderBy: FlowOrderByWithSubEntity = buildOrderBy( + 'externalReference.systemID', + 'asc' + ); + + const result = await flowService.getFlowIDsFromEntity( + context.models, + orderBy + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(3); + // Since order is asc, the first element should be 'CERF' + expect(result[0]).toEqual(externalReferences[0].flowID); + }); + + it("Case 1.2: if entity is 'externalReference' and order 'desc'", async () => { + const orderBy: FlowOrderByWithSubEntity = buildOrderBy( + 'externalReference.systemID', + 'desc' + ); + + const result = await flowService.getFlowIDsFromEntity( + context.models, + orderBy + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(3); + // Since order is desc, the first element should be 'OCT' + expect(result[0]).toEqual(externalReferences[3].flowID); + }); + + it("Case 2.1: if entity is a flowObject 'objectType' and order 'asc'", async () => { + const orderBy: FlowOrderByWithSubEntity = buildOrderBy( + 'organization.source.name', + 'asc' + ); + + const result = await flowService.getFlowIDsFromEntity( + context.models, + orderBy + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(4); + + // Since order is asc, the first element should be 'AAAA' + expect(result[0]).toEqual(flowObjectsOrganizations[0].flowID); + }); + + it("Case 2.2: if entity is a flowObject 'objectType' and order 'desc'", async () => { + const orderBy: FlowOrderByWithSubEntity = buildOrderBy( + 'organization.source.name', + 'desc' + ); + + const result = await flowService.getFlowIDsFromEntity( + context.models, + orderBy + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(4); + + // Since order is desc, the first element should be 'ZZZZ' + expect(result[0]).toEqual(flowObjectsOrganizations[4].flowID); + }); + }); +}); diff --git a/tests/utils/connection.ts b/tests/utils/connection.ts index ff67e7ca..47cce237 100644 --- a/tests/utils/connection.ts +++ b/tests/utils/connection.ts @@ -25,7 +25,7 @@ export async function createDbConnection(connection: t.TypeOf) { return knex; } catch (error) { - console.log(error); + console.error(error); throw new Error( 'Unable to connect to Postgres via Knex. Ensure a valid connection.' );