package server

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	gosync "sync"
	"syscall"
	"testing"
	"time"

	jose "github.com/go-jose/go-jose/v4"
	"github.com/golang-jwt/jwt/v5"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/metadata"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/fake"
	"sigs.k8s.io/yaml"

	dynfake "k8s.io/client-go/dynamic/fake"
	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/argoproj/argo-cd/v3/common"
	"github.com/argoproj/argo-cd/v3/pkg/apiclient"
	"github.com/argoproj/argo-cd/v3/pkg/apiclient/session"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	apps "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
	"github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
	servercache "github.com/argoproj/argo-cd/v3/server/cache"
	"github.com/argoproj/argo-cd/v3/server/rbacpolicy"
	"github.com/argoproj/argo-cd/v3/test"
	"github.com/argoproj/argo-cd/v3/util/assets"
	"github.com/argoproj/argo-cd/v3/util/cache"
	appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
	"github.com/argoproj/argo-cd/v3/util/oidc"
	"github.com/argoproj/argo-cd/v3/util/rbac"
	settings_util "github.com/argoproj/argo-cd/v3/util/settings"
	testutil "github.com/argoproj/argo-cd/v3/util/test"
)

type FakeArgoCDServer struct {
	*ArgoCDServer
	TmpAssetsDir string
}

func fakeServer(t *testing.T) (*FakeArgoCDServer, func()) {
	t.Helper()
	cm := test.NewFakeConfigMap()
	secret := test.NewFakeSecret()
	kubeclientset := fake.NewClientset(cm, secret)
	appClientSet := apps.NewSimpleClientset()
	redis, closer := test.NewInMemoryRedis()
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
	tmpAssetsDir := t.TempDir()
	dynamicClient := dynfake.NewSimpleDynamicClient(runtime.NewScheme())
	fakeClient := clientfake.NewClientBuilder().Build()

	port, err := test.GetFreePort()
	if err != nil {
		panic(err)
	}

	argoCDOpts := ArgoCDServerOpts{
		ListenPort:            port,
		Namespace:             test.FakeArgoCDNamespace,
		KubeClientset:         kubeclientset,
		AppClientset:          appClientSet,
		Insecure:              true,
		DisableAuth:           true,
		XFrameOptions:         "sameorigin",
		ContentSecurityPolicy: "frame-ancestors 'self';",
		Cache: servercache.NewCache(
			appstatecache.NewCache(
				cache.NewCache(cache.NewInMemoryCache(1*time.Hour)),
				1*time.Minute,
			),
			1*time.Minute,
			1*time.Minute,
		),
		RedisClient:             redis,
		RepoClientset:           mockRepoClient,
		StaticAssetsDir:         tmpAssetsDir,
		DynamicClientset:        dynamicClient,
		KubeControllerClientset: fakeClient,
	}
	srv := NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})
	fakeSrv := &FakeArgoCDServer{srv, tmpAssetsDir}
	return fakeSrv, closer
}

func TestEnforceProjectToken(t *testing.T) {
	projectName := "testProj"
	roleName := "testRole"
	subFormat := "proj:%s:%s"
	policyTemplate := "p, %s, applications, get, %s/%s, %s"
	defaultObject := "*"
	defaultEffect := "allow"
	defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test")
	defaultIssuedAt := int64(1)
	defaultSub := fmt.Sprintf(subFormat, projectName, roleName)
	defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect)
	defaultId := "testId"

	role := v1alpha1.ProjectRole{Name: roleName, Policies: []string{defaultPolicy}, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}, {ID: defaultId}}}

	jwtTokenByRole := make(map[string]v1alpha1.JWTTokens)
	jwtTokenByRole[roleName] = v1alpha1.JWTTokens{Items: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}, {ID: defaultId}}}

	existingProj := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: projectName, Namespace: test.FakeArgoCDNamespace},
		Spec: v1alpha1.AppProjectSpec{
			Roles: []v1alpha1.ProjectRole{role},
		},
		Status: v1alpha1.AppProjectStatus{JWTTokensByRole: jwtTokenByRole},
	}
	cm := test.NewFakeConfigMap()
	secret := test.NewFakeSecret()
	kubeclientset := fake.NewClientset(cm, secret)
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}

	t.Run("TestEnforceProjectTokenSuccessful", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		cancel := test.StartInformer(s.projInformer)
		defer cancel()
		claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
		assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.Name))
		assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenWithDiffCreateAtFailure", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		diffCreateAt := defaultIssuedAt + 1
		claims := jwt.MapClaims{"sub": defaultSub, "iat": diffCreateAt}
		assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenIncorrectSubFormatFailure", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		invalidSub := "proj:test"
		claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt}
		assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenNoTokenFailure", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		nonExistentToken := "fake-token"
		invalidSub := fmt.Sprintf(subFormat, projectName, nonExistentToken)
		claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt}
		assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenNotJWTTokenFailure", func(t *testing.T) {
		proj := existingProj.DeepCopy()
		proj.Spec.Roles[0].JWTTokens = nil
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
		assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenExplicitDeny", func(t *testing.T) {
		denyApp := "testDenyApp"
		allowPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect)
		denyPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, denyApp, "deny")
		role := v1alpha1.ProjectRole{Name: roleName, Policies: []string{allowPolicy, denyPolicy}, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}}}
		proj := existingProj.DeepCopy()
		proj.Spec.Roles[0] = role

		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		cancel := test.StartInformer(s.projInformer)
		defer cancel()
		claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
		allowedObject := fmt.Sprintf("%s/%s", projectName, "test")
		denyObject := fmt.Sprintf("%s/%s", projectName, denyApp)
		assert.True(t, s.enf.Enforce(claims, "applications", "get", allowedObject))
		assert.False(t, s.enf.Enforce(claims, "applications", "get", denyObject))
	})

	t.Run("TestEnforceProjectTokenWithIdSuccessful", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		cancel := test.StartInformer(s.projInformer)
		defer cancel()
		claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId}
		assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.Name))
		assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	})

	t.Run("TestEnforceProjectTokenWithInvalidIdFailure", func(t *testing.T) {
		s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
		invalidId := "invalidId"
		claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId}
		res := s.enf.Enforce(claims, "applications", "get", invalidId)
		assert.False(t, res)
	})
}

