diff --git a/CHANGELOG b/CHANGELOG index ca07da7c9..c265d8e0f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,8 +19,7 @@ - [report type]: - [future tense verb] [reporting enhancement] - REST/JSON API enhancements: - - [API entity]: - - [future tense verb] [API enhancement] + - Boards, Lists, Cards: add initial implementation - Security Fixes: - High: (Authenticated|Unauthenticated) (admin|author|contributor) [vulnerability description] - Medium: (Authenticated|Unauthenticated) (admin|author|contributor) [vulnerability description] diff --git a/app/models/card.rb b/app/models/card.rb index 4f044a277..b0dd2ac55 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -84,7 +84,6 @@ def to_xml(xml_builder, includes: [], version: 3) end end - private def local_fields { 'List' => list.name.parameterize(preserve_case: true, separator: '_'), @@ -92,6 +91,8 @@ def local_fields } end + private + # We are saving the board_id to the card's version so that if the card's list # is deleted, we still have an idea if the card's board still exists. def add_board_id_to_version diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 010681c10..b54ccba39 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -69,6 +69,29 @@ ], "note": "False positive: Attachment.pwd is set by the admin to specify the directory for the attachments" }, + { + "warning_type": "File Access", + "warning_code": 16, + "fingerprint": "180c4b532ac14a974f17a0884b71b2dfd1bfe01815b4c84ae070f082842a49fc", + "check_name": "FileAccess", + "message": "Model attribute used in file name", + "file": "engines/dradis-api/app/controllers/dradis/ce/api/v3/attachments_controller.rb", + "line": 56, + "link": "https://brakemanscanner.org/docs/warning_types/file_access/", + "code": "File.rename(Attachment.find(params[:filename], :conditions => ({ :node_id => current_project.nodes.find(params[:node_id]).id })).fullpath, Attachment.pwd.join(current_project.nodes.find(params[:node_id]).id.to_s, CGI.unescape(attachment_params[:filename])).to_s)", + "render_path": null, + "location": { + "type": "method", + "class": "Dradis::CE::API::V3::AttachmentsController", + "method": "update" + }, + "user_input": "Attachment.find(params[:filename], :conditions => ({ :node_id => current_project.nodes.find(params[:node_id]).id })).fullpath", + "confidence": "Medium", + "cwe_id": [ + 22 + ], + "note": "False positive: The destination filename is prepended by the Attachments directory and validated as such to prevent being moved to the other directories" + }, { "warning_type": "File Access", "warning_code": 16, @@ -209,6 +232,30 @@ ], "note": "False positive: params[:uploader] here is being validated in the controller" }, + { + "warning_type": "Denial of Service", + "warning_code": 76, + "fingerprint": "d4b56fe0de40fbaed1bfdd6cc44d57ac2dc2b4aef5293c2afc326aef6e0c88e6", + "check_name": "RegexDoS", + "message": "Model attribute used in regular expression", + "file": "engines/dradis-api/app/controllers/dradis/ce/api/v3/attachments_controller.rb", + "line": 55, + "link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/", + "code": "/^#{Attachment.pwd}/", + "render_path": null, + "location": { + "type": "method", + "class": "Dradis::CE::API::V3::AttachmentsController", + "method": "update" + }, + "user_input": "Attachment.pwd", + "confidence": "Medium", + "cwe_id": [ + 20, + 185 + ], + "note": "False positive: Attachment.pwd is set by the admin to specify the directory for the attachments" + }, { "warning_type": "Remote Code Execution", "warning_code": 24, @@ -267,6 +314,6 @@ "note": "False positive: The params is used to fetch the boards and cannot be manipulated by user input" } ], - "updated": "2023-03-30 20:21:19 +0800", + "updated": "2023-08-18 10:40:16 -0400", "brakeman_version": "5.4.0" } diff --git a/engines/dradis-api/CHANGELOG b/engines/dradis-api/CHANGELOG new file mode 100644 index 000000000..0c13ed97d --- /dev/null +++ b/engines/dradis-api/CHANGELOG @@ -0,0 +1,2 @@ +v3 (Aug 2023) + - Add Boards, Lists, Cards endpoints. diff --git a/engines/dradis-api/README.md b/engines/dradis-api/README.md index 9d15c57cf..d1ebd2cfc 100644 --- a/engines/dradis-api/README.md +++ b/engines/dradis-api/README.md @@ -4,5 +4,25 @@ This plugin provides an external HTTP API that you can use to query / publish data to your Dradis instance. +## Bumping the API version + +Rewatch: http://railscasts.com/episodes/350-rest-api-versioning + +- When we bump the API version, we copy everything from the previous version and +start making changes while leaving the originals untouched. +- This means the entire controllers/vX/ and views/vX folders. +- Initially it duplicates the code, but eventually the new version is going to +evolve over time, while the original version will remain a snapshot of the +functionality that's frozen in time. +- I think we can safely deprecate older API versions after 2 years. See the +comments in the v1 controllers/ files for guidance on what to include in the +deprecated files. +- You'll also need to duplicate the routes block, and update the :default route +constraint to point to the new version. +- You'll need to duplicate the request specs too. Update the previous specs w/ +a `let(:api_version)` block and `include_context 'versioned API'` +- Update the engine's CHANGELOG w/ a list of the breaking changes. + + ## Links, licensing, etc. See the main repo's [README.md](https://github.com/dradis/dradis-ce/blob/master/README.md) diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v1/evidence_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v1/evidence_controller.rb index f321f0e02..022b81208 100644 --- a/engines/dradis-api/app/controllers/dradis/ce/api/v1/evidence_controller.rb +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v1/evidence_controller.rb @@ -51,7 +51,6 @@ def set_node def evidence_params params.require(:evidence).permit(:content, :issue_id) end - end end end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v1/notes_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v1/notes_controller.rb index 10cf48a8a..b70e2a990 100644 --- a/engines/dradis-api/app/controllers/dradis/ce/api/v1/notes_controller.rb +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v1/notes_controller.rb @@ -52,7 +52,6 @@ def set_node def note_params params.require(:note).permit(:category_id, :text) end - end end end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/attachments_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/attachments_controller.rb new file mode 100644 index 000000000..679433a81 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/attachments_controller.rb @@ -0,0 +1,85 @@ +module Dradis::CE::API + module V3 + class AttachmentsController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_node + + skip_before_action :json_required, only: [:create] + + def index + @attachments = @node.attachments.each(&:close) + end + + def show + begin + @attachment = Attachment.find(params[:filename], conditions: { node_id: @node.id }) + rescue + raise ActiveRecord::RecordNotFound, "Couldn't find attachment with filename '#{params[:filename]}'" + end + end + + def create + uploaded_files = params.fetch(:files, []) + + @attachments = [] + uploaded_files.each do |uploaded_file| + attachment_name = NamingService.name_file( + original_filename: uploaded_file.original_filename, + pathname: Attachment.pwd.join(@node.id.to_s) + ) + + attachment = Attachment.new(attachment_name, node_id: @node.id) + attachment << uploaded_file.read + attachment.save + + @attachments << attachment + end + + if @attachments.any? && @attachments.count == uploaded_files.count + render status: 201 + else + render status: 422 + end + end + + def update + attachment = Attachment.find(params[:filename], conditions: { node_id: @node.id }) + attachment.close + + begin + new_name = CGI::unescape(attachment_params[:filename]) + destination = Attachment.pwd.join(@node.id.to_s, new_name).to_s + + if !File.exist?(destination) && !destination.match(/^#{Attachment.pwd}/).nil? + File.rename attachment.fullpath, destination + @attachment = Attachment.find(new_name, conditions: { node_id: @node.id }) + else + raise 'Destination file already exists' + end + rescue + @attachment = attachment + render status: 422 + end + end + + def destroy + @attachment = Attachment.find(params[:filename], conditions: { node_id: @node.id }) + @attachment.delete + + render_successful_destroy_message + end + + private + + def set_node + @node = current_project.nodes.find(params[:node_id]) + end + + def attachment_params + params.require(:attachment).permit(:filename) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/boards_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/boards_controller.rb new file mode 100644 index 000000000..4a88aff48 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/boards_controller.rb @@ -0,0 +1,53 @@ +module Dradis::CE::API + module V3 + class BoardsController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + def index + @boards = current_project.boards.includes(:lists, lists: [:cards]).order('updated_at desc') + @boards = @boards.page(params[:page].to_i) if params[:page] + end + + def show + @board = current_project.boards.includes(:lists, lists: [:cards]).find(params[:id]) + end + + def create + @board = current_project.boards.new(board_params) + # we are mimicking the hidden_field used in the UI to set the node_id in CE + @board.node_id = current_project.methodology_library.id if !params[:node_id] + + if @board.save + track_created(@board) + render status: 201, location: dradis_api.board_url(@board) + else + render_validation_errors(@board) + end + end + + def update + @board = current_project.boards.find(params[:id]) + if @board.update(board_params) + track_updated(@board) + render board: @board + else + render_validation_errors(@board) + end + end + + def destroy + board = current_project.boards.find(params[:id]) + board.destroy + track_destroyed(board) + render_successful_destroy_message + end + + protected + + def board_params + params.require(:board).permit(:name, :node_id) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/cards_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/cards_controller.rb new file mode 100644 index 000000000..5f1662442 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/cards_controller.rb @@ -0,0 +1,64 @@ +module Dradis::CE::API + module V3 + class CardsController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_board + before_action :set_list + + def index + @cards = @list.cards.includes(:assignees).order('updated_at desc') + @cards = @cards.page(params[:page].to_i) if params[:page] + end + + def show + @card = @list.cards.includes(:assignees).find(params[:id]) + end + + def create + @card = @list.cards.build(card_params) + # Set the new card as the last card of the list + @card.previous_id = @list.last_card.try(:id) + + if @card.save + track_created(@card) + render status: 201, location: board_list_card_path(@board, @list, @card) + else + render_validation_errors(@card) + end + end + + def update + @card = @list.cards.find(params[:id]) + if @card.update(card_params) + track_updated(@card) + render list: @card + else + render_validation_errors(@card) + end + end + + def destroy + @card = @list.cards.find(params[:id]) + @card.destroy + track_destroyed(@card) + render_successful_destroy_message + end + + private + + def set_board + @board = current_project.boards.includes(:lists).find(params[:board_id]) + end + + def set_list + @list = @board.lists.includes(:cards, cards: :assignees).find(params[:list_id]) + end + + def card_params + params.require(:card).permit(:name, :description, :due_date, assignee_ids: []) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/evidence_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/evidence_controller.rb new file mode 100644 index 000000000..d78f403a5 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/evidence_controller.rb @@ -0,0 +1,56 @@ +module Dradis::CE::API + module V3 + class EvidenceController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_node + + def index + @evidence = @node.evidence.order('updated_at desc') + @evidence = @evidence.page(params[:page].to_i) if params[:page] + end + + def show + @evidence = @node.evidence.find(params[:id]) + end + + def create + @evidence = @node.evidence.build(evidence_params) + if @evidence.save + track_created(@evidence) + render status: 201, location: node_evidence_path(@node, @evidence) + else + render_validation_errors(@evidence) + end + end + + def update + @evidence = @node.evidence.find(params[:id]) + if @evidence.update(evidence_params) + track_updated(@evidence) + render evidence: @evidence + else + render_validation_errors(@evidence) + end + end + + def destroy + @evidence = @node.evidence.find(params[:id]) + @evidence.destroy + track_destroyed(@evidence) + render_successful_destroy_message + end + + private + + def set_node + @node = current_project.nodes.find(params[:node_id]) + end + + def evidence_params + params.require(:evidence).permit(:content, :issue_id) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/issues_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/issues_controller.rb new file mode 100644 index 000000000..3141b9861 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/issues_controller.rb @@ -0,0 +1,73 @@ +module Dradis::CE::API + module V3 + class IssuesController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_issue, except: [:index] + before_action :validate_state, only: [:create, :update] + + def index + @issues = current_project.issues.includes(:tags).order('updated_at desc') + @issues = @issues.page(params[:page].to_i) if params[:page] + @issues = @issues.sort + end + + def show + end + + def create + @issue.assign_attributes(issue_params) + @issue.author = current_user.email + @issue.category = Category.issue + @issue.node = current_project.issue_library + + if @issue.save + track_created(@issue) + @issue.tag_from_field_content! + render status: 201, location: dradis_api.issue_url(@issue) + else + render_validation_errors(@issue) + end + end + + def update + if @issue.update(issue_params) + track_updated(@issue) + render node: @node + else + render_validation_errors(@issue) + end + end + + def destroy + @issue.destroy + track_destroyed(@issue) + render_successful_destroy_message + end + + private + + def issue_params + params.require(:issue).permit(:state, :text) + end + + def set_issue + if params[:id] + @issue = current_project.issues.find(params[:id]) + else + @issue = current_project.issues.new + end + end + + def validate_state + return if issue_params[:state].nil? + + unless Issue.states.keys.include? issue_params[:state] + @issue.errors.add(:state, 'invalid value.') + render_validation_errors(@issue) + end + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/lists_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/lists_controller.rb new file mode 100644 index 000000000..0db794ec3 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/lists_controller.rb @@ -0,0 +1,58 @@ +module Dradis::CE::API + module V3 + class ListsController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_board + + def index + @lists = @board.lists.includes(:cards, cards: :assignees).order('updated_at desc') + @lists = @lists.page(params[:page].to_i) if params[:page] + end + + def show + @list = @board.lists.includes(:cards, cards: :assignees).find(params[:id]) + end + + def create + @list = @board.lists.build(list_params) + @list.previous_id = @board.last_list.try(:id) + + if @list.save + track_created(@list) + render status: 201, location: board_list_path(@board, @list) + else + render_validation_errors(@list) + end + end + + def update + @list = @board.lists.find(params[:id]) + if @list.update(list_params) + track_updated(@list) + render list: @list + else + render_validation_errors(@list) + end + end + + def destroy + @list = @board.lists.find(params[:id]) + @list.destroy + track_destroyed(@list) + render_successful_destroy_message + end + + private + + def set_board + @board = current_project.boards.includes(:lists).find(params[:board_id]) + end + + def list_params + params.require(:list).permit(:name) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/nodes_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/nodes_controller.rb new file mode 100644 index 000000000..e4c176a86 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/nodes_controller.rb @@ -0,0 +1,51 @@ +module Dradis::CE::API + module V3 + class NodesController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + def index + @nodes = current_project.nodes.user_nodes.includes(:evidence, :notes, evidence: [:issue]).order('updated_at desc') + @nodes = @nodes.page(params[:page].to_i) if params[:page] + end + + def show + @node = current_project.nodes.includes(:evidence, :notes, evidence: [:issue]).find(params[:id]) + end + + def create + @node = current_project.nodes.new(node_params) + + if @node.save + track_created(@node) + render status: 201, location: dradis_api.node_url(@node) + else + render_validation_errors(@node) + end + end + + def update + @node = current_project.nodes.find(params[:id]) + if @node.update(node_params) + track_updated(@node) + render node: @node + else + render_validation_errors(@node) + end + end + + def destroy + node = current_project.nodes.find(params[:id]) + node.destroy + track_destroyed(node) + render_successful_destroy_message + end + + protected + + def node_params + params.require(:node).permit(:label, :type_id, :parent_id, :position) + end + end + end +end diff --git a/engines/dradis-api/app/controllers/dradis/ce/api/v3/notes_controller.rb b/engines/dradis-api/app/controllers/dradis/ce/api/v3/notes_controller.rb new file mode 100644 index 000000000..f2fa63ee1 --- /dev/null +++ b/engines/dradis-api/app/controllers/dradis/ce/api/v3/notes_controller.rb @@ -0,0 +1,57 @@ +module Dradis::CE::API + module V3 + class NotesController < Dradis::CE::API::APIController + include ActivityTracking + include Dradis::CE::API::ProjectScoped + + before_action :set_node + + def index + @notes = @node.notes.order('updated_at desc') + @notes = @notes.page(params[:page].to_i) if params[:page] + end + + def show + @note = @node.notes.find(params[:id]) + end + + def create + @note = @node.notes.build(note_params) + @note.category ||= Category.default + if @note.save + track_created(@note) + render status: 201, location: node_note_path(@node, @note) + else + render_validation_errors(@note) + end + end + + def update + @note = @node.notes.find(params[:id]) + if @note.update(note_params) + track_updated(@note) + render note: @note + else + render_validation_errors(@note) + end + end + + def destroy + @note = @node.notes.find(params[:id]) + @note.destroy + track_destroyed(@note) + render_successful_destroy_message + end + + private + + def set_node + @node = current_project.nodes.find(params[:node_id]) + end + + def note_params + params.require(:note).permit(:category_id, :text) + end + end + end +end diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/_attachment.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/_attachment.json.jbuilder new file mode 100644 index 000000000..7e8146c6a --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/_attachment.json.jbuilder @@ -0,0 +1,2 @@ +json.filename attachment.filename +json.link main_app.project_node_attachment_path(current_project, @node, attachment.filename) diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/create.json.jbuilder new file mode 100644 index 000000000..bfea03fba --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/create.json.jbuilder @@ -0,0 +1 @@ +json.array! @attachments, partial: 'attachment', as: :attachment diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/index.json.jbuilder new file mode 100644 index 000000000..bfea03fba --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @attachments, partial: 'attachment', as: :attachment diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/show.json.jbuilder new file mode 100644 index 000000000..59c969898 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'attachment', attachment: @attachment diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/update.json.jbuilder new file mode 100644 index 000000000..59c969898 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/attachments/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'attachment', attachment: @attachment diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/boards/_board.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/_board.json.jbuilder new file mode 100644 index 000000000..b4087edbf --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/_board.json.jbuilder @@ -0,0 +1,5 @@ +json.(board, :id, :name, :node_id, :created_at, :updated_at) + +json.lists board.lists do |list| + json.partial! list +end diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/boards/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/create.json.jbuilder new file mode 100644 index 000000000..61f42ee5d --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'board', board: @board diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/boards/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/index.json.jbuilder new file mode 100644 index 000000000..d6bd623fd --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @boards, partial: 'board', as: :board diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/boards/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/show.json.jbuilder new file mode 100644 index 000000000..61f42ee5d --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'board', board: @board diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/boards/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/update.json.jbuilder new file mode 100644 index 000000000..61f42ee5d --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/boards/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'board', board: @board diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/cards/_card.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/_card.json.jbuilder new file mode 100644 index 000000000..4832cbdb8 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/_card.json.jbuilder @@ -0,0 +1,3 @@ +json.(card, :id, :description, :due_date, :name, :fields) + +json.assignees card.assignees, :id, :email diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/cards/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/create.json.jbuilder new file mode 100644 index 000000000..1d3e1ddfb --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'card', card: @card diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/cards/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/index.json.jbuilder new file mode 100644 index 000000000..94b005399 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @cards, partial: 'card', as: :card diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/cards/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/show.json.jbuilder new file mode 100644 index 000000000..1d3e1ddfb --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'card', card: @card diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/cards/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/update.json.jbuilder new file mode 100644 index 000000000..1d3e1ddfb --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/cards/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'card', card: @card diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/_evidence.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/_evidence.json.jbuilder new file mode 100644 index 000000000..6d1c38732 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/_evidence.json.jbuilder @@ -0,0 +1,6 @@ +json.(evidence, :id, :author, :content, :fields) +json.issue do |json| + json.id evidence.issue_id + json.title evidence.issue.title + json.url dradis_api.issue_url(evidence.issue_id) +end diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/create.json.jbuilder new file mode 100644 index 000000000..5d185d168 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'evidence', evidence: @evidence diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/index.json.jbuilder new file mode 100644 index 000000000..5c269b4db --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @evidence, partial: 'evidence', as: :evidence diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/show.json.jbuilder new file mode 100644 index 000000000..5d185d168 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'evidence', evidence: @evidence diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/update.json.jbuilder new file mode 100644 index 000000000..5d185d168 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/evidence/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'evidence', evidence: @evidence diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/issues/_issue.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/_issue.json.jbuilder new file mode 100644 index 000000000..7919cfb81 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/_issue.json.jbuilder @@ -0,0 +1,2 @@ +json.(issue, :id, :author, :title, :fields, :state, :text, :created_at, :updated_at) +json.tags issue.tags, :color, :display_name diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/issues/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/create.json.jbuilder new file mode 100644 index 000000000..9b46d5f82 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'issue', issue: @issue diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/issues/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/index.json.jbuilder new file mode 100644 index 000000000..9228c78db --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @issues, partial: 'issue', as: :issue diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/issues/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/show.json.jbuilder new file mode 100644 index 000000000..9b46d5f82 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'issue', issue: @issue diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/issues/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/update.json.jbuilder new file mode 100644 index 000000000..9b46d5f82 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/issues/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'issue', issue: @issue diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/lists/_list.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/_list.json.jbuilder new file mode 100644 index 000000000..31e3e113f --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/_list.json.jbuilder @@ -0,0 +1,5 @@ +json.(list, :id, :name, :created_at, :updated_at) + +json.cards list.cards do |card| + json.partial! card +end diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/lists/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/create.json.jbuilder new file mode 100644 index 000000000..06b8bf001 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'list', list: @list diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/lists/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/index.json.jbuilder new file mode 100644 index 000000000..5e1be1f20 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @lists, partial: 'list', as: :list diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/lists/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/show.json.jbuilder new file mode 100644 index 000000000..06b8bf001 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'list', list: @list diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/lists/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/update.json.jbuilder new file mode 100644 index 000000000..06b8bf001 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/lists/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'list', list: @list diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/_node.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/_node.json.jbuilder new file mode 100644 index 000000000..dbd11c0b2 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/_node.json.jbuilder @@ -0,0 +1,9 @@ +json.(node, :id, :label, :type_id, :parent_id, :position, :created_at, :updated_at) + +json.evidence node.evidence do |evidence| + json.partial! evidence +end + +json.notes node.notes do |note| + json.partial! note +end diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/create.json.jbuilder new file mode 100644 index 000000000..b1745574e --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'node', node: @node diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/index.json.jbuilder new file mode 100644 index 000000000..fb4e3ff66 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @nodes, partial: 'node', as: :node diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/show.json.jbuilder new file mode 100644 index 000000000..b1745574e --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'node', node: @node diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/update.json.jbuilder new file mode 100644 index 000000000..b1745574e --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/nodes/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'node', node: @node diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/notes/_note.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/_note.json.jbuilder new file mode 100644 index 000000000..c28ebdc46 --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/_note.json.jbuilder @@ -0,0 +1 @@ +json.(note, :id, :author, :category_id, :title, :fields, :text) diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/notes/create.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/create.json.jbuilder new file mode 100644 index 000000000..13a17106a --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'note', note: @note diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/notes/index.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/index.json.jbuilder new file mode 100644 index 000000000..346e0a5fc --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @notes, partial: 'note', as: :note diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/notes/show.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/show.json.jbuilder new file mode 100644 index 000000000..13a17106a --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'note', note: @note diff --git a/engines/dradis-api/app/views/dradis/ce/api/v3/notes/update.json.jbuilder b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/update.json.jbuilder new file mode 100644 index 000000000..13a17106a --- /dev/null +++ b/engines/dradis-api/app/views/dradis/ce/api/v3/notes/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'note', note: @note diff --git a/engines/dradis-api/config/routes.rb b/engines/dradis-api/config/routes.rb index 017844318..fda6dfd22 100644 --- a/engines/dradis-api/config/routes.rb +++ b/engines/dradis-api/config/routes.rb @@ -1,12 +1,29 @@ Dradis::CE::API::Engine::routes.draw do scope path: :api do defaults format: 'json' do - scope module: :v1, constraints: Dradis::CE::API::RoutingConstraints.new(version: 1, default: true) do + scope module: :v1, constraints: Dradis::CE::API::RoutingConstraints.new(version: 1) do resources :issues resources :nodes do resources :evidence resources :notes - constraints(:filename => /.*/) do + constraints(filename: /.*/) do + resources :attachments, param: :filename + end + end + end + + scope module: :v3, constraints: Dradis::CE::API::RoutingConstraints.new(version: 3, default: true) do + resources :boards do + resources :lists do + resources :cards do + end + end + end + resources :issues + resources :nodes do + resources :evidence + resources :notes + constraints(filename: /.*/) do resources :attachments, param: :filename end end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v1/attachments_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v1/attachments_spec.rb index 726135288..91fcf6fd5 100644 --- a/engines/dradis-api/spec/requests/dradis/ce/api/v1/attachments_spec.rb +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v1/attachments_spec.rb @@ -1,12 +1,14 @@ require 'rails_helper' -describe "Attachments API" do - include_context "project scoped API" - include_context "https" +describe 'Attachments API' do + include_context 'project scoped API' + include_context 'https' + include_context 'versioned API' + let(:api_version) { 1 } let(:node) { create(:node, project: current_project) } - context "as unauthenticated user" do + context 'as unauthenticated user' do [ ['get', '/api/nodes/1/attachments/'], ['get', '/api/nodes/1/attachments/image.jpg'], @@ -24,8 +26,8 @@ end end - context "as authorized user" do - include_context "authorized API user" + context 'as authorized user' do + include_context 'authorized API user' before(:each) do FileUtils.rm_rf Dir[Attachment.pwd.join('*')] until Dir[Attachment.pwd.join('*')].count == 0 @@ -35,82 +37,82 @@ FileUtils.rm_rf Dir[Attachment.pwd.join('*')] end - describe "GET /api/nodes/:node_id/attachments" do + describe 'GET /api/nodes/:node_id/attachments' do before do - @attachments = ["image0.png", "image1.png", "image2.png"] + @attachments = ['image0.png', 'image1.png', 'image2.png'] @attachments.each do |attachment| create(:attachment, filename: attachment, node: node) end # an attachment in another node - create(:attachment, filename: "image3.png", node: create(:node, project: current_project)) + create(:attachment, filename: 'image3.png', node: create(:node, project: current_project)) get "/api/nodes/#{node.id}/attachments", env: @env end let(:retrieved_attachments) { JSON.parse(response.body) } - it "responds with HTTP code 200" do + it 'responds with HTTP code 200' do expect(response.status).to eq(200) end - it "retrieves all the attachments for the given node" do + it 'retrieves all the attachments for the given node' do expect(retrieved_attachments.count).to eq @attachments.count - retrieved_filenames = retrieved_attachments.map{ |json| json['filename'] } + retrieved_filenames = retrieved_attachments.map { |json| json['filename'] } expect(retrieved_filenames).to match_array(@attachments) end - it "returns JSON information about attachments" do - attachment_0 = retrieved_attachments.detect { |n| n["filename"] == "image0.png" } - attachment_1 = retrieved_attachments.detect { |n| n["filename"] == "image1.png" } - attachment_2 = retrieved_attachments.detect { |n| n["filename"] == "image2.png" } + it 'returns JSON information about attachments' do + attachment_0 = retrieved_attachments.detect { |n| n['filename'] == 'image0.png' } + attachment_1 = retrieved_attachments.detect { |n| n['filename'] == 'image1.png' } + attachment_2 = retrieved_attachments.detect { |n| n['filename'] == 'image2.png' } expect(attachment_0).to eq({ - "filename" => "image0.png", - "link" => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image0.png" + 'filename' => 'image0.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image0.png" }) expect(attachment_1).to eq({ - "filename" => "image1.png", - "link" => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image1.png" + 'filename' => 'image1.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image1.png" }) expect(attachment_2).to eq({ - "filename" => "image2.png", - "link" => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image2.png" + 'filename' => 'image2.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image2.png" }) end - it "doesn't return attachments from other nodes" do - expect(retrieved_attachments.map{ |json| json['filename'] }).not_to include "image3.png" + it 'doesn\'t return attachments from other nodes' do + expect(retrieved_attachments.map { |json| json['filename'] }).not_to include 'image3.png' end end - describe "GET /api/nodes/:node_id/attachments/:filename" do + describe 'GET /api/nodes/:node_id/attachments/:filename' do before do - create(:attachment, filename: "image.png", node: node) + create(:attachment, filename: 'image.png', node: node) get "/api/nodes/#{node.id}/attachments/image.png", env: @env end - it "responds with HTTP code 200" do + it 'responds with HTTP code 200' do expect(response.status).to eq 200 end - it "responds with HTTP code 404 when not found" do + it 'responds with HTTP code 404 when not found' do get "/api/nodes/#{node.id}/attachments/image_ko.png", env: @env expect(response.status).to eq 404 json_response = JSON.parse(response.body) - expect(json_response["message"]).to eq "Couldn't find attachment with filename 'image_ko.png'" + expect(json_response['message']).to eq "Couldn't find attachment with filename 'image_ko.png'" end - it "returns JSON information about the attachment" do + it 'returns JSON information about the attachment' do retrieved_attachment = JSON.parse(response.body) expect(retrieved_attachment.keys).to match_array(%w[filename link]) - expect(retrieved_attachment["filename"]).to eq "image.png" - expect(retrieved_attachment["link"]).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image.png" + expect(retrieved_attachment['filename']).to eq 'image.png' + expect(retrieved_attachment['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image.png" end end - describe "POST /api/nodes/:node_id/attachments" do + describe 'POST /api/nodes/:node_id/attachments' do let(:post_attachment) { file = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) params = { files: [file] } @@ -119,13 +121,13 @@ post url , params: params, env: @env } - it "returns 201 when file saved" do + it 'returns 201 when file saved' do post_attachment expect(response.status).to eq 201 expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'rails.png'))).to be true end - it "returns 422 when no file saved" do + it 'returns 422 when no file saved' do url = "/api/nodes/#{node.id}/attachments" post url , params: {}, env: @env @@ -133,11 +135,11 @@ expect(File.exist?(Attachment.pwd.join(node.id.to_s))).to be false end - it "auto-renames the upload if an attachment with the same name already exists" do + it 'auto-renames the upload if an attachment with the same name already exists' do node_attachments = Attachment.pwd.join(node.id.to_s) - FileUtils.mkdir_p( node_attachments ) + FileUtils.mkdir_p(node_attachments) - create(:attachment, filename: "rails.png", node: node) + create(:attachment, filename: 'rails.png', node: node) expect(Dir["#{node_attachments}/*"].count).to eq(1) post_attachment @@ -145,7 +147,7 @@ expect(Dir["#{node_attachments}/*"].count).to eq(2) end - it "returns JSON information about the attachments" do + it 'returns JSON information about the attachments' do file1 = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) file2 = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) params = { files: [file1, file2] } @@ -155,96 +157,96 @@ retrieved_attachments = JSON.parse(response.body) - attachment_0 = retrieved_attachments.detect { |n| n["filename"] == "rails.png" } - attachment_1 = retrieved_attachments.detect { |n| n["filename"] == "rails_copy-01.png" } + attachment_0 = retrieved_attachments.detect { |n| n['filename'] == 'rails.png' } + attachment_1 = retrieved_attachments.detect { |n| n['filename'] == 'rails_copy-01.png' } expect(attachment_0.keys).to match_array %w[filename link] - expect(attachment_0["filename"]).to eq "rails.png" - expect(attachment_0["link"]).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails.png" + expect(attachment_0['filename']).to eq 'rails.png' + expect(attachment_0['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails.png" expect(attachment_1.keys).to match_array %w[filename link] - expect(attachment_1["filename"]).to eq "rails_copy-01.png" - expect(attachment_1["link"]).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails_copy-01.png" + expect(attachment_1['filename']).to eq 'rails_copy-01.png' + expect(attachment_1['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails_copy-01.png" end end - describe "PUT /api/nodes/:node_id/attachments/:filename" do + describe 'PUT /api/nodes/:node_id/attachments/:filename' do before do - create(:attachment, filename: "image.png", node: node) + create(:attachment, filename: 'image.png', node: node) end let(:url) { "/api/nodes/#{node.id}/attachments/image.png" } let(:put_attachment) { put url, params: params.to_json, env: @env } - context "when content_type header = application/json" do - include_context "content_type: application/json" + context 'when content_type header = application/json' do + include_context 'content_type: application/json' - context "with params for a valid attachment" do - let(:params) { { attachment: { filename: "image_renamed.png" } } } + context 'with params for a valid attachment' do + let(:params) { { attachment: { filename: 'image_renamed.png' } } } - it "responds with HTTP code 200 if attachment exists" do + it 'responds with HTTP code 200 if attachment exists' do put_attachment expect(response.status).to eq 200 end - it "responds with HTTP code 404 if attachemnt doesn't exist" do + it 'responds with HTTP code 404 if attachment doesn\'t exist' do bad_url = "/api/nodes/#{node.id}/attachments/image_ko.png" put bad_url, params: params, env: @env expect(response.status).to eq(400) end - it "updates the attachment" do + it 'updates the attachment' do put_attachment expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be false expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image_renamed.png'))).to be true end - it "returns the attributes of the updated attachment as JSON" do + it 'returns the attributes of the updated attachment as JSON' do put_attachment retrieved_attachment = JSON.parse(response.body) - expect(retrieved_attachment["filename"]).to eq "image_renamed.png" + expect(retrieved_attachment['filename']).to eq 'image_renamed.png' end - it "responds with HTTP code 422 if attachment already exists" do - create(:attachment, filename: "image_renamed.png", node: node) + it 'responds with HTTP code 422 if attachment already exists' do + create(:attachment, filename: 'image_renamed.png', node: node) put_attachment expect(response.status).to eq(422) retrieved_attachment = JSON.parse(response.body) - expect(retrieved_attachment["filename"]).to eq "image.png" + expect(retrieved_attachment['filename']).to eq 'image.png' end end - context "with params for an invalid attachment" do - let(:params) { { attachment: { filename: "a"*65536 } } } # too long + context 'with params for an invalid attachment' do + let(:params) { { attachment: { filename: 'a' * 65536 } } } # too long - it "responds with HTTP code 422" do + it 'responds with HTTP code 422' do put_attachment expect(response.status).to eq 422 end - it "doesn't update the attachment" do + it 'doesn\'t update the attachment' do put_attachment expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image_renamed.png'))).to be false end end - context "when no :attachment param is sent" do + context 'when no :attachment param is sent' do let(:params) { {} } - it "doesn't update the attachment" do + it 'doesn\'t update the attachment' do put_attachment expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true end - it "responds with HTTP code 422" do + it 'responds with HTTP code 422' do put_attachment expect(response.status).to eq 422 end end - context "when invalid JSON is sent" do - it "responds with HTTP code 400" do + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do json_payload = '{"attachment":{"filename":"A malformed label", , }}' put url, params: json_payload, env: @env expect(response.status).to eq(400) @@ -252,10 +254,10 @@ end end - context "when JSON is not sent" do - let(:params) { { attachment: { filename: "image_renamed.jpg" } } } + context 'when JSON is not sent' do + let(:params) { { attachment: { filename: 'image_renamed.jpg' } } } - it "responds with HTTP code 415" do + it 'responds with HTTP code 415' do put url, params: params, env: @env expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true expect(response.status).to eq 415 @@ -263,30 +265,30 @@ end end - describe "DELETE /api/nodes/:node_id/attachments/:filename" do - let(:attachment) { "image.png" } + describe 'DELETE /api/nodes/:node_id/attachments/:filename' do + let(:attachment) { 'image.png' } let(:delete_attachment) do create(:attachment, filename: attachment, node: node) delete "/api/nodes/#{node.id}/attachments/#{attachment}", env: @env end - it "deletes the attachment" do + it 'deletes the attachment' do delete_attachment expect(File.exist?(Attachment.pwd.join(node.id.to_s, attachment))).to\ - be false + be false end - it "responds with error code 200" do + it 'responds with error code 200' do delete_attachment expect(response.status).to eq(200) end - it "returns JSON with a success message" do + it 'returns JSON with a success message' do delete_attachment parsed_response = JSON.parse(response.body) - expect(parsed_response["message"]).to eq\ - "Resource deleted successfully" + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' end end end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v1/evidence_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v1/evidence_spec.rb index 712fe81f3..0b2e15774 100644 --- a/engines/dradis-api/spec/requests/dradis/ce/api/v1/evidence_spec.rb +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v1/evidence_spec.rb @@ -4,7 +4,9 @@ include_context 'project scoped API' include_context 'https' + include_context 'versioned API' + let(:api_version) { 1 } let(:node) { create(:node, project: current_project) } let(:issue) { create(:issue, node: current_project.issue_library) } @@ -38,7 +40,7 @@ Evidence.create!(node: node, content: "#[a]#\nA", issue: @issues[0]), Evidence.create!(node: node, content: "#[b]#\nB", issue: @issues[1]), Evidence.create!(node: node, content: "#[c]#\nC", issue: @issues[2]), - ] << create_list(:evidence, 30, issue: @issues[3], node: node) + ] + create_list(:evidence, 30, issue: @issues[3], node: node) @other_evidence = create(:evidence, issue: issue, node: current_project.issue_library) get "/api/nodes/#{node.id}/evidence?#{params}", env: @env end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v1/issues_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v1/issues_spec.rb index 51744626a..fa8f01f34 100644 --- a/engines/dradis-api/spec/requests/dradis/ce/api/v1/issues_spec.rb +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v1/issues_spec.rb @@ -4,6 +4,9 @@ include_context 'project scoped API' include_context 'https' + include_context 'versioned API' + + let(:api_version) { 1 } context 'as unauthenticated user' do [ diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v1/nodes_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v1/nodes_spec.rb index bc69fc7ef..5f4c89f81 100644 --- a/engines/dradis-api/spec/requests/dradis/ce/api/v1/nodes_spec.rb +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v1/nodes_spec.rb @@ -4,6 +4,9 @@ include_context 'project scoped API' include_context 'https' + include_context 'versioned API' + + let(:api_version) { 1 } context 'as unauthenticated user' do [ @@ -41,7 +44,7 @@ let(:params) { '' } it 'retrieves all the nodes' do - retrieved_node_labels = @retrieved_nodes.map{ |p| p['label'] } + retrieved_node_labels = @retrieved_nodes.map { |p| p['label'] } expect(@retrieved_nodes.count).to eq(@nodes.count) expect(retrieved_node_labels).to match_array(@node_labels) @@ -86,7 +89,7 @@ end it 'creates a new node' do - expect{valid_post}.to change{Node.count}.by(1) + expect { valid_post }.to change { Node.count }.by(1) expect(response.status).to eq(201) retrieved_node = JSON.parse(response.body) @@ -104,7 +107,7 @@ include_examples 'creates an Activity', :create, Node it 'throws 415 unless JSON is sent' do - params = { node: { } } + params = { node: {} } post '/api/nodes', params: params, env: @env expect(response.status).to eq(415) end @@ -116,7 +119,7 @@ end it 'throws 422 if no :node param is sent' do - params = { } + params = {} post '/api/nodes', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') expect(response.status).to eq(422) end @@ -170,7 +173,7 @@ end it 'throws 422 if no :node param is sent' do - params = { } + params = {} put "/api/nodes/#{ node.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') expect(response.status).to eq(422) end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v1/notes_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v1/notes_spec.rb index 7333ca823..1720367a7 100644 --- a/engines/dradis-api/spec/requests/dradis/ce/api/v1/notes_spec.rb +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v1/notes_spec.rb @@ -4,7 +4,9 @@ include_context 'project scoped API' include_context 'https' + include_context 'versioned API' + let(:api_version) { 1 } let(:node) { create(:node, project: current_project) } context 'as unauthenticated user' do diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/attachments_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/attachments_spec.rb new file mode 100644 index 000000000..fc2b43625 --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/attachments_spec.rb @@ -0,0 +1,293 @@ +require 'rails_helper' + +describe 'Attachments API' do + include_context 'project scoped API' + include_context 'https' + + let(:node) { create(:node, project: current_project) } + + context 'as unauthenticated user' do + [ + ['get', '/api/nodes/1/attachments/'], + ['get', '/api/nodes/1/attachments/image.jpg'], + ['post', '/api/nodes/1/attachments/'], + ['put', '/api/nodes/1/attachments/image.jpg'], + ['patch', '/api/nodes/1/attachments/image.jpg'], + ['delete', '/api/nodes/1/attachments/image.jpg'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + before(:each) do + FileUtils.rm_rf Dir[Attachment.pwd.join('*')] until Dir[Attachment.pwd.join('*')].count == 0 + end + + after(:all) do + FileUtils.rm_rf Dir[Attachment.pwd.join('*')] + end + + describe 'GET /api/nodes/:node_id/attachments' do + before do + @attachments = ['image0.png', 'image1.png', 'image2.png'] + @attachments.each do |attachment| + create(:attachment, filename: attachment, node: node) + end + + # an attachment in another node + create(:attachment, filename: 'image3.png', node: create(:node, project: current_project)) + + get "/api/nodes/#{node.id}/attachments", env: @env + end + + let(:retrieved_attachments) { JSON.parse(response.body) } + + it 'responds with HTTP code 200' do + expect(response.status).to eq(200) + end + + it 'retrieves all the attachments for the given node' do + expect(retrieved_attachments.count).to eq @attachments.count + retrieved_filenames = retrieved_attachments.map { |json| json['filename'] } + expect(retrieved_filenames).to match_array(@attachments) + end + + it 'returns JSON information about attachments' do + attachment_0 = retrieved_attachments.detect { |n| n['filename'] == 'image0.png' } + attachment_1 = retrieved_attachments.detect { |n| n['filename'] == 'image1.png' } + attachment_2 = retrieved_attachments.detect { |n| n['filename'] == 'image2.png' } + + expect(attachment_0).to eq({ + 'filename' => 'image0.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image0.png" + }) + expect(attachment_1).to eq({ + 'filename' => 'image1.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image1.png" + }) + expect(attachment_2).to eq({ + 'filename' => 'image2.png', + 'link' => "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image2.png" + }) + end + + it 'doesn\'t return attachments from other nodes' do + expect(retrieved_attachments.map { |json| json['filename'] }).not_to include 'image3.png' + end + end + + describe 'GET /api/nodes/:node_id/attachments/:filename' do + before do + create(:attachment, filename: 'image.png', node: node) + + get "/api/nodes/#{node.id}/attachments/image.png", env: @env + end + + it 'responds with HTTP code 200' do + expect(response.status).to eq 200 + end + + it 'responds with HTTP code 404 when not found' do + get "/api/nodes/#{node.id}/attachments/image_ko.png", env: @env + expect(response.status).to eq 404 + json_response = JSON.parse(response.body) + expect(json_response['message']).to eq "Couldn't find attachment with filename 'image_ko.png'" + end + + it 'returns JSON information about the attachment' do + retrieved_attachment = JSON.parse(response.body) + expect(retrieved_attachment.keys).to match_array(%w[filename link]) + expect(retrieved_attachment['filename']).to eq 'image.png' + expect(retrieved_attachment['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/image.png" + end + end + + describe 'POST /api/nodes/:node_id/attachments' do + let(:post_attachment) { + file = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) + params = { files: [file] } + url = "/api/nodes/#{node.id}/attachments" + + post url , params: params, env: @env + } + + it 'returns 201 when file saved' do + post_attachment + expect(response.status).to eq 201 + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'rails.png'))).to be true + end + + it 'returns 422 when no file saved' do + url = "/api/nodes/#{node.id}/attachments" + post url , params: {}, env: @env + + expect(response.status).to eq 422 + expect(File.exist?(Attachment.pwd.join(node.id.to_s))).to be false + end + + it 'auto-renames the upload if an attachment with the same name already exists' do + node_attachments = Attachment.pwd.join(node.id.to_s) + FileUtils.mkdir_p(node_attachments) + + create(:attachment, filename: 'rails.png', node: node) + expect(Dir["#{node_attachments}/*"].count).to eq(1) + + post_attachment + + expect(Dir["#{node_attachments}/*"].count).to eq(2) + end + + it 'returns JSON information about the attachments' do + file1 = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) + file2 = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/rails.png')) + params = { files: [file1, file2] } + url = "/api/nodes/#{node.id}/attachments" + + post url , params: params, env: @env + + retrieved_attachments = JSON.parse(response.body) + + attachment_0 = retrieved_attachments.detect { |n| n['filename'] == 'rails.png' } + attachment_1 = retrieved_attachments.detect { |n| n['filename'] == 'rails_copy-01.png' } + + expect(attachment_0.keys).to match_array %w[filename link] + expect(attachment_0['filename']).to eq 'rails.png' + expect(attachment_0['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails.png" + expect(attachment_1.keys).to match_array %w[filename link] + expect(attachment_1['filename']).to eq 'rails_copy-01.png' + expect(attachment_1['link']).to eq "/projects/#{current_project.id}/nodes/#{node.id}/attachments/rails_copy-01.png" + end + end + + describe 'PUT /api/nodes/:node_id/attachments/:filename' do + before do + create(:attachment, filename: 'image.png', node: node) + end + + let(:url) { "/api/nodes/#{node.id}/attachments/image.png" } + let(:put_attachment) { put url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid attachment' do + let(:params) { { attachment: { filename: 'image_renamed.png' } } } + + it 'responds with HTTP code 200 if attachment exists' do + put_attachment + expect(response.status).to eq 200 + end + + it 'responds with HTTP code 404 if attachemnt doesn\'t exist' do + bad_url = "/api/nodes/#{node.id}/attachments/image_ko.png" + put bad_url, params: params, env: @env + expect(response.status).to eq(400) + end + + it 'updates the attachment' do + put_attachment + + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be false + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image_renamed.png'))).to be true + end + + it 'returns the attributes of the updated attachment as JSON' do + put_attachment + retrieved_attachment = JSON.parse(response.body) + expect(retrieved_attachment['filename']).to eq 'image_renamed.png' + end + + it 'responds with HTTP code 422 if attachment already exists' do + create(:attachment, filename: 'image_renamed.png', node: node) + put_attachment + expect(response.status).to eq(422) + retrieved_attachment = JSON.parse(response.body) + expect(retrieved_attachment['filename']).to eq 'image.png' + end + end + + context 'with params for an invalid attachment' do + let(:params) { { attachment: { filename: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + put_attachment + expect(response.status).to eq 422 + end + + it 'doesn\'t update the attachment' do + put_attachment + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image_renamed.png'))).to be false + end + end + + context 'when no :attachment param is sent' do + let(:params) { {} } + + it 'doesn\'t update the attachment' do + put_attachment + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true + end + + it 'responds with HTTP code 422' do + put_attachment + expect(response.status).to eq 422 + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"attachment":{"filename":"A malformed label", , }}' + put url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + let(:params) { { attachment: { filename: 'image_renamed.jpg' } } } + + it 'responds with HTTP code 415' do + put url, params: params, env: @env + expect(File.exist?(Attachment.pwd.join(node.id.to_s, 'image.png'))).to be true + expect(response.status).to eq 415 + end + end + end + + describe 'DELETE /api/nodes/:node_id/attachments/:filename' do + let(:attachment) { 'image.png' } + + let(:delete_attachment) do + create(:attachment, filename: attachment, node: node) + delete "/api/nodes/#{node.id}/attachments/#{attachment}", env: @env + end + + it 'deletes the attachment' do + delete_attachment + expect(File.exist?(Attachment.pwd.join(node.id.to_s, attachment))).to\ + be false + end + + it 'responds with error code 200' do + delete_attachment + expect(response.status).to eq(200) + end + + it 'returns JSON with a success message' do + delete_attachment + parsed_response = JSON.parse(response.body) + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' + end + end + end +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/boards_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/boards_spec.rb new file mode 100644 index 000000000..512331f7b --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/boards_spec.rb @@ -0,0 +1,195 @@ +require 'rails_helper' + +describe 'Boards API' do + + include_context 'project scoped API' + include_context 'https' + + context 'as unauthenticated user' do + [ + ['get', '/api/boards/'], + ['get', '/api/boards/1'], + ['post', '/api/boards/'], + ['put', '/api/boards/1'], + ['patch', '/api/boards/1'], + ['delete', '/api/boards/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + describe 'GET /api/boards' do + before do + @boards = create_list(:board, 30, project: current_project).sort_by(&:updated_at) + @board_names = @boards.map(&:name) + + get "/api/boards?#{params}", env: @env + + expect(response.status).to eq(200) + @retrieved_boards = JSON.parse(response.body) + end + + context 'without params' do + let(:params) { '' } + + it 'retrieves all the boards' do + retrieved_board_names = @retrieved_boards.map { |p| p['name'] } + + expect(@retrieved_boards.count).to eq(@boards.count) + expect(retrieved_board_names).to match_array(@board_names) + end + end + + context 'with params' do + let(:params) { 'page=2' } + + it 'retrieves the paginated boards' do + expect(@retrieved_boards.count).to eq(5) + end + end + end + + describe 'GET /api/boards/:id' do + it 'retrieves a specific board' do + board = create(:board, name: 'Existing Board', project: current_project) + + get "/api/boards/#{ board.id }", env: @env + expect(response.status).to eq(200) + + retrieved_board = JSON.parse(response.body) + expect(retrieved_board['name']).to eq board.name + end + end + + describe 'POST /api/boards' do + let(:valid_post) do + post '/api/boards', params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + let(:valid_params) do + { + board: { + name: 'New Board', + node_id: current_project.methodology_library.id + } + } + end + + it 'creates a new board' do + expect { valid_post }.to change { Board.count }.by(1) + expect(response.status).to eq(201) + + retrieved_board = JSON.parse(response.body) + + expect(response.location).to eq(dradis_api.board_url(retrieved_board['id'])) + + valid_params[:board].each do |attr, value| + expect(retrieved_board[attr.to_s]).to eq value + end + end + + # Activity shared example was originally written for feature requests and + # expects a 'submit_form' let variable to be defined: + let(:submit_form) { valid_post } + include_examples 'creates an Activity', :create, Board + + it 'throws 415 unless JSON is sent' do + params = { board: {} } + post '/api/boards', params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if board is invalid' do + params = { board: { name: '' } } + post '/api/boards', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 422 if no :board param is sent' do + params = {} + post '/api/boards', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 400 if invalid JSON is sent' do + invalid_tokens = ', , ' + json_payload = %Q|{"board":{"name":"A malformed name"#{ invalid_tokens }}}| + post '/api/boards', params: json_payload, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(400) + end + end + + describe 'PUT /api/boards/:id' do + + let(:board) { create(:board, name: 'Existing Board', project: current_project) } + + let(:valid_put) do + put "/api/boards/#{ board.id }", params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + let(:valid_params) { { board: { name: 'Updated Board' } } } + + it 'updates a board' do + valid_put + expect(response.status).to eq(200) + expect(current_project.boards.find(board.id).name).to eq valid_params[:board][:name] + retrieved_board = JSON.parse(response.body) + expect(retrieved_board['name']).to eq valid_params[:board][:name] + end + + let(:submit_form) { valid_put } + let(:model) { board } + include_examples 'creates an Activity', :update + + it 'throws 415 unless JSON is sent' do + params = { board: { name: 'Bad Board' } } + put "/api/boards/#{ board.id }", params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if board is invalid' do + params = { board: { name: '' } } + put "/api/boards/#{ board.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 422 if no :board param is sent' do + params = {} + put "/api/boards/#{ board.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 400 if invalid JSON is sent' do + invalid_tokens = ', , ' + json_payload = %Q|{"board":{"name":"A malformed name"#{ invalid_tokens }}}| + put "/api/boards/#{ board.id }", params: json_payload, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(400) + end + + end + + describe 'DELETE /api/boards/:id' do + + let(:board) { create(:board, name: 'Existing Board', project: current_project) } + let(:delete_board) { delete "/api/boards/#{ board.id }", env: @env } + + it 'deletes a board' do + delete_board + expect(response.status).to eq(200) + + expect { current_project.boards.find(board.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + let(:model) { board } + let(:submit_form) { delete_board } + include_examples 'creates an Activity', :destroy + end + end + +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/cards_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/cards_spec.rb new file mode 100644 index 000000000..30ebb252e --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/cards_spec.rb @@ -0,0 +1,304 @@ +require 'rails_helper' + +describe 'Cards API' do + + include_context 'project scoped API' + include_context 'https' + + let(:board) { create(:board, project: current_project) } + let(:list) { create(:list, board: board) } + + context 'as unauthenticated user' do + [ + ['get', '/api/boards/1/lists/1/cards'], + ['get', '/api/boards/1/lists/1/cards/1'], + ['post', '/api/boards/1/lists/1/cards'], + ['put', '/api/boards/1/lists/1/cards/1'], + ['patch', '/api/boards/1/lists/1/cards/1'], + ['delete', '/api/boards/1/lists/1/cards/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + describe 'GET /api/boards/:board_id/lists/:list_id/cards' do + before do + @cards = [ + Card.create!(list: list, description: "#[a]#\nA", name: 'Card A'), + Card.create!(list: list, description: "#[b]#\nB", name: 'Card B'), + Card.create!(list: list, description: "#[c]#\nC", name: 'Card C'), + ] + create_list(:card, 30, list: list) + @other_card = create(:card, list: create(:list, board: board)) + get "/api/boards/#{board.id}/lists/#{list.id}/cards?#{params}", env: @env + end + + let(:retrieved_cards) { JSON.parse(response.body) } + + context 'without params' do + let(:params) { '' } + + it 'responds with HTTP code 200' do + expect(response.status).to eq(200) + end + + it 'retrieves all the cards for the given list' do + expect(retrieved_cards.count).to eq 33 + card_names = retrieved_cards.map { |json| json['name'] } + expect(card_names).to match_array @cards.map(&:name) + end + + it 'returns JSON data about the cards\'s fields' do + ev_0 = retrieved_cards.find { |n| n['id'] == @cards[0].id } + ev_1 = retrieved_cards.find { |n| n['id'] == @cards[1].id } + ev_2 = retrieved_cards.find { |n| n['id'] == @cards[2].id } + + expect(ev_0['fields'].keys).to \ + match_array (@cards[0].local_fields.keys << 'a') + expect(ev_0['fields']['a']).to eq 'A' + + expect(ev_1['fields'].keys).to \ + match_array (@cards[2].local_fields.keys << 'b') + expect(ev_1['fields']['b']).to eq 'B' + + expect(ev_2['fields'].keys).to \ + match_array (@cards[2].local_fields.keys << 'c') + expect(ev_2['fields']['c']).to eq 'C' + end + + it 'doesn\'t return cards from other lists' do + retrieved_ids = retrieved_cards.map { |n| n['id'] } + expect(retrieved_ids).not_to include @other_card.id + end + end + + context 'with params' do + let(:params) { 'page=2' } + + it 'returns the paginated card' do + expect(retrieved_cards.count).to eq 8 + + end + end + end + + describe 'GET /api/boards/:board_id/lists/:list_id/cards/:id' do + before do + @card = list.cards.create!( + description: "#[foo]#\nbar\n#[fizz]#\nbuzz", + name: 'My rspec card', + ) + get "/api/boards/#{board.id}/lists/#{list.id}/cards/#{@card.id}", env: @env + end + + it 'responds with HTTP code 200' do + expect(response.status).to eq 200 + end + + it 'returns JSON information about the card' do + retrieved_card = JSON.parse(response.body) + expect(retrieved_card['id']).to eq @card.id + expect(retrieved_card['name']).to eq @card.name + expect(retrieved_card['fields'].keys).to match_array( + @card.local_fields.keys + %w(fizz foo) + ) + expect(retrieved_card['fields']['foo']).to eq 'bar' + expect(retrieved_card['fields']['fizz']).to eq 'buzz' + end + end + + describe 'POST /api/boards/:board_id/lists/:list_id/cards' do + let(:url) { "/api/boards/#{board.id}/lists/#{list.id}/cards" } + let(:post_card) { post url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid card' do + let(:params) { { card: { description: 'New description', name: 'New name' } } } + + it 'responds with HTTP code 201' do + post_card + expect(response.status).to eq 201 + end + + it 'creates an card' do + expect { post_card }.to change { list.cards.count } + new_card = list.cards.last + expect(new_card.description).to eq 'New description' + expect(new_card.name).to eq 'New name' + end + + let(:submit_form) { post_card } + include_examples 'creates an Activity', :create, Card + include_examples 'sets the whodunnit', :create, Card + end + + context 'with params for an invalid card' do + let(:params) { { card: { description: 'New card' } } } # no name or list + + it 'responds with HTTP code 422' do + post_card + expect(response.status).to eq 422 + end + + it "doesn't create a card" do + expect { post_card }.not_to change { Card.count } + end + end + + context 'when no :card param is sent' do + let(:params) { {} } + + it "doesn't create an card" do + expect { post_card }.not_to change { Card.count } + end + + it 'responds with HTTP code 422' do + post_card + expect(response.status).to eq(422) + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"card":{"name":"A malformed name", , }}' + post url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + it 'responds with HTTP code 415' do + params = { card: {} } + post url, params: params, env: @env + expect(response.status).to eq(415) + end + end + end + + describe 'PUT /api/boards/:board_id/lists/:list_id/cards/:id' do + let(:card) do + create(:card, list: list, description: 'My description') + end + + let(:url) { "/api/boards/#{board.id}/lists/#{list.id}/cards/#{card.id}" } + let(:put_card) { put url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid card' do + let(:params) { { card: { description: 'New description' } } } + + it 'responds with HTTP code 200' do + put_card + expect(response.status).to eq 200 + end + + it 'updates the card' do + put_card + expect(card.reload.description).to eq 'New description' + end + + it 'returns the attributes of the updated card as JSON' do + put_card + retrieved_card = JSON.parse(response.body) + expect(retrieved_card['description']).to eq 'New description' + end + + let(:submit_form) { put_card } + let(:model) { card } + include_examples 'creates an Activity', :update + include_examples 'sets the whodunnit', :update + end + + context 'with params for an invalid card' do + let(:params) { { card: { description: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + put_card + expect(response.status).to eq 422 + end + + it "doesn't update the card" do + expect { put_card }.not_to change { card.reload.attributes } + end + end + + context 'when no :card param is sent' do + let(:params) { {} } + + it "doesn't update the card" do + expect { put_card }.not_to change { card.reload.attributes } + end + + it 'responds with HTTP code 422' do + put_card + expect(response.status).to eq 422 + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"card":{"name":"A malformed name", , }}' + put url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + let(:params) { { card: { description: 'New Card' } } } + + it 'responds with HTTP code 415' do + expect { put url, params: params, env: @env }.not_to change { card.reload.attributes } + expect(response.status).to eq 415 + end + end + end + + describe 'DELETE /api/boards/:board_id/lists/:list_id/cards/:id' do + # the Card model adds Board info to PaperTrail, which by default is + # disabled during :testing + before { PaperTrail.enabled = true } + after { PaperTrail.enabled = false } + + let(:card) { create(:card, list: list, description: 'My Card') } + + let(:delete_card) do + delete "/api/boards/#{board.id}/lists/#{list.id}/cards/#{card.id}", env: @env + end + + it 'deletes the card' do + card_id = card.id + delete_card + expect(Card.find_by_id(card_id)).to be_nil + end + + it 'responds with error code 200' do + delete_card + expect(response.status).to eq(200) + end + + it 'returns JSON with a success message' do + delete_card + parsed_response = JSON.parse(response.body) + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' + end + + let(:submit_form) { delete_card } + let(:model) { card } + include_examples 'creates an Activity', :destroy + end + end +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/evidence_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/evidence_spec.rb new file mode 100644 index 000000000..81049940a --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/evidence_spec.rb @@ -0,0 +1,307 @@ +require 'rails_helper' + +describe 'Evidence API' do + + include_context 'project scoped API' + include_context 'https' + + let(:node) { create(:node, project: current_project) } + let(:issue) { create(:issue, node: current_project.issue_library) } + + context 'as unauthenticated user' do + [ + ['get', '/api/nodes/1/evidence/'], + ['get', '/api/nodes/1/evidence/1'], + ['post', '/api/nodes/1/evidence/'], + ['put', '/api/nodes/1/evidence/1'], + ['patch', '/api/nodes/1/evidence/1'], + ['delete', '/api/nodes/1/evidence/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + let(:category) { create(:category) } + + describe 'GET /api/nodes/:node_id/evidence' do + before do + @issues = create_list(:issue, 4, node: current_project.issue_library) + @evidence = [ + Evidence.create!(node: node, content: "#[a]#\nA", issue: @issues[0]), + Evidence.create!(node: node, content: "#[b]#\nB", issue: @issues[1]), + Evidence.create!(node: node, content: "#[c]#\nC", issue: @issues[2]), + ] + create_list(:evidence, 30, issue: @issues[3], node: node) + @other_evidence = create(:evidence, issue: issue, node: current_project.issue_library) + get "/api/nodes/#{node.id}/evidence?#{params}", env: @env + end + + let(:retrieved_evidence) { JSON.parse(response.body) } + + context 'without params' do + let(:params) { '' } + + it 'responds with HTTP code 200' do + expect(response.status).to eq(200) + end + + it 'retrieves all the evidence for the given node' do + expect(retrieved_evidence.count).to eq 33 + issue_titles = retrieved_evidence.map { |json| json['issue']['title'] }.uniq + expect(issue_titles).to match_array @issues.map(&:title) + end + + it 'returns JSON data about the evidence\'s fields and issue' do + ev_0 = retrieved_evidence.find { |n| n['issue']['id'] == @issues[0].id } + ev_1 = retrieved_evidence.find { |n| n['issue']['id'] == @issues[1].id } + ev_2 = retrieved_evidence.find { |n| n['issue']['id'] == @issues[2].id } + + expect(ev_0['fields'].keys).to \ + match_array (@evidence[0].local_fields.keys << 'a') + expect(ev_0['fields']['a']).to eq 'A' + expect(ev_0['issue']['title']).to eq @issues[0].title + expect(ev_1['fields'].keys).to \ + match_array (@evidence[2].local_fields.keys << 'b') + expect(ev_1['fields']['b']).to eq 'B' + expect(ev_1['issue']['title']).to eq @issues[1].title + expect(ev_2['fields'].keys).to \ + match_array (@evidence[2].local_fields.keys << 'c') + expect(ev_2['fields']['c']).to eq 'C' + expect(ev_2['issue']['title']).to eq @issues[2].title + end + + it 'doesn\'t return evidence from other nodes' do + retrieved_ids = retrieved_evidence.map { |n| n['id'] } + expect(retrieved_ids).not_to include @other_evidence.id + end + end + + context 'with params' do + let(:params) { 'page=2' } + + it 'returns the paginated evidence' do + expect(retrieved_evidence.count).to eq 8 + + end + end + end + + describe 'GET /api/nodes/:node_id/evidence/:id' do + before do + @issue = create(:issue, node: current_project.issue_library) + @evidence = node.evidence.create!( + content: "#[foo]#\nbar\n#[fizz]#\nbuzz", + issue: @issue, + ) + get "/api/nodes/#{node.id}/evidence/#{@evidence.id}", env: @env + end + + it 'responds with HTTP code 200' do + expect(response.status).to eq 200 + end + + it 'returns JSON information about the evidence' do + retrieved_evidence = JSON.parse(response.body) + expect(retrieved_evidence['id']).to eq @evidence.id + expect(retrieved_evidence['author']).to eq @evidence.author + expect(retrieved_evidence['fields'].keys).to match_array( + @evidence.local_fields.keys + %w(fizz foo) + ) + expect(retrieved_evidence['fields']['foo']).to eq 'bar' + expect(retrieved_evidence['fields']['fizz']).to eq 'buzz' + expect(retrieved_evidence['issue']['id']).to eq @issue.id + expect(retrieved_evidence['issue']['title']).to eq @issue.title + end + end + + describe 'POST /api/nodes/:node_id/evidence' do + let(:url) { "/api/nodes/#{node.id}/evidence" } + let(:issue) { create(:issue, node: current_project.issue_library) } + let(:post_evidence) { post url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid evidence' do + let(:params) { { evidence: { content: 'New evidence', issue_id: issue.id } } } + + it 'responds with HTTP code 201' do + post_evidence + expect(response.status).to eq 201 + end + + it 'creates an evidence' do + expect { post_evidence }.to change { node.evidence.count } + new_evidence = node.evidence.last + expect(new_evidence.content).to eq 'New evidence' + expect(new_evidence.issue).to eq issue + end + + let(:submit_form) { post_evidence } + include_examples 'creates an Activity', :create, Evidence + include_examples 'sets the whodunnit', :create, Evidence + end + + context 'with params for an invalid evidence' do + let(:params) { { evidence: { content: 'New evidence' } } } # no issue + + it 'responds with HTTP code 422' do + post_evidence + expect(response.status).to eq 422 + end + + it "doesn't create an evidence" do + expect { post_evidence }.not_to change { Evidence.count } + end + end + + context 'when no :evidence param is sent' do + let(:params) { {} } + + it "doesn't create an evidence" do + expect { post_evidence }.not_to change { Evidence.count } + end + + it 'responds with HTTP code 422' do + post_evidence + expect(response.status).to eq(422) + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"evidence":{"label":"A malformed label", , }}' + post url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + it 'responds with HTTP code 415' do + params = { evidence: {} } + post url, params: params, env: @env + expect(response.status).to eq(415) + end + end + end + + describe 'PUT /api/nodes/:node_id/evidence/:id' do + let(:evidence) do + create(:evidence, node: node, content: 'My content', issue: issue) + end + + let(:url) { "/api/nodes/#{node.id}/evidence/#{evidence.id}" } + let(:put_evidence) { put url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid evidence' do + let(:params) { { evidence: { content: 'New content' } } } + + it 'responds with HTTP code 200' do + put_evidence + expect(response.status).to eq 200 + end + + it 'updates the evidence' do + put_evidence + expect(evidence.reload.content).to eq 'New content' + end + + it 'returns the attributes of the updated evidence as JSON' do + put_evidence + retrieved_evidence = JSON.parse(response.body) + expect(retrieved_evidence['content']).to eq 'New content' + end + + let(:submit_form) { put_evidence } + let(:model) { evidence } + include_examples 'creates an Activity', :update + include_examples 'sets the whodunnit', :update + end + + context 'with params for an invalid evidence' do + let(:params) { { evidence: { content: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + put_evidence + expect(response.status).to eq 422 + end + + it "doesn't update the evidence" do + expect { put_evidence }.not_to change { evidence.reload.attributes } + end + end + + context 'when no :evidence param is sent' do + let(:params) { {} } + + it "doesn't update the evidence" do + expect { put_evidence }.not_to change { evidence.reload.attributes } + end + + it 'responds with HTTP code 422' do + put_evidence + expect(response.status).to eq 422 + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"evidence":{"label":"A malformed label", , }}' + put url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + let(:params) { { evidence: { content: 'New Evidence' } } } + + it 'responds with HTTP code 415' do + expect { put url, params: params, env: @env }.not_to change { evidence.reload.attributes } + expect(response.status).to eq 415 + end + end + end + + describe 'DELETE /api/nodes/:node_id/evidence/:id' do + let(:evidence) { create(:evidence, node: node, content: 'My Evidence', issue: issue) } + + let(:delete_evidence) do + delete "/api/nodes/#{node.id}/evidence/#{evidence.id}", env: @env + end + + it 'deletes the evidence' do + evidence_id = evidence.id + delete_evidence + expect(Evidence.find_by_id(evidence_id)).to be_nil + end + + it 'responds with error code 200' do + delete_evidence + expect(response.status).to eq(200) + end + + it 'returns JSON with a success message' do + delete_evidence + parsed_response = JSON.parse(response.body) + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' + end + + let(:submit_form) { delete_evidence } + let(:model) { evidence } + include_examples 'creates an Activity', :destroy + end + end +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/issues_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/issues_spec.rb new file mode 100644 index 000000000..51744626a --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/issues_spec.rb @@ -0,0 +1,220 @@ +require 'rails_helper' + +describe 'Issues API' do + + include_context 'project scoped API' + include_context 'https' + + context 'as unauthenticated user' do + [ + ['get', '/api/issues/'], + ['get', '/api/issues/1'], + ['post', '/api/issues/'], + ['put', '/api/issues/1'], + ['patch', '/api/issues/1'], + ['delete', '/api/issues/1'], + ].each do |verb, url| + describe '#{verb.upcase} #{url}' do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + describe 'GET /api/issues' do + before(:each) do + @issues = create_list(:issue, 30, node: current_project.issue_library).sort_by(&:title) + + get path, env: @env + expect(response.status).to eq(200) + + @retrieved_issues = JSON.parse(response.body) + end + + context 'without params' do + let(:path) { '/api/issues' } + + it 'retrieves all the issues' do + titles = @issues.map(&:title) + retrieved_titles = @retrieved_issues.map { |json| json['title'] } + + expect(@retrieved_issues.count).to eq(@issues.count) + expect(retrieved_titles).to match_array(titles) + end + + it 'includes fields' do + @retrieved_issues.each do |issue| + expect(issue).to have_key('id') + db_issue = Issue.find(issue['id']) + + expect(issue['fields']).not_to be_empty + expect(issue['fields'].count).to eq(db_issue.fields.count) + expect(issue['fields'].keys).to eq(db_issue.fields.keys) + end + end + end + + context 'with params' do + let(:path) { '/api/issues?page=2' } + + it 'retrieves the paginated issues' do + expect(@retrieved_issues.count).to eq(5) + end + end + end + + describe 'GET /api/issue/:id' do + before(:each) do + @issue = create(:issue, :tagged_issue, node: current_project.issue_library, text: "#[a]#\nb\n\n#[c]#\nd\n\n#[e]#\nf\n\n") + + get "/api/issues/#{ @issue.id }", env: @env + expect(response.status).to eq(200) + + @retrieved_issue = JSON.parse(response.body) + end + + it 'retrieves a specific issue' do + expect(@retrieved_issue['id']).to eq @issue.id + end + + it 'includes fields' do + expect(@retrieved_issue['fields']).not_to be_empty + expect(@retrieved_issue['fields'].keys).to eq @issue.fields.keys + expect(@retrieved_issue['fields'].count).to eq @issue.fields.count + end + + it 'includes tags' do + tag = @issue.tags.first + expect(@retrieved_issue['tags']).to eq [{ 'color' => tag.color, 'display_name' => tag.display_name }] + end + + it 'includes the author' do + expect(@retrieved_issue['author']).to eq @issue.author + end + + it 'includes the state' do + expect(@retrieved_issue['state']).to eq @issue.state + end + end + + describe 'POST /api/issues' do + let(:valid_params) do + { issue: { text: "#[Title]#\nRspec issue\n\n#[c]#\nd\n\n#[e]#\nf\n\n", state: 'ready_for_review' } } + end + let(:valid_post) do + post '/api/issues', params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + + it 'creates a new issue' do + expect { valid_post }.to change { current_project.issues.count }.by(1) + expect(response.status).to eq(201) + retrieved_issue = JSON.parse(response.body) + expect(retrieved_issue['text']).to eq valid_params[:issue][:text] + end + + it 'tags the issue from the Tags field' do + tag_name = '!2ca02c_info' + valid_params[:issue][:text] << "#[Tags]#\n\n#{tag_name}\n\n" + + expect { valid_post }.to change { current_project.issues.count }.by(1) + expect(response.status).to eq(201) + + retrieved_issue = JSON.parse(response.body) + database_issue = current_project.issues.find(retrieved_issue['id']) + + expect(database_issue.tag_list).to eq(tag_name) + end + + it 'sets the issue state' do + expect { valid_post }.to change { current_project.issues.count }.by(1) + expect(response.status).to eq(201) + + retrieved_issue = JSON.parse(response.body) + expect(retrieved_issue['state']).to eq('ready_for_review') + end + + let(:submit_form) { valid_post } + include_examples 'creates an Activity', :create, Issue + include_examples 'sets the whodunnit', :create, Issue + + it 'throws 415 unless JSON is sent' do + params = { issue: { name: 'Bad Issue' } } + post '/api/issues', params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if issue is invalid' do + params = { issue: { text: 'A' * (65535 + 1) } } + expect { + post '/api/issues', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + }.not_to change { current_project.issues.count } + expect(response.status).to eq(422) + end + + it 'throws 422 if state is invalid' do + params = { issue: { text: "#[Title]#\nIssue test\n", state: 'fakestate' } } + expect { + post '/api/issues', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + }.not_to change { current_project.issues.count } + expect(response.status).to eq(422) + end + end + + describe 'PUT /api/issues/:id' do + let(:issue) { create(:issue, node: current_project.issue_library, text: 'Existing Issue') } + let(:valid_params) { { issue: { text: 'Updated Issue', state: 'ready_for_review' } } } + let(:valid_put) do + put "/api/issues/#{issue.id}", params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + + it 'updates a issue' do + valid_put + expect(response.status).to eq(200) + + expect(current_project.issues.find(issue.id).text).to eq valid_params[:issue][:text] + + retrieved_issue = JSON.parse(response.body) + expect(retrieved_issue['text']).to eq valid_params[:issue][:text] + expect(retrieved_issue['state']).to eq valid_params[:issue][:state] + end + + let(:submit_form) { valid_put } + let(:model) { issue } + include_examples 'creates an Activity', :update + include_examples 'sets the whodunnit', :update + + it 'throws 415 unless JSON is sent' do + params = { issue: { text: 'Bad issuet' } } + put "/api/issues/#{ issue.id }", params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if issue is invalid' do + params = { issue: { text: 'B' * (65535 + 1) } } + put "/api/issues/#{ issue.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + end + + describe 'DELETE /api/issue/:id' do + let(:issue) { create(:issue, node: current_project.issue_library) } + + let(:delete_issue) { delete "/api/issues/#{ issue.id }", env: @env } + + it 'deletes a issue' do + delete_issue + expect(response.status).to eq(200) + expect { current_project.issues.find(issue.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + let(:submit_form) { delete_issue } + let(:model) { issue } + include_examples 'creates an Activity', :destroy + end + end +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/lists_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/lists_spec.rb new file mode 100644 index 000000000..dd2173c46 --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/lists_spec.rb @@ -0,0 +1,271 @@ +require 'rails_helper' + +describe 'Lists API' do + + include_context 'project scoped API' + include_context 'https' + + let(:board) { create(:board, project: current_project) } + + context 'as unauthenticated user' do + [ + ['get', '/api/boards/1/lists'], + ['get', '/api/boards/1/lists/1'], + ['post', '/api/boards/1/lists'], + ['put', '/api/boards/1/lists/1'], + ['patch', '/api/boards/1/lists/1'], + ['delete', '/api/boards/1/lists/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + describe 'GET /api/boards/:board_id/lists' do + before do + @lists = create_list(:list, 30, board: board) + @other_list = create(:list, board: create(:board, project: current_project)) + get "/api/boards/#{board.id}/lists?#{params}", env: @env + end + + let(:retrieved_lists) { JSON.parse(response.body) } + + context 'without params' do + let(:params) { '' } + + it 'responds with HTTP code 200' do + expect(response.status).to eq(200) + end + + it 'retrieves all the lists for the given board' do + expect(retrieved_lists.count).to eq 30 + list_names = retrieved_lists.map { |json| json['name'] } + expect(list_names).to match_array @lists.map(&:name) + end + + it 'returns JSON data about the lists' do + li_0 = retrieved_lists.find { |n| n['id'] == @lists[0].id } + li_1 = retrieved_lists.find { |n| n['id'] == @lists[1].id } + li_2 = retrieved_lists.find { |n| n['id'] == @lists[2].id } + + expect(li_0['name']).to eq(@lists[0].name) + expect(li_1['name']).to eq(@lists[1].name) + expect(li_2['name']).to eq(@lists[2].name) + end + + it 'doesn\'t return lists from other boards' do + retrieved_ids = retrieved_lists.map { |n| n['id'] } + expect(retrieved_ids).not_to include @other_list.id + end + end + + context 'with params' do + let(:params) { 'page=2' } + + it 'returns the paginated evidence' do + expect(retrieved_lists.count).to eq 5 + end + end + end + + describe 'GET /api/boards/:board_id/lists/:list_id' do + before do + @list = create(:list, board: board) + get "/api/boards/#{board.id}/lists/#{@list.id}", env: @env + end + + it 'responds with HTTP code 200' do + expect(response.status).to eq 200 + end + + it 'returns JSON information about the list' do + retrieved_list = JSON.parse(response.body) + expect(retrieved_list['id']).to eq @list.id + expect(retrieved_list['name']).to eq @list.name + end + end + + describe 'POST /api/boards/:board_id/lists' do + let(:url) { "/api/boards/#{board.id}/lists" } + let(:post_list) { post url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid list' do + let(:params) { { list: { name: 'New name' } } } + + it 'responds with HTTP code 201' do + post_list + expect(response.status).to eq 201 + end + + it 'creates an list' do + expect { post_list }.to change { board.lists.count } + new_list = board.lists.last + expect(new_list.name).to eq 'New name' + end + + let(:submit_form) { post_list } + include_examples 'creates an Activity', :create, List + end + + context 'with params for an invalid list' do + let(:params) { { list: {} } } # no name + + it 'responds with HTTP code 422' do + post_list + expect(response.status).to eq 422 + end + + it "doesn't create a list" do + expect { post_list }.not_to change { List.count } + end + end + + context 'when no :list param is sent' do + let(:params) { {} } + + it "doesn't create an list" do + expect { post_list }.not_to change { List.count } + end + + it 'responds with HTTP code 422' do + post_list + expect(response.status).to eq(422) + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"list":{"name":"A malformed name", , }}' + post url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + it 'responds with HTTP code 415' do + params = { list: {} } + post url, params: params, env: @env + expect(response.status).to eq(415) + end + end + end + + describe 'PUT /api/boards/:board_id/lists/:id' do + let(:list) { create(:list, board: board) } + let(:url) { "/api/boards/#{board.id}/lists/#{list.id}" } + let(:put_list) { put url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid list' do + let(:params) { { list: { name: 'New name' } } } + + it 'responds with HTTP code 200' do + put_list + expect(response.status).to eq 200 + end + + it 'updates the list' do + put_list + expect(list.reload.name).to eq 'New name' + end + + it 'returns the attributes of the updated list as JSON' do + put_list + retrieved_list = JSON.parse(response.body) + expect(retrieved_list['name']).to eq 'New name' + end + + let(:submit_form) { put_list } + let(:model) { list } + include_examples 'creates an Activity', :update + end + + context 'with params for an invalid list' do + let(:params) { { list: { name: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + put_list + expect(response.status).to eq 422 + end + + it "doesn't update the list" do + expect { put_list }.not_to change { list.reload.attributes } + end + end + + context 'when no :list param is sent' do + let(:params) { {} } + + it "doesn't update the list" do + expect { put_list }.not_to change { list.reload.attributes } + end + + it 'responds with HTTP code 422' do + put_list + expect(response.status).to eq 422 + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"list":{"name":"A malformed name", , }}' + put url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + let(:params) { { list: { name: 'New List' } } } + + it 'responds with HTTP code 415' do + expect { put url, params: params, env: @env }.not_to change { list.reload.attributes } + expect(response.status).to eq 415 + end + end + end + + describe 'DELETE /api/boards/:board_id/lists/:id' do + let(:list) { create(:list, board: board) } + + let(:delete_list) do + delete "/api/boards/#{board.id}/lists/#{list.id}", env: @env + end + + it 'deletes the list' do + list_id = list.id + delete_list + expect(List.find_by_id(list_id)).to be_nil + end + + it 'responds with error code 200' do + delete_list + expect(response.status).to eq(200) + end + + it 'returns JSON with a success message' do + delete_list + parsed_response = JSON.parse(response.body) + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' + end + + let(:submit_form) { delete_list } + let(:model) { list } + include_examples 'creates an Activity', :destroy + end + end +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/nodes_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/nodes_spec.rb new file mode 100644 index 000000000..19679a9af --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/nodes_spec.rb @@ -0,0 +1,205 @@ +require 'rails_helper' + +describe 'Nodes API' do + + include_context 'project scoped API' + include_context 'https' + + context 'as unauthenticated user' do + [ + ['get', '/api/nodes/'], + ['get', '/api/nodes/1'], + ['post', '/api/nodes/'], + ['put', '/api/nodes/1'], + ['patch', '/api/nodes/1'], + ['delete', '/api/nodes/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + describe 'GET /api/nodes' do + before do + @nodes = create_list(:node, 30, project: current_project).sort_by(&:updated_at) + @node_labels = @nodes.map(&:label) + + get "/api/nodes?#{params}", env: @env + + expect(response.status).to eq(200) + @retrieved_nodes = JSON.parse(response.body) + end + + context 'without params' do + let(:params) { '' } + + it 'retrieves all the nodes' do + retrieved_node_labels = @retrieved_nodes.map { |p| p['label'] } + + expect(@retrieved_nodes.count).to eq(@nodes.count) + expect(retrieved_node_labels).to match_array(@node_labels) + end + end + + context 'with params' do + let(:params) { 'page=2' } + + it 'retrieves the paginated nodes' do + expect(@retrieved_nodes.count).to eq(5) + end + end + end + + describe 'GET /api/nodes/:id' do + it 'retrieves a specific node' do + node = create(:node, label: 'Existing Node', project: current_project) + + get "/api/nodes/#{ node.id }", env: @env + expect(response.status).to eq(200) + + retrieved_node = JSON.parse(response.body) + expect(retrieved_node['label']).to eq node.label + end + end + + describe 'POST /api/nodes' do + let!(:parent_node_id) { current_project.plugin_parent_node.id } + let(:valid_post) do + post '/api/nodes', params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + let(:valid_params) do + { + node: { + label: 'New Node', + type_id: Node::Types::HOST, + parent_id: parent_node_id, + position: 3 + } + } + end + + it 'creates a new node' do + expect { valid_post }.to change { Node.count }.by(1) + expect(response.status).to eq(201) + + retrieved_node = JSON.parse(response.body) + + expect(response.location).to eq(dradis_api.node_url(retrieved_node['id'])) + + valid_params[:node].each do |attr, value| + expect(retrieved_node[attr.to_s]).to eq value + end + end + + # Activity shared example was originally written for feature requests and + # expects a 'submit_form' let variable to be defined: + let(:submit_form) { valid_post } + include_examples 'creates an Activity', :create, Node + + it 'throws 415 unless JSON is sent' do + params = { node: {} } + post '/api/nodes', params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if node is invalid' do + params = { node: { label: '' } } + post '/api/nodes', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 422 if no :node param is sent' do + params = {} + post '/api/nodes', params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 400 if invalid JSON is sent' do + invalid_tokens = ', , ' + json_payload = %Q|{"node":{"label":"A malformed label"#{ invalid_tokens }}}| + post '/api/nodes', params: json_payload, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(400) + end + end + + describe 'PUT /api/nodes/:id' do + + let(:node) { create(:node, label: 'Existing Node', project: current_project) } + + let(:valid_put) do + put "/api/nodes/#{ node.id }", params: valid_params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + end + let(:valid_params) { { node: { label: 'Updated Node' } } } + + it 'updates a node' do + valid_put + expect(response.status).to eq(200) + expect(current_project.nodes.find(node.id).label).to eq valid_params[:node][:label] + retrieved_node = JSON.parse(response.body) + expect(retrieved_node['label']).to eq valid_params[:node][:label] + end + + let(:submit_form) { valid_put } + let(:model) { node } + include_examples 'creates an Activity', :update + + it 'assigns :type_id' do + params = { node: { type_id: Node::Types::HOST } } + put "/api/nodes/#{ node.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(200) + expect(current_project.nodes.find(node.id).type_id).to eq(Node::Types::HOST) + end + + it 'throws 415 unless JSON is sent' do + params = { node: { label: 'Bad Node' } } + put "/api/nodes/#{ node.id }", params: params, env: @env + expect(response.status).to eq(415) + end + + it 'throws 422 if node is invalid' do + params = { node: { label: '' } } + put "/api/nodes/#{ node.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 422 if no :node param is sent' do + params = {} + put "/api/nodes/#{ node.id }", params: params.to_json, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(422) + end + + it 'throws 400 if invalid JSON is sent' do + invalid_tokens = ', , ' + json_payload = %Q|{"node":{"label":"A malformed label"#{ invalid_tokens }}}| + put "/api/nodes/#{ node.id }", params: json_payload, env: @env.merge('CONTENT_TYPE' => 'application/json') + expect(response.status).to eq(400) + end + + end + + describe 'DELETE /api/nodes/:id' do + + let(:node) { create(:node, label: 'Existing Node', project: current_project) } + let(:delete_node) { delete "/api/nodes/#{ node.id }", env: @env } + + it 'deletes a node' do + delete_node + expect(response.status).to eq(200) + + expect { current_project.nodes.find(node.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + let(:model) { node } + let(:submit_form) { delete_node } + include_examples 'creates an Activity', :destroy + end + end + +end diff --git a/engines/dradis-api/spec/requests/dradis/ce/api/v3/notes_spec.rb b/engines/dradis-api/spec/requests/dradis/ce/api/v3/notes_spec.rb new file mode 100644 index 000000000..7333ca823 --- /dev/null +++ b/engines/dradis-api/spec/requests/dradis/ce/api/v3/notes_spec.rb @@ -0,0 +1,319 @@ +require 'rails_helper' + +describe 'Notes API' do + + include_context 'project scoped API' + include_context 'https' + + let(:node) { create(:node, project: current_project) } + + context 'as unauthenticated user' do + [ + ['get', '/api/nodes/1/notes/'], + ['get', '/api/nodes/1/notes/1'], + ['post', '/api/nodes/1/notes/'], + ['put', '/api/nodes/1/notes/1'], + ['patch', '/api/nodes/1/notes/1'], + ['delete', '/api/nodes/1/notes/1'], + ].each do |verb, url| + describe "#{verb.upcase} #{url}" do + it 'throws 401' do + send(verb, url, params: {}, env: @env) + expect(response.status).to eq 401 + end + end + end + end + + context 'as authorized user' do + include_context 'authorized API user' + + let(:category) { create(:category) } + + describe 'GET /api/nodes/:node_id/notes' do + before do + @notes = [ + create(:note, node: node, text: "#[Title]#\nNote 0\n\n#[foo]#\nbar"), + create(:note, node: node, text: "#[Title]#\nNote 1\n\n#[uno]#\none"), + create(:note, node: node, text: "#[Title]#\nNote 2\n\n#[dos]#\ntwo"), + ] + create_list(:note, 30, node: node) + @other_note = create( + :note, node: create(:node, project: current_project), text: "#[Title]#\nOther Note" + ) + get "/api/nodes/#{node.id}/notes?#{params}", env: @env + end + + let(:retrieved_notes) { JSON.parse(response.body) } + + context 'without params' do + let (:params) { '' } + + it 'responds with HTTP code 200' do + expect(response.status).to eq(200) + end + + it 'retrieves all the notes for the given node' do + expect(retrieved_notes.count).to eq 33 + retrieved_titles = retrieved_notes.map { |json| json['title'] } + expect(retrieved_titles).to match_array(@notes.map(&:title)) + end + + it 'includes fields' do + note_0 = retrieved_notes.detect { |n| n['title'] == 'Note 0' } + note_1 = retrieved_notes.detect { |n| n['title'] == 'Note 1' } + note_2 = retrieved_notes.detect { |n| n['title'] == 'Note 2' } + + expect(note_0['fields'].keys).to match_array %w[Title foo] + expect(note_0['fields']['foo']).to eq 'bar' + expect(note_1['fields'].keys).to match_array %w[Title uno] + expect(note_1['fields']['uno']).to eq 'one' + expect(note_2['fields'].keys).to match_array %w[Title dos] + expect(note_2['fields']['dos']).to eq 'two' + end + + it 'doesn\'t return notes from other nodes' do + retrieved_ids = retrieved_notes.map { |n| n['id'] } + expect(retrieved_ids).not_to include @other_note.id + end + end + + context 'with params' do + let (:params) { 'page=2' } + + it 'retrieves the paginated notes for the given node' do + expect(retrieved_notes.count).to eq 8 + end + end + end + + describe 'GET /api/nodes/:node_id/notes/:id' do + before do + @note = node.notes.create!( + text: "#[Title]#\nMy note\n#[foo]#\nbar\n#[fizz]#\nbuzz", + category: category, + ) + get "/api/nodes/#{node.id}/notes/#{@note.id}", env: @env + end + + it 'responds with HTTP code 200' do + expect(response.status).to eq 200 + end + + it 'returns JSON information about the note' do + retrieved_note = JSON.parse(response.body) + expect(retrieved_note['id']).to eq @note.id + expect(retrieved_note['title']).to eq 'My note' + expect(retrieved_note['category_id']).to eq category.id + expect(retrieved_note['author']).to eq @note.author + expect(retrieved_note['fields'].keys).to match_array( + %w[foo fizz Title] + ) + expect(retrieved_note['fields']['foo']).to eq 'bar' + expect(retrieved_note['fields']['fizz']).to eq 'buzz' + end + end + + describe 'POST /api/nodes/:node_id/notes' do + let(:url) { "/api/nodes/#{node.id}/notes" } + let(:post_note) { post url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid note' do + let(:params) { { note: { text: 'New note' } } } + + it 'responds with HTTP code 201' do + post_note + expect(response.status).to eq 201 + end + + let(:submit_form) { post_note } + include_examples 'creates an Activity', :create, Note + include_examples 'sets the whodunnit', :create, Note + + context 'specifying a category' do + before { params[:note][:category_id] = category.id } + + it 'creates a note with the given node & category' do + expect { post_note }.to change { node.notes.count }.by(1) + note = node.notes.last + expect(note.category).to eq category + end + + it 'returns the attributes of the new note as JSON' do + post_note + retrieved_note = JSON.parse(response.body) + params[:note].each do |attr, value| + expect(retrieved_note[attr.to_s]).to eq value + end + expect(response.location).to eq( + dradis_api.node_note_path(node.id, retrieved_note['id']) + ) + end + end + + context 'and category is not specified' do + it 'creates a note with the given node & default category' do + expect { post_note }.to change { node.notes.count }.by(1) + expect(node.notes.last.category).to eq Category.default + end + end + end + + context 'with params for an invalid note' do + let(:params) { { note: { text: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + post_note + expect(response.status).to eq 422 + end + + it 'doesn\'t create a note' do + expect { post_note }.not_to change { Note.count } + end + end + + context 'when no :note param is sent' do + let(:params) { {} } + + it 'doesn\'t create a note' do + expect { post_note }.not_to change { Note.count } + end + + it 'responds with HTTP code 422' do + post_note + expect(response.status).to eq(422) + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"note":{"label":"A malformed label", , }}' + post url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + it 'responds with HTTP code 415' do + params = { note: {} } + post url, params: params, env: @env + expect(response.status).to eq(415) + end + end + end + + describe 'PUT /api/nodes/:node_id/notes/:id' do + let(:note) do + create(:note, node: node, text: 'My text') + end + + let(:url) { "/api/nodes/#{node.id}/notes/#{note.id}" } + let(:put_note) { put url, params: params.to_json, env: @env } + + context 'when content_type header = application/json' do + include_context 'content_type: application/json' + + context 'with params for a valid note' do + let(:params) { { note: { text: 'New text' } } } + + it 'responds with HTTP code 200' do + put_note + expect(response.status).to eq 200 + end + + it 'updates the note' do + put_note + expect(note.reload.text).to eq 'New text' + end + + let(:submit_form) { put_note } + let(:model) { note } + include_examples 'creates an Activity', :update + include_examples 'sets the whodunnit', :update + + it 'returns the attributes of the updated note as JSON' do + put_note + retrieved_note = JSON.parse(response.body) + expect(retrieved_note['text']).to eq 'New text' + end + end + + context 'with params for an invalid note' do + let(:params) { { note: { text: 'a' * 65536 } } } # too long + + it 'responds with HTTP code 422' do + put_note + expect(response.status).to eq 422 + end + + it 'doesn\'t update the note' do + expect { put_note }.not_to change { note.reload.attributes } + end + end + + context 'when no :note param is sent' do + let(:params) { {} } + + it 'doesn\'t update the note' do + expect { put_note }.not_to change { note.reload.attributes } + end + + it 'responds with HTTP code 422' do + put_note + expect(response.status).to eq 422 + end + end + + context 'when invalid JSON is sent' do + it 'responds with HTTP code 400' do + json_payload = '{"note":{"label":"A malformed label", , }}' + put url, params: json_payload, env: @env + expect(response.status).to eq(400) + end + end + end + + context 'when JSON is not sent' do + let(:params) { { note: { text: 'New Note' } } } + + it 'responds with HTTP code 415' do + expect { put url, params: params, env: @env }.not_to change { note.reload.attributes } + expect(response.status).to eq 415 + end + end + end + + describe 'DELETE /api/nodes/:node_id/notes/:id' do + let(:note) { create(:note, node: node, text: 'My Note') } + + let(:delete_note) do + delete "/api/nodes/#{node.id}/notes/#{note.id}", env: @env + end + + it 'deletes the note' do + note_id = note.id + delete_note + expect(Note.find_by_id(note_id)).to be_nil + end + + it 'responds with error code 200' do + delete_note + expect(response.status).to eq(200) + end + + let(:submit_form) { delete_note } + let(:model) { note } + include_examples 'creates an Activity', :destroy + + it 'returns JSON with a success message' do + delete_note + parsed_response = JSON.parse(response.body) + expect(parsed_response['message']).to eq\ + 'Resource deleted successfully' + end + end + end +end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index f5d45092f..17064b8b2 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :list do - sequence(:name){ |n| "List-#{n}" } + sequence(:name) { |n| "List-#{n}" } association :board end end