Skip to content

Commit 2947acc

Browse files
NalinNairclaude
andcommitted
🛡️ fix(prompts): address PR danny-avila#11882 review — ObjectId casting, null error return, unit tests
- getOwnedPromptGroupIds: validate and cast author to ObjectId (consistent with rest of Prompt.js, prevents silent empty-list on invalid input) - getPromptGroup catch: return null instead of { message } object so callers (canAccessPromptGroupResource) don't treat errors as found - Add format.spec.ts with 8 tests covering MY_PROMPTS, SHARED_PROMPTS, ALL filtering logic including deduplication and legacy fallback paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e58b0a7 commit 2947acc

File tree

2 files changed

+134
-2
lines changed

2 files changed

+134
-2
lines changed

‎api/models/Prompt.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,14 @@ async function getListPromptGroupsByAccess({
376376
*/
377377
const getOwnedPromptGroupIds = async (author) => {
378378
try {
379-
const groups = await PromptGroup.find({ author }, { _id: 1 }).lean();
379+
if (!author || !ObjectId.isValid(author)) {
380+
logger.warn('getOwnedPromptGroupIds called with invalid author', { author });
381+
return [];
382+
}
383+
const groups = await PromptGroup.find(
384+
{ author: new ObjectId(author) },
385+
{ _id: 1 },
386+
).lean();
380387
return groups.map((g) => g._id);
381388
} catch (error) {
382389
logger.error('Error getting owned prompt group IDs', error);
@@ -567,7 +574,7 @@ module.exports = {
567574
return group;
568575
} catch (error) {
569576
logger.error('Error getting prompt group', error);
570-
return { message: 'Error getting prompt group' };
577+
return null;
571578
}
572579
},
573580
/**
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Types } from 'mongoose';
2+
import { filterAccessibleIdsBySharedLogic } from './format';
3+
4+
const id = () => new Types.ObjectId();
5+
6+
describe('filterAccessibleIdsBySharedLogic', () => {
7+
const ownedA = id();
8+
const ownedB = id();
9+
const sharedC = id();
10+
const sharedD = id();
11+
const publicE = id();
12+
const publicF = id();
13+
14+
// accessible = everything the ACL resolver says this user can see
15+
const accessibleIds = [ownedA, ownedB, sharedC, sharedD];
16+
const ownedPromptGroupIds = [ownedA, ownedB];
17+
const publicPromptGroupIds = [publicE, publicF];
18+
19+
function toStrings(ids: Types.ObjectId[]) {
20+
return ids.map((i) => i.toString()).sort();
21+
}
22+
23+
describe('MY_PROMPTS (searchShared=false)', () => {
24+
it('returns only owned IDs when ownedPromptGroupIds provided', async () => {
25+
const result = await filterAccessibleIdsBySharedLogic({
26+
accessibleIds,
27+
searchShared: false,
28+
searchSharedOnly: false,
29+
publicPromptGroupIds,
30+
ownedPromptGroupIds,
31+
});
32+
expect(toStrings(result)).toEqual(toStrings([ownedA, ownedB]));
33+
});
34+
35+
it('legacy fallback: excludes public IDs when ownedPromptGroupIds omitted', async () => {
36+
// accessible includes a public ID for this test
37+
const accessible = [ownedA, ownedB, publicE];
38+
const result = await filterAccessibleIdsBySharedLogic({
39+
accessibleIds: accessible,
40+
searchShared: false,
41+
searchSharedOnly: false,
42+
publicPromptGroupIds,
43+
});
44+
// Should exclude publicE, keep ownedA and ownedB
45+
expect(toStrings(result)).toEqual(toStrings([ownedA, ownedB]));
46+
});
47+
});
48+
49+
describe('SHARED_PROMPTS (searchSharedOnly=true)', () => {
50+
it('returns accessible + public minus owned when ownedPromptGroupIds provided', async () => {
51+
const result = await filterAccessibleIdsBySharedLogic({
52+
accessibleIds,
53+
searchShared: true,
54+
searchSharedOnly: true,
55+
publicPromptGroupIds,
56+
ownedPromptGroupIds,
57+
});
58+
// Should include sharedC, sharedD, publicE, publicF (not ownedA, ownedB)
59+
expect(toStrings(result)).toEqual(toStrings([sharedC, sharedD, publicE, publicF]));
60+
});
61+
62+
it('deduplicates when an ID appears in both accessible and public', async () => {
63+
const overlap = id();
64+
const result = await filterAccessibleIdsBySharedLogic({
65+
accessibleIds: [ownedA, overlap],
66+
searchShared: true,
67+
searchSharedOnly: true,
68+
publicPromptGroupIds: [overlap, publicE],
69+
ownedPromptGroupIds: [ownedA],
70+
});
71+
// overlap should appear once, not twice
72+
expect(toStrings(result)).toEqual(toStrings([overlap, publicE]));
73+
});
74+
75+
it('legacy fallback: returns intersection of public and accessible when ownedPromptGroupIds omitted', async () => {
76+
// publicE is also accessible
77+
const accessible = [ownedA, publicE];
78+
const result = await filterAccessibleIdsBySharedLogic({
79+
accessibleIds: accessible,
80+
searchShared: true,
81+
searchSharedOnly: true,
82+
publicPromptGroupIds: [publicE, publicF],
83+
});
84+
// Only publicE is in both accessible and public
85+
expect(toStrings(result)).toEqual(toStrings([publicE]));
86+
});
87+
88+
it('legacy fallback: returns empty when no public IDs', async () => {
89+
const result = await filterAccessibleIdsBySharedLogic({
90+
accessibleIds,
91+
searchShared: true,
92+
searchSharedOnly: true,
93+
publicPromptGroupIds: [],
94+
});
95+
expect(result).toEqual([]);
96+
});
97+
});
98+
99+
describe('ALL (searchShared=true, searchSharedOnly=false)', () => {
100+
it('returns union of accessible + public, deduplicated', async () => {
101+
const result = await filterAccessibleIdsBySharedLogic({
102+
accessibleIds,
103+
searchShared: true,
104+
searchSharedOnly: false,
105+
publicPromptGroupIds,
106+
ownedPromptGroupIds,
107+
});
108+
expect(toStrings(result)).toEqual(
109+
toStrings([ownedA, ownedB, sharedC, sharedD, publicE, publicF]),
110+
);
111+
});
112+
113+
it('deduplicates overlapping IDs', async () => {
114+
const overlap = id();
115+
const result = await filterAccessibleIdsBySharedLogic({
116+
accessibleIds: [ownedA, overlap],
117+
searchShared: true,
118+
searchSharedOnly: false,
119+
publicPromptGroupIds: [overlap, publicE],
120+
ownedPromptGroupIds,
121+
});
122+
expect(toStrings(result)).toEqual(toStrings([ownedA, overlap, publicE]));
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)