func TestEnforceClaims(t *testing.T) {
	kubeclientset := fake.NewClientset(test.NewFakeConfigMap())
	enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil)
	_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
	rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister())
	enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims)
	policy := `
g, org2:team2, role:admin
g, bob, role:admin
`
	_ = enf.SetUserPolicy(policy)
	allowed := []jwt.Claims{
		jwt.MapClaims{"groups": []string{"org1:team1", "org2:team2"}},
		jwt.RegisteredClaims{Subject: "admin"},
	}
	for _, c := range allowed {
		if !assert.True(t, enf.Enforce(c, "applications", "delete", "foo/obj")) {
			log.Errorf("%v: expected true, got false", c)
		}
	}

	disallowed := []jwt.Claims{
		jwt.MapClaims{"groups": []string{"org3:team3"}},
		jwt.RegisteredClaims{Subject: "nobody"},
	}
	for _, c := range disallowed {
		if !assert.False(t, enf.Enforce(c, "applications", "delete", "foo/obj")) {
			log.Errorf("%v: expected true, got false", c)
		}
	}
}

func TestDefaultRoleWithClaims(t *testing.T) {
	kubeclientset := fake.NewClientset()
	enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil)
	_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
	rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister())
	enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims)
	claims := jwt.MapClaims{"groups": []string{"org1:team1", "org2:team2"}}

	assert.False(t, enf.Enforce(claims, "applications", "get", "foo/bar"))
	// after setting the default role to be the read-only role, this should now pass
	enf.SetDefaultRole("role:readonly")
	assert.True(t, enf.Enforce(claims, "applications", "get", "foo/bar"))
}

func TestEnforceNilClaims(t *testing.T) {
	kubeclientset := fake.NewClientset(test.NewFakeConfigMap())
	enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil)
	_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
	rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister())
	enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims)
	assert.False(t, enf.Enforce(nil, "applications", "get", "foo/obj"))
	enf.SetDefaultRole("role:readonly")
	assert.True(t, enf.Enforce(nil, "applications", "get", "foo/obj"))
}

func TestInitializingExistingDefaultProject(t *testing.T) {
	cm := test.NewFakeConfigMap()
	secret := test.NewFakeSecret()
	kubeclientset := fake.NewClientset(cm, secret)
	defaultProj := &v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{Name: v1alpha1.DefaultAppProjectName, Namespace: test.FakeArgoCDNamespace},
		Spec:       v1alpha1.AppProjectSpec{},
	}
	appClientSet := apps.NewSimpleClientset(defaultProj)

	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}

	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: kubeclientset,
		AppClientset:  appClientSet,
		RepoClientset: mockRepoClient,
	}

	argocd := NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})
	assert.NotNil(t, argocd)

	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(t.Context(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
	require.NoError(t, err)
	assert.NotNil(t, proj)
	assert.Equal(t, v1alpha1.DefaultAppProjectName, proj.Name)
}

func TestInitializingNotExistingDefaultProject(t *testing.T) {
	cm := test.NewFakeConfigMap()
	secret := test.NewFakeSecret()
	kubeclientset := fake.NewClientset(cm, secret)
	appClientSet := apps.NewSimpleClientset()
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}

	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: kubeclientset,
		AppClientset:  appClientSet,
		RepoClientset: mockRepoClient,
	}

	argocd := NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})
	assert.NotNil(t, argocd)

	proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(t.Context(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})
	require.NoError(t, err)
	assert.NotNil(t, proj)
	assert.Equal(t, v1alpha1.DefaultAppProjectName, proj.Name)
}

func TestEnforceProjectGroups(t *testing.T) {
	projectName := "testProj"
	roleName := "testRole"
	subFormat := "proj:%s:%s"
	policyTemplate := "p, %s, applications, get, %s/%s, %s"
	groupName := "my-org:my-team"

	defaultObject := "*"
	defaultEffect := "allow"
	defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test")
	defaultIssuedAt := int64(1)
	defaultSub := fmt.Sprintf(subFormat, projectName, roleName)
	defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect)

	existingProj := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Name:      projectName,
			Namespace: test.FakeArgoCDNamespace,
		},
		Spec: v1alpha1.AppProjectSpec{
			Roles: []v1alpha1.ProjectRole{
				{
					Name:     roleName,
					Policies: []string{defaultPolicy},
					Groups: []string{
						groupName,
					},
				},
			},
		},
	}
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
	kubeclientset := fake.NewClientset(test.NewFakeConfigMap(), test.NewFakeSecret())
	s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
	cancel := test.StartInformer(s.projInformer)
	defer cancel()
	claims := jwt.MapClaims{
		"iat":    defaultIssuedAt,
		"groups": []string{groupName},
	}
	assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.Name))
	assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	assert.False(t, s.enf.Enforce(claims, "clusters", "get", "test"))

	// now remove the group and make sure it fails
	log.Println(existingProj.ProjectPoliciesString())
	existingProj.Spec.Roles[0].Groups = nil
	log.Println(existingProj.ProjectPoliciesString())
	_, _ = s.AppClientset.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Update(t.Context(), &existingProj, metav1.UpdateOptions{})
	time.Sleep(100 * time.Millisecond) // this lets the informer get synced
	assert.False(t, s.enf.Enforce(claims, "projects", "get", existingProj.Name))
	assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
	assert.False(t, s.enf.Enforce(claims, "clusters", "get", "test"))
}

func TestRevokedToken(t *testing.T) {
	projectName := "testProj"
	roleName := "testRole"
	subFormat := "proj:%s:%s"
	policyTemplate := "p, %s, applications, get, %s/%s, %s"
	defaultObject := "*"
	defaultEffect := "allow"
	defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test")
	defaultIssuedAt := int64(1)
	defaultSub := fmt.Sprintf(subFormat, projectName, roleName)
	defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect)
	kubeclientset := fake.NewClientset(test.NewFakeConfigMap(), test.NewFakeSecret())
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}

	jwtTokenByRole := make(map[string]v1alpha1.JWTTokens)
	jwtTokenByRole[roleName] = v1alpha1.JWTTokens{Items: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}}}

	existingProj := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Name:      projectName,
			Namespace: test.FakeArgoCDNamespace,
		},
		Spec: v1alpha1.AppProjectSpec{
			Roles: []v1alpha1.ProjectRole{
				{
					Name:     roleName,
					Policies: []string{defaultPolicy},
					JWTTokens: []v1alpha1.JWTToken{
						{
							IssuedAt: defaultIssuedAt,
						},
					},
				},
			},
		},
		Status: v1alpha1.AppProjectStatus{
			JWTTokensByRole: jwtTokenByRole,
		},
	}

	s := NewServer(t.Context(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{})
	cancel := test.StartInformer(s.projInformer)
	defer cancel()
	claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
	assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.Name))
	assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
}

func TestCertsAreNotGeneratedInInsecureMode(t *testing.T) {
	s, closer := fakeServer(t)
	defer closer()
	assert.True(t, s.Insecure)
	assert.Nil(t, s.settings.Certificate)
}

