diff --git a/app/controllers/api/v1/aspects_controller.rb b/app/controllers/api/v1/aspects_controller.rb index 066fb8bbe..2a73a73e0 100644 --- a/app/controllers/api/v1/aspects_controller.rb +++ b/app/controllers/api/v1/aspects_controller.rb @@ -12,8 +12,10 @@ module Api end def index - aspects = current_user.aspects.map {|a| AspectPresenter.new(a).as_api_json(false) } - render json: aspects + aspects_query = current_user.aspects + aspects_page = index_pager(aspects_query).response + aspects_page[:data] = aspects_page[:data].map {|a| AspectPresenter.new(a).as_api_json(false) } + render json: aspects_page end def show diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 8f60537c7..7e0f5137e 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -44,6 +44,14 @@ module Api def current_user current_token ? current_token.authorization.user : nil end + + def index_pager(query) + Api::Paging::RestPaginatorBuilder.new(query, request).index_pager(params) + end + + def time_pager(query) + Api::Paging::RestPaginatorBuilder.new(query, request).time_pager(params) + end end end end diff --git a/app/controllers/api/v1/comments_controller.rb b/app/controllers/api/v1/comments_controller.rb index 33f32ab01..7b00151c4 100644 --- a/app/controllers/api/v1/comments_controller.rb +++ b/app/controllers/api/v1/comments_controller.rb @@ -29,8 +29,11 @@ module Api end def index - comments = comment_service.find_for_post(params[:post_id]) - render json: comments.map {|x| comment_as_json(x) } + comments_query = comment_service.find_for_post(params[:post_id]) + params[:after] = Time.utc(1900).iso8601 if params.permit(:before, :after).empty? + comments_page = time_pager(comments_query).response + comments_page[:data] = comments_page[:data].map {|x| comment_as_json(x) } + render json: comments_page end def destroy diff --git a/app/controllers/api/v1/contacts_controller.rb b/app/controllers/api/v1/contacts_controller.rb index 3fc4dcf2e..f72c68805 100644 --- a/app/controllers/api/v1/contacts_controller.rb +++ b/app/controllers/api/v1/contacts_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "api/paging/index_paginator" + module Api module V1 class ContactsController < Api::V1::BaseController @@ -16,8 +18,12 @@ module Api end def index - contacts = aspects_membership_service.contacts_in_aspect(params[:aspect_id]) - render json: contacts.map {|c| ContactPresenter.new(c, current_user).as_api_json_without_contact } + contacts_query = aspects_membership_service.contacts_in_aspect(params[:aspect_id]) + contacts_page = index_pager(contacts_query).response + contacts_page[:data] = contacts_page[:data].map do |c| + ContactPresenter.new(c, current_user).as_api_json_without_contact + end + render json: contacts_page end def create diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 894077a41..99659f040 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -21,10 +21,11 @@ module Api params.permit(:only_after, :only_unread) mapped_params = {} mapped_params[:only_after] = params[:only_after] if params.has_key?(:only_after) - mapped_params[:unread] = params[:only_unread] if params.has_key?(:only_unread) - conversations = conversation_service.all_for_user(mapped_params) - render json: conversations.map {|x| conversation_as_json(x) } + conversations_query = conversation_service.all_for_user(mapped_params) + conversations_page = pager(conversations_query, "conversations.created_at").response + conversations_page[:data] = conversations_page[:data].map {|x| conversation_as_json(x) } + render json: conversations_page end def show @@ -34,7 +35,7 @@ module Api def create params.require(%i[subject body recipients]) - recipient_ids = JSON.parse(params[:recipients]).map {|p| Person.find_from_guid_or_username(id: p).id } + recipient_ids = params[:recipients].map {|p| Person.find_from_guid_or_username(id: p).id } conversation = conversation_service.build( params[:subject], params[:body], @@ -58,12 +59,18 @@ module Api head :no_content end + private + def conversation_service ConversationService.new(current_user) end def conversation_as_json(conversation) - ConversationPresenter.new(conversation).as_api_json + ConversationPresenter.new(conversation, current_user).as_api_json + end + + def pager(query, sort_field) + Api::Paging::RestPaginatorBuilder.new(query, request).time_pager(params, sort_field) end end end diff --git a/app/controllers/api/v1/likes_controller.rb b/app/controllers/api/v1/likes_controller.rb index 9e7b8d0b1..46525b951 100644 --- a/app/controllers/api/v1/likes_controller.rb +++ b/app/controllers/api/v1/likes_controller.rb @@ -20,8 +20,10 @@ module Api end def show - likes = like_service.find_for_post(params[:post_id]) - render json: likes.map {|x| like_json(x) } + likes_query = like_service.find_for_post(params[:post_id]) + likes_page = index_pager(likes_query).response + likes_page[:data] = likes_page[:data].map {|x| like_json(x) } + render json: likes_page end def create diff --git a/app/controllers/api/v1/messages_controller.rb b/app/controllers/api/v1/messages_controller.rb index 119fe91ab..a1b92a203 100644 --- a/app/controllers/api/v1/messages_controller.rb +++ b/app/controllers/api/v1/messages_controller.rb @@ -29,12 +29,13 @@ module Api def index conversation = conversation_service.find!(params[:conversation_id]) conversation.set_read(current_user) - render( - json: conversation.messages.map {|x| message_json(x) }, - status: :created - ) + messages_page = index_pager(conversation.messages).response + messages_page[:data] = messages_page[:data].map {|x| message_json(x) } + render json: messages_page end + private + def conversation_service ConversationService.new(current_user) end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index c2d806b38..3e7fbd445 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -27,8 +27,12 @@ module Api def index after_date = Date.iso8601(params[:only_after]) if params.has_key?(:only_after) - notifications = service.index(params[:only_unread], after_date) - render json: notifications.map {|note| NotificationPresenter.new(note, default_serializer_options).as_api_json } + notifications_query = service.index(params[:only_unread], after_date) + notifications_page = time_pager(notifications_query).response + notifications_page[:data] = notifications_page[:data].map do |note| + NotificationPresenter.new(note, default_serializer_options).as_api_json + end + render json: notifications_page rescue ArgumentError render json: I18n.t("api.endpoint_errors.notifications.cant_process"), status: :unprocessable_entity end diff --git a/app/controllers/api/v1/photos_controller.rb b/app/controllers/api/v1/photos_controller.rb index a9a13e633..45d8d147c 100644 --- a/app/controllers/api/v1/photos_controller.rb +++ b/app/controllers/api/v1/photos_controller.rb @@ -16,8 +16,9 @@ module Api end def index - photos = current_user.photos - render json: photos.map {|p| PhotoPresenter.new(p).as_api_json(true) } + photos_page = time_pager(current_user.photos).response + photos_page[:data] = photos_page[:data].map {|photo| PhotoPresenter.new(photo).as_api_json(true) } + render json: photos_page end def show diff --git a/app/controllers/api/v1/reshares_controller.rb b/app/controllers/api/v1/reshares_controller.rb index 903b870d4..6781ef77c 100644 --- a/app/controllers/api/v1/reshares_controller.rb +++ b/app/controllers/api/v1/reshares_controller.rb @@ -20,10 +20,15 @@ module Api end def show - reshares = reshare_service.find_for_post(params[:post_id]).map do |r| - {guid: r.guid, author: PersonPresenter.new(r.author).as_api_json} + reshares_query = reshare_service.find_for_post(params[:post_id]) + reshares_page = index_pager(reshares_query).response + reshares_page[:data] = reshares_page[:data].map do |r| + { + guid: r.guid, + author: PersonPresenter.new(r.author).as_api_json + } end - render json: reshares + render json: reshares_page end def create @@ -34,6 +39,8 @@ module Api render json: PostPresenter.new(reshare, current_user).as_api_response end + private + def reshare_service @reshare_service ||= ReshareService.new(current_user) end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index dd56e1ca6..42e7a846b 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -14,17 +14,27 @@ module Api def user_index parameters = params.permit(:tag, :name_or_handle) raise RuntimeError if parameters.keys.length != 1 - people = if params.has_key?(:tag) + people_query = if params.has_key?(:tag) Person.profile_tagged_with(params[:tag]) else Person.search(params[:name_or_handle], current_user) end - render json: people.map {|p| PersonPresenter.new(p).as_api_json } + user_page = index_pager(people_query).response + user_page[:data] = user_page[:data].map {|p| PersonPresenter.new(p).as_api_json } + render json: user_page end def post_index - posts = Stream::Tag.new(current_user, params.require(:tag)).posts - render json: posts.map {|p| PostPresenter.new(p).as_api_response } + posts_query = Stream::Tag.new(current_user, params.require(:tag)).posts + posts_page = time_pager(posts_query, "posts.created_at", "created_at").response + posts_page[:data] = posts_page[:data].map {|post| PostPresenter.new(post).as_api_response } + render json: posts_page + end + + private + + def time_pager(query, query_time_field, data_time_field) + Api::Paging::RestPaginatorBuilder.new(query, request).time_pager(params, query_time_field, data_time_field) end end end diff --git a/app/controllers/api/v1/streams_controller.rb b/app/controllers/api/v1/streams_controller.rb index 2f46af2b5..04c922a0d 100644 --- a/app/controllers/api/v1/streams_controller.rb +++ b/app/controllers/api/v1/streams_controller.rb @@ -9,12 +9,12 @@ module Api def aspects aspect_ids = params.has_key?(:aspect_ids) ? JSON.parse(params[:aspect_ids]) : [] - @stream = Stream::Aspect.new(current_user, aspect_ids, max_time: max_time) + @stream = Stream::Aspect.new(current_user, aspect_ids, max_time: stream_max_time) stream_responder end def activity - stream_responder(Stream::Activity) + stream_responder(Stream::Activity, "posts.interacted_at", "interacted_at") end def multi @@ -39,10 +39,25 @@ module Api private - def stream_responder(stream_klass=nil) - @stream = stream_klass.present? ? stream_klass.new(current_user, max_time: max_time) : @stream + def stream_responder(stream_klass=nil, query_time_field="posts.created_at", data_time_field="created_at") + @stream = stream_klass.present? ? stream_klass.new(current_user, max_time: stream_max_time) : @stream + posts_page = pager(@stream.stream_posts, query_time_field, data_time_field).response + posts_page[:data] = posts_page[:data].map {|post| PostPresenter.new(post, current_user).as_api_response } + posts_page[:links].delete(:previous) + render json: posts_page + end - render json: @stream.stream_posts.map {|p| PostPresenter.new(p, current_user).as_api_response } + def stream_max_time + if params.has_key?("before") + Time.iso8601(params["before"]) + else + max_time + end + end + + def pager(query, query_time_field, data_time_field) + Api::Paging::RestPaginatorBuilder.new(query, request, true, 15) + .time_pager(params, query_time_field, data_time_field) end end end diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 63b09a14f..3ceb9c801 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -44,20 +44,26 @@ module Api return end - contacts_with_profile = AspectsMembershipService.new(current_user).all_contacts - render json: contacts_with_profile.map {|c| PersonPresenter.new(c.person).as_api_json } + contacts_query = AspectsMembershipService.new(current_user).all_contacts + contacts_page = index_pager(contacts_query).response + contacts_page[:data] = contacts_page[:data].map {|c| PersonPresenter.new(c.person).as_api_json } + render json: contacts_page end def photos person = Person.find_by!(guid: params[:user_id]) - photos = Photo.visible(current_user, person, :all, Time.current) - render json: photos.map {|photo| PhotoPresenter.new(photo).as_api_json(true) } + photos_query = Photo.visible(current_user, person, :all, Time.current) + photos_page = time_pager(photos_query).response + photos_page[:data] = photos_page[:data].map {|photo| PhotoPresenter.new(photo).as_api_json(true) } + render json: photos_page end def posts person = Person.find_by!(guid: params[:user_id]) - posts = current_user.posts_from(person) - render json: posts.map {|post| PostPresenter.new(post, current_user).as_api_response } + posts_query = current_user.posts_from(person, false) + posts_page = time_pager(posts_query).response + posts_page[:data] = posts_page[:data].map {|post| PostPresenter.new(post, current_user).as_api_response } + render json: posts_page end private diff --git a/app/models/user/querying.rb b/app/models/user/querying.rb index 66dce96ac..767896c3f 100644 --- a/app/models/user/querying.rb +++ b/app/models/user/querying.rb @@ -67,8 +67,10 @@ module User::Querying contact_for(person).aspects end - def posts_from(person) - Post.from_person_visible_by_user(self, person).order("posts.created_at desc") + def posts_from(person, with_order=true) + base_query = Post.from_person_visible_by_user(self, person) + return base_query.order("posts.created_at desc") if with_order + base_query end def photos_from(person, opts={}) diff --git a/app/presenters/conversation_presenter.rb b/app/presenters/conversation_presenter.rb index 525937982..927df5d24 100644 --- a/app/presenters/conversation_presenter.rb +++ b/app/presenters/conversation_presenter.rb @@ -2,8 +2,7 @@ class ConversationPresenter < BasePresenter def as_api_json - read = !@presentable.conversation_visibilities.empty? && - @presentable.conversation_visibilities[0].unread == 0 + read = @presentable.conversation_visibilities.find_by(person_id: current_user.person_id)&.unread == 0 { guid: @presentable.guid, subject: @presentable.subject, diff --git a/app/presenters/notification_presenter.rb b/app/presenters/notification_presenter.rb index 445a98867..492b9a647 100644 --- a/app/presenters/notification_presenter.rb +++ b/app/presenters/notification_presenter.rb @@ -3,7 +3,7 @@ class NotificationPresenter < BasePresenter def as_api_json(include_target=true) data = base_hash - data = data.merge(target: target_json) if include_target + data = data.merge(target: target_json) if include_target && target data end diff --git a/lib/api/paging/index_paginator.rb b/lib/api/paging/index_paginator.rb new file mode 100644 index 000000000..a9518fb8d --- /dev/null +++ b/lib/api/paging/index_paginator.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Api + module Paging + class IndexPaginator + def initialize(query_base, current_page, limit) + @query_base = query_base + @current_page = current_page.to_i + @limit = limit.to_i + end + + def page_data + @page_data ||= @query_base.paginate(page: @current_page, per_page: @limit) + @max_page = (@query_base.count * 1.0 / @limit * 1.0).ceil + @max_page = 1 if @max_page < 1 + @page_data + end + + def next_page(for_url=true) + page_data + return nil if for_url && @current_page == @max_page + return "page=#{@current_page + 1}" if for_url + IndexPaginator.new(@query_base, @current_page + 1, @limit) + end + + def previous_page(for_url=true) + page_data + return nil if for_url && @current_page == 1 + return "page=#{@current_page - 1}" if for_url + IndexPaginator.new(@query_base, @current_page - 1, @limit) + end + + def filter_parameters(parameters) + parameters.delete(:page) + end + end + end +end diff --git a/lib/api/paging/rest_paged_response_builder.rb b/lib/api/paging/rest_paged_response_builder.rb new file mode 100644 index 000000000..95ab7c059 --- /dev/null +++ b/lib/api/paging/rest_paged_response_builder.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Api + module Paging + class RestPagedResponseBuilder + def initialize(pager, request, allowed_params=nil) + @pager = pager + @base_url = request.original_url.split("?").first if request + @query_parameters = if allowed_params + allowed_params + elsif request&.query_parameters + request&.query_parameters + else + {} + end + end + + def response + { + links: navigation_builder, + data: @pager.page_data + } + end + + private + + def navigation_builder + previous_page = @pager.previous_page + links = {} + links[:previous] = link_builder(previous_page) if previous_page + next_page = @pager.next_page + links[:next] = link_builder(next_page) if next_page + links + end + + def link_builder(page_parameter) + "#{@base_url}?#{filtered_original_parameters}#{page_parameter}" + end + + def filtered_original_parameters + @pager.filter_parameters(@query_parameters) + return "" if @query_parameters.empty? + @query_parameters.map {|k, v| "#{k}=#{v}" }.join("&") + "&" + end + end + end +end diff --git a/lib/api/paging/rest_paginator_builder.rb b/lib/api/paging/rest_paginator_builder.rb new file mode 100644 index 000000000..24cda8311 --- /dev/null +++ b/lib/api/paging/rest_paginator_builder.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Api + module Paging + class RestPaginatorBuilder + MAX_LIMIT = 100 + DEFAULT_LIMIT = 15 + + def initialize(base_query, request, allow_default_page=true, default_limit=DEFAULT_LIMIT) + @base_query = base_query + @request = request + @allow_default_page = allow_default_page + @default_limit = if default_limit < MAX_LIMIT && default_limit > 0 + default_limit + else + DEFAULT_LIMIT + end + end + + def index_pager(params) + current_page = current_page_settings(params) + paged_response_builder(IndexPaginator.new(@base_query, current_page, limit_settings(params))) + end + + def time_pager(params, query_time_field="created_at", data_time_field=query_time_field) + is_descending, current_time = time_settings(params) + paged_response_builder( + TimePaginator.new( + query_base: @base_query, + query_time_field: query_time_field, + data_time_field: data_time_field, + current_time: current_time, + is_descending: is_descending, + limit: limit_settings(params) + ) + ) + end + + private + + def current_page_settings(params) + if params["page"] + requested_page = params["page"] + requested_page = 1 if requested_page < 1 + requested_page + elsif @allow_default_page + 1 + else + raise ActionController::ParameterMissing + end + end + + def paged_response_builder(paginator) + Api::Paging::RestPagedResponseBuilder.new(paginator, @request) + end + + def time_settings(params) + time_params = params.permit("before", "after") + time_params["before"] = (Time.current + 1.year).iso8601 if time_params.empty? && @allow_default_page + raise "Missing time parameters for query building" if time_params.empty? + if time_params["before"] + is_descending = true + current_time = Time.iso8601(time_params["before"]) + else + is_descending = false + current_time = Time.iso8601(time_params["after"]) + end + [is_descending, current_time] + end + + def limit_settings(params) + requested_limit = params["per_page"] + return @default_limit unless requested_limit + requested_limit = [1, requested_limit].max + [requested_limit, MAX_LIMIT].min + end + end + end +end diff --git a/lib/api/paging/time_paginator.rb b/lib/api/paging/time_paginator.rb new file mode 100644 index 000000000..9ec696473 --- /dev/null +++ b/lib/api/paging/time_paginator.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Api + module Paging + class TimePaginator + def initialize(opts={}) + @query_base = opts[:query_base] + @query_time_field = opts[:query_time_field] + @data_time_field = opts[:data_time_field] + @current_time = opts[:current_time] + @limit = opts[:limit] + @is_descending = opts[:is_descending] + direction = if @is_descending + "<" + else + ">" + end + @time_query_string = "#{@query_time_field} #{direction} ?" + @sort_string = if @is_descending + "#{@query_time_field} DESC" + else + "#{@query_time_field} ASC" + end + end + + def page_data + return @data if @data + @data = @query_base.where([@time_query_string, @current_time.iso8601(3)]).limit(@limit).order(@sort_string) + time_data = @data.map {|d| d[@data_time_field] }.sort + @min_time = time_data.first + @max_time = time_data.last + 0.001.seconds if time_data.last + @data + end + + def next_page(for_url=true) + page_data + return nil unless next_time + return next_page_as_query_parameter if for_url + TimePaginator.new( + query_base: @query_base, + query_time_field: @query_time_field, + query_data_field: @data_time_field, + current_time: next_time, + is_descending: @is_descending, + limit: @limit + ) + end + + def previous_page(for_url=true) + page_data + return nil unless previous_time + return previous_page_as_query_parameter if for_url + TimePaginator.new( + query_base: @query_base, + query_time_field: @query_time_field, + query_data_field: @data_time_field, + current_time: previous_time, + is_descending: !@is_descending, + limit: @limit + ) + end + + def filter_parameters(parameters) + parameters.delete(:before) + parameters.delete(:after) + end + + private + + def next_time + if @is_descending + @min_time + else + @max_time + end + end + + def previous_time + if @is_descending + @max_time + else + @min_time + end + end + + def next_page_as_query_parameter + if @is_descending + "before=#{next_time.iso8601(3)}" + else + "after=#{next_time.iso8601(3)}" + end + end + + def previous_page_as_query_parameter + if @is_descending + "after=#{previous_time.iso8601(3)}" + else + "before=#{previous_time.iso8601(3)}" + end + end + end + end +end diff --git a/spec/integration/api/aspects_controller_spec.rb b/spec/integration/api/aspects_controller_spec.rb index 0d97290e8..df86f06a5 100644 --- a/spec/integration/api/aspects_controller_spec.rb +++ b/spec/integration/api/aspects_controller_spec.rb @@ -20,7 +20,7 @@ describe Api::V1::AspectsController do params: {access_token: access_token} ) expect(response.status).to eq(200) - aspects = JSON.parse(response.body) + aspects = response_body_data(response) expect(aspects.length).to eq(auth.user.aspects.length) aspects.each do |aspect| found_aspect = auth.user.aspects.find_by(id: aspect["id"]) @@ -299,4 +299,8 @@ describe Api::V1::AspectsController do end end end + + def response_body_data(response) + JSON.parse(response.body)["data"] + end end diff --git a/spec/integration/api/comments_controller_spec.rb b/spec/integration/api/comments_controller_spec.rb index 94aac5e60..6b4607488 100644 --- a/spec/integration/api/comments_controller_spec.rb +++ b/spec/integration/api/comments_controller_spec.rb @@ -70,8 +70,8 @@ describe Api::V1::CommentsController do params: {access_token: access_token} ) expect(response.status).to eq(200) - expect(response_body(response).length).to eq(2) - comments = response_body(response) + comments = response_body_data(response) + expect(comments.length).to eq(2) confirm_comment_format(comments[0], auth.user, @comment_text1) confirm_comment_format(comments[1], auth.user, @comment_text2) end @@ -289,6 +289,10 @@ describe Api::V1::CommentsController do JSON.parse(response.body) end + def response_body_data(response) + JSON.parse(response.body)["data"] + end + private # rubocop:disable Metrics/AbcSize diff --git a/spec/integration/api/contacts_controller_spec.rb b/spec/integration/api/contacts_controller_spec.rb index 02f300cbe..42ec41c4f 100644 --- a/spec/integration/api/contacts_controller_spec.rb +++ b/spec/integration/api/contacts_controller_spec.rb @@ -26,7 +26,7 @@ describe Api::V1::ContactsController do params: {access_token: access_token} ) expect(response.status).to eq(200) - contacts = JSON.parse(response.body) + contacts = response_body_data(response) expect(contacts.length).to eq(1) confirm_person_format(contacts[0], alice) @@ -35,7 +35,7 @@ describe Api::V1::ContactsController do params: {access_token: access_token} ) expect(response.status).to eq(200) - contacts = JSON.parse(response.body) + contacts = response_body_data(response) expect(contacts.length).to eq(@aspect1.contacts.length) end end @@ -218,6 +218,10 @@ describe Api::V1::ContactsController do end end + def response_body_data(response) + JSON.parse(response.body)["data"] + end + def aspects_membership_service(user=auth.user) AspectsMembershipService.new(user) end diff --git a/spec/integration/api/conversations_controller_spec.rb b/spec/integration/api/conversations_controller_spec.rb index aebe950a6..442a3c51a 100644 --- a/spec/integration/api/conversations_controller_spec.rb +++ b/spec/integration/api/conversations_controller_spec.rb @@ -13,10 +13,10 @@ describe Api::V1::ConversationsController do alice.share_with auth.user.person, alice.aspects[0] auth.user.disconnected_by(eve) - @conversation = { + @conversation_request = { subject: "new conversation", body: "first message", - recipients: JSON.generate([alice.guid]), + recipients: [alice.guid], access_token: access_token } end @@ -24,10 +24,10 @@ describe Api::V1::ConversationsController do describe "#create" do context "with valid data" do it "creates the conversation" do - post api_v1_conversations_path, params: @conversation + post api_v1_conversations_path, params: @conversation_request expect(response.status).to eq 201 conversation = JSON.parse(response.body) - confirm_conversation_format(conversation, @conversation, [auth.user, alice]) + confirm_conversation_format(conversation, @conversation_request, [auth.user, alice]) end end @@ -75,7 +75,7 @@ describe Api::V1::ConversationsController do incomplete_conversation = { subject: "new conversation", body: "first message", - recipients: JSON.generate(["999_999_999"]), + recipients: ["999_999_999"], access_token: access_token } post api_v1_conversations_path, params: incomplete_conversation @@ -87,7 +87,7 @@ describe Api::V1::ConversationsController do incomplete_conversation = { subject: "new conversation", body: "first message", - recipients: JSON.generate([eve.guid]), + recipients: [eve.guid], access_token: access_token } post api_v1_conversations_path, params: incomplete_conversation @@ -99,25 +99,28 @@ describe Api::V1::ConversationsController do describe "#index" do before do - post api_v1_conversations_path, params: @conversation - post api_v1_conversations_path, params: @conversation + post api_v1_conversations_path, params: @conversation_request + @read_conversation_guid = JSON.parse(response.body)["guid"] + @read_conversation = conversation_service.find!(@read_conversation_guid) + post api_v1_conversations_path, params: @conversation_request sleep(1) - post api_v1_conversations_path, params: @conversation - conversation_guid = JSON.parse(response.body)["guid"] - conversation = conversation_service.find!(conversation_guid) - conversation.conversation_visibilities[0].unread = 1 - conversation.conversation_visibilities[0].save! - conversation.conversation_visibilities[1].unread = 1 - conversation.conversation_visibilities[1].save! - @date = conversation.created_at + post api_v1_conversations_path, params: @conversation_request + @conversation_guid = JSON.parse(response.body)["guid"] + @conversation = conversation_service.find!(@conversation_guid) + @conversation.conversation_visibilities[0].unread = 1 + @conversation.conversation_visibilities[0].save! + @conversation.conversation_visibilities[1].unread = 1 + @conversation.conversation_visibilities[1].save! + @date = @conversation.created_at end it "returns all the user conversations" do get api_v1_conversations_path, params: {access_token: access_token} expect(response.status).to eq 200 - returned_conversations = JSON.parse(response.body) + returned_conversations = response_body_data(response) expect(returned_conversations.length).to eq 3 - confirm_conversation_format(returned_conversations[0], @conversation, [auth.user, alice]) + actual_conversation = returned_conversations.select {|c| c["guid"] == @read_conversation_guid }[0] + confirm_conversation_format(actual_conversation, @read_conversation, [auth.user, alice]) end it "returns all the user unread conversations" do @@ -126,7 +129,7 @@ describe Api::V1::ConversationsController do params: {only_unread: true, access_token: access_token} ) expect(response.status).to eq 200 - expect(JSON.parse(response.body).length).to eq 2 + expect(response_body_data(response).length).to eq 2 end it "returns all the user conversations after a given date" do @@ -135,14 +138,16 @@ describe Api::V1::ConversationsController do params: {only_after: @date, access_token: access_token} ) expect(response.status).to eq 200 - expect(JSON.parse(response.body).length).to eq 1 + expect(response_body_data(response).length).to eq 1 end end describe "#show" do context "valid conversation ID" do before do - post api_v1_conversations_path, params: @conversation + post api_v1_conversations_path, params: @conversation_request + @conversation_guid = JSON.parse(response.body)["guid"] + @conversation = conversation_service.find!(@conversation_guid) end it "returns the corresponding conversation" do @@ -177,13 +182,13 @@ describe Api::V1::ConversationsController do auth.user.person, auth_participant.user.aspects[0] ) - @conversation = { + @conversation_request = { subject: "new conversation", body: "first message", - recipients: JSON.generate([auth_participant.user.guid]), + recipients: [auth_participant.user.guid], access_token: access_token } - post api_v1_conversations_path, params: @conversation + post api_v1_conversations_path, params: @conversation_request @conversation_guid = JSON.parse(response.body)["guid"] end @@ -251,6 +256,10 @@ describe Api::V1::ConversationsController do private + def response_body_data(response) + JSON.parse(response.body)["data"] + end + # rubocop:disable Metrics/AbcSize def confirm_conversation_format(conversation, ref_conversation, ref_participants) expect(conversation["guid"]).to_not be_nil diff --git a/spec/integration/api/likes_controller_spec.rb b/spec/integration/api/likes_controller_spec.rb index 8870fef22..720a7925d 100644 --- a/spec/integration/api/likes_controller_spec.rb +++ b/spec/integration/api/likes_controller_spec.rb @@ -23,7 +23,7 @@ describe Api::V1::LikesController do params: {access_token: access_token} ) expect(response.status).to eq(200) - likes = response_body(response) + likes = response_body_data(response) expect(likes.length).to eq(0) end @@ -36,7 +36,7 @@ describe Api::V1::LikesController do params: {access_token: access_token} ) expect(response.status).to eq(200) - likes = response_body(response) + likes = response_body_data(response) expect(likes.length).to eq(3) confirm_like_format(likes, alice) confirm_like_format(likes, bob) @@ -170,4 +170,8 @@ describe Api::V1::LikesController do def response_body(response) JSON.parse(response.body) end + + def response_body_data(response) + JSON.parse(response.body)["data"] + end end diff --git a/spec/integration/api/messages_controller_spec.rb b/spec/integration/api/messages_controller_spec.rb index cbedc52b5..f1076a29a 100644 --- a/spec/integration/api/messages_controller_spec.rb +++ b/spec/integration/api/messages_controller_spec.rb @@ -15,7 +15,7 @@ describe Api::V1::MessagesController do @conversation = { subject: "new conversation", body: "first message", - recipients: JSON.generate([alice.guid]), + recipients: [alice.guid], access_token: access_token } @@ -43,7 +43,7 @@ describe Api::V1::MessagesController do api_v1_conversation_messages_path(@conversation_guid), params: {access_token: access_token} ) - messages = JSON.parse(response.body) + messages = response_body_data(response) expect(messages.length).to eq 2 confirm_message_format(messages[1], @message_text, auth.user) end @@ -93,7 +93,8 @@ describe Api::V1::MessagesController do api_v1_conversation_messages_path(@conversation_guid), params: {access_token: access_token} ) - messages = JSON.parse(response.body) + expect(response.status).to eq 200 + messages = response_body_data(response) expect(messages.length).to eq 1 confirm_message_format(messages[0], "first message", auth.user) @@ -105,10 +106,14 @@ describe Api::V1::MessagesController do private + def response_body_data(response) + JSON.parse(response.body)["data"] + end + def get_conversation(conversation_id) conversation_service = ConversationService.new(auth.user) raw_conversation = conversation_service.find!(conversation_id) - ConversationPresenter.new(raw_conversation).as_api_json + ConversationPresenter.new(raw_conversation, auth.user).as_api_json end def confirm_message_format(message, ref_message, author) diff --git a/spec/integration/api/notifications_controller_spec.rb b/spec/integration/api/notifications_controller_spec.rb index e8c52f38e..37e147cd0 100644 --- a/spec/integration/api/notifications_controller_spec.rb +++ b/spec/integration/api/notifications_controller_spec.rb @@ -26,7 +26,7 @@ describe Api::V1::NotificationsController do params: {access_token: access_token} ) expect(response.status).to eq(200) - notification = JSON.parse(response.body) + notification = response_body_data(response) expect(notification.length).to eq(1) confirm_notification_format(notification[0], @notification, "also_commented", nil) end @@ -37,7 +37,7 @@ describe Api::V1::NotificationsController do params: {only_unread: true, access_token: access_token} ) expect(response.status).to eq(200) - notification = JSON.parse(response.body) + notification = response_body_data(response) expect(notification.length).to eq(1) @notification.set_read_state(true) get( @@ -45,7 +45,7 @@ describe Api::V1::NotificationsController do params: {only_unread: true, access_token: access_token} ) expect(response.status).to eq(200) - notification = JSON.parse(response.body) + notification = response_body_data(response) expect(notification.length).to eq(0) end @@ -55,7 +55,7 @@ describe Api::V1::NotificationsController do params: {only_after: (Date.current - 1.day).iso8601, access_token: access_token} ) expect(response.status).to eq(200) - notification = JSON.parse(response.body) + notification = response_body_data(response) expect(notification.length).to eq(1) @notification.set_read_state(true) get( @@ -63,7 +63,7 @@ describe Api::V1::NotificationsController do params: {only_after: (Date.current + 1.day).iso8601, access_token: access_token} ) expect(response.status).to eq(200) - notification = JSON.parse(response.body) + notification = response_body_data(response) expect(notification.length).to eq(0) end end @@ -195,6 +195,10 @@ describe Api::V1::NotificationsController do private + def response_body_data(response) + JSON.parse(response.body)["data"] + end + # rubocop:disable Metrics/AbcSize def confirm_notification_format(notification, ref_notification, expected_type, target) expect(notification["guid"]).to eq(ref_notification.guid) diff --git a/spec/integration/api/photos_controller_spec.rb b/spec/integration/api/photos_controller_spec.rb index 7cd9eb2e0..7b8e6349a 100644 --- a/spec/integration/api/photos_controller_spec.rb +++ b/spec/integration/api/photos_controller_spec.rb @@ -93,7 +93,7 @@ describe Api::V1::PhotosController do params: {access_token: access_token} ) expect(response.status).to eq(200) - photos = JSON.parse(response.body) + photos = response_body_data(response) expect(photos.length).to eq(2) end end @@ -267,6 +267,10 @@ describe Api::V1::PhotosController do end end + def response_body_data(response) + JSON.parse(response.body)["data"] + end + # rubocop:disable Metrics/AbcSize def confirm_photo_format(photo, ref_photo, ref_user) expect(photo["guid"]).to eq(ref_photo.guid) diff --git a/spec/integration/api/reshares_controller_spec.rb b/spec/integration/api/reshares_controller_spec.rb index 1c3c54adc..f02a148fd 100644 --- a/spec/integration/api/reshares_controller_spec.rb +++ b/spec/integration/api/reshares_controller_spec.rb @@ -35,7 +35,7 @@ describe Api::V1::ResharesController do ) expect(response.status).to eq(200) - reshares = JSON.parse(response.body) + reshares = response_body_data(response) expect(reshares.length).to eq(1) reshare = reshares[0] expect(reshare["guid"]).not_to be_nil @@ -57,7 +57,7 @@ describe Api::V1::ResharesController do } ) expect(response.status).to eq(200) - reshares = JSON.parse(response.body) + reshares = response_body_data(response) expect(reshares.length).to eq(0) end end @@ -169,7 +169,6 @@ describe Api::V1::ResharesController do access_token: access_token } ) - puts(response.body) expect(response.status).to eq(404) expect(response.body).to eq(I18n.t("api.endpoint_errors.posts.post_not_found")) end @@ -204,6 +203,10 @@ describe Api::V1::ResharesController do @reshare_service ||= ReshareService.new(user) end + def response_body_data(response) + JSON.parse(response.body)["data"] + end + # rubocop:disable Metrics/AbcSize def confirm_person_format(post_person, user) expect(post_person["guid"]).to eq(user.guid) diff --git a/spec/integration/api/search_controller_spec.rb b/spec/integration/api/search_controller_spec.rb index 13276277b..eaea24f60 100644 --- a/spec/integration/api/search_controller_spec.rb +++ b/spec/integration/api/search_controller_spec.rb @@ -39,7 +39,7 @@ describe Api::V1::SearchController do params: {tag: "one", access_token: access_token} ) expect(response.status).to eq(200) - users = JSON.parse(response.body) + users = response_body_data(response) expect(users.length).to eq(14) end @@ -49,7 +49,7 @@ describe Api::V1::SearchController do params: {name_or_handle: "Terry", access_token: access_token} ) expect(response.status).to eq(200) - users = JSON.parse(response.body) + users = response_body_data(response) expect(users.length).to eq(1) end @@ -59,7 +59,7 @@ describe Api::V1::SearchController do params: {name_or_handle: "findable", access_token: access_token} ) expect(response.status).to eq(200) - users = JSON.parse(response.body) + users = response_body_data(response) expect(users.length).to eq(1) end @@ -69,7 +69,7 @@ describe Api::V1::SearchController do params: {name_or_handle: "Closed", access_token: access_token} ) expect(response.status).to eq(200) - users = JSON.parse(response.body) + users = response_body_data(response) expect(users.length).to eq(0) end @@ -79,7 +79,7 @@ describe Api::V1::SearchController do params: {name_or_handle: "unsearchable@example.org", access_token: access_token} ) expect(response.status).to eq(200) - users = JSON.parse(response.body) + users = response_body_data(response) expect(users.length).to eq(0) end @@ -131,7 +131,7 @@ describe Api::V1::SearchController do params: {tag: "tag2", access_token: access_token} ) expect(response.status).to eq(200) - posts = JSON.parse(response.body) + posts = response_body_data(response) expect(posts.length).to eq(2) end @@ -152,4 +152,8 @@ describe Api::V1::SearchController do expect(response.status).to eq(401) end end + + def response_body_data(response) + JSON.parse(response.body)["data"] + end end diff --git a/spec/integration/api/streams_controller_spec.rb b/spec/integration/api/streams_controller_spec.rb index 82c3b227c..9ca4dcf21 100644 --- a/spec/integration/api/streams_controller_spec.rb +++ b/spec/integration/api/streams_controller_spec.rb @@ -20,7 +20,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 1 confirm_post_format(post[0], auth.user, @status) end @@ -31,7 +31,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 1 confirm_post_format(post[0], auth.user, @status) end @@ -52,7 +52,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 0 end end @@ -64,7 +64,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 1 confirm_post_format(post[0], auth.user, @status) end @@ -77,7 +77,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 1 confirm_post_format(post[0], auth.user, @status) end @@ -90,7 +90,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 0 end end @@ -102,7 +102,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 0 end end @@ -114,7 +114,7 @@ describe Api::V1::StreamsController do params: {access_token: access_token} ) expect(response.status).to eq 200 - post = JSON.parse(response.body) + post = response_body_data(response) expect(post.length).to eq 1 confirm_post_format(post[0], auth.user, @status) end @@ -205,4 +205,8 @@ describe Api::V1::StreamsController do confirm_person_format(root["author"], root_poster) end # rubocop:enable Metrics/AbcSize + + def response_body_data(response) + JSON.parse(response.body)["data"] + end end diff --git a/spec/integration/api/users_controller_spec.rb b/spec/integration/api/users_controller_spec.rb index 77d61e636..015de2440 100644 --- a/spec/integration/api/users_controller_spec.rb +++ b/spec/integration/api/users_controller_spec.rb @@ -237,7 +237,7 @@ describe Api::V1::UsersController do params: {access_token: access_token} ) expect(response.status).to eq(200) - contacts = JSON.parse(response.body) + contacts = response_body_data(response) expect(contacts.length).to eq(0) auth.user.share_with(alice.person, auth.user.aspects.first) @@ -246,7 +246,7 @@ describe Api::V1::UsersController do params: {access_token: access_token} ) expect(response.status).to eq(200) - contacts = JSON.parse(response.body) + contacts = response_body_data(response) expect(contacts.length).to eq(1) confirm_person_format(contacts[0], alice) end @@ -301,7 +301,7 @@ describe Api::V1::UsersController do params: {access_token: access_token} ) expect(response.status).to eq(200) - photos = JSON.parse(response.body) + photos = response_body_data(response) expect(photos.length).to eq(3) guids = photos.map {|photo| photo["guid"] } expect(guids).to include(@public_photo1.guid, @public_photo2.guid, @shared_photo1.guid) @@ -361,7 +361,7 @@ describe Api::V1::UsersController do params: {access_token: access_token} ) expect(response.status).to eq(200) - posts = JSON.parse(response.body) + posts = response_body_data(response) expect(posts.length).to eq(3) guids = posts.map {|post| post["guid"] } expect(guids).to include(@public_post1.guid, @public_post2.guid, @shared_post1.guid) @@ -370,13 +370,13 @@ describe Api::V1::UsersController do confirm_post_format(post[0], alice, @public_post1) end - it "returns logged in user's photos" do + it "returns logged in user's posts" do get( api_v1_user_posts_path(auth.user.guid), params: {access_token: access_token} ) expect(response.status).to eq(200) - posts = JSON.parse(response.body) + posts = response_body_data(response) expect(posts.length).to eq(2) end end @@ -483,4 +483,8 @@ describe Api::V1::UsersController do end end # rubocop:enable Metrics/AbcSize + + def response_body_data(response) + JSON.parse(response.body)["data"] + end end diff --git a/spec/lib/api/paging/index_paginator_spec.rb b/spec/lib/api/paging/index_paginator_spec.rb new file mode 100644 index 000000000..d49973df2 --- /dev/null +++ b/spec/lib/api/paging/index_paginator_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +describe Api::Paging::IndexPaginator do + before do + (1..7).each do |i| + public = !(i == 1 || i == 6) + alice.post( + :status_message, + text: "Post #{i}", + public: public + ) + end + @alice_search = alice.posts.where(public: true).order("id ASC") + @limit = 2 + end + + it "goes through using direct paging" do + pager = Api::Paging::IndexPaginator.new(@alice_search, 1, @limit) + page = pager.page_data + validate_page(page, :created_at, false) + page_count = 0 + until page&.empty? + page_count += 1 + pager = pager.next_page(false) + page = pager.page_data + validate_page(page, :created_at, false) + end + expect(page_count).to eq(3) + end + + it "goes through using Query Parameter data" do + page_num = 1 + pager = Api::Paging::IndexPaginator.new(@alice_search, page_num, @limit) + page = pager.page_data + validate_page(page, :created_at, false) + page_count = 0 + until page&.empty? + page_count += 1 + break unless pager.next_page + np = pager.next_page.split("=").last.to_i + pager = Api::Paging::IndexPaginator.new(@alice_search, np, @limit) + page = pager.page_data + validate_page(page, :created_at, false) + end + expect(page_count).to eq(3) + end + + def validate_page(page, field, is_descending) + expect(page.length).to be <= @limit + last_value = nil + page.each do |element| + last_value ||= element[field] + if is_descending + expect(last_value).to be >= element[field] + else + expect(last_value).to be <= element[field] + end + last_value = element[field] + end + end +end diff --git a/spec/lib/api/paging/rest_paged_response_builder_spec.rb b/spec/lib/api/paging/rest_paged_response_builder_spec.rb new file mode 100644 index 000000000..6a371cbc4 --- /dev/null +++ b/spec/lib/api/paging/rest_paged_response_builder_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe Api::Paging::RestPagedResponseBuilder do + before do + @pager = Api::Paging::IndexPaginator.new(alice.posts, 1, 5) + end + it "returns page of data" do + builder = Api::Paging::RestPagedResponseBuilder.new(@pager, nil) + response = builder.response + expect(response[:links]).not_to be_nil + expect(response[:data]).not_to be_nil + end +end diff --git a/spec/lib/api/paging/rest_paginator_builder_spec.rb b/spec/lib/api/paging/rest_paginator_builder_spec.rb new file mode 100644 index 000000000..646cc6544 --- /dev/null +++ b/spec/lib/api/paging/rest_paginator_builder_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +describe Api::Paging::RestPaginatorBuilder do + it "generates page response builder called with index-based pager" do + params = ActionController::Parameters.new(page: 1) + pager = Api::Paging::RestPaginatorBuilder.new(alice.posts, nil).index_pager(params) + expect(pager.is_a?(Api::Paging::RestPagedResponseBuilder)).to be_truthy + end + + it "generates page response builder with time-based pager" do + params = ActionController::Parameters.new(before: Time.current.iso8601) + pager = Api::Paging::RestPaginatorBuilder.new(alice.posts, nil).time_pager(params) + expect(pager.is_a?(Api::Paging::RestPagedResponseBuilder)).to be_truthy + end +end diff --git a/spec/lib/api/paging/time_paginator_spec.rb b/spec/lib/api/paging/time_paginator_spec.rb new file mode 100644 index 000000000..211645ea9 --- /dev/null +++ b/spec/lib/api/paging/time_paginator_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +describe Api::Paging::TimePaginator do + before do + (1..7).each do |i| + public = !(i == 1 || i == 6) + alice.post( + :status_message, + text: "Post #{i}", + public: public + ) + sleep(0.01.seconds) + end + @alice_search = alice.posts.where(public: true) + @limit = 2 + end + + it "goes through decending using direct paging" do + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: Time.current, + is_descending: true, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, true, nil) + while page&.empty? + pager = pager.next_page(false) + page = pager.page_data + last_time = validate_page(page, :created_at, true, last_time) + end + end + + it "goes through descending using Query Parameter data" do + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: Time.current, + is_descending: true, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, true, nil) + while page&.empty? + next_time = Time.iso8601(pager.next_page.split("=").last) + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: next_time, + is_descending: true, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, true, last_time) + end + end + + it "goes through ascending using direct paging" do + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: (Time.current - 1.year), + is_descending: false, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, false, nil) + while page&.empty? + pager = pager.next_page(false) + page = pager.page_data + last_time = validate_page(page, :created_at, false, last_time) + end + end + + it "goes through ascending using Query Parameter data" do + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: (Time.current - 1.year), + is_descending: false, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, false, nil) + while page&.empty? + next_time = Time.iso8601(pager.next_page.split("=").last) + pager = Api::Paging::TimePaginator.new( + query_base: @alice_search, + query_time_field: :created_at, + data_time_field: :created_at, + current_time: next_time, + is_descending: false, + limit: @limit + ) + page = pager.page_data + last_time = validate_page(page, :created_at, false, last_time) + end + end + + def validate_page(page, field, is_descending, last_time) + expect(page.length).to be <= @limit + page.each do |element| + last_time ||= element[field] + if is_descending + expect(last_time).to be >= element[field] + else + expect(last_time).to be <= element[field] + end + last_time = element[field] + end + last_time + end +end diff --git a/spec/presenters/conversation_presenter.rb b/spec/presenters/conversation_presenter_spec.rb similarity index 100% rename from spec/presenters/conversation_presenter.rb rename to spec/presenters/conversation_presenter_spec.rb