fix: enforce private feedback access across list and detail
Deploy EG-BIM QA Gateway / deploy (push) Successful in 50s

This commit is contained in:
root committed 2026-11-13 10:28:00 +09:00
1 parent e2e70c13ac
commit a5a5f24f92
10 files changed
+379 -24

No files matched your search

+36 -5
View File
@@ -21,15 +21,16 @@ function element() {
};
}
async function detailPage({ comments = [], cachedComments = [], createResponse = {}, patchResponse = {}, authRefresh = false, user = USER, postOwner = USER.requesterId } = {}) {
async function detailPage({ comments = [], cachedComments = [], cachedPosts = [], createResponse = {}, patchResponse = {}, authRefresh = false, user = USER, postOwner = USER.requesterId, isSecret = false, detailStatus = 200, commentsStatus = 200 } = {}) {
const elements = new Map([
'#detail', '[data-detail-category]', '[data-detail-title]', '[data-detail-status]',
'[data-detail-date]', '[data-detail-author]', '[data-detail-company]',
'[data-detail-department]', '[data-detail-content]', '[data-detail-secret]',
'#comments', '#commentCount', '#commentForm', '#commentContent',
'#commentError', '#commentSubmit', '[data-comment-edit-input]', '#editButton', '#deleteButton'
'#commentError', '#commentSubmit', '[data-comment-edit-input]', '#editButton', '#deleteButton',
'.comment-box', '[data-detail-attachments]'
].map((selector) => [selector, element()]));
const storage = new Map([[COMMENTS_KEY, JSON.stringify({ [POST]: cachedComments })]]);
const storage = new Map([[COMMENTS_KEY, JSON.stringify({ [POST]: cachedComments })], ['baron.qa.posts.egbim', JSON.stringify(cachedPosts)]]);
const requests = [];
const context = vm.createContext({
window: { QA_CONFIG: {}, location: { origin: 'https://qa.example.test', search: '?id=' + POST }, confirm: () => true },
@@ -45,8 +46,8 @@ async function detailPage({ comments = [], cachedComments = [], createResponse =
else if (method === 'PATCH') body = patchResponse;
else if (method === 'DELETE') body = { ok: true };
else if (path.endsWith('/comments')) body = { comments };
else body = { id: POST, title: '문의 제목', contents: '문의 내용', requester_id: postOwner, requester_name: USER.name };
return new Response(JSON.stringify(body));
else body = { id: POST, title: '문의 제목', contents: '문의 내용', requester_id: postOwner, requester_name: USER.name, is_secret: isSecret };
return new Response(JSON.stringify(body), { status: method === 'GET' ? path.endsWith('/comments') ? commentsStatus : detailStatus : 200 });
}
});
vm.runInContext(source.replace(/\}\)\(\);\s*$/, `
@@ -142,6 +143,36 @@ test('a cached comment without an ID remains read-only when loading the page', a
assert.equal(page.requests.some((request) => ['PATCH', 'DELETE'].includes(request.method)), false);
});
for (const status of [401, 403, 404, 500]) {
test(`detail HTTP ${status} never falls back to a previous account's saved post or comments`, async () => {
const page = await detailPage({ detailStatus: status, cachedPosts: [{ id: POST, title: 'CACHED_TITLE', content: 'CACHED_BODY', secret: true }], cachedComments: [comment({ content: 'CACHED_COMMENT' })] });
assert.doesNotMatch(page.elements.get('[data-detail-title]').textContent, /CACHED_/);
assert.doesNotMatch(page.elements.get('[data-detail-content]').textContent, /CACHED_/);
assert.doesNotMatch(page.html(), /CACHED_/);
assert.equal(page.elements.get('#commentForm').hidden, true);
assert.equal(page.elements.get('#editButton').hidden, true);
assert.equal(page.elements.get('#deleteButton').hidden, true);
assert.equal(page.requests.some(request => request.path.endsWith('/comments')), false);
});
}
test('a non-owner private detail stays hidden even if the API incorrectly returns the whole post', async () => {
const page = await detailPage({ postOwner: 'another-user', isSecret: true, cachedComments: [comment()] });
assert.equal(page.elements.get('[data-detail-title]').textContent, '비밀글입니다.');
assert.doesNotMatch(page.elements.get('[data-detail-content]').textContent, /문의 내용/);
assert.equal(page.elements.get('#commentForm').hidden, true);
assert.equal(page.elements.get('[data-detail-attachments]').hidden, true);
assert.equal(page.requests.some(request => request.path.endsWith('/comments')), false);
});
test('comment errors never restore cached comments, and own private comments are not persisted', async () => {
const denied = await detailPage({ commentsStatus: 403, cachedComments: [comment({ content: 'CACHED_COMMENT' })] });
assert.doesNotMatch(denied.html(), /CACHED_COMMENT/);
const own = await detailPage({ isSecret: true, comments: [comment()] });
assert.match(own.html(), /원래 댓글/);
assert.equal(own.stored(), undefined);
});
test('another author post hides both post buttons while the viewer own comment keeps its actions', async () => {
const page = await detailPage({ postOwner: 'another-user', comments: [comment(), comment({ id: SECOND, author_id: 'another-user' })] });
assert.equal(page.elements.get('#editButton').hidden, true);