func TestGracefulShutdown(t *testing.T) {
	port, err := test.GetFreePort()
	require.NoError(t, err)
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
	kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret())
	redis, redisCloser := test.NewInMemoryRedis()
	defer redisCloser()
	s := NewServer(
		t.Context(),
		ArgoCDServerOpts{
			ListenPort:    port,
			Namespace:     test.FakeArgoCDNamespace,
			KubeClientset: kubeclientset,
			AppClientset:  apps.NewSimpleClientset(),
			RepoClientset: mockRepoClient,
			RedisClient:   redis,
		},
		ApplicationSetOpts{},
	)

	projInformerCancel := test.StartInformer(s.projInformer)
	defer projInformerCancel()
	appInformerCancel := test.StartInformer(s.appInformer)
	defer appInformerCancel()
	appsetInformerCancel := test.StartInformer(s.appsetInformer)
	defer appsetInformerCancel()
	clusterInformerCancel := test.StartInformer(s.clusterInformer)
	defer clusterInformerCancel()

	lns, err := s.Listen()
	require.NoError(t, err)

	shutdown := false
	runCtx, runCancel := context.WithTimeout(t.Context(), 2*time.Second)
	defer runCancel()

	err = s.healthCheck(&http.Request{URL: &url.URL{Path: "/healthz", RawQuery: "full=true"}})
	require.Error(t, err, "API Server is not running. It either hasn't started or is restarting.")

	var wg gosync.WaitGroup
	wg.Add(1)
	go func(shutdown *bool) {
		defer wg.Done()
		s.Run(runCtx, lns)
		*shutdown = true
	}(&shutdown)

	for {
		if s.available.Load() {
			err = s.healthCheck(&http.Request{URL: &url.URL{Path: "/healthz", RawQuery: "full=true"}})
			require.NoError(t, err)
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	s.stopCh <- syscall.SIGINT

	wg.Wait()

	err = s.healthCheck(&http.Request{URL: &url.URL{Path: "/healthz", RawQuery: "full=true"}})
	require.Error(t, err, "API Server is terminating and unable to serve requests.")

	assert.True(t, s.terminateRequested.Load())
	assert.False(t, s.available.Load())
	assert.True(t, shutdown)
}

func TestOIDCRefresh(t *testing.T) {
	port, err := test.GetFreePort()
	require.NoError(t, err)
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
	cm := test.NewFakeConfigMap()
	cm.Data["oidc.config"] = `
name: Test OIDC
issuer: $oidc.myoidc.issuer
clientID: $oidc.myoidc.clientId
clientSecret: $oidc.myoidc.clientSecret
`
	secret := test.NewFakeSecret()
	issuerURL := "http://oidc.127.0.0.1.nip.io"
	updatedIssuerURL := "http://newoidc.127.0.0.1.nip.io"
	secret.Data["oidc.myoidc.issuer"] = []byte(issuerURL)
	secret.Data["oidc.myoidc.clientId"] = []byte("myClientId")
	secret.Data["oidc.myoidc.clientSecret"] = []byte("myClientSecret")

	kubeclientset := fake.NewSimpleClientset(cm, secret)
	redis, redisCloser := test.NewInMemoryRedis()
	defer redisCloser()
	s := NewServer(
		t.Context(),
		ArgoCDServerOpts{
			ListenPort:    port,
			Namespace:     test.FakeArgoCDNamespace,
			KubeClientset: kubeclientset,
			AppClientset:  apps.NewSimpleClientset(),
			RepoClientset: mockRepoClient,
			RedisClient:   redis,
		},
		ApplicationSetOpts{},
	)
	projInformerCancel := test.StartInformer(s.projInformer)
	defer projInformerCancel()
	appInformerCancel := test.StartInformer(s.appInformer)
	defer appInformerCancel()
	appsetInformerCancel := test.StartInformer(s.appsetInformer)
	defer appsetInformerCancel()
	clusterInformerCancel := test.StartInformer(s.clusterInformer)
	defer clusterInformerCancel()

	shutdown := false

	lns, err := s.Listen()
	require.NoError(t, err)
	runCtx := t.Context()

	var wg gosync.WaitGroup
	wg.Add(1)
	go func(shutdown *bool) {
		defer wg.Done()
		s.Run(runCtx, lns)
		*shutdown = true
	}(&shutdown)

	for !s.available.Load() {
		time.Sleep(10 * time.Millisecond)
	}
	assert.True(t, s.available.Load())
	assert.Equal(t, issuerURL, s.ssoClientApp.IssuerURL())

	// Update oidc config
	secret.Data["oidc.myoidc.issuer"] = []byte(updatedIssuerURL)
	secret.ResourceVersion = "12345"
	_, err = kubeclientset.CoreV1().Secrets(test.FakeArgoCDNamespace).Update(runCtx, secret, metav1.UpdateOptions{})
	require.NoError(t, err)

	// Wait for graceful shutdown
	wg.Wait()
	for s.available.Load() {
		time.Sleep(10 * time.Millisecond)
	}

	assert.False(t, s.available.Load())

	shutdown = false
	wg.Add(1)
	go func(shutdown *bool) {
		defer wg.Done()
		s.Run(runCtx, lns)
		*shutdown = true
	}(&shutdown)

	for !s.available.Load() {
		time.Sleep(10 * time.Millisecond)
	}
	assert.True(t, s.available.Load())
	assert.Equal(t, updatedIssuerURL, s.ssoClientApp.IssuerURL())

	s.stopCh <- syscall.SIGINT
	wg.Wait()
}

func TestAuthenticate(t *testing.T) {
	type testData struct {
		test             string
		user             string
		errorMsg         string
		anonymousEnabled bool
	}
	tests := []testData{
		{
			test:             "TestNoSessionAnonymousDisabled",
			errorMsg:         "no session information",
			anonymousEnabled: false,
		},
		{
			test:             "TestSessionPresent",
			user:             "admin:login",
			anonymousEnabled: false,
		},
		{
			test:             "TestSessionNotPresentAnonymousEnabled",
			anonymousEnabled: true,
		},
	}

	for _, testData := range tests {
		t.Run(testData.test, func(t *testing.T) {
			cm := test.NewFakeConfigMap()
			if testData.anonymousEnabled {
				cm.Data["users.anonymous.enabled"] = "true"
			}
			secret := test.NewFakeSecret()
			kubeclientset := fake.NewSimpleClientset(cm, secret)
			appClientSet := apps.NewSimpleClientset()
			mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
			argoCDOpts := ArgoCDServerOpts{
				Namespace:     test.FakeArgoCDNamespace,
				KubeClientset: kubeclientset,
				AppClientset:  appClientSet,
				RepoClientset: mockRepoClient,
			}
			argocd := NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})
			ctx := t.Context()
			if testData.user != "" {
				token, err := argocd.sessionMgr.Create(testData.user, 0, "abc")
				require.NoError(t, err)
				ctx = metadata.NewIncomingContext(t.Context(), metadata.Pairs(apiclient.MetaDataTokenKey, token))
			}

			_, err := argocd.Authenticate(ctx)
			if testData.errorMsg != "" {
				assert.Errorf(t, err, testData.errorMsg)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) {
	t.Helper()
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.RequestURI {
		case "/api/dex/.well-known/openid-configuration":
			_, err := fmt.Fprintf(w, `
{
  "issuer": "%[1]s/api/dex",
  "authorization_endpoint": "%[1]s/api/dex/auth",
  "token_endpoint": "%[1]s/api/dex/token",
  "jwks_uri": "%[1]s/api/dex/keys",
  "userinfo_endpoint": "%[1]s/api/dex/userinfo",
  "device_authorization_endpoint": "%[1]s/api/dex/device/code",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256", "HS256"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "iat",
    "exp",
    "email",
    "email_verified",
    "locale",
    "name",
    "preferred_username",
    "at_hash"
  ]
}`, url)
			if err != nil {
				t.Fail()
			}
		case "/api/dex/keys":
			pubKey, err := jwt.ParseRSAPublicKeyFromPEM(testutil.Cert)
			require.NoError(t, err)
			jwks := jose.JSONWebKeySet{
				Keys: []jose.JSONWebKey{
					{
						Key: pubKey,
					},
				},
			}
			out, err := json.Marshal(jwks)
			require.NoError(t, err)
			_, err = w.Write(out)
			require.NoError(t, err)
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}
}

func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool, useDexForSSO bool, additionalOIDCConfig settings_util.OIDCConfig) (argocd *ArgoCDServer, oidcURL string) {
	t.Helper()
	cm := test.NewFakeConfigMap()
	if anonymousEnabled {
		cm.Data["users.anonymous.enabled"] = "true"
	}
	ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
		// Start with a placeholder. We need the server URL before setting up the real handler.
	}))
	ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		dexMockHandler(t, ts.URL)(w, r)
	})
	oidcServer := ts
	if !useDexForSSO {
		oidcServer = testutil.GetOIDCTestServer(t, nil)
	}
	if withFakeSSO {
		cm.Data["url"] = ts.URL
		if useDexForSSO {
			cm.Data["dex.config"] = `
connectors:
  # OIDC
  - type: OIDC
    id: oidc
    name: OIDC
    config:
    issuer: https://auth.example.gom
    clientID: test-client
    clientSecret: $dex.oidc.clientSecret`
		} else {
			// override required oidc config fields but keep other configs as passed in
			additionalOIDCConfig.Name = "Okta"
			additionalOIDCConfig.Issuer = oidcServer.URL
			additionalOIDCConfig.ClientID = "argo-cd"
			additionalOIDCConfig.ClientSecret = "$oidc.okta.clientSecret"
			oidcConfigString, err := yaml.Marshal(additionalOIDCConfig)
			require.NoError(t, err)
			cm.Data["oidc.config"] = string(oidcConfigString)
			// Avoid bothering with certs for local tests.
			cm.Data["oidc.tls.insecure.skip.verify"] = "true"
		}
	}
	secret := test.NewFakeSecret()
	kubeclientset := fake.NewSimpleClientset(cm, secret)
	appClientSet := apps.NewSimpleClientset()
	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: kubeclientset,
		AppClientset:  appClientSet,
		RepoClientset: mockRepoClient,
	}
	if withFakeSSO && useDexForSSO {
		argoCDOpts.DexServerAddr = ts.URL
	}
	argocd = NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})
	var err error
	argocd.ssoClientApp, err = oidc.NewClientApp(argocd.settings, argocd.DexServerAddr, argocd.DexTLSConfig, argocd.BaseHRef, cache.NewInMemoryCache(24*time.Hour))
	require.NoError(t, err)
	return argocd, oidcServer.URL
}

