Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
36993a83
Commit
36993a83
authored
3 years ago
by
Justin Boyson
Committed by
David O'Regan
3 years ago
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Check for empty position object before stringify
Updates specs to check for correct string parsing
parent
da75cd82
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
189 additions
and
119 deletions
+189
-119
app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
...s/batch_comments/stores/modules/batch_comments/actions.js
+14
-8
app/assets/javascripts/notes/components/noteable_note.vue
app/assets/javascripts/notes/components/noteable_note.vue
+6
-2
changelogs/unreleased/298827-graphql-getting-mr-diff-discussions-often-returns-500.yml
...graphql-getting-mr-diff-discussions-often-returns-500.yml
+5
-0
spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
...ch_comments/stores/modules/batch_comments/actions_spec.js
+33
-26
spec/frontend/notes/components/noteable_note_spec.js
spec/frontend/notes/components/noteable_note_spec.js
+131
-83
No files found.
app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
View file @
36993a83
import
{
isEmpty
}
from
'
lodash
'
;
import
{
deprecatedCreateFlash
as
flash
}
from
'
~/flash
'
;
import
{
scrollToElement
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
...
...
@@ -88,18 +89,23 @@ export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGet
export
const
updateDraft
=
(
{
commit
,
getters
},
{
note
,
noteText
,
resolveDiscussion
,
position
,
callback
},
)
=>
service
.
update
(
getters
.
getNotesData
.
draftsPath
,
{
draftId
:
note
.
id
,
note
:
noteText
,
resolveDiscussion
,
position
:
JSON
.
stringify
(
position
),
})
)
=>
{
const
params
=
{
draftId
:
note
.
id
,
note
:
noteText
,
resolveDiscussion
,
};
// Stringifying an empty object yields `{}` which breaks graphql queries
// https://gitlab.com/gitlab-org/gitlab/-/issues/298827
if
(
!
isEmpty
(
position
))
params
.
position
=
JSON
.
stringify
(
position
);
return
service
.
update
(
getters
.
getNotesData
.
draftsPath
,
params
)
.
then
((
res
)
=>
res
.
data
)
.
then
((
data
)
=>
commit
(
types
.
RECEIVE_DRAFT_UPDATE_SUCCESS
,
data
))
.
then
(
callback
)
.
catch
(()
=>
flash
(
__
(
'
An error occurred while updating the comment
'
)));
};
export
const
scrollToDraft
=
({
dispatch
,
rootGetters
},
draft
)
=>
{
const
discussion
=
draft
.
discussion_id
&&
rootGetters
.
getDiscussion
(
draft
.
discussion_id
);
...
...
This diff is collapsed.
Click to expand it.
app/assets/javascripts/notes/components/noteable_note.vue
View file @
36993a83
<
script
>
import
{
GlSprintf
,
GlSafeHtmlDirective
as
SafeHtml
}
from
'
@gitlab/ui
'
;
import
$
from
'
jquery
'
;
import
{
escape
}
from
'
lodash
'
;
import
{
escape
,
isEmpty
}
from
'
lodash
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
INLINE_DIFF_LINES_KEY
}
from
'
~/diffs/constants
'
;
import
httpStatusCodes
from
'
~/lib/utils/http_status
'
;
...
...
@@ -282,9 +282,13 @@ export default {
note
:
{
target_type
:
this
.
getNoteableData
.
targetType
,
target_id
:
this
.
note
.
noteable_id
,
note
:
{
note
:
noteText
,
position
:
JSON
.
stringify
(
position
)
},
note
:
{
note
:
noteText
},
},
};
// Stringifying an empty object yields `{}` which breaks graphql queries
// https://gitlab.com/gitlab-org/gitlab/-/issues/298827
if
(
!
isEmpty
(
position
))
data
.
note
.
note
.
position
=
JSON
.
stringify
(
position
);
this
.
isRequesting
=
true
;
this
.
oldContent
=
this
.
note
.
note_html
;
// eslint-disable-next-line vue/no-mutating-props
...
...
This diff is collapsed.
Click to expand it.
changelogs/unreleased/298827-graphql-getting-mr-diff-discussions-often-returns-500.yml
0 → 100644
View file @
36993a83
---
title
:
fix stringify empty position object
merge_request
:
56037
author
:
type
:
fixed
This diff is collapsed.
Click to expand it.
spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
View file @
36993a83
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
TEST_HOST
}
from
'
helpers/test_constants
'
;
import
testAction
from
'
helpers/vuex_action_helper
'
;
import
service
from
'
~/batch_comments/services/drafts_service
'
;
import
*
as
actions
from
'
~/batch_comments/stores/modules/batch_comments/actions
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
...
...
@@ -201,6 +202,12 @@ describe('Batch comments store actions', () => {
describe
(
'
updateDraft
'
,
()
=>
{
let
getters
;
service
.
update
=
jest
.
fn
();
service
.
update
.
mockResolvedValue
({
data
:
{
id
:
1
}
});
const
commit
=
jest
.
fn
();
let
context
;
let
params
;
beforeEach
(()
=>
{
getters
=
{
...
...
@@ -208,43 +215,43 @@ describe('Batch comments store actions', () => {
draftsPath
:
TEST_HOST
,
},
};
});
it
(
'
commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data
'
,
(
done
)
=>
{
const
commit
=
jest
.
fn
();
const
context
=
{
context
=
{
getters
,
commit
,
};
res
=
{
id
:
1
};
mock
.
onAny
().
reply
(
200
,
res
);
params
=
{
note
:
{
id
:
1
},
noteText
:
'
test
'
};
});
actions
.
updateDraft
(
context
,
{
note
:
{
id
:
1
},
noteText
:
'
test
'
,
callback
()
{}
})
.
then
(()
=>
{
expect
(
commit
).
toHaveBeenCalledWith
(
'
RECEIVE_DRAFT_UPDATE_SUCCESS
'
,
{
id
:
1
});
})
.
then
(
done
)
.
catch
(
done
.
fail
);
afterEach
(()
=>
jest
.
clearAllMocks
());
it
(
'
commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data
'
,
()
=>
{
return
actions
.
updateDraft
(
context
,
{
...
params
,
callback
()
{}
}).
then
(()
=>
{
expect
(
commit
).
toHaveBeenCalledWith
(
'
RECEIVE_DRAFT_UPDATE_SUCCESS
'
,
{
id
:
1
});
});
});
it
(
'
calls passed callback
'
,
(
done
)
=>
{
const
commit
=
jest
.
fn
();
const
context
=
{
getters
,
commit
,
};
it
(
'
calls passed callback
'
,
()
=>
{
const
callback
=
jest
.
fn
();
res
=
{
id
:
1
};
mock
.
onAny
().
reply
(
200
,
res
);
return
actions
.
updateDraft
(
context
,
{
...
params
,
callback
}).
then
(()
=>
{
expect
(
callback
).
toHaveBeenCalled
();
});
});
actions
.
updateDraft
(
context
,
{
note
:
{
id
:
1
},
noteText
:
'
test
'
,
callback
})
.
then
(()
=>
{
expect
(
callback
).
toHaveBeenCalled
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
it
(
'
does not stringify empty position
'
,
()
=>
{
return
actions
.
updateDraft
(
context
,
{
...
params
,
position
:
{},
callback
()
{}
}).
then
(()
=>
{
expect
(
service
.
update
.
mock
.
calls
[
0
][
1
].
position
).
toBeUndefined
();
});
});
it
(
'
stringifies a non-empty position
'
,
()
=>
{
const
position
=
{
test
:
true
};
const
expectation
=
JSON
.
stringify
(
position
);
return
actions
.
updateDraft
(
context
,
{
...
params
,
position
,
callback
()
{}
}).
then
(()
=>
{
expect
(
service
.
update
.
mock
.
calls
[
0
][
1
].
position
).
toBe
(
expectation
);
});
});
});
...
...
This diff is collapsed.
Click to expand it.
spec/frontend/notes/components/noteable_note_spec.js
View file @
36993a83
import
{
mount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
escape
}
from
'
lodash
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
NoteActions
from
'
~/notes/components/note_actions.vue
'
;
import
NoteBody
from
'
~/notes/components/note_body.vue
'
;
import
NoteHeader
from
'
~/notes/components/note_header.vue
'
;
...
...
@@ -13,7 +14,7 @@ describe('issue_note', () => {
let
wrapper
;
const
findMultilineComment
=
()
=>
wrapper
.
find
(
'
[data-testid="multiline-comment"]
'
);
beforeEach
((
)
=>
{
const
createWrapper
=
(
props
=
{}
)
=>
{
store
=
createStore
();
store
.
dispatch
(
'
setNoteableData
'
,
noteableDataMock
);
store
.
dispatch
(
'
setNotesData
'
,
notesDataMock
);
...
...
@@ -23,6 +24,7 @@ describe('issue_note', () => {
store
,
propsData
:
{
note
,
...
props
,
},
localVue
,
stubs
:
[
...
...
@@ -33,14 +35,18 @@ describe('issue_note', () => {
'
multiline-comment-form
'
,
],
});
}
)
;
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
mutiline comments
'
,
()
=>
{
it
(
'
should render if has multiline comment
'
,
()
=>
{
beforeEach
(()
=>
{
createWrapper
();
});
it
(
'
should render if has multiline comment
'
,
async
()
=>
{
const
position
=
{
line_range
:
{
start
:
{
...
...
@@ -69,9 +75,8 @@ describe('issue_note', () => {
line
,
});
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findMultilineComment
().
text
()).
toEqual
(
'
Comment on lines 1 to 2
'
);
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
findMultilineComment
().
text
()).
toBe
(
'
Comment on lines 1 to 2
'
);
});
it
(
'
should only render if it has everything it needs
'
,
()
=>
{
...
...
@@ -147,108 +152,151 @@ describe('issue_note', () => {
});
});
it
(
'
should render user information
'
,
()
=>
{
const
{
author
}
=
note
;
const
avatar
=
wrapper
.
find
(
UserAvatarLink
);
const
avatarProps
=
avatar
.
props
(
);
describe
(
'
rendering
'
,
()
=>
{
beforeEach
(()
=>
{
createWrapper
(
);
}
);
expect
(
avatarProps
.
linkHref
).
toBe
(
author
.
path
);
expect
(
avatarProps
.
imgSrc
).
toBe
(
author
.
avatar_url
);
expect
(
avatarProps
.
imgAlt
).
toBe
(
author
.
name
);
expect
(
avatarProps
.
imgSize
).
toBe
(
40
);
});
it
(
'
should render user information
'
,
()
=>
{
const
{
author
}
=
note
;
const
avatar
=
wrapper
.
findComponent
(
UserAvatarLink
);
const
avatarProps
=
avatar
.
props
();
it
(
'
should render note header content
'
,
()
=>
{
const
noteHeader
=
wrapper
.
find
(
NoteHeader
);
const
noteHeaderProps
=
noteHeader
.
props
();
expect
(
avatarProps
.
linkHref
).
toBe
(
author
.
path
);
expect
(
avatarProps
.
imgSrc
).
toBe
(
author
.
avatar_url
);
expect
(
avatarProps
.
imgAlt
).
toBe
(
author
.
name
);
expect
(
avatarProps
.
imgSize
).
toBe
(
40
);
});
expect
(
noteHeaderProps
.
author
).
toEqual
(
note
.
author
);
expect
(
noteHeaderProps
.
createdAt
).
toEqual
(
note
.
created_at
);
expect
(
noteHeaderProps
.
noteId
).
toEqual
(
note
.
id
);
});
it
(
'
should render note header content
'
,
()
=>
{
const
noteHeader
=
wrapper
.
findComponent
(
NoteHeader
);
const
noteHeaderProps
=
noteHeader
.
props
();
it
(
'
should render note actions
'
,
()
=>
{
const
{
author
}
=
note
;
const
noteActions
=
wrapper
.
find
(
NoteActions
);
const
noteActionsProps
=
noteActions
.
props
();
expect
(
noteActionsProps
.
authorId
).
toBe
(
author
.
id
);
expect
(
noteActionsProps
.
noteId
).
toBe
(
note
.
id
);
expect
(
noteActionsProps
.
noteUrl
).
toBe
(
note
.
noteable_note_url
);
expect
(
noteActionsProps
.
accessLevel
).
toBe
(
note
.
human_access
);
expect
(
noteActionsProps
.
canEdit
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteActionsProps
.
canAwardEmoji
).
toBe
(
note
.
current_user
.
can_award_emoji
);
expect
(
noteActionsProps
.
canDelete
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteActionsProps
.
canReportAsAbuse
).
toBe
(
true
);
expect
(
noteActionsProps
.
canResolve
).
toBe
(
false
);
expect
(
noteActionsProps
.
reportAbusePath
).
toBe
(
note
.
report_abuse_path
);
expect
(
noteActionsProps
.
resolvable
).
toBe
(
false
);
expect
(
noteActionsProps
.
isResolved
).
toBe
(
false
);
expect
(
noteActionsProps
.
isResolving
).
toBe
(
false
);
expect
(
noteActionsProps
.
resolvedBy
).
toEqual
({});
});
expect
(
noteHeaderProps
.
author
).
toBe
(
note
.
author
);
expect
(
noteHeaderProps
.
createdAt
).
toBe
(
note
.
created_at
);
expect
(
noteHeaderProps
.
noteId
).
toBe
(
note
.
id
);
});
it
(
'
should render issue body
'
,
()
=>
{
const
noteBody
=
wrapper
.
find
(
NoteBody
);
const
noteBodyProps
=
noteBody
.
props
();
it
(
'
should render note actions
'
,
()
=>
{
const
{
author
}
=
note
;
const
noteActions
=
wrapper
.
findComponent
(
NoteActions
);
const
noteActionsProps
=
noteActions
.
props
();
expect
(
noteBodyProps
.
note
).
toEqual
(
note
);
expect
(
noteBodyProps
.
line
).
toBe
(
null
);
expect
(
noteBodyProps
.
canEdit
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteBodyProps
.
isEditing
).
toBe
(
false
);
expect
(
noteBodyProps
.
helpPagePath
).
toBe
(
''
);
});
expect
(
noteActionsProps
.
authorId
).
toBe
(
author
.
id
);
expect
(
noteActionsProps
.
noteId
).
toBe
(
note
.
id
);
expect
(
noteActionsProps
.
noteUrl
).
toBe
(
note
.
noteable_note_url
);
expect
(
noteActionsProps
.
accessLevel
).
toBe
(
note
.
human_access
);
expect
(
noteActionsProps
.
canEdit
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteActionsProps
.
canAwardEmoji
).
toBe
(
note
.
current_user
.
can_award_emoji
);
expect
(
noteActionsProps
.
canDelete
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteActionsProps
.
canReportAsAbuse
).
toBe
(
true
);
expect
(
noteActionsProps
.
canResolve
).
toBe
(
false
);
expect
(
noteActionsProps
.
reportAbusePath
).
toBe
(
note
.
report_abuse_path
);
expect
(
noteActionsProps
.
resolvable
).
toBe
(
false
);
expect
(
noteActionsProps
.
isResolved
).
toBe
(
false
);
expect
(
noteActionsProps
.
isResolving
).
toBe
(
false
);
expect
(
noteActionsProps
.
resolvedBy
).
toEqual
({});
});
it
(
'
prevents note preview xss
'
,
(
done
)
=>
{
const
imgSrc
=
'
data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
'
;
const
noteBody
=
`<img src="
${
imgSrc
}
" onload="alert(1)" />`
;
const
alertSpy
=
jest
.
spyOn
(
window
,
'
alert
'
);
store
.
hotUpdate
({
actions
:
{
updateNote
()
{},
setSelectedCommentPositionHover
()
{},
},
it
(
'
should render issue body
'
,
(
)
=>
{
const
noteBody
=
wrapper
.
findComponent
(
NoteBody
)
;
const
noteBodyProps
=
noteBody
.
props
()
;
expect
(
noteBodyProps
.
note
).
toBe
(
note
);
expect
(
noteBodyProps
.
line
).
toBe
(
null
);
expect
(
noteBodyProps
.
canEdit
).
toBe
(
note
.
current_user
.
can_edit
);
expect
(
noteBodyProps
.
isEditing
).
toBe
(
false
);
expect
(
noteBodyProps
.
helpPagePath
).
toBe
(
''
);
});
const
noteBodyComponent
=
wrapper
.
find
(
NoteBody
);
noteBodyComponent
.
vm
.
$emit
(
'
handleFormUpdate
'
,
noteBody
,
null
,
()
=>
{});
it
(
'
prevents note preview xss
'
,
async
()
=>
{
const
noteBody
=
'
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert(1)" />
'
;
const
alertSpy
=
jest
.
spyOn
(
window
,
'
alert
'
).
mockImplementation
(()
=>
{});
const
noteBodyComponent
=
wrapper
.
findComponent
(
NoteBody
);
store
.
hotUpdate
({
actions
:
{
updateNote
()
{},
setSelectedCommentPositionHover
()
{},
},
});
noteBodyComponent
.
vm
.
$emit
(
'
handleFormUpdate
'
,
noteBody
,
null
,
()
=>
{});
setImmediate
(()
=>
{
await
waitForPromises
();
expect
(
alertSpy
).
not
.
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
note
.
note_html
).
toEqual
(
escape
(
noteBody
));
done
();
expect
(
wrapper
.
vm
.
note
.
note_html
).
toBe
(
escape
(
noteBody
));
});
});
describe
(
'
cancel edit
'
,
()
=>
{
it
(
'
restores content of updated note
'
,
(
done
)
=>
{
beforeEach
(()
=>
{
createWrapper
();
});
it
(
'
restores content of updated note
'
,
async
()
=>
{
const
updatedText
=
'
updated note text
'
;
store
.
hotUpdate
({
actions
:
{
updateNote
()
{},
},
});
const
noteBody
=
wrapper
.
find
(
NoteBody
);
const
noteBody
=
wrapper
.
find
Component
(
NoteBody
);
noteBody
.
vm
.
resetAutoSave
=
()
=>
{};
noteBody
.
vm
.
$emit
(
'
handleFormUpdate
'
,
updatedText
,
null
,
()
=>
{});
wrapper
.
vm
.
$nextTick
()
.
then
(()
=>
{
const
noteBodyProps
=
noteBody
.
props
();
expect
(
noteBodyProps
.
note
.
note_html
).
toBe
(
updatedText
);
noteBody
.
vm
.
$emit
(
'
cancelForm
'
);
})
.
then
(()
=>
wrapper
.
vm
.
$nextTick
())
.
then
(()
=>
{
const
noteBodyProps
=
noteBody
.
props
();
expect
(
noteBodyProps
.
note
.
note_html
).
toBe
(
note
.
note_html
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
await
wrapper
.
vm
.
$nextTick
();
let
noteBodyProps
=
noteBody
.
props
();
expect
(
noteBodyProps
.
note
.
note_html
).
toBe
(
updatedText
);
noteBody
.
vm
.
$emit
(
'
cancelForm
'
);
await
wrapper
.
vm
.
$nextTick
();
noteBodyProps
=
noteBody
.
props
();
expect
(
noteBodyProps
.
note
.
note_html
).
toBe
(
note
.
note_html
);
});
});
describe
(
'
formUpdateHandler
'
,
()
=>
{
const
updateNote
=
jest
.
fn
();
const
params
=
[
''
,
null
,
jest
.
fn
(),
''
];
const
updateActions
=
()
=>
{
store
.
hotUpdate
({
actions
:
{
updateNote
,
setSelectedCommentPositionHover
()
{},
},
});
};
afterEach
(()
=>
updateNote
.
mockReset
());
it
(
'
responds to handleFormUpdate
'
,
()
=>
{
createWrapper
();
updateActions
();
wrapper
.
findComponent
(
NoteBody
).
vm
.
$emit
(
'
handleFormUpdate
'
,
...
params
);
expect
(
wrapper
.
emitted
(
'
handleUpdateNote
'
)).
toBeTruthy
();
});
it
(
'
does not stringify empty position
'
,
()
=>
{
createWrapper
();
updateActions
();
wrapper
.
findComponent
(
NoteBody
).
vm
.
$emit
(
'
handleFormUpdate
'
,
...
params
);
expect
(
updateNote
.
mock
.
calls
[
0
][
1
].
note
.
note
.
position
).
toBeUndefined
();
});
it
(
'
stringifies populated position
'
,
()
=>
{
const
position
=
{
test
:
true
};
const
expectation
=
JSON
.
stringify
(
position
);
createWrapper
({
note
:
{
...
note
,
position
}
});
updateActions
();
wrapper
.
findComponent
(
NoteBody
).
vm
.
$emit
(
'
handleFormUpdate
'
,
...
params
);
expect
(
updateNote
.
mock
.
calls
[
0
][
1
].
note
.
note
.
position
).
toBe
(
expectation
);
});
});
});
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment