package authority

import (
	"context"
	"errors"
	"reflect"
	"testing"

	"github.com/go-jose/go-jose/v3"
	"github.com/stretchr/testify/assert"

	"github.com/smallstep/linkedca"

	"github.com/smallstep/certificates/authority/admin"
	"github.com/smallstep/certificates/authority/administrator"
	"github.com/smallstep/certificates/authority/config"
	"github.com/smallstep/certificates/authority/policy"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/smallstep/certificates/db"
)

func TestAuthority_checkPolicy(t *testing.T) {
	type test struct {
		ctx          context.Context
		currentAdmin *linkedca.Admin
		otherAdmins  []*linkedca.Admin
		policy       *linkedca.Policy
		err          *PolicyError
	}
	tests := map[string]func(t *testing.T) test{
		"fail/NewX509PolicyEngine-error": func(t *testing.T) test {
			return test{
				ctx: context.Background(),
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"**.local"},
						},
					},
				},
				err: &PolicyError{
					Typ: ConfigurationFailure,
					Err: errors.New("cannot parse permitted domain constraint \"**.local\": domain constraint \"**.local\" can only have wildcard as starting character"),
				},
			}
		},
		"fail/currentAdmin-evaluation-error": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "*"},
				otherAdmins:  []*linkedca.Admin{},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"*.local"},
						},
					},
				},
				err: &PolicyError{
					Typ: EvaluationFailure,
					Err: errors.New("cannot parse dns domain \"*\""),
				},
			}
		},
		"fail/currentAdmin-lockout": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins: []*linkedca.Admin{
					{
						Subject: "otherAdmin",
					},
				},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"*.local"},
						},
					},
				},
				err: &PolicyError{
					Typ: AdminLockOut,
					Err: errors.New("the provided policy would lock out [step] from the CA. Please create an x509 policy to include [step] as an allowed DNS name"),
				},
			}
		},
		"fail/otherAdmins-evaluation-error": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins: []*linkedca.Admin{
					{
						Subject: "other",
					},
					{
						Subject: "**",
					},
				},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "other", "*.local"},
						},
					},
				},
				err: &PolicyError{
					Typ: EvaluationFailure,
					Err: errors.New("cannot parse dns domain \"**\""),
				},
			}
		},
		"fail/otherAdmins-lockout": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins: []*linkedca.Admin{
					{
						Subject: "otherAdmin",
					},
				},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step"},
						},
					},
				},
				err: &PolicyError{
					Typ: AdminLockOut,
					Err: errors.New("the provided policy would lock out [otherAdmin] from the CA. Please create an x509 policy to include [otherAdmin] as an allowed DNS name"),
				},
			}
		},
		"ok/no-policy": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins:  []*linkedca.Admin{},
				policy:       nil,
			}
		},
		"ok/empty-policy": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins:  []*linkedca.Admin{},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{},
						},
					},
				},
			}
		},
		"ok/policy": func(t *testing.T) test {
			return test{
				ctx:          context.Background(),
				currentAdmin: &linkedca.Admin{Subject: "step"},
				otherAdmins: []*linkedca.Admin{
					{
						Subject: "otherAdmin",
					},
				},
				policy: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			}
		},
	}

	for name, prep := range tests {
		tc := prep(t)
		t.Run(name, func(t *testing.T) {
			a := &Authority{}

			err := a.checkPolicy(tc.ctx, tc.currentAdmin, tc.otherAdmins, tc.policy)

			if tc.err == nil {
				assert.Nil(t, err)
			} else {
				assert.IsType(t, &PolicyError{}, err)

				var pe *PolicyError
				if assert.True(t, errors.As(err, &pe)) {
					assert.Equal(t, tc.err.Typ, pe.Typ)
					assert.Equal(t, tc.err.Error(), pe.Error())
				}
			}
		})
	}
}

func mustPolicyEngine(t *testing.T, options *policy.Options) *policy.Engine {
	engine, err := policy.New(options)
	if err != nil {
		t.Fatal(err)
	}
	return engine
}

func TestAuthority_reloadPolicyEngines(t *testing.T) {

	existingPolicyEngine, err := policy.New(&policy.Options{
		X509: &policy.X509PolicyOptions{
			AllowedNames: &policy.X509NameOptions{
				DNSDomains: []string{"*.hosts.example.com"},
			},
		},
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.hosts.example.com"},
				},
			},
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					EmailAddresses: []string{"@mails.example.com"},
				},
			},
		},
	})
	assert.NoError(t, err)

	newX509Options := &policy.Options{
		X509: &policy.X509PolicyOptions{
			AllowedNames: &policy.X509NameOptions{
				DNSDomains: []string{"*.local"},
			},
			DeniedNames: &policy.X509NameOptions{
				DNSDomains: []string{"badhost.local"},
			},
			AllowWildcardNames: true,
		},
	}

	newSSHHostOptions := &policy.Options{
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.local"},
				},
				DeniedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"badhost.local"},
				},
			},
		},
	}

	newSSHUserOptions := &policy.Options{
		SSH: &policy.SSHPolicyOptions{
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					Principals: []string{"*"},
				},
				DeniedNames: &policy.SSHNameOptions{
					Principals: []string{"root"},
				},
			},
		},
	}

	newSSHOptions := &policy.Options{
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.local"},
				},
				DeniedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"badhost.local"},
				},
			},
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					Principals: []string{"*"},
				},
				DeniedNames: &policy.SSHNameOptions{
					Principals: []string{"root"},
				},
			},
		},
	}

	newOptions := &policy.Options{
		X509: &policy.X509PolicyOptions{
			AllowedNames: &policy.X509NameOptions{
				DNSDomains: []string{"*.local"},
			},
			DeniedNames: &policy.X509NameOptions{
				DNSDomains: []string{"badhost.local"},
			},
			AllowWildcardNames: true,
		},
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.local"},
				},
				DeniedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"badhost.local"},
				},
			},
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					Principals: []string{"*"},
				},
				DeniedNames: &policy.SSHNameOptions{
					Principals: []string{"root"},
				},
			},
		},
	}

	newAdminX509Options := &policy.Options{
		X509: &policy.X509PolicyOptions{
			AllowedNames: &policy.X509NameOptions{
				DNSDomains: []string{"*.local"},
			},
		},
	}

	newAdminSSHHostOptions := &policy.Options{
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.local"},
				},
			},
		},
	}

	newAdminSSHUserOptions := &policy.Options{
		SSH: &policy.SSHPolicyOptions{
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					EmailAddresses: []string{"@example.com"},
				},
			},
		},
	}

	newAdminOptions := &policy.Options{
		X509: &policy.X509PolicyOptions{
			AllowedNames: &policy.X509NameOptions{
				DNSDomains: []string{"*.local"},
			},
			DeniedNames: &policy.X509NameOptions{
				DNSDomains: []string{"badhost.local"},
			},
			AllowWildcardNames: true,
		},
		SSH: &policy.SSHPolicyOptions{
			Host: &policy.SSHHostCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"*.local"},
				},
				DeniedNames: &policy.SSHNameOptions{
					DNSDomains: []string{"badhost.local"},
				},
			},
			User: &policy.SSHUserCertificateOptions{
				AllowedNames: &policy.SSHNameOptions{
					EmailAddresses: []string{"@example.com"},
				},
				DeniedNames: &policy.SSHNameOptions{
					EmailAddresses: []string{"baduser@example.com"},
				},
			},
		},
	}

	tests := []struct {
		name     string
		config   *config.Config
		adminDB  admin.DB
		ctx      context.Context
		expected *policy.Engine
		wantErr  bool
	}{
		{
			name: "fail/standalone-x509-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						X509: &policy.X509PolicyOptions{
							AllowedNames: &policy.X509NameOptions{
								DNSDomains: []string{"**.local"},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/standalone-ssh-host-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							Host: &policy.SSHHostCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"**.local"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/standalone-ssh-user-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							User: &policy.SSHUserCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									EmailAddresses: []string{"**example.com"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/adminDB.GetAuthorityPolicy-error",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return nil, errors.New("force")
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/admin-x509-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						X509: &linkedca.X509Policy{
							Allow: &linkedca.X509Names{
								Dns: []string{"**.local"},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/admin-ssh-host-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						Ssh: &linkedca.SSHPolicy{
							Host: &linkedca.SSHHostPolicy{
								Allow: &linkedca.SSHHostNames{
									Dns: []string{"**.local"},
								},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "fail/admin-ssh-user-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						Ssh: &linkedca.SSHPolicy{
							User: &linkedca.SSHUserPolicy{
								Allow: &linkedca.SSHUserNames{
									Emails: []string{"@@example.com"},
								},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  true,
			expected: existingPolicyEngine,
		},
		{
			name: "ok/linkedca-unsupported",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB:  &linkedCaClient{},
			ctx:      context.Background(),
			wantErr:  false,
			expected: existingPolicyEngine,
		},
		{
			name: "ok/standalone-no-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy:      nil,
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, nil),
		},
		{
			name: "ok/standalone-x509-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						X509: &policy.X509PolicyOptions{
							AllowedNames: &policy.X509NameOptions{
								DNSDomains: []string{"*.local"},
							},
							DeniedNames: &policy.X509NameOptions{
								DNSDomains: []string{"badhost.local"},
							},
							AllowWildcardNames: true,
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newX509Options),
		},
		{
			name: "ok/standalone-ssh-host-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							Host: &policy.SSHHostCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"*.local"},
								},
								DeniedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"badhost.local"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newSSHHostOptions),
		},
		{
			name: "ok/standalone-ssh-user-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							User: &policy.SSHUserCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									Principals: []string{"*"},
								},
								DeniedNames: &policy.SSHNameOptions{
									Principals: []string{"root"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newSSHUserOptions),
		},
		{
			name: "ok/standalone-ssh-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							Host: &policy.SSHHostCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"*.local"},
								},
								DeniedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"badhost.local"},
								},
							},
							User: &policy.SSHUserCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									Principals: []string{"*"},
								},
								DeniedNames: &policy.SSHNameOptions{
									Principals: []string{"root"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newSSHOptions),
		},
		{
			name: "ok/standalone-full-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: false,
					Policy: &policy.Options{
						X509: &policy.X509PolicyOptions{
							AllowedNames: &policy.X509NameOptions{
								DNSDomains: []string{"*.local"},
							},
							DeniedNames: &policy.X509NameOptions{
								DNSDomains: []string{"badhost.local"},
							},
							AllowWildcardNames: true,
						},
						SSH: &policy.SSHPolicyOptions{
							Host: &policy.SSHHostCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"*.local"},
								},
								DeniedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"badhost.local"},
								},
							},
							User: &policy.SSHUserCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									Principals: []string{"*"},
								},
								DeniedNames: &policy.SSHNameOptions{
									Principals: []string{"root"},
								},
							},
						},
					},
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newOptions),
		},
		{
			name: "ok/admin-x509-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						X509: &linkedca.X509Policy{
							Allow: &linkedca.X509Names{
								Dns: []string{"*.local"},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newAdminX509Options),
		},
		{
			name: "ok/admin-ssh-host-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						Ssh: &linkedca.SSHPolicy{
							Host: &linkedca.SSHHostPolicy{
								Allow: &linkedca.SSHHostNames{
									Dns: []string{"*.local"},
								},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newAdminSSHHostOptions),
		},
		{
			name: "ok/admin-ssh-user-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						Ssh: &linkedca.SSHPolicy{
							User: &linkedca.SSHUserPolicy{
								Allow: &linkedca.SSHUserNames{
									Emails: []string{"@example.com"},
								},
							},
						},
					}, nil
				},
			},
			ctx:      context.Background(),
			wantErr:  false,
			expected: mustPolicyEngine(t, newAdminSSHUserOptions),
		},
		{
			name: "ok/admin-full-policy",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
				},
			},
			ctx: context.Background(),
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						X509: &linkedca.X509Policy{
							Allow: &linkedca.X509Names{
								Dns: []string{"*.local"},
							},
							Deny: &linkedca.X509Names{
								Dns: []string{"badhost.local"},
							},
							AllowWildcardNames: true,
						},
						Ssh: &linkedca.SSHPolicy{
							Host: &linkedca.SSHHostPolicy{
								Allow: &linkedca.SSHHostNames{
									Dns: []string{"*.local"},
								},
								Deny: &linkedca.SSHHostNames{
									Dns: []string{"badhost.local"},
								},
							},
							User: &linkedca.SSHUserPolicy{
								Allow: &linkedca.SSHUserNames{
									Emails: []string{"@example.com"},
								},
								Deny: &linkedca.SSHUserNames{
									Emails: []string{"baduser@example.com"},
								},
							},
						},
					}, nil
				},
			},
			wantErr:  false,
			expected: mustPolicyEngine(t, newAdminOptions),
		},
		{
			// both DB and JSON config; DB config is taken if Admin API is enabled
			name: "ok/admin-over-standalone",
			config: &config.Config{
				AuthorityConfig: &config.AuthConfig{
					EnableAdmin: true,
					Policy: &policy.Options{
						SSH: &policy.SSHPolicyOptions{
							Host: &policy.SSHHostCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"*.local"},
								},
								DeniedNames: &policy.SSHNameOptions{
									DNSDomains: []string{"badhost.local"},
								},
							},
							User: &policy.SSHUserCertificateOptions{
								AllowedNames: &policy.SSHNameOptions{
									Principals: []string{"*"},
								},
								DeniedNames: &policy.SSHNameOptions{
									Principals: []string{"root"},
								},
							},
						},
					},
				},
			},
			ctx: context.Background(),
			adminDB: &admin.MockDB{
				MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
					return &linkedca.Policy{
						X509: &linkedca.X509Policy{
							Allow: &linkedca.X509Names{
								Dns: []string{"*.local"},
							},
							Deny: &linkedca.X509Names{
								Dns: []string{"badhost.local"},
							},
							AllowWildcardNames: true,
						},
					}, nil
				},
			},
			wantErr:  false,
			expected: mustPolicyEngine(t, newX509Options),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				config:       tt.config,
				adminDB:      tt.adminDB,
				policyEngine: existingPolicyEngine,
			}
			if err := a.reloadPolicyEngines(tt.ctx); (err != nil) != tt.wantErr {
				t.Errorf("Authority.reloadPolicyEngines() error = %v, wantErr %v", err, tt.wantErr)
			}

			assert.Equal(t, tt.expected, a.policyEngine)
		})
	}
}