func TestGetClaims(t *testing.T) {
	t.Parallel()

	defaultExpiry := jwt.NewNumericDate(time.Now().Add(time.Hour * 24))
	defaultExpiryUnix := float64(defaultExpiry.Unix())

	type testData struct {
		test                  string
		claims                jwt.MapClaims
		expectedErrorContains string
		expectedClaims        jwt.MapClaims
		expectNewToken        bool
		additionalOIDCConfig  settings_util.OIDCConfig
	}
	tests := []testData{
		{
			test: "GetClaims",
			claims: jwt.MapClaims{
				"aud": "argo-cd",
				"exp": defaultExpiry,
				"sub": "randomUser",
			},
			expectedErrorContains: "",
			expectedClaims: jwt.MapClaims{
				"aud": "argo-cd",
				"exp": defaultExpiryUnix,
				"sub": "randomUser",
			},
			expectNewToken:       false,
			additionalOIDCConfig: settings_util.OIDCConfig{},
		},
		{
			// note: a passing test with user info groups can never be achieved since the user never logged in properly
			// therefore the oidcClient's cache contains no accessToken for the user info endpoint
			// and since the oidcClient cache is unexported (for good reasons) we can't mock this behaviour
			test: "GetClaimsWithUserInfoGroupsEnabled",
			claims: jwt.MapClaims{
				"aud": common.ArgoCDClientAppID,
				"exp": defaultExpiry,
				"sub": "randomUser",
			},
			expectedErrorContains: "invalid session",
			expectedClaims: jwt.MapClaims{
				"aud": common.ArgoCDClientAppID,
				"exp": defaultExpiryUnix,
				"sub": "randomUser",
			},
			expectNewToken: false,
			additionalOIDCConfig: settings_util.OIDCConfig{
				EnableUserInfoGroups:    true,
				UserInfoPath:            "/userinfo",
				UserInfoCacheExpiration: "5m",
			},
		},
		{
			test: "GetClaimsWithGroupsString",
			claims: jwt.MapClaims{
				"aud":    common.ArgoCDClientAppID,
				"exp":    defaultExpiry,
				"sub":    "randomUser",
				"groups": "group1",
			},
			expectedErrorContains: "",
			expectedClaims: jwt.MapClaims{
				"aud":    common.ArgoCDClientAppID,
				"exp":    defaultExpiryUnix,
				"sub":    "randomUser",
				"groups": "group1",
			},
		},
	}

	for _, testData := range tests {
		testDataCopy := testData

		t.Run(testDataCopy.test, func(t *testing.T) {
			t.Parallel()

			// Must be declared here to avoid race.
			ctx := t.Context() //nolint:ineffassign,staticcheck

			argocd, oidcURL := getTestServer(t, false, true, false, testDataCopy.additionalOIDCConfig)

			// create new JWT and store it on the context to simulate an incoming request
			testDataCopy.claims["iss"] = oidcURL
			testDataCopy.expectedClaims["iss"] = oidcURL
			token := jwt.NewWithClaims(jwt.SigningMethodRS512, testDataCopy.claims)
			key, err := jwt.ParseRSAPrivateKeyFromPEM(testutil.PrivateKey)
			require.NoError(t, err)
			tokenString, err := token.SignedString(key)
			require.NoError(t, err)
			ctx = metadata.NewIncomingContext(t.Context(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))

			gotClaims, newToken, err := argocd.getClaims(ctx)

			// Note: testutil.oidcMockHandler currently doesn't implement reissuing expired tokens
			// so newToken will always be empty
			if testDataCopy.expectNewToken {
				assert.NotEmpty(t, newToken)
			}
			if testDataCopy.expectedClaims == nil {
				assert.Nil(t, gotClaims)
			} else {
				assert.Equal(t, testDataCopy.expectedClaims, gotClaims)
			}
			if testDataCopy.expectedErrorContains != "" {
				assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "getClaims should have thrown an error and return an error")
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestAuthenticate_3rd_party_JWTs(t *testing.T) {
	t.Parallel()

	// Marshaling single strings to strings is typical, so we test for this relatively common behavior.
	jwt.MarshalSingleStringAsArray = false

	type testData struct {
		test                  string
		anonymousEnabled      bool
		claims                jwt.RegisteredClaims
		expectedErrorContains string
		expectedClaims        any
		useDex                bool
	}
	tests := []testData{
		// Dex
		{
			test:                  "anonymous disabled, no audience",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        nil,
		},
		{
			test:                  "anonymous enabled, no audience",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{},
			expectedErrorContains: "",
			expectedClaims:        "",
		},
		{
			test:                  "anonymous disabled, unexpired token, admin claim",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			expectedErrorContains: "",
			expectedClaims:        jwt.MapClaims{"sub": "admin"},
		},
		{
			test:                  "anonymous enabled, unexpired token, admin claim",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			expectedErrorContains: "",
			expectedClaims:        jwt.MapClaims{"sub": "admin"},
		},
		{
			test:                  "anonymous disabled, expired token, admin claim",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        jwt.MapClaims{"iss": "sso"},
		},
		{
			test:                  "anonymous enabled, expired token, admin claim",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
			expectedErrorContains: "",
			expectedClaims:        "",
		},
		{
			test:                  "anonymous disabled, unexpired token, admin claim, incorrect audience",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"incorrect-audience"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        nil,
		},
		// External OIDC
		{
			test:                  "external OIDC: anonymous disabled, no audience",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			useDex:                true,
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        nil,
		},
		{
			test:                  "external OIDC: anonymous enabled, no audience",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{},
			useDex:                true,
			expectedErrorContains: "",
			expectedClaims:        "",
		},
		{
			test:                  "external OIDC: anonymous disabled, unexpired token, admin claim",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			useDex:                true,
			expectedErrorContains: "",
			expectedClaims:        jwt.MapClaims{"sub": "admin"},
		},
		{
			test:                  "external OIDC: anonymous enabled, unexpired token, admin claim",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			useDex:                true,
			expectedErrorContains: "",
			expectedClaims:        jwt.MapClaims{"sub": "admin"},
		},
		{
			test:                  "external OIDC: anonymous disabled, expired token, admin claim",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
			useDex:                true,
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        jwt.MapClaims{"iss": "sso"},
		},
		{
			test:                  "external OIDC: anonymous enabled, expired token, admin claim",
			anonymousEnabled:      true,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
			useDex:                true,
			expectedErrorContains: "",
			expectedClaims:        "",
		},
		{
			test:                  "external OIDC: anonymous disabled, unexpired token, admin claim, incorrect audience",
			anonymousEnabled:      false,
			claims:                jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"incorrect-audience"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
			useDex:                true,
			expectedErrorContains: common.TokenVerificationError,
			expectedClaims:        nil,
		},
	}

	for _, testData := range tests {
		testDataCopy := testData

		t.Run(testDataCopy.test, func(t *testing.T) {
			t.Parallel()

			// Must be declared here to avoid race.
			ctx := t.Context() //nolint:staticcheck

			argocd, oidcURL := getTestServer(t, testDataCopy.anonymousEnabled, true, testDataCopy.useDex, settings_util.OIDCConfig{})

			if testDataCopy.useDex {
				testDataCopy.claims.Issuer = oidcURL + "/api/dex"
			} else {
				testDataCopy.claims.Issuer = oidcURL
			}

			// go-oidc explicitly requires the use of an asymmetric signature algorithm.
			// We use the respective signature algorithm based on the mock provider implementation.
			var signingMethod jwt.SigningMethod
			if testDataCopy.useDex {
				signingMethod = jwt.SigningMethodRS256
			} else {
				signingMethod = jwt.SigningMethodRS512
			}
			key, err := jwt.ParseRSAPrivateKeyFromPEM(testutil.PrivateKey)
			require.NoError(t, err)
			token := jwt.NewWithClaims(signingMethod, testDataCopy.claims)
			tokenString, err := token.SignedString(key)
			require.NoError(t, err)
			ctx = metadata.NewIncomingContext(t.Context(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))

			ctx, err = argocd.Authenticate(ctx)
			claims := ctx.Value("claims")
			if testDataCopy.expectedClaims == nil {
				assert.Nil(t, claims)
			} else if expectedMap, ok := testDataCopy.expectedClaims.(jwt.MapClaims); ok {
				actualMap, ok := claims.(jwt.MapClaims)
				assert.True(t, ok, "expected claims to be jwt.MapClaims, got %T", claims)
				for k, v := range expectedMap {
					assert.Equal(t, v, actualMap[k], "claim %q mismatch", k)
				}
			} else {
				assert.Equal(t, testDataCopy.expectedClaims, claims)
			}
			if testDataCopy.expectedErrorContains != "" {
				assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestAuthenticate_no_request_metadata(t *testing.T) {
	t.Parallel()

	type testData struct {
		test                  string
		anonymousEnabled      bool
		expectedErrorContains string
		expectedClaims        any
	}
	tests := []testData{
		{
			test:                  "anonymous disabled",
			anonymousEnabled:      false,
			expectedErrorContains: "no session information",
			expectedClaims:        nil,
		},
		{
			test:                  "anonymous enabled",
			anonymousEnabled:      true,
			expectedErrorContains: "",
			expectedClaims:        "",
		},
	}

	for _, testData := range tests {
		testDataCopy := testData

		t.Run(testDataCopy.test, func(t *testing.T) {
			t.Parallel()

			argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{})
			ctx := t.Context()

			ctx, err := argocd.Authenticate(ctx)
			claims := ctx.Value("claims")
			assert.Equal(t, testDataCopy.expectedClaims, claims)
			if testDataCopy.expectedErrorContains != "" {
				assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestAuthenticate_no_SSO(t *testing.T) {
	t.Parallel()

	type testData struct {
		test                 string
		anonymousEnabled     bool
		expectedErrorMessage string
		expectedClaims       any
	}
	tests := []testData{
		{
			test:                 "anonymous disabled",
			anonymousEnabled:     false,
			expectedErrorMessage: "SSO is not configured",
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled",
			anonymousEnabled:     true,
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
	}

	for _, testData := range tests {
		testDataCopy := testData

		t.Run(testDataCopy.test, func(t *testing.T) {
			t.Parallel()

			// Must be declared here to avoid race.
			ctx := t.Context() //nolint:ineffassign,staticcheck

			argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false, true, settings_util.OIDCConfig{})
			token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{Issuer: dexURL + "/api/dex"})
			tokenString, err := token.SignedString([]byte("key"))
			require.NoError(t, err)
			ctx = metadata.NewIncomingContext(t.Context(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))

			ctx, err = argocd.Authenticate(ctx)
			claims := ctx.Value("claims")
			assert.Equal(t, testDataCopy.expectedClaims, claims)
			if testDataCopy.expectedErrorMessage != "" {
				assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestAuthenticate_bad_request_metadata(t *testing.T) {
	t.Parallel()

	type testData struct {
		test                 string
		anonymousEnabled     bool
		metadata             metadata.MD
		expectedErrorMessage string
		expectedClaims       any
	}
	tests := []testData{
		{
			test:                 "anonymous disabled, empty metadata",
			anonymousEnabled:     false,
			metadata:             metadata.MD{},
			expectedErrorMessage: "no session information",
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled, empty metadata",
			anonymousEnabled:     true,
			metadata:             metadata.MD{},
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
		{
			test:                 "anonymous disabled, empty tokens",
			anonymousEnabled:     false,
			metadata:             metadata.MD{apiclient.MetaDataTokenKey: []string{}},
			expectedErrorMessage: "no session information",
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled, empty tokens",
			anonymousEnabled:     true,
			metadata:             metadata.MD{apiclient.MetaDataTokenKey: []string{}},
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
		{
			test:                 "anonymous disabled, bad tokens",
			anonymousEnabled:     false,
			metadata:             metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
			expectedErrorMessage: "token contains an invalid number of segments",
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled, bad tokens",
			anonymousEnabled:     true,
			metadata:             metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
		{
			test:                 "anonymous disabled, bad auth header",
			anonymousEnabled:     false,
			metadata:             metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
			expectedErrorMessage: common.TokenVerificationError,
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled, bad auth header",
			anonymousEnabled:     true,
			metadata:             metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
		{
			test:                 "anonymous disabled, bad auth cookie",
			anonymousEnabled:     false,
			metadata:             metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
			expectedErrorMessage: common.TokenVerificationError,
			expectedClaims:       nil,
		},
		{
			test:                 "anonymous enabled, bad auth cookie",
			anonymousEnabled:     true,
			metadata:             metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
			expectedErrorMessage: "",
			expectedClaims:       "",
		},
	}

	for _, testData := range tests {
		testDataCopy := testData

		t.Run(testDataCopy.test, func(t *testing.T) {
			t.Parallel()

			// Must be declared here to avoid race.
			ctx := t.Context() //nolint:ineffassign,staticcheck

			argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{})
			ctx = metadata.NewIncomingContext(t.Context(), testDataCopy.metadata)

			ctx, err := argocd.Authenticate(ctx)
			claims := ctx.Value("claims")
			assert.Equal(t, testDataCopy.expectedClaims, claims)
			if testDataCopy.expectedErrorMessage != "" {
				assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func Test_getToken(t *testing.T) {
	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
	t.Run("Empty", func(t *testing.T) {
		assert.Empty(t, getToken(metadata.New(map[string]string{})))
	})
	t.Run("Token", func(t *testing.T) {
		assert.Equal(t, token, getToken(metadata.New(map[string]string{"token": token})))
	})
	t.Run("Authorisation", func(t *testing.T) {
		assert.Empty(t, getToken(metadata.New(map[string]string{"authorization": "Bearer invalid"})))
		assert.Equal(t, token, getToken(metadata.New(map[string]string{"authorization": "Bearer " + token})))
	})
	t.Run("Cookie", func(t *testing.T) {
		assert.Empty(t, getToken(metadata.New(map[string]string{"grpcgateway-cookie": "argocd.token=invalid"})))
		assert.Equal(t, token, getToken(metadata.New(map[string]string{"grpcgateway-cookie": "argocd.token=" + token})))
	})
}

func TestTranslateGrpcCookieHeader(t *testing.T) {
	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
		AppClientset:  apps.NewSimpleClientset(),
		RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
	}
	argocd := NewServer(t.Context(), argoCDOpts, ApplicationSetOpts{})

	t.Run("TokenIsNotEmpty", func(t *testing.T) {
		recorder := httptest.NewRecorder()
		err := argocd.translateGrpcCookieHeader(t.Context(), recorder, &session.SessionResponse{
			Token: "xyz",
		})
		require.NoError(t, err)
		assert.Equal(t, "argocd.token=xyz; path=/; SameSite=lax; httpOnly; Secure", recorder.Result().Header.Get("Set-Cookie"))
		assert.Len(t, recorder.Result().Cookies(), 1)
	})

	t.Run("TokenIsLongerThan4093", func(t *testing.T) {
		recorder := httptest.NewRecorder()
		err := argocd.translateGrpcCookieHeader(t.Context(), recorder, &session.SessionResponse{
			Token: "abc.xyz." + strings.Repeat("x", 4093),
		})
		require.NoError(t, err)
		assert.Regexp(t, "argocd.token=.*; path=/; SameSite=lax; httpOnly; Secure", recorder.Result().Header.Get("Set-Cookie"))
		assert.Len(t, recorder.Result().Cookies(), 2)
	})

	t.Run("TokenIsEmpty", func(t *testing.T) {
		recorder := httptest.NewRecorder()
		err := argocd.translateGrpcCookieHeader(t.Context(), recorder, &session.SessionResponse{
			Token: "",
		})
		require.NoError(t, err)
		assert.Empty(t, recorder.Result().Header.Get("Set-Cookie"))
	})
}

func TestInitializeDefaultProject_ProjectDoesNotExist(t *testing.T) {
	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
		AppClientset:  apps.NewSimpleClientset(),
		RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
	}

	err := initializeDefaultProject(argoCDOpts)
	require.NoError(t, err)

	proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1().
		AppProjects(test.FakeArgoCDNamespace).Get(t.Context(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})

	require.NoError(t, err)

	assert.Equal(t, v1alpha1.AppProjectSpec{
		SourceRepos:              []string{"*"},
		Destinations:             []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
		ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{{Group: "*", Kind: "*"}},
	}, proj.Spec)
}

func TestInitializeDefaultProject_ProjectAlreadyInitialized(t *testing.T) {
	existingDefaultProject := v1alpha1.AppProject{
		ObjectMeta: metav1.ObjectMeta{
			Name:      v1alpha1.DefaultAppProjectName,
			Namespace: test.FakeArgoCDNamespace,
		},
		Spec: v1alpha1.AppProjectSpec{
			SourceRepos:  []string{"some repo"},
			Destinations: []v1alpha1.ApplicationDestination{{Server: "some cluster", Namespace: "*"}},
		},
	}

	argoCDOpts := ArgoCDServerOpts{
		Namespace:     test.FakeArgoCDNamespace,
		KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
		AppClientset:  apps.NewSimpleClientset(&existingDefaultProject),
		RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
	}

	err := initializeDefaultProject(argoCDOpts)
	require.NoError(t, err)

	proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1().
		AppProjects(test.FakeArgoCDNamespace).Get(t.Context(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{})

	require.NoError(t, err)

	assert.Equal(t, proj.Spec, existingDefaultProject.Spec)
}

func TestOIDCConfigChangeDetection_SecretsChanged(t *testing.T) {
	// Given
	rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})
	require.NoError(t, err, "no error expected when marshalling OIDC config")

	originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"}

	argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets}

	originalOIDCConfig := argoSettings.OIDCConfig()

	assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value")
	assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value")

	// When
	newSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "a!Better!Secret"}
	argoSettings.Secrets = newSecrets
	result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings)

	// Then
	assert.True(t, result, "secrets have changed, expect interpolated OIDCConfig to change")
}

func TestOIDCConfigChangeDetection_ConfigChanged(t *testing.T) {
	// Given
	rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		Name:         "argocd",
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})

	require.NoError(t, err, "no error expected when marshalling OIDC config")

	originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"}

	argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets}

	originalOIDCConfig := argoSettings.OIDCConfig()

	assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value")
	assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value")

	// When
	newRawOICDConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		Name:         "cat",
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})

	require.NoError(t, err, "no error expected when marshalling OIDC config")
	argoSettings.OIDCConfigRAW = string(newRawOICDConfig)
	result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings)

	// Then
	assert.True(t, result, "no error expected since OICD config created")
}

func TestOIDCConfigChangeDetection_ConfigCreated(t *testing.T) {
	// Given
	argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: ""}
	originalOIDCConfig := argoSettings.OIDCConfig()

	// When
	newRawOICDConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		Name:         "cat",
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})
	require.NoError(t, err, "no error expected when marshalling OIDC config")
	newSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"}
	argoSettings.OIDCConfigRAW = string(newRawOICDConfig)
	argoSettings.Secrets = newSecrets
	result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings)

	// Then
	assert.True(t, result, "no error expected since new OICD config created")
}

func TestOIDCConfigChangeDetection_ConfigDeleted(t *testing.T) {
	// Given
	rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})
	require.NoError(t, err, "no error expected when marshalling OIDC config")

	originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"}

	argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets}

	originalOIDCConfig := argoSettings.OIDCConfig()

	assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value")
	assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value")

	// When
	argoSettings.OIDCConfigRAW = ""
	argoSettings.Secrets = make(map[string]string)
	result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings)

	// Then
	assert.True(t, result, "no error expected since OICD config deleted")
}

func TestOIDCConfigChangeDetection_NoChange(t *testing.T) {
	// Given
	rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{
		ClientID:     "$k8ssecret:clientid",
		ClientSecret: "$k8ssecret:clientsecret",
	})
	require.NoError(t, err, "no error expected when marshalling OIDC config")

	originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"}

	argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets}

	originalOIDCConfig := argoSettings.OIDCConfig()

	assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value")
	assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value")

	// When
	result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings)

	// Then
	assert.False(t, result, "no error since no config change")
}

func TestIsMainJsBundle(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		url            string
		isMainJsBundle bool
	}{
		{
			name:           "localhost with valid main bundle",
			url:            "https://localhost:8080/main.e4188e5adc97bbfc00c3.js",
			isMainJsBundle: true,
		},
		{
			name:           "localhost and deep path with valid main bundle",
			url:            "https://localhost:8080/some/argo-cd-instance/main.e4188e5adc97bbfc00c3.js",
			isMainJsBundle: true,
		},
		{
			name:           "font file",
			url:            "https://localhost:8080/assets/fonts/google-fonts/Heebo-Bols.woff2",
			isMainJsBundle: false,
		},
		{
			name:           "no dot after main",
			url:            "https://localhost:8080/main/e4188e5adc97bbfc00c3.js",
			isMainJsBundle: false,
		},
		{
			name:           "wrong extension character",
			url:            "https://localhost:8080/main.e4188e5adc97bbfc00c3/js",
			isMainJsBundle: false,
		},
		{
			name:           "wrong hash length",
			url:            "https://localhost:8080/main.e4188e5adc97bbfc00c3abcdefg.js",
			isMainJsBundle: false,
		},
	}
	for _, testCase := range testCases {
		testCaseCopy := testCase
		t.Run(testCaseCopy.name, func(t *testing.T) {
			t.Parallel()
			testURL, _ := url.Parse(testCaseCopy.url)
			isMainJsBundle := isMainJsBundle(testURL)
			assert.Equal(t, testCaseCopy.isMainJsBundle, isMainJsBundle)
		})
	}
}

func TestCacheControlHeaders(t *testing.T) {
	testCases := []struct {
		name                        string
		filename                    string
		createFile                  bool
		expectedStatus              int
		expectedCacheControlHeaders []string
	}{
		{
			name:                        "file exists",
			filename:                    "exists.html",
			createFile:                  true,
			expectedStatus:              http.StatusOK,
			expectedCacheControlHeaders: nil,
		},
		{
			name:                        "file does not exist",
			filename:                    "missing.html",
			createFile:                  false,
			expectedStatus:              404,
			expectedCacheControlHeaders: nil,
		},
		{
			name:                        "main js bundle exists",
			filename:                    "main.e4188e5adc97bbfc00c3.js",
			createFile:                  true,
			expectedStatus:              http.StatusOK,
			expectedCacheControlHeaders: []string{"public, max-age=31536000, immutable"},
		},
		{
			name:                        "main js bundle does not exists",
			filename:                    "main.e4188e5adc97bbfc00c0.js",
			createFile:                  false,
			expectedStatus:              404,
			expectedCacheControlHeaders: nil,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			argocd, closer := fakeServer(t)
			defer closer()

			handler := argocd.newStaticAssetsHandler()

			rr := httptest.NewRecorder()
			req := httptest.NewRequestWithContext(t.Context(), "", "/"+testCase.filename, http.NoBody)

			fp := filepath.Join(argocd.TmpAssetsDir, testCase.filename)

			if testCase.createFile {
				tmpFile, err := os.Create(fp)
				require.NoError(t, err)
				err = tmpFile.Close()
				require.NoError(t, err)
			}

			handler(rr, req)

			assert.Equal(t, testCase.expectedStatus, rr.Code)

			cacheControl := rr.Result().Header["Cache-Control"]
			assert.Equal(t, testCase.expectedCacheControlHeaders, cacheControl)
		})
	}
}

