require "minitest/autorun" require_relative "../lib/backlog" # Minimal stand-ins for the planka-api gem's error hierarchy, so tests never # require (or need) the real gem installed — only the shape our code # rescues against. module Planka class Error < StandardError; end class ValidationError < Error; end end module BacklogTestHelpers # A hand-rolled fake of the subset of Planka::Client's resource surface # BoardEnsurer touches: projects, boards, lists, labels. In-memory only, # no network. Mirrors the gem's real return shapes (string-keyed hashes). class FakePlankaClient Project = Struct.new(:id, :name, :type, :ownerProjectManagerId, :managers, keyword_init: true) Board = Struct.new(:id, :project_id, :name, keyword_init: true) List = Struct.new(:id, :board_id, :name, :type, :position, keyword_init: true) Label = Struct.new(:id, :board_id, :name, :color, keyword_init: true) Card = Struct.new(:id, :list_id, :board_id, :name, :type, :position, :description, keyword_init: true) attr_reader :projects, :boards, :lists, :labels, :cards def initialize(reject_colors: []) @next_id = 0 @projects_store = [] @boards_store = [] @lists_store = [] @labels_store = [] @cards_store = [] @card_labels_store = [] @projects = ProjectsResource.new(self) @boards = BoardsResource.new(self) @lists = ListsResource.new(self) @labels = LabelsResource.new(self, reject_colors: reject_colors) @cards = CardsResource.new(self) @comments = CommentsResource.new(self) end attr_reader :comments def next_id = (@next_id += 1).to_s attr_reader :projects_store, :boards_store, :lists_store, :labels_store, :cards_store, :card_labels_store def attach_label(card_id, label_id) @card_labels_store << { "id" => next_id, "cardId" => card_id, "labelId" => label_id } end class ProjectsResource def initialize(client) = @client = client def list = @client.projects_store.map { |p| stringify(p) } def create(attrs) project = FakePlankaClient::Project.new( id: @client.next_id, name: attrs[:name], type: attrs[:type], ownerProjectManagerId: @client.next_id, managers: [] ) @client.projects_store << project stringify(project) end def update(id, attrs) project = @client.projects_store.find { |p| p.id == id } project.ownerProjectManagerId = attrs[:ownerProjectManagerId] if attrs.key?(:ownerProjectManagerId) stringify(project) end def add_manager(project_id, user_id) project = @client.projects_store.find { |p| p.id == project_id } project.managers << user_id { "id" => @client.next_id, "projectId" => project_id, "userId" => user_id } end private def stringify(p) = { "id" => p.id, "name" => p.name, "type" => p.type } end class BoardsResource def initialize(client) = @client = client def list(project_id) @client.boards_store.select { |b| b.project_id == project_id }.map { |b| stringify(b) } end def create(project_id, attrs) board = FakePlankaClient::Board.new(id: @client.next_id, project_id: project_id, name: attrs[:name]) @client.boards_store << board stringify(board) end def update(id, attrs) board = @client.boards_store.find { |b| b.id == id } board.name = attrs[:name] if attrs.key?(:name) stringify(board) end def get(id) board = @client.boards_store.find { |b| b.id == id } { "item" => stringify(board), "included" => { "lists" => @client.lists.list(id), "labels" => @client.labels.list(id), "cards" => @client.cards.all_for_board(id), "cardLabels" => @client.card_labels_store.dup } } end private def stringify(b) = { "id" => b.id, "projectId" => b.project_id, "name" => b.name } end class ListsResource def initialize(client) = @client = client def list(board_id) @client.lists_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) } end def create(board_id, attrs) raise ArgumentError, "list create requires type" unless attrs[:type] list = FakePlankaClient::List.new(id: @client.next_id, board_id: board_id, name: attrs[:name], type: attrs[:type], position: attrs[:position]) @client.lists_store << list stringify(list) end private def stringify(l) { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "type" => l.type, "position" => l.position } end end class CardsResource def initialize(client) = @client = client def create(list_id, attrs) raise ArgumentError, "card create requires type" unless attrs[:type] list = @client.lists_store.find { |l| l.id == list_id } card = FakePlankaClient::Card.new( id: @client.next_id, list_id: list_id, board_id: list.board_id, name: attrs[:name], type: attrs[:type], position: attrs[:position], description: attrs[:description] ) @client.cards_store << card stringify(card) end def all_for_board(board_id) @client.cards_store.select { |c| c.board_id == board_id }.map { |c| stringify(c) } end private def stringify(c) { "id" => c.id, "listId" => c.list_id, "boardId" => c.board_id, "name" => c.name, "type" => c.type, "position" => c.position, "description" => c.description } end end class LabelsResource def initialize(client, reject_colors: []) @client = client @reject_colors = reject_colors end def list(board_id) @client.labels_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) } end def create(board_id, attrs) raise Planka::ValidationError, "color #{attrs[:color]} rejected" if @reject_colors.include?(attrs[:color]) label = FakePlankaClient::Label.new(id: @client.next_id, board_id: board_id, name: attrs[:name], color: attrs[:color]) @client.labels_store << label stringify(label) end def attach_to_card(card_id, label_id) @client.attach_label(card_id, label_id) @client.card_labels_store.last end private def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "color" => l.color } end class CommentsResource def initialize(client) @client = client @store = [] end attr_reader :store def create(card_id, attrs) comment = { "id" => @client.next_id, "cardId" => card_id, "text" => attrs[:text] } @store << comment comment end end end end