func TestAuthority_checkAuthorityPolicy(t *testing.T) {
	type fields struct {
		provisioners *provisioner.Collection
		admins       *administrator.Collection
		db           db.AuthDB
		adminDB      admin.DB
	}
	type args struct {
		ctx          context.Context
		currentAdmin *linkedca.Admin
		provName     string
		p            *linkedca.Policy
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name:   "no policy",
			fields: fields{},
			args: args{
				currentAdmin: nil,
				provName:     "prov",
				p:            nil,
			},
			wantErr: false,
		},
		{
			name: "fail/adminDB.GetAdmins-error",
			fields: fields{
				admins: administrator.NewCollection(nil),
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return nil, errors.New("force")
					},
				},
			},
			args: args{
				currentAdmin: &linkedca.Admin{Subject: "step"},
				provName:     "prov",
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: true,
		},
		{
			name: "fail/policy",
			fields: fields{
				admins: administrator.NewCollection(nil),
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{
							{
								Id:      "adminID1",
								Subject: "anotherAdmin",
							},
							{
								Id:      "adminID2",
								Subject: "step",
							},
							{
								Id:      "adminID3",
								Subject: "otherAdmin",
							},
						}, nil
					},
				},
			},
			args: args{
				currentAdmin: &linkedca.Admin{Subject: "step"},
				provName:     "prov",
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: true,
		},
		{
			name: "ok",
			fields: fields{
				admins: administrator.NewCollection(nil),
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{
							{
								Id:      "adminID2",
								Subject: "step",
							},
							{
								Id:      "adminID3",
								Subject: "otherAdmin",
							},
						}, nil
					},
				},
			},
			args: args{
				currentAdmin: &linkedca.Admin{Subject: "step"},
				provName:     "prov",
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				provisioners: tt.fields.provisioners,
				admins:       tt.fields.admins,
				db:           tt.fields.db,
				adminDB:      tt.fields.adminDB,
			}
			if err := a.checkAuthorityPolicy(tt.args.ctx, tt.args.currentAdmin, tt.args.p); (err != nil) != tt.wantErr {
				t.Errorf("Authority.checkProvisionerPolicy() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestAuthority_checkProvisionerPolicy(t *testing.T) {
	jwkProvisioner := &provisioner.JWK{
		ID:   "jwkID",
		Type: "JWK",
		Name: "jwkProv",
		Key:  &jose.JSONWebKey{KeyID: "jwkKeyID"},
	}
	provisioners := provisioner.NewCollection(testAudiences)
	provisioners.Store(jwkProvisioner)
	admins := administrator.NewCollection(provisioners)
	admins.Store(&linkedca.Admin{
		Id:            "adminID",
		Subject:       "step",
		ProvisionerId: "jwkID",
	}, jwkProvisioner)
	type fields struct {
		provisioners *provisioner.Collection
		admins       *administrator.Collection
		db           db.AuthDB
		adminDB      admin.DB
	}
	type args struct {
		ctx      context.Context
		provName string
		p        *linkedca.Policy
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name:   "no policy",
			fields: fields{},
			args: args{
				provName: "prov",
				p:        nil,
			},
			wantErr: false,
		},
		{
			name: "fail/policy",
			fields: fields{
				provisioners: provisioners,
				admins:       admins,
			},
			args: args{
				provName: "jwkProv",
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"otherAdmin"}, // step not in policy
						},
					},
				},
			},
			wantErr: true,
		},
		{
			name: "ok",
			fields: fields{
				provisioners: provisioners,
				admins:       admins,
			},
			args: args{
				provName: "jwkProv",
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				provisioners: tt.fields.provisioners,
				admins:       tt.fields.admins,
				db:           tt.fields.db,
				adminDB:      tt.fields.adminDB,
			}
			if err := a.checkProvisionerPolicy(tt.args.ctx, tt.args.provName, tt.args.p); (err != nil) != tt.wantErr {
				t.Errorf("Authority.checkProvisionerPolicy() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestAuthority_RemoveAuthorityPolicy(t *testing.T) {
	type fields struct {
		config  *config.Config
		db      db.AuthDB
		adminDB admin.DB
	}
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr *PolicyError
	}{
		{
			name: "fail/adminDB.DeleteAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockDeleteAuthorityPolicy: func(ctx context.Context) error {
						return errors.New("force")
					},
				},
			},
			wantErr: &PolicyError{
				Typ: StoreFailure,
				Err: errors.New("force"),
			},
		},
		{
			name: "fail/a.reloadPolicyEngines",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockDeleteAuthorityPolicy: func(ctx context.Context) error {
						return nil
					},
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return nil, errors.New("force")
					},
				},
			},
			wantErr: &PolicyError{
				Typ: ReloadFailure,
				Err: errors.New("error reloading policy engines when deleting authority policy: error getting policy to (re)load policy engines: force"),
			},
		},
		{
			name: "ok",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockDeleteAuthorityPolicy: func(ctx context.Context) error {
						return nil
					},
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return nil, nil
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				config:  tt.fields.config,
				db:      tt.fields.db,
				adminDB: tt.fields.adminDB,
			}
			err := a.RemoveAuthorityPolicy(tt.args.ctx)
			if err != nil {
				var pe *PolicyError
				if assert.True(t, errors.As(err, &pe)) {
					assert.Equal(t, tt.wantErr.Typ, pe.Typ)
					assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
				}
				return
			}
		})
	}
}