func TestReplaceBaseHRef(t *testing.T) {
	testCases := []struct {
		name        string
		data        string
		expected    string
		replaceWith string
	}{
		{
			name: "non-root basepath",
			data: `<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Argo CD</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/>
    <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/>
    <link href="assets/fonts.css" rel="stylesheet">
</head>

<body>
    <noscript>
        <p>
        Your browser does not support JavaScript. Please enable JavaScript to view the site.
        Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>.
        </p>
    </noscript>
    <div id="app"></div>
</body>

</html>`,
			expected: `<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Argo CD</title>
    <base href="/path1/path2/path3/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/>
    <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/>
    <link href="assets/fonts.css" rel="stylesheet">
</head>

<body>
    <noscript>
        <p>
        Your browser does not support JavaScript. Please enable JavaScript to view the site.
        Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>.
        </p>
    </noscript>
    <div id="app"></div>
</body>

</html>`,
			replaceWith: `<base href="/path1/path2/path3/">`,
		},
		{
			name: "root basepath",
			data: `<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Argo CD</title>
    <base href="/any/path/test/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/>
    <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/>
    <link href="assets/fonts.css" rel="stylesheet">
</head>

<body>
    <noscript>
        <p>
        Your browser does not support JavaScript. Please enable JavaScript to view the site.
        Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>.
        </p>
    </noscript>
    <div id="app"></div>
</body>

</html>`,
			expected: `<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Argo CD</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/>
    <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/>
    <link href="assets/fonts.css" rel="stylesheet">
</head>

<body>
    <noscript>
        <p>
        Your browser does not support JavaScript. Please enable JavaScript to view the site.
        Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>.
        </p>
    </noscript>
    <div id="app"></div>
</body>

</html>`,
			replaceWith: `<base href="/">`,
		},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			result := replaceBaseHRef(testCase.data, testCase.replaceWith)
			assert.Equal(t, testCase.expected, result)
		})
	}
}

