Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KMS-532: Create CSV output for schemes #17

Merged
merged 14 commits into from
Feb 28, 2025
49 changes: 48 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
"@vitejs/plugin-react": "^4.1.0",
"@xmldom/xmldom": "^0.8.10",
"aws-cdk-lib": "^2.177.0",
"csv": "^6.3.11",
"date-fns": "^4.1.0",
"esbuild": "^0.19.5",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-testing-library": "^6.2.2",
"fast-xml-parser": "^4.5.1",
"html-escaper": "^3.0.3",
"lodash": "^4.17.21",
"node-http-proxy": "^0.2.4",
"postcss-scss": "^4.0.9",
"prop-types": "^15.8.1",
Expand Down
104 changes: 104 additions & 0 deletions serverless/src/getConcepts/__tests__/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
} from 'vitest'

import { getConcepts } from '@/getConcepts/handler'
import createCsv from '@/shared/createCsv'
import { getApplicationConfig } from '@/shared/getConfig'
import getCsvHeaders from '@/shared/getCsvHeaders'
import getCsvMetadata from '@/shared/getCsvMetadata'
import getCsvPaths from '@/shared/getCsvPaths'
import { getFilteredTriples } from '@/shared/getFilteredTriples'
import { getGcmdMetadata } from '@/shared/getGcmdMetadata'
import { getRootConcepts } from '@/shared/getRootConcepts'
Expand All @@ -21,6 +25,10 @@ vi.mock('@/shared/processTriples')
vi.mock('@/shared/getConfig')
vi.mock('@/shared/getGcmdMetadata')
vi.mock('@/shared/getRootConcepts')
vi.mock('@/shared/createCsv')
vi.mock('@/shared/getCsvMetadata')
vi.mock('@/shared/getCsvHeaders')
vi.mock('@/shared/getCsvPaths')

describe('getConcepts', () => {
const mockDefaultHeaders = { 'X-Custom-Header': 'value' }
Expand Down Expand Up @@ -621,4 +629,100 @@ describe('getConcepts', () => {
expect(console.error).toHaveBeenCalledWith(`Error retrieving concept, error=${mockError.toString()}`)
})
})

describe('when format is csv', () => {
test('returns CSV data when format=csv is specified', async () => {
const mockScheme = 'testScheme'
const mockCsvMetadata = { some: 'metadata' }
const mockCsvHeaders = ['Header1', 'Header2']
const mockCsvPaths = [['Path1', 'Path2'], ['Path3', 'Path4']]
const mockCsvContent = 'Header1,Header2\nPath1,Path2\nPath3,Path4'

getCsvMetadata.mockResolvedValue(mockCsvMetadata)
getCsvHeaders.mockResolvedValue(mockCsvHeaders)
getCsvPaths.mockResolvedValue(mockCsvPaths)
createCsv.mockResolvedValue(mockCsvContent)

const event = {
queryStringParameters: {
format: 'csv',
scheme: mockScheme
}
}

const result = await getConcepts(event)

expect(getCsvMetadata).toHaveBeenCalledWith(mockScheme)
expect(getCsvHeaders).toHaveBeenCalledWith(mockScheme)
expect(getCsvPaths).toHaveBeenCalledWith(mockScheme, 2) // 2 is the length of mockCsvHeaders
expect(createCsv).toHaveBeenCalledWith(mockCsvMetadata, mockCsvHeaders, mockCsvPaths)

expect(result).toEqual({
statusCode: 200,
body: mockCsvContent,
headers: {
...mockDefaultHeaders
// 'Content-Type': 'text/csv',
// 'Content-Disposition': `attachment; filename=${mockScheme}.csv`
}
})
})

test('returns 500 error when CSV generation fails', async () => {
const mockError = new Error('CSV generation failed')
getCsvMetadata.mockRejectedValue(mockError)

const event = {
queryStringParameters: {
format: 'csv',
scheme: 'testScheme'
}
}

const result = await getConcepts(event)

expect(result).toEqual({
headers: mockDefaultHeaders,
statusCode: 500,
body: JSON.stringify({
error: mockError.toString()
})
})

expect(console.error).toHaveBeenCalledWith(`Error retrieving full path, error=${mockError.toString()}`)
})

test('handles missing scheme parameter', async () => {
const mockCsvMetadata = { some: 'metadata' }
const mockCsvHeaders = ['Header1', 'Header2']
const mockCsvPaths = [['Path1', 'Path2'], ['Path3', 'Path4']]
const mockCsvContent = 'Header1,Header2\nPath1,Path2\nPath3,Path4'

getCsvMetadata.mockResolvedValue(mockCsvMetadata)
getCsvHeaders.mockResolvedValue(mockCsvHeaders)
getCsvPaths.mockResolvedValue(mockCsvPaths)
createCsv.mockResolvedValue(mockCsvContent)

const event = {
queryStringParameters: {
format: 'csv'
}
}

const result = await getConcepts(event)

expect(getCsvMetadata).toHaveBeenCalledWith('')
expect(getCsvHeaders).toHaveBeenCalledWith('')
expect(getCsvPaths).toHaveBeenCalledWith('', 2)
expect(createCsv).toHaveBeenCalledWith(mockCsvMetadata, mockCsvHeaders, mockCsvPaths)

expect(result).toEqual({
statusCode: 200,
body: mockCsvContent,
headers: {
...mockDefaultHeaders
}
})
})
})
})
40 changes: 40 additions & 0 deletions serverless/src/getConcepts/handler.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { XMLBuilder } from 'fast-xml-parser'

