Files
coder/cli/sessionstore/sessionstore_test.go
T
Zach 7ecfd1aa07 fix: isolate keyring usage by parallel test processes (#21256)
This change ensures keyring tests that utilize the real OS keyring use
credentials that are isolated by process ID so that parallel test processes
do not access the same credentials.

https://github.com/coder/internal/issues/1192
2025-12-15 09:40:59 -07:00

401 lines
11 KiB
Go

package sessionstore_test
import (
"encoding/json"
"errors"
"net/url"
"os"
"path"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/cli/sessionstore"
"github.com/coder/coder/v2/cli/sessionstore/testhelpers"
)
type storedCredentials map[string]struct {
CoderURL string `json:"coder_url"`
APIToken string `json:"api_token"`
}
func TestKeyring(t *testing.T) {
t.Parallel()
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
t.Skip("linux is not supported yet")
}
// This test exercises use of the operating system keyring. As a result,
// the operating system keyring is expected to be available.
const (
testURL = "http://127.0.0.1:1337"
testURL2 = "http://127.0.0.1:1338"
)
t.Run("ReadNonExistent", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
t.Cleanup(func() { _ = backend.Delete(srvURL) })
_, err = backend.Read(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err), "expected os.ErrNotExist when reading non-existent token")
})
t.Run("DeleteNonExistent", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
t.Cleanup(func() { _ = backend.Delete(srvURL) })
err = backend.Delete(srvURL)
require.Error(t, err)
require.True(t, errors.Is(err, os.ErrNotExist), "expected os.ErrNotExist when deleting non-existent token")
})
t.Run("WriteAndRead", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
t.Cleanup(func() { _ = backend.Delete(srvURL) })
dir := t.TempDir()
expSessionFile := path.Join(dir, "session")
const inputToken = "test-keyring-token-12345"
err = backend.Write(srvURL, inputToken)
require.NoError(t, err)
// Verify no session file was created (keyring stores in OS keyring, not file)
_, err = os.Stat(expSessionFile)
require.True(t, errors.Is(err, os.ErrNotExist), "expected session token file to not exist when using keyring")
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, inputToken, token)
// Clean up
err = backend.Delete(srvURL)
require.NoError(t, err)
})
t.Run("WriteAndDelete", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
t.Cleanup(func() { _ = backend.Delete(srvURL) })
const inputToken = "test-keyring-token-67890"
err = backend.Write(srvURL, inputToken)
require.NoError(t, err)
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, inputToken, token)
err = backend.Delete(srvURL)
require.NoError(t, err)
_, err = backend.Read(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err), "expected os.ErrNotExist after deleting token")
})
t.Run("OverwriteToken", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
t.Cleanup(func() { _ = backend.Delete(srvURL) })
// Write first token
const firstToken = "first-keyring-token"
err = backend.Write(srvURL, firstToken)
require.NoError(t, err)
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, firstToken, token)
// Overwrite with second token
const secondToken = "second-keyring-token"
err = backend.Write(srvURL, secondToken)
require.NoError(t, err)
token, err = backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, secondToken, token)
// Clean up
err = backend.Delete(srvURL)
require.NoError(t, err)
})
t.Run("MultipleServers", func(t *testing.T) {
t.Parallel()
backend := sessionstore.NewKeyringWithService(testhelpers.KeyringServiceName(t))
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
srvURL2, err := url.Parse(testURL2)
require.NoError(t, err)
t.Cleanup(func() {
_ = backend.Delete(srvURL)
_ = backend.Delete(srvURL2)
})
// Write token for server 1
const token1 = "token-for-server-1"
err = backend.Write(srvURL, token1)
require.NoError(t, err)
// Write token for server 2 (should NOT overwrite server 1)
const token2 = "token-for-server-2"
err = backend.Write(srvURL2, token2)
require.NoError(t, err)
// Read server 1's credential
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, token1, token)
// Read server 2's credential
token, err = backend.Read(srvURL2)
require.NoError(t, err)
require.Equal(t, token2, token)
// Delete server 1's credential
err = backend.Delete(srvURL)
require.NoError(t, err)
// Verify server 1's credential is gone
_, err = backend.Read(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
// Verify server 2's credential still exists
token, err = backend.Read(srvURL2)
require.NoError(t, err)
require.Equal(t, token2, token)
// Clean up remaining credentials
err = backend.Delete(srvURL2)
require.NoError(t, err)
})
t.Run("StorageFormat", func(t *testing.T) {
t.Parallel()
// The storage format must remain consistent to ensure we don't break
// compatibility with other Coder related applications that may read
// or decode the same credential.
const testURL1 = "http://127.0.0.1:1337"
srv1URL, err := url.Parse(testURL1)
require.NoError(t, err)
const testURL2 = "http://127.0.0.1:1338"
srv2URL, err := url.Parse(testURL2)
require.NoError(t, err)
serviceName := testhelpers.KeyringServiceName(t)
backend := sessionstore.NewKeyringWithService(serviceName)
t.Cleanup(func() {
_ = backend.Delete(srv1URL)
_ = backend.Delete(srv2URL)
})
// Write token for server 1
const token1 = "token-server-1"
err = backend.Write(srv1URL, token1)
require.NoError(t, err)
// Write token for server 2 (should NOT overwrite server 1's token)
const token2 = "token-server-2"
err = backend.Write(srv2URL, token2)
require.NoError(t, err)
// Verify both credentials are stored in the raw format and can
// be extracted through the Backend API.
rawCredential := readRawKeychainCredential(t, serviceName)
storedCreds := make(storedCredentials)
err = json.Unmarshal(rawCredential, &storedCreds)
require.NoError(t, err, "unmarshalling stored credentials")
// Both credentials should exist
require.Len(t, storedCreds, 2)
require.Equal(t, token1, storedCreds[srv1URL.Host].APIToken)
require.Equal(t, token2, storedCreds[srv2URL.Host].APIToken)
// Read individual credentials
token, err := backend.Read(srv1URL)
require.NoError(t, err)
require.Equal(t, token1, token)
token, err = backend.Read(srv2URL)
require.NoError(t, err)
require.Equal(t, token2, token)
// Cleanup
err = backend.Delete(srv1URL)
require.NoError(t, err)
err = backend.Delete(srv2URL)
require.NoError(t, err)
})
}
func TestFile(t *testing.T) {
const (
testURL = "http://127.0.0.1:1337"
testURL2 = "http://127.0.0.1:1338"
)
t.Parallel()
t.Run("ReadNonExistent", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
_, err = backend.Read(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
})
t.Run("WriteAndRead", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
// Write a token
const inputToken = "test-token-12345"
err = backend.Write(srvURL, inputToken)
require.NoError(t, err)
// Verify the session file was created
sessionFile := config.Root(dir).Session()
require.True(t, sessionFile.Exists())
// Read the token back
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, inputToken, token)
})
t.Run("WriteAndDelete", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
// Write a token
const inputToken = "test-token-67890"
err = backend.Write(srvURL, inputToken)
require.NoError(t, err)
// Verify the token was written
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, inputToken, token)
// Delete the token
err = backend.Delete(srvURL)
require.NoError(t, err)
// Verify the token is gone
_, err = backend.Read(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
})
t.Run("DeleteNonExistent", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
// Attempt to delete a non-existent token
err = backend.Delete(srvURL)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
})
t.Run("OverwriteToken", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
// Write first token
const firstToken = "first-token"
err = backend.Write(srvURL, firstToken)
require.NoError(t, err)
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, firstToken, token)
// Overwrite with second token
const secondToken = "second-token"
err = backend.Write(srvURL, secondToken)
require.NoError(t, err)
token, err = backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, secondToken, token)
})
t.Run("WriteIgnoresURL", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
backend := sessionstore.NewFile(func() config.Root { return config.Root(dir) })
srvURL, err := url.Parse(testURL)
require.NoError(t, err)
srvURL2, err := url.Parse(testURL2)
require.NoError(t, err)
//nolint:gosec // Write with first URL test token
const firstToken = "token-for-url1"
err = backend.Write(srvURL, firstToken)
require.NoError(t, err)
//nolint:gosec // Write with second URL - should overwrite
const secondToken = "token-for-url2"
err = backend.Write(srvURL2, secondToken)
require.NoError(t, err)
// Should have the second token (File backend doesn't differentiate by URL)
token, err := backend.Read(srvURL)
require.NoError(t, err)
require.Equal(t, secondToken, token)
})
}