func Test_enforceContentTypes(t *testing.T) {
	t.Parallel()

	getBaseHandler := func(t *testing.T, allow bool) http.Handler {
		t.Helper()
		return http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
			assert.True(t, allow, "http handler was hit when it should have been blocked by content type enforcement")
			writer.WriteHeader(http.StatusOK)
		})
	}

	t.Run("GET - not providing a content type, should still succeed", func(t *testing.T) {
		t.Parallel()

		handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
		req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
		w := httptest.NewRecorder()
		handler(w, req)
		resp := w.Result()
		assert.Equal(t, http.StatusOK, resp.StatusCode)
	})

	t.Run("POST", func(t *testing.T) {
		t.Parallel()

		handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
		req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", http.NoBody)
		w := httptest.NewRecorder()
		handler(w, req)
		resp := w.Result()
		assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode, "didn't provide a content type, should have gotten an error")

		req = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", http.NoBody)
		req.Header = map[string][]string{"Content-Type": {"application/json"}}
		w = httptest.NewRecorder()
		handler(w, req)
		resp = w.Result()
		assert.Equal(t, http.StatusOK, resp.StatusCode, "should have passed, since an allowed content type was provided")

		req = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", http.NoBody)
		req.Header = map[string][]string{"Content-Type": {"not-allowed"}}
		w = httptest.NewRecorder()
		handler(w, req)
		resp = w.Result()
		assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode, "should not have passed, since a disallowed content type was provided")
	})
}

