Skip to content

Commit

Permalink
KMS-516: Fixed update to delete first, then update. (#13)
Browse files Browse the repository at this point in the history
* KMS-516: Fixed update to delete the record first.
---------

Co-authored-by: Christopher Gokey <[email protected]>
  • Loading branch information
cgokey and Christopher Gokey authored Feb 14, 2025
1 parent 4687368 commit 2c6bf16
Show file tree
Hide file tree
Showing 14 changed files with 922 additions and 218 deletions.
4 changes: 2 additions & 2 deletions serverless-configs/aws-functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ createConcept:
- http:
method: post
cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)}
path: concept/{conceptId}
path: concept
createConcepts:
handler: serverless/src/createConcepts/handler.default
timeout: ${env:LAMBDA_TIMEOUT, '30'}
Expand All @@ -53,7 +53,7 @@ updateConcept:
- http:
method: put
cors: ${file(./serverless-configs/${self:provider.name}-cors-configuration.yml)}
path: concept/{conceptId}
path: concept

deleteConcept:
handler: serverless/src/deleteConcept/handler.default
Expand Down
125 changes: 100 additions & 25 deletions serverless/src/createConcept/__tests__/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,90 +6,165 @@ import {
} from 'vitest'
import createConcept from '../handler'
import conceptIdExists from '../../utils/conceptIdExists'
import getConceptId from '../../utils/getConceptId'
import { getApplicationConfig } from '../../utils/getConfig'
import { sparqlRequest } from '../../utils/sparqlRequest'

// Mock the dependencies
vi.mock('../../utils/conceptIdExists')
vi.mock('../../utils/getConceptId')
vi.mock('../../utils/getConfig')
vi.mock('../../utils/sparqlRequest')

describe('createConcept', () => {
const mockEvent = {
body: '<rdf:RDF>...</rdf:RDF>',
pathParameters: { conceptId: '123' }
}

const mockRdfXml = '<rdf:RDF>...</rdf:RDF>'
const mockEvent = { body: mockRdfXml }
const mockDefaultHeaders = { 'Content-Type': 'application/json' }
beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'log').mockImplementation(() => {})
})
const mockConceptId = '123'
const mockConceptIRI = `https://gcmd.earthdata.nasa.gov/kms/concept/${mockConceptId}`

beforeEach(() => {
vi.resetAllMocks()
getApplicationConfig.mockReturnValue({ defaultResponseHeaders: mockDefaultHeaders })
getConceptId.mockReturnValue(mockConceptId)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
})

test('should return 404 if concept already exists', async () => {
conceptIdExists.mockResolvedValue(true)
test('should handle missing body in event', async () => {
const eventWithoutBody = {}

const result = await createConcept(mockEvent)
const result = await createConcept(eventWithoutBody)

expect(result).toEqual({
statusCode: 404,
body: JSON.stringify({ message: 'Concept https://gcmd.earthdata.nasa.gov/kms/concept/123 already exists.' }),
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'Missing RDF/XML data in request body'
}),
headers: mockDefaultHeaders
})
})

test('should create concept and return 200 if concept does not exist', async () => {
test('should successfully create a concept', async () => {
conceptIdExists.mockResolvedValue(false)
sparqlRequest.mockResolvedValue({ ok: true })

const result = await createConcept(mockEvent)

expect(getConceptId).toHaveBeenCalledWith(mockRdfXml)
expect(conceptIdExists).toHaveBeenCalledWith(mockConceptIRI)
expect(sparqlRequest).toHaveBeenCalledWith({
contentType: 'application/rdf+xml',
accept: 'application/rdf+xml',
path: '/statements',
method: 'POST',
body: mockEvent.body
body: mockRdfXml
})

expect(result).toEqual({
statusCode: 201,
body: JSON.stringify({
message: 'Successfully created concept',
conceptId: mockConceptId
}),
headers: mockDefaultHeaders
})
})

test('should return 409 if concept already exists', async () => {
conceptIdExists.mockResolvedValue(true)

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 200,
body: 'Successfully loaded RDF XML into RDF4J',
statusCode: 409,
body: JSON.stringify({ message: `Concept ${mockConceptIRI} already exists.` }),
headers: mockDefaultHeaders
})
})

test('should return 500 if sparqlRequest fails', async () => {
test('should handle getConceptId throwing an error', async () => {
getConceptId.mockImplementation(() => {
throw new Error('Invalid XML')
})

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'Invalid XML'
}),
headers: mockDefaultHeaders
})
})

