2022-12-22 10:54:03 +00:00
package clientapi
import (
"context"
"net/http"
"net/http/httptest"
2023-03-27 14:39:33 +01:00
"reflect"
2022-12-22 10:54:03 +00:00
"testing"
2023-03-22 08:21:32 +00:00
"time"
2022-12-22 10:54:03 +00:00
2023-01-19 20:02:32 +00:00
"github.com/matrix-org/dendrite/federationapi"
2023-03-17 11:09:45 +00:00
"github.com/matrix-org/dendrite/internal/caching"
2023-03-22 08:21:32 +00:00
"github.com/matrix-org/dendrite/internal/httputil"
"github.com/matrix-org/dendrite/internal/sqlutil"
2022-12-22 10:54:03 +00:00
"github.com/matrix-org/dendrite/roomserver"
2023-01-19 20:02:32 +00:00
"github.com/matrix-org/dendrite/roomserver/api"
2023-03-27 14:39:33 +01:00
basepkg "github.com/matrix-org/dendrite/setup/base"
2022-12-22 10:54:03 +00:00
"github.com/matrix-org/dendrite/setup/config"
2023-03-22 08:21:32 +00:00
"github.com/matrix-org/dendrite/setup/jetstream"
2023-01-19 20:02:32 +00:00
"github.com/matrix-org/dendrite/syncapi"
2022-12-22 10:54:03 +00:00
"github.com/matrix-org/gomatrixserverlib"
2023-04-06 09:55:01 +01:00
"github.com/matrix-org/gomatrixserverlib/fclient"
2022-12-22 10:54:03 +00:00
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/dendrite/test/testrig"
"github.com/matrix-org/dendrite/userapi"
uapi "github.com/matrix-org/dendrite/userapi/api"
)
func TestAdminResetPassword ( t * testing . T ) {
aliceAdmin := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeAdmin ) )
bob := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeUser ) )
vhUser := & test . User { ID : "@vhuser:vh1" }
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-03-22 08:21:32 +00:00
cfg , processCtx , close := testrig . CreateConfig ( t , dbType )
defer close ( )
natsInstance := jetstream . NATSInstance { }
2022-12-22 10:54:03 +00:00
// add a vhost
2023-03-22 08:21:32 +00:00
cfg . Global . VirtualHosts = append ( cfg . Global . VirtualHosts , & config . VirtualHost {
2023-04-06 09:55:01 +01:00
SigningIdentity : fclient . SigningIdentity { ServerName : "vh1" } ,
2022-12-22 10:54:03 +00:00
} )
2023-03-22 08:21:32 +00:00
routers := httputil . NewRouters ( )
cm := sqlutil . NewConnectionManager ( processCtx , cfg . Global . DatabaseOptions )
caches := caching . NewRistrettoCache ( 128 * 1024 * 1024 , time . Hour , caching . DisableMetrics )
rsAPI := roomserver . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , caches , caching . DisableMetrics )
2022-12-22 10:54:03 +00:00
// Needed for changing the password/login
2023-03-22 08:21:32 +00:00
userAPI := userapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , rsAPI , nil )
2022-12-22 10:54:03 +00:00
// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
2023-03-22 08:21:32 +00:00
AddPublicRoutes ( processCtx , routers , cfg , & natsInstance , nil , rsAPI , nil , nil , nil , userAPI , nil , nil , caching . DisableMetrics )
2022-12-22 10:54:03 +00:00
// Create the users in the userapi and login
2023-03-31 09:15:01 +01:00
accessTokens := map [ * test . User ] userDevice {
aliceAdmin : { } ,
bob : { } ,
vhUser : { } ,
2022-12-22 10:54:03 +00:00
}
2023-03-27 14:39:33 +01:00
createAccessTokens ( t , accessTokens , userAPI , ctx , routers )
2022-12-22 10:54:03 +00:00
testCases := [ ] struct {
name string
requestingUser * test . User
userID string
requestOpt test . HTTPRequestOpt
wantOK bool
withHeader bool
} {
{ name : "Missing auth" , requestingUser : bob , wantOK : false , userID : bob . ID } ,
{ name : "Bob is denied access" , requestingUser : bob , wantOK : false , withHeader : true , userID : bob . ID } ,
{ name : "Alice is allowed access" , requestingUser : aliceAdmin , wantOK : true , withHeader : true , userID : bob . ID , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } {
"password" : util . RandomString ( 8 ) ,
} ) } ,
{ name : "missing userID does not call function" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : "" } , // this 404s
{ name : "rejects empty password" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : bob . ID , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } {
"password" : "" ,
} ) } ,
{ name : "rejects unknown server name" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : "@doesnotexist:localhost" , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } { } ) } ,
{ name : "rejects unknown user" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : "@doesnotexist:test" , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } { } ) } ,
{ name : "allows changing password for different vhost" , requestingUser : aliceAdmin , wantOK : true , withHeader : true , userID : vhUser . ID , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } {
"password" : util . RandomString ( 8 ) ,
} ) } ,
{ name : "rejects existing user, missing body" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : bob . ID } ,
{ name : "rejects invalid userID" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : "!notauserid:test" , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } { } ) } ,
{ name : "rejects invalid json" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : bob . ID , requestOpt : test . WithJSONBody ( t , ` { invalidJSON} ` ) } ,
{ name : "rejects too weak password" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : bob . ID , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } {
"password" : util . RandomString ( 6 ) ,
} ) } ,
{ name : "rejects too long password" , requestingUser : aliceAdmin , wantOK : false , withHeader : true , userID : bob . ID , requestOpt : test . WithJSONBody ( t , map [ string ] interface { } {
"password" : util . RandomString ( 513 ) ,
} ) } ,
}
for _ , tc := range testCases {
2023-01-19 20:02:32 +00:00
tc := tc // ensure we don't accidentally only test the last test case
2022-12-22 10:54:03 +00:00
t . Run ( tc . name , func ( t * testing . T ) {
req := test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/resetPassword/" + tc . userID )
if tc . requestOpt != nil {
req = test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/resetPassword/" + tc . userID , tc . requestOpt )
}
if tc . withHeader {
2023-03-31 09:15:01 +01:00
req . Header . Set ( "Authorization" , "Bearer " + accessTokens [ tc . requestingUser ] . accessToken )
2022-12-22 10:54:03 +00:00
}
rec := httptest . NewRecorder ( )
2023-03-22 08:21:32 +00:00
routers . DendriteAdmin . ServeHTTP ( rec , req )
2022-12-22 10:54:03 +00:00
t . Logf ( "%s" , rec . Body . String ( ) )
if tc . wantOK && rec . Code != http . StatusOK {
t . Fatalf ( "expected http status %d, got %d: %s" , http . StatusOK , rec . Code , rec . Body . String ( ) )
}
} )
}
} )
}
2023-01-19 20:02:32 +00:00
func TestPurgeRoom ( t * testing . T ) {
aliceAdmin := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeAdmin ) )
bob := test . NewUser ( t )
room := test . NewRoom ( t , aliceAdmin , test . RoomPreset ( test . PresetTrustedPrivateChat ) )
// Invite Bob
room . CreateAndInsert ( t , aliceAdmin , gomatrixserverlib . MRoomMember , map [ string ] interface { } {
"membership" : "invite" ,
} , test . WithStateKey ( bob . ID ) )
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-03-22 08:21:32 +00:00
cfg , processCtx , close := testrig . CreateConfig ( t , dbType )
caches := caching . NewRistrettoCache ( 128 * 1024 * 1024 , time . Hour , caching . DisableMetrics )
natsInstance := jetstream . NATSInstance { }
defer close ( )
2023-01-19 20:02:32 +00:00
2023-03-22 08:21:32 +00:00
routers := httputil . NewRouters ( )
cm := sqlutil . NewConnectionManager ( processCtx , cfg . Global . DatabaseOptions )
rsAPI := roomserver . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , caches , caching . DisableMetrics )
userAPI := userapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , rsAPI , nil )
2023-01-19 20:02:32 +00:00
// this starts the JetStream consumers
2023-03-22 08:21:32 +00:00
syncapi . AddPublicRoutes ( processCtx , routers , cfg , cm , & natsInstance , userAPI , rsAPI , caches , caching . DisableMetrics )
federationapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , nil , rsAPI , caches , nil , true )
2023-01-19 20:02:32 +00:00
rsAPI . SetFederationAPI ( nil , nil )
// Create the room
if err := api . SendEvents ( ctx , rsAPI , api . KindNew , room . Events ( ) , "test" , "test" , "test" , nil , false ) ; err != nil {
t . Fatalf ( "failed to send events: %v" , err )
}
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
2023-03-22 08:21:32 +00:00
AddPublicRoutes ( processCtx , routers , cfg , & natsInstance , nil , rsAPI , nil , nil , nil , userAPI , nil , nil , caching . DisableMetrics )
2023-01-19 20:02:32 +00:00
// Create the users in the userapi and login
2023-03-31 09:15:01 +01:00
accessTokens := map [ * test . User ] userDevice {
aliceAdmin : { } ,
2023-01-19 20:02:32 +00:00
}
2023-03-27 14:39:33 +01:00
createAccessTokens ( t , accessTokens , userAPI , ctx , routers )
2023-01-19 20:02:32 +00:00
testCases := [ ] struct {
name string
roomID string
wantOK bool
} {
{ name : "Can purge existing room" , wantOK : true , roomID : room . ID } ,
{ name : "Can not purge non-existent room" , wantOK : false , roomID : "!doesnotexist:localhost" } ,
{ name : "rejects invalid room ID" , wantOK : false , roomID : "@doesnotexist:localhost" } ,
}
for _ , tc := range testCases {
tc := tc // ensure we don't accidentally only test the last test case
t . Run ( tc . name , func ( t * testing . T ) {
req := test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/purgeRoom/" + tc . roomID )
2023-03-31 09:15:01 +01:00
req . Header . Set ( "Authorization" , "Bearer " + accessTokens [ aliceAdmin ] . accessToken )
2023-01-19 20:02:32 +00:00
rec := httptest . NewRecorder ( )
2023-03-22 08:21:32 +00:00
routers . DendriteAdmin . ServeHTTP ( rec , req )
2023-01-19 20:02:32 +00:00
t . Logf ( "%s" , rec . Body . String ( ) )
if tc . wantOK && rec . Code != http . StatusOK {
t . Fatalf ( "expected http status %d, got %d: %s" , http . StatusOK , rec . Code , rec . Body . String ( ) )
}
} )
}
} )
}
2023-03-27 14:39:33 +01:00
func TestAdminEvacuateRoom ( t * testing . T ) {
aliceAdmin := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeAdmin ) )
bob := test . NewUser ( t )
room := test . NewRoom ( t , aliceAdmin )
// Join Bob
room . CreateAndInsert ( t , bob , gomatrixserverlib . MRoomMember , map [ string ] interface { } {
"membership" : "join" ,
} , test . WithStateKey ( bob . ID ) )
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
cfg , processCtx , close := testrig . CreateConfig ( t , dbType )
caches := caching . NewRistrettoCache ( 128 * 1024 * 1024 , time . Hour , caching . DisableMetrics )
natsInstance := jetstream . NATSInstance { }
defer close ( )
routers := httputil . NewRouters ( )
cm := sqlutil . NewConnectionManager ( processCtx , cfg . Global . DatabaseOptions )
rsAPI := roomserver . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , caches , caching . DisableMetrics )
userAPI := userapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , rsAPI , nil )
// this starts the JetStream consumers
fsAPI := federationapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , nil , rsAPI , caches , nil , true )
rsAPI . SetFederationAPI ( fsAPI , nil )
// Create the room
if err := api . SendEvents ( ctx , rsAPI , api . KindNew , room . Events ( ) , "test" , "test" , api . DoNotSendToOtherServers , nil , false ) ; err != nil {
t . Fatalf ( "failed to send events: %v" , err )
}
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
AddPublicRoutes ( processCtx , routers , cfg , & natsInstance , nil , rsAPI , nil , nil , nil , userAPI , nil , nil , caching . DisableMetrics )
// Create the users in the userapi and login
2023-03-31 09:15:01 +01:00
accessTokens := map [ * test . User ] userDevice {
aliceAdmin : { } ,
2023-03-27 14:39:33 +01:00
}
createAccessTokens ( t , accessTokens , userAPI , ctx , routers )
testCases := [ ] struct {
name string
roomID string
wantOK bool
wantAffected [ ] string
} {
{ name : "Can evacuate existing room" , wantOK : true , roomID : room . ID , wantAffected : [ ] string { aliceAdmin . ID , bob . ID } } ,
{ name : "Can not evacuate non-existent room" , wantOK : false , roomID : "!doesnotexist:localhost" , wantAffected : [ ] string { } } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
req := test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/evacuateRoom/" + tc . roomID )
2023-03-31 09:15:01 +01:00
req . Header . Set ( "Authorization" , "Bearer " + accessTokens [ aliceAdmin ] . accessToken )
2023-03-27 14:39:33 +01:00
rec := httptest . NewRecorder ( )
routers . DendriteAdmin . ServeHTTP ( rec , req )
t . Logf ( "%s" , rec . Body . String ( ) )
if tc . wantOK && rec . Code != http . StatusOK {
t . Fatalf ( "expected http status %d, got %d: %s" , http . StatusOK , rec . Code , rec . Body . String ( ) )
}
affectedArr := gjson . GetBytes ( rec . Body . Bytes ( ) , "affected" ) . Array ( )
affected := make ( [ ] string , 0 , len ( affectedArr ) )
for _ , x := range affectedArr {
affected = append ( affected , x . Str )
}
if ! reflect . DeepEqual ( affected , tc . wantAffected ) {
t . Fatalf ( "expected affected %#v, but got %#v" , tc . wantAffected , affected )
}
} )
}
2023-04-03 19:19:26 +01:00
// Wait for the FS API to have consumed every message
js , _ := natsInstance . Prepare ( processCtx , & cfg . Global . JetStream )
timeout := time . After ( time . Second )
for {
select {
case <- timeout :
t . Fatalf ( "FS API didn't process all events in time" )
default :
}
info , err := js . ConsumerInfo ( cfg . Global . JetStream . Prefixed ( jetstream . OutputRoomEvent ) , cfg . Global . JetStream . Durable ( "FederationAPIRoomServerConsumer" ) + "Pull" )
if err != nil {
time . Sleep ( time . Millisecond * 10 )
continue
}
if info . NumPending == 0 && info . NumAckPending == 0 {
break
}
}
2023-03-27 14:39:33 +01:00
} )
}
func TestAdminEvacuateUser ( t * testing . T ) {
aliceAdmin := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeAdmin ) )
bob := test . NewUser ( t )
room := test . NewRoom ( t , aliceAdmin )
room2 := test . NewRoom ( t , aliceAdmin )
// Join Bob
room . CreateAndInsert ( t , bob , gomatrixserverlib . MRoomMember , map [ string ] interface { } {
"membership" : "join" ,
} , test . WithStateKey ( bob . ID ) )
room2 . CreateAndInsert ( t , bob , gomatrixserverlib . MRoomMember , map [ string ] interface { } {
"membership" : "join" ,
} , test . WithStateKey ( bob . ID ) )
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
cfg , processCtx , close := testrig . CreateConfig ( t , dbType )
caches := caching . NewRistrettoCache ( 128 * 1024 * 1024 , time . Hour , caching . DisableMetrics )
natsInstance := jetstream . NATSInstance { }
defer close ( )
routers := httputil . NewRouters ( )
cm := sqlutil . NewConnectionManager ( processCtx , cfg . Global . DatabaseOptions )
rsAPI := roomserver . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , caches , caching . DisableMetrics )
userAPI := userapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , rsAPI , nil )
// this starts the JetStream consumers
fsAPI := federationapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , basepkg . CreateFederationClient ( cfg , nil ) , rsAPI , caches , nil , true )
rsAPI . SetFederationAPI ( fsAPI , nil )
// Create the room
if err := api . SendEvents ( ctx , rsAPI , api . KindNew , room . Events ( ) , "test" , "test" , api . DoNotSendToOtherServers , nil , false ) ; err != nil {
t . Fatalf ( "failed to send events: %v" , err )
}
if err := api . SendEvents ( ctx , rsAPI , api . KindNew , room2 . Events ( ) , "test" , "test" , api . DoNotSendToOtherServers , nil , false ) ; err != nil {
t . Fatalf ( "failed to send events: %v" , err )
}
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
AddPublicRoutes ( processCtx , routers , cfg , & natsInstance , nil , rsAPI , nil , nil , nil , userAPI , nil , nil , caching . DisableMetrics )
// Create the users in the userapi and login
2023-03-31 09:15:01 +01:00
accessTokens := map [ * test . User ] userDevice {
aliceAdmin : { } ,
2023-03-27 14:39:33 +01:00
}
createAccessTokens ( t , accessTokens , userAPI , ctx , routers )
testCases := [ ] struct {
name string
userID string
wantOK bool
wantAffectedRooms [ ] string
} {
{ name : "Can evacuate existing user" , wantOK : true , userID : bob . ID , wantAffectedRooms : [ ] string { room . ID , room2 . ID } } ,
{ name : "invalid userID is rejected" , wantOK : false , userID : "!notauserid:test" , wantAffectedRooms : [ ] string { } } ,
{ name : "Can not evacuate user from different server" , wantOK : false , userID : "@doesnotexist:localhost" , wantAffectedRooms : [ ] string { } } ,
{ name : "Can not evacuate non-existent user" , wantOK : false , userID : "@doesnotexist:test" , wantAffectedRooms : [ ] string { } } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
req := test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/evacuateUser/" + tc . userID )
2023-03-31 09:15:01 +01:00
req . Header . Set ( "Authorization" , "Bearer " + accessTokens [ aliceAdmin ] . accessToken )
2023-03-27 14:39:33 +01:00
rec := httptest . NewRecorder ( )
routers . DendriteAdmin . ServeHTTP ( rec , req )
t . Logf ( "%s" , rec . Body . String ( ) )
if tc . wantOK && rec . Code != http . StatusOK {
t . Fatalf ( "expected http status %d, got %d: %s" , http . StatusOK , rec . Code , rec . Body . String ( ) )
}
affectedArr := gjson . GetBytes ( rec . Body . Bytes ( ) , "affected" ) . Array ( )
affected := make ( [ ] string , 0 , len ( affectedArr ) )
for _ , x := range affectedArr {
affected = append ( affected , x . Str )
}
if ! reflect . DeepEqual ( affected , tc . wantAffectedRooms ) {
t . Fatalf ( "expected affected %#v, but got %#v" , tc . wantAffectedRooms , affected )
}
} )
}
// Wait for the FS API to have consumed every message
js , _ := natsInstance . Prepare ( processCtx , & cfg . Global . JetStream )
timeout := time . After ( time . Second )
for {
select {
case <- timeout :
t . Fatalf ( "FS API didn't process all events in time" )
default :
}
info , err := js . ConsumerInfo ( cfg . Global . JetStream . Prefixed ( jetstream . OutputRoomEvent ) , cfg . Global . JetStream . Durable ( "FederationAPIRoomServerConsumer" ) + "Pull" )
if err != nil {
time . Sleep ( time . Millisecond * 10 )
continue
}
if info . NumPending == 0 && info . NumAckPending == 0 {
break
}
}
} )
}
func TestAdminMarkAsStale ( t * testing . T ) {
aliceAdmin := test . NewUser ( t , test . WithAccountType ( uapi . AccountTypeAdmin ) )
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
cfg , processCtx , close := testrig . CreateConfig ( t , dbType )
caches := caching . NewRistrettoCache ( 128 * 1024 * 1024 , time . Hour , caching . DisableMetrics )
natsInstance := jetstream . NATSInstance { }
defer close ( )
routers := httputil . NewRouters ( )
cm := sqlutil . NewConnectionManager ( processCtx , cfg . Global . DatabaseOptions )
rsAPI := roomserver . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , caches , caching . DisableMetrics )
userAPI := userapi . NewInternalAPI ( processCtx , cfg , cm , & natsInstance , rsAPI , nil )
// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
AddPublicRoutes ( processCtx , routers , cfg , & natsInstance , nil , rsAPI , nil , nil , nil , userAPI , nil , nil , caching . DisableMetrics )
// Create the users in the userapi and login
2023-03-31 09:15:01 +01:00
accessTokens := map [ * test . User ] userDevice {
aliceAdmin : { } ,
2023-03-27 14:39:33 +01:00
}
createAccessTokens ( t , accessTokens , userAPI , ctx , routers )
testCases := [ ] struct {
name string
userID string
wantOK bool
} {
{ name : "local user is not allowed" , userID : aliceAdmin . ID } ,
{ name : "invalid userID" , userID : "!notvalid:test" } ,
{ name : "remote user is allowed" , userID : "@alice:localhost" , wantOK : true } ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
req := test . NewRequest ( t , http . MethodPost , "/_dendrite/admin/refreshDevices/" + tc . userID )
2023-03-31 09:15:01 +01:00
req . Header . Set ( "Authorization" , "Bearer " + accessTokens [ aliceAdmin ] . accessToken )
2023-03-27 14:39:33 +01:00
rec := httptest . NewRecorder ( )
routers . DendriteAdmin . ServeHTTP ( rec , req )
t . Logf ( "%s" , rec . Body . String ( ) )
if tc . wantOK && rec . Code != http . StatusOK {
t . Fatalf ( "expected http status %d, got %d: %s" , http . StatusOK , rec . Code , rec . Body . String ( ) )
}
} )
}
} )
}