func TestServeExtensions_IsolatesEachFile(t *testing.T) {
	tmpDir := t.TempDir()

	// Write two extension files
	err := os.WriteFile(filepath.Join(tmpDir, "extension-a.js"), []byte(`console.log("ext-a");`), 0o644)
	require.NoError(t, err)
	err = os.WriteFile(filepath.Join(tmpDir, "extension-b.js"), []byte(`console.log("ext-b");`), 0o644)
	require.NoError(t, err)

	argocd, closer := fakeServer(t)
	defer closer()

	w := httptest.NewRecorder()
	argocd.serveExtensions(tmpDir, w)
	body := w.Body.String()

	// Each file must be wrapped in its own try/catch so a failure in one
	// does not prevent subsequent extensions from loading.
	assert.Contains(t, body, "try {")
	assert.Contains(t, body, "} catch(e) {")
	assert.Contains(t, body, `console.log("ext-a");`)
	assert.Contains(t, body, `console.log("ext-b");`)

	// Verify the try/catch for ext-a appears before ext-b's content
	idxTry := strings.Index(body, "try {")
	idxExtB := strings.Index(body, `console.log("ext-b");`)
	assert.Less(t, idxTry, idxExtB, "try block should wrap ext-a before ext-b content appears")

	// There should be two separate try/catch blocks (one per file)
	assert.Equal(t, 2, strings.Count(body, "try {"), "each extension file should have its own try block")
	assert.Equal(t, 2, strings.Count(body, "} catch(e) {"), "each extension file should have its own catch block")
}

