Ctrl K
ComunidadGuías…

coderd/apikey/apikey_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package apikey_test

import (
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd/apikey"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbtime"
)

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

	type testcase struct {
		name   string
		params apikey.CreateParams
		fail   bool
	}

	cases := []testcase{
		{
			name: "OK",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Duration(0),
				ExpiresAt:       time.Now().Add(time.Hour),
				LifetimeSeconds: int64(time.Hour.Seconds()),
				TokenName:       "hello",
				RemoteAddr:      "1.2.3.4",
				Scope:           database.ApiKeyScopeCoderApplicationConnect,
			},
		},
		{
			name: "InvalidScope",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Duration(0),
				ExpiresAt:       time.Now().Add(time.Hour),
				LifetimeSeconds: int64(time.Hour.Seconds()),
				TokenName:       "hello",
				RemoteAddr:      "1.2.3.4",
				Scope:           database.APIKeyScope("test"),
			},
			fail: true,
		},
		{
			name: "DeploymentSessionDuration",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Hour,
				LifetimeSeconds: 0,
				ExpiresAt:       time.Time{},
				TokenName:       "hello",
				RemoteAddr:      "1.2.3.4",
				Scope:           database.ApiKeyScopeCoderApplicationConnect,
			},
		},
		{
			name: "LifetimeSeconds",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Duration(0),
				LifetimeSeconds: int64(time.Hour.Seconds()),
				ExpiresAt:       time.Time{},
				TokenName:       "hello",
				RemoteAddr:      "1.2.3.4",
				Scope:           database.ApiKeyScopeCoderApplicationConnect,
			},
		},
		{
			name: "DefaultIP",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Duration(0),
				ExpiresAt:       time.Now().Add(time.Hour),
				LifetimeSeconds: int64(time.Hour.Seconds()),
				TokenName:       "hello",
				RemoteAddr:      "",
				Scope:           database.ApiKeyScopeCoderApplicationConnect,
			},
		},
		{
			name: "DefaultScope",
			params: apikey.CreateParams{
				UserID:          uuid.New(),
				LoginType:       database.LoginTypeOIDC,
				DefaultLifetime: time.Duration(0),
				ExpiresAt:       time.Now().Add(time.Hour),
				LifetimeSeconds: int64(time.Hour.Seconds()),
				TokenName:       "hello",
				RemoteAddr:      "1.2.3.4",
				Scope:           "",
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			key, keystr, err := apikey.Generate(tc.params)
			if tc.fail {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.NotEmpty(t, keystr)
			require.NotEmpty(t, key.ID)
			require.NotEmpty(t, key.HashedSecret)

			// Assert the string secret is formatted correctly
			keytokens := strings.Split(keystr, "-")
			require.Len(t, keytokens, 2)
			require.Equal(t, key.ID, keytokens[0])

			// Assert that the hashed secret is correct.
			equal := apikey.ValidateHash(key.HashedSecret, keytokens[1])
			require.True(t, equal, "valid secret")

			assert.Equal(t, tc.params.UserID, key.UserID)
			assert.WithinDuration(t, dbtime.Now(), key.CreatedAt, time.Second*5)
			assert.WithinDuration(t, dbtime.Now(), key.UpdatedAt, time.Second*5)

			switch {
			case tc.params.LifetimeSeconds > 0:
				assert.Equal(t, tc.params.LifetimeSeconds, key.LifetimeSeconds)
			case !tc.params.ExpiresAt.IsZero():
				// Should not be a delta greater than 5 seconds.
				assert.InDelta(t, time.Until(tc.params.ExpiresAt).Seconds(), key.LifetimeSeconds, 5)
			default:
				assert.Equal(t, int64(tc.params.DefaultLifetime.Seconds()), key.LifetimeSeconds)
			}

			switch {
			case !tc.params.ExpiresAt.IsZero():
				assert.Equal(t, tc.params.ExpiresAt.UTC(), key.ExpiresAt)
			case tc.params.LifetimeSeconds > 0:
				assert.WithinDuration(t, dbtime.Now().Add(time.Duration(tc.params.LifetimeSeconds)*time.Second), key.ExpiresAt, time.Second*5)
			default:
				assert.WithinDuration(t, dbtime.Now().Add(tc.params.DefaultLifetime), key.ExpiresAt, time.Second*5)
			}

			if tc.params.RemoteAddr != "" {
				assert.Equal(t, tc.params.RemoteAddr, key.IPAddress.IPNet.IP.String())
			} else {
				assert.Equal(t, "0.0.0.0", key.IPAddress.IPNet.IP.String())
			}

			if tc.params.Scope != "" {
				assert.True(t, key.Scopes.Has(tc.params.Scope))
			} else {
				assert.True(t, key.Scopes.Has(database.ApiKeyScopeCoderAll))
			}

			if tc.params.TokenName != "" {
				assert.Equal(t, tc.params.TokenName, key.TokenName)
			}
			if tc.params.LoginType != "" {
				assert.Equal(t, tc.params.LoginType, key.LoginType)
			}
		})
	}
}

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

	cases := []struct {
		name   string
		params apikey.CreateParams
		want   database.APIKeyScopes
		fail   bool
	}{
		{
			name:   "SingularAlias",
			params: apikey.CreateParams{Scope: "all"},
			want:   database.APIKeyScopes{database.ApiKeyScopeCoderAll},
		},
		{
			name:   "PluralAlias",
			params: apikey.CreateParams{Scopes: database.APIKeyScopes{"application_connect"}},
			want:   database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect},
		},
		{
			name: "PluralAliasAndCanonical",
			params: apikey.CreateParams{
				Scopes: database.APIKeyScopes{"all", database.ApiKeyScopeCoderAll},
			},
			want: database.APIKeyScopes{database.ApiKeyScopeCoderAll},
		},
		{
			name: "PluralMixed",
			params: apikey.CreateParams{
				Scopes: database.APIKeyScopes{"all", database.ApiKeyScopeWorkspaceRead},
			},
			want: database.APIKeyScopes{database.ApiKeyScopeCoderAll, database.ApiKeyScopeWorkspaceRead},
		},
		{
			name:   "PluralInvalid",
			params: apikey.CreateParams{Scopes: database.APIKeyScopes{"not_a_real_scope"}},
			fail:   true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			params := tc.params
			params.UserID = uuid.New()
			params.LoginType = database.LoginTypePassword
			params.DefaultLifetime = time.Hour

			requested := slices.Clone(params.Scopes)

			key, _, err := apikey.Generate(params)
			if tc.fail {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tc.want, key.Scopes)
			// Generate must not canonicalize through the caller's slice.
			require.Equal(t, requested, params.Scopes)
		})
	}
}

// TestInvalid just ensures the false case is asserted by some tests.
// Otherwise, a function that just `returns true` might pass all tests incorrectly.
func TestInvalid(t *testing.T) {
	t.Parallel()

	require.Falsef(t, apikey.ValidateHash([]byte{}, "secret"), "empty hash")

	secret, hash, err := apikey.GenerateSecret(10)
	require.NoError(t, err)

	require.Falsef(t, apikey.ValidateHash(hash, secret+"_"), "different secret")
	require.Falsef(t, apikey.ValidateHash(hash[:len(hash)-1], secret), "different hash length")
}