func TestAuthority_GetAuthorityPolicy(t *testing.T) {
	type fields struct {
		config  *config.Config
		db      db.AuthDB
		adminDB admin.DB
	}
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *linkedca.Policy
		wantErr *PolicyError
	}{
		{
			name: "fail/adminDB.GetAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return nil, errors.New("force")
					},
				},
			},
			wantErr: &PolicyError{
				Typ: InternalFailure,
				Err: errors.New("force"),
			},
		},
		{
			name: "ok",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return &linkedca.Policy{}, nil
					},
				},
			},
			want: &linkedca.Policy{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				config:  tt.fields.config,
				db:      tt.fields.db,
				adminDB: tt.fields.adminDB,
			}
			got, err := a.GetAuthorityPolicy(tt.args.ctx)
			if err != nil {
				var pe *PolicyError
				if assert.True(t, errors.As(err, &pe)) {
					assert.Equal(t, tt.wantErr.Typ, pe.Typ)
					assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
				}
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Authority.GetAuthorityPolicy() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAuthority_CreateAuthorityPolicy(t *testing.T) {
	type fields struct {
		config  *config.Config
		db      db.AuthDB
		adminDB admin.DB
	}
	type args struct {
		ctx context.Context
		adm *linkedca.Admin
		p   *linkedca.Policy
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *linkedca.Policy
		wantErr *PolicyError
	}{
		{
			name: "fail/a.checkAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return nil, errors.New("force")
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: InternalFailure,
				Err: errors.New("error retrieving admins: force"),
			},
		},
		{
			name: "fail/adminDB.CreateAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
					MockCreateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {
						return errors.New("force")
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: StoreFailure,
				Err: errors.New("force"),
			},
		},
		{
			name: "fail/a.reloadPolicyEngines",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return nil, errors.New("force")
					},
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: ReloadFailure,
				Err: errors.New("error reloading policy engines when creating authority policy: error getting policy to (re)load policy engines: force"),
			},
		},
		{
			name: "ok",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return &linkedca.Policy{
							X509: &linkedca.X509Policy{
								Allow: &linkedca.X509Names{
									Dns: []string{"step", "otherAdmin"},
								},
							},
						}, nil
					},
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			want: &linkedca.Policy{
				X509: &linkedca.X509Policy{
					Allow: &linkedca.X509Names{
						Dns: []string{"step", "otherAdmin"},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				config:  tt.fields.config,
				db:      tt.fields.db,
				adminDB: tt.fields.adminDB,
			}
			got, err := a.CreateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
			if err != nil {
				var pe *PolicyError
				if assert.True(t, errors.As(err, &pe)) {
					assert.Equal(t, tt.wantErr.Typ, pe.Typ)
					assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
				}
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Authority.CreateAuthorityPolicy() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAuthority_UpdateAuthorityPolicy(t *testing.T) {
	type fields struct {
		config  *config.Config
		db      db.AuthDB
		adminDB admin.DB
	}
	type args struct {
		ctx context.Context
		adm *linkedca.Admin
		p   *linkedca.Policy
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *linkedca.Policy
		wantErr *PolicyError
	}{
		{
			name: "fail/a.checkAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return nil, errors.New("force")
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: InternalFailure,
				Err: errors.New("error retrieving admins: force"),
			},
		},
		{
			name: "fail/adminDB.UpdateAuthorityPolicy",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
					MockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {
						return errors.New("force")
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: StoreFailure,
				Err: errors.New("force"),
			},
		},
		{
			name: "fail/a.reloadPolicyEngines",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return nil, errors.New("force")
					},
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			wantErr: &PolicyError{
				Typ: ReloadFailure,
				Err: errors.New("error reloading policy engines when updating authority policy: error getting policy to (re)load policy engines: force"),
			},
		},
		{
			name: "ok",
			fields: fields{
				config: &config.Config{
					AuthorityConfig: &config.AuthConfig{
						EnableAdmin: true,
					},
				},
				adminDB: &admin.MockDB{
					MockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {
						return &linkedca.Policy{
							X509: &linkedca.X509Policy{
								Allow: &linkedca.X509Names{
									Dns: []string{"step", "otherAdmin"},
								},
							},
						}, nil
					},
					MockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {
						return nil
					},
					MockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {
						return []*linkedca.Admin{}, nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				adm: &linkedca.Admin{Subject: "step"},
				p: &linkedca.Policy{
					X509: &linkedca.X509Policy{
						Allow: &linkedca.X509Names{
							Dns: []string{"step", "otherAdmin"},
						},
					},
				},
			},
			want: &linkedca.Policy{
				X509: &linkedca.X509Policy{
					Allow: &linkedca.X509Names{
						Dns: []string{"step", "otherAdmin"},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &Authority{
				config:  tt.fields.config,
				db:      tt.fields.db,
				adminDB: tt.fields.adminDB,
			}
			got, err := a.UpdateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
			if err != nil {
				var pe *PolicyError
				if assert.True(t, errors.As(err, &pe)) {
					assert.Equal(t, tt.wantErr.Typ, pe.Typ)
					assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
				}
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Authority.UpdateAuthorityPolicy() = %v, want %v", got, tt.want)
			}
		})
	}
}
