Select Git revision
Forked from
ai3 / tools / acmeserver
Source project has a limited visibility.
actions_test.go 23.45 KiB
package accountserver
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"testing"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"git.autistici.org/ai3/go-common/pwhash"
"git.autistici.org/id/auth"
sso "git.autistici.org/id/go-sso"
)
const testUser = "testuser@example.com"
type fakeBackend struct {
users map[string]*User
resources map[string]*Resource
passwords map[string]string
recoveryPasswords map[string]string
resourcePasswords map[string]string
appSpecificPasswords map[string][]*AppSpecificPasswordInfo
encryptionKeys map[string][]*ct.EncryptedKey
}
func (b *fakeBackend) NewTransaction() (TX, error) {
return b, nil
}
func (b *fakeBackend) Commit(_ context.Context) error {
return nil
}
func (b *fakeBackend) NextUID(_ context.Context) (int, error) {
return 42, nil
}
func (b *fakeBackend) CanAccessResource(_ context.Context, username string, rsrc *Resource) bool {
owner := strings.Split(string(rsrc.ID), "/")[0]
return owner == username
}
func (b *fakeBackend) GetUser(_ context.Context, username string) (*RawUser, error) {
u, ok := b.users[username]
if !ok {
return nil, errors.New("user not found in fake backend")
}
return &RawUser{
User: *u,
Password: b.passwords[username],
RecoveryPassword: b.recoveryPasswords[username],
Keys: b.encryptionKeys[username],
}, nil
}
func (b *fakeBackend) SearchUser(_ context.Context, pattern string, limit int) ([]string, error) {
var out []string
for username := range b.users {
if strings.HasPrefix(username, pattern) {
out = append(out, username)
}
}
return out, nil
}
func (b *fakeBackend) UpdateUser(_ context.Context, user *User) error {
b.users[user.Name] = user
return nil
}
func (b *fakeBackend) CreateUser(_ context.Context, user *User) (*User, error) {
for _, r := range user.Resources {
r.ID = makeResourceID(user.Name, r.Type, r.Name)
}
b.users[user.Name] = user
return user, nil
}
func (b *fakeBackend) GetResource(_ context.Context, resourceID ResourceID) (*RawResource, error) {
owner := strings.Split(resourceID.String(), "/")[0]
r := b.resources[resourceID.String()]
return &RawResource{Resource: *r, Owner: owner}, nil
}
func (b *fakeBackend) FindResource(_ context.Context, req FindResourceRequest) (*RawResource, error) {
for id, r := range b.resources {
if r.Type == req.Type && r.Name == req.Name {
owner := strings.Split(id, "/")[0]
return &RawResource{Resource: *r, Owner: owner}, nil
}
}
return nil, nil
}
func (b *fakeBackend) SearchResource(_ context.Context, pattern string, limit int) ([]*RawResource, error) {
var out []*RawResource
for id, r := range b.resources {
owner := strings.Split(id, "/")[0]
// Emulate LDAP wildcard syntax.
if ok, _ := filepath.Match(pattern, r.Name); ok {
out = append(out, &RawResource{Resource: *r, Owner: owner})
}
}
return out, nil
}
func (b *fakeBackend) UpdateResource(_ context.Context, r *Resource) error {
b.resources[r.ID.String()] = r
return nil
}
func makeResourceID(owner, rtype, rname string) ResourceID {
return ResourceID(fmt.Sprintf("%s/%s/%s", owner, rtype, rname))
}
func (b *fakeBackend) CreateResources(_ context.Context, u *User, rsrcs []*Resource) ([]*Resource, error) {
var out []*Resource
for _, r := range rsrcs {
if !r.ID.Empty() {
return nil, errors.New("resource ID not empty")
}
var username string
if u != nil {
username = u.Name
}
r.ID = makeResourceID(username, r.Type, r.Name)
if _, ok := b.resources[r.ID.String()]; ok {
return nil, errors.New("resource already exists")
}
b.resources[r.ID.String()] = r
out = append(out, r)
}
return out, nil
}
func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password string) error {
b.passwords[user.Name] = password
return nil
}
func (b *fakeBackend) SetAccountRecoveryHint(_ context.Context, user *User, hint, response string) error {
b.users[user.Name].AccountRecoveryHint = hint
b.recoveryPasswords[user.Name] = response
return nil
}
func (b *fakeBackend) DeleteAccountRecoveryHint(_ context.Context, user *User) error {
b.users[user.Name].AccountRecoveryHint = ""
delete(b.recoveryPasswords, user.Name)
return nil
}
func (b *fakeBackend) SetResourcePassword(_ context.Context, r *Resource, password string) error {
b.resourcePasswords[r.ID.String()] = password
return nil
}
func (b *fakeBackend) GetUserEncryptionKeys(_ context.Context, user *User) ([]*ct.EncryptedKey, error) {
return b.encryptionKeys[user.Name], nil
}
func (b *fakeBackend) SetUserEncryptionKeys(_ context.Context, user *User, keys []*ct.EncryptedKey) error {
b.encryptionKeys[user.Name] = keys
b.users[user.Name].HasEncryptionKeys = true
return nil
}
func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User, pub []byte) error {
return nil
}
func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *AppSpecificPasswordInfo, _ string) error {
b.appSpecificPasswords[user.Name] = append(b.appSpecificPasswords[user.Name], info)
return nil
}
func (b *fakeBackend) DeleteApplicationSpecificPassword(_ context.Context, user *User, id string) error {
return nil
}
func (b *fakeBackend) SetUserTOTPSecret(_ context.Context, user *User, secret string) error {
return nil
}
func (b *fakeBackend) DeleteUserTOTPSecret(_ context.Context, user *User) error {
return nil
}
func (b *fakeBackend) HasAnyResource(_ context.Context, rsrcs []FindResourceRequest) (bool, error) {
for _, fr := range rsrcs {
for _, r := range b.resources {
if r.Type == fr.Type && r.Name == fr.Name {
return true, nil
}
}
}
return false, nil
}
// Make the fakeBackend match the auth.Client interface.
func (b *fakeBackend) Authenticate(_ context.Context, req *auth.Request) (*auth.Response, error) {
var pw string
var ok bool
switch req.Service {
case defaultUserAuthService:
pw, ok = b.passwords[req.Username]
case defaultAccountRecoveryAuthService:
pw, ok = b.recoveryPasswords[req.Username]
default:
return nil, errors.New("unknown service")
}
if !ok {
return nil, errors.New("no such user")
}
if !pwhash.ComparePassword(pw, string(req.Password)) {
return &auth.Response{Status: auth.StatusError}, nil
}
return &auth.Response{Status: auth.StatusOK}, nil
}
const testAdminGroupName = "admins"
// Fake SSO validator: the sso ticket username is just the ticket
// itself. The only invalid value is the empty string.
type fakeValidator struct {
adminUser string
}
func (v *fakeValidator) Validate(tkt, nonce, service string, _ []string) (*sso.Ticket, error) {
if tkt == "" {
return nil, errors.New("empty sso ticket")
}
var groups []string
if tkt == v.adminUser {
groups = []string{testAdminGroupName}
}
return &sso.Ticket{
User: tkt,
Service: service,
Domain: "test",
Groups: groups,
}, nil
}
func (b *fakeBackend) addUser(user *User, pw, rpw string) {
b.users[user.Name] = user
//b.resources[user.Name] = make(map[string]*Resource)
b.passwords[user.Name] = pwhash.Encrypt(pw)
if rpw != "" {
b.recoveryPasswords[user.Name] = pwhash.Encrypt(rpw)
}
for _, r := range user.Resources {
b.resources[r.ID.String()] = r
}
}
func createFakeBackend() *fakeBackend {
fb := &fakeBackend{
users: make(map[string]*User),
resources: make(map[string]*Resource),
passwords: make(map[string]string),
recoveryPasswords: make(map[string]string),
resourcePasswords: make(map[string]string),
appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo),
encryptionKeys: make(map[string][]*ct.EncryptedKey),
}
fb.addUser(&User{
Name: testUser,
Status: UserStatusActive,
Shard: "host1",
UID: 4242,
Resources: []*Resource{
{
ID: makeResourceID(testUser, ResourceTypeEmail, testUser),
Name: testUser,
Type: ResourceTypeEmail,
Status: ResourceStatusActive,
Shard: "host1",
OriginalShard: "host1",
Email: &Email{
Maildir: "example.com/testuser",
},
},
{
ID: makeResourceID(testUser, ResourceTypeDAV, "dav1"),
Name: "dav1",
Type: ResourceTypeDAV,
Status: ResourceStatusActive,
DAV: &WebDAV{
UID: 4242,
Homedir: "/home/dav1",
},
},
},
}, "password", "recoverypassword")
return fb
}
func testConfig() *Config {
var c Config
c.Validation.ForbiddenUsernames = []string{"root"}
c.Validation.AvailableDomains = map[string][]string{
ResourceTypeEmail: []string{"example.com"},
ResourceTypeMailingList: []string{"example.com"},
}
c.SSO.Domain = "mydomain"
c.SSO.Service = "service/"
c.SSO.AdminGroup = testAdminGroupName
c.Shards.Available = map[string][]string{
ResourceTypeEmail: []string{"host1", "host2", "host3"},
ResourceTypeMailingList: []string{"host1", "host2", "host3"},
ResourceTypeWebsite: []string{"host1", "host2", "host3"},
ResourceTypeDomain: []string{"host1", "host2", "host3"},
ResourceTypeDAV: []string{"host1", "host2", "host3"},
}
c.Shards.Allowed = c.Shards.Available
return &c
}
func testService(admin string) *AccountService {
be := createFakeBackend()
svc, _ := newAccountServiceInternal(be, testConfig(), &fakeValidator{admin}, be)
return svc
}
func getUser(t testing.TB, svc *AccountService, username string) *User {
req := &GetUserRequest{
UserRequestBase: UserRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
Username: testUser,
},
IncludeInactive: true,
}
resp, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal(err)
}
return resp.(*User)
}
func TestService_GetUser(t *testing.T) {
svc := testService("")
user := getUser(t, svc, testUser)
if user.Name != testUser {
t.Fatalf("bad response: %+v", user)
}
}
func TestService_GetUser_ResourceGroups(t *testing.T) {
fb := createFakeBackend()
svc, _ := newAccountServiceInternal(fb, testConfig(), &fakeValidator{}, nil)
fb.addUser(&User{
Name: "testuser2",
Status: UserStatusActive,
Resources: []*Resource{
{
ID: makeResourceID("testuser2", ResourceTypeDAV, "dav1"),
Type: ResourceTypeDAV,
Name: "dav1",
DAV: &WebDAV{
Homedir: "/home/users/investici.org/dav1",
},
},
{
ID: makeResourceID("testuser2", ResourceTypeDAV, "dav1-domain2"),
Type: ResourceTypeDAV,
Name: "dav1-domain2",
DAV: &WebDAV{
Homedir: "/home/users/investici.org/dav1/html-domain2.com/subdir",
},
},
{
ID: makeResourceID("testuser2", ResourceTypeDomain, "domain1.com"),
Type: ResourceTypeDomain,
Name: "domain1.com",
Website: &Website{
DocumentRoot: "/home/users/investici.org/dav1/html-domain1.com",
},
},
{
ID: makeResourceID("testuser2", ResourceTypeDomain, "domain2.com"),
Type: ResourceTypeDomain,
Name: "domain2.com",
Website: &Website{
DocumentRoot: "/home/users/investici.org/dav1/html-domain2.com",
},
},
{
ID: makeResourceID("testuser2", ResourceTypeDatabase, "db2"),
ParentID: makeResourceID("testuser2", ResourceTypeDomain, "domain2.com"),
Type: ResourceTypeDatabase,
Name: "db2",
Database: &Database{},
},
},
}, "", "")
req := &GetUserRequest{
UserRequestBase: UserRequestBase{
RequestBase: RequestBase{
SSO: "testuser2",
},
Username: "testuser2",
},
}
resp, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal(err)
}
user := resp.(*User)
var grouped []*Resource
for _, r := range user.Resources {
switch r.Type {
case ResourceTypeWebsite, ResourceTypeDomain, ResourceTypeDAV, ResourceTypeDatabase:
grouped = append(grouped, r)
}
}
var group string
for _, r := range grouped {
if r.Group == "" {
t.Errorf("group not set on %s", r.ID)
continue
}
if group == "" {
group = r.Group
} else if group != r.Group {
t.Errorf("wrong group on %s (%s, expected %s)", r.ID, r.Group, group)
}
}
}
func TestService_Auth(t *testing.T) {
svc := testService("adminuser")
for _, td := range []struct {
sso string
expectedOk bool
}{
{testUser, true},
{"otheruser", false},
{"adminuser", true},
} {
req := &GetUserRequest{
UserRequestBase: UserRequestBase{
RequestBase: RequestBase{
SSO: td.sso,
},
Username: testUser,
},
}
_, err := svc.Handle(context.TODO(), req)
if err != nil {
if !IsAuthError(err) {
t.Errorf("error for sso_user=%s is not an auth error: %v", td.sso, err)
} else if td.expectedOk {
t.Errorf("error with sso_user=%s: %v", td.sso, err)
}
} else if !td.expectedOk {
t.Errorf("no error with sso_user=%s", td.sso)
}
}
}
func TestService_ChangePassword(t *testing.T) {
fb := createFakeBackend()
svc, _ := newAccountServiceInternal(fb, testConfig(), &fakeValidator{}, fb)
testdata := []struct {
password string
newPassword string
expectedOk bool
}{
// First, fail cur_password authentication.
{"BADPASS", "new_password", false},
// Ordering is important as it is meant to emulate
// setting the password, failing to reset it, then
// succeeding.
{"password", "new_password", true},
{"BADPASS", "new_password_2", false},
{"new_password", "new_password_2", true},
}
for _, td := range testdata {
req := &ChangeUserPasswordRequest{
PrivilegedRequestBase: PrivilegedRequestBase{
UserRequestBase: UserRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
Username: testUser,
},
CurPassword: td.password,
},
Password: td.newPassword,
}
_, err := svc.Handle(context.TODO(), req)
if err == nil && !td.expectedOk {
t.Fatalf("ChangeUserPassword(old=%s new=%s) should have failed but didn't", td.password, td.newPassword)
} else if err != nil && td.expectedOk {
t.Fatalf("ChangeUserPassword(old=%s new=%s) failed: %v", td.password, td.newPassword, err)
}
}
if _, ok := fb.passwords[testUser]; !ok {
t.Error("password was not set on the backend")
}
// if len(fb.encryptionKeys[testUser]) != 1 {
// t.Errorf("no encryption keys were set")
// }
}
// Lower level test that basically corresponds to the same operations
// as TestService_ChangePassword above, but exercises the
// initializeUserEncryptionKeys / updateUserEncryptionKeys code path
// directly.
// func TestService_EncryptionKeys(t *testing.T) {
// fb := createFakeBackend()
// svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
// tx, _ := fb.NewTransaction()
// ctx := context.Background()
// user, _ := getUserOrDie(ctx, tx, testUser)
// // Set the keys to something.
// keys, _, err := svc.initializeEncryptionKeys(ctx, tx, user, "password")
// if err != nil {
// t.Fatal("init", err)
// }
// if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil {
// t.Fatal("SetUserEncryptionKeys", err)
// }
// if n := len(fb.encryptionKeys[testUser]); n != 1 {
// t.Fatalf("found %d encryption keys, expected 1", n)
// }
// // Try to read (decrypt) them again using bad / good passwords.
// if _, _, err := svc.readOrInitializeEncryptionKeys(ctx, tx, user, "BADPASS", "new_password"); err == nil {
// t.Fatal("read with bad password did not fail")
// }
// if _, _, err := svc.readOrInitializeEncryptionKeys(ctx, tx, user, "password", "new_password"); err != nil {
// t.Fatal("readOrInitialize", err)
// }
// }
// Try adding aliases to the email resource.
func TestService_AddEmailAlias(t *testing.T) {
svc := testService("")
// Find the resource ID.
user := getUser(t, svc, testUser)
emailID := user.GetSingleResourceByType(ResourceTypeEmail).ID
testdata := []struct {
addr string
expectedOk bool
}{
{"alias@example.com", true},
{"another-example-address@example.com", true},
{"root@example.com", false},
{"alias@other-domain.com", false},
}
for _, td := range testdata {
req := &AddEmailAliasRequest{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
ResourceID: emailID,
},
Addr: td.addr,
}
_, err := svc.Handle(context.TODO(), req)
if err != nil && td.expectedOk {
t.Errorf("AddEmailAlias(%s) failed: %v", td.addr, err)
} else if err == nil && !td.expectedOk {
t.Errorf("AddEmailAlias(%s) did not fail but should have", td.addr)
}
}
}
func TestService_CreateResource(t *testing.T) {
svc := testService("admin")
req := &CreateResourcesRequest{
AdminRequestBase: AdminRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
},
Username: testUser,
Resources: []*Resource{
&Resource{
Type: ResourceTypeDAV,
Name: "dav2",
Status: ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
DAV: &WebDAV{
Homedir: "/home/dav2",
},
},
},
}
// The request should succeed the first time around.
obj, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal("CreateResources", err)
}
resp := obj.(*CreateResourcesResponse)
// Check that a ResourceID has been set.
if len(resp.Resources) != 1 {
t.Fatalf("bad response, expected 1 resource, got %+v", resp)
}
rsrc := resp.Resources[0]
if rsrc.ID.Empty() {
t.Fatal("Resource ID is empty!")
}
// The object already exists, so the same request should fail now.
_, err = svc.Handle(context.TODO(), req)
if err == nil {
t.Fatal("creating a duplicate resource did not fail")
}
}
func TestService_CreateResource_List(t *testing.T) {
svc := testService("admin")
// A list is an example of a user-less (global) resource.
req := &CreateResourcesRequest{
AdminRequestBase: AdminRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
},
Username: testUser,
Resources: []*Resource{
&Resource{
Type: ResourceTypeMailingList,
Name: "list@example.com",
Status: ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
List: &MailingList{
Admins: []string{testUser},
},
},
},
}
// The request should succeed.
_, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal("CreateResources", err)
}
}
func TestService_UpdateResource(t *testing.T) {
svc := testService("admin")
// Find the resource, modify a copy of it.
user := getUser(t, svc, testUser)
email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
email.Email.QuotaLimit = 9000
req := &AdminUpdateResourceRequest{
AdminResourceRequestBase: AdminResourceRequestBase{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
ResourceID: email.ID,
},
},
Resource: email,
}
_, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal("UpdateResource", err)
}
}
func TestService_UpdateResource_FailsValidation(t *testing.T) {
svc := testService("admin")
// Find the resource, modify a copy of it, but set a parameter
// that will fail validation (i.e. a bad shard).
user := getUser(t, svc, testUser)
email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
email.Shard = "invalid-shard"
req := &AdminUpdateResourceRequest{
AdminResourceRequestBase: AdminResourceRequestBase{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
ResourceID: email.ID,
},
},
Resource: email,
}
_, err := svc.Handle(context.TODO(), req)
if err == nil {
t.Fatal("UpdateResource() accepted an invalid 'shard' attribute")
}
}
func TestService_CreateUser(t *testing.T) {
svc := testService("admin")
//emailResourceID := NewResourceID(ResourceTypeEmail, "testuser2@example.com", "testuser2@example.com")
req := &CreateUserRequest{
AdminRequestBase: AdminRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
},
User: &User{
Name: "testuser2@example.com",
Resources: []*Resource{
&Resource{
Type: ResourceTypeEmail,
Name: "testuser2@example.com",
//Status: ResourceStatusActive,
//Shard: "host2",
//OriginalShard: "host2",
Email: &Email{
Maildir: "example.com/testuser2",
},
},
},
},
}
// The request should succeed the first time around.
resp, err := svc.Handle(context.Background(), req)
if err != nil {
t.Fatal("CreateResources", err)
}
cresp := resp.(*CreateUserResponse)
if cresp.User.Name != "testuser2@example.com" {
t.Fatalf("unexpected user in response: got %s, expected testuser2", cresp.User.Name)
}
// Hit the database to verify that the object has been created.
tx, _ := svc.backend.NewTransaction()
user, _ := tx.GetUser(context.Background(), "testuser2@example.com")
if user == nil {
t.Fatal("GetUser returned nil")
}
// Verify that the new resource has default fields set.
resource := user.GetSingleResourceByType(ResourceTypeEmail)
if resource == nil {
t.Fatalf("no email resource in user %+v", user)
}
if resource.Shard == "" {
t.Fatalf("resource shard is unset: %+v", resource)
}
}
func TestService_CreateUser_FailIfNotAdmin(t *testing.T) {
svc := testService("admin")
req := &CreateUserRequest{
AdminRequestBase: AdminRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
},
User: &User{
Name: "testuser2@example.com",
Resources: []*Resource{
&Resource{
Type: ResourceTypeEmail,
Name: "testuser2@example.com",
Status: ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
Email: &Email{
Maildir: "example.com/testuser2",
},
},
},
},
}
// The request should succeed the first time around.
_, err := svc.Handle(context.TODO(), req)
if err == nil {
t.Fatal("unauthorized CreateResources did not fail")
}
}
func TestService_ResetResourcePassword(t *testing.T) {
svc := testService("")
user := getUser(t, svc, testUser)
id := user.GetSingleResourceByType(ResourceTypeDAV).ID
req := &ResetResourcePasswordRequest{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
ResourceID: id,
},
}
resp, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal("ResetResourcePassword", err)
}
rresp := resp.(*ResetResourcePasswordResponse)
if len(rresp.Password) < 10 {
t.Fatalf("short password: %q", rresp.Password)
}
storedPw, ok := svc.backend.(*fakeBackend).resourcePasswords[id.String()]
if !ok {
t.Fatal("resource password was not actually set on the backend")
}
if storedPw == rresp.Password {
t.Fatal("oops, it appears that the password was stored in cleartext on the backend")
}
}
func TestService_AccountRecovery(t *testing.T) {
svc := testService("")
// Bad recovery response.
req := &AccountRecoveryRequest{
Username: testUser,
RecoveryPassword: "BADPASS",
Password: "new_password",
}
_, err := svc.Handle(context.TODO(), req)
if err == nil {
t.Fatal("oops, recovered account with bad password")
}
// Successful account recovery.
req = &AccountRecoveryRequest{
Username: testUser,
RecoveryPassword: "recoverypassword",
Password: "new_password",
}
_, err = svc.Handle(context.TODO(), req)
if err != nil {
t.Fatalf("account recovery failed: %v", err)
}
}
func TestService_DisableUser(t *testing.T) {
svc := testService("")
req := &DisableUserRequest{
UserRequestBase: UserRequestBase{
RequestBase: RequestBase{
SSO: testUser,
},
Username: testUser,
},
}
_, err := svc.Handle(context.Background(), req)
if err != nil {
t.Fatal("DisableUser", err)
}
// Verify that everything has been disabled.
user := getUser(t, svc, testUser)
if user.Status != UserStatusInactive {
t.Fatalf("user status is '%s', expected inactive", user.Status)
}
for _, rsrc := range user.Resources {
if rsrc.Status != ResourceStatusInactive {
t.Fatalf("resource %s status is '%s', expected inactive", rsrc.ID, rsrc.Status)
}
}
}