mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
71b132b9e7
Removes `t.Parallel()` from `TestKeyring` and `TestWindowsKeyring_WriteReadDelete`. The OS keyring is a shared system resource that's flaky under concurrent access, especially Windows Credential Manager in CI. Fixes coder/internal#1370
387 lines
11 KiB
Go
387 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"`
|
|
}
|
|
|
|
//nolint:paralleltest, tparallel // OS keyring is flaky under concurrent access
|
|
func TestKeyring(t *testing.T) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
// 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)
|
|
})
|
|
}
|