1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-04-11 11:19:56 +02:00

Permissions feature branch (#2578)

* wip

* Added data migration for populating categories

* wip

* Added data migration for populating categories

* Store WIP

* migration WIP

* category CRUD APIs complete

* category block API WIP

* block category update API done

* Fetcehed data into store

* Started displayting sidebar data

* sidebar WIP

* Dashboard - basic changes

* Sidebar dashboard btn and board switcher UI only

* Sidebar dashboard btn and board switcher UI only

* create category dialog WIP

* Create category webapp side done

* Integrated move card to other category

* board to block

* Disabled dashboard route for now as we'll implement it in phase 2

* WIP

* Added logic to open last board/view on per team level

* Add workspace to teams and boards migrations (#1986)

* Add workspace to teams and boards migrations

* Update json annotations on board models

* boards search dialog WIP

* Seach dialog WIP

* Implemented opening boiard from search results

* Boards switcher styliung

* Handled update category WS event

* Template support

* personal server support and styling fixes

* test fix WIP

* Fixed a bug causing boards to not be moved correctly beteen categories

* Fixed webapp tests

* fix

* Store changes (#2011)

* Permissions phase 1 - Websocket updates (#2014)

* Store changes

* Websockets changes

* Permissions phase 1 - Permissions service (#2015)

* Store changes

* Websockets changes

* Permissions service

* Api and app updates (#2016)

* Store changes

* Websockets changes

* Permissions service

* New API and App changes

* Delete and Patch boards and blocks endpoints

* Used correct variable

* Webapp changes WIP

* Open correct team URL

* Fixed get block API

* Used React context for workspace users

* WIP

* On load navigation sorted out

* WIP

* Nav fix

* categories WS broadcast

* Used real search API

* Fixed unfurl ppreview

* set active team in sidebar

* IMplemented navigation on changing team in sidebar

* Misc fixes

* close rows inside transaction (#2045)

* update syntax for mysql (#2044)

* Upadted mutator for new patchBlock API

* Updated patchBlock API to use new URL

* Listeining to correct event in plugin mode

* Implemented WS messages for category operations:

* Fix duplicated build tags on Makefile

* Sidebar enhancements

* Add missing prefix to SQLite migration and fix flaky tests

* Sidebar boards menu enhancement

* Fix board page interactions (#2144)

* Fix patch board card properties error

* Fix board interactions

* Fix insert blocks interactions

* Fix app tests (#2104)

* Add json1 tag to vscode launch (#2157)

* Fix add, delete and update boards and add board patch generation (#2146)

* Fix update boards and add board patch generation

* Make add board and add template work, as well as deleting a board

* Update the state on board deletion

* Delete unused variable

* Fix bad parenthesis

* Fix board creation inside plugin, options were coming null due websocket message serialization

* update property type mutators to use boards API (#2168)

* Add permissions modal (#2196)

* Initial integration

* Permissions modal, websocket updates and API tests implemented

* Avoid updating/removing user if there is only one admin left

* Fix duplicates on board search

* Adds integration test

* Addressing PR review comments

Co-authored-by: Jesús Espino <jespinog@gmail.com>

* Merge

* I'm able to compile now

* Some fixes around tests execution

* Fixing migrations

* Fixing migrations order

* WIP

* Fixing some other compilation problems on tests

* Some typescript tests fixed

* Fixing javascript tests

* Fixing compilation

* Fixing some problems to create boards

* Load the templates on initial load

* Improvements over initial team templates import

* Adding new fields in the database

* Working on adding duplicate board api

* Removing RootID concept entirely

* Improving a bit the subscriptions

* Fixing store tests for notificationHints

* Fixing more tests

* fixing tests

* Fixing tests

* Fixing tests

* Fixing some small bugs related to templates

* Fixing registration link generation/regeneration

* Fixing cypress tests

* Adding store tests for duplicateBoard and duplicateBlock

* Addressing some TODO comments

* Making the export api simpler

* Add redirect component for old workspace urls

* Removing Dashboard code

* Delete only the built-in templates on update

* fixing tests

* Adding users autocompletion

* Updating snapshots

* Fixing bad merge

* fix panic when creating new card in notifysubscriptions (#2352)

* fix lint errors (#2353)

* fix lint errors

* fix panic when creating new card in notifysubscriptions (#2352)

* fix lint errors

* fix unit test

* Revert "fix unit test"

This reverts commit 0ad78aed65745521c0bb45790c9ea91b6c316c44.

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>

* fix sql syntax error for SearchUsersByTeam (#2357)

* Fix mentions delivery (#2358)

* fix sql syntax error for SearchUsersByTeam

* fix mentions delivery

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* update api for octoClient calls, pass correct variables to mutator (#2359)

* Fixing tests after merge

* Fix sidebar context menu UI issue (#2399)

* Fix notification diff for text blocks (#2386)

* fix notification diff for text blocks; fix various linter errors.

* fix URLs to cards

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* Permissions branch: Fix card links (#2391)

* fix notification diff for text blocks; fix various linter errors.

* fix URLs to cards

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* Fixing sqlite tests

* Fixing server tests

* Update migrations to create global templates. (#2397)

* fix duplicate templates

* revert migrate.go

* update UI for empty templates

* implement updating built-in templates as global (teamId = 0)

* handle error if board not found

* update unit test

* fix more tests

* Update blocks_test.go

Fix merge issue

* fix migration sql error (#2414)

* Fixing frontend tests

* Set target team ID when using a global template (#2419)

* Fix some server tests

* Fixing onboarding creation

* Permissions branch: Fix unit tests and CI errors (part 1) (#2425)

* Fixing some small memory leaks (#2400)

* Fixing some small memory leaks

* fixing tests

* passing the tags to all test targets

* Increasing the timeout of the tests

* Fix some type checkings

* Permissions branch: Fixes all the linter errors (#2429)

* fix linter errors

* Reestructuring the router and splitting in more subcomponents (#2403)

* Reestructuring the router and splitting in more subcomponents

* Removing console.log calls

* Removing unneeded selector

* Addressing PR comment

* Fix redirection to one team when you load directly the boards home path

* Using properly the lastTeamID to redirect the user if needed

* don't allow last admin change/deleted (#2416)

* don't allow last admin change/deleted

* update for i18-extract

* fixed en.json

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>

* Splitting BoardPage component into simpler/smaller components (#2435)

* Splitting BoardPage component into simpler/smaller components

* Removing unneeded import

* Replace go migrate with morph permissions (#2424)

* merge origin/replace-go-migrate-with-morph

* run go mod tidy on mattermost-plugin and increase test timeout

* fix merge issue temprorarily

* remove some debug changes

* fixing the linter

* Allow always team 0 (global) templates fetch (#2472)

* Fix problem with viewId 0 in the URL (#2473)

* Migrate from binddata to goembed (#2471)

* Adding join logic to the board switcher (#2434)

* Adding join logic to the board switcher

* Using already existing client function and removing the joinBoard one

* Adding support for autojoin based on url

* Fixing frontend tests

* fix webapp compile error, missing enableSharedBoards (#2501)

* Fixing duplication on postgres

* Adding back views to the sidebar (#2494)

* Fix #2507. Update Swagger comments (#2508)

* Fix the flash of the template selector on board/team switch (#2490)

* Fix the flash of the template selector on board/team switch

* More fixes specially around error handling

* Fixing the bot badge (#2487)

* simplifying a bit the team store sync between channels and focalboard (#2481)

* Fix menu tests (#2528)

* fix failing menu tests

* fix lint error

* Added keyboard shortcut for boards switcher (#2407)

* Added keyboard shortcut for boards switcher

* Fixed a type error

* Added some inline comments

* Fixed lint

* Fixed bug with scroll jumping when the card is opened: (#2477)

- avoid remounting of `ScrollingComponent` for each render of `Kanban` component
  - property `autoFocus` set to false for `CalculationOptions` because it triggers `blur` even for the button in Jest tests and closes the menu
  - snapshots for tests with `CalculationOptions` updated

* Adding the frontend support for permissions and applying it to a big part of the interface. (#2536)

* Initial work on permissions gates

* Applying permissions gates in more places

* Adding more checks to the interface

* Adding more permissions gates and keeping the store up to date

* fixing some tests

* Fixing some more tests

* Fixing another test

* Fixing all tests and adding some more

* Adding no-permission snapshot tests

* Addressing PR review comments

* Fixing invert behavior

* Permissions branch:  No sqlstore calls after app shutdown (#2530)

* fix webapp compile error, missing enableSharedBoards

* refactor app init wip

* - ensure all block change notifications are finished before shutting down app
- fix unit tests for mysql (insert_at only has 1 second resolution!)

* adjust logging

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* Fixed migrations to allow upgrading from previous version (#2535)

* Added mechanism to check if schema migration is needed

* WIP

* WIP

* WIP

* WIP

* Fixed migration

* Fixed for SQLite

* minor cleaniup

* Deleted old schema migration table after running migrations

* Removed a debug log

* Fixed a bug where the code always tried to delete a table which may or may not exist

* Show properly the user avatar in the ShareBoard component (#2542)

* Fixing the last CI problems from the permissions-branch (#2541)

* Fix history ordering

* Giving some times to avoid possible race conditions

* Empty

* Reverting accidental change in the config.json

* Optimizing table view (#2540)

* Optimizing table view

* Reducing the amount of rendering for tables

* Some other performance improvements

* Improve the activeView updates

* Some extra simplifications

* Another small improvement

* Fixing tests

* Fixing linter errors

* Reducing a bit the amount of dependency with big objects in the store

* Small simplification

* Removing Commenter role from the user role selector (#2561)

* Shareboard cleanup (#2550)

* Initial work on permissions gates

* Applying permissions gates in more places

* Adding more checks to the interface

* Adding more permissions gates and keeping the store up to date

* fixing some tests

* Fixing some more tests

* Fixing another test

* Fixing all tests and adding some more

* Adding no-permission snapshot tests

* Addressing PR review comments

* cleanup some shareboard settings

* remove unused property, fix for user items being displayed for non admin

* revert change, allow users to show

Co-authored-by: Jesús Espino <jespinog@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* Fixing comments and cards with the new optimizations in the store (#2560)

* Fixing property creation (#2563)

* Fix user selection in table view (#2565)

* Fixing focus new row in table view (#2567)

* Permissions branch: Fix sqlite table lock (CI) (#2568)

* fix sqlite table lock

* remove test db on teardown

* revert .gitignore

* fix goimport on migration code

* fix typo

* more linter fixes

* clean up tmp db for sqlstore tests

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>

* Fixing snapshots

* Migrating center panel to functional component (#2562)

* Migrating center panel to functional component

* Fixing some tests

* Fixing another test

* Fixing linter errors

* Fixing types errors

* Fixing linter error

* Fixing cypress tests

* Fixing the last cypress test

* Simpliying a bit the code

* Making property insertion more robust

* Updating checkbox test

Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com>
Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Co-authored-by: kamre <eremchenko@gmail.com>
This commit is contained in:
Jesús Espino 2022-03-22 15:24:34 +01:00 committed by GitHub
parent f3b8a49ea9
commit aa540e73ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
452 changed files with 33279 additions and 14457 deletions

1
.vscode/launch.json vendored
View File

@ -9,6 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"buildFlags": "-tags 'json1'",
"program": "${workspaceFolder}/server/main",
"cwd": "${workspaceFolder}"
},

View File

@ -12,6 +12,8 @@ ifeq ($(BUILD_NUMBER),)
BUILD_DATE := n/a
endif
BUILD_TAGS += json1
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildNumber=$(BUILD_NUMBER)"
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildDate=$(BUILD_DATE)"
LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildHash=$(BUILD_HASH)"
@ -35,25 +37,25 @@ ci: server-test
server: ## Build server for local environment.
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=dev")
cd server; go build -ldflags '$(LDFLAGS)' -o ../bin/focalboard-server ./main
cd server; go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/focalboard-server ./main
server-mac: ## Build server for Mac.
mkdir -p bin/mac
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=mac")
cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -o ../bin/mac/focalboard-server ./main
cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/mac/focalboard-server ./main
server-linux: ## Build server for Linux.
mkdir -p bin/linux
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux")
cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/linux/focalboard-server ./main
cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main
server-win: ## Build server for Windows.
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/win/focalboard-server.exe ./main
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/win/focalboard-server.exe ./main
server-dll: ## Build server as Windows DLL.
$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win")
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main
cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main
server-linux-package: server-linux webapp
rm -rf package
@ -83,7 +85,6 @@ server-linux-package-docker:
generate: ## Install and run code generators.
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
cd server; go get -modfile=go.tools.mod github.com/jteeuwen/go-bindata
cd server; go generate ./...
server-lint: ## Run linters on server code.
@ -101,18 +102,18 @@ modd-precheck:
fi; \
watch: modd-precheck ## Run both server and webapp watching for changes
modd
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd
watch-single-user: modd-precheck ## Run both server and webapp in single user mode watching for changes
env FOCALBOARDSERVER_ARGS=--single-user modd
env FOCALBOARDSERVER_ARGS=--single-user FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd
watch-server-test: modd-precheck ## Run server tests watching for changes
modd -f modd-servertest.conf
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-servertest.conf
server-test: server-test-sqlite server-test-mysql server-test-postgres ## Run server tests
server-test-sqlite: ## Run server tests using sqlite
cd server; go test -race -v -count=1 ./...
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
server-test-mysql: export FB_UNIT_TESTING=1
server-test-mysql: export FB_STORE_TEST_DB_TYPE=mysql
@ -120,8 +121,9 @@ server-test-mysql: export FB_STORE_TEST_DOCKER_PORT=44445
server-test-mysql: ## Run server tests using mysql
@echo Starting docker container for mysql
docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
docker-compose -f ./docker-testing/docker-compose-mysql.yml run start_dependencies
cd server; go test -race -v -count=1 ./...
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans
server-test-postgres: export FB_UNIT_TESTING=1
@ -130,8 +132,9 @@ server-test-postgres: export FB_STORE_TEST_DOCKER_PORT=44446
server-test-postgres: ## Run server tests using postgres
@echo Starting docker container for postgres
docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
docker-compose -f ./docker-testing/docker-compose-postgres.yml run start_dependencies
cd server; go test -race -v -count=1 ./...
cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./...
docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans
webapp: ## Build webapp.
@ -141,7 +144,7 @@ webapp-test: ## jest tests for webapp
cd webapp; npm run test
watch-plugin: modd-precheck ## Run and upload the plugin to a development server
modd -f modd-watchplugin.conf
env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-watchplugin.conf
live-watch-plugin: modd-precheck ## Run and update locally the plugin in the development server
cd mattermost-plugin; make live-watch

View File

@ -1,8 +1,8 @@
{
"serverRoot": "http://localhost:8000",
"port": 8000,
"dbtype": "sqlite3",
"dbconfig": "./focalboard.db",
"port": 8000,
"dbtype": "sqlite3",
"dbconfig": "./focalboard.db",
"dbtableprefix": "",
"postgres_dbconfig": "dbname=focalboard sslmode=disable",
"test_dbconfig": "file::memory:?cache=shared",
@ -21,5 +21,5 @@
"authMode": "native",
"logging_cfg_file": "",
"audit_cfg_file": "",
"enablepublicsharedboards": false
"enablePublicSharedBoards": false
}

View File

@ -38,7 +38,7 @@ export async function logIn(host: string, username: string, password: string) {
export async function getBoards(host: string, token: string) {
const json = await request('GET', host, 'workspaces/0/blocks?type=board', null, token) as Board[]
return json.filter(board => !board.fields.isTemplate)
return json.filter(board => !board.isTemplate)
}
export async function findUrlPropertyId(host: string, token: string, boardId: string) {

View File

@ -129,7 +129,7 @@ function convert(input: Asana): Block[] {
type: 'select',
options
}
board.fields.cardProperties = [cardProperty]
board.cardProperties = [cardProperty]
blocks.push(board)
// Board view

View File

@ -90,25 +90,25 @@ function convert(items: any[]) {
board.title = 'Jira import'
// Compile standard properties
board.fields.cardProperties = []
board.cardProperties = []
const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._))
board.fields.cardProperties.push(priorityProperty)
board.cardProperties.push(priorityProperty)
const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._))
board.fields.cardProperties.push(statusProperty)
board.cardProperties.push(statusProperty)
const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._))
board.fields.cardProperties.push(resolutionProperty)
board.cardProperties.push(resolutionProperty)
const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._))
board.fields.cardProperties.push(typeProperty)
board.cardProperties.push(typeProperty)
const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._))
board.fields.cardProperties.push(assigneeProperty)
board.cardProperties.push(assigneeProperty)
const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._))
board.fields.cardProperties.push(reporterProperty)
board.cardProperties.push(reporterProperty)
const originalUrlProperty: IPropertyTemplate = {
id: Utils.createGuid(),
@ -116,7 +116,7 @@ function convert(items: any[]) {
type: 'url',
options: []
}
board.fields.cardProperties.push(originalUrlProperty)
board.cardProperties.push(originalUrlProperty)
const createdDateProperty: IPropertyTemplate = {
id: Utils.createGuid(),
@ -124,7 +124,7 @@ function convert(items: any[]) {
type: 'date',
options: []
}
board.fields.cardProperties.push(createdDateProperty)
board.cardProperties.push(createdDateProperty)
blocks.push(board)
@ -240,4 +240,4 @@ function showHelp() {
exit(1)
}
export { run }
export { run }

View File

@ -135,7 +135,7 @@ function convert(input: any[], title: string): Block[] {
type: 'select',
options: []
}
board.fields.cardProperties.push(cardProperty)
board.cardProperties.push(cardProperty)
})
// Set all column types to select
@ -177,7 +177,7 @@ function convert(input: any[], title: string): Block[] {
continue
}
const cardProperty = board.fields.cardProperties.find((o) => o.name === key)!
const cardProperty = board.cardProperties.find((o) => o.name === key)!
let option = cardProperty.options.find((o) => o.value === value)
if (!option) {
const color = optionColors[optionColorIndex % optionColors.length]

View File

@ -82,7 +82,7 @@ function convert(input: Todoist, project: Project): Block[] {
console.log(`Board: ${project.name}`)
board.rootId = board.id
board.title = project.name
board.fields.description = project.name
board.description = project.name
// Convert lists (columns) to a Select property
const optionIdMap = new Map<string, string>()
@ -114,7 +114,7 @@ function convert(input: Todoist, project: Project): Block[] {
type: 'select',
options
}
board.fields.cardProperties = [cardProperty]
board.cardProperties = [cardProperty]
blocks.push(board)
// Board view

View File

@ -68,7 +68,7 @@ function convert(input: Trello): Block[] {
console.log(`Board: ${input.name}`)
board.rootId = board.id
board.title = input.name
board.fields.description = input.desc
board.description = input.desc
// Convert lists (columns) to a Select property
const optionIdMap = new Map<string, string>()
@ -92,7 +92,7 @@ function convert(input: Trello): Block[] {
type: 'select',
options
}
board.fields.cardProperties = [cardProperty]
board.cardProperties = [cardProperty]
blocks.push(board)
// Board view

View File

@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/mattermost/focalboard/server/server"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
"github.com/webview/webview"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -66,6 +67,8 @@ func runServer(port int) (*server.Server, error) {
return nil, err
}
permissionsService := localpermissions.New(db, logger)
params := server.Params{
Cfg: config,
SingleUserToken: sessionToken,
@ -74,6 +77,7 @@ func runServer(port int) (*server.Server, error) {
ServerID: "",
WSAdapter: nil,
NotifyBackends: nil,
PermissionsService: permissionsService,
}
server, err := server.New(params)

View File

@ -61,7 +61,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@ -91,7 +90,6 @@ github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
@ -235,7 +233,6 @@ github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkb
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY=
github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -260,6 +257,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -272,20 +270,15 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ=
github.com/dhui/dktest v0.3.4 h1:VbUEcaSP+U2/yUr9d2JhSThXYEnDlGabRSHe2rIE46E=
github.com/dhui/dktest v0.3.4/go.mod h1:4m4n6lmXlmVfESth7mzdcv8nBI5mOb5UROPqjM02csU=
github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible h1:nhVo1udYfMj0Jsw0lnqrTjjf33aLpdgW9Wve9fHVzhQ=
github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@ -315,6 +308,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@ -410,10 +404,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
github.com/golang-migrate/migrate/v4 v4.15.0 h1:LKvQ+CgezLw0zuR/ib1y9sQStG0vepWaEVUsQof0bo0=
github.com/golang-migrate/migrate/v4 v4.15.0/go.mod h1:g9qbiDvB47WyrRnNu2t2gMZFNHKnatsYRxsGZbCi4EM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@ -542,7 +534,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@ -557,7 +548,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
@ -684,6 +674,7 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
@ -778,6 +769,8 @@ github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08=
github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3 h1:+E2WOqMrCGZTGjhmWVsszj2Qqx7Amh/OBUedkLLtnP4=
github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3/go.mod h1:VH26NcOr3xgkSBAvh4q+9+RoBD/M9gYO2F1PISq9KMw=
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14=
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw=
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -807,6 +800,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@ -852,7 +846,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@ -905,9 +898,7 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
@ -990,6 +981,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
@ -1233,7 +1225,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -1321,6 +1312,7 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1512,9 +1504,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed h1:E159xujlywdAeN3FqsTBPzRKGUq/pDHolXbuttkC36E=
golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1612,6 +1606,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1830,31 +1825,147 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM=
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc=
modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8=
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q=
modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ=
modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y=
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw=
modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk=
modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo=
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

View File

@ -14,6 +14,7 @@ import (
"github.com/mattermost/focalboard/server/server"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
"github.com/mattermost/focalboard/server/services/store/sqlstore"
@ -29,16 +30,17 @@ import (
)
const (
boardsFeatureFlagName = "BoardsFeatureFlags"
pluginName = "focalboard"
sharedBoardsName = "enablepublicsharedboards"
boardsFeatureFlagName = "BoardsFeatureFlags"
pluginName = "focalboard"
sharedBoardsName = "enablepublicsharedboards"
notifyFreqCardSecondsKey = "notify_freq_card_seconds"
notifyFreqBoardSecondsKey = "notify_freq_board_seconds"
)
type BoardsEmbed struct {
OriginalPath string `json:"originalPath"`
WorkspaceID string `json:"workspaceID"`
TeamID string `json:"teamID"`
ViewID string `json:"viewID"`
BoardID string `json:"boardID"`
CardID string `json:"cardID"`
@ -112,7 +114,9 @@ func (p *Plugin) OnActivate() error {
db = layeredStore
}
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db), logger)
permissionsService := mmpermissions.New(db, p.API)
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db, permissionsService), db, logger)
backendParams := notifyBackendParams{
cfg: cfg,
@ -137,13 +141,14 @@ func (p *Plugin) OnActivate() error {
mentionsBackend.AddListener(subscriptionsBackend)
params := server.Params{
Cfg: cfg,
SingleUserToken: "",
DBStore: db,
Logger: logger,
ServerID: serverID,
WSAdapter: p.wsPluginAdapter,
NotifyBackends: notifyBackends,
Cfg: cfg,
SingleUserToken: "",
DBStore: db,
Logger: logger,
ServerID: serverID,
WSAdapter: p.wsPluginAdapter,
NotifyBackends: notifyBackends,
PermissionsService: permissionsService,
}
server, err := server.New(params)
@ -373,11 +378,11 @@ func postWithBoardsEmbed(post *mmModel.Post) *mmModel.Post {
return post
}
workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
if workspaceID != "" && boardID != "" && viewID != "" && cardID != "" {
if teamID != "" && boardID != "" && viewID != "" && cardID != "" {
b, _ := json.Marshal(BoardsEmbed{
WorkspaceID: workspaceID,
TeamID: teamID,
BoardID: boardID,
ViewID: viewID,
CardID: cardID,
@ -430,7 +435,7 @@ func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPost
return firstLink, newPostMessage
}
func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardID string) {
func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) {
// The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths
index := -1
for i := 0; i < len(pathArray); i++ {
@ -441,7 +446,7 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI
}
if index == -1 {
return workspaceID, boardID, viewID, cardID
return teamID, boardID, viewID, cardID
}
// If at index, the parameter in the path is boards,
@ -450,27 +455,27 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI
// If at index, the parameter in the path is plugins,
// then we've copied this from a shared board
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/workspace/workspaceID/boardID/viewID/cardID
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID
// For card links copied on a shared board, the path looks like
// {...Mattermost Url}.../plugins/focalboard/workspace/workspaceID/shared/boardID/viewID/cardID?r=read_token
// {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token
// This is a non-shared board card link
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "workspace" {
workspaceID = pathArray[index+2]
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" {
teamID = pathArray[index+2]
boardID = pathArray[index+3]
viewID = pathArray[index+4]
cardID = pathArray[index+5]
} else if len(pathArray)-index == 8 && pathArray[index] == "plugins" &&
pathArray[index+1] == "focalboard" &&
pathArray[index+2] == "workspace" &&
pathArray[index+2] == "team" &&
pathArray[index+4] == "shared" { // This is a shared board card link
workspaceID = pathArray[index+3]
teamID = pathArray[index+3]
boardID = pathArray[index+5]
viewID = pathArray[index+6]
cardID = pathArray[index+7]
}
return workspaceID, boardID, viewID, cardID
return teamID, boardID, viewID, cardID
}
func isBoardsLink(link string) bool {
@ -489,6 +494,6 @@ func isBoardsLink(link string) bool {
return false
}
workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
return workspaceID != "" && boardID != "" && viewID != "" && cardID != ""
teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
return teamID != "" && boardID != "" && viewID != "" && cardID != ""
}

View File

@ -36,17 +36,34 @@ function mapStateToProps(state: GlobalState) {
}
}
class FocalboardEmbeddedData {
teamID: string
cardID: string
boardID: string
readToken: string
originalPath: string
constructor(rawData: string) {
const parsed = JSON.parse(rawData)
this.teamID = parsed.teamID || parsed.workspaceID
this.cardID = parsed.cardID
this.boardID = parsed.boardID
this.readToken = parsed.readToken
this.originalPath = parsed.originalPath
}
}
const BoardsUnfurl = (props: Props): JSX.Element => {
if (!props.embed || !props.embed.data) {
return <></>
}
const {embed, locale} = props
const focalboardInformation = JSON.parse(embed.data)
const {workspaceID, cardID, boardID, readToken, originalPath} = focalboardInformation
const focalboardInformation: FocalboardEmbeddedData = new FocalboardEmbeddedData(embed.data)
const {teamID, cardID, boardID, readToken, originalPath} = focalboardInformation
const baseURL = window.location.origin
if (!workspaceID || !cardID || !boardID) {
if (!teamID || !cardID || !boardID) {
return <></>
}
@ -57,20 +74,19 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
useEffect(() => {
const fetchData = async () => {
const [cards, boards] = await Promise.all(
const [cards, fetchedBoard] = await Promise.all(
[
octoClient.getBlocksWithBlockID(cardID, workspaceID, readToken),
octoClient.getBlocksWithBlockID(boardID, workspaceID, readToken),
octoClient.getBlocksWithBlockID(cardID, boardID, readToken),
octoClient.getBoard(boardID),
],
)
const [firstCard] = cards as Card[]
const [firstBoard] = boards as Board[]
if (!firstCard || !firstBoard) {
if (!firstCard || !fetchedBoard) {
setLoading(false)
return null
}
setCard(firstCard)
setBoard(firstBoard)
setBoard(fetchedBoard)
if (firstCard.fields.contentOrder.length) {
let [firstContentBlockID] = firstCard.fields?.contentOrder
@ -79,7 +95,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
[firstContentBlockID] = firstContentBlockID
}
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, workspaceID, readToken) as ContentBlock[]
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, boardID, readToken) as ContentBlock[]
const [firstContentBlock] = contentBlock
if (!firstContentBlock) {
setLoading(false)
@ -103,8 +119,8 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
let totalNumberOfCheckBoxes = 0
// We will just display the first 3 or less select/multi-select properties and do a +n for remainder if any remainder
for (let i = 0; i < board.fields.cardProperties.length; i++) {
const optionInBoard = board.fields.cardProperties[i]
for (let i = 0; i < board.cardProperties.length; i++) {
const optionInBoard = board.cardProperties[i]
let valueToLookUp = card.fields.properties[optionInBoard.id]
// Since these are always set and not included in the card properties
@ -132,7 +148,11 @@ const BoardsUnfurl = (props: Props): JSX.Element => {
continue
}
propertiesToDisplay.push({optionName: optionInBoard.name, optionValue: optionSelected.value, optionValueColour: optionSelected.color})
propertiesToDisplay.push({
optionName: optionInBoard.name,
optionValue: optionSelected.value,
optionValueColour: optionSelected.color,
})
}
remainder += (Object.keys(card.fields.properties).length - propertiesToDisplay.length - totalNumberOfCheckBoxes)
html = Utils.htmlFromMarkdown(content?.title || '')

View File

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React from 'react'
import PropTypes from 'prop-types'
import {Utils} from '../../../webapp/src/utils'
@ -10,9 +9,12 @@ type State = {
hasError: boolean
}
export default class ErrorBoundary extends React.Component {
type Props = {
children: React.ReactNode
}
export default class ErrorBoundary extends React.Component<Props, State> {
state = {hasError: false}
propTypes = {children: PropTypes.node.isRequired}
msg = 'Redirecting to error page...'
handleError = (): void => {

View File

@ -9,18 +9,22 @@ import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/r
import {GlobalState} from 'mattermost-redux/types/store'
const windowAny = (window as any)
import {selectTeam} from 'mattermost-redux/actions/teams'
import {SuiteWindow} from '../../../webapp/src/types/index'
const windowAny = (window as SuiteWindow)
windowAny.baseURL = '/plugins/focalboard'
windowAny.frontendBaseURL = '/boards'
windowAny.isFocalboardPlugin = true
import App from '../../../webapp/src/app'
import store from '../../../webapp/src/store'
import {setTeam} from '../../../webapp/src/store/teams'
import {Utils} from '../../../webapp/src/utils'
import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader'
import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
import {setMattermostTheme} from '../../../webapp/src/theme'
import {UserSettings} from '../../../webapp/src/userSettings'
import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient'
@ -30,7 +34,13 @@ import '../../../webapp/src/styles/labels.scss'
import octoClient from '../../../webapp/src/octoClient'
import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl'
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG, ACTION_UPDATE_SUBSCRIPTION} from './../../../webapp/src/wsclient'
import wsClient, {
MMWebSocketClient,
ACTION_UPDATE_BLOCK,
ACTION_UPDATE_CLIENT_CONFIG,
ACTION_UPDATE_SUBSCRIPTION,
ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BLOCK_CATEGORY, ACTION_UPDATE_BOARD,
} from './../../../webapp/src/wsclient'
import manifest from './manifest'
import ErrorBoundary from './error_boundary'
@ -164,6 +174,7 @@ export default class Plugin {
let theme = mmStore.getState().entities.preferences.myPreferences.theme
setMattermostTheme(theme)
let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId
let prevTeamID: string
mmStore.subscribe(() => {
const currentUserId = mmStore.getState().entities.users.currentUserId
const currentChannel = mmStore.getState().entities.channels.currentChannelId
@ -171,22 +182,41 @@ export default class Plugin {
localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel)
lastViewedChannel = currentChannel
}
// Watch for change in active team.
// This handles the user selecting a team from the team sidebar.
const currentTeamID = mmStore.getState().entities.teams.currentTeamId
if (currentTeamID && currentTeamID !== prevTeamID) {
prevTeamID = currentTeamID
store.dispatch(setTeam(currentTeamID))
browserHistory.push(`/team/${currentTeamID}`)
wsClient.subscribeToTeam(currentTeamID)
}
})
if (this.registry.registerProduct) {
windowAny.frontendBaseURL = subpath + '/boards'
const goToFocalboardWorkspace = () => {
const currentChannel = mmStore.getState().entities.channels.currentChannelId
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {workspaceID: currentChannel})
window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener')
const goToFocalboard = () => {
const currentTeam = mmStore.getState().entities.teams.currentTeamId
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {teamID: currentTeam})
window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}`, '_blank', 'noopener')
}
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboardWorkspace, 'Boards', 'Boards')
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboard, 'Boards', 'Boards')
this.registry.registerProduct(
'/boards',
'product-boards',
'Boards',
'/boards',
MainApp,
HeaderComponent,
() => null,
true,
)
const goToFocalboardTemplate = () => {
const currentChannel = mmStore.getState().entities.channels.currentChannelId
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {workspaceID: currentChannel})
UserSettings.lastBoardId = null
UserSettings.lastViewId = null
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {channelID: currentChannel})
window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener')
}
@ -194,11 +224,9 @@ export default class Plugin {
this.channelHeaderButtonId = registry.registerChannelIntroButtonAction(<FocalboardIcon/>, goToFocalboardTemplate, 'Boards')
}
this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent)
if (this.registry.registerAppBarComponent) {
const appBarIconURL = windowAny.baseURL + '/public/app-bar-icon.png'
this.registry.registerAppBarComponent(appBarIconURL, goToFocalboardWorkspace, 'Open Boards Workspace')
this.registry.registerAppBarComponent(appBarIconURL, goToFocalboard, 'Open Boards')
}
this.registry.registerPostWillRenderEmbedComponent((embed) => embed.type === 'boards', BoardsUnfurl, false)
@ -247,7 +275,9 @@ export default class Plugin {
}
// register websocket handlers
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data))
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD}`, (e: any) => wsClient.updateHandler(e.data))
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data))
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data))
this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data))
@ -267,6 +297,16 @@ export default class Plugin {
}
}
})
windowAny.setTeamInSidebar = (teamID: string) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mmStore.dispatch(selectTeam(teamID))
}
windowAny.getCurrentTeamId = (): string => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return mmStore.getState().entities.teams.currentTeamId
}
}
uninitialize(): void {

View File

@ -10,7 +10,7 @@ export interface PluginRegistry {
registerCustomRoute(route: string, component: React.ElementType)
registerProductRoute(route: string, component: React.ElementType)
unregisterComponent(componentId: string)
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType)
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCentreComponent: React.ElementType, headerRightComponent?: React.ElementType, showTeamSidebar: boolean)
registerPostWillRenderEmbedComponent(match: (embed: {type: string, data: any}) => void, component: any, toggleable: boolean)
registerWebSocketEventHandler(event: string, handler: (e: any) => void)
unregisterWebSocketEventHandler(event: string)

View File

@ -1,3 +1,3 @@
**/*.go {
prep: cd server && go test -race -v ./...
prep: cd server && go test -tags $FOCALBOARD_BUILD_TAGS -race -v ./...
}

View File

@ -1,5 +1,5 @@
**/*.go !**/*_test.go {
prep: cd server && go build -o ../bin/focalboard-server ./main
prep: cd server && go build -tags $FOCALBOARD_BUILD_TAGS -o ../bin/focalboard-server ./main
daemon +sigterm: ./bin/focalboard-server $FOCALBOARDSERVER_ARGS
}

View File

@ -13,7 +13,7 @@ linters-settings:
disable:
- fieldalignment
lll:
line-length: 150
line-length: 180
dupl:
threshold: 200
revive:

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import (
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
)
@ -13,26 +14,20 @@ const (
archiveExtension = ".boardarchive"
)
func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/workspaces/{workspaceID}/archive/export archiveExport
func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/boards/{boardID}/archive/export archiveExportBoard
//
// Exports an archive of all blocks for one or more boards. If board_id is provided then
// only that board will be exported, otherwise all boards in the workspace are exported.
// Exports an archive of all blocks for one boards.
//
// ---
// produces:
// - application/json
// parameters:
// - name: workspaceID
// - name: boardID
// in: path
// description: Workspace ID
// description: Id of board to export
// required: true
// type: string
// - name: board_id
// in: path
// description: Id of board to to export
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
@ -47,25 +42,92 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
boardID := query.Get("board_id")
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
vars := mux.Vars(r)
boardID := vars["boardID"]
auditRec := a.makeAuditRecord(r, "archiveExport", audit.Fail)
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("BoardID", boardID)
var boardIDs []string
if boardID != "" {
boardIDs = []string{boardID}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
return
}
opts := model.ExportArchiveOptions{
WorkspaceID: container.WorkspaceID,
BoardIDs: boardIDs,
TeamID: board.TeamID,
BoardIDs: []string{board.ID},
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
}
auditRec.Success()
}
func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v1/teams/{teamID}/archive/export archiveExportTeam
//
// Exports an archive of all blocks for all the boards in a team.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Id of team
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// content:
// application-octet-stream:
// type: string
// format: binary
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("TeamID", teamID)
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
ids := []string{}
for _, board := range boards {
ids = append(ids, board.ID)
}
opts := model.ExportArchiveOptions{
TeamID: teamID,
BoardIDs: ids,
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
@ -81,7 +143,7 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) {
}
func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /api/v1/workspaces/{workspaceID}/archive/import archiveImport
// swagger:operation POST /api/v1/boards/{boardID}/archive/import archiveImport
//
// Import an archive of boards.
//
@ -91,7 +153,7 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// consumes:
// - multipart/form-data
// parameters:
// - name: workspaceID
// - name: boardID
// in: path
// description: Workspace ID
// required: true
@ -111,16 +173,13 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// schema:
// "$ref": "#/definitions/ErrorResponse"
container, err := a.getContainer(r)
if err != nil {
a.noContainerErrorResponse(w, r.URL.Path, err)
return
}
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
fmt.Fprintf(w, "%v", err)
@ -134,8 +193,8 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
auditRec.AddMeta("size", handle.Size)
opt := model.ImportArchiveOptions{
WorkspaceID: container.WorkspaceID,
ModifiedBy: userID,
TeamID: teamID,
ModifiedBy: userID,
}
if err := a.app.ImportArchive(file, opt); err != nil {

View File

@ -17,12 +17,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin
userID = session.UserID
}
workspaceID := "unknown"
container, err := a.getContainer(r)
if err == nil {
workspaceID = container.WorkspaceID
}
teamID := "unknown"
rec := &audit.Record{
APIPath: r.URL.Path,
Event: event,
@ -31,7 +26,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin
SessionID: sessionID,
Client: r.UserAgent(),
IPAddress: r.RemoteAddr,
Meta: []audit.Meta{{K: audit.KeyWorkspaceID, V: workspaceID}},
Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}},
}
return rec

View File

@ -302,13 +302,13 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
// Validate token
if len(registerData.Token) > 0 {
workspace, err2 := a.app.GetRootWorkspace()
team, err2 := a.app.GetRootTeam()
if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
return
}
if registerData.Token != workspace.SignupToken {
if registerData.Token != team.SignupToken {
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid token", nil)
return
}

View File

@ -1,12 +1,16 @@
package app
import (
"time"
"github.com/mattermost/focalboard/server/auth"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/metrics"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/services/webhook"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/focalboard/server/ws"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -14,6 +18,12 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/filestore"
)
const (
blockChangeNotifierQueueSize = 100
blockChangeNotifierPoolSize = 10
blockChangeNotifierShutdownTimeout = time.Second * 10
)
type Services struct {
Auth *auth.Auth
Store store.Store
@ -22,19 +32,21 @@ type Services struct {
Metrics *metrics.Metrics
Notifications *notify.Service
Logger *mlog.Logger
Permissions permissions.PermissionsService
SkipTemplateInit bool
}
type App struct {
config *config.Configuration
store store.Store
auth *auth.Auth
wsAdapter ws.Adapter
filesBackend filestore.FileBackend
webhook *webhook.Client
metrics *metrics.Metrics
notifications *notify.Service
logger *mlog.Logger
config *config.Configuration
store store.Store
auth *auth.Auth
wsAdapter ws.Adapter
filesBackend filestore.FileBackend
webhook *webhook.Client
metrics *metrics.Metrics
notifications *notify.Service
logger *mlog.Logger
blockChangeNotifier *utils.CallbackQueue
}
func (a *App) SetConfig(config *config.Configuration) {
@ -43,15 +55,16 @@ func (a *App) SetConfig(config *config.Configuration) {
func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App {
app := &App{
config: config,
store: services.Store,
auth: services.Auth,
wsAdapter: wsAdapter,
filesBackend: services.FilesBackend,
webhook: services.Webhook,
metrics: services.Metrics,
notifications: services.Notifications,
logger: services.Logger,
config: config,
store: services.Store,
auth: services.Auth,
wsAdapter: wsAdapter,
filesBackend: services.FilesBackend,
webhook: services.Webhook,
metrics: services.Metrics,
notifications: services.Notifications,
logger: services.Logger,
blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger),
}
app.initialize(services.SkipTemplateInit)
return app

View File

@ -3,7 +3,6 @@ package app
import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/auth"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -25,8 +24,8 @@ func (a *App) GetSession(token string) (*model.Session, error) {
}
// IsValidReadToken validates the read token for a block.
func (a *App) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) {
return a.auth.IsValidReadToken(c, blockID, readToken)
func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) {
return a.auth.IsValidReadToken(boardID, readToken)
}
// GetRegisteredUserCount returns the number of registered users.

View File

@ -1,137 +1,192 @@
package app
import (
"errors"
"fmt"
"path/filepath"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([]model.Block, error) {
var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards")
func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) {
if boardID == "" {
return []model.Block{}, nil
}
if blockType != "" && parentID != "" {
return a.store.GetBlocksWithParentAndType(c, parentID, blockType)
return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType)
}
if blockType != "" {
return a.store.GetBlocksWithType(c, blockType)
return a.store.GetBlocksWithType(boardID, blockType)
}
return a.store.GetBlocksWithParent(c, parentID)
return a.store.GetBlocksWithParent(boardID, parentID)
}
func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) {
return a.store.GetBlocksWithRootID(c, rootID)
func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) {
board, err := a.GetBoard(boardID)
if err != nil {
return nil, err
}
if board == nil {
return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err)
}
blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
for _, block := range blocks {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
}
return nil
})
return blocks, err
}
func (a *App) GetRootID(c store.Container, blockID string) (string, error) {
return a.store.GetRootID(c, blockID)
func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
return a.store.GetBlocksWithBoardID(boardID)
}
func (a *App) GetParentID(c store.Container, blockID string) (string, error) {
return a.store.GetParentID(c, blockID)
}
func (a *App) PatchBlock(c store.Container, blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
oldBlock, err := a.store.GetBlock(c, blockID)
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
oldBlock, err := a.store.GetBlock(blockID)
if err != nil {
return nil
}
err = a.store.PatchBlock(c, blockID, blockPatch, modifiedByID)
board, err := a.store.GetBoard(oldBlock.BoardID)
if err != nil {
return err
}
err = a.store.PatchBlock(blockID, blockPatch, modifiedByID)
if err != nil {
return err
}
a.metrics.IncrementBlocksPatched(1)
block, err := a.store.GetBlock(c, blockID)
block, err := a.store.GetBlock(blockID)
if err != nil {
return nil
}
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block)
go func() {
a.blockChangeNotifier.Enqueue(func() error {
// broadcast on websocket
a.wsAdapter.BroadcastBlockChange(board.TeamID, *block)
// broadcast on webhooks
a.webhook.NotifyUpdate(*block)
a.notifyBlockChanged(notify.Update, c, block, oldBlock, modifiedByID)
}()
// send notifications
a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID)
return nil
})
return nil
}
func (a *App) PatchBlocks(c store.Container, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
oldBlocks := make([]model.Block, 0, len(blockPatches.BlockIDs))
for _, blockID := range blockPatches.BlockIDs {
oldBlock, err := a.store.GetBlock(c, blockID)
oldBlock, err := a.store.GetBlock(blockID)
if err != nil {
return nil
}
oldBlocks = append(oldBlocks, *oldBlock)
}
err := a.store.PatchBlocks(c, blockPatches, modifiedByID)
err := a.store.PatchBlocks(blockPatches, modifiedByID)
if err != nil {
return err
}
a.metrics.IncrementBlocksPatched(len(oldBlocks))
for i, blockID := range blockPatches.BlockIDs {
newBlock, err := a.store.GetBlock(c, blockID)
if err != nil {
return nil
}
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *newBlock)
go func(currentIndex int) {
a.blockChangeNotifier.Enqueue(func() error {
a.metrics.IncrementBlocksPatched(len(oldBlocks))
for i, blockID := range blockPatches.BlockIDs {
newBlock, err := a.store.GetBlock(blockID)
if err != nil {
return nil
}
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
a.webhook.NotifyUpdate(*newBlock)
a.notifyBlockChanged(notify.Update, c, newBlock, &oldBlocks[currentIndex], modifiedByID)
}(i)
}
a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID)
}
return nil
})
return nil
}
func (a *App) InsertBlock(c store.Container, block model.Block, modifiedByID string) error {
err := a.store.InsertBlock(c, &block, modifiedByID)
func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
board, bErr := a.store.GetBoard(block.BoardID)
if bErr != nil {
return bErr
}
err := a.store.InsertBlock(&block, modifiedByID)
if err == nil {
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, block)
a.metrics.IncrementBlocksInserted(1)
go func() {
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(block)
a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID)
}()
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
return nil
})
}
return err
}
func (a *App) InsertBlocks(c store.Container, blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) {
func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) {
if len(blocks) == 0 {
return []model.Block{}, nil
}
// all blocks must belong to the same board
boardID := blocks[0].BoardID
for _, block := range blocks {
if block.BoardID != boardID {
return nil, ErrBlocksFromMultipleBoards
}
}
board, err := a.store.GetBoard(boardID)
if err != nil {
return nil, err
}
needsNotify := make([]model.Block, 0, len(blocks))
for i := range blocks {
err := a.store.InsertBlock(c, &blocks[i], modifiedByID)
err := a.store.InsertBlock(&blocks[i], modifiedByID)
if err != nil {
return nil, err
}
blocks[i].WorkspaceID = c.WorkspaceID
needsNotify = append(needsNotify, blocks[i])
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, blocks[i])
a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i])
a.metrics.IncrementBlocksInserted(1)
}
go func() {
a.blockChangeNotifier.Enqueue(func() error {
for _, b := range needsNotify {
block := b
a.webhook.NotifyUpdate(block)
if allowNotifications {
a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID)
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
}
}
}()
return nil
})
return blocks, nil
}
func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks []model.Block) error {
func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error {
// Images attached in cards have a path comprising the card's board ID.
// When we create a template from this board, we need to copy the files
// with the new board ID in path.
@ -139,7 +194,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
// template) to fail to load.
// look up ID of source board, which may be different than the blocks.
board, err := a.GetBlockByID(store.Container{}, sourceBoardID)
board, err := a.GetBlockByID(sourceBoardID)
if err != nil || board == nil {
return fmt.Errorf("cannot fetch board %s for CopyCardFiles: %w", sourceBoardID, err)
}
@ -153,8 +208,8 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
ext := filepath.Ext(fileName.(string))
destFilename := utils.NewID(utils.IDTypeNone) + ext
sourceFilePath := filepath.Join(board.WorkspaceID, sourceBoardID, fileName.(string))
destinationFilePath := filepath.Join(destWorkspaceID, block.RootID, destFilename)
sourceFilePath := filepath.Join(sourceBoardID, fileName.(string))
destinationFilePath := filepath.Join(block.BoardID, destFilename)
a.logger.Debug(
"Copying card file",
@ -179,24 +234,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks
return nil
}
func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) {
func (a *App) GetSubTree(boardID, blockID string, levels int, opts model.QuerySubtreeOptions) ([]model.Block, error) {
// Only 2 or 3 levels are supported for now
if levels >= 3 {
return a.store.GetSubTree3(c, blockID, model.QuerySubtreeOptions{})
return a.store.GetSubTree3(boardID, blockID, opts)
}
return a.store.GetSubTree2(c, blockID, model.QuerySubtreeOptions{})
return a.store.GetSubTree2(boardID, blockID, opts)
}
func (a *App) GetAllBlocks(c store.Container) ([]model.Block, error) {
return a.store.GetAllBlocks(c)
func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
return a.store.GetBlock(blockID)
}
func (a *App) GetBlockByID(c store.Container, blockID string) (*model.Block, error) {
return a.store.GetBlock(c, blockID)
}
func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
block, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) error {
block, err := a.store.GetBlock(c, blockID)
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return err
}
@ -206,7 +263,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
return nil
}
err = a.store.DeleteBlock(c, blockID, modifiedBy)
err = a.store.DeleteBlock(blockID, modifiedBy)
if err != nil {
return err
}
@ -214,7 +271,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
if block.Type == model.TypeImage {
fileName, fileIDExists := block.Fields["fileId"]
if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString {
filePath := filepath.Join(block.WorkspaceID, block.RootID, fileName)
filePath := filepath.Join(block.BoardID, fileName)
err = a.filesBackend.RemoveFile(filePath)
if err != nil {
@ -225,31 +282,32 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
}
}
a.wsAdapter.BroadcastBlockDelete(c.WorkspaceID, blockID, block.ParentID)
a.metrics.IncrementBlocksDeleted(1)
go func() {
a.notifyBlockChanged(notify.Delete, c, block, block, modifiedBy)
}()
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
a.metrics.IncrementBlocksDeleted(1)
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
return nil
})
return nil
}
func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string) error {
blocks, err := a.store.GetBlockHistory(c, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
if err != nil {
return err
}
if len(blocks) == 0 {
// deleting non-existing block not considered an error
// undeleting non-existing block not considered an error
return nil
}
err = a.store.UndeleteBlock(c, blockID, modifiedBy)
err = a.store.UndeleteBlock(blockID, modifiedBy)
if err != nil {
return err
}
block, err := a.store.GetBlock(c, blockID)
block, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
@ -259,12 +317,19 @@ func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string
return nil
}
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block)
a.metrics.IncrementBlocksInserted(1)
go func() {
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
a.wsAdapter.BroadcastBlockChange(board.TeamID, *block)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(*block)
a.notifyBlockChanged(notify.Add, c, block, nil, modifiedBy)
}()
a.notifyBlockChanged(notify.Add, block, nil, modifiedBy)
return nil
})
return nil
}
@ -272,13 +337,17 @@ func (a *App) GetBlockCountsByType() (map[string]int64, error) {
return a.store.GetBlockCountsByType()
}
func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block *model.Block, oldBlock *model.Block, modifiedByID string) {
func (a *App) GetBlocksForBoard(boardID string) ([]model.Block, error) {
return a.store.GetBlocksForBoard(boardID)
}
func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) {
if a.notifications == nil {
return
}
// find card and board for the changed block.
board, card, err := a.store.GetBoardAndCard(c, block)
board, card, err := a.getBoardAndCard(block)
if err != nil {
a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err))
return
@ -286,7 +355,7 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block
evt := notify.BlockChangeEvent{
Action: action,
Workspace: c.WorkspaceID,
TeamID: board.TeamID,
Board: board,
Card: card,
BlockChanged: block,
@ -295,3 +364,35 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block
}
a.notifications.BlockChanged(evt)
}
const (
maxSearchDepth = 50
)
// getBoardAndCard returns the first parent of type `card` its board for the specified block.
// `board` and/or `card` may return nil without error if the block does not belong to a board or card.
func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) {
board, err = a.store.GetBoard(block.BoardID)
if err != nil {
return board, card, err
}
var count int // don't let invalid blocks hierarchy cause infinite loop.
iter := block
for {
count++
if card == nil && iter.Type == model.TypeCard {
card = iter
}
if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth {
break
}
iter, err = a.store.GetBlock(iter.ParentID)
if err != nil || iter == nil {
return board, card, err
}
}
return board, card, nil
}

View File

@ -3,10 +3,9 @@ package app
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/golang/mock/gomock"
st "github.com/mattermost/focalboard/server/services/store"
"github.com/stretchr/testify/require"
)
@ -18,47 +17,28 @@ func (be blockError) Error() string {
return be.msg
}
func TestGetParentID(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: "0",
}
t.Run("success query", func(t *testing.T) {
th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("test-parent-id", nil)
result, err := th.App.GetParentID(container, "test-id")
require.NoError(t, err)
require.Equal(t, "test-parent-id", result)
})
t.Run("fail query", func(t *testing.T) {
th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("", blockError{"block-not-found"})
_, err := th.App.GetParentID(container, "test-id")
require.Error(t, err)
require.ErrorIs(t, err, blockError{"block-not-found"})
})
}
func TestInsertBlock(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: "0",
}
t.Run("success scenerio", func(t *testing.T) {
block := model.Block{}
th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(nil)
err := th.App.InsertBlock(container, block, "user-id-1")
boardID := testBoardID
block := model.Block{BoardID: boardID}
board := &model.Board{ID: boardID}
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(nil)
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
err := th.App.InsertBlock(block, "user-id-1")
require.NoError(t, err)
})
t.Run("error scenerio", func(t *testing.T) {
block := model.Block{}
th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(blockError{"error"})
err := th.App.InsertBlock(container, block, "user-id-1")
boardID := testBoardID
block := model.Block{BoardID: boardID}
board := &model.Board{ID: boardID}
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(blockError{"error"})
err := th.App.InsertBlock(block, "user-id-1")
require.Error(t, err, "error")
})
}
@ -67,20 +47,17 @@ func TestPatchBlocks(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: "0",
}
t.Run("patchBlocks success scenerio", func(t *testing.T) {
blockPatches := model.BlockPatchBatch{}
th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
err := th.App.PatchBlocks(container, &blockPatches, "user-id-1")
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil)
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
require.NoError(t, err)
})
t.Run("patchBlocks error scenerio", func(t *testing.T) {
blockPatches := model.BlockPatchBatch{}
th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
err := th.App.PatchBlocks(container, &blockPatches, "user-id-1")
th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"})
err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1")
require.Error(t, err, "error")
})
}
@ -89,27 +66,32 @@ func TestDeleteBlock(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: "0",
}
t.Run("success scenerio", func(t *testing.T) {
boardID := testBoardID
board := &model.Board{ID: boardID}
block := model.Block{
ID: "block-id",
ID: "block-id",
BoardID: board.ID,
}
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
err := th.App.DeleteBlock(container, "block-id", "user-id-1")
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil)
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
err := th.App.DeleteBlock("block-id", "user-id-1")
require.NoError(t, err)
})
t.Run("error scenerio", func(t *testing.T) {
boardID := testBoardID
board := &model.Board{ID: boardID}
block := model.Block{
ID: "block-id",
ID: "block-id",
BoardID: board.ID,
}
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
err := th.App.DeleteBlock(container, "block-id", "user-id-1")
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil)
err := th.App.DeleteBlock("block-id", "user-id-1")
require.Error(t, err, "error")
})
}
@ -118,22 +100,22 @@ func TestUndeleteBlock(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: "0",
}
t.Run("success scenerio", func(t *testing.T) {
boardID := testBoardID
board := &model.Board{ID: boardID}
block := model.Block{
ID: "block-id",
ID: "block-id",
BoardID: board.ID,
}
th.Store.EXPECT().GetBlockHistory(
gomock.Eq(container),
gomock.Eq("block-id"),
gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}),
).Return([]model.Block{block}, nil)
th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
err := th.App.UndeleteBlock(container, "block-id", "user-id-1")
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil)
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
err := th.App.UndeleteBlock("block-id", "user-id-1")
require.NoError(t, err)
})
@ -142,13 +124,12 @@ func TestUndeleteBlock(t *testing.T) {
ID: "block-id",
}
th.Store.EXPECT().GetBlockHistory(
gomock.Eq(container),
gomock.Eq("block-id"),
gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}),
).Return([]model.Block{block}, nil)
th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil)
err := th.App.UndeleteBlock(container, "block-id", "user-id-1")
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
err := th.App.UndeleteBlock("block-id", "user-id-1")
require.Error(t, err, "error")
})
}

254
server/app/boards.go Normal file
View File

@ -0,0 +1,254 @@
package app
import (
"database/sql"
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
)
var (
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
ErrNewBoardCannotHaveID = errors.New("new board cannot have an ID")
)
func (a *App) GetBoard(boardID string) (*model.Board, error) {
board, err := a.store.GetBoard(boardID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return board, nil
}
func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) {
bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate)
if err != nil {
return nil, nil, err
}
go func() {
teamID := ""
for _, board := range bab.Boards {
teamID = board.TeamID
a.wsAdapter.BroadcastBoardChange(teamID, board)
}
for _, block := range bab.Blocks {
a.wsAdapter.BroadcastBlockChange(teamID, block)
}
for _, member := range members {
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
}
}()
return bab, members, err
}
func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
return a.store.GetBoardsForUserAndTeam(userID, teamID)
}
func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) {
return a.store.GetTemplateBoards(teamID)
}
func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) {
if board.ID != "" {
return nil, ErrNewBoardCannotHaveID
}
board.ID = utils.NewID(utils.IDTypeBoard)
var newBoard *model.Board
var member *model.BoardMember
var err error
if addMember {
newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID)
} else {
newBoard, err = a.store.InsertBoard(board, userID)
}
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard)
if addMember {
a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member)
}
}()
return newBoard, nil
}
func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) {
updatedBoard, err := a.store.PatchBoard(boardID, patch, userID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard)
}()
return updatedBoard, nil
}
func (a *App) DeleteBoard(boardID, userID string) error {
board, err := a.store.GetBoard(boardID)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
if err := a.store.DeleteBoard(boardID, userID); err != nil {
return err
}
go func() {
a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID)
}()
return nil
}
func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
return a.store.GetMembersForBoard(boardID)
}
func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
return a.store.GetMembersForUser(userID)
}
func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) {
board, err := a.store.GetBoard(member.BoardID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
if existingMembership != nil {
return existingMembership, nil
}
newMember, err := a.store.SaveMember(member)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
}()
return newMember, nil
}
func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) {
board, bErr := a.store.GetBoard(member.BoardID)
if errors.Is(bErr, sql.ErrNoRows) {
return nil, nil
}
if bErr != nil {
return nil, bErr
}
oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
// if we're updating an admin, we need to check that there is at
// least still another admin on the board
if oldMember.SchemeAdmin && !member.SchemeAdmin {
isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID)
if err2 != nil {
return nil, err2
}
if isLastAdmin {
return nil, ErrBoardMemberIsLastAdmin
}
}
newMember, err := a.store.SaveMember(member)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member)
}()
return newMember, nil
}
func (a *App) isLastAdmin(userID, boardID string) (bool, error) {
members, err := a.store.GetMembersForBoard(boardID)
if err != nil {
return false, err
}
for _, m := range members {
if m.SchemeAdmin && m.UserID != userID {
return false, nil
}
}
return true, nil
}
func (a *App) DeleteBoardMember(boardID, userID string) error {
board, bErr := a.store.GetBoard(boardID)
if errors.Is(bErr, sql.ErrNoRows) {
return nil
}
if bErr != nil {
return bErr
}
oldMember, err := a.store.GetMemberForBoard(boardID, userID)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
// if we're removing an admin, we need to check that there is at
// least still another admin on the board
if oldMember.SchemeAdmin {
isLastAdmin, err := a.isLastAdmin(userID, boardID)
if err != nil {
return err
}
if isLastAdmin {
return ErrBoardMemberIsLastAdmin
}
}
if err := a.store.DeleteMember(boardID, userID); err != nil {
return err
}
go func() {
a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID)
}()
return nil
}
func (a *App) SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error) {
return a.store.SearchBoardsForUserAndTeam(term, userID, teamID)
}

View File

@ -0,0 +1,126 @@
package app
import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) {
var newBab *model.BoardsAndBlocks
var members []*model.BoardMember
var err error
if addMember {
newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID)
} else {
newBab, err = a.store.CreateBoardsAndBlocks(bab, userID)
}
if err != nil {
return nil, err
}
// all new boards should belong to the same team
teamID := newBab.Boards[0].TeamID
// This can be synchronous because this action is not common
for _, board := range newBab.Boards {
a.wsAdapter.BroadcastBoardChange(teamID, board)
}
for _, block := range newBab.Blocks {
b := block
a.wsAdapter.BroadcastBlockChange(teamID, b)
a.metrics.IncrementBlocksInserted(1)
a.webhook.NotifyUpdate(b)
a.notifyBlockChanged(notify.Add, &b, nil, userID)
}
if addMember {
for _, member := range members {
a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member)
}
}
return newBab, nil
}
func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) {
oldBlocksMap := map[string]*model.Block{}
for _, blockID := range pbab.BlockIDs {
block, err := a.store.GetBlock(blockID)
if err != nil {
return nil, err
}
oldBlocksMap[blockID] = block
}
bab, err := a.store.PatchBoardsAndBlocks(pbab, userID)
if err != nil {
return nil, err
}
a.blockChangeNotifier.Enqueue(func() error {
teamID := bab.Boards[0].TeamID
for _, block := range bab.Blocks {
oldBlock, ok := oldBlocksMap[block.ID]
if !ok {
a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID))
continue
}
b := block
a.metrics.IncrementBlocksPatched(1)
a.wsAdapter.BroadcastBlockChange(teamID, b)
a.webhook.NotifyUpdate(b)
a.notifyBlockChanged(notify.Update, &b, oldBlock, userID)
}
for _, board := range bab.Boards {
a.wsAdapter.BroadcastBoardChange(board.TeamID, board)
}
return nil
})
return bab, nil
}
func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error {
firstBoard, err := a.store.GetBoard(dbab.Boards[0])
if err != nil {
return err
}
// we need the block entity to notify of the block changes, so we
// fetch and store the blocks first
blocks := []*model.Block{}
for _, blockID := range dbab.Blocks {
block, err := a.store.GetBlock(blockID)
if err != nil {
return err
}
blocks = append(blocks, block)
}
if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil {
return err
}
a.blockChangeNotifier.Enqueue(func() error {
for _, block := range blocks {
a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID)
a.metrics.IncrementBlocksDeleted(1)
a.notifyBlockChanged(notify.Update, block, block, userID)
}
for _, boardID := range dbab.Boards {
a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID)
}
return nil
})
return nil
}

103
server/app/category.go Normal file
View File

@ -0,0 +1,103 @@
package app
import (
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
)
var (
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
ErrorCategoryDeleted = errors.New("category is deleted")
)
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
category.Hydrate()
if err := category.IsValid(); err != nil {
return nil, err
}
if err := a.store.CreateCategory(*category); err != nil {
return nil, err
}
createdCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*createdCategory)
}()
return createdCategory, nil
}
func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) {
// verify if category belongs to the user
existingCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
if existingCategory.DeleteAt != 0 {
return nil, ErrorCategoryDeleted
}
if existingCategory.UserID != category.UserID {
return nil, ErrorCategoryPermissionDenied
}
category.UpdateAt = utils.GetMillis()
if err = category.IsValid(); err != nil {
return nil, err
}
if err = a.store.UpdateCategory(*category); err != nil {
return nil, err
}
updatedCategory, err := a.store.GetCategory(category.ID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*updatedCategory)
}()
return updatedCategory, nil
}
func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) {
existingCategory, err := a.store.GetCategory(categoryID)
if err != nil {
return nil, err
}
// category is already deleted. This avoids
// overriding the original deleted at timestamp
if existingCategory.DeleteAt != 0 {
return existingCategory, nil
}
// verify if category belongs to the user
if existingCategory.UserID != userID {
return nil, ErrorCategoryPermissionDenied
}
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
return nil, err
}
deletedCategory, err := a.store.GetCategory(categoryID)
if err != nil {
return nil, err
}
go func() {
a.wsAdapter.BroadcastCategoryChange(*deletedCategory)
}()
return deletedCategory, nil
}

View File

@ -0,0 +1,26 @@
package app
import "github.com/mattermost/focalboard/server/model"
func (a *App) GetUserCategoryBlocks(userID, teamID string) ([]model.CategoryBlocks, error) {
return a.store.GetUserCategoryBlocks(userID, teamID)
}
func (a *App) AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID string) error {
err := a.store.AddUpdateCategoryBlock(userID, categoryID, blockID)
if err != nil {
return err
}
go func() {
a.wsAdapter.BroadcastCategoryBlockChange(
teamID,
userID,
model.BlockCategoryWebsocketData{
BlockID: blockID,
CategoryID: categoryID,
})
}()
return nil
}

View File

@ -7,7 +7,6 @@ import (
"io"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/wiggin77/merror"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -18,10 +17,7 @@ var (
)
func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
container := store.Container{
WorkspaceID: opt.WorkspaceID,
}
boards, err := a.getBoardsForArchive(container, opt.BoardIDs)
boards, err := a.getBoardsForArchive(opt.BoardIDs)
if err != nil {
return err
}
@ -71,7 +67,7 @@ func (a *App) writeArchiveVersion(zw *zip.Writer) error {
}
// writeArchiveBoard writes a single board to the archive in a zip directory.
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.ExportArchiveOptions) error {
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
// create a directory per board
w, err := zw.Create(board.ID + "/board.jsonl")
if err != nil {
@ -79,18 +75,14 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.Exp
}
// write the board block first
if err = a.writeArchiveBlockLine(w, board); err != nil {
if err = a.writeArchiveBoardLine(w, board); err != nil {
return err
}
var files []string
container := store.Container{
WorkspaceID: opt.WorkspaceID,
}
// write the board's blocks
// TODO: paginate this
blocks, err := a.GetBlocksWithRootID(container, board.ID)
blocks, err := a.GetBlocksWithBoardID(board.ID)
if err != nil {
return err
}
@ -143,6 +135,32 @@ func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error {
return err
}
// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error {
b, err := json.Marshal(&board)
if err != nil {
return err
}
line := model.ArchiveLine{
Type: "board",
Data: b,
}
b, err = json.Marshal(&line)
if err != nil {
return err
}
_, err = w.Write(b)
if err != nil {
return err
}
// jsonl files need a newline
_, err = w.Write(newline)
return err
}
// writeArchiveFile writes a single file to the archive.
func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error {
dest, err := zw.Create(boardID + "/" + filename)
@ -150,12 +168,12 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
return err
}
src, err := a.GetFileReader(opt.WorkspaceID, boardID, filename)
src, err := a.GetFileReader(opt.TeamID, boardID, filename)
if err != nil {
// just log this; image file is missing but we'll still export an equivalent board
a.logger.Error("image file missing for export",
mlog.String("filename", filename),
mlog.String("workspace_id", opt.WorkspaceID),
mlog.String("team_id", opt.TeamID),
mlog.String("board_id", boardID),
)
return nil
@ -168,27 +186,25 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string,
// getBoardsForArchive fetches all the specified boards, or all boards in the workspace/team
// if `boardIDs` is empty.
func (a *App) getBoardsForArchive(container store.Container, boardIDs []string) ([]model.Block, error) {
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
if len(boardIDs) == 0 {
boards, err := a.GetBlocks(container, "", model.TypeBoard)
if err != nil {
return nil, fmt.Errorf("could not fetch all boards: %w", err)
}
return boards, nil
// TODO: implement this
// boards, err := a.GetAllBoards("", "board")
// if err != nil {
// return nil, fmt.Errorf("could not fetch all boards: %w", err)
// }
// return boards, nil
return []model.Board{}, nil
}
boards := make([]model.Block, 0, len(boardIDs))
boards := make([]model.Board, 0, len(boardIDs))
for _, id := range boardIDs {
b, err := a.GetBlockByID(container, id)
b, err := a.GetBoard(id)
if err != nil {
return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
}
if b.Type != model.TypeBoard {
return nil, fmt.Errorf("block %s is not a board: %w", b.ID, model.ErrInvalidBoardBlock)
}
boards = append(boards, *b)
}
return boards, nil

View File

@ -13,7 +13,7 @@ import (
"github.com/mattermost/mattermost-server/v6/shared/filestore"
)
func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) {
func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) {
// NOTE: File extension includes the dot
fileExtension := strings.ToLower(filepath.Ext(filename))
if fileExtension == ".jpeg" {
@ -21,7 +21,7 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
}
createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension)
filePath := filepath.Join(workspaceID, rootID, createdFilename)
filePath := filepath.Join(teamID, rootID, createdFilename)
_, appErr := a.filesBackend.WriteFile(reader, filePath)
if appErr != nil {
@ -31,14 +31,14 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (
return createdFilename, nil
}
func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
filePath := filepath.Join(workspaceID, rootID, filename)
func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) {
filePath := filepath.Join(teamID, rootID, filename)
exists, err := a.filesBackend.FileExists(filePath)
if err != nil {
return nil, err
}
// FIXUP: Check the deprecated old location
if workspaceID == "0" && !exists {
if teamID == "0" && !exists {
oldExists, err2 := a.filesBackend.FileExists(filename)
if err2 != nil {
return nil, err2

View File

@ -5,16 +5,17 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks"
"github.com/stretchr/testify/assert"
)
const (
testFileName = "temp-file-name"
testRootID = "test-root-id"
testFilePath = "1/test-root-id/temp-file-name"
testBoardID = "test-board-id"
testFilePath = "1/test-board-id/temp-file-name"
)
type TestError struct{}
@ -45,7 +46,7 @@ func TestGetFileReader(t *testing.T) {
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
actual, _ := th.App.GetFileReader("1", testRootID, testFileName)
actual, _ := th.App.GetFileReader("1", testBoardID, testFileName)
assert.Equal(t, mockedReadCloseSeek, actual)
})
@ -71,7 +72,7 @@ func TestGetFileReader(t *testing.T) {
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
actual, err := th.App.GetFileReader("1", testRootID, testFileName)
actual, err := th.App.GetFileReader("1", testBoardID, testFileName)
assert.Error(t, err, mockedError)
assert.Nil(t, actual)
})
@ -98,13 +99,13 @@ func TestGetFileReader(t *testing.T) {
mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc)
mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc)
actual, err := th.App.GetFileReader("1", testRootID, testFileName)
actual, err := th.App.GetFileReader("1", testBoardID, testFileName)
assert.Error(t, err, mockedError)
assert.Nil(t, actual)
})
t.Run("should move file from old filepath to new filepath, if file doesnot exists in new filepath and workspace id is 0", func(t *testing.T) {
filePath := "0/test-root-id/temp-file-name"
filePath := "0/test-board-id/temp-file-name"
workspaceid := "0"
mockedFileBackend := &mocks.FileBackend{}
th.App.filesBackend = mockedFileBackend
@ -134,12 +135,12 @@ func TestGetFileReader(t *testing.T) {
mockedFileBackend.On("MoveFile", testFileName, filePath).Return(moveFileFunc)
mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc)
actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName)
actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName)
assert.Equal(t, mockedReadCloseSeek, actual)
})
t.Run("should return file reader, if file doesnot exists in new filepath and old file path", func(t *testing.T) {
filePath := "0/test-root-id/temp-file-name"
filePath := "0/test-board-id/temp-file-name"
fileName := testFileName
workspaceid := "0"
mockedFileBackend := &mocks.FileBackend{}
@ -170,7 +171,7 @@ func TestGetFileReader(t *testing.T) {
mockedFileBackend.On("MoveFile", fileName, filePath).Return(moveFileFunc)
mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc)
actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName)
actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName)
assert.Equal(t, mockedReadCloseSeek, actual)
})
}
@ -186,7 +187,7 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, "/")
assert.Equal(t, "1", paths[0])
assert.Equal(t, testRootID, paths[1])
assert.Equal(t, testBoardID, paths[1])
fileName = paths[2]
return int64(10)
}
@ -196,7 +197,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testRootID, fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName)
assert.Equal(t, fileName, actual)
assert.Nil(t, err)
})
@ -209,7 +210,7 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, "/")
assert.Equal(t, "1", paths[0])
assert.Equal(t, "test-root-id", paths[1])
assert.Equal(t, "test-board-id", paths[1])
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
return int64(10)
}
@ -219,7 +220,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
assert.Nil(t, err)
assert.NotNil(t, actual)
})
@ -233,7 +234,7 @@ func TestSaveFile(t *testing.T) {
writeFileFunc := func(reader io.Reader, path string) int64 {
paths := strings.Split(path, "/")
assert.Equal(t, "1", paths[0])
assert.Equal(t, "test-root-id", paths[1])
assert.Equal(t, "test-board-id", paths[1])
assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1])
return int64(10)
}
@ -243,7 +244,7 @@ func TestSaveFile(t *testing.T) {
}
mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName)
actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName)
assert.Equal(t, "", actual)
assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error())
})

View File

@ -29,11 +29,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
defer ctrl.Finish()
cfg := config.Configuration{}
store := mockstore.NewMockStore(ctrl)
auth := auth.New(&cfg, store)
auth := auth.New(&cfg, store, nil)
logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)
sessionToken := "TESTTOKEN"
wsserver := ws.NewServer(auth, sessionToken, false, logger)
wsserver := ws.NewServer(auth, sessionToken, false, logger, store)
webhook := webhook.NewClient(&cfg, logger)
metricsService := metrics.NewMetrics(metrics.InstanceInfo{})
@ -49,6 +48,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
app2 := New(&cfg, wsserver, appServices)
tearDown := func() {
app2.Shutdown()
if logger != nil {
_ = logger.Shutdown()
}

View File

@ -14,7 +14,6 @@ import (
"github.com/krolaw/zipstream"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -84,7 +83,7 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
continue
}
// save file with original filename so it matches name in image block.
filePath := filepath.Join(opt.WorkspaceID, boardID, filename)
filePath := filepath.Join(opt.TeamID, boardID, filename)
_, err := a.filesBackend.WriteFile(zr, filePath)
if err != nil {
return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err)
@ -103,7 +102,10 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error {
func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) {
// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks.
// We don't want to load the whole file in memory, even though it's a single board.
blocks := make([]model.Block, 0, 10)
boardsAndBlocks := &model.BoardsAndBlocks{
Blocks: make([]model.Block, 0, 10),
Boards: make([]*model.Board, 0, 10),
}
lineReader := bufio.NewReader(r)
userID := opt.ModifiedBy
@ -137,7 +139,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
}
block.ModifiedBy = userID
block.UpdateAt = now
blocks = append(blocks, block)
boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block)
case "board":
var board model.Board
if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil {
return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2)
}
board.ModifiedBy = userID
board.UpdateAt = now
board.TeamID = opt.TeamID
boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board)
default:
return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type)
}
@ -154,36 +165,33 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str
}
modInfoCache := make(map[string]interface{})
modBlocks := make([]model.Block, 0, len(blocks))
for _, block := range blocks {
b := block
if opt.BlockModifier != nil && !opt.BlockModifier(&b, modInfoCache) {
modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards))
for _, board := range boardsAndBlocks.Boards {
b := *board
if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) {
a.logger.Debug("skipping insert block per block modifier",
mlog.String("blockID", block.ID),
mlog.String("block_type", block.Type.String()),
mlog.String("blockID", board.ID),
)
continue
}
modBlocks = append(modBlocks, b)
}
blocks = model.GenerateBlockIDs(modBlocks, a.logger)
container := store.Container{
WorkspaceID: opt.WorkspaceID,
modBoards = append(modBoards, &b)
}
boardsAndBlocks.Boards = modBoards
var err error
blocks, err = a.InsertBlocks(container, blocks, opt.ModifiedBy, false)
boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger)
if err != nil {
return "", fmt.Errorf("error inserting archive blocks: %w", err)
}
boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false)
if err != nil {
return "", fmt.Errorf("error inserting archive blocks: %w", err)
}
// find new board id
for _, block := range blocks {
if block.Type == model.TypeBoard {
return block.ID, nil
}
for _, board := range boardsAndBlocks.Boards {
return board.ID, nil
}
return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock)
}

View File

@ -1,6 +1,10 @@
package app
import "github.com/mattermost/mattermost-server/v6/shared/mlog"
import (
"context"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
// initialize is called when the App is first created.
func (a *App) initialize(skipTemplateInit bool) {
@ -10,3 +14,13 @@ func (a *App) initialize(skipTemplateInit bool) {
}
}
}
func (a *App) Shutdown() {
if a.blockChangeNotifier != nil {
ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout)
defer cancel()
if !a.blockChangeNotifier.Shutdown(ctx) {
a.logger.Warn("blockChangeNotifier shutdown timed out")
}
}
}

View File

@ -4,7 +4,6 @@ import (
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
)
const (
@ -21,17 +20,12 @@ const (
var (
errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks")
errCannotCreateBoard = errors.New("new board wasn't created")
)
func (a *App) PrepareOnboardingTour(userID string) (string, string, error) {
// create a private workspace for the user
workspaceID, err := a.store.CreatePrivateWorkspace(userID)
if err != nil {
return "", "", err
}
func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) {
// copy the welcome board into this workspace
boardID, err := a.createWelcomeBoard(userID, workspaceID)
boardID, err := a.createWelcomeBoard(userID, teamID)
if err != nil {
return "", "", err
}
@ -48,18 +42,18 @@ func (a *App) PrepareOnboardingTour(userID string) (string, string, error) {
return "", "", err
}
return workspaceID, boardID, nil
return teamID, boardID, nil
}
func (a *App) getOnboardingBoardID() (string, error) {
blocks, err := a.store.GetDefaultTemplateBlocks()
boards, err := a.store.GetTemplateBoards(globalTeamID)
if err != nil {
return "", err
}
var onboardingBoardID string
for _, block := range blocks {
if block.Type == model.TypeBoard && block.Title == WelcomeBoardTitle {
for _, block := range boards {
if block.Title == WelcomeBoardTitle {
onboardingBoardID = block.ID
break
}
@ -72,42 +66,20 @@ func (a *App) getOnboardingBoardID() (string, error) {
return onboardingBoardID, nil
}
func (a *App) createWelcomeBoard(userID, workspaceID string) (string, error) {
func (a *App) createWelcomeBoard(userID, teamID string) (string, error) {
onboardingBoardID, err := a.getOnboardingBoardID()
if err != nil {
return "", err
}
blocks, err := a.GetSubTree(store.Container{WorkspaceID: "0"}, onboardingBoardID, 3)
bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false)
if err != nil {
return "", err
}
blocks = model.GenerateBlockIDs(blocks, a.logger)
if errUpdateFileIDs := a.CopyCardFiles(onboardingBoardID, workspaceID, blocks); errUpdateFileIDs != nil {
return "", errUpdateFileIDs
if len(bab.Boards) != 1 {
return "", errCannotCreateBoard
}
// we're copying from a global template, so we need to set the
// `isTemplate` flag to false on the board
var welcomeBoardID string
for i := range blocks {
if blocks[i].Type == model.TypeBoard {
blocks[i].Fields["isTemplate"] = false
if blocks[i].Title == WelcomeBoardTitle {
welcomeBoardID = blocks[i].ID
break
}
}
}
model.StampModificationMetadata(userID, blocks, nil)
_, err = a.InsertBlocks(store.Container{WorkspaceID: workspaceID}, blocks, userID, false)
if err != nil {
return "", err
}
return welcomeBoardID, nil
return bab.Boards[0].ID, nil
}

View File

@ -3,44 +3,32 @@ package app
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/stretchr/testify/assert"
)
const (
testTeamID = "team_id"
)
func TestPrepareOnboardingTour(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
t.Run("base case", func(t *testing.T) {
welcomeBoard := model.Block{
ID: "block_id_1",
Type: model.TypeBoard,
Title: "Welcome to Boards!",
Fields: map[string]interface{}{
"isTemplate": true,
},
teamID := testTeamID
userID := "user_id_1"
welcomeBoard := model.Board{
ID: "board_id_1",
Title: "Welcome to Boards!",
TeamID: "0",
IsTemplate: true,
}
blocks := []model.Block{welcomeBoard}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetSubTree3(
store.Container{WorkspaceID: "0"},
"block_id_1",
gomock.Any(),
).Return([]model.Block{welcomeBoard}, nil)
th.Store.EXPECT().InsertBlock(
store.Container{WorkspaceID: "workspace_id_1"},
gomock.Any(),
"user_id_1",
).Return(nil)
th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil)
th.Store.EXPECT().CreatePrivateWorkspace("user_id_1").Return("workspace_id_1", nil)
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
nil, nil)
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
userPropPatch := model.UserPropPatch{
UpdatedFields: map[string]string{
@ -50,11 +38,11 @@ func TestPrepareOnboardingTour(t *testing.T) {
},
}
th.Store.EXPECT().PatchUserProps("user_id_1", userPropPatch).Return(nil)
th.Store.EXPECT().PatchUserProps(userID, userPropPatch).Return(nil)
workspaceID, boardID, err := th.App.PrepareOnboardingTour("user_id_1")
teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID)
assert.NoError(t, err)
assert.Equal(t, "workspace_id_1", workspaceID)
assert.Equal(t, testTeamID, teamID)
assert.NotEmpty(t, boardID)
})
}
@ -64,88 +52,41 @@ func TestCreateWelcomeBoard(t *testing.T) {
defer tearDown()
t.Run("base case", func(t *testing.T) {
welcomeBoard := model.Block{
ID: "block_id_1",
Type: model.TypeBoard,
Title: "Welcome to Boards!",
Fields: map[string]interface{}{
"isTemplate": true,
},
teamID := testTeamID
userID := "user_id_1"
welcomeBoard := model.Board{
ID: "board_id_1",
Title: "Welcome to Boards!",
TeamID: "0",
IsTemplate: true,
}
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).
Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil)
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
blocks := []model.Block{welcomeBoard}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetSubTree3(
store.Container{WorkspaceID: "0"},
"block_id_1",
gomock.Any(),
).Return([]model.Block{welcomeBoard}, nil)
th.Store.EXPECT().InsertBlock(
store.Container{WorkspaceID: "workspace_id_1"},
gomock.Any(),
"user_id_1",
).Return(nil)
th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
boardID, err := th.App.createWelcomeBoard(userID, teamID)
assert.Nil(t, err)
assert.NotEmpty(t, boardID)
})
t.Run("template doesn't contain a board", func(t *testing.T) {
welcomeBoard := model.Block{
ID: "block_id_1",
Type: model.TypeComment,
Title: "Welcome to Boards!",
}
blocks := []model.Block{welcomeBoard}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetSubTree3(
store.Container{WorkspaceID: "0"},
"buixxjic3xjfkieees4iafdrznc",
gomock.Any(),
).Return([]model.Block{welcomeBoard}, nil)
th.Store.EXPECT().InsertBlock(
store.Container{WorkspaceID: "workspace_id_1"},
gomock.Any(),
"user_id_1",
).Return(nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
teamID := testTeamID
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", teamID)
assert.Error(t, err)
assert.Empty(t, boardID)
})
t.Run("template doesn't contain the welcome board", func(t *testing.T) {
welcomeBoard := model.Block{
ID: "block_id_1",
Type: model.TypeBoard,
Title: "Jean luc Picard",
Fields: map[string]interface{}{
"isTemplate": true,
},
teamID := testTeamID
welcomeBoard := model.Board{
ID: "board_id_1",
Title: "Other template",
TeamID: teamID,
IsTemplate: true,
}
blocks := []model.Block{welcomeBoard}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetSubTree3(
store.Container{WorkspaceID: "0"},
"buixxjic3xjfkieees4iafdrznc",
gomock.Any(),
).Return([]model.Block{welcomeBoard}, nil)
th.Store.EXPECT().InsertBlock(
store.Container{WorkspaceID: "workspace_id_1"},
gomock.Any(),
"user_id_1",
).Return(nil)
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1")
assert.Error(t, err)
assert.Empty(t, boardID)
@ -157,24 +98,13 @@ func TestGetOnboardingBoardID(t *testing.T) {
defer tearDown()
t.Run("base case", func(t *testing.T) {
board := model.Block{
ID: "board_id_1",
Type: model.TypeBoard,
Title: "Welcome to Boards!",
welcomeBoard := model.Board{
ID: "board_id_1",
Title: "Welcome to Boards!",
TeamID: "0",
IsTemplate: true,
}
card := model.Block{
ID: "card_id_1",
Type: model.TypeCard,
ParentID: board.ID,
}
blocks := []model.Block{
board,
card,
}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.NoError(t, err)
@ -182,9 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) {
})
t.Run("no blocks found", func(t *testing.T) {
blocks := []model.Block{}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.Error(t, err)
@ -192,24 +120,13 @@ func TestGetOnboardingBoardID(t *testing.T) {
})
t.Run("onboarding board doesn't exists", func(t *testing.T) {
board := model.Block{
ID: "board_id_1",
Type: model.TypeBoard,
Title: "Some board title",
welcomeBoard := model.Board{
ID: "board_id_1",
Title: "Other template",
TeamID: "0",
IsTemplate: true,
}
card := model.Block{
ID: "card_id_1",
Type: model.TypeCard,
ParentID: board.ID,
}
blocks := []model.Block{
board,
card,
}
th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil)
th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil)
onboardingBoardID, err := th.App.getOnboardingBoardID()
assert.Error(t, err)

View File

@ -5,11 +5,10 @@ import (
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
)
func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, error) {
sharing, err := a.store.GetSharing(c, rootID)
func (a *App) GetSharing(boardID string) (*model.Sharing, error) {
sharing, err := a.store.GetSharing(boardID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
@ -19,6 +18,6 @@ func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, erro
return sharing, nil
}
func (a *App) UpsertSharing(c store.Container, sharing model.Sharing) error {
return a.store.UpsertSharing(c, sharing)
func (a *App) UpsertSharing(sharing model.Sharing) error {
return a.store.UpsertSharing(sharing)
}

View File

@ -4,9 +4,7 @@ import (
"database/sql"
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
st "github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
@ -16,10 +14,6 @@ func TestGetSharing(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
}
t.Run("should get a sharing successfully", func(t *testing.T) {
want := &model.Sharing{
ID: utils.NewID(utils.IDTypeBlock),
@ -28,9 +22,9 @@ func TestGetSharing(t *testing.T) {
ModifiedBy: "otherid",
UpdateAt: utils.GetMillis(),
}
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(want, nil)
th.Store.EXPECT().GetSharing("test-id").Return(want, nil)
result, err := th.App.GetSharing(container, "test-id")
result, err := th.App.GetSharing("test-id")
require.NoError(t, err)
require.Equal(t, result, want)
@ -38,11 +32,11 @@ func TestGetSharing(t *testing.T) {
})
t.Run("should fail to get a sharing", func(t *testing.T) {
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(
th.Store.EXPECT().GetSharing("test-id").Return(
nil,
errors.New("sharing not found"),
)
result, err := th.App.GetSharing(container, "test-id")
result, err := th.App.GetSharing("test-id")
require.Nil(t, result)
require.Error(t, err)
@ -50,11 +44,11 @@ func TestGetSharing(t *testing.T) {
})
t.Run("should return a tuple of nil", func(t *testing.T) {
th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(
th.Store.EXPECT().GetSharing("test-id").Return(
nil,
sql.ErrNoRows,
)
result, err := th.App.GetSharing(container, "test-id")
result, err := th.App.GetSharing("test-id")
require.Nil(t, result)
require.NoError(t, err)
@ -65,9 +59,6 @@ func TestUpsertSharing(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
container := st.Container{
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
}
sharing := model.Sharing{
ID: utils.NewID(utils.IDTypeBlock),
Enabled: true,
@ -77,15 +68,15 @@ func TestUpsertSharing(t *testing.T) {
}
t.Run("should success to upsert sharing", func(t *testing.T) {
th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(nil)
err := th.App.UpsertSharing(container, sharing)
th.Store.EXPECT().UpsertSharing(sharing).Return(nil)
err := th.App.UpsertSharing(sharing)
require.NoError(t, err)
})
t.Run("should fail to upsert a sharing", func(t *testing.T) {
th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(errors.New("sharing not found"))
err := th.App.UpsertSharing(container, sharing)
th.Store.EXPECT().UpsertSharing(sharing).Return(errors.New("sharing not found"))
err := th.App.UpsertSharing(sharing)
require.Error(t, err)
require.Equal(t, "sharing not found", err.Error())

View File

@ -2,41 +2,40 @@ package app
import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
)
func (a *App) CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) {
sub, err := a.store.CreateSubscription(c, sub)
func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) {
sub, err := a.store.CreateSubscription(sub)
if err != nil {
return nil, err
}
a.notifySubscriptionChanged(c, sub)
a.notifySubscriptionChanged(sub)
return sub, nil
}
func (a *App) DeleteSubscription(c store.Container, blockID string, subscriberID string) (*model.Subscription, error) {
sub, err := a.store.GetSubscription(c, blockID, subscriberID)
func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) {
sub, err := a.store.GetSubscription(blockID, subscriberID)
if err != nil {
return nil, err
}
if err := a.store.DeleteSubscription(c, blockID, subscriberID); err != nil {
if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil {
return nil, err
}
sub.DeleteAt = utils.GetMillis()
a.notifySubscriptionChanged(c, sub)
a.notifySubscriptionChanged(sub)
return sub, nil
}
func (a *App) GetSubscriptions(c store.Container, subscriberID string) ([]*model.Subscription, error) {
return a.store.GetSubscriptions(c, subscriberID)
func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) {
return a.store.GetSubscriptions(subscriberID)
}
func (a *App) notifySubscriptionChanged(c store.Container, subscription *model.Subscription) {
func (a *App) notifySubscriptionChanged(subscription *model.Subscription) {
if a.notifications == nil {
return
}
a.notifications.BroadcastSubscriptionChange(c.WorkspaceID, subscription)
a.notifications.BroadcastSubscriptionChange(subscription)
}

68
server/app/teams.go Normal file
View File

@ -0,0 +1,68 @@
package app
import (
"database/sql"
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (a *App) GetRootTeam() (*model.Team, error) {
teamID := "0"
team, _ := a.store.GetTeam(teamID)
if team == nil {
team = &model.Team{
ID: teamID,
SignupToken: utils.NewID(utils.IDTypeToken),
}
err := a.store.UpsertTeamSignupToken(*team)
if err != nil {
a.logger.Error("Unable to initialize team", mlog.Err(err))
return nil, err
}
team, err = a.store.GetTeam(teamID)
if err != nil {
a.logger.Error("Unable to get initialized team", mlog.Err(err))
return nil, err
}
a.logger.Info("initialized team")
}
return team, nil
}
func (a *App) GetTeam(id string) (*model.Team, error) {
team, err := a.store.GetTeam(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return team, nil
}
func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) {
return a.store.GetTeamsForUser(userID)
}
func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool {
return a.auth.DoesUserHaveTeamAccess(userID, teamID)
}
func (a *App) UpsertTeamSettings(team model.Team) error {
return a.store.UpsertTeamSettings(team)
}
func (a *App) UpsertTeamSignupToken(team model.Team) error {
return a.store.UpsertTeamSignupToken(team)
}
func (a *App) GetTeamCount() (int64, error) {
return a.store.GetTeamCount()
}

152
server/app/teams_test.go Normal file
View File

@ -0,0 +1,152 @@
package app
import (
"database/sql"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errInvalidTeam = errors.New("invalid team id")
var mockTeam = &model.Team{
ID: "mock-team-id",
Title: "MockTeam",
}
var errUpsertSignupToken = errors.New("upsert error")
func TestGetRootTeam(t *testing.T) {
var newRootTeam = &model.Team{
ID: "0",
Title: "NewRootTeam",
}
testCases := []struct {
title string
teamToReturnBeforeUpsert *model.Team
teamToReturnAfterUpsert *model.Team
isError bool
}{
{
"Success, Return new root team, when root team returned by mockstore is nil",
nil,
newRootTeam,
false,
},
{
"Success, Return existing root team, when root team returned by mockstore is notnil",
newRootTeam,
nil,
false,
},
{
"Fail, Return nil, when root team returned by mockstore is nil, and upsert new root team fails",
nil,
nil,
true,
},
}
for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnBeforeUpsert, nil)
th.Store.EXPECT().UpsertTeamSignupToken(gomock.Any()).DoAndReturn(
func(arg0 model.Team) error {
if tc.isError {
return errUpsertSignupToken
}
th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnAfterUpsert, nil)
return nil
})
rootTeam, err := th.App.GetRootTeam()
if tc.isError {
require.Error(t, err)
} else {
assert.NotNil(t, rootTeam.ID)
assert.NotNil(t, rootTeam.SignupToken)
assert.Equal(t, "", rootTeam.ModifiedBy)
assert.Equal(t, int64(0), rootTeam.UpdateAt)
assert.Equal(t, "NewRootTeam", rootTeam.Title)
require.NoError(t, err)
require.NotNil(t, rootTeam)
}
})
}
}
func TestGetTeam(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
testCases := []struct {
title string
teamID string
isError bool
}{
{
"Success, Return new root team, when team returned by mockstore is not nil",
"mock-team-id",
false,
},
{
"Success, Return nil, when get team returns an sql error",
"team-not-available-id",
false,
},
{
"Fail, Return nil, when get team by mockstore returns an error",
"invalid-team-id",
true,
},
}
th.Store.EXPECT().GetTeam("mock-team-id").Return(mockTeam, nil)
th.Store.EXPECT().GetTeam("invalid-team-id").Return(nil, errInvalidTeam)
th.Store.EXPECT().GetTeam("team-not-available-id").Return(nil, sql.ErrNoRows)
for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
t.Log(tc.title)
team, err := th.App.GetTeam(tc.teamID)
if tc.isError {
require.Error(t, err)
} else if tc.teamID != "team-not-available-id" {
assert.NotNil(t, team.ID)
assert.NotNil(t, team.SignupToken)
assert.Equal(t, "mock-team-id", team.ID)
assert.Equal(t, "", team.ModifiedBy)
assert.Equal(t, int64(0), team.UpdateAt)
assert.Equal(t, "MockTeam", team.Title)
require.NoError(t, err)
require.NotNil(t, team)
}
})
}
}
func TestTeamOperations(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().UpsertTeamSettings(*mockTeam).Return(nil)
th.Store.EXPECT().UpsertTeamSignupToken(*mockTeam).Return(nil)
th.Store.EXPECT().GetTeamCount().Return(int64(10), nil)
errUpsertTeamSettings := th.App.UpsertTeamSettings(*mockTeam)
assert.NoError(t, errUpsertTeamSettings)
errUpsertTeamSignupToken := th.App.UpsertTeamSignupToken(*mockTeam)
assert.NoError(t, errUpsertTeamSignupToken)
count, errGetTeamCount := th.App.GetTeamCount()
assert.NoError(t, errGetTeamCount)
assert.Equal(t, int64(10), count)
}

Binary file not shown.

View File

@ -14,21 +14,26 @@ import (
const (
defaultTemplateVersion = 2
globalTeamID = "0"
)
//go:embed templates.boardarchive
var defTemplates []byte
// initializeTemplates imports default templates if the blocks table is empty.
func (a *App) InitTemplates() error {
return a.initializeTemplates()
}
// initializeTemplates imports default templates if the boards table is empty.
func (a *App) initializeTemplates() error {
blocks, err := a.store.GetDefaultTemplateBlocks()
boards, err := a.store.GetTemplateBoards(globalTeamID)
if err != nil {
return fmt.Errorf("cannot initialize templates: %w", err)
}
a.logger.Debug("Fetched template blocks", mlog.Int("count", len(blocks)))
a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards)))
isNeeded, reason := a.isInitializationNeeded(blocks)
isNeeded, reason := a.isInitializationNeeded(boards)
if !isNeeded {
a.logger.Debug("Template import not needed, skipping")
return nil
@ -36,67 +41,55 @@ func (a *App) initializeTemplates() error {
a.logger.Debug("Importing new default templates", mlog.String("reason", reason))
if err := a.store.RemoveDefaultTemplates(blocks); err != nil {
return fmt.Errorf("cannot remove old templates: %w", err)
// Remove in case of newer Templates
if err = a.store.RemoveDefaultTemplates(boards); err != nil {
return fmt.Errorf("cannot remove old template boards: %w", err)
}
r := bytes.NewReader(defTemplates)
opt := model.ImportArchiveOptions{
WorkspaceID: "0",
TeamID: globalTeamID,
ModifiedBy: "system",
BlockModifier: fixTemplateBlock,
BoardModifier: fixTemplateBoard,
}
return a.ImportArchive(r, opt)
if err = a.ImportArchive(r, opt); err != nil {
return fmt.Errorf("cannot initialize global templates for team %s: %w", globalTeamID, err)
}
return nil
}
// isInitializationNeeded returns true if the blocks table contains no default templates,
// or contains at least one default template with an old version number.
func (a *App) isInitializationNeeded(blocks []model.Block) (bool, string) {
if len(blocks) == 0 {
func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) {
if len(boards) == 0 {
return true, "no default templates found"
}
// look for any template blocks with the wrong version number (or no version #).
for _, block := range blocks {
v, ok := block.Fields["templateVer"]
if !ok {
return true, "block missing templateVer"
// look for any built-in template boards with the wrong version number (or no version #).
for _, board := range boards {
// if not built-in board...skip
if board.CreatedBy != "system" {
continue
}
version, ok := v.(float64)
if !ok {
return true, "templateVer NaN"
}
if version < defaultTemplateVersion {
return true, "templateVer too old"
if board.TemplateVersion < defaultTemplateVersion {
return true, "template_version too old"
}
}
return false, ""
}
// fixTemplateBlock fixes a block to be inserted as part of a template.
func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool {
// cache contains ids of skipped blocks. Ensure their children are skipped as well.
if _, ok := cache[block.ParentID]; ok {
cache[block.ID] = struct{}{}
return false
}
// fixTemplateBoard fixes a board to be inserted as part of a template.
func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool {
// filter out template blocks; we only want the non-template
// blocks which we will turn into default template blocks.
if b, ok := block.Fields["isTemplate"]; ok {
if val, ok := b.(bool); ok && val {
cache[block.ID] = struct{}{}
return false
}
if board.IsTemplate {
cache[board.ID] = struct{}{}
}
// remove '(NEW)' from title & force template flag
if block.Type == model.TypeBoard {
block.Title = strings.ReplaceAll(block.Title, "(NEW)", "")
block.Fields["isTemplate"] = true
block.Fields["templateVer"] = defaultTemplateVersion
}
board.Title = strings.ReplaceAll(board.Title, "(NEW)", "")
board.IsTemplate = true
board.TemplateVersion = defaultTemplateVersion
return true
}

View File

@ -2,8 +2,12 @@ package app
import "github.com/mattermost/focalboard/server/model"
func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) {
return a.store.GetUsersByWorkspace(workspaceID)
func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) {
return a.store.GetUsersByTeam(teamID)
}
func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) {
return a.store.SearchUsersByTeam(teamID, searchQuery)
}
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {

View File

@ -1,67 +0,0 @@
package app
import (
"database/sql"
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func (a *App) GetRootWorkspace() (*model.Workspace, error) {
workspaceID := "0"
workspace, _ := a.store.GetWorkspace(workspaceID)
if workspace == nil {
workspace = &model.Workspace{
ID: workspaceID,
SignupToken: utils.NewID(utils.IDTypeToken),
}
err := a.store.UpsertWorkspaceSignupToken(*workspace)
if err != nil {
a.logger.Error("Unable to initialize workspace", mlog.Err(err))
return nil, err
}
workspace, err = a.store.GetWorkspace(workspaceID)
if err != nil {
a.logger.Error("Unable to get initialized workspace", mlog.Err(err))
return nil, err
}
a.logger.Info("initialized workspace")
}
return workspace, nil
}
func (a *App) GetWorkspace(id string) (*model.Workspace, error) {
workspace, err := a.store.GetWorkspace(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return workspace, nil
}
func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID)
}
func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error {
return a.store.UpsertWorkspaceSettings(workspace)
}
func (a *App) UpsertWorkspaceSignupToken(workspace model.Workspace) error {
return a.store.UpsertWorkspaceSignupToken(workspace)
}
func (a *App) GetWorkspaceCount() (int64, error) {
return a.store.GetWorkspaceCount()
}
func (a *App) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) {
return a.store.GetUserWorkspaces(userID)
}

View File

@ -1,165 +0,0 @@
package app
import (
"database/sql"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errInvalidWorkspace = errors.New("invalid workspace id")
var mockWorkspace = &model.Workspace{
ID: "mock-workspace-id",
Title: "MockWorkspace",
}
var mockUserWorkspaces = []model.UserWorkspace{
{
ID: "mock-user-workspace-id",
Title: "MockUserWorkspace",
},
}
var errUpsertSignupToken = errors.New("upsert error")
func TestGetRootWorkspace(t *testing.T) {
var newRootWorkspace = &model.Workspace{
ID: "0",
Title: "NewRootWorkspace",
}
testCases := []struct {
title string
workSpaceToReturnBeforeUpsert *model.Workspace
workSpaceToReturnAfterUpsert *model.Workspace
isError bool
}{
{
"Success, Return new root workspace, when root workspace returned by mockstore is nil",
nil,
newRootWorkspace,
false,
},
{
"Success, Return existing root workspace, when root workspace returned by mockstore is notnil",
newRootWorkspace,
nil,
false,
},
{
"Fail, Return nil, when root workspace returned by mockstore is nil, and upsert new root workspace fails",
nil,
nil,
true,
},
}
for _, eachTestacase := range testCases {
t.Run(eachTestacase.title, func(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
t.Log(eachTestacase.title)
th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnBeforeUpsert, nil)
th.Store.EXPECT().UpsertWorkspaceSignupToken(gomock.Any()).DoAndReturn(
func(arg0 model.Workspace) error {
if eachTestacase.isError {
return errUpsertSignupToken
}
th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnAfterUpsert, nil)
return nil
})
rootWorkSpace, err := th.App.GetRootWorkspace()
if eachTestacase.isError {
require.Error(t, err)
} else {
assert.NotNil(t, rootWorkSpace.ID)
assert.NotNil(t, rootWorkSpace.SignupToken)
assert.Equal(t, "", rootWorkSpace.ModifiedBy)
assert.Equal(t, int64(0), rootWorkSpace.UpdateAt)
assert.Equal(t, "NewRootWorkspace", rootWorkSpace.Title)
require.NoError(t, err)
require.NotNil(t, rootWorkSpace)
}
})
}
}
func TestGetWorkspace(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
testCases := []struct {
title string
workspaceID string
isError bool
}{
{
"Success, Return new root workspace, when workspace returned by mockstore is not nil",
"mock-workspace-id",
false,
},
{
"Success, Return nil, when get workspace returns an sql error",
"workspace-not-available-id",
false,
},
{
"Fail, Return nil, when get workspace by mockstore retruns an error",
"invalid-workspace-id",
true,
},
}
th.Store.EXPECT().GetWorkspace("mock-workspace-id").Return(mockWorkspace, nil)
th.Store.EXPECT().GetWorkspace("invalid-workspace-id").Return(nil, errInvalidWorkspace)
th.Store.EXPECT().GetWorkspace("workspace-not-available-id").Return(nil, sql.ErrNoRows)
for _, eachTestacase := range testCases {
t.Run(eachTestacase.title, func(t *testing.T) {
t.Log(eachTestacase.title)
workSpace, err := th.App.GetWorkspace(eachTestacase.workspaceID)
if eachTestacase.isError {
require.Error(t, err)
} else if eachTestacase.workspaceID != "workspace-not-available-id" {
assert.NotNil(t, workSpace.ID)
assert.NotNil(t, workSpace.SignupToken)
assert.Equal(t, "mock-workspace-id", workSpace.ID)
assert.Equal(t, "", workSpace.ModifiedBy)
assert.Equal(t, int64(0), workSpace.UpdateAt)
assert.Equal(t, "MockWorkspace", workSpace.Title)
require.NoError(t, err)
require.NotNil(t, workSpace)
}
})
}
}
func TestWorkspaceOperations(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().UpsertWorkspaceSettings(*mockWorkspace).Return(nil)
th.Store.EXPECT().UpsertWorkspaceSignupToken(*mockWorkspace).Return(nil)
th.Store.EXPECT().GetWorkspaceCount().Return(int64(10), nil)
th.Store.EXPECT().GetUserWorkspaces("mock-user-id").Return(mockUserWorkspaces, nil)
errUpsertWorkspaceSettings := th.App.UpsertWorkspaceSettings(*mockWorkspace)
assert.NoError(t, errUpsertWorkspaceSettings)
errUpsertWorkspaceSignupToken := th.App.UpsertWorkspaceSignupToken(*mockWorkspace)
assert.NoError(t, errUpsertWorkspaceSignupToken)
count, errGetWorkspaceCount := th.App.GetWorkspaceCount()
assert.NoError(t, errGetWorkspaceCount)
assert.Equal(t, int64(10), count)
userWorkSpace, errGetUserWorkSpace := th.App.GetUserWorkspaces("mock-user-id")
assert.NoError(t, errGetUserWorkSpace)
assert.NotNil(t, userWorkSpace)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/pkg/errors"
@ -13,19 +14,20 @@ import (
type AuthInterface interface {
GetSession(token string) (*model.Session, error)
IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error)
DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool
IsValidReadToken(boardID string, readToken string) (bool, error)
DoesUserHaveTeamAccess(userID string, teamID string) bool
}
// Auth authenticates sessions.
type Auth struct {
config *config.Configuration
store store.Store
config *config.Configuration
store store.Store
permissions permissions.PermissionsService
}
// New returns a new Auth.
func New(config *config.Configuration, store store.Store) *Auth {
return &Auth{config: config, store: store}
func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth {
return &Auth{config: config, store: store, permissions: permissions}
}
// GetSession Get a user active session and refresh the session if needed.
@ -44,14 +46,9 @@ func (a *Auth) GetSession(token string) (*model.Session, error) {
return session, nil
}
// IsValidReadToken validates the read token for a block.
func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) {
rootID, err := a.store.GetRootID(c, blockID)
if err != nil {
return false, err
}
sharing, err := a.store.GetSharing(c, rootID)
// IsValidReadToken validates the read token for a board.
func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) {
sharing, err := a.store.GetSharing(boardID)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
@ -59,17 +56,13 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str
return false, err
}
if sharing != nil && (sharing.ID == rootID && sharing.Enabled && sharing.Token == readToken) {
if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) {
return true, nil
}
return false, nil
}
func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool {
hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID)
if err != nil {
return false
}
return hasAccess
func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool {
return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam)
}

View File

@ -1,15 +1,16 @@
package auth
import (
"database/sql"
"testing"
"github.com/golang/mock/gomock"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
mockpermissions "github.com/mattermost/focalboard/server/services/permissions/mocks"
"github.com/mattermost/focalboard/server/services/store/mockstore"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
@ -31,14 +32,19 @@ var mockSession = &model.Session{
func setupTestHelper(t *testing.T) *TestHelper {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctrlPermissions := gomock.NewController(t)
defer ctrlPermissions.Finish()
cfg := config.Configuration{}
mockStore := mockstore.NewMockStore(ctrl)
newAuth := New(&cfg, mockStore)
mockPermissions := mockpermissions.NewMockStore(ctrlPermissions)
logger, err := mlog.NewLogger()
require.NoError(t, err)
newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger))
// called during default template setup for every test
mockStore.EXPECT().GetDefaultTemplateBlocks().AnyTimes()
mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes()
mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes()
mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes()
return &TestHelper{
Auth: newAuth,
@ -83,55 +89,57 @@ func TestGetSession(t *testing.T) {
}
func TestIsValidReadToken(t *testing.T) {
th := setupTestHelper(t)
// ToDo: reimplement
validBlockID := "testBlockID"
mockContainer := store.Container{
WorkspaceID: "testWorkspaceID",
}
validReadToken := "testReadToken"
mockSharing := model.Sharing{
ID: "testRootID",
Enabled: true,
Token: validReadToken,
}
// th := setupTestHelper(t)
testcases := []struct {
title string
container store.Container
blockID string
readToken string
isError bool
isSuccess bool
}{
{"fail, error GetRootID", mockContainer, "badBlock", "", true, false},
{"fail, rootID not found", mockContainer, "goodBlockID", "", false, false},
{"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false},
{"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false},
{"success", mockContainer, validBlockID, validReadToken, false, true},
}
// validBlockID := "testBlockID"
// mockContainer := store.Container{
// TeamID: "testTeamID",
// }
// validReadToken := "testReadToken"
// mockSharing := model.Sharing{
// ID: "testRootID",
// Enabled: true,
// Token: validReadToken,
// }
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block"))
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil)
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil)
th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2)
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows)
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error"))
th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2)
// testcases := []struct {
// title string
// container store.Container
// blockID string
// readToken string
// isError bool
// isSuccess bool
// }{
// {"fail, error GetRootID", mockContainer, "badBlock", "", true, false},
// {"fail, rootID not found", mockContainer, "goodBlockID", "", false, false},
// {"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false},
// {"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false},
// {"success", mockContainer, validBlockID, validReadToken, false, true},
// }
for _, test := range testcases {
t.Run(test.title, func(t *testing.T) {
success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken)
if test.isError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if test.isSuccess {
require.True(t, success)
} else {
require.False(t, success)
}
})
}
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block"))
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil)
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil)
// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2)
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows)
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error"))
// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2)
// for _, test := range testcases {
// t.Run(test.title, func(t *testing.T) {
// success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken)
// if test.isError {
// require.Error(t, err)
// } else {
// require.NoError(t, err)
// }
// if test.isSuccess {
// require.True(t, success)
// } else {
// require.False(t, success)
// }
// })
// }
}

View File

@ -9,7 +9,6 @@ import (
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/focalboard/server/model"
store "github.com/mattermost/focalboard/server/services/store"
)
// MockAuthInterface is a mock of AuthInterface interface.
@ -35,18 +34,18 @@ func (m *MockAuthInterface) EXPECT() *MockAuthInterfaceMockRecorder {
return m.recorder
}
// DoesUserHaveWorkspaceAccess mocks base method.
func (m *MockAuthInterface) DoesUserHaveWorkspaceAccess(arg0, arg1 string) bool {
// DoesUserHaveTeamAccess mocks base method.
func (m *MockAuthInterface) DoesUserHaveTeamAccess(arg0, arg1 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DoesUserHaveWorkspaceAccess", arg0, arg1)
ret := m.ctrl.Call(m, "DoesUserHaveTeamAccess", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// DoesUserHaveWorkspaceAccess indicates an expected call of DoesUserHaveWorkspaceAccess.
func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call {
// DoesUserHaveTeamAccess indicates an expected call of DoesUserHaveTeamAccess.
func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveTeamAccess(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveWorkspaceAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveWorkspaceAccess), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveTeamAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveTeamAccess), arg0, arg1)
}
// GetSession mocks base method.
@ -65,16 +64,16 @@ func (mr *MockAuthInterfaceMockRecorder) GetSession(arg0 interface{}) *gomock.Ca
}
// IsValidReadToken mocks base method.
func (m *MockAuthInterface) IsValidReadToken(arg0 store.Container, arg1, arg2 string) (bool, error) {
func (m *MockAuthInterface) IsValidReadToken(arg0, arg1 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1, arg2)
ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsValidReadToken indicates an expected call of IsValidReadToken.
func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1, arg2 interface{}) *gomock.Call {
func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1, arg2)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1)
}

View File

@ -101,8 +101,8 @@ func (c *Client) DoAPIPut(url, data string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "")
}
func (c *Client) DoAPIDelete(url string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "")
func (c *Client) DoAPIDelete(url string, data string) (*http.Response, error) {
return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, data, "")
}
func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, error) {
@ -152,20 +152,50 @@ func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* eta
return rp, nil
}
func (c *Client) GetBlocksRoute() string {
return "/workspaces/0/blocks"
func (c *Client) GetTeamRoute(teamID string) string {
return fmt.Sprintf("%s/%s", c.GetTeamsRoute(), teamID)
}
func (c *Client) GetBlockRoute(id string) string {
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(), id)
func (c *Client) GetTeamsRoute() string {
return "/teams"
}
func (c *Client) GetSubtreeRoute(id string) string {
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(id))
func (c *Client) GetBlockRoute(boardID, blockID string) string {
return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID)
}
func (c *Client) GetBlocks() ([]model.Block, *Response) {
r, err := c.DoAPIGet(c.GetBlocksRoute(), "")
func (c *Client) GetSubtreeRoute(boardID, blockID string) string {
return fmt.Sprintf("%s/subtree", c.GetBlockRoute(boardID, blockID))
}
func (c *Client) GetBoardsRoute() string {
return "/boards"
}
func (c *Client) GetBoardRoute(boardID string) string {
return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID)
}
func (c *Client) GetBlocksRoute(boardID string) string {
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
}
func (c *Client) GetBoardsAndBlocksRoute() string {
return "/boards-and-blocks"
}
func (c *Client) GetTeam(teamID string) (*model.Team, *Response) {
r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.TeamFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) GetBlocksForBoard(boardID string) ([]model.Block, *Response) {
r, err := c.DoAPIGet(c.GetBlocksRoute(boardID), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -174,8 +204,8 @@ func (c *Client) GetBlocks() ([]model.Block, *Response) {
return model.BlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, *Response) {
r, err := c.DoAPIPatch(c.GetBlockRoute(blockID), toJSON(blockPatch))
func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch) (bool, *Response) {
r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID), toJSON(blockPatch))
if err != nil {
return false, BuildErrorResponse(r, err)
}
@ -184,8 +214,46 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool,
return true, BuildResponse(r)
}
func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) {
r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks))
func (c *Client) DuplicateBoard(boardID string, asTemplate bool, teamID string) (bool, *Response) {
queryParams := "?asTemplate=false&"
if asTemplate {
queryParams = "?asTemplate=true"
}
r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/duplicate"+queryParams, "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) DuplicateBlock(boardID, blockID string, asTemplate bool) (bool, *Response) {
queryParams := "?asTemplate=false"
if asTemplate {
queryParams = "?asTemplate=true"
}
r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/duplicate"+queryParams, "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) {
r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/undelete", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) InsertBlocks(boardID string, blocks []model.Block) ([]model.Block, *Response) {
r, err := c.DoAPIPost(c.GetBlocksRoute(boardID), toJSON(blocks))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -194,8 +262,8 @@ func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) {
return model.BlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
r, err := c.DoAPIDelete(c.GetBlockRoute(blockID))
func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) {
r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID), "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
@ -204,18 +272,8 @@ func (c *Client) DeleteBlock(blockID string) (bool, *Response) {
return true, BuildResponse(r)
}
func (c *Client) UndeleteBlock(blockID string) (bool, *Response) {
r, err := c.DoAPIPost(c.GetBlockRoute(blockID)+"/undelete", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) {
r, err := c.DoAPIGet(c.GetSubtreeRoute(blockID), "")
func (c *Client) GetSubtree(boardID, blockID string) ([]model.Block, *Response) {
r, err := c.DoAPIGet(c.GetSubtreeRoute(boardID, blockID), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -224,14 +282,45 @@ func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) {
return model.BlocksFromJSON(r.Body), BuildResponse(r)
}
// Boards and blocks.
func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) {
r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks) (bool, *Response) {
r, err := c.DoAPIDelete(c.GetBoardsAndBlocksRoute(), toJSON(dbab))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
// Sharing
func (c *Client) GetSharingRoute(rootID string) string {
return fmt.Sprintf("/workspaces/0/sharing/%s", rootID)
func (c *Client) GetSharingRoute(boardID string) string {
return fmt.Sprintf("%s/sharing", c.GetBoardRoute(boardID))
}
func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) {
r, err := c.DoAPIGet(c.GetSharingRoute(rootID), "")
func (c *Client) GetSharing(boardID string) (*model.Sharing, *Response) {
r, err := c.DoAPIGet(c.GetSharingRoute(boardID), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -241,7 +330,7 @@ func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) {
return &sharing, BuildResponse(r)
}
func (c *Client) PostSharing(sharing model.Sharing) (bool, *Response) {
func (c *Client) PostSharing(sharing *model.Sharing) (bool, *Response) {
r, err := c.DoAPIPost(c.GetSharingRoute(sharing.ID), toJSON(sharing))
if err != nil {
return false, BuildErrorResponse(r, err)
@ -338,11 +427,116 @@ func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest)
return true, BuildResponse(r)
}
func (c *Client) GetWorkspaceUploadFileRoute(workspaceID, rootID string) string {
return fmt.Sprintf("/workspaces/%s/%s/files", workspaceID, rootID)
func (c *Client) CreateBoard(board *model.Board) (*model.Board, *Response) {
r, err := c.DoAPIPost(c.GetBoardsRoute(), toJSON(board))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) (*api.FileUploadResponse, *Response) {
func (c *Client) PatchBoard(boardID string, patch *model.BoardPatch) (*model.Board, *Response) {
r, err := c.DoAPIPatch(c.GetBoardRoute(boardID), toJSON(patch))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) DeleteBoard(boardID string) (bool, *Response) {
r, err := c.DoAPIDelete(c.GetBoardRoute(boardID), "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) GetBoard(boardID, readToken string) (*model.Board, *Response) {
url := c.GetBoardRoute(boardID)
if readToken != "" {
url += fmt.Sprintf("?read_token=%s", readToken)
}
r, err := c.DoAPIGet(url, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) GetBoardsForTeam(teamID string) ([]*model.Board, *Response) {
r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardsFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) SearchBoardsForTeam(teamID, term string) ([]*model.Board, *Response) {
r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?q="+term, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardsFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) GetMembersForBoard(boardID string) ([]*model.BoardMember, *Response) {
r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/members", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardMembersFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, *Response) {
r, err := c.DoAPIPost(c.GetBoardRoute(member.BoardID)+"/members", toJSON(member))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
}
func (c *Client) DeleteBoardMember(member *model.BoardMember) (bool, *Response) {
r, err := c.DoAPIDelete(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
func (c *Client) GetTeamUploadFileRoute(teamID, boardID string) string {
return fmt.Sprintf("%s/%s/files", c.GetTeamRoute(teamID), boardID)
}
func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.FileUploadResponse, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(api.UploadFormFileKey, "file")
@ -358,7 +552,7 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader)
r.Header.Add("Content-Type", writer.FormDataContentType())
}
r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetWorkspaceUploadFileRoute(workspaceID, rootID), body, "", opt)
r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamUploadFileRoute(teamID, boardID), body, "", opt)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -372,12 +566,12 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader)
return fileUploadResponse, BuildResponse(r)
}
func (c *Client) GetSubscriptionsRoute(workspaceID string) string {
return fmt.Sprintf("/workspaces/%s/subscriptions", workspaceID)
func (c *Client) GetSubscriptionsRoute() string {
return "/subscriptions"
}
func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) (*model.Subscription, *Response) {
r, err := c.DoAPIPost(c.GetSubscriptionsRoute(workspaceID), toJSON(&sub))
func (c *Client) CreateSubscription(sub *model.Subscription) (*model.Subscription, *Response) {
r, err := c.DoAPIPost(c.GetSubscriptionsRoute(), toJSON(&sub))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
@ -390,10 +584,10 @@ func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription)
return subNew, BuildResponse(r)
}
func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscriberID string) *Response {
url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(workspaceID), blockID, subscriberID)
func (c *Client) DeleteSubscription(blockID string, subscriberID string) *Response {
url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(), blockID, subscriberID)
r, err := c.DoAPIDelete(url)
r, err := c.DoAPIDelete(url, "")
if err != nil {
return BuildErrorResponse(r, err)
}
@ -402,8 +596,8 @@ func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscrib
return BuildResponse(r)
}
func (c *Client) GetSubscriptions(workspaceID string, subscriberID string) ([]*model.Subscription, *Response) {
url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(workspaceID), subscriberID)
func (c *Client) GetSubscriptions(subscriberID string) ([]*model.Subscription, *Response) {
url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(), subscriberID)
r, err := c.DoAPIGet(url, "")
if err != nil {

View File

@ -5,7 +5,6 @@ go 1.16
require (
github.com/Masterminds/squirrel v1.5.0
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-migrate/migrate/v4 v4.14.1
github.com/golang/mock v1.5.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
@ -14,6 +13,7 @@ require (
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattermost/mattermost-plugin-api v0.0.21
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/oklog/run v1.1.0

View File

@ -47,7 +47,6 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -68,7 +67,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8=
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
@ -161,7 +159,6 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -185,6 +182,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -195,19 +193,14 @@ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFau
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dhui/dktest v0.3.3 h1:DBuH/9GFaWbDRa42qsut/hbQu+srAQ0rPWnUoiGX7CA=
github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ=
github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@ -233,6 +226,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
@ -290,9 +284,7 @@ github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhD
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE=
github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@ -346,6 +338,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
@ -366,8 +359,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@ -403,7 +397,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@ -416,7 +409,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0=
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
@ -513,6 +505,8 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -564,6 +558,7 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
@ -592,6 +587,8 @@ github.com/mattermost/mattermost-plugin-api v0.0.21/go.mod h1:qz19Y+5HLbjtzY2RZ6
github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go.mod h1:kmxJuVgpX13Th+e5L1ZsBs4aq+ETmmDg9joo5r4cIw8=
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 h1:A7TCgCGF9JmAHBQv9qGm5SfPYTAl8dOXy/u6lCSV8ow=
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08=
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14=
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw=
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -616,6 +613,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@ -658,7 +656,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@ -705,9 +702,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
@ -782,6 +777,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -1063,6 +1059,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1203,6 +1200,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1213,8 +1211,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1287,9 +1287,11 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1454,17 +1456,139 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM=
modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc=
modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8=
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q=
modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ=
modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y=
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw=
modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg=
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk=
modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo=
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

View File

@ -11,40 +11,38 @@ import (
)
func TestGetBlocks(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
initialCount := len(blocks)
board := th.CreateBoard("team-id", model.BoardTypeOpen)
initialID1 := utils.NewID(utils.IDTypeBlock)
initialID2 := utils.NewID(utils.IDTypeBlock)
newBlocks := []model.Block{
{
ID: initialID1,
RootID: initialID1,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
},
{
ID: initialID2,
RootID: initialID2,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
},
}
newBlocks, resp = th.Client.InsertBlocks(newBlocks)
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 2)
blockID1 := newBlocks[0].ID
blockID2 := newBlocks[1].ID
blocks, resp = th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+2)
require.Len(t, blocks, 2)
blockIDs := make([]string, len(blocks))
for i, b := range blocks {
@ -55,12 +53,10 @@ func TestGetBlocks(t *testing.T) {
}
func TestPostBlock(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
initialCount := len(blocks)
board := th.CreateBoard("team-id", model.BoardTypeOpen)
var blockID1 string
var blockID2 string
@ -70,21 +66,21 @@ func TestPostBlock(t *testing.T) {
initialID1 := utils.NewID(utils.IDTypeBlock)
block := model.Block{
ID: initialID1,
RootID: initialID1,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
Title: "New title",
}
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
blockID1 = newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+1)
require.Len(t, blocks, 1)
blockIDs := make([]string, len(blocks))
for i, b := range blocks {
@ -99,21 +95,21 @@ func TestPostBlock(t *testing.T) {
newBlocks := []model.Block{
{
ID: initialID2,
RootID: initialID2,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
},
{
ID: initialID3,
RootID: initialID3,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
},
}
newBlocks, resp := th.Client.InsertBlocks(newBlocks)
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 2)
blockID2 = newBlocks[0].ID
@ -121,9 +117,9 @@ func TestPostBlock(t *testing.T) {
require.NotEqual(t, initialID2, blockID2)
require.NotEqual(t, initialID3, blockID3)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+3)
require.Len(t, blocks, 3)
blockIDs := make([]string, len(blocks))
for i, b := range blocks {
@ -137,22 +133,22 @@ func TestPostBlock(t *testing.T) {
t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) {
block := model.Block{
ID: blockID1,
RootID: blockID1,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 20,
Type: model.TypeBoard,
Type: model.TypeCard,
Title: "Updated title",
}
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
blockID4 := newBlocks[0].ID
require.NotEqual(t, blockID1, blockID4)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+4)
require.Len(t, blocks, 4)
var block4 model.Block
for _, b := range blocks {
@ -166,42 +162,41 @@ func TestPostBlock(t *testing.T) {
}
func TestPatchBlock(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
initialID := utils.NewID(utils.IDTypeBlock)
board := th.CreateBoard("team-id", model.BoardTypeOpen)
time.Sleep(10 * time.Millisecond)
block := model.Block{
ID: initialID,
RootID: initialID,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
Title: "New title",
Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"},
}
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
require.NoError(t, resp.Error)
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
th.CheckOK(resp)
require.Len(t, newBlocks, 1)
blockID := newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
initialCount := len(blocks)
t.Run("Patch a block basic field", func(t *testing.T) {
newTitle := "Updated title"
blockPatch := &model.BlockPatch{
Title: &newTitle,
}
_, resp := th.Client.PatchBlock(blockID, blockPatch)
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount)
require.Len(t, blocks, 1)
var updatedBlock model.Block
for _, b := range blocks {
@ -221,12 +216,12 @@ func TestPatchBlock(t *testing.T) {
},
}
_, resp := th.Client.PatchBlock(blockID, blockPatch)
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount)
require.Len(t, blocks, 1)
var updatedBlock model.Block
for _, b := range blocks {
@ -244,12 +239,12 @@ func TestPatchBlock(t *testing.T) {
DeletedFields: []string{"test", "test3", "test100"},
}
_, resp := th.Client.PatchBlock(blockID, blockPatch)
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount)
require.Len(t, blocks, 1)
var updatedBlock model.Block
for _, b := range blocks {
@ -265,35 +260,34 @@ func TestPatchBlock(t *testing.T) {
}
func TestDeleteBlock(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
initialCount := len(blocks)
board := th.CreateBoard("team-id", model.BoardTypeOpen)
time.Sleep(10 * time.Millisecond)
var blockID string
t.Run("Create a block", func(t *testing.T) {
initialID := utils.NewID(utils.IDTypeBlock)
block := model.Block{
ID: initialID,
RootID: initialID,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
Title: "New title",
}
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
require.NotZero(t, newBlocks[0].ID)
require.NotEqual(t, initialID, newBlocks[0].ID)
blockID = newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+1)
require.Len(t, blocks, 1)
blockIDs := make([]string, len(blocks))
for i, b := range blocks {
@ -307,43 +301,45 @@ func TestDeleteBlock(t *testing.T) {
// id,insert_at on block history
time.Sleep(10 * time.Millisecond)
_, resp := th.Client.DeleteBlock(blockID)
_, resp := th.Client.DeleteBlock(board.ID, blockID)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount)
require.Empty(t, blocks)
})
}
func TestUndeleteBlock(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
blocks, resp := th.Client.GetBlocks()
board := th.CreateBoard("team-id", model.BoardTypeOpen)
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
initialCount := len(blocks)
var blockID string
t.Run("Create a block", func(t *testing.T) {
initialID := utils.NewID(utils.IDTypeBlock)
initialID := utils.NewID(utils.IDTypeBoard)
block := model.Block{
ID: initialID,
RootID: initialID,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Title: "New title",
}
newBlocks, resp := th.Client.InsertBlocks([]model.Block{block})
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
require.NoError(t, resp.Error)
require.Len(t, newBlocks, 1)
require.NotZero(t, newBlocks[0].ID)
require.NotEqual(t, initialID, newBlocks[0].ID)
blockID = newBlocks[0].ID
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+1)
@ -359,10 +355,10 @@ func TestUndeleteBlock(t *testing.T) {
// id,insert_at on block history
time.Sleep(10 * time.Millisecond)
_, resp := th.Client.DeleteBlock(blockID)
_, resp := th.Client.DeleteBlock(board.ID, blockID)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount)
})
@ -372,10 +368,10 @@ func TestUndeleteBlock(t *testing.T) {
// id,insert_at on block history
time.Sleep(10 * time.Millisecond)
_, resp := th.Client.UndeleteBlock(blockID)
_, resp := th.Client.UndeleteBlock(board.ID, blockID)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+1)
})
@ -384,12 +380,10 @@ func TestUndeleteBlock(t *testing.T) {
func TestGetSubtree(t *testing.T) {
t.Skip("TODO: fix flaky test")
th := SetupTestHelper().InitBasic()
th := SetupTestHelperWithToken(t).Start()
defer th.TearDown()
blocks, resp := th.Client.GetBlocks()
require.NoError(t, resp.Error)
initialCount := len(blocks)
board := th.CreateBoard("team-id", model.BoardTypeOpen)
parentBlockID := utils.NewID(utils.IDTypeBlock)
childBlockID1 := utils.NewID(utils.IDTypeBlock)
@ -399,14 +393,14 @@ func TestGetSubtree(t *testing.T) {
newBlocks := []model.Block{
{
ID: parentBlockID,
RootID: parentBlockID,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
Type: model.TypeCard,
},
{
ID: childBlockID1,
RootID: parentBlockID,
BoardID: board.ID,
ParentID: parentBlockID,
CreateAt: 2,
UpdateAt: 2,
@ -414,7 +408,7 @@ func TestGetSubtree(t *testing.T) {
},
{
ID: childBlockID2,
RootID: parentBlockID,
BoardID: board.ID,
ParentID: parentBlockID,
CreateAt: 2,
UpdateAt: 2,
@ -422,12 +416,12 @@ func TestGetSubtree(t *testing.T) {
},
}
_, resp := th.Client.InsertBlocks(newBlocks)
_, resp := th.Client.InsertBlocks(board.ID, newBlocks)
require.NoError(t, resp.Error)
blocks, resp := th.Client.GetBlocks()
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
require.NoError(t, resp.Error)
require.Len(t, blocks, initialCount+1) // GetBlocks returns root blocks (null ParentID)
require.Len(t, blocks, 1) // GetBlocks returns root blocks (null ParentID)
blockIDs := make([]string, len(blocks))
for i, b := range blocks {
@ -437,7 +431,7 @@ func TestGetSubtree(t *testing.T) {
})
t.Run("Get subtree for parent ID", func(t *testing.T) {
blocks, resp := th.Client.GetSubtree(parentBlockID)
blocks, resp := th.Client.GetSubtree(board.ID, parentBlockID)
require.NoError(t, resp.Error)
require.Len(t, blocks, 3)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,816 @@
package integrationtests
import (
"testing"
"github.com/mattermost/focalboard/server/model"
"github.com/stretchr/testify/require"
)
func TestCreateBoardsAndBlocks(t *testing.T) {
teamID := testTeamID
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th := SetupTestHelper(t).Start()
defer th.TearDown()
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{},
Blocks: []model.Block{},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckUnauthorized(resp)
require.Nil(t, bab)
})
t.Run("invalid boards and blocks", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
t.Run("no boards", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{},
Blocks: []model.Block{
{ID: "block-id", BoardID: "board-id", Type: model.TypeCard},
},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("no blocks", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
},
Blocks: []model.Block{},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("blocks from nonexistent boards", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
},
Blocks: []model.Block{
{ID: "block-id", BoardID: "nonexistent-board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("boards with no IDs", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate},
{TeamID: teamID, Type: model.BoardTypePrivate},
},
Blocks: []model.Block{
{ID: "block-id", BoardID: "board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("boards from different teams", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id-1", TeamID: "team-id-1", Type: model.BoardTypePrivate},
{ID: "board-id-2", TeamID: "team-id-2", Type: model.BoardTypePrivate},
},
Blocks: []model.Block{
{ID: "block-id", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("creating boards and blocks", func(t *testing.T) {
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen},
{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate},
},
Blocks: []model.Block{
{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
},
}
bab, resp := th.Client.CreateBoardsAndBlocks(newBab)
th.CheckOK(resp)
require.NotNil(t, bab)
require.Len(t, bab.Boards, 2)
require.Len(t, bab.Blocks, 2)
// board 1 should have been created with a new ID, and its
// block should be there too
boardsTermPublic, resp := th.Client.SearchBoardsForTeam(teamID, "public")
th.CheckOK(resp)
require.Len(t, boardsTermPublic, 1)
board1 := boardsTermPublic[0]
require.Equal(t, "public board", board1.Title)
require.Equal(t, model.BoardTypeOpen, board1.Type)
require.NotEqual(t, "board-id-1", board1.ID)
blocks1, err := th.Server.App().GetBlocksForBoard(board1.ID)
require.NoError(t, err)
require.Len(t, blocks1, 1)
require.Equal(t, "block 1", blocks1[0].Title)
// board 1 should have been created with a new ID, and its
// block should be there too
boardsTermPrivate, resp := th.Client.SearchBoardsForTeam(teamID, "private")
th.CheckOK(resp)
require.Len(t, boardsTermPrivate, 1)
board2 := boardsTermPrivate[0]
require.Equal(t, "private board", board2.Title)
require.Equal(t, model.BoardTypePrivate, board2.Type)
require.NotEqual(t, "board-id-2", board2.ID)
blocks2, err := th.Server.App().GetBlocksForBoard(board2.ID)
require.NoError(t, err)
require.Len(t, blocks2, 1)
require.Equal(t, "block 2", blocks2[0].Title)
// user should be an admin of both newly created boards
user1 := th.GetUser1()
members1, err := th.Server.App().GetMembersForBoard(board1.ID)
require.NoError(t, err)
require.Len(t, members1, 1)
require.Equal(t, user1.ID, members1[0].UserID)
members2, err := th.Server.App().GetMembersForBoard(board2.ID)
require.NoError(t, err)
require.Len(t, members2, 1)
require.Equal(t, user1.ID, members2[0].UserID)
})
})
}
func TestPatchBoardsAndBlocks(t *testing.T) {
teamID := "team-id"
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th := SetupTestHelper(t).Start()
defer th.TearDown()
pbab := &model.PatchBoardsAndBlocks{}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckUnauthorized(resp)
require.Nil(t, bab)
})
t.Run("invalid patch boards and blocks", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title 1"
newTitle := "new title 1"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
t.Run("no board IDs", func(t *testing.T) {
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("missmatch board IDs and patches", func(t *testing.T) {
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("no block IDs", func(t *testing.T) {
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("missmatch block IDs and patches", func(t *testing.T) {
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("block that doesn't belong to any board", func(t *testing.T) {
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, "board-id-2"},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
})
t.Run("if the user doesn't have permissions for one of the boards, nothing should be updated", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title 2"
newTitle := "new title 2"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, false)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckForbidden(resp)
require.Nil(t, bab)
})
t.Run("boards belonging to different teams should be rejected", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title 3"
newTitle := "new title 3"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: "different-team-id",
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, "board-id-2"},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("patches should be rejected if one is invalid", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title 4"
newTitle := "new title 4"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, false)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
var invalidPatchType model.BoardType = "invalid"
invalidPatch := &model.BoardPatch{Type: &invalidPatchType}
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
invalidPatch,
},
BlockIDs: []string{block1.ID, "board-id-2"},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("patches should be rejected if there is a block that doesn't belong to the boards being patched", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title"
newTitle := "new title"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckBadRequest(resp)
require.Nil(t, bab)
})
t.Run("patches should be applied if they're valid and they're related", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
userID := th.GetUser1().ID
initialTitle := "initial title"
newTitle := "new title"
newBoard1 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, userID, true)
require.NoError(t, err)
require.NotNil(t, board1)
newBoard2 := &model.Board{
Title: initialTitle,
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, userID, true)
require.NoError(t, err)
require.NotNil(t, board2)
newBlock1 := model.Block{
ID: "block-id-1",
BoardID: board1.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID))
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
newBlock2 := model.Block{
ID: "block-id-2",
BoardID: board2.ID,
Title: initialTitle,
}
require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID))
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
pbab := &model.PatchBoardsAndBlocks{
BoardIDs: []string{board1.ID, board2.ID},
BoardPatches: []*model.BoardPatch{
{Title: &newTitle},
{Title: &newTitle},
},
BlockIDs: []string{block1.ID, block2.ID},
BlockPatches: []*model.BlockPatch{
{Title: &newTitle},
{Title: &newTitle},
},
}
bab, resp := th.Client.PatchBoardsAndBlocks(pbab)
th.CheckOK(resp)
require.NotNil(t, bab)
require.Len(t, bab.Boards, 2)
require.Len(t, bab.Blocks, 2)
// ensure that the entities have been updated
rBoard1, err := th.Server.App().GetBoard(board1.ID)
require.NoError(t, err)
require.Equal(t, newTitle, rBoard1.Title)
rBlock1, err := th.Server.App().GetBlockByID(block1.ID)
require.NoError(t, err)
require.Equal(t, newTitle, rBlock1.Title)
rBoard2, err := th.Server.App().GetBoard(board2.ID)
require.NoError(t, err)
require.Equal(t, newTitle, rBoard2.Title)
rBlock2, err := th.Server.App().GetBlockByID(block2.ID)
require.NoError(t, err)
require.Equal(t, newTitle, rBlock2.Title)
})
}
func TestDeleteBoardsAndBlocks(t *testing.T) {
teamID := "team-id"
t.Run("a non authenticated user should be rejected", func(t *testing.T) {
th := SetupTestHelper(t).Start()
defer th.TearDown()
dbab := &model.DeleteBoardsAndBlocks{}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckUnauthorized(resp)
require.False(t, success)
})
t.Run("invalid delete boards and blocks", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
// a board is required for the permission checks
newBoard := &model.Board{
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true)
require.NoError(t, err)
require.NotNil(t, board)
t.Run("no boards", func(t *testing.T) {
dbab := &model.DeleteBoardsAndBlocks{
Blocks: []string{"block-id-1"},
}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckBadRequest(resp)
require.False(t, success)
})
t.Run("no blocks", func(t *testing.T) {
dbab := &model.DeleteBoardsAndBlocks{
Boards: []string{board.ID},
}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckBadRequest(resp)
require.False(t, success)
})
t.Run("boards from different teams", func(t *testing.T) {
newOtherTeamsBoard := &model.Board{
TeamID: "another-team-id",
Type: model.BoardTypeOpen,
}
otherTeamsBoard, err := th.Server.App().CreateBoard(newOtherTeamsBoard, th.GetUser1().ID, true)
require.NoError(t, err)
require.NotNil(t, board)
dbab := &model.DeleteBoardsAndBlocks{
Boards: []string{board.ID, otherTeamsBoard.ID},
Blocks: []string{"block-id-1"},
}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckBadRequest(resp)
require.False(t, success)
})
})
t.Run("if the user has no permissions to one of the boards, nothing should be deleted", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
// the user is an admin of the first board
newBoard1 := &model.Board{
Type: model.BoardTypeOpen,
}
board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true)
require.NoError(t, err)
require.NotNil(t, board1)
// but not of the second
newBoard2 := &model.Board{
Type: model.BoardTypeOpen,
}
board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false)
require.NoError(t, err)
require.NotNil(t, board2)
dbab := &model.DeleteBoardsAndBlocks{
Boards: []string{board1.ID, board2.ID},
Blocks: []string{"block-id-1"},
}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckForbidden(resp)
require.False(t, success)
})
t.Run("all boards and blocks should be deleted if the request is correct", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
newBab := &model.BoardsAndBlocks{
Boards: []*model.Board{
{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen},
{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate},
},
Blocks: []model.Block{
{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1},
},
}
bab, err := th.Server.App().CreateBoardsAndBlocks(newBab, th.GetUser1().ID, true)
require.NoError(t, err)
require.Len(t, bab.Boards, 2)
require.Len(t, bab.Blocks, 2)
// ensure that the entities have been successfully created
board1, err := th.Server.App().GetBoard("board-id-1")
require.NoError(t, err)
require.NotNil(t, board1)
block1, err := th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.NotNil(t, block1)
board2, err := th.Server.App().GetBoard("board-id-2")
require.NoError(t, err)
require.NotNil(t, board2)
block2, err := th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.NotNil(t, block2)
// call the API to delete boards and blocks
dbab := &model.DeleteBoardsAndBlocks{
Boards: []string{"board-id-1", "board-id-2"},
Blocks: []string{"block-id-1", "block-id-2"},
}
success, resp := th.Client.DeleteBoardsAndBlocks(dbab)
th.CheckOK(resp)
require.True(t, success)
// ensure that the entities have been successfully deleted
board1, err = th.Server.App().GetBoard("board-id-1")
require.NoError(t, err)
require.Nil(t, board1)
block1, err = th.Server.App().GetBlockByID("block-id-1")
require.NoError(t, err)
require.Nil(t, block1)
board2, err = th.Server.App().GetBoard("board-id-2")
require.NoError(t, err)
require.Nil(t, board2)
block2, err = th.Server.App().GetBlockByID("block-id-2")
require.NoError(t, err)
require.Nil(t, block2)
})
}

View File

@ -4,19 +4,30 @@ import (
"errors"
"net/http"
"os"
"testing"
"time"
"github.com/mattermost/focalboard/server/api"
"github.com/mattermost/focalboard/server/client"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/server"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
"github.com/mattermost/focalboard/server/services/store/sqlstore"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/stretchr/testify/require"
)
const (
user1Username = "user1"
user2Username = "user2"
password = "Pa$$word"
)
type TestHelper struct {
T *testing.T
Server *server.Server
Client *client.Client
Client2 *client.Client
@ -80,11 +91,14 @@ func newTestServer(singleUserToken string) *server.Server {
panic(err)
}
permissionsService := localpermissions.New(db, logger)
params := server.Params{
Cfg: cfg,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
Cfg: cfg,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
PermissionsService: permissionsService,
}
srv, err := server.New(params)
@ -95,23 +109,26 @@ func newTestServer(singleUserToken string) *server.Server {
return srv
}
func SetupTestHelper() *TestHelper {
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
sessionToken := "TESTTOKEN"
th := &TestHelper{}
th := &TestHelper{T: t}
th.Server = newTestServer(sessionToken)
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
return th
}
func SetupTestHelperWithoutToken() *TestHelper {
th := &TestHelper{}
func SetupTestHelper(t *testing.T) *TestHelper {
th := &TestHelper{T: t}
th.Server = newTestServer("")
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
return th
}
func (th *TestHelper) InitBasic() *TestHelper {
// Start starts the test server and ensures that it's correctly
// responding to requests before returning.
func (th *TestHelper) Start() *TestHelper {
go func() {
if err := th.Server.Start(); err != nil {
panic(err)
@ -144,51 +161,28 @@ func (th *TestHelper) InitBasic() *TestHelper {
return th
}
var ErrRegisterFail = errors.New("register failed")
// InitBasic starts the test server and initializes the clients of the
// helper, registering them and logging them into the system.
func (th *TestHelper) InitBasic() *TestHelper {
th.Start()
func (th *TestHelper) InitUsers(username1 string, username2 string) error {
workspace, err := th.Server.App().GetRootWorkspace()
if err != nil {
return err
}
// user1
th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "")
clients := []*client.Client{th.Client, th.Client2}
usernames := []string{username1, username2}
// get token
team, resp := th.Client.GetTeam("0")
th.CheckOK(resp)
require.NotNil(th.T, team)
require.NotNil(th.T, team.SignupToken)
for i, client := range clients {
// register a new user
password := utils.NewID(utils.IDTypeNone)
registerRequest := &api.RegisterRequest{
Username: usernames[i],
Email: usernames[i] + "@example.com",
Password: password,
Token: workspace.SignupToken,
}
success, resp := client.Register(registerRequest)
if resp.Error != nil {
return resp.Error
}
if !success {
return ErrRegisterFail
}
// user2
th.RegisterAndLogin(th.Client2, user2Username, "user2@sample.com", password, team.SignupToken)
// login
loginRequest := &api.LoginRequest{
Type: "normal",
Username: registerRequest.Username,
Email: registerRequest.Email,
Password: registerRequest.Password,
}
data, resp := client.Login(loginRequest)
if resp.Error != nil {
return resp.Error
}
client.Token = data.Token
}
return nil
return th
}
var ErrRegisterFail = errors.New("register failed")
func (th *TestHelper) TearDown() {
defer func() { _ = th.Server.Logger().Shutdown() }()
@ -198,4 +192,96 @@ func (th *TestHelper) TearDown() {
}
os.RemoveAll(th.Server.Config().FilesPath)
if err := os.Remove(th.Server.Config().DBConfigString); err == nil {
th.Server.Logger().Debug("Removed test database", mlog.String("file", th.Server.Config().DBConfigString))
}
}
func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) {
req := &api.RegisterRequest{
Username: username,
Email: email,
Password: password,
Token: token,
}
success, resp := th.Client.Register(req)
th.CheckOK(resp)
require.True(th.T, success)
th.Login(client, username, password)
}
func (th *TestHelper) Login(client *client.Client, username, password string) {
req := &api.LoginRequest{
Type: "normal",
Username: username,
Password: password,
}
data, resp := client.Login(req)
th.CheckOK(resp)
require.NotNil(th.T, data)
}
func (th *TestHelper) Login1() {
th.Login(th.Client, user1Username, password)
}
func (th *TestHelper) Login2() {
th.Login(th.Client2, user2Username, password)
}
func (th *TestHelper) Logout(client *client.Client) {
client.Token = ""
}
func (th *TestHelper) Me(client *client.Client) *model.User {
user, resp := client.GetMe()
th.CheckOK(resp)
require.NotNil(th.T, user)
return user
}
func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *model.Board {
newBoard := &model.Board{
TeamID: teamID,
Type: boardType,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
return board
}
func (th *TestHelper) GetUser1() *model.User {
return th.Me(th.Client)
}
func (th *TestHelper) GetUser2() *model.User {
return th.Me(th.Client2)
}
func (th *TestHelper) CheckOK(r *client.Response) {
require.Equal(th.T, http.StatusOK, r.StatusCode)
require.NoError(th.T, r.Error)
}
func (th *TestHelper) CheckBadRequest(r *client.Response) {
require.Equal(th.T, http.StatusBadRequest, r.StatusCode)
require.Error(th.T, r.Error)
}
func (th *TestHelper) CheckNotFound(r *client.Response) {
require.Equal(th.T, http.StatusNotFound, r.StatusCode)
require.Error(th.T, r.Error)
}
func (th *TestHelper) CheckUnauthorized(r *client.Response) {
require.Equal(th.T, http.StatusUnauthorized, r.StatusCode)
require.Error(th.T, r.Error)
}
func (th *TestHelper) CheckForbidden(r *client.Response) {
require.Equal(th.T, http.StatusForbidden, r.StatusCode)
require.Error(th.T, r.Error)
}

View File

@ -1,6 +1,7 @@
package integrationtests
import (
"net/http"
"testing"
"github.com/mattermost/focalboard/server/model"
@ -9,60 +10,84 @@ import (
)
func TestSharing(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
rootID := utils.NewID(utils.IDTypeBlock)
var boardID string
token := utils.NewID(utils.IDTypeToken)
t.Run("an unauthenticated client should not be able to get a sharing", func(t *testing.T) {
th.Logout(th.Client)
sharing, resp := th.Client.GetSharing("board-id")
th.CheckUnauthorized(resp)
require.Nil(t, sharing)
})
t.Run("Check no initial sharing", func(t *testing.T) {
sharing, resp := th.Client.GetSharing(rootID)
require.NoError(t, resp.Error)
require.Empty(t, sharing.ID)
require.False(t, sharing.Enabled)
th.Login1()
teamID := "0"
newBoard := &model.Board{
TeamID: teamID,
Type: model.BoardTypeOpen,
}
board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true)
require.NoError(t, err)
require.NotNil(t, board)
boardID = board.ID
s, err := th.Server.App().GetSharing(boardID)
require.NoError(t, err)
require.Nil(t, s)
sharing, resp := th.Client.GetSharing(boardID)
th.CheckNotFound(resp)
require.Nil(t, sharing)
})
t.Run("POST sharing, config = false", func(t *testing.T) {
sharing := model.Sharing{
ID: rootID,
ID: boardID,
Token: token,
Enabled: true,
UpdateAt: 1,
}
// it will fail with default config
success, resp := th.Client.PostSharing(sharing)
success, resp := th.Client.PostSharing(&sharing)
require.False(t, success)
require.Error(t, resp.Error)
t.Run("GET sharing", func(t *testing.T) {
sharing, resp := th.Client.GetSharing(rootID)
// Expect no error, but no Id returned
require.NoError(t, resp.Error)
require.NotNil(t, sharing)
require.Equal(t, "", sharing.ID)
sharing, resp := th.Client.GetSharing(boardID)
// Expect not found error
require.Error(t, resp.Error)
require.Equal(t, resp.StatusCode, http.StatusNotFound)
require.Nil(t, sharing)
})
})
t.Run("POST sharing, config = true", func(t *testing.T) {
th.Server.Config().EnablePublicSharedBoards = true
sharing := model.Sharing{
ID: rootID,
ID: boardID,
Token: token,
Enabled: true,
UpdateAt: 1,
}
// it will succeed with updated config
success, resp := th.Client.PostSharing(sharing)
success, resp := th.Client.PostSharing(&sharing)
require.True(t, success)
require.NoError(t, resp.Error)
t.Run("GET sharing", func(t *testing.T) {
sharing, resp := th.Client.GetSharing(rootID)
sharing, resp := th.Client.GetSharing(boardID)
require.NoError(t, resp.Error)
require.NotNil(t, sharing)
require.Equal(t, sharing.ID, rootID)
require.Equal(t, sharing.ID, boardID)
require.True(t, sharing.Enabled)
require.Equal(t, sharing.Token, token)
})

View File

@ -6,13 +6,12 @@ import (
"github.com/mattermost/focalboard/server/client"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createTestSubscriptions(client *client.Client, num int, workspaceID string) ([]*model.Subscription, string, error) {
func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscription, string, error) {
newSubs := make([]*model.Subscription, 0, num)
user, resp := client.GetMe()
@ -20,29 +19,27 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
return nil, "", fmt.Errorf("cannot get current user: %w", resp.Error)
}
board := model.Block{
ID: utils.NewID(utils.IDTypeBoard),
RootID: workspaceID,
board := &model.Board{
TeamID: "0",
Type: model.BoardTypeOpen,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeBoard,
}
boards, resp := client.InsertBlocks([]model.Block{board})
board, resp = client.CreateBoard(board)
if resp.Error != nil {
return nil, "", fmt.Errorf("cannot insert test board block: %w", resp.Error)
}
board = boards[0]
for n := 0; n < num; n++ {
newBlock := model.Block{
ID: utils.NewID(utils.IDTypeCard),
RootID: board.ID,
BoardID: board.ID,
CreateAt: 1,
UpdateAt: 1,
Type: model.TypeCard,
}
newBlocks, resp := client.InsertBlocks([]model.Block{newBlock})
newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock})
if resp.Error != nil {
return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error)
}
@ -51,12 +48,11 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
sub := &model.Subscription{
BlockType: newBlock.Type,
BlockID: newBlock.ID,
WorkspaceID: workspaceID,
SubscriberType: model.SubTypeUser,
SubscriberID: user.ID,
}
subNew, resp := client.CreateSubscription(workspaceID, sub)
subNew, resp := client.CreateSubscription(sub)
if resp.Error != nil {
return nil, "", resp.Error
}
@ -66,20 +62,16 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string)
}
func TestCreateSubscription(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
container := store.Container{
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
}
t.Run("Create valid subscription", func(t *testing.T) {
subs, userID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID)
subs, userID, err := createTestSubscriptions(th.Client, 5)
require.NoError(t, err)
require.Len(t, subs, 5)
// fetch the newly created subscriptions and compare
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID)
subsFound, resp := th.Client.GetSubscriptions(userID)
require.NoError(t, resp.Error)
require.Len(t, subsFound, 5)
assert.ElementsMatch(t, subs, subsFound)
@ -90,47 +82,38 @@ func TestCreateSubscription(t *testing.T) {
require.NoError(t, resp.Error)
sub := &model.Subscription{
WorkspaceID: container.WorkspaceID,
SubscriberID: user.ID,
}
_, resp = th.Client.CreateSubscription(container.WorkspaceID, sub)
_, resp = th.Client.CreateSubscription(sub)
require.Error(t, resp.Error)
})
t.Run("Create subscription for another user", func(t *testing.T) {
sub := &model.Subscription{
WorkspaceID: container.WorkspaceID,
SubscriberID: utils.NewID(utils.IDTypeUser),
}
_, resp := th.Client.CreateSubscription(container.WorkspaceID, sub)
_, resp := th.Client.CreateSubscription(sub)
require.Error(t, resp.Error)
})
}
func TestGetSubscriptions(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
err := th.InitUsers("user1", "user2")
require.NoError(t, err, "failed to init users")
container := store.Container{
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
}
t.Run("Get subscriptions for user", func(t *testing.T) {
mySubs, user1ID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID)
mySubs, user1ID, err := createTestSubscriptions(th.Client, 5)
require.NoError(t, err)
require.Len(t, mySubs, 5)
// create more subscriptions with different user
otherSubs, _, err := createTestSubscriptions(th.Client2, 10, container.WorkspaceID)
otherSubs, _, err := createTestSubscriptions(th.Client2, 10)
require.NoError(t, err)
require.Len(t, otherSubs, 10)
// fetch the newly created subscriptions for current user, making sure only
// the ones created for the current user are returned.
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, user1ID)
subsFound, resp := th.Client.GetSubscriptions(user1ID)
require.NoError(t, resp.Error)
require.Len(t, subsFound, 5)
assert.ElementsMatch(t, mySubs, subsFound)
@ -138,23 +121,19 @@ func TestGetSubscriptions(t *testing.T) {
}
func TestDeleteSubscription(t *testing.T) {
th := SetupTestHelper().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
container := store.Container{
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
}
t.Run("Delete valid subscription", func(t *testing.T) {
subs, userID, err := createTestSubscriptions(th.Client, 3, container.WorkspaceID)
subs, userID, err := createTestSubscriptions(th.Client, 3)
require.NoError(t, err)
require.Len(t, subs, 3)
resp := th.Client.DeleteSubscription(container.WorkspaceID, subs[1].BlockID, userID)
resp := th.Client.DeleteSubscription(subs[1].BlockID, userID)
require.NoError(t, resp.Error)
// fetch the subscriptions and ensure the list is correct
subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID)
subsFound, resp := th.Client.GetSubscriptions(userID)
require.NoError(t, resp.Error)
require.Len(t, subsFound, 2)
@ -167,7 +146,7 @@ func TestDeleteSubscription(t *testing.T) {
user, resp := th.Client.GetMe()
require.NoError(t, resp.Error)
resp = th.Client.DeleteSubscription(container.WorkspaceID, "bogus", user.ID)
resp = th.Client.DeleteSubscription("bogus", user.ID)
require.Error(t, resp.Error)
})
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/mattermost/focalboard/server/api"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
"github.com/stretchr/testify/require"
)
@ -16,7 +17,7 @@ const (
)
func TestUserRegister(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).Start()
defer th.TearDown()
// register
@ -29,14 +30,14 @@ func TestUserRegister(t *testing.T) {
require.NoError(t, resp.Error)
require.True(t, success)
// register again will failed
// register again will fail
success, resp = th.Client.Register(registerRequest)
require.Error(t, resp.Error)
require.False(t, success)
}
func TestUserLogin(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).Start()
defer th.TearDown()
t.Run("with nonexist user", func(t *testing.T) {
@ -78,7 +79,7 @@ func TestUserLogin(t *testing.T) {
}
func TestGetMe(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).Start()
defer th.TearDown()
t.Run("not login yet", func(t *testing.T) {
@ -120,7 +121,7 @@ func TestGetMe(t *testing.T) {
}
func TestGetUser(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).Start()
defer th.TearDown()
// register
@ -165,7 +166,7 @@ func TestGetUser(t *testing.T) {
}
func TestUserChangePassword(t *testing.T) {
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).Start()
defer th.TearDown()
// register
@ -210,30 +211,58 @@ func randomBytes(t *testing.T, n int) []byte {
return bb
}
func TestWorkspaceUploadFile(t *testing.T) {
func TestTeamUploadFile(t *testing.T) {
t.Run("no permission", func(t *testing.T) { // native auth, but not login
th := SetupTestHelperWithoutToken().InitBasic()
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
workspaceID := "0"
rootID := utils.NewID(utils.IDTypeBlock)
teamID := "0"
boardID := utils.NewID(utils.IDTypeBoard)
data := randomBytes(t, 1024)
result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data))
result, resp := th.Client.TeamUploadFile(teamID, boardID, bytes.NewReader(data))
require.Error(t, resp.Error)
require.Nil(t, result)
})
t.Run("success", func(t *testing.T) { // single token auth
th := SetupTestHelper().InitBasic()
t.Run("a board admin should be able to update a file", func(t *testing.T) { // single token auth
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
workspaceID := "0"
rootID := utils.NewID(utils.IDTypeBlock)
teamID := "0"
newBoard := &model.Board{
Type: model.BoardTypeOpen,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NotNil(t, board)
data := randomBytes(t, 1024)
result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data))
require.NoError(t, resp.Error)
result, resp := th.Client.TeamUploadFile(teamID, board.ID, bytes.NewReader(data))
th.CheckOK(resp)
require.NotNil(t, result)
require.NotEmpty(t, result.FileID)
// TODO get the uploaded file
})
t.Run("user that doesn't belong to the board should not be able to upload a file", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
teamID := "0"
newBoard := &model.Board{
Type: model.BoardTypeOpen,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NotNil(t, board)
data := randomBytes(t, 1024)
// a user that doesn't belong to the board tries to upload the file
result, resp := th.Client2.TeamUploadFile(teamID, board.ID, bytes.NewReader(data))
th.CheckForbidden(resp)
require.Nil(t, result)
})
}

View File

@ -13,6 +13,7 @@ import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/server"
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
)
import (
"github.com/mattermost/mattermost-server/v6/shared/mlog"
@ -145,11 +146,14 @@ func main() {
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
}
permissionsService := localpermissions.New(db, logger)
params := server.Params{
Cfg: config,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
Cfg: config,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
PermissionsService: permissionsService,
}
server, err := server.New(params)
@ -233,11 +237,14 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
}
permissionsService := localpermissions.New(db, logger)
params := server.Params{
Cfg: config,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
Cfg: config,
SingleUserToken: singleUserToken,
DBStore: db,
Logger: logger,
PermissionsService: permissionsService,
}
pServer, err = server.New(params)

View File

@ -19,10 +19,6 @@ type Block struct {
// required: false
ParentID string `json:"parentId"`
// The id for this block's root block
// required: true
RootID string `json:"rootId"`
// The id for user who created this block
// required: true
CreatedBy string `json:"createdBy"`
@ -59,9 +55,13 @@ type Block struct {
// required: false
DeleteAt int64 `json:"deleteAt"`
// The workspace id that the block belongs to
// Deprecated. The workspace id that the block belongs to
// required: false
WorkspaceID string `json:"-"`
// The board id that the block belongs to
// required: true
WorkspaceID string `json:"workspaceId"`
BoardID string `json:"boardId"`
}
// BlockPatch is a patch for modify blocks
@ -71,10 +71,6 @@ type BlockPatch struct {
// required: false
ParentID *string `json:"parentId"`
// The id for this block's root block
// required: false
RootID *string `json:"rootId"`
// The schema version of this block
// required: false
Schema *int64 `json:"schema"`
@ -94,6 +90,10 @@ type BlockPatch struct {
// The block removed fields
// required: false
DeletedFields []string `json:"deletedFields"`
// The board id that the block belongs to
// required: false
BoardID *string `json:"boardId"`
}
// BlockPatchBatch is a batch of IDs and patches for modify blocks
@ -106,11 +106,11 @@ type BlockPatchBatch struct {
BlockPatches []BlockPatch `json:"block_patches"`
}
// BlockModifier is a callback that can modify each block during an import.
// BoardModifier is a callback that can modify each board during an import.
// A cache of arbitrary data will be passed for each call and any changes
// to the cache will be preserved for the next call.
// Return true to import the block or false to skip import.
type BlockModifier func(block *Block, cache map[string]interface{}) bool
type BoardModifier func(board *Board, cache map[string]interface{}) bool
func BlocksFromJSON(data io.Reader) []Block {
var blocks []Block
@ -123,12 +123,12 @@ func (b Block) LogClone() interface{} {
return struct {
ID string
ParentID string
RootID string
BoardID string
Type BlockType
}{
ID: b.ID,
ParentID: b.ParentID,
RootID: b.RootID,
BoardID: b.BoardID,
Type: b.Type,
}
}
@ -139,8 +139,8 @@ func (p *BlockPatch) Patch(block *Block) *Block {
block.ParentID = *p.ParentID
}
if p.RootID != nil {
block.RootID = *p.RootID
if p.BoardID != nil {
block.BoardID = *p.BoardID
}
if p.Schema != nil {

View File

@ -20,61 +20,61 @@ func TestGenerateBlockIDs(t *testing.T) {
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID, blocks[0].ID)
require.Zero(t, blocks[0].RootID)
require.Zero(t, blocks[0].BoardID)
require.Zero(t, blocks[0].ParentID)
})
t.Run("Should generate a new ID for a single block with references", func(t *testing.T) {
blockID := utils.NewID(utils.IDTypeBlock)
rootID := utils.NewID(utils.IDTypeBlock)
boardID := utils.NewID(utils.IDTypeBlock)
parentID := utils.NewID(utils.IDTypeBlock)
blocks := []Block{{ID: blockID, RootID: rootID, ParentID: parentID}}
blocks := []Block{{ID: blockID, BoardID: boardID, ParentID: parentID}}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID, blocks[0].ID)
require.Equal(t, rootID, blocks[0].RootID)
require.Equal(t, boardID, blocks[0].BoardID)
require.Equal(t, parentID, blocks[0].ParentID)
})
t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
rootID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
blockID2 := utils.NewID(utils.IDTypeBlock)
rootID2 := blockID1
boardID2 := blockID1
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
blocks := []Block{block1, block2}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, rootID1, blocks[0].RootID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.NotEqual(t, rootID2, blocks[1].RootID)
require.NotEqual(t, boardID2, blocks[1].BoardID)
require.Equal(t, parentID2, blocks[1].ParentID)
// blockID1 was referenced, so it should still be after the ID
// changes
require.Equal(t, blocks[0].ID, blocks[1].RootID)
require.Equal(t, blocks[0].ID, blocks[1].BoardID)
})
t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
rootID1 := ""
boardID1 := ""
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
blockID2 := utils.NewID(utils.IDTypeBlock)
rootID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := ""
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
blocks := []Block{block1, block2}
@ -82,37 +82,37 @@ func TestGenerateBlockIDs(t *testing.T) {
// only the IDs should have changed
require.NotEqual(t, blockID1, blocks[0].ID)
require.Zero(t, blocks[0].RootID)
require.Zero(t, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.Equal(t, rootID2, blocks[1].RootID)
require.Equal(t, boardID2, blocks[1].BoardID)
require.Zero(t, blocks[1].ParentID)
})
t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
rootID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1}
block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
// linked to 1
blockID2 := utils.NewID(utils.IDTypeBlock)
rootID2 := blockID1
boardID2 := blockID1
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2}
block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
// linked to 2
blockID3 := utils.NewID(utils.IDTypeBlock)
rootID3 := blockID2
boardID3 := blockID2
parentID3 := utils.NewID(utils.IDTypeBlock)
block3 := Block{ID: blockID3, RootID: rootID3, ParentID: parentID3}
block3 := Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3}
// linked to 1
blockID4 := utils.NewID(utils.IDTypeBlock)
rootID4 := blockID1
boardID4 := blockID1
parentID4 := utils.NewID(utils.IDTypeBlock)
block4 := Block{ID: blockID4, RootID: rootID4, ParentID: parentID4}
block4 := Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4}
// blocks are shuffled
blocks := []Block{block4, block2, block1, block3}
@ -121,44 +121,44 @@ func TestGenerateBlockIDs(t *testing.T) {
// block 1
require.NotEqual(t, blockID1, blocks[2].ID)
require.Equal(t, rootID1, blocks[2].RootID)
require.Equal(t, boardID1, blocks[2].BoardID)
require.Equal(t, parentID1, blocks[2].ParentID)
// block 2
require.NotEqual(t, blockID2, blocks[1].ID)
require.NotEqual(t, rootID2, blocks[1].RootID)
require.Equal(t, blocks[2].ID, blocks[1].RootID) // link to 1
require.NotEqual(t, boardID2, blocks[1].BoardID)
require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1
require.Equal(t, parentID2, blocks[1].ParentID)
// block 3
require.NotEqual(t, blockID3, blocks[3].ID)
require.NotEqual(t, rootID3, blocks[3].RootID)
require.Equal(t, blocks[1].ID, blocks[3].RootID) // link to 2
require.NotEqual(t, boardID3, blocks[3].BoardID)
require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2
require.Equal(t, parentID3, blocks[3].ParentID)
// block 4
require.NotEqual(t, blockID4, blocks[0].ID)
require.NotEqual(t, rootID4, blocks[0].RootID)
require.Equal(t, blocks[2].ID, blocks[0].RootID) // link to 1
require.NotEqual(t, boardID4, blocks[0].BoardID)
require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1
require.Equal(t, parentID4, blocks[0].ParentID)
})
t.Run("Should update content order", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
rootID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := Block{
ID: blockID1,
RootID: rootID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID2 := utils.NewID(utils.IDTypeBlock)
rootID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := Block{
ID: blockID2,
RootID: rootID2,
BoardID: boardID2,
ParentID: parentID2,
Fields: map[string]interface{}{
"contentOrder": []interface{}{
@ -172,11 +172,11 @@ func TestGenerateBlockIDs(t *testing.T) {
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, rootID1, blocks[0].RootID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.Equal(t, rootID2, blocks[1].RootID)
require.Equal(t, boardID2, blocks[1].BoardID)
require.Equal(t, parentID2, blocks[1].ParentID)
// since block 1 was referenced in block 2,
@ -189,35 +189,35 @@ func TestGenerateBlockIDs(t *testing.T) {
t.Run("Should update content order when it contain slices", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
rootID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := Block{
ID: blockID1,
RootID: rootID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID2 := utils.NewID(utils.IDTypeBlock)
block2 := Block{
ID: blockID2,
RootID: rootID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID3 := utils.NewID(utils.IDTypeBlock)
block3 := Block{
ID: blockID3,
RootID: rootID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID4 := utils.NewID(utils.IDTypeBlock)
rootID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := utils.NewID(utils.IDTypeBlock)
block4 := Block{
ID: blockID4,
RootID: rootID2,
BoardID: boardID2,
ParentID: parentID2,
Fields: map[string]interface{}{
"contentOrder": []interface{}{
@ -235,11 +235,11 @@ func TestGenerateBlockIDs(t *testing.T) {
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, rootID1, blocks[0].RootID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID4, blocks[3].ID)
require.Equal(t, rootID2, blocks[3].RootID)
require.Equal(t, boardID2, blocks[3].BoardID)
require.Equal(t, parentID2, blocks[3].ParentID)
// since block 1 was referenced in block 2,

View File

@ -20,8 +20,8 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block {
blockIDs[block.ID] = block.Type
}
if _, ok := referenceIDs[block.RootID]; !ok {
referenceIDs[block.RootID] = true
if _, ok := referenceIDs[block.BoardID]; !ok {
referenceIDs[block.BoardID] = true
}
if _, ok := referenceIDs[block.ParentID]; !ok {
referenceIDs[block.ParentID] = true
@ -81,7 +81,7 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block {
newBlocks := make([]Block, len(blocks))
for i, block := range blocks {
block.ID = getExistingOrNewID(block.ID)
block.RootID = getExistingOrOldID(block.RootID)
block.BoardID = getExistingOrOldID(block.BoardID)
block.ParentID = getExistingOrOldID(block.ParentID)
blockMod := block

304
server/model/board.go Normal file
View File

@ -0,0 +1,304 @@
package model
import (
"encoding/json"
"io"
)
type BoardType string
const (
BoardTypeOpen BoardType = "O"
BoardTypePrivate BoardType = "P"
)
// Board groups a set of blocks and its layout
// swagger:model
type Board struct {
// The ID for the board
// required: true
ID string `json:"id"`
// The ID of the team that the board belongs to
// required: true
TeamID string `json:"teamId"`
// The ID of the channel that the board was created from
// required: false
ChannelID string `json:"channelId"`
// The ID of the user that created the board
// required: true
CreatedBy string `json:"createdBy"`
// The ID of the last user that updated the board
// required: true
ModifiedBy string `json:"modifiedBy"`
// The type of the board
// required: true
Type BoardType `json:"type"`
// The title of the board
// required: false
Title string `json:"title"`
// The description of the board
// required: false
Description string `json:"description"`
// The icon of the board
// required: false
Icon string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription bool `json:"showDescription"`
// Marks the template boards
// required: false
IsTemplate bool `json:"isTemplate"`
// Marks the template boards
// required: false
TemplateVersion int `json:"templateVersion"`
// The properties of the board
// required: false
Properties map[string]interface{} `json:"properties"`
// The properties of the board cards
// required: false
CardProperties []map[string]interface{} `json:"cardProperties"`
// The calculations on the board's cards
// required: false
ColumnCalculations map[string]interface{} `json:"columnCalculations"`
// The creation time
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time. Set to indicate this block is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
}
// BoardPatch is a patch for modify boards
// swagger:model
type BoardPatch struct {
// The type of the board
// required: false
Type *BoardType `json:"type"`
// The title of the board
// required: false
Title *string `json:"title"`
// The description of the board
// required: false
Description *string `json:"description"`
// The icon of the board
// required: false
Icon *string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription *bool `json:"showDescription"`
// The board updated properties
// required: false
UpdatedProperties map[string]interface{} `json:"updatedProperties"`
// The board removed properties
// required: false
DeletedProperties []string `json:"deletedProperties"`
// The board updated card properties
// required: false
UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"`
// The board removed card properties
// required: false
DeletedCardProperties []string `json:"deletedCardProperties"`
// The board updated column calculations
// required: false
UpdatedColumnCalculations map[string]interface{} `json:"updatedColumnCalculations"`
// The board deleted column calculations
// required: false
DeletedColumnCalculations []string `json:"deletedColumnCalculations"`
}
// BoardMember stores the information of the membership of a user on a board
// swagger:model
type BoardMember struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The independent roles of the user on the board
// required: false
Roles string `json:"roles"`
// Marks the user as an admin of the board
// required: true
SchemeAdmin bool `json:"schemeAdmin"`
// Marks the user as an editor of the board
// required: true
SchemeEditor bool `json:"schemeEditor"`
// Marks the user as an commenter of the board
// required: true
SchemeCommenter bool `json:"schemeCommenter"`
// Marks the user as an viewer of the board
// required: true
SchemeViewer bool `json:"schemeViewer"`
}
func BoardFromJSON(data io.Reader) *Board {
var board *Board
_ = json.NewDecoder(data).Decode(&board)
return board
}
func BoardsFromJSON(data io.Reader) []*Board {
var boards []*Board
_ = json.NewDecoder(data).Decode(&boards)
return boards
}
func BoardMemberFromJSON(data io.Reader) *BoardMember {
var boardMember *BoardMember
_ = json.NewDecoder(data).Decode(&boardMember)
return boardMember
}
func BoardMembersFromJSON(data io.Reader) []*BoardMember {
var boardMembers []*BoardMember
_ = json.NewDecoder(data).Decode(&boardMembers)
return boardMembers
}
// Patch returns an updated version of the board.
func (p *BoardPatch) Patch(board *Board) *Board {
if p.Type != nil {
board.Type = *p.Type
}
if p.Title != nil {
board.Title = *p.Title
}
if p.Description != nil {
board.Description = *p.Description
}
if p.Icon != nil {
board.Icon = *p.Icon
}
if p.ShowDescription != nil {
board.ShowDescription = *p.ShowDescription
}
for key, property := range p.UpdatedProperties {
board.Properties[key] = property
}
for _, key := range p.DeletedProperties {
delete(board.Properties, key)
}
if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 {
// first we accumulate all properties indexed by ID
cardPropertyMap := map[string]map[string]interface{}{}
for _, prop := range board.CardProperties {
id, ok := prop["id"].(string)
if !ok {
// bad property, skipping
continue
}
cardPropertyMap[id] = prop
}
// if there are properties marked for removal, we delete them
for _, propertyID := range p.DeletedCardProperties {
delete(cardPropertyMap, propertyID)
}
// if there are properties marked for update, we replace the
// existing ones or add them
for _, newprop := range p.UpdatedCardProperties {
id, ok := newprop["id"].(string)
if !ok {
// bad new property, skipping
continue
}
cardPropertyMap[id] = newprop
}
// and finally we flatten and save the updated properties
newCardProperties := []map[string]interface{}{}
for _, p := range cardPropertyMap {
newCardProperties = append(newCardProperties, p)
}
board.CardProperties = newCardProperties
}
for key, columnCalculation := range p.UpdatedColumnCalculations {
board.ColumnCalculations[key] = columnCalculation
}
for _, key := range p.DeletedColumnCalculations {
delete(board.ColumnCalculations, key)
}
return board
}
func IsBoardTypeValid(t BoardType) bool {
return t == BoardTypeOpen || t == BoardTypePrivate
}
func (p *BoardPatch) IsValid() error {
if p.Type != nil && !IsBoardTypeValid(*p.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
return nil
}
type InvalidBoardErr struct {
msg string
}
func (ibe InvalidBoardErr) Error() string {
return ibe.msg
}
func (b *Board) IsValid() error {
if b.TeamID == "" {
return InvalidBoardErr{"empty-team-id"}
}
if !IsBoardTypeValid(b.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
return nil
}

View File

@ -0,0 +1,164 @@
package model
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/mattermost/focalboard/server/utils"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required")
var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required")
var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty")
var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match")
var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match")
type BlockDoesntBelongToAnyBoardErr struct {
blockID string
}
func (e BlockDoesntBelongToAnyBoardErr) Error() string {
return fmt.Sprintf("block %s doesn't belong to any board", e.blockID)
}
// BoardsAndBlocks is used to operate over boards and blocks at the
// same time
// swagger:model
type BoardsAndBlocks struct {
// The boards
// required: false
Boards []*Board `json:"boards"`
// The blocks
// required: false
Blocks []Block `json:"blocks"`
}
func (bab *BoardsAndBlocks) IsValid() error {
if len(bab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(bab.Blocks) == 0 {
return ErrNoBlocksInBoardsAndBlocks
}
boardsMap := map[string]bool{}
for _, board := range bab.Boards {
boardsMap[board.ID] = true
}
for _, block := range bab.Blocks {
if _, ok := boardsMap[block.BoardID]; !ok {
return BlockDoesntBelongToAnyBoardErr{block.ID}
}
}
return nil
}
// DeleteBoardsAndBlocks is used to list the boards and blocks to
// delete on a request
// swagger:model
type DeleteBoardsAndBlocks struct {
// The boards
// required: true
Boards []string `json:"boards"`
// The blocks
// required: true
Blocks []string `json:"blocks"`
}
func (dbab *DeleteBoardsAndBlocks) IsValid() error {
if len(dbab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(dbab.Blocks) == 0 {
return ErrNoBlocksInBoardsAndBlocks
}
return nil
}
// PatchBoardsAndBlocks is used to patch multiple boards and blocks on
// a single request
// swagger:model
type PatchBoardsAndBlocks struct {
// The board IDs to patch
// required: true
BoardIDs []string `json:"boardIDs"`
// The board patches
// required: true
BoardPatches []*BoardPatch `json:"boardPatches"`
// The block IDs to patch
// required: true
BlockIDs []string `json:"blockIDs"`
// The block patches
// required: true
BlockPatches []*BlockPatch `json:"blockPatches"`
}
func (dbab *PatchBoardsAndBlocks) IsValid() error {
if len(dbab.BoardIDs) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(dbab.BoardIDs) != len(dbab.BoardPatches) {
return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks
}
if len(dbab.BlockIDs) == 0 {
return ErrNoBlocksInBoardsAndBlocks
}
if len(dbab.BlockIDs) != len(dbab.BlockPatches) {
return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks
}
return nil
}
func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger *mlog.Logger) (*BoardsAndBlocks, error) {
if err := bab.IsValid(); err != nil {
return nil, err
}
blocksByBoard := map[string][]Block{}
for _, block := range bab.Blocks {
blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block)
}
boards := []*Board{}
blocks := []Block{}
for _, board := range bab.Boards {
newID := utils.NewID(utils.IDTypeBoard)
for _, block := range blocksByBoard[board.ID] {
block.BoardID = newID
blocks = append(blocks, block)
}
board.ID = newID
boards = append(boards, board)
}
newBab := &BoardsAndBlocks{
Boards: boards,
Blocks: GenerateBlockIDs(blocks, logger),
}
return newBab, nil
}
func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks {
var bab *BoardsAndBlocks
_ = json.NewDecoder(data).Decode(&bab)
return bab
}

View File

@ -0,0 +1,264 @@
package model
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func TestIsValidBoardsAndBlocks(t *testing.T) {
t.Run("no boards", func(t *testing.T) {
bab := &BoardsAndBlocks{
Blocks: []Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
})
t.Run("no blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
}
require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks)
})
t.Run("block that doesn't belong to the boards", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
Blocks: []Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"})
})
t.Run("valid boards and blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
Blocks: []Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.NoError(t, bab.IsValid())
})
}
func TestGenerateBoardsAndBlocksIDs(t *testing.T) {
logger, err := mlog.NewLogger()
require.NoError(t, err)
getBlockByType := func(blocks []Block, blockType BlockType) Block {
for _, b := range blocks {
if b.Type == blockType {
return b
}
}
return Block{}
}
getBoardByTitle := func(boards []*Board, title string) *Board {
for _, b := range boards {
if b.Title == title {
return b
}
}
return nil
}
t.Run("invalid boards and blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Blocks: []Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
require.Error(t, err)
require.Nil(t, rBab)
})
t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"},
{ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"},
{ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"},
},
Blocks: []Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeView},
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeText},
},
}
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
require.NoError(t, err)
require.NotNil(t, rBab)
// all boards and blocks should have refreshed their IDs, and
// blocks should be correctly linked to the new board IDs
board1 := getBoardByTitle(rBab.Boards, "board1")
require.NotNil(t, board1)
require.NotEmpty(t, board1.ID)
require.NotEqual(t, "board-id-1", board1.ID)
board2 := getBoardByTitle(rBab.Boards, "board2")
require.NotNil(t, board2)
require.NotEmpty(t, board2.ID)
require.NotEqual(t, "board-id-2", board2.ID)
board3 := getBoardByTitle(rBab.Boards, "board3")
require.NotNil(t, board3)
require.NotEmpty(t, board3.ID)
require.NotEqual(t, "board-id-3", board3.ID)
block1 := getBlockByType(rBab.Blocks, TypeCard)
require.NotNil(t, block1)
require.NotEmpty(t, block1.ID)
require.NotEqual(t, "block-id-1", block1.ID)
require.Equal(t, board1.ID, block1.BoardID)
block2 := getBlockByType(rBab.Blocks, TypeView)
require.NotNil(t, block2)
require.NotEmpty(t, block2.ID)
require.NotEqual(t, "block-id-2", block2.ID)
require.Equal(t, board2.ID, block2.BoardID)
block3 := getBlockByType(rBab.Blocks, TypeText)
require.NotNil(t, block3)
require.NotEmpty(t, block3.ID)
require.NotEqual(t, "block-id-3", block3.ID)
require.Equal(t, board2.ID, block3.BoardID)
})
}
func TestIsValidPatchBoardsAndBlocks(t *testing.T) {
newTitle := "new title"
newDescription := "new description"
var schema int64 = 1
t.Run("no board ids", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
{Schema: &schema},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
})
t.Run("missmatch board IDs and patches", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks)
})
t.Run("no block ids", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
{Description: &newDescription},
},
BlockIDs: []string{},
}
require.ErrorIs(t, pbab.IsValid(), ErrNoBlocksInBoardsAndBlocks)
})
t.Run("missmatch block IDs and patches", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
{Description: &newDescription},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
{Schema: &schema},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks)
})
t.Run("valid", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
{Description: &newDescription},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
},
}
require.NoError(t, pbab.IsValid())
})
}
func TestIsValidDeleteBoardsAndBlocks(t *testing.T) {
/*
TODO fix this
t.Run("no board ids", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Blocks: []string{"block-id-1"},
}
require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr)
})
t.Run("no block ids", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Boards: []string{"board-id-1", "board-id-2"},
}
require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr)
})
t.Run("valid", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Boards: []string{"board-id-1", "board-id-2"},
Blocks: []string{"block-id-1"},
}
require.NoError(t, dbab.IsValid())
})
*/
}

57
server/model/category.go Normal file
View File

@ -0,0 +1,57 @@
package model
import (
"strings"
"github.com/mattermost/focalboard/server/utils"
)
type Category struct {
ID string `json:"id"`
Name string `json:"name"`
UserID string `json:"userID"`
TeamID string `json:"teamID"`
CreateAt int64 `json:"createAt"`
UpdateAt int64 `json:"updateAt"`
DeleteAt int64 `json:"deleteAt"`
}
func (c *Category) Hydrate() {
c.ID = utils.NewID(utils.IDTypeNone)
c.CreateAt = utils.GetMillis()
c.UpdateAt = c.CreateAt
}
func (c *Category) IsValid() error {
if strings.TrimSpace(c.ID) == "" {
return newErrInvalidCategory("category ID cannot be empty")
}
if strings.TrimSpace(c.Name) == "" {
return newErrInvalidCategory("category name cannot be empty")
}
if strings.TrimSpace(c.UserID) == "" {
return newErrInvalidCategory("category user ID cannot be empty")
}
if strings.TrimSpace(c.TeamID) == "" {
return newErrInvalidCategory("category team id ID cannot be empty")
}
return nil
}
type ErrInvalidCategory struct {
msg string
}
func newErrInvalidCategory(msg string) *ErrInvalidCategory {
return &ErrInvalidCategory{
msg: msg,
}
}
func (e *ErrInvalidCategory) Error() string {
return e.msg
}

View File

@ -0,0 +1,11 @@
package model
type CategoryBlocks struct {
Category
BlockIDs []string `json:"blockIDs"`
}
type BlockCategoryWebsocketData struct {
BlockID string `json:"blockID"`
CategoryID string `json:"categoryID"`
}

7
server/model/database.go Normal file
View File

@ -0,0 +1,7 @@
package model
const (
SqliteDBType = "sqlite3"
PostgresDBType = "postgres"
MysqlDBType = "mysql"
)

View File

@ -33,7 +33,7 @@ type ArchiveLine struct {
// ExportArchiveOptions provides options when exporting one or more boards
// to an archive.
type ExportArchiveOptions struct {
WorkspaceID string
TeamID string
// BoardIDs is the list of boards to include in the archive.
// Empty slice means export all boards from workspace/team.
@ -42,9 +42,9 @@ type ExportArchiveOptions struct {
// ImportArchiveOptions provides options when importing an archive.
type ImportArchiveOptions struct {
WorkspaceID string
TeamID string
ModifiedBy string
BlockModifier BlockModifier
BoardModifier BoardModifier
}
// ErrUnsupportedArchiveVersion is an error returned when trying to import an

View File

@ -18,10 +18,6 @@ type NotificationHint struct {
// required: true
BlockID string `json:"block_id"`
// WorkspaceID is id of workspace the block belongs to
// required: true
WorkspaceID string `json:"workspace_id"`
// ModifiedByID is the id of the user who made the block change
ModifiedByID string `json:"modified_by_id"`
@ -41,9 +37,6 @@ func (s *NotificationHint) IsValid() error {
if s.BlockID == "" {
return ErrInvalidNotificationHint{"missing block id"}
}
if s.WorkspaceID == "" {
return ErrInvalidNotificationHint{"missing workspace id"}
}
if s.BlockType == "" {
return ErrInvalidNotificationHint{"missing block type"}
}
@ -57,7 +50,6 @@ func (s *NotificationHint) Copy() *NotificationHint {
return &NotificationHint{
BlockType: s.BlockType,
BlockID: s.BlockID,
WorkspaceID: s.WorkspaceID,
ModifiedByID: s.ModifiedByID,
CreateAt: s.CreateAt,
NotifyAt: s.NotifyAt,
@ -68,14 +60,12 @@ func (s *NotificationHint) LogClone() interface{} {
return struct {
BlockType BlockType `json:"block_type"`
BlockID string `json:"block_id"`
WorkspaceID string `json:"workspace_id"`
ModifiedByID string `json:"modified_by_id"`
CreateAt string `json:"create_at"`
NotifyAt string `json:"notify_at"`
}{
BlockType: s.BlockType,
BlockID: s.BlockID,
WorkspaceID: s.WorkspaceID,
ModifiedByID: s.ModifiedByID,
CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli),
NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli),

View File

@ -0,0 +1,19 @@
package model
import (
mmModel "github.com/mattermost/mattermost-server/v6/model"
)
var (
PermissionViewTeam = mmModel.PermissionViewTeam
PermissionViewMembers = mmModel.PermissionViewMembers
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
PermissionCreatePrivateChannel = mmModel.PermissionCreatePrivateChannel
PermissionManageBoardType = &mmModel.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""}
PermissionDeleteBoard = &mmModel.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""}
PermissionViewBoard = &mmModel.Permission{Id: "view_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardRoles = &mmModel.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""}
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
)

View File

@ -147,30 +147,10 @@ func (pd PropDef) ParseDate(s string) (string, error) {
// schema for all cards within the board.
// The result is provided as a map for quick lookup, and the original order is
// preserved via the `Index` field.
func ParsePropertySchema(board *Block) (PropSchema, error) {
if board == nil || board.Type != TypeBoard {
return nil, ErrInvalidBoardBlock
}
func ParsePropertySchema(board *Board) (PropSchema, error) {
schema := make(map[string]PropDef)
// cardProperties contains a slice of maps (untyped at this point).
cardPropsIface, ok := board.Fields["cardProperties"]
if !ok {
return schema, nil
}
cardProps, ok := cardPropsIface.([]interface{})
if !ok || len(cardProps) == 0 {
return schema, nil
}
for i, cp := range cardProps {
prop, ok := cp.(map[string]interface{})
if !ok {
return nil, ErrInvalidPropSchema
}
for i, prop := range board.CardProperties {
pd := PropDef{
ID: getMapString("id", prop),
Index: i,

View File

@ -13,14 +13,13 @@ import (
)
func Test_parsePropertySchema(t *testing.T) {
board := &Block{
ID: utils.NewID(utils.IDTypeBoard),
Type: TypeBoard,
Title: "Test Board",
WorkspaceID: utils.NewID(utils.IDTypeWorkspace),
board := &Board{
ID: utils.NewID(utils.IDTypeBoard),
Title: "Test Board",
TeamID: utils.NewID(utils.IDTypeTeam),
}
err := json.Unmarshal([]byte(fieldsExample), &board.Fields)
err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties)
require.NoError(t, err)
t.Run("parse schema", func(t *testing.T) {
@ -46,93 +45,74 @@ func Test_parsePropertySchema(t *testing.T) {
}
const (
fieldsExample = `
{
"cardProperties":[
{
"id":"7c212e78-9345-4c60-81b5-0b0e37ce463f",
"name":"Type",
"options":[
{
"color":"propColorYellow",
"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23",
"value":"Ad Hoc"
},
{
"color":"propColorBlue",
"id":"def6317c-ec11-410d-8a6b-ea461320f392",
"value":"Standup"
},
{
"color":"propColorPurple",
"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7",
"value":"Weekly Sync"
}
],
"type":"select"
},
{
"id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4",
"name":"Summary",
"options":[
],
"type":"text"
},
{
"id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c",
"name":"Color",
"options":[
{
"color":"propColorDefault",
"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1",
"value":"RED"
},
{
"color":"propColorDefault",
"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311",
"value":"GREEN"
},
{
"color":"propColorDefault",
"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be",
"value":"BLUE"
}
],
"type":"select"
},
{
"id":"aawg1s8rxq8o1bbksxmsmpsdd3r",
"name":"MyTextProp",
"options":[
],
"type":"text"
},
{
"id":"awdwfigo4kse63bdfp56mzhip6w",
"name":"MyCheckBox",
"options":[
],
"type":"checkbox"
},
{
"id":"a8spou7if43eo1rqzb9qeq488so",
"name":"MyDate",
"options":[
],
"type":"date"
}
],
"columnCalculations":[
],
"description":"",
"icon":"🗒️",
"isTemplate":false,
"showDescription":false
}
`
cardPropertiesExample = `[
{
"id":"7c212e78-9345-4c60-81b5-0b0e37ce463f",
"name":"Type",
"options":[
{
"color":"propColorYellow",
"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23",
"value":"Ad Hoc"
},
{
"color":"propColorBlue",
"id":"def6317c-ec11-410d-8a6b-ea461320f392",
"value":"Standup"
},
{
"color":"propColorPurple",
"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7",
"value":"Weekly Sync"
}
],
"type":"select"
},
{
"id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4",
"name":"Summary",
"options":[],
"type":"text"
},
{
"id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c",
"name":"Color",
"options":[
{
"color":"propColorDefault",
"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1",
"value":"RED"
},
{
"color":"propColorDefault",
"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311",
"value":"GREEN"
},
{
"color":"propColorDefault",
"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be",
"value":"BLUE"
}
],
"type":"select"
},
{
"id":"aawg1s8rxq8o1bbksxmsmpsdd3r",
"name":"MyTextProp",
"options":[],
"type":"text"
},
{
"id":"awdwfigo4kse63bdfp56mzhip6w",
"name":"MyCheckBox",
"options":[],
"type":"checkbox"
},
{
"id":"a8spou7if43eo1rqzb9qeq488so",
"name":"MyDate",
"options":[],
"type":"date"
}
]`
)

View File

@ -31,10 +31,6 @@ type Subscription struct {
// required: true
BlockID string `json:"blockId"`
// WorkspaceID is id of the workspace the block belongs to
// required: true
WorkspaceID string `json:"workspaceId"`
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
// required: true
SubscriberType SubscriberType `json:"subscriberType"`
@ -63,9 +59,6 @@ func (s *Subscription) IsValid() error {
if s.BlockID == "" {
return ErrInvalidSubscription{"missing block id"}
}
if s.WorkspaceID == "" {
return ErrInvalidSubscription{"missing workspace id"}
}
if s.BlockType == "" {
return ErrInvalidSubscription{"missing block type"}
}

46
server/model/team.go Normal file
View File

@ -0,0 +1,46 @@
package model
import (
"encoding/json"
"io"
)
// Team is information global to a team
// swagger:model
type Team struct {
// ID of the team
// required: true
ID string `json:"id"`
// Title of the team
// required: false
Title string `json:"title"`
// Token required to register new users
// required: true
SignupToken string `json:"signupToken"`
// Team settings
// required: false
Settings map[string]interface{} `json:"settings"`
// ID of user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time
// required: true
UpdateAt int64 `json:"updateAt"`
}
func TeamFromJSON(data io.Reader) *Team {
var team *Team
_ = json.NewDecoder(data).Decode(&team)
return team
}
func TeamsFromJSON(data io.Reader) []*Team {
var teams []*Team
_ = json.NewDecoder(data).Decode(&teams)
return teams
}

View File

@ -57,6 +57,8 @@ type User struct {
IsBot bool `json:"is_bot"`
}
// UserPropPatch is a user property patch
// swagger:model
type UserPropPatch struct {
// The user prop updated fields
// required: false

View File

@ -1,45 +0,0 @@
package model
// Workspace is information global to a workspace
// swagger:model
type Workspace struct {
// ID of the workspace
// required: true
ID string `json:"id"`
// Title of the workspace
// required: false
Title string `json:"title"`
// Token required to register new users
// required: true
SignupToken string `json:"signupToken"`
// Workspace settings
// required: false
Settings map[string]interface{} `json:"settings"`
// ID of user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time
// required: true
UpdateAt int64 `json:"updateAt"`
}
// UserWorkspace is a summary of a single association between
// a user and a workspace
// swagger:model
type UserWorkspace struct {
// ID of the workspace
// required: true
ID string `json:"id"`
// Title of the workspace
// required: false
Title string `json:"title"`
// Number of boards in the workspace
BoardCount int `json:"boardCount"`
}

View File

@ -5,6 +5,7 @@ import (
"github.com/mattermost/focalboard/server/services/config"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/ws"
@ -12,13 +13,14 @@ import (
)
type Params struct {
Cfg *config.Configuration
SingleUserToken string
DBStore store.Store
Logger *mlog.Logger
ServerID string
WSAdapter ws.Adapter
NotifyBackends []notify.Backend
Cfg *config.Configuration
SingleUserToken string
DBStore store.Store
Logger *mlog.Logger
ServerID string
WSAdapter ws.Adapter
NotifyBackends []notify.Backend
PermissionsService permissions.PermissionsService
}
func (p Params) CheckValid() error {
@ -33,6 +35,10 @@ func (p Params) CheckValid() error {
if p.Logger == nil {
return ErrServerParam{name: "Logger", issue: "cannot be nil"}
}
if p.PermissionsService == nil {
return ErrServerParam{name: "Permissions", issue: "cannot be nil"}
}
return nil
}

View File

@ -74,12 +74,12 @@ func New(params Params) (*Server, error) {
return nil, err
}
authenticator := auth.New(params.Cfg, params.DBStore)
authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService)
// if no ws adapter is provided, we spin up a websocket server
wsAdapter := params.WSAdapter
if wsAdapter == nil {
wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger)
wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore)
}
filesBackendSettings := filestore.FileBackendSettings{}
@ -137,18 +137,19 @@ func New(params Params) (*Server, error) {
Metrics: metricsService,
Notifications: notificationService,
Logger: params.Logger,
Permissions: params.PermissionsService,
}
app := app.New(params.Cfg, wsAdapter, appServices)
focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.Logger, auditService)
focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService)
// Local router for admin APIs
localRouter := mux.NewRouter()
focalboardAPI.RegisterAdminRoutes(localRouter)
// Init workspace
if _, err := app.GetRootWorkspace(); err != nil {
params.Logger.Error("Unable to get root workspace", mlog.Err(err))
// Init team
if _, err := app.GetRootTeam(); err != nil {
params.Logger.Error("Unable to get root team", mlog.Err(err))
return nil, err
}
@ -202,6 +203,11 @@ func New(params Params) (*Server, error) {
server.initHandlers()
if err := app.InitTemplates(); err != nil {
params.Logger.Error("Unable initialize team templates", mlog.Err(err))
return nil, err
}
return &server, nil
}
@ -272,13 +278,13 @@ func (s *Server) Start() error {
for blockType, count := range blockCounts {
s.metricsService.ObserveBlockCount(blockType, count)
}
workspaceCount, err := s.store.GetWorkspaceCount()
teamCount, err := s.store.GetTeamCount()
if err != nil {
s.logger.Error("Error updating metrics", mlog.String("group", "workspaces"), mlog.Err(err))
s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err))
return
}
s.logger.Log(mlog.LvlFBMetrics, "Workspace metrics collected", mlog.Int64("workspace_count", workspaceCount))
s.metricsService.ObserveWorkspaceCount(workspaceCount)
s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount))
s.metricsService.ObserveTeamCount(teamCount)
}
// metricsUpdater() Calling this immediately causes integration unit tests to fail.
s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency)
@ -336,6 +342,8 @@ func (s *Server) Shutdown() error {
s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err))
}
s.app.Shutdown()
defer s.logger.Info("Server.Shutdown")
return s.store.Shutdown()
@ -472,13 +480,13 @@ func initTelemetry(opts telemetryOptions) *telemetry.Service {
}
return m, nil
})
telemetryService.RegisterTracker("workspaces", func() (telemetry.Tracker, error) {
count, err := opts.app.GetWorkspaceCount()
telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) {
count, err := opts.app.GetTeamCount()
if err != nil {
return nil, err
}
m := map[string]interface{}{
"workspaces": count,
"teams": count,
}
return m, nil
})

View File

@ -7,15 +7,15 @@ import (
const (
DefMaxQueueSize = 1000
KeyAPIPath = "api_path"
KeyEvent = "event"
KeyStatus = "status"
KeyUserID = "user_id"
KeySessionID = "session_id"
KeyClient = "client"
KeyIPAddress = "ip_address"
KeyClusterID = "cluster_id"
KeyWorkspaceID = "workspace_id"
KeyAPIPath = "api_path"
KeyEvent = "event"
KeyStatus = "status"
KeyUserID = "user_id"
KeySessionID = "session_id"
KeyClient = "client"
KeyIPAddress = "ip_address"
KeyClusterID = "cluster_id"
KeyTeamID = "team_id"
Success = "success"
Attempt = "attempt"

View File

@ -2,7 +2,6 @@ package auth
import "regexp"
//nolint:lll
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
// IsEmailValid checks if the email provided passes the required structure and length.

View File

@ -8,10 +8,10 @@ import (
)
const (
MetricsNamespace = "focalboard"
MetricsSubsystemBlocks = "blocks"
MetricsSubsystemWorkspaces = "workspaces"
MetricsSubsystemSystem = "system"
MetricsNamespace = "focalboard"
MetricsSubsystemBlocks = "blocks"
MetricsSubsystemTeams = "teams"
MetricsSubsystemSystem = "system"
MetricsCloudInstallationLabel = "installationId"
)
@ -38,8 +38,8 @@ type Metrics struct {
blocksPatchedCount prometheus.Counter
blocksDeletedCount prometheus.Counter
blockCount *prometheus.GaugeVec
workspaceCount prometheus.Gauge
blockCount *prometheus.GaugeVec
teamCount prometheus.Gauge
blockLastActivity prometheus.Gauge
}
@ -143,14 +143,14 @@ func NewMetrics(info InstanceInfo) *Metrics {
}, []string{"BlockType"})
m.registry.MustRegister(m.blockCount)
m.workspaceCount = prometheus.NewGauge(prometheus.GaugeOpts{
m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemWorkspaces,
Name: "workspaces_total",
Help: "Total number of workspaces.",
Subsystem: MetricsSubsystemTeams,
Name: "teams_total",
Help: "Total number of teams.",
ConstLabels: additionalLabels,
})
m.registry.MustRegister(m.workspaceCount)
m.registry.MustRegister(m.teamCount)
m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: MetricsNamespace,
@ -209,8 +209,8 @@ func (m *Metrics) ObserveBlockCount(blockType string, count int64) {
}
}
func (m *Metrics) ObserveWorkspaceCount(count int64) {
func (m *Metrics) ObserveTeamCount(count int64) {
if m != nil {
m.workspaceCount.Set(float64(count))
m.teamCount.Set(float64(count))
}
}

View File

@ -12,6 +12,6 @@ import (
// SubscriptionDelivery provides an interface for delivering subscription notifications to other systems, such as
// channels server via plugin API.
type SubscriptionDelivery interface {
SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriberType model.SubscriberType,
SubscriptionDeliverSlackAttachments(subscriberID string, subscriberType model.SubscriberType,
attachments []*mm_model.SlackAttachment) error
}

View File

@ -8,14 +8,13 @@ import (
"sort"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
// Diff represents a difference between two versions of a block.
type Diff struct {
Board *model.Block
Board *model.Board
Card *model.Block
Authors StringMap
@ -40,16 +39,15 @@ type PropDiff struct {
}
type SchemaDiff struct {
Board *model.Block
Board *model.Board
OldPropDef *model.PropDef
NewPropDef *model.PropDef
}
type diffGenerator struct {
container store.Container
board *model.Block
card *model.Block
board *model.Board
card *model.Block
store Store
hint *model.NotificationHint
@ -63,7 +61,7 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
Limit: 1,
Descending: true,
}
blocks, err := dg.store.GetBlockHistory(dg.container, dg.hint.BlockID, opts)
blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts)
if err != nil {
return nil, fmt.Errorf("could not get block for notification: %w", err)
}
@ -84,7 +82,10 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
switch block.Type {
case model.TypeBoard:
return dg.generateDiffsForBoard(block, schema)
dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID))
// TODO: Fix this
// return dg.generateDiffsForBoard(block, schema)
return nil, nil
case model.TypeCard:
diff, err := dg.generateDiffsForCard(block, schema)
if err != nil || diff == nil {
@ -100,27 +101,29 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) {
}
}
func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.PropSchema) ([]*Diff, error) {
// TODO: fix this
/*
func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) {
opts := model.QuerySubtreeOptions{
AfterUpdateAt: dg.lastNotifyAt,
}
// find all child blocks of the board that updated since last notify.
blocks, err := dg.store.GetSubTree2(dg.container, board.ID, opts)
find all child blocks of the board that updated since last notify.
blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err)
}
var diffs []*Diff
// generate diff for board title change or description
generate diff for board title change or description
boardDiff, err := dg.generateDiffForBlock(board, schema)
if err != nil {
return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err)
}
if boardDiff != nil {
// TODO: phase 2 feature (generate schema diffs and add to board diff) goes here.
TODO: phase 2 feature (generate schema diffs and add to board diff) goes here.
diffs = append(diffs, boardDiff)
}
@ -136,6 +139,7 @@ func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.
}
return diffs, nil
}
*/
func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) {
// generate diff for card title change and properties.
@ -148,7 +152,7 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr
opts := model.QuerySubtreeOptions{
AfterUpdateAt: dg.lastNotifyAt,
}
blocks, err := dg.store.GetSubTree2(dg.container, card.ID, opts)
blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err)
}
@ -214,7 +218,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode
Limit: 1,
Descending: true,
}
history, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts)
history, err := dg.store.GetBlockHistory(newBlock.ID, opts)
if err != nil {
return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err)
}
@ -237,7 +241,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode
AfterUpdateAt: dg.lastNotifyAt,
Descending: true,
}
chgBlocks, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts)
chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts)
if err != nil {
return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err)
}

View File

@ -34,7 +34,7 @@ var (
// DiffConvOpts provides options when converting diffs to slack attachments.
type DiffConvOpts struct {
Language string
MakeCardLink func(block *model.Block, board *model.Block, card *model.Block) string
MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string
Logger *mlog.Logger
}
@ -49,7 +49,7 @@ func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template
t = template.New(key)
if opts.MakeCardLink == nil {
opts.MakeCardLink = func(block *model.Block, _ *model.Block, _ *model.Block) string {
opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string {
return fmt.Sprintf("`%s`", block.Title)
}
}
@ -160,6 +160,7 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac
mlog.String("card_id", cardDiff.Card.ID),
mlog.String("new_block_id", cardDiff.NewBlock.ID),
mlog.String("old_block_id", cardDiff.OldBlock.ID),
mlog.Int("childDiffs", len(cardDiff.Diffs)),
)
buf.Reset()

View File

@ -144,12 +144,8 @@ func (n *notifier) notify() {
}
func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
c := store.Container{
WorkspaceID: hint.WorkspaceID,
}
// get the subscriber list
subs, err := n.store.GetSubscribersForBlock(c, hint.BlockID)
subs, err := n.store.GetSubscribersForBlock(hint.BlockID)
if err != nil {
return err
}
@ -162,7 +158,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
oldestNotifiedAt := subs[0].NotifiedAt
// need the block's board and card.
board, card, err := n.store.GetBoardAndCardByID(c, hint.BlockID)
board, card, err := n.store.GetBoardAndCardByID(hint.BlockID)
if err != nil || board == nil || card == nil {
return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err)
}
@ -175,7 +171,6 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
)
dg := &diffGenerator{
container: c,
board: board,
card: card,
store: n.store,
@ -204,8 +199,8 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
opts := DiffConvOpts{
Language: "en", // TODO: use correct language with i18n available on server.
MakeCardLink: func(block *model.Block, board *model.Block, card *model.Block) string {
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.WorkspaceID, board.ID, card.ID))
MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string {
return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID))
},
Logger: n.logger,
}
@ -236,7 +231,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
mlog.String("subscriber_type", string(sub.SubscriberType)),
)
if err = n.delivery.SubscriptionDeliverSlackAttachments(hint.WorkspaceID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil {
if err = n.delivery.SubscriptionDeliverSlackAttachments(sub.SubscriberID, sub.SubscriberType, attachments); err != nil {
merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w",
sub.SubscriberID, sub.SubscriberType, err))
}
@ -262,7 +257,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error {
}
// update the last notified_at for all subscribers since we at least attempted to notify all of them.
err = dg.store.UpdateSubscribersNotifiedAt(dg.container, dg.hint.BlockID, notifiedAt)
err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt)
if err != nil {
merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err))
}

View File

@ -7,21 +7,20 @@ import (
"time"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/store"
)
type Store interface {
GetBlock(c store.Container, blockID string) (*model.Block, error)
GetBlockHistory(c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error)
GetSubTree2(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
GetBoardAndCardByID(c store.Container, blockID string) (board *model.Block, card *model.Block, err error)
GetBlock(blockID string) (*model.Block, error)
GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error)
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error)
GetUserByID(userID string) (*model.User, error)
CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error)
GetSubscribersForBlock(c store.Container, blockID string) ([]*model.Subscriber, error)
GetSubscribersCountForBlock(c store.Container, blockID string) (int, error)
UpdateSubscribersNotifiedAt(c store.Container, blockID string, notifyAt int64) error
CreateSubscription(sub *model.Subscription) (*model.Subscription, error)
GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error)
GetSubscribersCountForBlock(blockID string) (int, error)
UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error
UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error)
GetNextNotificationHint(remove bool) (*model.NotificationHint, error)

View File

@ -9,7 +9,6 @@ import (
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/services/store"
"github.com/mattermost/focalboard/server/ws"
"github.com/wiggin77/merror"
@ -77,8 +76,6 @@ func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration {
switch blockType {
case model.TypeCard:
return time.Second * time.Duration(b.notifyFreqCardSeconds)
case model.TypeBoard:
return time.Second * time.Duration(b.notifyFreqBoardSeconds)
default:
return defBlockNotificationFreq
}
@ -95,35 +92,30 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
merr := merror.New()
var err error
c := store.Container{
WorkspaceID: evt.Workspace,
}
// if new card added, automatically subscribe the author.
if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard {
sub := &model.Subscription{
BlockType: model.TypeCard,
BlockID: evt.BlockChanged.ID,
WorkspaceID: evt.Workspace,
SubscriberType: model.SubTypeUser,
SubscriberID: evt.ModifiedByID,
}
if sub, err = b.store.CreateSubscription(c, sub); err != nil {
if _, err = b.store.CreateSubscription(sub); err != nil {
b.logger.Warn("Cannot subscribe card author to card",
mlog.String("card_id", evt.BlockChanged.ID),
mlog.Err(err),
)
}
b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub)
b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub)
}
// notify board subscribers
subs, err := b.store.GetSubscribersForBlock(c, evt.Board.ID)
subs, err := b.store.GetSubscribersForBlock(evt.Board.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err))
}
if err = b.notifySubscribers(subs, evt.Board, evt.ModifiedByID); err != nil {
if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedByID); err != nil {
merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err))
}
@ -132,21 +124,21 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
}
// notify card subscribers
subs, err = b.store.GetSubscribersForBlock(c, evt.Card.ID)
subs, err = b.store.GetSubscribersForBlock(evt.Card.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err))
}
if err = b.notifySubscribers(subs, evt.Card, evt.ModifiedByID); err != nil {
if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedByID); err != nil {
merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err))
}
// notify block subscribers (if/when other types can be subscribed to)
if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID {
subs, err := b.store.GetSubscribersForBlock(c, evt.BlockChanged.ID)
subs, err := b.store.GetSubscribersForBlock(evt.BlockChanged.ID)
if err != nil {
merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err))
}
if err := b.notifySubscribers(subs, evt.BlockChanged, evt.ModifiedByID); err != nil {
if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedByID); err != nil {
merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err))
}
}
@ -154,24 +146,26 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error {
}
// notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database.
func (b *Backend) notifySubscribers(subs []*model.Subscriber, block *model.Block, modifiedByID string) error {
func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error {
if len(subs) == 0 {
return nil
}
hint := &model.NotificationHint{
BlockType: block.Type,
BlockID: block.ID,
WorkspaceID: block.WorkspaceID,
BlockType: idType,
BlockID: blockID,
ModifiedByID: modifiedByID,
}
hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(block.Type))
hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType))
if err != nil {
return fmt.Errorf("cannot upsert notification hint: %w", err)
}
if err := b.notifier.onNotifyHint(hint); err != nil {
return err
}
return b.notifier.onNotifyHint(hint)
return nil
}
// OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification
@ -188,17 +182,13 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
sub := &model.Subscription{
BlockType: model.TypeCard,
BlockID: evt.Card.ID,
WorkspaceID: evt.Workspace,
SubscriberType: model.SubTypeUser,
SubscriberID: userID,
}
c := store.Container{
WorkspaceID: evt.Workspace,
}
var err error
if sub, err = b.store.CreateSubscription(c, sub); err != nil {
if sub, err = b.store.CreateSubscription(sub); err != nil {
b.logger.Warn("Cannot subscribe mentioned user to card",
mlog.String("user_id", userID),
mlog.String("card_id", evt.Card.ID),
@ -206,7 +196,7 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) {
)
return
}
b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub)
b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub)
b.logger.Debug("Subscribed mentioned user to card",
mlog.String("user_id", userID),

View File

@ -14,13 +14,7 @@ import (
// MentionDeliver notifies a user they have been mentioned in a block.
func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) {
// determine which team the workspace is associated with
teamID, err := pd.getTeamID(evt)
if err != nil {
return "", fmt.Errorf("cannot determine teamID for block change notification: %w", err)
}
member, err := teamMemberFromUsername(pd.api, mentionUsername, teamID)
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID)
if err != nil {
if isErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
@ -30,16 +24,6 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
}
}
// check that user is a member of the channel
_, err = pd.api.GetChannelMember(evt.Workspace, member.UserId)
if err != nil {
if pd.api.IsErrNotFound(err) {
// mentioned user is not a member of the channel; fail silently.
return "", nil
}
return "", fmt.Errorf("cannot fetch channel member for user %s: %w", member.UserId, err)
}
author, err := pd.api.GetUserByID(evt.ModifiedByID)
if err != nil {
return "", fmt.Errorf("cannot find user: %w", err)
@ -49,7 +33,7 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string,
if err != nil {
return "", fmt.Errorf("cannot get direct channel: %w", err)
}
link := utils.MakeCardLink(pd.serverRoot, evt.Workspace, evt.Board.ID, evt.Card.ID)
link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID)
post := &model.Post{
UserId: pd.botID,

View File

@ -4,7 +4,10 @@
package plugindelivery
import (
"fmt"
"github.com/mattermost/focalboard/server/services/notify"
"github.com/mattermost/focalboard/server/utils"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
@ -52,11 +55,32 @@ func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery {
}
}
func (pd *PluginDelivery) getTeamID(evt notify.BlockChangeEvent) (string, error) {
// for now, the workspace ID is also the channel ID
channel, err := pd.api.GetChannelByID(evt.Workspace)
func (pd *PluginDelivery) Deliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) error {
member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID)
if err != nil {
return "", err
if isErrNotFound(err) {
// not really an error; could just be someone typed "@sometext"
return nil
} else {
return fmt.Errorf("cannot lookup mentioned user: %w", err)
}
}
return channel.TeamId, nil
author, err := pd.api.GetUserByID(evt.ModifiedByID)
if err != nil {
return fmt.Errorf("cannot find user: %w", err)
}
channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID)
if err != nil {
return fmt.Errorf("cannot get direct channel: %w", err)
}
link := utils.MakeCardLink(pd.serverRoot, evt.TeamID, evt.Board.ID, evt.Card.ID)
post := &mm_model.Post{
UserId: pd.botID,
ChannelId: channel.Id,
Message: formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged),
}
return pd.api.CreatePost(post)
}

View File

@ -17,10 +17,10 @@ var (
)
// SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to.
func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriptionType model.SubscriberType,
func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(subscriberID string, subscriptionType model.SubscriberType,
attachments []*mm_model.SlackAttachment) error {
// check subscriber is member of channel
_, err := pd.api.GetChannelMember(workspaceID, subscriberID)
_, err := pd.api.GetUserByID(subscriberID)
if err != nil {
if pd.api.IsErrNotFound(err) {
// subscriber is not a member of the channel; fail silently.

View File

@ -22,8 +22,8 @@ const (
type BlockChangeEvent struct {
Action Action
Workspace string
Board *model.Block
TeamID string
Board *model.Board
Card *model.Block
BlockChanged *model.Block
BlockOld *model.Block
@ -31,7 +31,7 @@ type BlockChangeEvent struct {
}
type SubscriptionChangeNotifier interface {
BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription)
BroadcastSubscriptionChange(subscription *model.Subscription)
}
// Backend provides an interface for sending notifications.
@ -113,7 +113,7 @@ func (s *Service) BlockChanged(evt BlockChangeEvent) {
// BroadcastSubscriptionChange sends a websocket message with details of the changed subscription to all
// connected users in the workspace.
func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
func (s *Service) BroadcastSubscriptionChange(subscription *model.Subscription) {
s.mux.RLock()
backends := make([]Backend, len(s.backends))
copy(backends, s.backends)
@ -122,11 +122,10 @@ func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *
for _, backend := range backends {
if scn, ok := backend.(SubscriptionChangeNotifier); ok {
s.logger.Debug("Delivering subscription change notification",
mlog.String("workspace_id", workspaceID),
mlog.String("block_id", subscription.BlockID),
mlog.String("subscriber_id", subscription.SubscriberID),
)
scn.BroadcastSubscriptionChange(workspaceID, subscription)
scn.BroadcastSubscriptionChange(subscription)
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localpermissions
import (
"testing"
"github.com/mattermost/focalboard/server/model"
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
type TestHelper struct {
t *testing.T
ctrl *gomock.Controller
store *permissionsMocks.MockStore
permissions *Service
}
func SetupTestHelper(t *testing.T) *TestHelper {
ctrl := gomock.NewController(t)
mockStore := permissionsMocks.NewMockStore(ctrl)
return &TestHelper{
t: t,
ctrl: ctrl,
store: mockStore,
permissions: New(mockStore, nil),
}
}
func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) {
for _, p := range hasPermissionTo {
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
th.store.EXPECT().
GetMemberForBoard(member.BoardID, member.UserID).
Return(member, nil).
Times(1)
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
assert.True(t, hasPermission)
})
}
for _, p := range hasNotPermissionTo {
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
th.store.EXPECT().
GetMemberForBoard(member.BoardID, member.UserID).
Return(member, nil).
Times(1)
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
assert.False(t, hasPermission)
})
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localpermissions
import (
"database/sql"
"errors"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/permissions"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
type Service struct {
store permissions.Store
logger *mlog.Logger
}
func New(store permissions.Store, logger *mlog.Logger) *Service {
return &Service{
store: store,
logger: logger,
}
}
func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool {
if userID == "" || teamID == "" || permission == nil {
return false
}
return true
}
func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool {
if userID == "" || boardID == "" || permission == nil {
return false
}
member, err := s.store.GetMemberForBoard(boardID, userID)
if errors.Is(err, sql.ErrNoRows) {
return false
}
if err != nil {
s.logger.Error("error getting member for board",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
mlog.Err(err),
)
return false
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
return member.SchemeAdmin
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
return member.SchemeAdmin || member.SchemeEditor
case model.PermissionViewBoard:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
default:
return false
}
}

View File

@ -0,0 +1,144 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package localpermissions
import (
"database/sql"
"testing"
"github.com/mattermost/focalboard/server/model"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/assert"
)
func TestHasPermissionToTeam(t *testing.T) {
th := SetupTestHelper(t)
t.Run("empty input should always unauthorize", func(t *testing.T) {
assert.False(t, th.permissions.HasPermissionToTeam("", "team-id", model.PermissionManageBoardCards))
assert.False(t, th.permissions.HasPermissionToTeam("user-id", "", model.PermissionManageBoardCards))
assert.False(t, th.permissions.HasPermissionToTeam("user-id", "team-id", nil))
})
t.Run("all users have all permissions on teams", func(t *testing.T) {
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards)
assert.True(t, hasPermission)
})
}
func TestHasPermissionToBoard(t *testing.T) {
th := SetupTestHelper(t)
t.Run("empty input should always unauthorize", func(t *testing.T) {
assert.False(t, th.permissions.HasPermissionToBoard("", "board-id", model.PermissionManageBoardCards))
assert.False(t, th.permissions.HasPermissionToBoard("user-id", "", model.PermissionManageBoardCards))
assert.False(t, th.permissions.HasPermissionToBoard("user-id", "board-id", nil))
})
t.Run("nonexistent user", func(t *testing.T) {
userID := "user-id"
boardID := "board-id"
th.store.EXPECT().
GetMemberForBoard(boardID, userID).
Return(nil, sql.ErrNoRows).
Times(1)
hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards)
assert.False(t, hasPermission)
})
t.Run("board admin", func(t *testing.T) {
member := &model.BoardMember{
UserID: "user-id",
BoardID: "board-id",
SchemeAdmin: true,
}
hasPermissionTo := []*mmModel.Permission{
model.PermissionManageBoardType,
model.PermissionDeleteBoard,
model.PermissionManageBoardRoles,
model.PermissionShareBoard,
model.PermissionManageBoardCards,
model.PermissionViewBoard,
model.PermissionManageBoardProperties,
}
hasNotPermissionTo := []*mmModel.Permission{}
th.checkBoardPermissions("admin", member, hasPermissionTo, hasNotPermissionTo)
})
t.Run("board editor", func(t *testing.T) {
member := &model.BoardMember{
UserID: "user-id",
BoardID: "board-id",
SchemeEditor: true,
}
hasPermissionTo := []*mmModel.Permission{
model.PermissionManageBoardCards,
model.PermissionViewBoard,
model.PermissionManageBoardProperties,
}
hasNotPermissionTo := []*mmModel.Permission{
model.PermissionManageBoardType,
model.PermissionDeleteBoard,
model.PermissionManageBoardRoles,
model.PermissionShareBoard,
}
th.checkBoardPermissions("editor", member, hasPermissionTo, hasNotPermissionTo)
})
t.Run("board commenter", func(t *testing.T) {
member := &model.BoardMember{
UserID: "user-id",
BoardID: "board-id",
SchemeCommenter: true,
}
hasPermissionTo := []*mmModel.Permission{
model.PermissionViewBoard,
}
hasNotPermissionTo := []*mmModel.Permission{
model.PermissionManageBoardType,
model.PermissionDeleteBoard,
model.PermissionManageBoardRoles,
model.PermissionShareBoard,
model.PermissionManageBoardCards,
model.PermissionManageBoardProperties,
}
th.checkBoardPermissions("commenter", member, hasPermissionTo, hasNotPermissionTo)
})
t.Run("board viewer", func(t *testing.T) {
member := &model.BoardMember{
UserID: "user-id",
BoardID: "board-id",
SchemeViewer: true,
}
hasPermissionTo := []*mmModel.Permission{
model.PermissionViewBoard,
}
hasNotPermissionTo := []*mmModel.Permission{
model.PermissionManageBoardType,
model.PermissionDeleteBoard,
model.PermissionManageBoardRoles,
model.PermissionShareBoard,
model.PermissionManageBoardCards,
model.PermissionManageBoardProperties,
}
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
})
}

View File

@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mmpermissions
import (
"testing"
"github.com/mattermost/focalboard/server/model"
mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks"
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
type TestHelper struct {
t *testing.T
ctrl *gomock.Controller
store *permissionsMocks.MockStore
api *mmpermissionsMocks.MockAPI
permissions *Service
}
func SetupTestHelper(t *testing.T) *TestHelper {
ctrl := gomock.NewController(t)
mockStore := permissionsMocks.NewMockStore(ctrl)
mockAPI := mmpermissionsMocks.NewMockAPI(ctrl)
return &TestHelper{
t: t,
ctrl: ctrl,
store: mockStore,
api: mockAPI,
permissions: New(mockStore, mockAPI),
}
}
func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, teamID string,
hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) {
for _, p := range hasPermissionTo {
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
th.store.EXPECT().
GetBoard(member.BoardID).
Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil).
Times(1)
th.api.EXPECT().
HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam).
Return(true).
Times(1)
th.store.EXPECT().
GetMemberForBoard(member.BoardID, member.UserID).
Return(member, nil).
Times(1)
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
assert.True(t, hasPermission)
})
}
for _, p := range hasNotPermissionTo {
th.t.Run(roleName+" "+p.Id, func(t *testing.T) {
th.store.EXPECT().
GetBoard(member.BoardID).
Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil).
Times(1)
th.api.EXPECT().
HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam).
Return(true).
Times(1)
th.store.EXPECT().
GetMemberForBoard(member.BoardID, member.UserID).
Return(member, nil).
Times(1)
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
assert.False(t, hasPermission)
})
}
}

Some files were not shown because too many files have changed in this diff Show More