2022-02-10 10:27:26 +00:00
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2022-05-05 11:00:18 +01:00
package userapi_test
2020-06-15 09:54:11 +01:00
import (
"context"
"fmt"
"reflect"
2023-02-20 14:26:09 +00:00
"sync"
2020-06-15 09:54:11 +01:00
"testing"
2022-02-10 10:27:26 +00:00
"time"
2020-06-15 09:54:11 +01:00
2023-04-03 19:19:26 +01:00
api2 "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
2023-03-17 11:09:45 +00:00
"github.com/matrix-org/dendrite/internal/sqlutil"
2023-02-20 13:58:03 +00:00
"github.com/matrix-org/dendrite/userapi/producers"
2022-10-21 09:48:25 +01:00
"github.com/matrix-org/gomatrixserverlib"
2023-04-06 09:55:01 +01:00
"github.com/matrix-org/gomatrixserverlib/fclient"
2023-04-19 15:50:33 +01:00
"github.com/matrix-org/gomatrixserverlib/spec"
2023-02-20 13:58:03 +00:00
"github.com/matrix-org/util"
"github.com/nats-io/nats.go"
2022-10-21 09:48:25 +01:00
"golang.org/x/crypto/bcrypt"
2022-12-05 15:00:02 +00:00
"github.com/matrix-org/dendrite/setup/config"
2022-05-05 11:00:18 +01:00
"github.com/matrix-org/dendrite/test"
2022-10-18 15:59:08 +01:00
"github.com/matrix-org/dendrite/test/testrig"
2020-06-15 09:54:11 +01:00
"github.com/matrix-org/dendrite/userapi/api"
2022-03-03 11:40:53 +00:00
"github.com/matrix-org/dendrite/userapi/internal"
2022-02-18 11:31:05 +00:00
"github.com/matrix-org/dendrite/userapi/storage"
2020-06-15 09:54:11 +01:00
)
const (
2023-04-19 15:50:33 +01:00
serverName = spec . ServerName ( "example.com" )
2020-06-15 09:54:11 +01:00
)
2022-02-10 10:27:26 +00:00
type apiTestOpts struct {
loginTokenLifetime time . Duration
2023-02-20 13:58:03 +00:00
serverName string
2022-02-10 10:27:26 +00:00
}
2023-02-20 14:26:09 +00:00
type dummyProducer struct {
callCount sync . Map
t * testing . T
}
2023-02-20 13:58:03 +00:00
2023-02-20 14:26:09 +00:00
func ( d * dummyProducer ) PublishMsg ( msg * nats . Msg , opts ... nats . PubOpt ) ( * nats . PubAck , error ) {
count , loaded := d . callCount . LoadOrStore ( msg . Subject , 1 )
if loaded {
c , ok := count . ( int )
if ! ok {
d . t . Fatalf ( "unexpected type: %T with value %q" , c , c )
}
d . callCount . Store ( msg . Subject , c + 1 )
d . t . Logf ( "Incrementing call counter for %s" , msg . Subject )
}
2023-02-20 13:58:03 +00:00
return & nats . PubAck { } , nil
}
2023-02-20 14:26:09 +00:00
func MustMakeInternalAPI ( t * testing . T , opts apiTestOpts , dbType test . DBType , publisher producers . JetStreamPublisher ) ( api . UserInternalAPI , storage . UserDatabase , func ( ) ) {
2022-02-10 10:27:26 +00:00
if opts . loginTokenLifetime == 0 {
2022-02-18 11:31:05 +00:00
opts . loginTokenLifetime = api . DefaultLoginTokenLifetime * time . Millisecond
2022-02-10 10:27:26 +00:00
}
2023-03-22 08:21:32 +00:00
cfg , ctx , close := testrig . CreateConfig ( t , dbType )
2023-02-20 13:58:03 +00:00
sName := serverName
if opts . serverName != "" {
2023-04-19 15:50:33 +01:00
sName = spec . ServerName ( opts . serverName )
2023-02-20 13:58:03 +00:00
}
2023-03-22 08:21:32 +00:00
cm := sqlutil . NewConnectionManager ( ctx , cfg . Global . DatabaseOptions )
accountDB , err := storage . NewUserDatabase ( ctx . Context ( ) , cm , & cfg . UserAPI . AccountDatabase , sName , bcrypt . MinCost , config . DefaultOpenIDTokenLifetimeMS , opts . loginTokenLifetime , "" )
2020-06-15 09:54:11 +01:00
if err != nil {
t . Fatalf ( "failed to create account DB: %s" , err )
}
2022-02-10 10:27:26 +00:00
2023-03-22 08:21:32 +00:00
keyDB , err := storage . NewKeyDatabase ( cm , & cfg . KeyServer . Database )
2023-02-20 13:58:03 +00:00
if err != nil {
t . Fatalf ( "failed to create key DB: %s" , err )
}
2023-04-06 09:55:01 +01:00
cfg . Global . SigningIdentity = fclient . SigningIdentity {
2023-03-22 08:21:32 +00:00
ServerName : sName ,
2020-06-15 09:54:11 +01:00
}
2023-02-20 14:26:09 +00:00
if publisher == nil {
publisher = & dummyProducer { t : t }
}
syncProducer := producers . NewSyncAPI ( accountDB , publisher , "client_data" , "notification_data" )
keyChangeProducer := & producers . KeyChange { DB : keyDB , JetStream : publisher , Topic : "keychange" }
2022-03-03 11:40:53 +00:00
return & internal . UserInternalAPI {
2023-02-20 13:58:03 +00:00
DB : accountDB ,
KeyDatabase : keyDB ,
2023-03-22 08:21:32 +00:00
Config : & cfg . UserAPI ,
2023-02-20 13:58:03 +00:00
SyncProducer : syncProducer ,
KeyChangeProducer : keyChangeProducer ,
2022-10-18 15:59:08 +01:00
} , accountDB , func ( ) {
close ( )
}
2020-06-15 09:54:11 +01:00
}
func TestQueryProfile ( t * testing . T ) {
aliceAvatarURL := "mxc://example.com/alice"
aliceDisplayName := "Alice"
testCases := [ ] struct {
2023-04-03 19:19:26 +01:00
userID string
wantRes * authtypes . Profile
2020-06-15 09:54:11 +01:00
wantErr error
} {
{
2023-04-03 19:19:26 +01:00
userID : fmt . Sprintf ( "@alice:%s" , serverName ) ,
wantRes : & authtypes . Profile {
Localpart : "alice" ,
2020-06-15 09:54:11 +01:00
DisplayName : aliceDisplayName ,
2023-04-03 19:19:26 +01:00
AvatarURL : aliceAvatarURL ,
ServerName : string ( serverName ) ,
2020-06-15 09:54:11 +01:00
} ,
} ,
{
2023-04-03 19:19:26 +01:00
userID : fmt . Sprintf ( "@bob:%s" , serverName ) ,
wantErr : api2 . ErrProfileNotExists ,
2020-06-15 09:54:11 +01:00
} ,
{
2023-04-03 19:19:26 +01:00
userID : "@alice:wrongdomain.com" ,
wantErr : api2 . ErrProfileNotExists ,
2020-06-15 09:54:11 +01:00
} ,
}
2022-08-11 15:29:33 +01:00
runCases := func ( testAPI api . UserInternalAPI , http bool ) {
mode := "monolith"
if http {
mode = "HTTP"
}
2020-06-15 09:54:11 +01:00
for _ , tc := range testCases {
2023-04-03 19:19:26 +01:00
profile , gotErr := testAPI . QueryProfile ( context . TODO ( ) , tc . userID )
2020-06-15 09:54:11 +01:00
if tc . wantErr == nil && gotErr != nil || tc . wantErr != nil && gotErr == nil {
2022-08-11 15:29:33 +01:00
t . Errorf ( "QueryProfile %s error, got %s want %s" , mode , gotErr , tc . wantErr )
2020-06-15 09:54:11 +01:00
continue
}
2023-04-03 19:19:26 +01:00
if ! reflect . DeepEqual ( tc . wantRes , profile ) {
t . Errorf ( "QueryProfile %s response got %+v want %+v" , mode , profile , tc . wantRes )
2020-06-15 09:54:11 +01:00
}
}
}
2022-12-05 15:00:02 +00:00
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , accountDB , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-12-05 15:00:02 +00:00
defer close ( )
_ , err := accountDB . CreateAccount ( context . TODO ( ) , "alice" , serverName , "foobar" , "" , api . AccountTypeUser )
2020-06-15 09:54:11 +01:00
if err != nil {
2022-12-05 15:00:02 +00:00
t . Fatalf ( "failed to make account: %s" , err )
2020-06-15 09:54:11 +01:00
}
2022-12-05 15:00:02 +00:00
if _ , _ , err := accountDB . SetAvatarURL ( context . TODO ( ) , "alice" , serverName , aliceAvatarURL ) ; err != nil {
t . Fatalf ( "failed to set avatar url: %s" , err )
}
if _ , _ , err := accountDB . SetDisplayName ( context . TODO ( ) , "alice" , serverName , aliceDisplayName ) ; err != nil {
t . Fatalf ( "failed to set display name: %s" , err )
}
2023-02-14 11:47:47 +00:00
runCases ( userAPI , false )
2020-06-15 09:54:11 +01:00
} )
}
2022-02-10 10:27:26 +00:00
2022-10-10 10:45:15 +01:00
// TestPasswordlessLoginFails ensures that a passwordless account cannot
// be logged into using an arbitrary password (effectively a regression test
// for https://github.com/matrix-org/dendrite/issues/2780).
func TestPasswordlessLoginFails ( t * testing . T ) {
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , accountDB , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-10-10 10:45:15 +01:00
defer close ( )
2022-11-11 16:41:37 +00:00
_ , err := accountDB . CreateAccount ( ctx , "auser" , serverName , "" , "" , api . AccountTypeAppService )
2022-10-10 10:45:15 +01:00
if err != nil {
t . Fatalf ( "failed to make account: %s" , err )
}
userReq := & api . QueryAccountByPasswordRequest {
Localpart : "auser" ,
PlaintextPassword : "apassword" ,
}
userRes := & api . QueryAccountByPasswordResponse { }
if err := userAPI . QueryAccountByPassword ( ctx , userReq , userRes ) ; err != nil {
t . Fatal ( err )
}
if userRes . Exists || userRes . Account != nil {
t . Fatal ( "QueryAccountByPassword should not return correctly for a passwordless account" )
}
} )
}
2022-02-10 10:27:26 +00:00
func TestLoginToken ( t * testing . T ) {
ctx := context . Background ( )
t . Run ( "tokenLoginFlow" , func ( t * testing . T ) {
2022-05-05 11:00:18 +01:00
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , accountDB , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-05-05 11:00:18 +01:00
defer close ( )
2022-11-11 16:41:37 +00:00
_ , err := accountDB . CreateAccount ( ctx , "auser" , serverName , "apassword" , "" , api . AccountTypeUser )
2022-05-05 11:00:18 +01:00
if err != nil {
t . Fatalf ( "failed to make account: %s" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
t . Log ( "Creating a login token like the SSO callback would..." )
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
creq := api . PerformLoginTokenCreationRequest {
Data : api . LoginTokenData { UserID : "@auser:example.com" } ,
}
var cresp api . PerformLoginTokenCreationResponse
if err := userAPI . PerformLoginTokenCreation ( ctx , & creq , & cresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenCreation failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
if cresp . Metadata . Token == "" {
t . Errorf ( "PerformLoginTokenCreation Token: got %q, want non-empty" , cresp . Metadata . Token )
}
if cresp . Metadata . Expiration . Before ( time . Now ( ) ) {
t . Errorf ( "PerformLoginTokenCreation Expiration: got %v, want non-expired" , cresp . Metadata . Expiration )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
t . Log ( "Querying the login token like /login with m.login.token would..." )
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
qreq := api . QueryLoginTokenRequest { Token : cresp . Metadata . Token }
var qresp api . QueryLoginTokenResponse
if err := userAPI . QueryLoginToken ( ctx , & qreq , & qresp ) ; err != nil {
t . Fatalf ( "QueryLoginToken failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
if qresp . Data == nil {
t . Errorf ( "QueryLoginToken Data: got %v, want non-nil" , qresp . Data )
} else if want := "@auser:example.com" ; qresp . Data . UserID != want {
t . Errorf ( "QueryLoginToken UserID: got %q, want %q" , qresp . Data . UserID , want )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
t . Log ( "Deleting the login token like /login with m.login.token would..." )
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
dreq := api . PerformLoginTokenDeletionRequest { Token : cresp . Metadata . Token }
var dresp api . PerformLoginTokenDeletionResponse
if err := userAPI . PerformLoginTokenDeletion ( ctx , & dreq , & dresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenDeletion failed: %v" , err )
}
} )
2022-02-10 10:27:26 +00:00
} )
t . Run ( "expiredTokenIsNotReturned" , func ( t * testing . T ) {
2022-05-05 11:00:18 +01:00
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { loginTokenLifetime : - 1 * time . Second } , dbType , nil )
2022-05-05 11:00:18 +01:00
defer close ( )
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
creq := api . PerformLoginTokenCreationRequest {
Data : api . LoginTokenData { UserID : "@auser:example.com" } ,
}
var cresp api . PerformLoginTokenCreationResponse
if err := userAPI . PerformLoginTokenCreation ( ctx , & creq , & cresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenCreation failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
qreq := api . QueryLoginTokenRequest { Token : cresp . Metadata . Token }
var qresp api . QueryLoginTokenResponse
if err := userAPI . QueryLoginToken ( ctx , & qreq , & qresp ) ; err != nil {
t . Fatalf ( "QueryLoginToken failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
if qresp . Data != nil {
t . Errorf ( "QueryLoginToken Data: got %v, want nil" , qresp . Data )
}
} )
2022-02-10 10:27:26 +00:00
} )
t . Run ( "deleteWorks" , func ( t * testing . T ) {
2022-05-05 11:00:18 +01:00
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-05-05 11:00:18 +01:00
defer close ( )
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
creq := api . PerformLoginTokenCreationRequest {
Data : api . LoginTokenData { UserID : "@auser:example.com" } ,
}
var cresp api . PerformLoginTokenCreationResponse
if err := userAPI . PerformLoginTokenCreation ( ctx , & creq , & cresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenCreation failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
dreq := api . PerformLoginTokenDeletionRequest { Token : cresp . Metadata . Token }
var dresp api . PerformLoginTokenDeletionResponse
if err := userAPI . PerformLoginTokenDeletion ( ctx , & dreq , & dresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenDeletion failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
qreq := api . QueryLoginTokenRequest { Token : cresp . Metadata . Token }
var qresp api . QueryLoginTokenResponse
if err := userAPI . QueryLoginToken ( ctx , & qreq , & qresp ) ; err != nil {
t . Fatalf ( "QueryLoginToken failed: %v" , err )
}
2022-02-10 10:27:26 +00:00
2022-05-05 11:00:18 +01:00
if qresp . Data != nil {
t . Errorf ( "QueryLoginToken Data: got %v, want nil" , qresp . Data )
}
} )
2022-02-10 10:27:26 +00:00
} )
t . Run ( "deleteUnknownIsNoOp" , func ( t * testing . T ) {
2022-05-05 11:00:18 +01:00
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
userAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-05-05 11:00:18 +01:00
defer close ( )
dreq := api . PerformLoginTokenDeletionRequest { Token : "non-existent token" }
var dresp api . PerformLoginTokenDeletionResponse
if err := userAPI . PerformLoginTokenDeletion ( ctx , & dreq , & dresp ) ; err != nil {
t . Fatalf ( "PerformLoginTokenDeletion failed: %v" , err )
}
} )
2022-02-10 10:27:26 +00:00
} )
}
2022-12-22 12:05:59 +00:00
func TestQueryAccountByLocalpart ( t * testing . T ) {
alice := test . NewUser ( t )
localpart , userServername , _ := gomatrixserverlib . SplitID ( '@' , alice . ID )
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
intAPI , db , close := MustMakeInternalAPI ( t , apiTestOpts { } , dbType , nil )
2022-12-22 12:05:59 +00:00
defer close ( )
createdAcc , err := db . CreateAccount ( ctx , localpart , userServername , "" , "" , alice . AccountType )
if err != nil {
t . Error ( err )
}
testCases := func ( t * testing . T , internalAPI api . UserInternalAPI ) {
// Query existing account
queryAccResp := & api . QueryAccountByLocalpartResponse { }
if err = internalAPI . QueryAccountByLocalpart ( ctx , & api . QueryAccountByLocalpartRequest {
Localpart : localpart ,
ServerName : userServername ,
} , queryAccResp ) ; err != nil {
t . Error ( err )
}
if ! reflect . DeepEqual ( createdAcc , queryAccResp . Account ) {
t . Fatalf ( "created and queried accounts don't match:\n%+v vs.\n%+v" , createdAcc , queryAccResp . Account )
}
// Query non-existent account, this should result in an error
err = internalAPI . QueryAccountByLocalpart ( ctx , & api . QueryAccountByLocalpartRequest {
Localpart : "doesnotexist" ,
ServerName : userServername ,
} , queryAccResp )
if err == nil {
t . Fatalf ( "expected an error, but got none: %+v" , queryAccResp )
}
}
2023-02-14 11:47:47 +00:00
testCases ( t , intAPI )
2022-12-22 12:05:59 +00:00
} )
}
2023-02-20 13:58:03 +00:00
func TestAccountData ( t * testing . T ) {
ctx := context . Background ( )
alice := test . NewUser ( t )
testCases := [ ] struct {
name string
inputData * api . InputAccountDataRequest
wantErr bool
} {
{
name : "not a local user" ,
inputData : & api . InputAccountDataRequest { UserID : "@notlocal:example.com" } ,
wantErr : true ,
} ,
{
name : "local user missing datatype" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID } ,
wantErr : true ,
} ,
{
name : "missing json" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID , DataType : "m.push_rules" , AccountData : nil } ,
wantErr : true ,
} ,
{
name : "with json" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID , DataType : "m.push_rules" , AccountData : [ ] byte ( "{}" ) } ,
} ,
{
name : "room data" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID , DataType : "m.push_rules" , AccountData : [ ] byte ( "{}" ) , RoomID : "!dummy:test" } ,
} ,
{
name : "ignored users" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID , DataType : "m.ignored_user_list" , AccountData : [ ] byte ( "{}" ) } ,
} ,
{
name : "m.fully_read" ,
inputData : & api . InputAccountDataRequest { UserID : alice . ID , DataType : "m.fully_read" , AccountData : [ ] byte ( "{}" ) } ,
} ,
}
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
intAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { serverName : "test" } , dbType , nil )
2023-02-20 13:58:03 +00:00
defer close ( )
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
res := api . InputAccountDataResponse { }
err := intAPI . InputAccountData ( ctx , tc . inputData , & res )
if tc . wantErr && err == nil {
t . Fatalf ( "expected an error, but got none" )
}
if ! tc . wantErr && err != nil {
t . Fatalf ( "expected no error, but got: %s" , err )
}
// query the data again and compare
queryRes := api . QueryAccountDataResponse { }
queryReq := api . QueryAccountDataRequest {
UserID : tc . inputData . UserID ,
DataType : tc . inputData . DataType ,
RoomID : tc . inputData . RoomID ,
}
err = intAPI . QueryAccountData ( ctx , & queryReq , & queryRes )
if err != nil && ! tc . wantErr {
t . Fatal ( err )
}
// verify global data
if tc . inputData . RoomID == "" {
if ! reflect . DeepEqual ( tc . inputData . AccountData , queryRes . GlobalAccountData [ tc . inputData . DataType ] ) {
t . Fatalf ( "expected accountdata to be %s, got %s" , string ( tc . inputData . AccountData ) , string ( queryRes . GlobalAccountData [ tc . inputData . DataType ] ) )
}
} else {
// verify room data
if ! reflect . DeepEqual ( tc . inputData . AccountData , queryRes . RoomAccountData [ tc . inputData . RoomID ] [ tc . inputData . DataType ] ) {
t . Fatalf ( "expected accountdata to be %s, got %s" , string ( tc . inputData . AccountData ) , string ( queryRes . RoomAccountData [ tc . inputData . RoomID ] [ tc . inputData . DataType ] ) )
}
}
} )
}
} )
}
func TestDevices ( t * testing . T ) {
ctx := context . Background ( )
dupeAccessToken := util . RandomString ( 8 )
displayName := "testing"
creationTests := [ ] struct {
name string
inputData * api . PerformDeviceCreationRequest
wantErr bool
wantNewDevID bool
} {
{
name : "not a local user" ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test1" , ServerName : "notlocal" } ,
wantErr : true ,
} ,
{
name : "implicit local user" ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test1" , AccessToken : util . RandomString ( 8 ) , NoDeviceListUpdate : true , DeviceDisplayName : & displayName } ,
} ,
{
name : "explicit local user" ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test2" , ServerName : "test" , AccessToken : util . RandomString ( 8 ) , NoDeviceListUpdate : true } ,
} ,
{
name : "dupe token - ok" ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test3" , ServerName : "test" , AccessToken : dupeAccessToken , NoDeviceListUpdate : true } ,
} ,
{
name : "dupe token - not ok" ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test3" , ServerName : "test" , AccessToken : dupeAccessToken , NoDeviceListUpdate : true } ,
wantErr : true ,
} ,
{
name : "test3 second device" , // used to test deletion later
inputData : & api . PerformDeviceCreationRequest { Localpart : "test3" , ServerName : "test" , AccessToken : util . RandomString ( 8 ) , NoDeviceListUpdate : true } ,
} ,
{
name : "test3 third device" , // used to test deletion later
wantNewDevID : true ,
inputData : & api . PerformDeviceCreationRequest { Localpart : "test3" , ServerName : "test" , AccessToken : util . RandomString ( 8 ) , NoDeviceListUpdate : true } ,
} ,
}
deletionTests := [ ] struct {
name string
inputData * api . PerformDeviceDeletionRequest
wantErr bool
wantDevices int
} {
{
name : "deletion - not a local user" ,
inputData : & api . PerformDeviceDeletionRequest { UserID : "@test:notlocalhost" } ,
wantErr : true ,
} ,
{
name : "deleting not existing devices should not error" ,
inputData : & api . PerformDeviceDeletionRequest { UserID : "@test1:test" , DeviceIDs : [ ] string { "iDontExist" } } ,
wantDevices : 1 ,
} ,
{
name : "delete all devices" ,
inputData : & api . PerformDeviceDeletionRequest { UserID : "@test1:test" } ,
wantDevices : 0 ,
} ,
{
name : "delete all devices" ,
inputData : & api . PerformDeviceDeletionRequest { UserID : "@test3:test" } ,
wantDevices : 0 ,
} ,
}
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
intAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { serverName : "test" } , dbType , nil )
2023-02-20 13:58:03 +00:00
defer close ( )
for _ , tc := range creationTests {
t . Run ( tc . name , func ( t * testing . T ) {
res := api . PerformDeviceCreationResponse { }
deviceID := util . RandomString ( 8 )
tc . inputData . DeviceID = & deviceID
if tc . wantNewDevID {
tc . inputData . DeviceID = nil
}
err := intAPI . PerformDeviceCreation ( ctx , tc . inputData , & res )
if tc . wantErr && err == nil {
t . Fatalf ( "expected an error, but got none" )
}
if ! tc . wantErr && err != nil {
t . Fatalf ( "expected no error, but got: %s" , err )
}
if ! res . DeviceCreated {
return
}
queryDevicesRes := api . QueryDevicesResponse { }
queryDevicesReq := api . QueryDevicesRequest { UserID : res . Device . UserID }
if err = intAPI . QueryDevices ( ctx , & queryDevicesReq , & queryDevicesRes ) ; err != nil {
t . Fatal ( err )
}
// We only want to verify one device
if len ( queryDevicesRes . Devices ) > 1 {
return
}
res . Device . AccessToken = ""
// At this point, there should only be one device
if ! reflect . DeepEqual ( * res . Device , queryDevicesRes . Devices [ 0 ] ) {
t . Fatalf ( "expected device to be\n%#v, got \n%#v" , * res . Device , queryDevicesRes . Devices [ 0 ] )
}
newDisplayName := "new name"
if tc . inputData . DeviceDisplayName == nil {
updateRes := api . PerformDeviceUpdateResponse { }
updateReq := api . PerformDeviceUpdateRequest {
RequestingUserID : fmt . Sprintf ( "@%s:%s" , tc . inputData . Localpart , "test" ) ,
DeviceID : deviceID ,
DisplayName : & newDisplayName ,
}
if err = intAPI . PerformDeviceUpdate ( ctx , & updateReq , & updateRes ) ; err != nil {
t . Fatal ( err )
}
}
queryDeviceInfosRes := api . QueryDeviceInfosResponse { }
queryDeviceInfosReq := api . QueryDeviceInfosRequest { DeviceIDs : [ ] string { * tc . inputData . DeviceID } }
if err = intAPI . QueryDeviceInfos ( ctx , & queryDeviceInfosReq , & queryDeviceInfosRes ) ; err != nil {
t . Fatal ( err )
}
gotDisplayName := queryDeviceInfosRes . DeviceInfo [ * tc . inputData . DeviceID ] . DisplayName
if tc . inputData . DeviceDisplayName != nil {
wantDisplayName := * tc . inputData . DeviceDisplayName
if wantDisplayName != gotDisplayName {
t . Fatalf ( "expected displayName to be %s, got %s" , wantDisplayName , gotDisplayName )
}
} else {
wantDisplayName := newDisplayName
if wantDisplayName != gotDisplayName {
t . Fatalf ( "expected displayName to be %s, got %s" , wantDisplayName , gotDisplayName )
}
}
} )
}
for _ , tc := range deletionTests {
t . Run ( tc . name , func ( t * testing . T ) {
delRes := api . PerformDeviceDeletionResponse { }
err := intAPI . PerformDeviceDeletion ( ctx , tc . inputData , & delRes )
if tc . wantErr && err == nil {
t . Fatalf ( "expected an error, but got none" )
}
if ! tc . wantErr && err != nil {
t . Fatalf ( "expected no error, but got: %s" , err )
}
if tc . wantErr {
return
}
queryDevicesRes := api . QueryDevicesResponse { }
queryDevicesReq := api . QueryDevicesRequest { UserID : tc . inputData . UserID }
if err = intAPI . QueryDevices ( ctx , & queryDevicesReq , & queryDevicesRes ) ; err != nil {
t . Fatal ( err )
}
if len ( queryDevicesRes . Devices ) != tc . wantDevices {
t . Fatalf ( "expected %d devices, got %d" , tc . wantDevices , len ( queryDevicesRes . Devices ) )
}
} )
}
} )
}
// Tests that the session ID of a device is not reused when reusing the same device ID.
func TestDeviceIDReuse ( t * testing . T ) {
ctx := context . Background ( )
test . WithAllDatabases ( t , func ( t * testing . T , dbType test . DBType ) {
2023-02-20 14:26:09 +00:00
publisher := & dummyProducer { t : t }
intAPI , _ , close := MustMakeInternalAPI ( t , apiTestOpts { serverName : "test" } , dbType , publisher )
2023-02-20 13:58:03 +00:00
defer close ( )
res := api . PerformDeviceCreationResponse { }
// create a first device
deviceID := util . RandomString ( 8 )
req := api . PerformDeviceCreationRequest { Localpart : "alice" , ServerName : "test" , DeviceID : & deviceID , NoDeviceListUpdate : true }
err := intAPI . PerformDeviceCreation ( ctx , & req , & res )
if err != nil {
t . Fatal ( err )
}
// Do the same request again, we expect a different sessionID
res2 := api . PerformDeviceCreationResponse { }
2023-02-20 14:26:09 +00:00
// Set NoDeviceListUpdate to false, to verify we don't send device list updates when
// reusing the same device ID
req . NoDeviceListUpdate = false
2023-02-20 13:58:03 +00:00
err = intAPI . PerformDeviceCreation ( ctx , & req , & res2 )
if err != nil {
t . Fatalf ( "expected no error, but got: %v" , err )
}
if res2 . Device . SessionID == res . Device . SessionID {
t . Fatalf ( "expected a different session ID, but they are the same" )
}
2023-02-20 14:26:09 +00:00
publisher . callCount . Range ( func ( key , value any ) bool {
if value != nil {
t . Fatalf ( "expected publisher to not get called, but got value %d for subject %s" , value , key )
}
return true
} )
2023-02-20 13:58:03 +00:00
} )
}