import { namespaces } from '@/shared/constants/namespaces'
import createCsv from '@/shared/createCsv'
import { getApplicationConfig } from '@/shared/getConfig'
import getCsvHeaders from '@/shared/getCsvHeaders'
import getCsvMetadata from '@/shared/getCsvMetadata'
import getCsvPaths from '@/shared/getCsvPaths'
import { getFilteredTriples } from '@/shared/getFilteredTriples'
import { getGcmdMetadata } from '@/shared/getGcmdMetadata'
import { getRootConcepts } from '@/shared/getRootConcepts'
Expand Down Expand Up @@ -33,6 +37,42 @@ import { toSkosJson } from '@/shared/toSkosJson'
*/
export const getConcepts = async (event) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is up with the tests, says this line through 134 are no longer covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have issue with local branch KMS-532

const { defaultResponseHeaders } = getApplicationConfig()
const { queryStringParameters = {} } = event
const { format = '', scheme = '' } = queryStringParameters
if (format === 'csv') {
try {
// Get CSV output metadata
const csvMetadata = await getCsvMetadata(scheme)
// Get CSV headers
const csvHeaders = await getCsvHeaders(scheme)
// Calculate CSV header count
const csvHeadersCount = csvHeaders.length
// Get CSV row data
const paths = await getCsvPaths(scheme, csvHeadersCount)
// Set CSV response header
const responseHeaders = {
...defaultResponseHeaders
// 'Content-Type': 'text/csv',
// 'Content-Disposition': `attachment; filename=${scheme}.csv`
}

return {
statusCode: 200,
body: await createCsv(csvMetadata, csvHeaders, paths),
headers: responseHeaders
}
} catch (error) {
console.error(`Error retrieving full path, error=${error.toString()}`)

return {
headers: defaultResponseHeaders,
statusCode: 500,
body: JSON.stringify({
error: error.toString()
})
}
}
}

const { conceptScheme, pattern } = event?.pathParameters || {}
const { page_num: pageNumStr = '1', page_size: pageSizeStr = '2000' } = event?.queryStringParameters || {}
Expand Down
60 changes: 60 additions & 0 deletions serverless/src/shared/__tests__/createCsv.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// CreateCsv.test.js
import { stringify } from 'csv'
import {
afterEach,
describe,
expect,
vi
} from 'vitest'

import createCsv from '../createCsv'

vi.mock('csv', () => ({
stringify: vi.fn()
}))

describe('createCsv', () => {
afterEach(() => {
vi.restoreAllMocks()
})

test('should create a CSV string with metadata, headers, and values', async () => {
const csvMetadata = ['Metadata 1', 'Metadata 2']
const csvHeaders = ['Header 1', 'Header 2']
const values = [
['Value 1A', 'Value 1B'],
['Value 2A', 'Value 2B']
]

const expectedOutput = '"Metadata 1","Metadata 2"\n'
+ '"Header 1","Header 2"\n'
+ '"Value 1A","Value 1B"\n'
+ '"Value 2A","Value 2B"\n'

stringify.mockImplementation((_, __, callback) => {
callback(null, expectedOutput)
})

const result = await createCsv(csvMetadata, csvHeaders, values)

expect(result).toBe(expectedOutput)
})

test('should handle empty input arrays', async () => {
stringify.mockImplementation((_, __, callback) => {
callback(null, '\n\n')
})

const result = await createCsv([], [], [])

expect(result).toBe('\n\n')
})

test('should reject with an error if stringify fails', async () => {
stringify.mockImplementation((_, __, callback) => {
callback(new Error('Stringify error'))
})

await expect(createCsv([], [], [])).rejects.toThrow('Stringify error')
})
})
Loading