Files
3e16dc554c Merge the Integrated Boards MVP feature branch (#35796)
* Add CreatedBy and UpdatedBy to the properties fields and values (#34485)

* Add CreatedBy and UpdatedBy to the properties fields and values

* Fix types

---------

Co-authored-by: Miguel de la Cruz <[email protected]>

* Adds ObjectType to the property fields table (#34908)

Co-authored-by: Miguel de la Cruz <[email protected]>

* Update ObjectType migration setting an empty value and marking the column as not null (#34915)

Co-authored-by: Miguel de la Cruz <[email protected]>

* Adds uniqueness mechanisms to the property fields (#35058)

* Adds uniqueness mechanisms to the property fields

After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.

* Adds uniqueness check to property updates

Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.

* Add translations to error messages

* Fixing retrylayer mocks

* Remove retrylayer duplication

* Address review comments

* Fix comment to avoid linter issues

* Address PR comments

* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.down.sql

Co-authored-by: Alejandro García Montoro <[email protected]>

* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql

Co-authored-by: Alejandro García Montoro <[email protected]>

* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql

Co-authored-by: Alejandro García Montoro <[email protected]>

* Update field validation to check only for valid target types

* Update migrations to avoid concurrent index creation within a transaction

* Update migrations to make all index ops concurrent

* Update tests to use valid PSAv2 property fields

* Adds a helper for valid PSAv2 TargetTypes

---------

Co-authored-by: Miguel de la Cruz <[email protected]>
Co-authored-by: Alejandro García Montoro <[email protected]>

* Fix property tests (#35388)

Co-authored-by: Miguel de la Cruz <[email protected]>

* Adds Integrated Boards feature flag (#35378)

Co-authored-by: Miguel de la Cruz <[email protected]>

* Adds Integrated Boards MVP API changes (#34822)

This PR includes the necessary changes for channels and posts
endpoints and adds a set of generic endpoints to retrieve and manage
property fields and values following the new Property System approach.

Co-authored-by: Miguel de la Cruz <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>

* Property System Architecture permissions for v2 (#35113)

* Adds uniqueness mechanisms to the property fields

After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.

* Adds uniqueness check to property updates

Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.

* Add translations to error messages

* Add the permissions to the migrations, model and update the store calls

* Adds the property field and property group app layer

* Adds authorization helpers for property fields and values

* Make sure that users cannot lock themselves out of property fields

* Migrate permissions from a JSON column to three normalized columns

* Remove the audit comment

* Use target level constants in authorization

* Log authorization membership failures

* Rename admin to sysadmin

* Fix i18n sorting

---------

Co-authored-by: Miguel de la Cruz <[email protected]>

* Add Views store and app layer (#35361)

* Add Views store and app layer for Integrated Boards

Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.

- public/model: View, ViewBoardProps, Subview, ViewPatch types with
  PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
  boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
  with channel-membership permission checks and WebSocket events
  (view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
  board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows

* Move permission checks out of App layer for views

- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field

* Make View service generic and enforce board validation in model

- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements

* Restore migrations files to match base branch

* Distinguish ErrNotFound from other errors in view store Get

* Use CONCURRENTLY and nontransactional for index operations in views migration

* Split views index creation into separate nontransactional migrations

* Update migrations.list

* Update i18n translations for views

* Fix makeView helper to include required Props for board view validation

* Rename ctx parameter from c to rctx in OAuthProvider mock

* Remove views service layer, call store directly from app

* Return 500 for unexpected DB errors in GetView, 404 only for not-found

* Harden View model: deep-copy Props, validate linked property IDs

- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
  with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
  are enforced
- Add tests covering all new behaviours

* Restore autotranslation worker_stopped i18n translation

* Fix view store test IDs and improve error handling in app layer

- Use model.NewId() for linked property IDs in testUpdateView to fix
  validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
  ErrNotFound (e.g. concurrent deletion TOCTOU race)

* Add View store mock to retrylayer test genStore helper

The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.

* Refactor view deletion and websocket event handling; update SQL store methods to use query builder

* revert property field store

* Remove useless migrations

* Add cursor-based pagination to View store GetForChannel

- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
  PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures

* Refactor test loops in ViewStore tests for improved readability

* change pagination to limit/offset

* Add upper-bound limits on View Subviews and LinkedProperties

Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.

---------

* MM-67388, MM-66528, MM-67750: Add View REST API endpoints, websocket events, and sort order (#35442)

* Add Views store and app layer for Integrated Boards

Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.

- public/model: View, ViewBoardProps, Subview, ViewPatch types with
  PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
  boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
  with channel-membership permission checks and WebSocket events
  (view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
  board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows

* Move permission checks out of App layer for views

- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field

* Make View service generic and enforce board validation in model

- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements

* Restore migrations files to match base branch

* Distinguish ErrNotFound from other errors in view store Get

* Use CONCURRENTLY and nontransactional for index operations in views migration

* Split views index creation into separate nontransactional migrations

* Update migrations.list

* Update i18n translations for views

* Fix makeView helper to include required Props for board view validation

* Rename ctx parameter from c to rctx in OAuthProvider mock

* Remove views service layer, call store directly from app

* Return 500 for unexpected DB errors in GetView, 404 only for not-found

* Harden View model: deep-copy Props, validate linked property IDs

- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
  with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
  are enforced
- Add tests covering all new behaviours

* Restore autotranslation worker_stopped i18n translation

* Fix view store test IDs and improve error handling in app layer

- Use model.NewId() for linked property IDs in testUpdateView to fix
  validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
  ErrNotFound (e.g. concurrent deletion TOCTOU race)

* Add View store mock to retrylayer test genStore helper

The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.

* Refactor view deletion and websocket event handling; update SQL store methods to use query builder

* revert property field store

* Add View API endpoints with OpenAPI spec, client methods, and i18n

Implement REST API for channel views (board-type) behind the
IntegratedBoards feature flag. Adds CRUD endpoints under
/api/v4/channels/{channel_id}/views with permission checks
matching the channel bookmark pattern.

* Remove useless migrations

* Add cursor-based pagination to View store GetForChannel

- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
  PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures

* Add cursor-based pagination to View API for channel views

* Enhance cursor handling in getViewsForChannel and update tests for pagination

* Refactor test loops in ViewStore tests for improved readability

* Refactor loop in TestGetViewsForChannel for improved readability

* change pagination to limit/offset

* switch to limit/offset pagination

* Add upper-bound limits on View Subviews and LinkedProperties

Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.

* Add view sort order API endpoint

Add POST /api/v4/channels/{channel_id}/views/{view_id}/sort_order
endpoint following the channel bookmarks reorder pattern. Includes
store, app, and API layers with full test coverage at each layer.

* Add connectionId to view WebSocket events and sort_order API spec

Thread connectionId from request header through all view handlers
(create, update, delete, sort_order) to WebSocket events, matching
the channel bookmarks pattern. Add sort_order endpoint to OpenAPI
spec. Update minimum server version to 11.6.

* Remove duplicate View/ViewPatch definitions from definitions.yaml

The merge from integrated-boards-mvp introduced duplicate View and
ViewPatch schema definitions that were already defined earlier in
the file with more detail (including ViewBoardProps ref and enums).

* Update minimum server version to 11.6 in views API spec

* Add missing translations for view sort order error messages

* Merge integrated-boards-mvp into ibmvp_api-views; remove spec files

* Fix flaky TestViewStore timestamp test on CI

Add sleep before UpdateSortOrder to ensure timestamps differ,
preventing same-millisecond comparisons on fast CI machines.

* remove duplicate views.yaml imclude

* Use c.boolString() for include_deleted query param in GetViewsForChannel

* Fix views.yaml sort order schema: use integer type and require body

* Refactor view sort order tests to use named IDs instead of array indices

Extract idA/idB/idC from views slice and add BEFORE/AFTER comments
to make stateful subtest ordering easier to follow.

* Return 404 instead of 403 for view operations on deleted channels

Deleted channels should appear non-existent to callers rather than
revealing their existence via a 403. Detailed error text explains
the context for debugging.

* add missing channel deleteat checks

* Use c.Params.Page instead of manual page query param parsing in getViewsForChannel

c.Params already validates and defaults page/per_page, so the manual
parsing was redundant.

* Add support for total count in views retrieval

* Add tests for handling deleted views in GetViewsForChannel and GetView

* Short-circuit negative newIndex in UpdateSortOrder before opening transaction

* Add per-channel limit on views to bound UpdateSortOrder cost

Without a cap, unbounded view creation makes sort-order updates
increasingly expensive (CASE WHEN per view, row locks). Adds
MaxViewsPerChannel=50 constant and enforces it in the app layer
before saving. Includes API and app layer tests.

* Remove include_deleted support from views API

Soft-deleted views are structural metadata with low risk, but no other
similar endpoint (e.g. channel bookmarks) exposes deleted records without
an admin gate. Rather than adding an admin-only permission check for
consistency, remove the feature entirely since there is no current use case.

* Update view permissions to require `create_post` instead of channel management permissions

* Remove obsolete view management error messages for direct and group messages

---------

* feat(migrations): add user tracking and object type to property fields

- Introduced user tracking columns (CreatedBy, UpdatedBy) to PropertyFields and PropertyValues.
- Added ObjectType column to PropertyFields with associated unique indexes for legacy and typed properties.
- Created new migration scripts for adding and dropping these features, including necessary indexes for data integrity.
- Established views for managing property fields with new attributes.

This update enhances the schema to support better tracking and categorization of property fields.

* Add Property System Architecture v2 API endpoints (#35583)

* Adds uniqueness mechanisms to the property fields

After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.

* Adds uniqueness check to property updates

Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.

* Add translations to error messages

* Add the permissions to the migrations, model and update the store calls

* Adds the property field and property group app layer

* Adds authorization helpers for property fields and values

* Make sure that users cannot lock themselves out of property fields

* Migrate permissions from a JSON column to three normalized columns

* Remove the audit comment

* Use target level constants in authorization

* Log authorization membership failures

* Rename admin to sysadmin

* Adds the Property System Architecture v2 API endpoints

* Adds permission checks to the create field endpoint

* Add target access checks to value endpoints

* Add default branches for object_type and target_type and extra guards for cursor client4 methods

* Fix vet API mismatch

* Fix error checks

* Fix linter

* Add merge semantics for property patch logic and API endpoint

* Fix i18n

* Fix duplicated patch elements and early return on bad cursor

* Update docs to use enums

* Fix i18n sorting

* Update app layer to return model.AppError

* Adds a limit to the number of property values that can be patched in the same request

* Require target_type filter when searching property fields

* Add objectType validation as part of field.IsValid()

* Fix linter

* Fix test with bad objecttpye

* Fix test grouping

---------

Co-authored-by: Miguel de la Cruz <[email protected]>

* MM-67968: Flatten view model — remove icon, subviews, typed board props (#35726)

* feat(views): flatten view model by removing icon, subview, and board props

Simplifies the View data model as part of MM-67968: removes Icon, Subview,
and ViewBoardProps types; renames ViewTypeBoard to ViewTypeKanban; replaces
typed Props with StringInterface (map[string]any); adds migration 000167
to drop the Icon column from the Views table.

* feat(api): update views OpenAPI spec to reflect flattened model

Removes ViewBoardProps, Subview, and icon from the View and ViewPatch
schemas. Changes type enum from board to kanban. Replaces typed props
with a free-form StringInterface object. Aligns with MM-67968.

* refactor(views): simplify store by dropping dbView and marshalViewProps

StringInterface already implements driver.Valuer and sql.Scanner, so the
manual JSON marshal/unmarshal and the dbView intermediate struct were
redundant. model.View now scans directly from the database. Also removes
the dead ViewMaxLinkedProperties constant and wraps the Commit() error in
UpdateSortOrder.

* fix(api): allow arbitrary JSON in view props OpenAPI schema

The props field was restricted to string values via
additionalProperties: { type: string }, conflicting with the Go model's
StringInterface (map[string]any). Changed to additionalProperties: true
in View, ViewPatch, and inline POST schemas.

---------

* Adds basic implementation of the generic redux store for PSAv2 (#35512)

* Adds basic implementation of the generic redux store for PSAv2

* Add created_by and updated_by to the test fixtures

* Make target_id, target_type and object_type mandatory

* Wrap getPropertyFieldsByIds and getPropertyValuesForTargetByFieldIds with createSelector

* Address PR comments

---------

Co-authored-by: Miguel de la Cruz <[email protected]>

* Adds websocket messages for the PSAv2 API events (#35696)

* Adds uniqueness mechanisms to the property fields

After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.

* Adds uniqueness check to property updates

Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.

* Add translations to error messages

* Add the permissions to the migrations, model and update the store calls

* Adds the property field and property group app layer

* Adds authorization helpers for property fields and values

* Make sure that users cannot lock themselves out of property fields

* Migrate permissions from a JSON column to three normalized columns

* Remove the audit comment

* Use target level constants in authorization

* Log authorization membership failures

* Rename admin to sysadmin

* Adds the Property System Architecture v2 API endpoints

* Adds permission checks to the create field endpoint

* Add target access checks to value endpoints

* Add default branches for object_type and target_type and extra guards for cursor client4 methods

* Fix vet API mismatch

* Fix error checks

* Fix linter

* Add merge semantics for property patch logic and API endpoint

* Fix i18n

* Fix duplicated patch elements and early return on bad cursor

* Update docs to use enums

* Fix i18n sorting

* Update app layer to return model.AppError

* Adds a limit to the number of property values that can be patched in the same request

* Adds websocket messages for the PSAv2 API events

* Add IsPSAv2 helper to the property field for clarity

* Add guard against nil returns on field deletion

* Add docs to the websocket endpoints

---------

Co-authored-by: Miguel de la Cruz <[email protected]>

* migrations: consolidate views migrations and reorder after master

- Merged 000165 (create Views) with 000167 (drop Icon) since Icon was never needed
- Renumbered branch migrations 159-166 → 160-167 so master's 000159 (deduplicate_policy_names) runs first
- Regenerated migrations.list

* Add API endpoint to retrieve posts for a specific view (#35604)

Automatic Merge

* Apply fixes after merge

* Return a more specific error from getting multiple fields

* Prevent getting broadcast params on field deletion if not needed

* Remove duplicated migration code

* Update property conflict code to always use master

* Adds nil guard when iterating on property fields

* Check that permission level is valid before getting rejected by the database

* Validate correctness on TargetID for PSAv2 fields

* Avoid PSAv1 using permissions or protected

* Fix test data after validation change

* Fix flaky search test

* Adds more posts for filter use cases to properly test exclusions

---------

Co-authored-by: Miguel de la Cruz <[email protected]>
Co-authored-by: Alejandro García Montoro <[email protected]>
Co-authored-by: Julien Tant <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
Co-authored-by: Julien Tant <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-03-27 10:36:35 +01:00

6372 lines
199 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
func TestPostStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("SaveMultiple", func(t *testing.T) { testPostStoreSaveMultiple(t, rctx, ss) })
t.Run("Save", func(t *testing.T) { testPostStoreSave(t, rctx, ss) })
t.Run("SaveAndUpdateChannelMsgCounts", func(t *testing.T) { testPostStoreSaveChannelMsgCounts(t, rctx, ss) })
t.Run("Get", func(t *testing.T) { testPostStoreGet(t, rctx, ss) })
t.Run("GetSingle", func(t *testing.T) { testPostStoreGetSingle(t, rctx, ss) })
t.Run("Update", func(t *testing.T) { testPostStoreUpdate(t, rctx, ss) })
t.Run("Delete", func(t *testing.T) { testPostStoreDelete(t, rctx, ss) })
t.Run("PermDelete1Level", func(t *testing.T) { testPostStorePermDelete1Level(t, rctx, ss) })
t.Run("PermDelete1Level2", func(t *testing.T) { testPostStorePermDelete1Level2(t, rctx, ss) })
t.Run("PermDeleteLimitExceeded", func(t *testing.T) { testPostStorePermDeleteLimitExceeded(t, rctx, ss) })
t.Run("GetWithChildren", func(t *testing.T) { testPostStoreGetWithChildren(t, rctx, ss) })
t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, rctx, ss) })
t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, rctx, ss) })
t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, rctx, ss) })
t.Run("GetPosts", func(t *testing.T) { testPostStoreGetPosts(t, rctx, ss) })
t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, rctx, ss) })
t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, rctx, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testPostCountsByDay(t, rctx, ss) })
t.Run("PostCounts", func(t *testing.T) { testPostCounts(t, rctx, ss) })
t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, rctx, ss, s) })
t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, rctx, ss) })
t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, rctx, ss) })
t.Run("GetPostsCreatedAt", func(t *testing.T) { testPostStoreGetPostsCreatedAt(t, rctx, ss) })
t.Run("Overwrite", func(t *testing.T) { testPostStoreOverwrite(t, rctx, ss) })
t.Run("OverwriteMultiple", func(t *testing.T) { testPostStoreOverwriteMultiple(t, rctx, ss) })
t.Run("GetPostsByIds", func(t *testing.T) { testPostStoreGetPostsByIds(t, rctx, ss) })
t.Run("GetPostsBatchForIndexing", func(t *testing.T) { testPostStoreGetPostsBatchForIndexing(t, rctx, ss) })
t.Run("PermanentDeleteBatch", func(t *testing.T) { testPostStorePermanentDeleteBatch(t, rctx, ss) })
t.Run("GetOldest", func(t *testing.T) { testPostStoreGetOldest(t, rctx, ss) })
t.Run("TestGetMaxPostSize", func(t *testing.T) { testGetMaxPostSize(t, rctx, ss) })
t.Run("GetParentsForExportAfter", func(t *testing.T) { testPostStoreGetParentsForExportAfter(t, rctx, ss) })
t.Run("GetRepliesForExport", func(t *testing.T) { testPostStoreGetRepliesForExport(t, rctx, ss) })
t.Run("GetDirectPostParentsForExportAfter", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfter(t, rctx, ss, s) })
t.Run("GetDirectPostParentsForExportAfterDeleted", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterDeleted(t, rctx, ss, s) })
t.Run("GetDirectPostParentsForExportAfterBatched", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterBatched(t, rctx, ss, s) })
t.Run("GetForThread", func(t *testing.T) { testPostStoreGetForThread(t, rctx, ss) })
t.Run("HasAutoResponsePostByUserSince", func(t *testing.T) { testHasAutoResponsePostByUserSince(t, rctx, ss) })
t.Run("GetPostsSinceUpdateForSync", func(t *testing.T) { testGetPostsSinceUpdateForSync(t, rctx, ss, s) })
t.Run("GetPostsSinceCreateForSync", func(t *testing.T) { testGetPostsSinceCreateForSync(t, rctx, ss, s) })
t.Run("GetPostsSinceForSyncExcludeMetadata", func(t *testing.T) { testGetPostsSinceForSyncExcludeMetadata(t, rctx, ss, s) })
t.Run("GetPostsSinceForSyncExcludedPostTypes", func(t *testing.T) { testGetPostsSinceForSyncExcludedPostTypes(t, rctx, ss, s) })
t.Run("GetPostsForReporting", func(t *testing.T) { testGetPostsForReporting(t, rctx, ss, s) })
t.Run("SetPostReminder", func(t *testing.T) { testSetPostReminder(t, rctx, ss, s) })
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, rctx, ss, s) })
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, rctx, ss, s) })
t.Run("DeleteAllPostRemindersForPost", func(t *testing.T) { testDeleteAllPostRemindersForPost(t, rctx, ss, s) })
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, rctx, ss) })
t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, rctx, ss) })
t.Run("RestoreContentFlaggedPost", func(t *testing.T) { testRestoreContentFlaggedPost(t, rctx, ss) })
}
func testPostStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Save post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestID()
p, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
})
t.Run("Save replies", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestID()
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestID()
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestID()
p1, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p1.ReplyCount)
p2, err := ss.Post().Save(rctx, &o2)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(2), p2.ReplyCount)
p3, err := ss.Post().Save(rctx, &o3)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(1), p3.ReplyCount)
})
t.Run("Try to save existing post", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestID()
_, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err, "couldn't save item")
_, err = ss.Post().Save(rctx, &o1)
require.Error(t, err, "shouldn't be able to update from save")
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestID()
_, err = ss.Post().Save(rctx, &rootPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestID()
replyPost.RootId = rootPost.Id
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(rctx, &replyPost)
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rctx, rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost.UpdateAt, rootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestID()
channel.DisplayName = NewTestID()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(rctx, &channel, 100)
require.NoError(t, err)
post := model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestID()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(rctx, &post)
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(1), rchannel.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestID()
post.CreateAt = 5
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(rctx, &post)
require.NoError(t, err)
rchannel2, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Equal(t, rchannel.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(2), rchannel2.TotalMsgCount)
post = model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestID()
// We need to sleep here to be sure the post is not created during the same millisecond
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(rctx, &post)
require.NoError(t, err)
rchannel3, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel3.LastPostAt, rchannel2.LastPostAt)
assert.Equal(t, int64(3), rchannel3.TotalMsgCount)
})
t.Run("Save post with type card", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1.Type = model.PostTypeCard
p, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, model.PostTypeCard, p.Type)
assert.Equal(t, o1.Message, p.Message)
})
t.Run("Save post with priority metadata set", func(t *testing.T) {
o1 := model.Post{}
o1.ChannelId = model.NewId()
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewPointer("important"),
RequestedAck: model.NewPointer(true),
PersistentNotifications: model.NewPointer(false),
},
}
p, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, int64(0), p.ReplyCount)
pp, err := ss.PostPriority().GetForPost(p.Id)
require.NoError(t, err, "couldn't save item")
assert.Equal(t, "important", *pp.Priority)
assert.Equal(t, true, *pp.RequestedAck)
assert.Equal(t, false, *pp.PersistentNotifications)
})
}
func testPostStoreSaveMultiple(t *testing.T, rctx request.CTX, ss store.Store) {
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestID()
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestID()
p3 := model.Post{}
p3.ChannelId = model.NewId()
p3.UserId = model.NewId()
p3.Message = NewTestID()
p4 := model.Post{}
p4.ChannelId = model.NewId()
p4.UserId = model.NewId()
p4.Message = NewTestID()
t.Run("Save correctly a new set of posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p1, &p2, &p3})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
for _, post := range newPosts {
storedPost, err := ss.Post().GetSingle(rctx, post.Id, false)
assert.NoError(t, err)
assert.Equal(t, post.ChannelId, storedPost.ChannelId)
assert.Equal(t, post.Message, storedPost.Message)
assert.Equal(t, post.UserId, storedPost.UserId)
}
})
t.Run("Save replies", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.RootId = model.NewId()
o1.Message = NewTestID()
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = o1.RootId
o2.Message = NewTestID()
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = model.NewId()
o3.Message = NewTestID()
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = model.NewId()
o4.Message = NewTestID()
newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&o1, &o2, &o3, &o4})
require.NoError(t, err, "couldn't save item")
require.Equal(t, -1, errIdx)
assert.Len(t, newPosts, 4)
assert.Equal(t, int64(2), newPosts[0].ReplyCount)
assert.Equal(t, int64(2), newPosts[1].ReplyCount)
assert.Equal(t, int64(1), newPosts[2].ReplyCount)
assert.Equal(t, int64(0), newPosts[3].ReplyCount)
})
t.Run("Try to save mixed, already saved and not saved posts", func(t *testing.T) {
newPosts, errIdx, err := ss.Post().SaveMultiple(rctx, []*model.Post{&p4, &p3})
require.Error(t, err)
require.Equal(t, 1, errIdx)
require.Nil(t, newPosts)
storedPost, err := ss.Post().GetSingle(rctx, p3.Id, false)
assert.NoError(t, err)
assert.Equal(t, p3.ChannelId, storedPost.ChannelId)
assert.Equal(t, p3.Message, storedPost.Message)
assert.Equal(t, p3.UserId, storedPost.UserId)
storedPost, err = ss.Post().GetSingle(rctx, p4.Id, false)
assert.Error(t, err)
assert.Nil(t, storedPost)
})
t.Run("Update reply should update the UpdateAt of the root post", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
rootPost := model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestID()
replyPost := model.Post{}
replyPost.ChannelId = rootPost.ChannelId
replyPost.UserId = model.NewId()
replyPost.Message = NewTestID()
replyPost.RootId = rootPost.Id
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&rootPost, &replyPost})
require.NoError(t, err)
rrootPost, err := ss.Post().GetSingle(rctx, rootPost.Id, false)
require.NoError(t, err)
assert.Equal(t, rrootPost.UpdateAt, rootPost.UpdateAt)
replyPost2 := model.Post{}
replyPost2.ChannelId = rootPost.ChannelId
replyPost2.UserId = model.NewId()
replyPost2.Message = NewTestID()
replyPost2.RootId = rootPost.Id
replyPost3 := model.Post{}
replyPost3.ChannelId = rootPost.ChannelId
replyPost3.UserId = model.NewId()
replyPost3.Message = NewTestID()
replyPost3.RootId = rootPost.Id
// Ensure update does not occur in the same timestamp as creation
time.Sleep(time.Millisecond)
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&replyPost2, &replyPost3})
require.NoError(t, err)
rrootPost2, err := ss.Post().GetSingle(rctx, rootPost.Id, false)
require.NoError(t, err)
assert.Greater(t, rrootPost2.UpdateAt, rrootPost.UpdateAt)
})
t.Run("Create a post should update the channel LastPostAt and the total messages count by one", func(t *testing.T) {
channel := model.Channel{}
channel.Name = NewTestID()
channel.DisplayName = NewTestID()
channel.Type = model.ChannelTypeOpen
_, err := ss.Channel().Save(rctx, &channel, 100)
require.NoError(t, err)
post1 := model.Post{}
post1.ChannelId = channel.Id
post1.UserId = model.NewId()
post1.Message = NewTestID()
post2 := model.Post{}
post2.ChannelId = channel.Id
post2.UserId = model.NewId()
post2.Message = NewTestID()
post2.CreateAt = 5
post3 := model.Post{}
post3.ChannelId = channel.Id
post3.UserId = model.NewId()
post3.Message = NewTestID()
_, _, err = ss.Post().SaveMultiple(rctx, []*model.Post{&post1, &post2, &post3})
require.NoError(t, err)
rchannel, err := ss.Channel().Get(channel.Id, false)
require.NoError(t, err)
assert.Greater(t, rchannel.LastPostAt, channel.LastPostAt)
assert.Equal(t, int64(3), rchannel.TotalMsgCount)
})
t.Run("Thread participants", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = "jessica hyde" + model.NewId() + "b"
root, err := ss.Post().Save(rctx, &o1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel4, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel5, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName5",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o2 := model.Post{}
o2.ChannelId = channel2.Id
o2.UserId = model.NewId()
o2.RootId = root.Id
o2.Message = "zz" + model.NewId() + "b"
o3 := model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = model.NewId()
o3.RootId = root.Id
o3.Message = "zz" + model.NewId() + "b"
o4 := model.Post{}
o4.ChannelId = channel4.Id
o4.UserId = o2.UserId
o4.RootId = root.Id
o4.Message = "zz" + model.NewId() + "b"
o5 := model.Post{}
o5.ChannelId = channel5.Id
o5.UserId = o1.UserId
o5.RootId = root.Id
o5.Message = "zz" + model.NewId() + "b"
_, err = ss.Post().Save(rctx, &o2)
require.NoError(t, err)
thread, errT := ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(1), thread.ReplyCount)
assert.Equal(t, int(1), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId}, thread.Participants)
_, err = ss.Post().Save(rctx, &o3)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(2), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o2.UserId, o3.UserId}, thread.Participants)
_, err = ss.Post().Save(rctx, &o4)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(3), thread.ReplyCount)
assert.Equal(t, int(2), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId}, thread.Participants)
_, err = ss.Post().Save(rctx, &o5)
require.NoError(t, err)
thread, errT = ss.Thread().Get(root.Id)
require.NoError(t, errT)
assert.Equal(t, int64(4), thread.ReplyCount)
assert.Equal(t, int(3), len(thread.Participants))
assert.Equal(t, model.StringArray{o3.UserId, o2.UserId, o1.UserId}, thread.Participants)
})
}
func testPostStoreSaveChannelMsgCounts(t *testing.T, rctx request.CTX, ss store.Store) {
c1 := &model.Channel{Name: model.NewId(), DisplayName: "posttestchannel", Type: model.ChannelTypeOpen, TeamId: model.NewId()}
_, err := ss.Channel().Save(rctx, c1, 1000000)
require.NoError(t, err)
o1 := model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
_, err = ss.Post().Save(rctx, &o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should update by 1")
o1.Id = ""
o1.Type = model.PostTypeAddToTeam
_, err = ss.Post().Save(rctx, &o1)
require.NoError(t, err)
o1.Id = ""
o1.Type = model.PostTypeRemoveFromTeam
_, err = ss.Post().Save(rctx, &o1)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, int64(1), c1.TotalMsgCount, "Message count should not update for team add/removed message")
oldLastPostAt := c1.LastPostAt
o2 := model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.CreateAt = int64(7)
_, err = ss.Post().Save(rctx, &o2)
require.NoError(t, err)
c1, err = ss.Channel().Get(c1.Id, false)
require.NoError(t, err)
assert.Equal(t, oldLastPostAt, c1.LastPostAt, "LastPostAt should not update for old message save")
}
func testPostStoreGet(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
etag1 := ss.Post().GetEtag(o1.ChannelId, false, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
etag2 := ss.Post().GetEtag(o1.ChannelId, false, false, false)
require.Equal(t, 0, strings.Index(etag2, fmt.Sprintf("%v.%v", model.CurrentVersion, o1.UpdateAt)), "Invalid Etag")
r1, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
_, err = ss.Post().Get(rctx, "123", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Missing id should have failed")
_, err = ss.Post().Get(rctx, "", model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "should fail for blank post ids")
}
func testPostStoreGetForThread(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("Post thread is followed", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestID()}
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: true,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.True(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post thread is explicitly not followed", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestID()}
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(o1.UserId, o1.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.False(t, *r1.Posts[o1.Id].IsFollowing)
})
t.Run("Post threadmembership does not exist", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestID()}
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
}
r1, err := ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[o1.Id].CreateAt, o1.CreateAt, "invalid returned post")
require.Nil(t, r1.Posts[o1.Id].IsFollowing)
})
t.Run("Pagination", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
now := model.GetMillis()
o1, err := ss.Post().Save(rctx, &model.Post{CreateAt: now, ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestID()})
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{CreateAt: now + 1, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
m1, err := ss.Post().Save(rctx, &model.Post{CreateAt: now + 2, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{CreateAt: now + 3, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
_, err = ss.Post().Save(rctx, &model.Post{CreateAt: now + 4, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id})
require.NoError(t, err)
opts := model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
}
r1, err := ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3) // including the root post
require.Len(t, r1.Posts, 3)
assert.True(t, *r1.HasNext)
lastPostID := r1.Order[len(r1.Order)-1]
lastPostCreateAt := r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3) // including the root post
require.Len(t, r1.Posts, 3)
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, *r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt := r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: true,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3) // including the root post
require.Len(t, r1.Posts, 3)
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, *r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "up",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[r1.Order[0]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[1]].ReplyCount, int64(4))
require.Len(t, r1.Order, 2) // including the root post
require.Len(t, r1.Posts, 2)
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, *r1.HasNext)
// Non-CRT mode
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "down",
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 2)
require.Len(t, r1.Posts, 2)
assert.True(t, *r1.HasNext)
lastPostID = r1.Order[len(r1.Order)-1]
lastPostCreateAt = r1.Posts[lastPostID].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 3,
Direction: "down",
FromPost: lastPostID,
FromCreateAt: lastPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Posts[r1.Order[0]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[1]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[2]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[3]].ReplyCount, int64(4))
require.Len(t, r1.Order, 4) // including the root post
require.Len(t, r1.Posts, 4)
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].CreateAt, lastPostCreateAt)
assert.False(t, *r1.HasNext)
// Going from bottom to top now.
firstPostCreateAt = r1.Posts[r1.Order[1]].CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 2,
Direction: "up",
FromPost: r1.Order[1],
FromCreateAt: firstPostCreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 2)
require.Len(t, r1.Posts, 2)
assert.LessOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, firstPostCreateAt)
assert.False(t, *r1.HasNext)
// Only with CreateAt
opts = model.GetPostsOptions{
CollapsedThreads: false,
PerPage: 1,
Direction: "down",
FromCreateAt: m1.CreateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 2) // including the root post
require.Len(t, r1.Posts, 2)
assert.GreaterOrEqual(t, r1.Posts[r1.Order[1]].CreateAt, m1.CreateAt)
assert.True(t, *r1.HasNext)
})
t.Run("Pagination with UpdateAt", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
now := model.GetMillis()
o1 := &model.Post{CreateAt: now, ChannelId: channel.Id, UserId: model.NewId(), Message: NewTestID()}
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
// Create replies with explicit UpdateAt timestamps
o2 := &model.Post{CreateAt: now + 1, UpdateAt: now + 1, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id}
_, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
m1 := &model.Post{CreateAt: now + 2, UpdateAt: now + 2, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id}
m1, err = ss.Post().Save(rctx, m1)
require.NoError(t, err)
o3 := &model.Post{CreateAt: now + 3, UpdateAt: now + 3, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id}
_, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
o4 := &model.Post{CreateAt: now + 4, UpdateAt: now + 4, ChannelId: o1.ChannelId, UserId: model.NewId(), Message: NewTestID(), RootId: o1.Id}
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
// Test pagination with UpdateAt in "down" direction
opts := model.GetPostsOptions{
UpdatesOnly: true,
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
}
r1, err := ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3) // including the root post
require.Len(t, r1.Posts, 3)
assert.Equal(t, r1.Posts[r1.Order[0]].UpdateAt, o4.CreateAt) // The root post always get updated with the createAt of the latest post in the thread.
assert.True(t, *r1.HasNext)
lastPostID := r1.Order[len(r1.Order)-1]
lastPostUpdateAt := r1.Posts[lastPostID].UpdateAt
// Continue pagination using UpdateAt
opts = model.GetPostsOptions{
UpdatesOnly: true,
CollapsedThreads: true,
PerPage: 2,
Direction: "down",
FromPost: lastPostID,
FromUpdateAt: lastPostUpdateAt,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3) // including the root post
require.Len(t, r1.Posts, 3)
assert.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].UpdateAt, lastPostUpdateAt)
assert.Equal(t, r1.Posts[r1.Order[0]].UpdateAt, o4.CreateAt) // The root post always get updated with the createAt of the latest post in the thread.
assert.False(t, *r1.HasNext)
// Non-CRT mode with UpdateAt pagination
opts = model.GetPostsOptions{
UpdatesOnly: true,
CollapsedThreads: false,
PerPage: 2,
Direction: "down",
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
// Ordering by updateAt will move the root post down, so we will get more posts in the thread.
require.Len(t, r1.Order, 3)
require.Len(t, r1.Posts, 3)
require.True(t, *r1.HasNext)
lastPostID = r1.Order[len(r1.Order)-1]
lastPostUpdateAt = r1.Posts[lastPostID].UpdateAt
opts = model.GetPostsOptions{
UpdatesOnly: true,
CollapsedThreads: false,
PerPage: 3,
Direction: "down",
FromPost: lastPostID,
FromUpdateAt: lastPostUpdateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 3)
require.Len(t, r1.Posts, 3)
require.Equal(t, r1.Posts[r1.Order[0]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[1]].ReplyCount, int64(4))
require.Equal(t, r1.Posts[r1.Order[2]].ReplyCount, int64(4))
require.GreaterOrEqual(t, r1.Posts[r1.Order[len(r1.Order)-1]].UpdateAt, lastPostUpdateAt)
assert.False(t, *r1.HasNext)
// Only with UpdateAt - direction down
opts = model.GetPostsOptions{
UpdatesOnly: true,
CollapsedThreads: false,
PerPage: 1,
Direction: "down",
FromUpdateAt: m1.UpdateAt,
SkipFetchThreads: false,
}
r1, err = ss.Post().Get(rctx, o1.Id, opts, o1.UserId, map[string]bool{})
require.NoError(t, err)
require.Len(t, r1.Order, 2)
require.Len(t, r1.Posts, 2)
require.GreaterOrEqual(t, r1.Posts[r1.Order[1]].UpdateAt, m1.UpdateAt)
require.True(t, *r1.HasNext)
})
}
func testPostStoreGetSingle(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = o1.UserId
o2.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = o1.UserId
o3.Message = model.NewRandomString(10)
o3.RootId = o1.Id
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = o1.UserId
o4.Message = model.NewRandomString(10)
o4.RootId = o1.Id
_, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
err = ss.Post().Delete(rctx, o2.Id, model.GetMillis(), o2.UserId)
require.NoError(t, err)
err = ss.Post().Delete(rctx, o4.Id, model.GetMillis(), o4.UserId)
require.NoError(t, err)
post, err := ss.Post().GetSingle(rctx, o1.Id, false)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o1.CreateAt, "invalid returned post")
require.Equal(t, int64(1), post.ReplyCount, "wrong replyCount computed")
_, err = ss.Post().GetSingle(rctx, o2.Id, false)
require.Error(t, err, "should not return deleted post")
post, err = ss.Post().GetSingle(rctx, o2.Id, true)
require.NoError(t, err)
require.Equal(t, post.CreateAt, o2.CreateAt, "invalid returned post")
require.NotZero(t, post.DeleteAt, "DeleteAt should be non-zero")
require.Zero(t, post.ReplyCount, "Post without replies should return zero ReplyCount")
_, err = ss.Post().GetSingle(rctx, "123", false)
require.Error(t, err, "Missing id should have failed")
}
func testPostStoreUpdate(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
r1, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Update(rctx, o1a, ro1)
require.NoError(t, err)
r1, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
require.Equal(t, ro1a.Message, o1a.Message, "Failed to update/get")
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Update(rctx, o2a, ro2)
require.NoError(t, err)
r2, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
require.Equal(t, ro2a.Message, o2a.Message, "Failed to update/get")
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Update(rctx, o3a, ro3)
require.NoError(t, err)
r3, err = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
if ro3a.Message != o3a.Message {
require.Equal(t, ro3a.Hashtags, o3a.Hashtags, "Failed to update/get")
}
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r4, err := ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Update(rctx, o4a, ro4)
require.NoError(t, err)
r4, err = ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
}
func testPostStoreDelete(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("single post, no replies", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a post
rootPost, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: model.NewRandomString(10),
})
require.NoError(t, err)
// Verify etag generation for the channel containing the post.
etag1 := ss.Post().GetEtag(rootPost.ChannelId, false, false, false)
require.Equal(t, 0, strings.Index(etag1, model.CurrentVersion+"."), "Invalid Etag")
// Verify the created post.
r1, err := ss.Post().Get(rctx, rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.NotNil(t, r1.Posts[rootPost.Id])
require.Equal(t, rootPost, r1.Posts[rootPost.Id])
// Mark the post as deleted by the user identified with deleteByID.
deleteByID := model.NewId()
err = ss.Post().Delete(rctx, rootPost.Id, model.GetMillis(), deleteByID)
require.NoError(t, err)
// Ensure the appropriate posts prop reflects the user deleting the post.
posts, err := ss.Post().GetPostsCreatedAt(rootPost.ChannelId, rootPost.CreateAt)
require.NoError(t, err)
require.NotEmpty(t, posts)
assert.Equal(t, deleteByID, posts[0].GetProp(model.PostPropsDeleteBy), "unexpected Props[model.PostPropsDeleteBy]")
// Verify that the post is no longer fetched by default.
_, err = ss.Post().Get(rctx, rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "fetching deleted post should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify etag generation for the channel containing the now deleted post.
etag2 := ss.Post().GetEtag(rootPost.ChannelId, false, false, false)
require.Equal(t, 0, strings.Index(etag2, model.CurrentVersion+"."), "Invalid Etag")
})
t.Run("thread with one reply", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Reply to that root post
replyPost, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost.Id,
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rctx, rootPost.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post deleted
_, err = ss.Post().Get(rctx, rootPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify the reply post deleted
_, err = ss.Post().Get(rctx, replyPost.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
})
t.Run("thread with multiple replies", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create another root post in a separate channel
rootPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Delete the root post
err = ss.Post().Delete(rctx, rootPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the root post and replies deleted
_, err = ss.Post().Get(rctx, rootPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, replyPost1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, replyPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
// Verify other root posts remain undeleted.
_, err = ss.Post().Get(rctx, rootPost2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
})
t.Run("root post update at is updated upon reply delete", func(t *testing.T) {
teamId := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamId,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Reply to that root post
_, err = ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a third time
_, err = ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
updatedRootPost, err := ss.Post().GetSingle(rctx, rootPost1.Id, false)
require.NoError(t, err)
beforeDeleteTime := updatedRootPost.UpdateAt
// Delete the reply previous to last
err = ss.Post().Delete(rctx, replyPost2.Id, model.GetMillis(), "")
require.NoError(t, err)
updatedRootPost, err = ss.Post().GetSingle(rctx, rootPost1.Id, false)
require.NoError(t, err)
require.Greater(t, updatedRootPost.UpdateAt, beforeDeleteTime)
})
t.Run("thread with multiple replies, update thread last reply at", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Reply to that root post
replyPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a second time
replyPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
// Reply to that root post a third time
replyPost3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
thread, err := ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the reply previous to last
err = ss.Post().Delete(rctx, replyPost2.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be unchanged
require.Equal(t, replyPost3.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(rctx, replyPost3.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should have changed
require.Equal(t, replyPost1.CreateAt, thread.LastReplyAt)
// Delete the last reply
err = ss.Post().Delete(rctx, replyPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
thread, err = ss.Thread().Get(rootPost1.Id)
require.NoError(t, err)
// last reply at should be 0
require.Equal(t, int64(0), thread.LastReplyAt)
})
t.Run("thread with file attachments", func(t *testing.T) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
// Create a root post
rootPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Create another root post
rootPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: NewTestID(),
})
require.NoError(t, err)
// Reply to first root post with file attachments
replyPost1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost1.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost1.Id,
})
require.NoError(t, err)
file11, err := ss.FileInfo().Save(rctx, &model.FileInfo{
Id: model.NewId(),
PostId: replyPost1.Id,
CreatorId: replyPost1.UserId,
Path: "file1.txt",
})
require.NoError(t, err)
file12, err := ss.FileInfo().Save(rctx, &model.FileInfo{
Id: model.NewId(),
PostId: replyPost1.Id,
CreatorId: replyPost1.UserId,
Path: "file2.png",
})
require.NoError(t, err)
// Reply to second root post with file attachments
replyPost2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: rootPost2.ChannelId,
UserId: model.NewId(),
Message: NewTestID(),
RootId: rootPost2.Id,
})
require.NoError(t, err)
file21, err := ss.FileInfo().Save(rctx, &model.FileInfo{
Id: model.NewId(),
PostId: replyPost2.Id,
CreatorId: replyPost2.UserId,
Path: "file1.txt",
})
require.NoError(t, err)
// Delete the first root post
err = ss.Post().Delete(rctx, rootPost1.Id, model.GetMillis(), "")
require.NoError(t, err)
// Verify the reply post's files are deleted
_, err = ss.FileInfo().Get(file11.Id)
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
_, err = ss.FileInfo().Get(file12.Id)
require.Error(t, err, "Deleted id should have failed")
require.IsType(t, &store.ErrNotFound{}, err)
// Verify the other reply post's files are NOT deleted
_, err = ss.FileInfo().Get(file21.Id)
require.NoError(t, err, "Not deleted id should have succeeded")
})
}
func testPostStorePermDelete1Level(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
r1 := &model.Reaction{}
r1.ChannelId = o1.ChannelId
r1.UserId = o2.UserId
r1.PostId = o1.Id
r1.EmojiName = "smile"
r1, err = ss.Reaction().Save(r1)
require.NoError(t, err)
r2 := &model.Reaction{}
r2.ChannelId = o1.ChannelId
r2.UserId = o1.UserId
r2.PostId = o2.Id
r2.EmojiName = "wave"
_, err = ss.Reaction().Save(r2)
require.NoError(t, err)
r3 := &model.Reaction{}
r3.ChannelId = o1.ChannelId
r3.UserId = model.NewId()
r3.PostId = o1.Id
r3.EmojiName = "sad"
r3, err = ss.Reaction().Save(r3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
r4 := &model.Reaction{}
r4.ChannelId = channel2.Id
r4.UserId = model.NewId()
r4.PostId = o3.Id
r4.EmojiName = "angry"
_, err = ss.Reaction().Save(r4)
require.NoError(t, err)
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel3.Id
o4.RootId = o1.Id
o4.UserId = o2.UserId
o4.Message = NewTestID()
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = o3.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestID()
o5, err = ss.Post().Save(rctx, o5)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o3.ChannelId
o6.RootId = o5.Id
o6.UserId = model.NewId()
o6.Message = NewTestID()
o6, err = ss.Post().Save(rctx, o6)
require.NoError(t, err)
var thread *model.Thread
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 2, thread.ReplyCount)
require.EqualValues(t, model.StringArray{o2.UserId}, thread.Participants)
err2 := ss.Post().PermanentDeleteByUser(rctx, o2.UserId)
require.NoError(t, err2)
thread, err = ss.Thread().Get(o1.Id)
require.NoError(t, err)
require.EqualValues(t, 0, thread.ReplyCount)
require.EqualValues(t, model.StringArray{}, thread.Participants)
_, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id shouldn't have failed")
reactions, err := ss.Reaction().GetForPost(o1.Id, false)
require.NoError(t, err, "Reactions should exist")
require.Equal(t, 2, len(reactions))
emojis := []string{r1.EmojiName, r3.EmojiName}
for _, reaction := range reactions {
require.Contains(t, emojis, reaction.EmojiName)
}
_, err = ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
reactions, err = ss.Reaction().GetForPost(o2.Id, false)
require.NoError(t, err, "No error for not found")
require.Equal(t, 0, len(reactions))
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.NotEmpty(t, thread)
err = ss.Post().PermanentDeleteByChannel(rctx, o3.ChannelId)
require.NoError(t, err)
thread, err = ss.Thread().Get(o5.Id)
require.NoError(t, err)
require.Nil(t, thread)
reactions, err = ss.Reaction().GetForPost(o3.Id, false)
require.NoError(t, err, "No error for not found")
require.Equal(t, 0, len(reactions))
reactions, err = ss.Reaction().GetForPost(o1.Id, false)
require.NoError(t, err, "Reactions should exist")
require.Equal(t, 2, len(reactions))
for _, reaction := range reactions {
require.Contains(t, emojis, reaction.EmojiName)
}
_, err = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, o6.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
}
func testPostStorePermDelete1Level2(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
err2 := ss.Post().PermanentDeleteByUser(rctx, o1.UserId)
require.NoError(t, err2)
_, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Deleted id should have failed")
_, err = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Deleted id should have failed")
}
func testPostStorePermDeleteLimitExceeded(t *testing.T, rctx request.CTX, ss store.Store) {
const maxPosts = 10000
teamID := model.NewId()
userID := model.NewId()
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "10KPosts",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
for range maxPosts + 100 {
post := &model.Post{
ChannelId: channel.Id,
UserId: userID,
Message: NewTestID(),
}
_, err = ss.Post().Save(rctx, post)
require.NoError(t, err)
}
err = ss.Post().PermanentDeleteByUser(rctx, userID)
var errLimitExceeded *store.ErrLimitExceeded
require.ErrorAs(t, err, &errLimitExceeded)
}
func testPostStoreGetWithChildren(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.RootId = o1.Id
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
pl, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 3, "invalid returned post")
dErr := ss.Post().Delete(rctx, o3.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 2, "invalid returned post")
dErr = ss.Post().Delete(rctx, o2.Id, model.GetMillis(), "")
require.NoError(t, dErr)
pl, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
require.Len(t, pl.Posts, 1, "invalid returned post")
}
func testPostStoreGetPostsWithDetails(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
_, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = o1.ChannelId
o2a.UserId = model.NewId()
o2a.Message = NewTestID()
o2a.RootId = o1.Id
o2a, err = ss.Post().Save(rctx, o2a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.RootId = o1.Id
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = o1.ChannelId
o4.UserId = model.NewId()
o4.Message = NewTestID()
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o5 := &model.Post{}
o5.ChannelId = o1.ChannelId
o5.UserId = model.NewId()
o5.Message = NewTestID()
o5.RootId = o4.Id
o5, err = ss.Post().Save(rctx, o5)
require.NoError(t, err)
r1, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r1.Order[0], o5.Id, "invalid order")
require.Equal(t, r1.Order[1], o4.Id, "invalid order")
require.Equal(t, r1.Order[2], o3.Id, "invalid order")
require.Equal(t, r1.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r1.Posts, 6, "wrong size")
require.Equal(t, r1.Posts[o1.Id].Message, o1.Message, "Missing parent")
r2, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 4}, false, map[string]bool{})
require.NoError(t, err)
require.Equal(t, r2.Order[0], o5.Id, "invalid order")
require.Equal(t, r2.Order[1], o4.Id, "invalid order")
require.Equal(t, r2.Order[2], o3.Id, "invalid order")
require.Equal(t, r2.Order[3], o2a.Id, "invalid order")
//the last 4, + o1 (o2a and o3's parent) + o2 (in same thread as o2a and o3)
require.Len(t, r2.Posts, 6, "wrong size")
require.Equal(t, r2.Posts[o1.Id].Message, o1.Message, "Missing parent")
// Run once to fill cache
_, err = ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = o1.ChannelId
o6.UserId = model.NewId()
o6.Message = NewTestID()
_, err = ss.Post().Save(rctx, o6)
require.NoError(t, err)
r3, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: o1.ChannelId, Page: 0, PerPage: 30}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, 7, len(r3.Order))
}
func testPostStoreGetPostsBeforeAfter(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("without threads", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
var posts []*model.Post
for range 10 {
post, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
posts = append(posts, post)
time.Sleep(time.Millisecond)
}
t.Run("should return error if negative Page/PerPage options are passed", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[0].Id, Page: 0, PerPage: -1}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
postList, err = ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[0].Id, Page: -1, PerPage: 10}, map[string]bool{})
assert.Nil(t, postList)
assert.Error(t, err)
assert.IsType(t, &store.ErrInvalidInput{}, err)
})
t.Run("should not return anything before the first post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[0].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[5].Id, Page: 0, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id, posts[2].Id, posts[1].Id, posts[0].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[0].Id: posts[0],
posts[1].Id: posts[1],
posts[2].Id: posts[2],
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should limit posts before", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[4].Id, posts[3].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[3].Id: posts[3],
posts[4].Id: posts[4],
}, postList.Posts)
})
t.Run("should not return anything after the last post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[len(posts)-1].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Equal(t, map[string]*model.Post{}, postList.Posts)
})
t.Run("should return posts after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[5].Id, PerPage: 10}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[9].Id, posts[8].Id, posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
posts[8].Id: posts[8],
posts[9].Id: posts[9],
}, postList.Posts)
})
t.Run("should limit posts after", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: posts[5].Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{posts[7].Id, posts[6].Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
posts[6].Id: posts[6],
posts[7].Id: posts[7],
}, postList.Posts)
})
})
t.Run("with threads", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
post1.ReplyCount = 1
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post1.Id,
Message: "message",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "message",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "message",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
post4.Id: post4,
post6.Id: post6,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post4.Id: post4,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (skipFetchThreads)", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each post and thread before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id, post2.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post2.Id: post2,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and thread before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 1, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post3.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post1.Id: post1,
post3.Id: post3,
}, postList.Posts)
})
t.Run("should return each post and the root of each thread after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2, SkipFetchThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post6.Id, post5.Id}, postList.Order)
assert.Equal(t, map[string]*model.Post{
post2.Id: post2,
post5.Id: post5,
post6.Id: post6,
}, postList.Posts)
})
})
t.Run("with threads (collapsedThreads)", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
// This creates a series of posts that looks like:
// post1
// post2
// post3 (in response to post1)
// post4 (in response to post2)
// post5
// post6 (in response to post2)
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post1",
})
require.NoError(t, err)
post1.ReplyCount = 1
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post2",
})
require.NoError(t, err)
post2.ReplyCount = 2
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post1.Id,
Message: "post3",
})
require.NoError(t, err)
post3.ReplyCount = 1
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "post4",
})
require.NoError(t, err)
post4.ReplyCount = 2
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "post5",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
RootId: post2.Id,
Message: "post6",
})
post6.ReplyCount = 2
require.NoError(t, err)
// Adding a post to a thread changes the UpdateAt timestamp of the parent post
post1.UpdateAt = post3.UpdateAt
post2.UpdateAt = post6.UpdateAt
t.Run("should return each root post before a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id, post1.Id}, postList.Order)
})
t.Run("should return each root post before a post with limit", func(t *testing.T) {
postList, err := ss.Post().GetPostsBefore(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 1, CollapsedThreads: true}, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{post2.Id}, postList.Order)
})
t.Run("should return each root after a post", func(t *testing.T) {
postList, err := ss.Post().GetPostsAfter(rctx, model.GetPostsOptions{ChannelId: channelID, PostId: post4.Id, PerPage: 2, CollapsedThreads: true}, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post5.Id}, postList.Order)
})
})
}
func testPostStoreGetPostsSince(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
_, err = ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(rctx, model.GetPostsSinceOptions{ChannelId: channelID, Time: post3.CreateAt}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 5)
assert.NotNil(t, postList.Posts[post1.Id], "should return the parent post")
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return empty list when nothing has changed", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
postList, err := ss.Post().GetPostsSince(rctx, model.GetPostsSinceOptions{ChannelId: channelID, Time: post1.CreateAt}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{}, postList.Order)
assert.Empty(t, postList.Posts)
})
t.Run("should not cache a timestamp of 0 when nothing has changed", func(t *testing.T) {
ss.Post().ClearCaches()
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
// Make a request that returns no results
postList, err := ss.Post().GetPostsSince(rctx, model.GetPostsSinceOptions{ChannelId: channelID, Time: post1.CreateAt}, true, map[string]bool{})
require.NoError(t, err)
require.Equal(t, model.NewPostList(), postList)
// And then ensure that it doesn't cause future requests to also return no results
postList, err = ss.Post().GetPostsSince(rctx, model.GetPostsSinceOptions{ChannelId: channelID, Time: post1.CreateAt - 1}, true, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{post1.Id}, postList.Order)
assert.Len(t, postList.Posts, 1)
assert.NotNil(t, postList.Posts[post1.Id])
})
}
func testPostStoreGetPosts(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
post1, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
RootId: post3.Id,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post6, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
RootId: post1.Id,
})
require.NoError(t, err)
t.Run("should return the last posts created in a channel", func(t *testing.T) {
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 30, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return the last posts created in a channel and the threads and the reply count must be 0", func(t *testing.T) {
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 2, SkipFetchThreads: false}, false, map[string]bool{})
assert.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
require.NotNil(t, postList.Posts[post1.Id])
require.NotNil(t, postList.Posts[post3.Id])
require.NotNil(t, postList.Posts[post5.Id])
require.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(0), postList.Posts[post1.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post3.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(0), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the last posts created in a channel without the threads and the reply count must be correct", func(t *testing.T) {
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 2, SkipFetchThreads: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 4)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return all posts in a channel included deleted posts", func(t *testing.T) {
err := ss.Post().Delete(rctx, post1.Id, 1, userID)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 30, SkipFetchThreads: false, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post1.Id])
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
})
t.Run("should return all posts in a channel included deleted posts without threads", func(t *testing.T) {
err := ss.Post().Delete(rctx, post5.Id, 1, userID)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: true}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post6.Id,
post5.Id,
post4.Id,
post3.Id,
post2.Id,
post1.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 6)
assert.NotNil(t, postList.Posts[post5.Id])
assert.NotNil(t, postList.Posts[post6.Id])
assert.Equal(t, int64(1), postList.Posts[post5.Id].ReplyCount)
assert.Equal(t, int64(1), postList.Posts[post6.Id].ReplyCount)
})
t.Run("should return the lasts posts created in channel without include deleted posts", func(t *testing.T) {
err := ss.Post().Delete(rctx, post6.Id, 1, userID)
require.NoError(t, err)
postList, err := ss.Post().GetPosts(rctx, model.GetPostsOptions{ChannelId: channelID, Page: 0, PerPage: 30, SkipFetchThreads: true, IncludeDeleted: false}, false, map[string]bool{})
require.NoError(t, err)
assert.Equal(t, []string{
post4.Id,
post3.Id,
post2.Id,
}, postList.Order)
assert.Len(t, postList.Posts, 3)
assert.NotNil(t, postList.Posts[post2.Id])
assert.NotNil(t, postList.Posts[post3.Id])
assert.NotNil(t, postList.Posts[post4.Id])
})
}
func testPostStoreGetPostBeforeAfter(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
o0 := &model.Post{}
o0.ChannelId = channelID
o0.UserId = model.NewId()
o0.Message = NewTestID()
_, err = ss.Post().Save(rctx, o0)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o1 := &model.Post{}
o1.ChannelId = channelID
o1.Type = model.PostTypeJoinChannel
o1.UserId = model.NewId()
o1.Message = "system_join_channel message"
_, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0a := &model.Post{}
o0a.ChannelId = channelID
o0a.UserId = model.NewId()
o0a.Message = NewTestID()
o0a.RootId = o1.Id
_, err = ss.Post().Save(rctx, o0a)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o0b := &model.Post{}
o0b.ChannelId = channelID
o0b.UserId = model.NewId()
o0b.Message = "deleted message"
o0b.RootId = o1.Id
o0b.DeleteAt = 1
_, err = ss.Post().Save(rctx, o0b)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
otherChannelPost := &model.Post{}
otherChannelPost.ChannelId = channel2.Id
otherChannelPost.UserId = model.NewId()
otherChannelPost.Message = NewTestID()
_, err = ss.Post().Save(rctx, otherChannelPost)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = channelID
o2.UserId = model.NewId()
o2.Message = NewTestID()
_, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2a := &model.Post{}
o2a.ChannelId = channelID
o2a.UserId = model.NewId()
o2a.Message = NewTestID()
o2a.RootId = o2.Id
_, err = ss.Post().Save(rctx, o2a)
require.NoError(t, err)
rPostID1, err := ss.Post().GetPostIdBeforeTime(channelID, o0a.CreateAt, false)
require.Equal(t, rPostID1, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostID1, err = ss.Post().GetPostIdAfterTime(channelID, o0b.CreateAt, false)
require.Equal(t, rPostID1, o2.Id, "should return before post o2")
require.NoError(t, err)
rPost1, err := ss.Post().GetPostAfterTime(channelID, o0b.CreateAt, false)
require.Equal(t, rPost1.Id, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostID2, err := ss.Post().GetPostIdBeforeTime(channelID, o0.CreateAt, false)
require.Empty(t, rPostID2, "should return no post")
require.NoError(t, err)
rPostID2, err = ss.Post().GetPostIdAfterTime(channelID, o0.CreateAt, false)
require.Equal(t, rPostID2, o1.Id, "should return before post o1")
require.NoError(t, err)
rPost2, err := ss.Post().GetPostAfterTime(channelID, o0.CreateAt, false)
require.Equal(t, rPost2.Id, o1.Id, "should return before post o1")
require.NoError(t, err)
rPostID3, err := ss.Post().GetPostIdBeforeTime(channelID, o2a.CreateAt, false)
require.Equal(t, rPostID3, o2.Id, "should return before post o2")
require.NoError(t, err)
rPostID3, err = ss.Post().GetPostIdAfterTime(channelID, o2a.CreateAt, false)
require.Empty(t, rPostID3, "should return no post")
require.NoError(t, err)
rPost3, err := ss.Post().GetPostAfterTime(channelID, o2a.CreateAt, false)
require.Empty(t, rPost3.Id, "should return no post")
require.NoError(t, err)
}
func testUserCountsWithPostsByDay(t *testing.T, rctx request.CTX, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestID()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestID()
o1, nErr = ss.Post().Save(rctx, o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestID()
_, nErr = ss.Post().Save(rctx, o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2.Message = NewTestID()
o2, nErr = ss.Post().Save(rctx, o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
o2a.Message = NewTestID()
_, nErr = ss.Post().Save(rctx, o2a)
require.NoError(t, nErr)
r1, err := ss.Post().AnalyticsUserCountsWithPostsByDay(t1.Id)
require.NoError(t, err)
row1 := r1[0]
require.Equal(t, float64(2), row1.Value, "wrong value")
row2 := r1[1]
require.Equal(t, float64(1), row2.Value, "wrong value")
}
func testPostCountsByDay(t *testing.T, rctx request.CTX, ss store.Store) {
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestID()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, nErr)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.CreateAt = utils.MillisFromTime(utils.Yesterday())
o1.Message = NewTestID()
o1.Hashtags = "hashtag"
o1, nErr = ss.Post().Save(rctx, o1)
require.NoError(t, nErr)
o1a := &model.Post{}
o1a.ChannelId = c1.Id
o1a.UserId = model.NewId()
o1a.CreateAt = o1.CreateAt
o1a.Message = NewTestID()
o1a.FileIds = []string{"fileId1"}
_, nErr = ss.Post().Save(rctx, o1a)
require.NoError(t, nErr)
o2 := &model.Post{}
o2.ChannelId = c1.Id
o2.UserId = model.NewId()
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2.Message = NewTestID()
o2.Filenames = []string{"filename1"}
o2, nErr = ss.Post().Save(rctx, o2)
require.NoError(t, nErr)
o2a := &model.Post{}
o2a.ChannelId = c1.Id
o2a.UserId = o2.UserId
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
o2a.Message = NewTestID()
o2a.Hashtags = "hashtag"
o2a.FileIds = []string{"fileId2"}
_, nErr = ss.Post().Save(rctx, o2a)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
b1 := &model.Post{}
b1.Message = "bot message one"
b1.ChannelId = c1.Id
b1.UserId = bot1.UserId
b1.CreateAt = utils.MillisFromTime(utils.Yesterday())
_, nErr = ss.Post().Save(rctx, b1)
require.NoError(t, nErr)
b1a := &model.Post{}
b1a.Message = "bot message two"
b1a.ChannelId = c1.Id
b1a.UserId = bot1.UserId
b1a.CreateAt = utils.MillisFromTime(utils.Yesterday()) - (1000 * 60 * 60 * 24 * 2)
_, nErr = ss.Post().Save(rctx, b1a)
require.NoError(t, nErr)
require.NoError(t, ss.Post().RefreshPostStats())
time.Sleep(1 * time.Second)
// summary of posts
// yesterday - 2 non-bot user posts, 1 bot user post
// 3 days ago - 2 non-bot user posts, 1 bot user post
// last 31 days, all users (including bots)
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: false}
r1, err := ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
assert.Equal(t, float64(3), r1[1].Value)
assert.Equal(t, utils.Yesterday().Format("2006-01-02"), r1[0].Name)
assert.Equal(t, utils.Yesterday().Add(-48*time.Hour).Format("2006-01-02"), r1[1].Name)
// last 31 days, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: false}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
assert.Equal(t, float64(1), r1[1].Value)
assert.Equal(t, utils.Yesterday().Format("2006-01-02"), r1[0].Name)
assert.Equal(t, utils.Yesterday().Add(-48*time.Hour).Format("2006-01-02"), r1[1].Name)
// yesterday only, all users (including bots)
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: false, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(3), r1[0].Value)
assert.Equal(t, utils.Yesterday().Format("2006-01-02"), r1[0].Name)
// yesterday only, bots only
postCountsOptions = &model.AnalyticsPostCountsOptions{TeamId: t1.Id, BotsOnly: true, YesterdayOnly: true}
r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions)
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
assert.Equal(t, utils.Yesterday().Format("2006-01-02"), r1[0].Name)
}
func testPostCounts(t *testing.T, rctx request.CTX, ss store.Store) {
now := time.Now()
twentyMinAgo := now.Add(-20 * time.Minute).UnixMilli()
fifteenMinAgo := now.Add(-15 * time.Minute).UnixMilli()
tenMinAgo := now.Add(-10 * time.Minute).UnixMilli()
t1 := &model.Team{}
t1.DisplayName = "DisplayName"
t1.Name = NewTestID()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
t1, err := ss.Team().Save(t1)
require.NoError(t, err)
c1 := &model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel2"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, nErr := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, nErr)
// system post
p1 := &model.Post{}
p1.Type = "system_add_to_channel"
p1.ChannelId = c1.Id
p1.UserId = model.NewId()
p1.Message = NewTestID()
p1.CreateAt = twentyMinAgo
p1.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = model.NewId()
p2.Message = NewTestID()
p2.Hashtags = "hashtag"
p2.CreateAt = twentyMinAgo
p2.UpdateAt = twentyMinAgo
p2, nErr = ss.Post().Save(rctx, p2)
require.NoError(t, nErr)
p3 := &model.Post{}
p3.ChannelId = c1.Id
p3.UserId = model.NewId()
p3.Message = NewTestID()
p3.FileIds = []string{"fileId1"}
p3.CreateAt = twentyMinAgo
p3.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(rctx, p3)
require.NoError(t, nErr)
p4 := &model.Post{}
p4.ChannelId = c1.Id
p4.UserId = model.NewId()
p4.Message = NewTestID()
p4.Filenames = []string{"filename1"}
p4.CreateAt = tenMinAgo
p4.UpdateAt = tenMinAgo
p4, nErr = ss.Post().Save(rctx, p4)
require.NoError(t, nErr)
p5 := &model.Post{}
p5.ChannelId = c1.Id
p5.UserId = p4.UserId
p5.Message = NewTestID()
p5.Hashtags = "hashtag"
p5.FileIds = []string{"fileId2"}
p5.CreateAt = tenMinAgo
p5.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(rctx, p5)
require.NoError(t, nErr)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, nErr = ss.Bot().Save(bot1)
require.NoError(t, nErr)
p6 := &model.Post{}
p6.Message = "bot message one"
p6.ChannelId = c1.Id
p6.UserId = bot1.UserId
p6.CreateAt = twentyMinAgo
p6.UpdateAt = twentyMinAgo
_, nErr = ss.Post().Save(rctx, p6)
require.NoError(t, nErr)
p7 := &model.Post{}
p7.Message = "bot message two"
p7.ChannelId = c1.Id
p7.UserId = bot1.UserId
p7.CreateAt = tenMinAgo
p7.UpdateAt = tenMinAgo
_, nErr = ss.Post().Save(rctx, p7)
require.NoError(t, nErr)
require.NoError(t, ss.Post().RefreshPostStats())
// total across all teams
c, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
assert.GreaterOrEqual(t, c, int64(7))
// total for single team
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id})
require.NoError(t, err)
assert.Equal(t, int64(7), c)
c, err = ss.Post().AnalyticsPostCountByTeam(t1.Id)
require.NoError(t, err)
assert.Equal(t, int64(7), c)
// with files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// with hashtags
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// with hashtags and files
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true, MustHaveHashtag: true})
require.NoError(t, err)
assert.Equal(t, int64(1), c)
// excluding system posts
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeSystemPosts: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// before update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: fifteenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// equal to update_at time
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
// since update_at and since post id
tenMinAgoIDs := []string{p4.Id, p5.Id, p7.Id}
sort.Strings(tenMinAgoIDs)
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo, SincePostID: tenMinAgoIDs[0]})
require.NoError(t, err)
assert.Equal(t, int64(2), c)
// delete 1 post
err = ss.Post().Delete(rctx, p2.Id, 1, p2.UserId)
require.NoError(t, err)
// total for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(6), c)
// total users only posts for single team with the deleted post excluded
c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true})
require.NoError(t, err)
assert.Equal(t, int64(3), c)
}
func testPostStoreGetFlaggedPostsForTeam(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.DeleteAt = 1
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = c1.Id
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(rctx, m0)
require.NoError(t, err)
teamID := model.NewId()
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestID()
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
c2 := &model.Channel{}
c2.DisplayName = "DMChannel1"
c2.Name = model.GetDMNameFromIds(NewTestID(), NewTestID())
c2.Type = model.ChannelTypeDirect
m1 := &model.ChannelMember{}
m1.ChannelId = c2.Id
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := &model.ChannelMember{}
m2.ChannelId = c2.Id
m2.UserId = model.NewId()
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
c2, err = ss.Channel().SaveDirectChannel(rctx, c2, m1, m2)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = c2.Id
o5.UserId = m2.UserId
o5.Message = NewTestID()
o5, err = ss.Post().Save(rctx, o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o6 := &model.Post{}
o6.ChannelId = channel3.Id
o6.UserId = m2.UserId
o6.Message = NewTestID()
o6, err = ss.Post().Save(rctx, o6)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
r1, err := ss.Post().GetFlaggedPosts(o1.ChannelId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r2, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r3, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, model.NewId(), 0, 2)
require.NoError(t, err)
require.Empty(t, r4.Order, "should have 0 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o5.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o6.Id,
Value: "true",
},
}
err = ss.Preference().Save(preferences)
require.NoError(t, err)
r4, err = ss.Post().GetFlaggedPostsForTeam(o1.UserId, c1.TeamId, 0, 10)
require.NoError(t, err)
require.Len(t, r4.Order, 3, "should have 3 posts")
// Manually truncate Channels table until testlib can handle cleanups
s.GetMaster().Exec("TRUNCATE Channels")
}
func testPostStoreGetFlaggedPosts(t *testing.T, rctx request.CTX, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.DeleteAt = 1
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
teamID := model.NewId()
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4 := &model.Post{}
o4.ChannelId = channel2.Id
o4.UserId = model.NewId()
o4.Message = NewTestID()
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m0 := &model.ChannelMember{}
m0.ChannelId = o1.ChannelId
m0.UserId = o1.UserId
m0.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(rctx, m0)
require.NoError(t, err)
r1, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Empty(t, r1.Order, "should be empty")
preferences := model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
},
}
nErr := ss.Preference().Save(preferences)
require.NoError(t, nErr)
r2, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r2.Order, 1, "should have 1 post")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o2.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r3, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1, 1)
require.NoError(t, err)
require.Len(t, r3.Order, 1, "should have 1 post")
r3, err = ss.Post().GetFlaggedPosts(o1.UserId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r3.Order, "should be empty")
r4, err := ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o3.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
preferences = model.Preferences{
{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o4.Id,
Value: "true",
},
}
nErr = ss.Preference().Save(preferences)
require.NoError(t, nErr)
r4, err = ss.Post().GetFlaggedPosts(o1.UserId, 0, 2)
require.NoError(t, err)
require.Len(t, r4.Order, 2, "should have 2 posts")
}
func testPostStoreGetFlaggedPostsForChannel(t *testing.T, rctx request.CTX, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, err := ss.Channel().Save(rctx, c1, -1)
require.NoError(t, err)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestID()
c2.Type = model.ChannelTypeOpen
c2, err = ss.Channel().Save(rctx, c2, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// deleted post
teamID := model.NewId()
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel3.Id
o3.UserId = o1.ChannelId
o3.Message = NewTestID()
o3.DeleteAt = 1
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
o4 := &model.Post{}
o4.ChannelId = c2.Id
o4.UserId = model.NewId()
o4.Message = NewTestID()
o4, err = ss.Post().Save(rctx, o4)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
// Post on channel where user is not a member
channel4, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName4",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5 := &model.Post{}
o5.ChannelId = channel4.Id
o5.UserId = model.NewId()
o5.Message = NewTestID()
o5, err = ss.Post().Save(rctx, o5)
require.NoError(t, err)
time.Sleep(2 * time.Millisecond)
m1 := &model.ChannelMember{}
m1.ChannelId = o1.ChannelId
m1.UserId = o1.UserId
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(rctx, m1)
require.NoError(t, err)
m2 := &model.ChannelMember{}
m2.ChannelId = o4.ChannelId
m2.UserId = o1.UserId
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err = ss.Channel().SaveMember(rctx, m2)
require.NoError(t, err)
r, err := ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
preference := model.Preference{
UserId: o1.UserId,
Category: model.PreferenceCategoryFlaggedPost,
Name: o1.Id,
Value: "true",
}
nErr := ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
preference.Name = o2.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
preference.Name = o3.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1, 1)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 post")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 1000, 10)
require.NoError(t, err)
require.Empty(t, r.Order, "should be empty")
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o1.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 2, "should have 2 posts")
preference.Name = o4.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o4.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 1, "should have 1 posts")
preference.Name = o5.Id
nErr = ss.Preference().Save(model.Preferences{preference})
require.NoError(t, nErr)
r, err = ss.Post().GetFlaggedPostsForChannel(o1.UserId, o5.ChannelId, 0, 10)
require.NoError(t, err)
require.Len(t, r.Order, 0, "should have 0 posts")
}
func testPostStoreGetPostsCreatedAt(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
createTime := model.GetMillis() + 1
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestID()
o0.CreateAt = createTime
o0, err = ss.Post().Save(rctx, o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.ChannelId
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1.CreateAt = createTime
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2.CreateAt = createTime + 1
_, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel2.Id
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.CreateAt = createTime
_, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
r1, _ := ss.Post().GetPostsCreatedAt(o1.ChannelId, createTime)
assert.Equal(t, 2, len(r1))
}
func testPostStoreOverwriteMultiple(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
channel3, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName3",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o5, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel3.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test2", "test3"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
r5, err := ss.Post().Get(rctx, o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro5 := r5.Posts[o5.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
require.Equal(t, ro4.Filenames, o4.Filenames, "Failed to save/get")
require.Equal(t, ro5.Message, o5.Message, "Failed to save/get")
require.Equal(t, ro5.Filenames, o5.Filenames, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, errIdx, err := ss.Post().OverwriteMultiple(rctx, []*model.Post{o1a, o2a, o3a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r1, nErr := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro1a := r1.Posts[o1.Id]
r2, nErr = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro2a := r2.Posts[o2.Id]
r3, nErr = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
o5a := ro5.Clone()
o5a.Filenames = []string{}
o5a.FileIds = []string{}
_, errIdx, err := ss.Post().OverwriteMultiple(rctx, []*model.Post{o4a, o5a})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
r4, nErr := ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro4a := r4.Posts[o4.Id]
r5, nErr = ss.Post().Get(rctx, o5.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, nErr)
ro5a := r5.Posts[o5.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
require.Empty(t, ro5a.Filenames, "Failed to clear Filenames")
require.Empty(t, ro5a.FileIds, "Failed to set FileIds")
})
}
func testPostStoreOverwrite(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.RootId = o1.Id
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
channel2, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName2",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o4, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channel2.Id,
UserId: model.NewId(),
Message: model.NewId(),
Filenames: []string{"test"},
})
require.NoError(t, err)
r1, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
r4, err := ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4 := r4.Posts[o4.Id]
require.Equal(t, ro1.Message, o1.Message, "Failed to save/get")
require.Equal(t, ro2.Message, o2.Message, "Failed to save/get")
require.Equal(t, ro3.Message, o3.Message, "Failed to save/get")
require.Equal(t, ro4.Message, o4.Message, "Failed to save/get")
t.Run("overwrite changing message", func(t *testing.T) {
o1a := ro1.Clone()
o1a.Message = ro1.Message + "BBBBBBBBBB"
_, err = ss.Post().Overwrite(rctx, o1a)
require.NoError(t, err)
o2a := ro2.Clone()
o2a.Message = ro2.Message + "DDDDDDD"
_, err = ss.Post().Overwrite(rctx, o2a)
require.NoError(t, err)
o3a := ro3.Clone()
o3a.Message = ro3.Message + "WWWWWWW"
_, err = ss.Post().Overwrite(rctx, o3a)
require.NoError(t, err)
r1, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1a := r1.Posts[o1.Id]
r2, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2a := r2.Posts[o2.Id]
r3, err = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3a := r3.Posts[o3.Id]
assert.Equal(t, ro1a.Message, o1a.Message, "Failed to overwrite/get")
assert.Equal(t, ro2a.Message, o2a.Message, "Failed to overwrite/get")
assert.Equal(t, ro3a.Message, o3a.Message, "Failed to overwrite/get")
})
t.Run("overwrite clearing filenames", func(t *testing.T) {
o4a := ro4.Clone()
o4a.Filenames = []string{}
o4a.FileIds = []string{model.NewId()}
_, err = ss.Post().Overwrite(rctx, o4a)
require.NoError(t, err)
r4, err = ss.Post().Get(rctx, o4.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro4a := r4.Posts[o4.Id]
require.Empty(t, ro4a.Filenames, "Failed to clear Filenames")
require.Len(t, ro4a.FileIds, 1, "Failed to set FileIds")
})
}
func testPostStoreGetPostsByIds(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = o1.ChannelId
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
r1, err := ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro1 := r1.Posts[o1.Id]
r2, err := ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro2 := r2.Posts[o2.Id]
r3, err := ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err)
ro3 := r3.Posts[o3.Id]
postIds := []string{
ro1.Id,
ro2.Id,
ro3.Id,
}
posts, err := ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
err = ss.Post().Delete(rctx, ro1.Id, model.GetMillis(), "")
require.NoError(t, err)
posts, err = ss.Post().GetPostsByIds(postIds)
require.NoError(t, err)
require.Len(t, posts, 3, "Expected 3 posts in results. Got %v", len(posts))
}
func testPostStoreGetPostsBatchForIndexing(t *testing.T, rctx request.CTX, ss store.Store) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(rctx, c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestID()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(rctx, c2, -1)
o1 := &model.Post{}
o1.ChannelId = c1.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1, err := ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = c2.Id
o2.UserId = model.NewId()
o2.Message = NewTestID()
_, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = c1.Id
o3.UserId = model.NewId()
o3.RootId = o1.Id
o3.Message = NewTestID()
_, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
// Getting all
r, err := ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 100)
require.NoError(t, err)
require.Len(t, r, 3, "Expected 3 posts in results. Got %v", len(r))
// Testing pagination
r, err = ss.Post().GetPostsBatchForIndexing(o1.CreateAt-1, "", 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 1, "Expected 1 post in results. Got %v", len(r))
r, err = ss.Post().GetPostsBatchForIndexing(r[0].CreateAt, r[0].Id, 1)
require.NoError(t, err)
require.Len(t, r, 0, "Expected 0 post in results. Got %v", len(r))
}
func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = channel.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1.CreateAt = 1000
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = channel.Id
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.CreateAt = 1000
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
o3 := &model.Post{}
o3.ChannelId = channel.Id
o3.UserId = model.NewId()
o3.Message = NewTestID()
o3.CreateAt = 100000
o3, err = ss.Post().Save(rctx, o3)
require.NoError(t, err)
deleted, _, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2000,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err)
require.Equal(t, int64(2), deleted)
_, err = ss.Post().Get(rctx, o1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge")
_, err = ss.Post().Get(rctx, o2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(rctx, o3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Should have found post 3 after purge")
rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 2, len(rows[0].Ids))
// Clean up retention ids table
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
t.Run("with pagination", func(t *testing.T) {
for range 3 {
_, err = ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err)
}
cursor := model.RetentionPolicyCursor{}
deleted, cursor, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2,
Limit: 2,
}, cursor)
require.NoError(t, err)
require.Equal(t, int64(2), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 2, len(rows[0].Ids))
// Clean up retention ids table
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2,
Limit: 2,
}, cursor)
require.NoError(t, err)
require.Equal(t, int64(1), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 1, len(rows[0].Ids))
// Clean up retention ids table
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
})
t.Run("with data retention policies", func(t *testing.T) {
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewPointer(int64(30)),
},
ChannelIDs: []string{channel.Id},
})
require.NoError(t, err2)
post := &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
}
post, err2 = ss.Post().Save(rctx, post)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2000,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "global policy should have been ignored due to granular policy")
nowMillis := post.CreateAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: nowMillis,
GlobalPolicyEndTime: 0,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by channel policy")
// Create a team policy which is stricter than the channel policy
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewPointer(int64(20)),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
post.Id = ""
post, err2 = ss.Post().Save(rctx, post)
require.NoError(t, err2)
nowMillis = post.CreateAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: nowMillis,
GlobalPolicyEndTime: 0,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err2, "channel policy should have overridden team policy")
// Delete channel policy and re-run team policy
err2 = ss.RetentionPolicy().RemoveChannels(channelPolicy.ID, []string{channel.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(channelPolicy.ID)
require.NoError(t, err2)
_, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: nowMillis,
GlobalPolicyEndTime: 0,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err2)
_, err2 = ss.Post().Get(rctx, post.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err2, "post should have been deleted by team policy")
err2 = ss.RetentionPolicy().RemoveTeams(teamPolicy.ID, []string{team.Id})
require.NoError(t, err2)
err2 = ss.RetentionPolicy().Delete(teamPolicy.ID)
require.NoError(t, err2)
// Clean up retention ids table
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
for _, row := range rows {
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(row)
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
}
})
t.Run("with channel, team and global policies", func(t *testing.T) {
c1 := &model.Channel{}
c1.TeamId = model.NewId()
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
c1, _ = ss.Channel().Save(rctx, c1, -1)
c2 := &model.Channel{}
c2.TeamId = model.NewId()
c2.DisplayName = "Channel2"
c2.Name = NewTestID()
c2.Type = model.ChannelTypeOpen
c2, _ = ss.Channel().Save(rctx, c2, -1)
channelPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewPointer(int64(30)),
},
ChannelIDs: []string{c1.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(channelPolicy.ID)
teamPolicy, err2 := ss.RetentionPolicy().Save(&model.RetentionPolicyWithTeamAndChannelIDs{
RetentionPolicy: model.RetentionPolicy{
DisplayName: "DisplayName",
PostDurationDays: model.NewPointer(int64(30)),
},
TeamIDs: []string{team.Id},
})
require.NoError(t, err2)
defer ss.RetentionPolicy().Delete(teamPolicy.ID)
// This one should be deleted by the channel policy
_, err2 = ss.Post().Save(rctx, &model.Post{
ChannelId: c1.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the team policy
_, err2 = ss.Post().Save(rctx, &model.Post{
ChannelId: channel.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
// This one, by the global policy
_, err2 = ss.Post().Save(rctx, &model.Post{
ChannelId: c2.Id,
UserId: model.NewId(),
Message: "message",
CreateAt: 1,
})
require.NoError(t, err2)
nowMillis := int64(1 + 30*model.DayInMilliseconds + 1)
deleted, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: nowMillis,
GlobalPolicyEndTime: 2,
Limit: 1000,
}, model.RetentionPolicyCursor{})
require.NoError(t, err2)
require.Equal(t, int64(3), deleted)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
// Each policy would generate it's own row
require.Equal(t, 3, len(rows))
// Clean up retention ids table
for _, row := range rows {
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(row)
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
}
})
t.Run("with preserve pinned posts true", func(t *testing.T) {
p1 := &model.Post{}
p1.ChannelId = channel.Id
p1.UserId = model.NewId()
p1.IsPinned = false
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, err = ss.Post().Save(rctx, p1)
require.NoError(t, err)
p2 := &model.Post{}
p2.ChannelId = channel.Id
p2.UserId = model.NewId()
p2.IsPinned = false
p2.Message = NewTestID()
p2.CreateAt = 1000
p2, err = ss.Post().Save(rctx, p2)
require.NoError(t, err)
p3 := &model.Post{}
p3.ChannelId = channel.Id
p3.UserId = model.NewId()
p3.IsPinned = false
p3.Message = NewTestID()
p3.CreateAt = 1000
p3, err = ss.Post().Save(rctx, p3)
require.NoError(t, err)
np3 := p3.Clone()
np3.IsPinned = true
np3, err = ss.Post().Update(rctx, np3, p3)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2000,
Limit: 1000,
PreservePinnedPosts: true,
}, model.RetentionPolicyCursor{})
require.NoError(t, err)
require.Equal(t, int64(3), deleted)
_, err = ss.Post().Get(rctx, p1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge")
_, err = ss.Post().Get(rctx, p2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(rctx, p3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 3 before update after purge")
_, err = ss.Post().Get(rctx, np3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.NoError(t, err, "Should have found updated post 3 after purge")
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 3, len(rows[0].Ids))
// Clean up retention ids table
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
// Clean up pinned post
_, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2000,
Limit: 1000,
PreservePinnedPosts: false,
}, model.RetentionPolicyCursor{})
require.NoError(t, err)
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
// Clean up retention ids table
_, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
})
t.Run("with preserve pinned posts false", func(t *testing.T) {
p1 := &model.Post{}
p1.ChannelId = channel.Id
p1.UserId = model.NewId()
p1.IsPinned = false
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, err = ss.Post().Save(rctx, p1)
require.NoError(t, err)
p2 := &model.Post{}
p2.ChannelId = channel.Id
p2.UserId = model.NewId()
p2.IsPinned = false
p2.Message = NewTestID()
p2.CreateAt = 1000
p2, err = ss.Post().Save(rctx, p2)
require.NoError(t, err)
p3 := &model.Post{}
p3.ChannelId = channel.Id
p3.UserId = model.NewId()
p3.IsPinned = false
p3.Message = NewTestID()
p3.CreateAt = 1000
p3, err = ss.Post().Save(rctx, p3)
require.NoError(t, err)
np3 := p3.Clone()
np3.IsPinned = true
np3, err = ss.Post().Update(rctx, np3, p3)
deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{
Now: 0,
GlobalPolicyEndTime: 2000,
Limit: 1000,
PreservePinnedPosts: false,
}, model.RetentionPolicyCursor{})
require.NoError(t, err)
require.Equal(t, int64(4), deleted)
_, err = ss.Post().Get(rctx, p1.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 1 after purge")
_, err = ss.Post().Get(rctx, p2.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 2 after purge")
_, err = ss.Post().Get(rctx, p3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found post 3 before update after purge")
_, err = ss.Post().Get(rctx, np3.Id, model.GetPostsOptions{}, "", map[string]bool{})
require.Error(t, err, "Should have not found updated post 3 after purge")
rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000)
require.NoError(t, err)
require.Equal(t, 1, len(rows))
require.Equal(t, 4, len(rows[0].Ids))
// Clean up retention ids table
deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0])
require.NoError(t, err)
require.Equal(t, int64(0), deleted)
})
}
func testPostStoreGetOldest(t *testing.T, rctx request.CTX, ss store.Store) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
o0 := &model.Post{}
o0.ChannelId = channel1.Id
o0.UserId = model.NewId()
o0.Message = NewTestID()
o0.CreateAt = 3
o0, err = ss.Post().Save(rctx, o0)
require.NoError(t, err)
o1 := &model.Post{}
o1.ChannelId = o0.Id
o1.UserId = model.NewId()
o1.Message = NewTestID()
o1.CreateAt = 2
o1, err = ss.Post().Save(rctx, o1)
require.NoError(t, err)
o2 := &model.Post{}
o2.ChannelId = o1.ChannelId
o2.UserId = model.NewId()
o2.Message = NewTestID()
o2.CreateAt = 1
o2, err = ss.Post().Save(rctx, o2)
require.NoError(t, err)
r1, err := ss.Post().GetOldest()
require.NoError(t, err)
assert.EqualValues(t, o2.Id, r1.Id)
}
func testGetMaxPostSize(t *testing.T, _ request.CTX, ss store.Store) {
assert.Equal(t, model.PostMessageMaxRunesV2, ss.Post().GetMaxPostSize())
}
func testPostStoreGetParentsForExportAfter(t *testing.T, rctx request.CTX, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestID()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(rctx, &c1, -1)
require.NoError(t, nErr)
c2 := model.Channel{}
c2.TeamId = t1.Id
c2.DisplayName = "Channel2"
c2.Name = NewTestID()
c2.Type = model.ChannelTypeOpen
_, nErr = ss.Channel().Save(rctx, &c2, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Username = model.NewUsername()
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(rctx, &u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c2.Id
p2.UserId = u1.Id
p2.Message = NewTestID()
p2.CreateAt = 1000
p2, nErr = ss.Post().Save(rctx, p2)
require.NoError(t, nErr)
nErr = ss.Channel().Delete(c2.Id, model.GetMillis())
require.NoError(t, nErr)
t.Run("without archived channels", func(t *testing.T) {
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, err)
found := false
foundArchived := false
for _, p := range posts {
if p.Id == p1.Id {
found = true
assert.Equal(t, p.Id, p1.Id)
assert.Equal(t, p.Message, p1.Message)
assert.Equal(t, p.Username, u1.Username)
assert.Equal(t, p.TeamName, t1.Name)
assert.Equal(t, p.ChannelName, c1.Name)
}
if p.Id == p2.Id {
foundArchived = true
}
}
assert.True(t, found)
assert.False(t, foundArchived, "posts from archived channel should not be returned")
})
t.Run("with archived channels", func(t *testing.T) {
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), true)
assert.NoError(t, err)
found := false
for _, p := range posts {
if p.Id == p2.Id {
found = true
assert.Equal(t, p.Id, p2.Id)
assert.Equal(t, p.Message, p2.Message)
assert.Equal(t, p.Username, u1.Username)
assert.Equal(t, p.TeamName, t1.Name)
assert.Equal(t, p.ChannelName, c2.Name)
}
}
assert.True(t, found)
})
t.Run("with flagged post", func(t *testing.T) {
err := ss.Preference().Save(model.Preferences([]model.Preference{
{
UserId: u1.Id,
Category: model.PreferenceCategoryFlaggedPost,
Name: p1.Id,
Value: "true",
},
}))
require.NoError(t, err)
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, err)
for _, p := range posts {
if p.Id == p1.Id {
require.NotNil(t, p.FlaggedBy)
assert.Equal(t, model.StringArray([]string{u1.Username}), p.FlaggedBy)
}
}
})
}
func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.Store) {
t1 := model.Team{}
t1.DisplayName = "Name"
t1.Name = NewTestID()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
c1 := model.Channel{}
c1.TeamId = t1.Id
c1.DisplayName = "Channel1"
c1.Name = NewTestID()
c1.Type = model.ChannelTypeOpen
_, nErr := ss.Channel().Save(rctx, &c1, -1)
require.NoError(t, nErr)
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(rctx, &u1)
require.NoError(t, err)
p1 := &model.Post{}
p1.ChannelId = c1.Id
p1.UserId = u1.Id
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
p2 := &model.Post{}
p2.ChannelId = c1.Id
p2.UserId = u1.Id
p2.Message = NewTestID()
p2.CreateAt = 1001
p2.RootId = p1.Id
p2, nErr = ss.Post().Save(rctx, p2)
require.NoError(t, nErr)
r1, err := ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
require.Len(t, r1, 1)
reply1 := r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
// Checking whether replies by deleted user are exported
u1.DeleteAt = 1002
_, err = ss.User().Update(rctx, &u1, false)
require.NoError(t, err)
r1, err = ss.Post().GetRepliesForExport(p1.Id)
assert.NoError(t, err)
require.Len(t, r1, 1)
reply1 = r1[0]
assert.Equal(t, reply1.Id, p2.Id)
assert.Equal(t, reply1.Message, p2.Message)
assert.Equal(t, reply1.Username, u1.Username)
}
func testPostStoreGetDirectPostParentsForExportAfter(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
teamID := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamID
o1.DisplayName = "Name"
o1.Name = model.GetDMNameFromIds(NewTestID(), NewTestID())
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(rctx, u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(rctx, u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(rctx, &o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, nErr)
assert.Equal(t, p1.Message, r1[0].Message)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMaster().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterDeleted(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
teamID := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamID
o1.DisplayName = "Name"
o1.Name = model.GetDMNameFromIds(NewTestID(), NewTestID())
o1.Type = model.ChannelTypeDirect
u1 := &model.User{}
u1.DeleteAt = 1
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(rctx, u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.DeleteAt = 1
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(rctx, u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(rctx, &o1, &m1, &m2)
o1.DeleteAt = 1
nErr = ss.Channel().SetDeleteAt(o1.Id, 1, 1)
assert.NoError(t, nErr)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestID()
p1.CreateAt = 1000
_, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
r1, nErr := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, nErr)
assert.Equal(t, 0, len(r1))
r1, nErr = ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26), true)
assert.NoError(t, nErr)
assert.Equal(t, 1, len(r1))
// Manually truncate Channels table until testlib can handle cleanups
s.GetMaster().Exec("TRUNCATE Channels")
}
func testPostStoreGetDirectPostParentsForExportAfterBatched(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
teamID := model.NewId()
o1 := model.Channel{}
o1.TeamId = teamID
o1.DisplayName = "Name"
o1.Name = model.GetDMNameFromIds(NewTestID(), NewTestID())
o1.Type = model.ChannelTypeDirect
var postIds []string
for range 150 {
u1 := &model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err := ss.User().Save(rctx, u1)
require.NoError(t, err)
_, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)
require.NoError(t, nErr)
u2 := &model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(rctx, u2)
require.NoError(t, err)
_, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)
require.NoError(t, nErr)
m1 := model.ChannelMember{}
m1.ChannelId = o1.Id
m1.UserId = u1.Id
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
m2 := model.ChannelMember{}
m2.ChannelId = o1.Id
m2.UserId = u2.Id
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
ss.Channel().SaveDirectChannel(rctx, &o1, &m1, &m2)
p1 := &model.Post{}
p1.ChannelId = o1.Id
p1.UserId = u1.Id
p1.Message = NewTestID()
p1.CreateAt = 1000
p1, nErr = ss.Post().Save(rctx, p1)
require.NoError(t, nErr)
postIds = append(postIds, p1.Id)
}
slices.Sort(postIds)
// Get all posts
r1, err := ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26), false)
assert.NoError(t, err)
assert.Equal(t, len(postIds), len(r1))
var exportedPostIds []string
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
slices.Sort(exportedPostIds)
assert.ElementsMatch(t, postIds, exportedPostIds)
// Get 100
r1, err = ss.Post().GetDirectPostParentsForExportAfter(100, strings.Repeat("0", 26), false)
assert.NoError(t, err)
assert.Equal(t, 100, len(r1))
exportedPostIds = []string{}
for i := range r1 {
exportedPostIds = append(exportedPostIds, r1[i].Id)
}
slices.Sort(exportedPostIds)
assert.ElementsMatch(t, postIds[:100], exportedPostIds)
// Manually truncate Channels table until testlib can handle cleanups
s.GetMaster().Exec("TRUNCATE Channels")
}
func testHasAutoResponsePostByUserSince(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return posts created after the given time", func(t *testing.T) {
teamID := model.NewId()
channel1, err := ss.Channel().Save(rctx, &model.Channel{
TeamId: teamID,
DisplayName: "DisplayName1",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channelID := channel1.Id
userID := model.NewId()
_, err = ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
// We need to sleep because SendAutoResponseIfNecessary
// runs in a goroutine.
time.Sleep(time.Millisecond)
post2, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "message",
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
post3, err := ss.Post().Save(rctx, &model.Post{
ChannelId: channelID,
UserId: userID,
Message: "auto response message",
Type: model.PostTypeAutoResponder,
})
require.NoError(t, err)
time.Sleep(time.Millisecond)
exists, err := ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelID, Time: post2.CreateAt}, userID)
require.NoError(t, err)
assert.True(t, exists)
err = ss.Post().Delete(rctx, post3.Id, time.Now().Unix(), userID)
require.NoError(t, err)
exists, err = ss.Post().HasAutoResponsePostByUserSince(model.GetPostsSinceOptions{ChannelId: channelID, Time: post2.CreateAt}, userID)
require.NoError(t, err)
assert.False(t, exists)
})
}
func testGetPostsSinceUpdateForSync(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
// create some posts.
channelID := model.NewId()
remoteID := model.NewPointer(model.NewId())
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 0"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 1"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 2"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 3", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 4", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 5", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 6", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 7"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 8", DeleteAt: model.GetMillis()},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 9", DeleteAt: model.GetMillis()},
}
for i, p := range data {
p.UpdateAt = first + (int64(i) * 300000)
if p.RemoteId == nil {
p.RemoteId = model.NewPointer(model.NewId())
}
_, err := ss.Post().Save(rctx, p)
require.NoError(t, err, "couldn't save post")
}
t.Run("Invalid channel id", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: model.NewId(),
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, cursorOut, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Empty(t, posts, "should return zero posts")
require.Equal(t, cursor, cursorOut)
})
t.Run("Get by channel, exclude remotes, exclude deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeRemoteId: *remoteID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data[0:3], data[7]), getPostIds(posts))
})
t.Run("Include deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
IncludeDeleted: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data), getPostIds(posts))
})
t.Run("Limit and cursor", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
t.Run("UpdateAt collisions", func(t *testing.T) {
// this test requires all the UpdateAt timestamps to be the same.
result, err := s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ?", model.GetMillis())
require.NoError(t, err)
rows, err := result.RowsAffected()
require.NoError(t, err)
require.Greater(t, rows, int64(0))
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
}
func testGetPostsSinceCreateForSync(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
// create some posts.
channelID := model.NewId()
remoteID := model.NewPointer(model.NewId())
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 0"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 1"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 2"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 3", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 4", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 5", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 6", RemoteId: remoteID},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 7"},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 8", DeleteAt: model.GetMillis()},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "test post 9", DeleteAt: model.GetMillis()},
}
for i, p := range data {
p.CreateAt = first + (int64(i) * 300000)
if p.RemoteId == nil {
p.RemoteId = model.NewPointer(model.NewId())
}
_, err := ss.Post().Save(rctx, p)
require.NoError(t, err, "couldn't save post")
}
t.Run("Invalid channel id", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: model.NewId(),
SinceCreateAt: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, cursorOut, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Empty(t, posts, "should return zero posts")
require.Equal(t, cursor, cursorOut)
})
t.Run("Get by channel, exclude remotes, exclude deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeRemoteId: *remoteID,
SinceCreateAt: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data[0:3], data[7]), getPostIds(posts))
})
t.Run("Include deleted", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
IncludeDeleted: true,
SinceCreateAt: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.ElementsMatch(t, getPostIds(data), getPostIds(posts))
})
t.Run("Limit and cursor", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
SinceCreateAt: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
t.Run("CreateAt collisions", func(t *testing.T) {
// this test requires all the CreateAt timestamps to be the same.
result, err := s.GetMaster().Exec("UPDATE Posts SET CreateAt = ?", model.GetMillis())
require.NoError(t, err)
rows, err := result.RowsAffected()
require.NoError(t, err)
require.Greater(t, rows, int64(0))
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts1, cursor, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts1, 5, "should get 5 posts")
posts2, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 5)
require.NoError(t, err)
require.Len(t, posts2, 3, "should get 3 posts")
require.ElementsMatch(t, getPostIds(data[0:8]), getPostIds(posts1, posts2...))
})
}
func testSetPostReminder(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
// Basic
userID := NewTestID()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestID(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: 1234,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
out := model.PostReminder{}
require.NoError(t, s.GetMaster().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
reminder.PostId = "notfound"
err = ss.Post().SetPostReminder(reminder)
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
// Upsert
reminder = &model.PostReminder{
TargetTime: 12345,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
require.NoError(t, s.GetMaster().Get(&out, `SELECT PostId, UserId, TargetTime FROM PostReminders WHERE PostId=? AND UserId=?`, reminder.PostId, reminder.UserId))
assert.Equal(t, reminder, &out)
}
func testGetPostReminders(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
times := []int64{100, 101, 102}
for _, tt := range times {
userID := NewTestID()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestID(),
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
reminder := &model.PostReminder{
TargetTime: tt,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
}
reminders, err := ss.Post().GetPostReminders(101)
require.NoError(t, err)
require.Len(t, reminders, 2)
// assert one reminder is left
reminders, err = ss.Post().GetPostReminders(102)
require.NoError(t, err)
require.Len(t, reminders, 1)
// assert everything is deleted.
reminders, err = ss.Post().GetPostReminders(103)
require.NoError(t, err)
require.Len(t, reminders, 0)
}
func testGetPostReminderMetadata(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
team := &model.Team{
Name: "teamname",
DisplayName: "display",
Type: model.TeamOpen,
}
team, err := ss.Team().Save(team)
require.NoError(t, err)
ch := &model.Channel{
TeamId: team.Id,
DisplayName: "channeldisplay",
Name: NewTestID(),
Type: model.ChannelTypeOpen,
}
ch, err = ss.Channel().Save(rctx, ch, -1)
require.NoError(t, err)
ch2 := &model.Channel{
TeamId: "",
DisplayName: "GM_display",
Name: NewTestID(),
Type: model.ChannelTypeGroup,
}
ch2, err = ss.Channel().Save(rctx, ch2, -1)
require.NoError(t, err)
u1 := &model.User{
Email: MakeEmail(),
Username: model.NewUsername(),
Locale: "es",
}
u1, err = ss.User().Save(rctx, u1)
require.NoError(t, err)
p1 := &model.Post{
UserId: u1.Id,
ChannelId: ch.Id,
Message: "hi there",
Type: model.PostTypeDefault,
}
p1, err = ss.Post().Save(rctx, p1)
require.NoError(t, err)
p2 := &model.Post{
UserId: u1.Id,
ChannelId: ch2.Id,
Message: "hi there 2",
Type: model.PostTypeDefault,
}
p2, err = ss.Post().Save(rctx, p2)
require.NoError(t, err)
meta, err := ss.Post().GetPostReminderMetadata(p1.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelID, ch.Id)
assert.Equal(t, meta.TeamName, team.Name)
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
meta, err = ss.Post().GetPostReminderMetadata(p2.Id)
require.NoError(t, err)
assert.Equal(t, meta.ChannelID, ch2.Id)
assert.Equal(t, meta.TeamName, "")
assert.Equal(t, meta.Username, u1.Username)
assert.Equal(t, meta.UserLocale, u1.Locale)
}
func getPostIds(posts []*model.Post, morePosts ...*model.Post) []string {
ids := make([]string, 0, len(posts)+len(morePosts))
for _, p := range posts {
ids = append(ids, p.Id)
}
for _, p := range morePosts {
ids = append(ids, p.Id)
}
return ids
}
func testGetNthRecentPostTime(t *testing.T, rctx request.CTX, ss store.Store) {
t.Skip("https://mattermost.atlassian.net/browse/MM-64438")
_, err := ss.Post().GetNthRecentPostTime(0)
assert.Error(t, err)
_, err = ss.Post().GetNthRecentPostTime(-1)
assert.Error(t, err)
diff := int64(10000)
now := utils.MillisFromTime(time.Now()) + diff
p1 := &model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = "test"
p1.CreateAt = now
p1, err = ss.Post().Save(rctx, p1)
require.NoError(t, err)
p2 := &model.Post{}
p2.ChannelId = p1.ChannelId
p2.UserId = p1.UserId
p2.Message = p1.Message
now = now + diff
p2.CreateAt = now
p2, err = ss.Post().Save(rctx, p2)
require.NoError(t, err)
bot1 := &model.Bot{
Username: "username",
Description: "a bot",
OwnerId: model.NewId(),
UserId: model.NewId(),
}
_, err = ss.Bot().Save(bot1)
require.NoError(t, err)
b1 := &model.Post{}
b1.Message = "bot test"
b1.ChannelId = p1.ChannelId
b1.UserId = bot1.UserId
now = now + diff
b1.CreateAt = now
_, err = ss.Post().Save(rctx, b1)
require.NoError(t, err)
p3 := &model.Post{}
p3.ChannelId = p1.ChannelId
p3.UserId = p1.UserId
p3.Message = p1.Message
now = now + diff
p3.CreateAt = now
p3, err = ss.Post().Save(rctx, p3)
require.NoError(t, err)
s1 := &model.Post{}
s1.Type = model.PostTypeJoinChannel
s1.ChannelId = p1.ChannelId
s1.UserId = model.NewId()
s1.Message = "system_join_channel message"
now = now + diff
s1.CreateAt = now
_, err = ss.Post().Save(rctx, s1)
require.NoError(t, err)
p4 := &model.Post{}
p4.ChannelId = p1.ChannelId
p4.UserId = p1.UserId
p4.Message = p1.Message
now = now + diff
p4.CreateAt = now
p4, err = ss.Post().Save(rctx, p4)
require.NoError(t, err)
r, err := ss.Post().GetNthRecentPostTime(1)
assert.NoError(t, err)
assert.Equal(t, p4.CreateAt, r)
// Skip system post
r, err = ss.Post().GetNthRecentPostTime(2)
assert.NoError(t, err)
assert.Equal(t, p3.CreateAt, r)
// Skip system & bot post
r, err = ss.Post().GetNthRecentPostTime(3)
assert.NoError(t, err)
assert.Equal(t, p2.CreateAt, r)
r, err = ss.Post().GetNthRecentPostTime(4)
assert.NoError(t, err)
assert.Equal(t, p1.CreateAt, r)
_, err = ss.Post().GetNthRecentPostTime(10000)
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
}
func testGetEditHistoryForPost(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("should return edit history for post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(rctx, post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(rctx, updatedPost, originalPost)
require.NoError(t, err)
// get edit history
edits, err := ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
require.Len(t, edits, 1)
require.Equal(t, originalPost.Id, edits[0].Id)
require.Equal(t, originalPost.UserId, edits[0].UserId)
require.Equal(t, originalPost.Message, edits[0].Message)
})
t.Run("should return error for not edited posts", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(rctx, post)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for non-existent post", func(t *testing.T) {
// get edit history
_, err := ss.Post().GetEditHistoryForPost("non-existent")
require.Error(t, err)
})
t.Run("should return error for deleted post", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(rctx, post)
require.NoError(t, err)
// delete post
err = ss.Post().Delete(rctx, post.Id, 100, post.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(originalPost.Id)
require.Error(t, err)
})
t.Run("should return error for deleted edit", func(t *testing.T) {
// create a post
post := &model.Post{
ChannelId: model.NewId(),
UserId: model.NewId(),
Message: "test",
}
originalPost, err := ss.Post().Save(rctx, post)
require.NoError(t, err)
// create an edit
updatedPost := originalPost.Clone()
updatedPost.Message = "test edited"
savedUpdatedPost, err := ss.Post().Update(rctx, updatedPost, originalPost)
require.NoError(t, err)
// delete edit
err = ss.Post().Delete(rctx, savedUpdatedPost.Id, 100, savedUpdatedPost.UserId)
require.NoError(t, err)
// get edit history
_, err = ss.Post().GetEditHistoryForPost(savedUpdatedPost.Id)
require.NoError(t, err)
})
}
// testGetPostsSinceForSyncExcludeMetadata tests the ExcludeChannelMetadataSystemPosts option
// in the GetPostsSinceForSync function to verify that database-level filtering works correctly
func testGetPostsSinceForSyncExcludeMetadata(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
// Create a channel
channelID := model.NewId()
// Create test posts - mix of regular posts and channel metadata system posts
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "regular post 1", Type: model.PostTypeDefault},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "changed header", Type: model.PostTypeHeaderChange},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "regular post 2", Type: model.PostTypeDefault},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "changed display name", Type: model.PostTypeDisplaynameChange},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "regular post 3", Type: model.PostTypeDefault},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "changed purpose", Type: model.PostTypePurposeChange},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "regular post 4", Type: model.PostTypeDefault},
}
// Save posts
for i, p := range data {
p.UpdateAt = first + (int64(i) * 300000)
p.CreateAt = first + (int64(i) * 300000)
p.RemoteId = model.NewPointer(model.NewId())
_, err := ss.Post().Save(rctx, p)
require.NoError(t, err, "couldn't save post")
}
t.Run("ExcludeChannelMetadataSystemPosts=true should filter out metadata posts", func(t *testing.T) {
// Set options with ExcludeChannelMetadataSystemPosts = true
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeChannelMetadataSystemPosts: true,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
// Verify only regular posts are returned
require.Len(t, posts, 4, "should return only 4 regular posts")
// Check that only default post types are returned
for _, post := range posts {
require.Equal(t, model.PostTypeDefault, post.Type, "only default posts should be returned")
}
// Verify we have the expected post IDs (only regular posts)
expectedIDs := []string{
data[0].Id, // regular post 1
data[2].Id, // regular post 2
data[4].Id, // regular post 3
data[6].Id, // regular post 4
}
postIDs := make([]string, 0, len(posts))
for _, p := range posts {
postIDs = append(postIDs, p.Id)
}
require.ElementsMatch(t, expectedIDs, postIDs, "returned posts should only be regular posts")
})
t.Run("ExcludeChannelMetadataSystemPosts=false should include all posts", func(t *testing.T) {
// Set options with ExcludeChannelMetadataSystemPosts = false
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludeChannelMetadataSystemPosts: false,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
// Verify all posts are returned
require.Len(t, posts, 7, "should return all 7 posts when not excluding metadata posts")
// Verify all post types are included by counting each type
postTypeCount := make(map[string]int)
for _, post := range posts {
postTypeCount[post.Type]++
}
require.Equal(t, 4, postTypeCount[model.PostTypeDefault], "should have 4 regular posts")
require.Equal(t, 1, postTypeCount[model.PostTypeHeaderChange], "should have 1 header change post")
require.Equal(t, 1, postTypeCount[model.PostTypeDisplaynameChange], "should have 1 display name change post")
require.Equal(t, 1, postTypeCount[model.PostTypePurposeChange], "should have 1 purpose change post")
})
}
func testGetPostsSinceForSyncExcludedPostTypes(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
channelID := model.NewId()
first := model.GetMillis()
data := []*model.Post{
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "regular post", Type: model.PostTypeDefault},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "card post", Type: model.PostTypeCard},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "another regular", Type: model.PostTypeDefault},
{Id: model.NewId(), ChannelId: channelID, UserId: model.NewId(), Message: "join post", Type: model.PostTypeJoinChannel},
}
for i, p := range data {
p.UpdateAt = first + (int64(i) * 300000)
p.CreateAt = first + (int64(i) * 300000)
p.RemoteId = model.NewPointer(model.NewId())
_, err := ss.Post().Save(rctx, p)
require.NoError(t, err, "couldn't save post")
}
t.Run("ExcludedPostTypes filters out specified types", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludedPostTypes: []string{model.PostTypeCard},
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Len(t, posts, 3, "should exclude the card post")
for _, post := range posts {
require.NotEqual(t, model.PostTypeCard, post.Type)
}
})
t.Run("ExcludedPostTypes with multiple types", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
ExcludedPostTypes: []string{model.PostTypeCard, model.PostTypeJoinChannel},
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Len(t, posts, 2, "should exclude card and join posts")
for _, post := range posts {
require.Equal(t, model.PostTypeDefault, post.Type)
}
})
t.Run("empty ExcludedPostTypes returns all posts", func(t *testing.T) {
opt := model.GetPostsSinceForSyncOptions{
ChannelId: channelID,
}
cursor := model.GetPostsSinceForSyncCursor{}
posts, _, err := ss.Post().GetPostsSinceForSync(opt, cursor, 100)
require.NoError(t, err)
require.Len(t, posts, 4, "should return all posts when no exclusions")
})
}
// buildReportPostQueryParams is a test helper to build ReportPostQueryParams for store tests.
// Store tests focus on SQL query logic, not parameter resolution/validation (which happens in API layer).
func buildReportPostQueryParams(channelId, timeField, sortDirection string, perPage int, includeDeleted, excludeSystemPosts bool) model.ReportPostQueryParams {
// Set defaults
if timeField == "" {
timeField = model.ReportingTimeFieldCreateAt
}
if sortDirection == "" {
sortDirection = model.ReportingSortDirectionAsc
}
if perPage == 0 {
perPage = 100
}
// Set initial cursor position based on sort direction
cursorTime := int64(0) // ASC: start from beginning
if sortDirection == model.ReportingSortDirectionDesc {
cursorTime = int64(^uint64(0) >> 1) // MaxInt64
}
return model.ReportPostQueryParams{
ChannelId: channelId,
CursorTime: cursorTime,
CursorId: "",
TimeField: timeField,
SortDirection: sortDirection,
IncludeDeleted: includeDeleted,
ExcludeSystemPosts: excludeSystemPosts,
PerPage: perPage,
}
}
func testGetPostsForReporting(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
channelID := model.NewId()
first := model.GetMillis()
// Create test posts with predictable timestamps
data := []*model.Post{
{ChannelId: channelID, UserId: model.NewId(), Message: "post 0"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 1"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 2"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 3"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 4"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 5"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 6"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 7"},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 8", DeleteAt: model.GetMillis()},
{ChannelId: channelID, UserId: model.NewId(), Message: "post 9", DeleteAt: model.GetMillis()},
{ChannelId: channelID, UserId: model.NewId(), Message: "header change", Type: model.PostTypeHeaderChange},
{ChannelId: channelID, UserId: model.NewId(), Message: "name change", Type: model.PostTypeDisplaynameChange},
}
for i, p := range data {
p.CreateAt = first + (int64(i) * 300000)
p.UpdateAt = first + (int64(i) * 300000)
saved, err := ss.Post().Save(rctx, p)
require.NoError(t, err, "couldn't save post")
data[i] = saved // Update with saved post (includes generated ID)
}
t.Run("Invalid channel id", func(t *testing.T) {
queryParams := buildReportPostQueryParams(model.NewId(), "", "", 0, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Empty(t, result.Posts, "should return zero posts")
require.Nil(t, result.NextCursor, "should not have next cursor")
})
t.Run("empty cursor is valid for first page", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 100, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err, "empty cursor should be valid")
require.NotNil(t, result)
require.Greater(t, len(result.Posts), 0, "should return posts")
})
t.Run("Basic cursor pagination with create_at ASC", func(t *testing.T) {
// First page
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 5, false, false)
result1, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result1.Posts, 5, "should get 5 posts")
require.NotNil(t, result1.NextCursor, "should have next cursor")
// Second page - decode cursor from first page
queryParams2, appErr := model.DecodeReportPostCursorV1(result1.NextCursor.Cursor)
require.Nil(t, appErr)
queryParams2.PerPage = 5
result2, err := ss.Post().GetPostsForReporting(rctx, *queryParams2)
require.NoError(t, err)
require.Len(t, result2.Posts, 5, "should get 5 posts")
require.Nil(t, result2.NextCursor, "should not have next cursor (10 non-deleted posts total: 0-7,10-11)")
})
t.Run("DESC sort order", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionDesc, 3, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result.Posts, 3, "should get 3 posts")
require.NotNil(t, result.NextCursor, "should have next cursor")
// Verify we got the right count
require.Len(t, result.Posts, 3, "should have 3 posts")
})
t.Run("Include deleted posts", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 100, true, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result.Posts, 12, "should get all 12 posts including deleted")
// Verify deleted posts are included
deletedCount := 0
for _, post := range result.Posts {
if post.DeleteAt > 0 {
deletedCount++
}
}
require.Equal(t, 2, deletedCount, "should have 2 deleted posts")
})
t.Run("Exclude channel metadata system posts", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 100, false, true)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result.Posts, 8, "should get 8 posts (10 regular posts - 2 deleted)")
// Verify no system posts for channel metadata are included
for _, post := range result.Posts {
require.NotEqual(t, model.PostTypeHeaderChange, post.Type)
require.NotEqual(t, model.PostTypeDisplaynameChange, post.Type)
require.NotEqual(t, model.PostTypePurposeChange, post.Type)
}
})
t.Run("Update_at time field", func(t *testing.T) {
// Simply test that we can query using update_at field
// Create a new channel for this test
channelID3 := model.NewId()
testPosts := []*model.Post{
{ChannelId: channelID3, UserId: model.NewId(), Message: "test 1"},
{ChannelId: channelID3, UserId: model.NewId(), Message: "test 2"},
{ChannelId: channelID3, UserId: model.NewId(), Message: "test 3"},
}
baseTime := model.GetMillis()
for i, p := range testPosts {
// Set CreateAt and UpdateAt to different values
p.CreateAt = baseTime + (int64(i) * 1000)
p.UpdateAt = baseTime + (int64(i) * 2000) // UpdateAt is different from CreateAt
saved, err := ss.Post().Save(rctx, p)
require.NoError(t, err)
testPosts[i] = saved
}
// Query by update_at field
queryParams := buildReportPostQueryParams(channelID3, model.ReportingTimeFieldUpdateAt, model.ReportingSortDirectionAsc, 100, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result.Posts, 3, "should get all 3 posts")
// Verify that querying by update_at works (posts are ordered by UpdateAt, not CreateAt)
require.Greater(t, len(result.Posts), 0, "should have posts")
})
t.Run("Timestamp collisions with ID tie-breaking", func(t *testing.T) {
// Create posts with identical timestamps
channelID2 := model.NewId()
sameTime := model.GetMillis()
collisionPosts := []*model.Post{
{ChannelId: channelID2, UserId: model.NewId(), Message: "collision 0"},
{ChannelId: channelID2, UserId: model.NewId(), Message: "collision 1"},
{ChannelId: channelID2, UserId: model.NewId(), Message: "collision 2"},
{ChannelId: channelID2, UserId: model.NewId(), Message: "collision 3"},
{ChannelId: channelID2, UserId: model.NewId(), Message: "collision 4"},
}
for i, p := range collisionPosts {
p.CreateAt = sameTime
p.UpdateAt = sameTime
saved, err := ss.Post().Save(rctx, p)
require.NoError(t, err)
collisionPosts[i] = saved // Update with saved post
}
// Paginate through posts with same timestamp
allPosts := make(map[string]*model.Post)
iterations := 0
maxIterations := 10 // Safety limit
queryParams := buildReportPostQueryParams(channelID2, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 2, false, false)
for iterations < maxIterations {
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
// Add posts from array to map for deduplication check
for _, post := range result.Posts {
allPosts[post.Id] = post
}
if result.NextCursor == nil {
break
}
// Decode cursor for next page
qp, appErr := model.DecodeReportPostCursorV1(result.NextCursor.Cursor)
require.Nil(t, appErr)
qp.PerPage = 2
queryParams = *qp
iterations++
}
require.Len(t, allPosts, 5, "should get all 5 posts despite timestamp collision")
})
t.Run("Per page limits", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 3, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Len(t, result.Posts, 3, "should respect per_page limit")
})
t.Run("Default per page when not specified", func(t *testing.T) {
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 0, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
// Should get all non-deleted posts in one page (default is 100)
require.Greater(t, len(result.Posts), 0, "should get posts with default per_page")
})
t.Run("Max per page limit enforced", func(t *testing.T) {
// Test that per_page > 1000 is capped at 1000 by the helper
queryParams := buildReportPostQueryParams(channelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 5000, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.NotNil(t, result, "should handle large per_page values")
})
t.Run("Verify query uses indexes efficiently", func(t *testing.T) {
// This test verifies that the query plan uses the expected indexes
// The Posts table has these relevant indexes:
// - idx_posts_channel_id_update_at (ChannelId, UpdateAt)
// - idx_posts_channel_id_delete_at_create_at (ChannelId, DeleteAt, CreateAt)
//
// For reporting queries, we expect the query to use index seeks, not table scans
//
// Note: The actual query plan depends on data distribution and statistics. This test just verifies the query executes
// efficiently by checking that it completes in a reasonable time.
// Create a larger dataset to better test index usage
largeChannelID := model.NewId()
largeBaseTime := model.GetMillis()
numPosts := 100
for i := range numPosts {
post := &model.Post{
ChannelId: largeChannelID,
UserId: model.NewId(),
Message: "large test post",
CreateAt: largeBaseTime + (int64(i) * 10000),
UpdateAt: largeBaseTime + (int64(i) * 10000),
}
_, err := ss.Post().Save(rctx, post)
require.NoError(t, err)
}
// Query with ASC direction - should use (ChannelId, CreateAt) index seek
// This query should be fast (< 100ms) if using indexes properly
// If it's doing a table scan, it would be much slower with larger datasets
queryParams := buildReportPostQueryParams(largeChannelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionAsc, 50, false, false)
result, err := ss.Post().GetPostsForReporting(rctx, queryParams)
require.NoError(t, err)
require.Greater(t, len(result.Posts), 0, "should return posts")
// Test DESC query which uses different index path
queryParamsDesc := buildReportPostQueryParams(largeChannelID, model.ReportingTimeFieldCreateAt, model.ReportingSortDirectionDesc, 50, false, false)
resultDesc, err := ss.Post().GetPostsForReporting(rctx, queryParamsDesc)
require.NoError(t, err)
require.Greater(t, len(resultDesc.Posts), 0, "should return posts")
// If we got results in reasonable time, the indexes are being used
// A full table scan would timeout or be noticeably slow
})
}
func testRestoreContentFlaggedPost(t *testing.T, rctx request.CTX, ss store.Store) {
channel := &model.Channel{
DisplayName: "Test Channel",
Name: "test_channel",
Type: model.ChannelTypeOpen,
}
channel, err := ss.Channel().Save(rctx, channel, -1)
require.NoError(t, err)
botId := model.NewId()
statusFieldId := model.NewId()
contentFlaggingManagedFieldId := model.NewId()
groupId := model.NewId()
setupFlaggedPost := func(rootId string) *model.Post {
post := &model.Post{}
post.ChannelId = channel.Id
post.UserId = model.NewId()
post.Message = NewTestID()
if rootId != "" {
post.RootId = rootId
}
var err error
post, err = ss.Post().Save(rctx, post)
require.NoError(t, err)
err = ss.Post().Delete(rctx, post.Id, model.GetMillis(), botId)
require.NoError(t, err)
statusPropertyValue := &model.PropertyValue{
TargetID: post.Id,
FieldID: statusFieldId,
Value: fmt.Appendf([]byte{}, "\"%s\"", model.ContentFlaggingStatusPending),
TargetType: model.PropertyValueTargetTypePost,
GroupID: groupId,
}
_, err = ss.PropertyValue().Create(statusPropertyValue)
require.NoError(t, err)
contentFlaggingManagedPropertyValue := &model.PropertyValue{
TargetID: post.Id,
FieldID: contentFlaggingManagedFieldId,
Value: json.RawMessage("true"),
TargetType: model.PropertyValueTargetTypePost,
GroupID: groupId,
}
_, err = ss.PropertyValue().Create(contentFlaggingManagedPropertyValue)
require.NoError(t, err)
return post
}
t.Run("Should restore a single root post", func(t *testing.T) {
post := setupFlaggedPost("")
fetchedPost, err := ss.Post().GetSingle(rctx, post.Id, true)
require.NoError(t, err)
require.Greater(t, fetchedPost.DeleteAt, int64(0))
err = ss.Post().RestoreContentFlaggedPost(post, statusFieldId, contentFlaggingManagedFieldId)
require.NoError(t, err)
fetchedPost, err = ss.Post().GetSingle(rctx, post.Id, false)
require.NoError(t, err)
require.Equal(t, int64(0), fetchedPost.DeleteAt)
})
t.Run("Should restore a thread reply and update thread's reply count", func(t *testing.T) {
rootPost := &model.Post{}
rootPost.ChannelId = channel.Id
rootPost.UserId = model.NewId()
rootPost.Message = NewTestID()
var err error
rootPost, err = ss.Post().Save(rctx, rootPost)
require.NoError(t, err)
post := setupFlaggedPost(rootPost.Id)
err = ss.Post().RestoreContentFlaggedPost(post, statusFieldId, contentFlaggingManagedFieldId)
require.NoError(t, err)
fetchedPost, err := ss.Post().GetSingle(rctx, post.Id, false)
require.NoError(t, err)
require.Equal(t, int64(0), fetchedPost.DeleteAt)
thread, err := ss.Thread().Get(rootPost.Id)
require.NoError(t, err)
require.Equal(t, int64(1), thread.ReplyCount)
})
}
func testDeleteAllPostRemindersForPost(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("should permanently delete all reminders for a post", func(t *testing.T) {
userID1 := NewTestID()
userID2 := NewTestID()
userID3 := NewTestID()
p1 := &model.Post{
UserId: userID1,
ChannelId: NewTestID(),
Message: "test post",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
// Create multiple reminders for the same post from different users
reminder1 := &model.PostReminder{
TargetTime: 1000,
PostId: p1.Id,
UserId: userID1,
}
require.NoError(t, ss.Post().SetPostReminder(reminder1))
reminder2 := &model.PostReminder{
TargetTime: 2000,
PostId: p1.Id,
UserId: userID2,
}
require.NoError(t, ss.Post().SetPostReminder(reminder2))
reminder3 := &model.PostReminder{
TargetTime: 3000,
PostId: p1.Id,
UserId: userID3,
}
require.NoError(t, ss.Post().SetPostReminder(reminder3))
// Verify reminders were created
var count int
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 3, count)
// Delete all reminders for the post
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
require.NoError(t, err)
// Verify all reminders were deleted
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 0, count)
})
t.Run("should only delete reminders for the specified post", func(t *testing.T) {
userID1 := NewTestID()
userID2 := NewTestID()
// Create two posts
p1 := &model.Post{
UserId: userID1,
ChannelId: NewTestID(),
Message: "test post 1",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
p2 := &model.Post{
UserId: userID1,
ChannelId: NewTestID(),
Message: "test post 2",
Type: model.PostTypeDefault,
}
p2, err = ss.Post().Save(rctx, p2)
require.NoError(t, err)
// Create reminders for both posts
reminder1 := &model.PostReminder{
TargetTime: 1000,
PostId: p1.Id,
UserId: userID1,
}
require.NoError(t, ss.Post().SetPostReminder(reminder1))
reminder2 := &model.PostReminder{
TargetTime: 2000,
PostId: p1.Id,
UserId: userID2,
}
require.NoError(t, ss.Post().SetPostReminder(reminder2))
reminder3 := &model.PostReminder{
TargetTime: 3000,
PostId: p2.Id,
UserId: userID1,
}
require.NoError(t, ss.Post().SetPostReminder(reminder3))
reminder4 := &model.PostReminder{
TargetTime: 4000,
PostId: p2.Id,
UserId: userID2,
}
require.NoError(t, ss.Post().SetPostReminder(reminder4))
// Delete all reminders for post1 only
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
require.NoError(t, err)
// Verify post1 reminders are deleted
var count int
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 0, count)
// Verify post2 reminders are still present
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p2.Id)
require.NoError(t, err)
require.Equal(t, 2, count)
})
t.Run("should not error when deleting from post with no reminders", func(t *testing.T) {
userID := NewTestID()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestID(),
Message: "test post",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
// Verify no reminders exist
var count int
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 0, count)
// Delete should not error
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
require.NoError(t, err)
})
t.Run("should not error when deleting from non-existent post", func(t *testing.T) {
nonExistentPostId := model.NewId()
// Delete should not error even for non-existent post
err := ss.Post().DeleteAllPostRemindersForPost(nonExistentPostId)
require.NoError(t, err)
})
t.Run("should handle deleting single reminder for post", func(t *testing.T) {
userID := NewTestID()
p1 := &model.Post{
UserId: userID,
ChannelId: NewTestID(),
Message: "test post",
Type: model.PostTypeDefault,
}
p1, err := ss.Post().Save(rctx, p1)
require.NoError(t, err)
// Create a single reminder
reminder := &model.PostReminder{
TargetTime: 1000,
PostId: p1.Id,
UserId: userID,
}
require.NoError(t, ss.Post().SetPostReminder(reminder))
// Verify reminder was created
var count int
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 1, count)
// Delete all reminders for the post
err = ss.Post().DeleteAllPostRemindersForPost(p1.Id)
require.NoError(t, err)
// Verify reminder was deleted
err = s.GetMaster().Get(&count, `SELECT COUNT(*) FROM PostReminders WHERE PostId=?`, p1.Id)
require.NoError(t, err)
require.Equal(t, 0, count)
})
}