diff --git a/src/controllers/fixes.js b/src/controllers/fixes.js index 646b26e22..c11bd0b0c 100644 --- a/src/controllers/fixes.js +++ b/src/controllers/fixes.js @@ -352,7 +352,7 @@ export class FixesController { } const { - executedBy, executedAt, publishedAt, changeDetails, suggestionIds, + executedBy, executedAt, publishedAt, changeDetails, suggestionIds, origin, } = context.data; const Suggestion = this.#Suggestion; @@ -387,6 +387,11 @@ export class FixesController { hasUpdates = true; } + if (origin !== fix.getOrigin() && hasText(origin)) { + fix.setOrigin(origin); + hasUpdates = true; + } + if (hasUpdates) { return ok(FixDto.toJSON(await fix.save())); } else { diff --git a/src/dto/fix.js b/src/dto/fix.js index 691ca129d..7779a4446 100644 --- a/src/dto/fix.js +++ b/src/dto/fix.js @@ -50,6 +50,7 @@ export const FixDto = { publishedAt: fix.getPublishedAt(), changeDetails: fix.getChangeDetails(), status: fix.getStatus(), + origin: fix.getOrigin(), }; // Include suggestions if they are attached to the fix entity diff --git a/test/controllers/fixes.test.js b/test/controllers/fixes.test.js index 8ca3b8ed1..0dc604d6a 100644 --- a/test/controllers/fixes.test.js +++ b/test/controllers/fixes.test.js @@ -1237,6 +1237,46 @@ describe('Fixes Controller', () => { expect(new Set(await fix.getSuggestions())).deep.equals(new Set(suggestions)); }); + it('can patch a fix origin field', async () => { + const suggestions = await Promise.all([ + createSuggestion({ type: 'CONTENT_UPDATE' }), + createSuggestion({ type: 'METADATA_UPDATE' }), + ]); + + fixEntitySuggestionCollection.allByFixEntityId.resolves(suggestions.map((s) => ({ + getSuggestionId: () => s.getId(), + getFixEntityId: () => fix.getId(), + }))); + + fixEntityCollection.setSuggestionsForFixEntity.resolves({ + createdItems: suggestions.map((s) => ({ + getSuggestionId: () => s.getId(), + getFixEntityId: () => fix.getId(), + })), + errorItems: [], + removedCount: 0, + }); + + suggestionCollection.batchGetByKeys.resolves({ + data: suggestions, + unprocessed: [], + }); + + const newOrigin = FixEntity.ORIGINS.ASO; + requestContext.data = { + origin: newOrigin, + suggestionIds: suggestions.map((s) => s.getId()), + }; + + const response = await fixesController.patchFix(requestContext); + expect(response).includes({ status: 200 }); + const responseData = await response.json(); + expect(responseData).deep.equals(FixDto.toJSON(fix)); + expect(responseData.origin).to.equal(FixEntity.ORIGINS.ASO); + expect(fix.getOrigin()).equals(newOrigin); + expect(new Set(await fix.getSuggestions())).deep.equals(new Set(suggestions)); + }); + it('responds 404 if a suggestion does not exist', async () => { requestContext.data = { suggestionIds: ['15345195-62e6-494c-81b1-1d0da0b51d84'], @@ -1483,12 +1523,67 @@ describe('Fixes Controller', () => { }); }); }); + + describe('FixDto', () => { + it('serializes fix entity with origin field', async () => { + const fixData = { + type: Suggestion.TYPES.CONTENT_UPDATE, + opportunityId, + changeDetails: { arbitrary: 'test value' }, + origin: FixEntity.ORIGINS.ASO, + status: FixEntity.STATUSES.DEPLOYED, + executedBy: 'test-user', + executedAt: '2025-05-19T01:23:45.678Z', + publishedAt: '2025-05-19T02:23:45.678Z', + }; + + const fix = await fixEntityCollection.create(fixData); + const serialized = FixDto.toJSON(fix); + + expect(serialized).to.include.keys([ + 'id', + 'opportunityId', + 'type', + 'createdAt', + 'executedBy', + 'executedAt', + 'publishedAt', + 'changeDetails', + 'status', + 'origin', + ]); + + expect(serialized.origin).to.equal(FixEntity.ORIGINS.ASO); + expect(serialized.status).to.equal(FixEntity.STATUSES.DEPLOYED); + expect(serialized.type).to.equal(Suggestion.TYPES.CONTENT_UPDATE); + expect(serialized.opportunityId).to.equal(opportunityId); + expect(serialized.changeDetails).to.deep.equal({ arbitrary: 'test value' }); + expect(serialized.executedBy).to.equal('test-user'); + expect(serialized.executedAt).to.equal('2025-05-19T01:23:45.678Z'); + expect(serialized.publishedAt).to.equal('2025-05-19T02:23:45.678Z'); + }); + + it('serializes fix entity with default origin when not specified', async () => { + const fixData = { + type: Suggestion.TYPES.METADATA_UPDATE, + opportunityId, + changeDetails: { arbitrary: 'default test' }, + }; + + const fix = await fixEntityCollection.create(fixData); + const serialized = FixDto.toJSON(fix); + + expect(serialized.origin).to.equal(FixEntity.ORIGINS.SPACECAT); + expect(serialized.status).to.equal(FixEntity.STATUSES.PENDING); + }); + }); }); const ISO_DATE = '2025-05-19T01:23:45.678Z'; function fakeCreateFix(data) { data.fixEntityId ??= crypto.randomUUID(); data.status ??= FixEntity.STATUSES.PENDING; + data.origin ??= FixEntity.ORIGINS.SPACECAT; data.changeDetails ??= { arbitrary: 'details' }; data.createdAt ??= ISO_DATE; data.executedAt ??= ISO_DATE; diff --git a/test/controllers/suggestions.test.js b/test/controllers/suggestions.test.js index 099a3d3d7..8b9a56a4a 100644 --- a/test/controllers/suggestions.test.js +++ b/test/controllers/suggestions.test.js @@ -729,6 +729,7 @@ describe('Suggestions Controller', () => { getPublishedAt: () => '2025-01-01T02:00:00.000Z', getChangeDetails: () => ({ file: 'index.js', changes: 'updated' }), getStatus: () => 'COMPLETED', + getOrigin: () => 'MANUAL', }, { getId: () => FIX_IDS[1], @@ -741,6 +742,7 @@ describe('Suggestions Controller', () => { getPublishedAt: () => null, getChangeDetails: () => ({ content: 'new content' }), getStatus: () => 'IN_PROGRESS', + getOrigin: () => 'MANUAL', }, ]; diff --git a/test/dto/fix.test.js b/test/dto/fix.test.js index fb0d153a0..7711a8ab9 100644 --- a/test/dto/fix.test.js +++ b/test/dto/fix.test.js @@ -39,6 +39,7 @@ describe('Fix DTO', () => { getPublishedAt: () => '2025-01-04T00:00:00.000Z', getChangeDetails: () => ({ field: 'value' }), getStatus: () => 'PENDING', + getOrigin: () => 'MANUAL', ...overrides, }); @@ -72,6 +73,7 @@ describe('Fix DTO', () => { publishedAt: '2025-01-04T00:00:00.000Z', changeDetails: { field: 'value' }, status: 'PENDING', + origin: 'MANUAL', }); expect(json).to.not.have.property('suggestions'); });