test('should handle missing concept ID', async () => {
getConceptId.mockReturnValue(null)

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'Invalid or missing concept ID'
}),
headers: mockDefaultHeaders
})
})

test('should handle sparqlRequest failure', async () => {
conceptIdExists.mockResolvedValue(false)
sparqlRequest.mockResolvedValue({
ok: false,
status: 500,
text: () => Promise.resolve('Server error')
text: async () => 'Internal Server Error'
})

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'HTTP error! status: 500'
}),
headers: mockDefaultHeaders
})

expect(console.log).toHaveBeenCalledWith('Response text:', 'Internal Server Error')
})

test('should handle conceptIdExists throwing an error', async () => {
conceptIdExists.mockRejectedValue(new Error('Database error'))

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 500,
body: 'Error loading RDF XML into RDF4J',
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'Database error'
}),
headers: mockDefaultHeaders
})
})

test('should return 500 if an error is thrown', async () => {
test('should handle sparqlRequest throwing an error', async () => {
conceptIdExists.mockResolvedValue(false)
sparqlRequest.mockRejectedValue(new Error('Network error'))

const result = await createConcept(mockEvent)

expect(result).toEqual({
statusCode: 500,
body: 'Error loading RDF XML into RDF4J',
statusCode: 400,
body: JSON.stringify({
message: 'Error creating concept',
error: 'Network error'
}),
headers: mockDefaultHeaders
})
})
Expand Down
50 changes: 31 additions & 19 deletions serverless/src/createConcept/handler.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import conceptIdExists from '../utils/conceptIdExists'
import getConceptId from '../utils/getConceptId'
import { getApplicationConfig } from '../utils/getConfig'
import { sparqlRequest } from '../utils/sparqlRequest'

Expand All @@ -12,8 +13,6 @@ import { sparqlRequest } from '../utils/sparqlRequest'
* @function createConcept
* @param {Object} event - The Lambda event object.
* @param {string} event.body - The RDF/XML representation of the concept to be created.
* @param {Object} event.pathParameters - The path parameters from the API Gateway event.
* @param {string} event.pathParameters.conceptId - The ID of the concept to be created.
* @returns {Promise<Object>} A promise that resolves to an object containing the statusCode, body, and headers.
*
* @example
Expand All @@ -34,22 +33,29 @@ import { sparqlRequest } from '../utils/sparqlRequest'
*/
const createConcept = async (event) => {
const { defaultResponseHeaders } = getApplicationConfig()
const { body: rdfXml } = event
const { conceptId } = event.pathParameters // Assuming the concept ID is passed as a path parameter
const { body: rdfXml } = event || {} // Use empty object as fallback

// Create the basic auth header
const conceptIRI = `https://gcmd.earthdata.nasa.gov/kms/concept/${conceptId}`
try {
if (!rdfXml) {
throw new Error('Missing RDF/XML data in request body')
}

const exists = await conceptIdExists(conceptIRI)
if (exists) {
return {
statusCode: 404,
body: JSON.stringify({ message: `Concept ${conceptIRI} already exists.` }),
headers: defaultResponseHeaders
const conceptId = getConceptId(rdfXml)
if (!conceptId) {
throw new Error('Invalid or missing concept ID')
}

const conceptIRI = `https://gcmd.earthdata.nasa.gov/kms/concept/${conceptId}`

const exists = await conceptIdExists(conceptIRI)
if (exists) {
return {
statusCode: 409,
body: JSON.stringify({ message: `Concept ${conceptIRI} already exists.` }),
headers: defaultResponseHeaders
}
}
}

try {
const response = await sparqlRequest({
contentType: 'application/rdf+xml',
accept: 'application/rdf+xml',
Expand All @@ -67,16 +73,22 @@ const createConcept = async (event) => {
console.log('Successfully loaded RDF XML into RDF4J')

return {
statusCode: 200,
body: 'Successfully loaded RDF XML into RDF4J',
statusCode: 201, // Changed from 200 to 201 Created
body: JSON.stringify({
message: 'Successfully created concept',
conceptId
}),
headers: defaultResponseHeaders
}
} catch (error) {
console.error('Error loading RDF XML into RDF4J:', error)
console.error('Error creating concept:', error)

return {
statusCode: 500,
body: 'Error loading RDF XML into RDF4J',
statusCode: 400, // Changed from 500 to 400 for client errors
body: JSON.stringify({
message: 'Error creating concept',
error: error.message
}),
headers: defaultResponseHeaders
}
}
Expand Down
Loading

0 comments on commit 2c6bf16

Please sign in to comment.