2021-05-11 00:10:19 -05:00
package libraryelements
2021-03-01 08:33:17 -06:00
import (
"encoding/json"
"fmt"
2023-04-20 04:24:41 -05:00
"net/http"
2021-03-01 08:33:17 -06:00
"testing"
"github.com/google/go-cmp/cmp"
2023-01-26 07:46:30 -06:00
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/dashboards"
2023-02-01 10:32:05 -06:00
"github.com/grafana/grafana/pkg/services/libraryelements/model"
2022-08-10 04:56:48 -05:00
"github.com/grafana/grafana/pkg/services/org"
2021-10-11 07:30:59 -05:00
"github.com/grafana/grafana/pkg/web"
2021-03-01 08:33:17 -06:00
)
2023-04-20 04:24:41 -05:00
func TestLibraryElementPermissionsGeneralFolder ( t * testing . T ) {
2021-03-01 08:33:17 -06:00
var generalFolderCases = [ ] struct {
2022-08-10 04:56:48 -05:00
role org . RoleType
2021-03-01 08:33:17 -06:00
status int
} {
2022-08-10 04:56:48 -05:00
{ org . RoleAdmin , 200 } ,
{ org . RoleEditor , 200 } ,
{ org . RoleViewer , 403 } ,
2021-03-01 08:33:17 -06:00
}
for _ , testCase := range generalFolderCases {
testScenario ( t , fmt . Sprintf ( "When %s tries to create a library panel in the General folder, it should return correct status" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
sc . reqContext . SignedInUser . OrgRole = testCase . role
2021-05-11 00:10:19 -05:00
command := getCreatePanelCommand ( 0 , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
require . Equal ( t , testCase . status , resp . Status ( ) )
} )
testScenario ( t , fmt . Sprintf ( "When %s tries to patch a library panel by moving it to the General folder, it should return correct status" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
folder := createFolder ( t , sc , "Folder" )
2022-11-10 03:41:03 -06:00
command := getCreatePanelCommand ( folder . ID , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
result := validateAndUnMarshalResponse ( t , resp )
sc . reqContext . SignedInUser . OrgRole = testCase . role
2023-02-01 10:32:05 -06:00
cmd := model . PatchLibraryElementCommand { FolderID : 0 , Version : 1 , Kind : int64 ( model . PanelElement ) }
2021-10-11 07:30:59 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
2021-11-29 03:18:01 -06:00
sc . ctx . Req . Body = mockRequestBody ( cmd )
resp = sc . service . patchHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
require . Equal ( t , testCase . status , resp . Status ( ) )
} )
testScenario ( t , fmt . Sprintf ( "When %s tries to patch a library panel by moving it from the General folder, it should return correct status" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
folder := createFolder ( t , sc , "Folder" )
2021-05-11 00:10:19 -05:00
command := getCreatePanelCommand ( 0 , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
result := validateAndUnMarshalResponse ( t , resp )
sc . reqContext . SignedInUser . OrgRole = testCase . role
2023-02-01 10:32:05 -06:00
cmd := model . PatchLibraryElementCommand { FolderID : folder . ID , Version : 1 , Kind : int64 ( model . PanelElement ) }
2021-10-11 07:30:59 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
2021-11-29 03:18:01 -06:00
sc . ctx . Req . Body = mockRequestBody ( cmd )
resp = sc . service . patchHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
require . Equal ( t , testCase . status , resp . Status ( ) )
} )
testScenario ( t , fmt . Sprintf ( "When %s tries to delete a library panel in the General folder, it should return correct status" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
2021-05-11 00:10:19 -05:00
cmd := getCreatePanelCommand ( 0 , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
result := validateAndUnMarshalResponse ( t , resp )
sc . reqContext . SignedInUser . OrgRole = testCase . role
2021-10-11 07:30:59 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
2021-03-01 08:33:17 -06:00
resp = sc . service . deleteHandler ( sc . reqContext )
require . Equal ( t , testCase . status , resp . Status ( ) )
} )
2023-04-20 04:24:41 -05:00
testScenario ( t , fmt . Sprintf ( "When %s tries to get a library panel from General folder, it should return correct response" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
cmd := getCreatePanelCommand ( 0 , "Library Panel in General Folder" )
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp := sc . service . createHandler ( sc . reqContext )
result := validateAndUnMarshalResponse ( t , resp )
result . Result . Meta . CreatedBy . Name = userInDbName
result . Result . Meta . CreatedBy . AvatarUrl = userInDbAvatar
result . Result . Meta . UpdatedBy . Name = userInDbName
result . Result . Meta . UpdatedBy . AvatarUrl = userInDbAvatar
result . Result . Meta . FolderName = "General"
result . Result . Meta . FolderUID = ""
sc . reqContext . SignedInUser . OrgRole = testCase . role
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
resp = sc . service . getHandler ( sc . reqContext )
require . Equal ( t , 200 , resp . Status ( ) )
var actual libraryElementResult
err := json . Unmarshal ( resp . Body ( ) , & actual )
require . NoError ( t , err )
if diff := cmp . Diff ( result . Result , actual . Result , getCompareOptions ( ) ... ) ; diff != "" {
t . Fatalf ( "Result mismatch (-want +got):\n%s" , diff )
}
} )
testScenario ( t , fmt . Sprintf ( "When %s tries to get all library panels from General folder, it should return correct response" , testCase . role ) ,
func ( t * testing . T , sc scenarioContext ) {
cmd := getCreatePanelCommand ( 0 , "Library Panel in General Folder" )
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp := sc . service . createHandler ( sc . reqContext )
result := validateAndUnMarshalResponse ( t , resp )
result . Result . Meta . CreatedBy . Name = userInDbName
result . Result . Meta . CreatedBy . AvatarUrl = userInDbAvatar
result . Result . Meta . UpdatedBy . Name = userInDbName
result . Result . Meta . UpdatedBy . AvatarUrl = userInDbAvatar
result . Result . Meta . FolderName = "General"
sc . reqContext . SignedInUser . OrgRole = testCase . role
resp = sc . service . getAllHandler ( sc . reqContext )
require . Equal ( t , 200 , resp . Status ( ) )
var actual libraryElementsSearch
err := json . Unmarshal ( resp . Body ( ) , & actual )
require . NoError ( t , err )
require . Equal ( t , 1 , len ( actual . Result . Elements ) )
if diff := cmp . Diff ( result . Result , actual . Result . Elements [ 0 ] , getCompareOptions ( ) ... ) ; diff != "" {
t . Fatalf ( "Result mismatch (-want +got):\n%s" , diff )
}
} )
2021-03-01 08:33:17 -06:00
}
2023-04-20 04:24:41 -05:00
}
2021-03-01 08:33:17 -06:00
2023-04-20 04:24:41 -05:00
func TestLibraryElementCreatePermissions ( t * testing . T ) {
var accessCases = [ ] struct {
permissions map [ string ] [ ] string
desc string
status int
2021-03-01 08:33:17 -06:00
} {
2023-04-20 04:24:41 -05:00
{
desc : "can create library elements when granted write access to the correct folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Folder" ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can create library elements when granted write access to all folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can't create library elements when granted write access to the wrong folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Other_folder" ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusForbidden ,
} ,
{
desc : "can't create library elements when granted read access to the right folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Folder" ) } ,
} ,
status : http . StatusForbidden ,
} ,
2021-03-01 08:33:17 -06:00
}
2023-04-20 04:24:41 -05:00
for _ , testCase := range accessCases {
testScenario ( t , testCase . desc ,
2021-03-01 08:33:17 -06:00
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
folder := createFolder ( t , sc , "Folder" )
sc . reqContext . SignedInUser . Permissions = map [ int64 ] map [ string ] [ ] string {
1 : testCase . permissions ,
}
2021-03-01 08:33:17 -06:00
2023-04-20 04:24:41 -05:00
command := getCreatePanelCommand ( folder . ID , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
2023-04-20 04:24:41 -05:00
require . Equal ( t , testCase . status , resp . Status ( ) )
2021-03-01 08:33:17 -06:00
} )
2023-04-20 04:24:41 -05:00
}
}
func TestLibraryElementPatchPermissions ( t * testing . T ) {
var accessCases = [ ] struct {
permissions map [ string ] [ ] string
desc string
status int
} {
{
desc : "can move library elements when granted write access to the source and destination folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "FromFolder" ) , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "ToFolder" ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can move library elements when granted write access to all folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can't move library elements when granted write access only to the source folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "FromFolder" ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusForbidden ,
} ,
{
desc : "can't move library elements when granted write access to the destination folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "ToFolder" ) } ,
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusForbidden ,
} ,
}
2021-03-01 08:33:17 -06:00
2023-04-20 04:24:41 -05:00
for _ , testCase := range accessCases {
testScenario ( t , testCase . desc ,
2021-03-01 08:33:17 -06:00
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
fromFolder := createFolder ( t , sc , "FromFolder" )
command := getCreatePanelCommand ( fromFolder . ID , "Library Panel Name" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
result := validateAndUnMarshalResponse ( t , resp )
2023-04-20 04:24:41 -05:00
toFolder := createFolder ( t , sc , "ToFolder" )
sc . reqContext . SignedInUser . Permissions = map [ int64 ] map [ string ] [ ] string {
1 : testCase . permissions ,
}
cmd := model . PatchLibraryElementCommand { FolderID : toFolder . ID , Version : 1 , Kind : int64 ( model . PanelElement ) }
2021-10-11 07:30:59 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp = sc . service . patchHandler ( sc . reqContext )
2023-04-20 04:24:41 -05:00
require . Equal ( t , testCase . status , resp . Status ( ) )
2021-03-01 08:33:17 -06:00
} )
}
2023-04-20 04:24:41 -05:00
}
2021-03-01 08:33:17 -06:00
2023-04-20 04:24:41 -05:00
func TestLibraryElementDeletePermissions ( t * testing . T ) {
var accessCases = [ ] struct {
permissions map [ string ] [ ] string
desc string
status int
2021-05-11 00:10:19 -05:00
} {
2023-04-20 04:24:41 -05:00
{
desc : "can delete library elements when granted write access to the correct folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Folder" ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can delete library elements when granted write access to all folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can't delete library elements when granted write access to the wrong folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersWrite : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Other_folder" ) } ,
} ,
status : http . StatusForbidden ,
} ,
{
desc : "can't delete library elements when granted read access to the right folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Folder" ) } ,
} ,
status : http . StatusForbidden ,
} ,
2021-05-11 00:10:19 -05:00
}
2023-04-20 04:24:41 -05:00
for _ , testCase := range accessCases {
testScenario ( t , testCase . desc ,
2021-05-11 00:10:19 -05:00
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
folder := createFolder ( t , sc , "Folder" )
command := getCreatePanelCommand ( folder . ID , "Library Panel Name" )
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
result := validateAndUnMarshalResponse ( t , resp )
2021-05-11 00:10:19 -05:00
2023-04-20 04:24:41 -05:00
sc . reqContext . SignedInUser . Permissions = map [ int64 ] map [ string ] [ ] string {
1 : testCase . permissions ,
2021-05-11 00:10:19 -05:00
}
2023-04-20 04:24:41 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
resp = sc . service . deleteHandler ( sc . reqContext )
require . Equal ( t , testCase . status , resp . Status ( ) )
2021-05-11 00:10:19 -05:00
} )
2023-04-20 04:24:41 -05:00
}
}
2021-05-11 00:10:19 -05:00
2023-04-20 04:24:41 -05:00
func TestLibraryElementsWithMissingFolders ( t * testing . T ) {
testScenario ( t , "When a user tries to create a library panel in a folder that doesn't exist, it should fail" ,
func ( t * testing . T , sc scenarioContext ) {
command := getCreatePanelCommand ( - 100 , "Library Panel Name" )
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
require . Equal ( t , 404 , resp . Status ( ) )
} )
testScenario ( t , "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail" ,
func ( t * testing . T , sc scenarioContext ) {
folder := createFolder ( t , sc , "Folder" )
command := getCreatePanelCommand ( folder . ID , "Library Panel Name" )
sc . reqContext . Req . Body = mockRequestBody ( command )
resp := sc . service . createHandler ( sc . reqContext )
result := validateAndUnMarshalResponse ( t , resp )
cmd := model . PatchLibraryElementCommand { FolderID : - 100 , Version : 1 , Kind : int64 ( model . PanelElement ) }
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp = sc . service . patchHandler ( sc . reqContext )
require . Equal ( t , 404 , resp . Status ( ) )
} )
}
func TestLibraryElementsGetPermissions ( t * testing . T ) {
var getCases = [ ] struct {
permissions map [ string ] [ ] string
desc string
status int
} {
{
desc : "can get a library element when granted read access to all folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
status : http . StatusOK ,
} ,
{
desc : "can't list library element when granted read access to the wrong folder" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( "Other_folder" ) } ,
} ,
status : http . StatusNotFound ,
} ,
}
for _ , testCase := range getCases {
testScenario ( t , testCase . desc ,
2021-05-11 00:10:19 -05:00
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
folder := createFolder ( t , sc , "Folder" )
cmd := getCreatePanelCommand ( folder . ID , "Library Panel" )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp := sc . service . createHandler ( sc . reqContext )
2021-05-11 00:10:19 -05:00
result := validateAndUnMarshalResponse ( t , resp )
result . Result . Meta . CreatedBy . Name = userInDbName
2023-01-29 22:14:12 -06:00
result . Result . Meta . CreatedBy . AvatarUrl = userInDbAvatar
2021-05-11 00:10:19 -05:00
result . Result . Meta . UpdatedBy . Name = userInDbName
2023-01-29 22:14:12 -06:00
result . Result . Meta . UpdatedBy . AvatarUrl = userInDbAvatar
2023-04-20 04:24:41 -05:00
result . Result . Meta . FolderName = folder . Title
result . Result . Meta . FolderUID = folder . UID
sc . reqContext . SignedInUser . OrgRole = org . RoleViewer
sc . reqContext . SignedInUser . Permissions = map [ int64 ] map [ string ] [ ] string {
1 : testCase . permissions ,
}
2021-05-11 00:10:19 -05:00
2021-10-11 07:30:59 -05:00
sc . ctx . Req = web . SetURLParams ( sc . ctx . Req , map [ string ] string { ":uid" : result . Result . UID } )
2021-05-11 00:10:19 -05:00
resp = sc . service . getHandler ( sc . reqContext )
2023-04-20 04:24:41 -05:00
require . Equal ( t , testCase . status , resp . Status ( ) )
2021-05-11 00:10:19 -05:00
} )
}
2023-04-20 04:24:41 -05:00
}
2021-05-11 00:10:19 -05:00
2023-04-20 04:24:41 -05:00
func TestLibraryElementsGetAllPermissions ( t * testing . T ) {
var getCases = [ ] struct {
permissions map [ string ] [ ] string
desc string
status int
expectedResultCount int
2021-03-01 08:33:17 -06:00
} {
2023-04-20 04:24:41 -05:00
{
desc : "can get all library elements when granted read access to all folders" ,
permissions : map [ string ] [ ] string {
dashboards . ActionFoldersRead : { dashboards . ScopeFoldersProvider . GetResourceAllScope ( ) } ,
} ,
expectedResultCount : 2 ,
status : http . StatusOK ,
} ,
{
desc : "can't get any library element when doesn't have access to any folders" ,
permissions : map [ string ] [ ] string { } ,
expectedResultCount : 0 ,
status : http . StatusOK ,
} ,
2021-03-01 08:33:17 -06:00
}
2023-04-20 04:24:41 -05:00
for _ , testCase := range getCases {
testScenario ( t , testCase . desc ,
2021-03-01 08:33:17 -06:00
func ( t * testing . T , sc scenarioContext ) {
2023-04-20 04:24:41 -05:00
for i := 1 ; i <= 2 ; i ++ {
folder := createFolder ( t , sc , fmt . Sprintf ( "Folder%d" , i ) )
cmd := getCreatePanelCommand ( folder . ID , fmt . Sprintf ( "Library Panel %d" , i ) )
2021-11-29 03:18:01 -06:00
sc . reqContext . Req . Body = mockRequestBody ( cmd )
resp := sc . service . createHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
result := validateAndUnMarshalResponse ( t , resp )
2022-11-10 03:41:03 -06:00
result . Result . Meta . FolderUID = folder . UID
2021-03-01 08:33:17 -06:00
}
2023-04-20 04:24:41 -05:00
sc . reqContext . SignedInUser . OrgRole = org . RoleViewer
sc . reqContext . SignedInUser . Permissions = map [ int64 ] map [ string ] [ ] string {
1 : testCase . permissions ,
2021-03-01 08:33:17 -06:00
}
2023-04-20 04:24:41 -05:00
resp := sc . service . getAllHandler ( sc . reqContext )
2021-03-01 08:33:17 -06:00
require . Equal ( t , 200 , resp . Status ( ) )
2021-05-11 00:10:19 -05:00
var actual libraryElementsSearch
2021-03-01 08:33:17 -06:00
err := json . Unmarshal ( resp . Body ( ) , & actual )
require . NoError ( t , err )
2023-04-20 04:24:41 -05:00
require . Equal ( t , testCase . expectedResultCount , len ( actual . Result . Elements ) )
2021-03-01 08:33:17 -06:00
} )
}
}