Skip to content
7 changes: 6 additions & 1 deletion src/controllers/fixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/dto/fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions test/controllers/fixes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions test/controllers/suggestions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -741,6 +742,7 @@ describe('Suggestions Controller', () => {
getPublishedAt: () => null,
getChangeDetails: () => ({ content: 'new content' }),
getStatus: () => 'IN_PROGRESS',
getOrigin: () => 'MANUAL',
},
];

Expand Down
2 changes: 2 additions & 0 deletions test/dto/fix.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('Fix DTO', () => {
getPublishedAt: () => '2025-01-04T00:00:00.000Z',
getChangeDetails: () => ({ field: 'value' }),
getStatus: () => 'PENDING',
getOrigin: () => 'MANUAL',
...overrides,
});

Expand Down Expand Up @@ -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');
});
Expand Down