func Test_StaticAssetsDir_no_symlink_traversal(t *testing.T) {
	tmpDir := t.TempDir()
	assetsDir := filepath.Join(tmpDir, "assets")
	err := os.MkdirAll(assetsDir, os.ModePerm)
	require.NoError(t, err)

	// Create a file in temp dir
	filePath := filepath.Join(tmpDir, "test.txt")
	err = os.WriteFile(filePath, []byte("test"), 0o644)
	require.NoError(t, err)

	argocd, closer := fakeServer(t)
	defer closer()

	// Create a symlink to the file
	symlinkPath := filepath.Join(argocd.StaticAssetsDir, "link.txt")
	err = os.Symlink(filePath, symlinkPath)
	require.NoError(t, err)

	// Make a request to get the file from the /assets endpoint
	req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/link.txt", http.NoBody)
	w := httptest.NewRecorder()
	argocd.newStaticAssetsHandler()(w, req)
	resp := w.Result()
	assert.Equal(t, http.StatusInternalServerError, resp.StatusCode, "should not have been able to access the symlinked file")

	// Make sure a normal file works
	normalFilePath := filepath.Join(argocd.StaticAssetsDir, "normal.txt")
	err = os.WriteFile(normalFilePath, []byte("normal"), 0o644)
	require.NoError(t, err)
	req = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/normal.txt", http.NoBody)
	w = httptest.NewRecorder()
	argocd.newStaticAssetsHandler()(w, req)
	resp = w.Result()
	assert.Equal(t, http.StatusOK, resp.StatusCode, "should have been able to access the normal file")
}
