fix: align in-memory authentication semantics
This commit is contained in:
@@ -106,6 +106,9 @@ func NewClientWithAuthData(authData AuthData, options ...ClientOption) (*Client,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !authData.zero() {
|
||||||
|
authData = client.authDataWithIdentity(authData)
|
||||||
|
}
|
||||||
client.setAuthData(authData)
|
client.setAuthData(authData)
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
@@ -131,8 +134,11 @@ func newClient(options ...ClientOption) (*Client, error) {
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAuthDataChanged configures serialized persistence for in-memory auth updates.
|
// WithAuthDataChanged configures synchronous persistence for in-memory auth updates.
|
||||||
// The callback runs under login serialization, but never while the auth data lock is held.
|
// The callback is serialized by the client's login lock and may acquire unrelated
|
||||||
|
// application locks, but it must not call Client methods other than AuthData.
|
||||||
|
// In particular, calling Login or another method that may refresh authentication
|
||||||
|
// will deadlock. Returning an error leaves the client's authentication unchanged.
|
||||||
func WithAuthDataChanged(callback func(AuthData) error) ClientOption {
|
func WithAuthDataChanged(callback func(AuthData) error) ClientOption {
|
||||||
return func(client *Client) error {
|
return func(client *Client) error {
|
||||||
if callback == nil {
|
if callback == nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,6 +48,31 @@ func TestNewClientWithAuthDataRejectsPartialData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewClientWithAuthDataGeneratesMissingIdentityWithoutMutatingInput(t *testing.T) {
|
||||||
|
input := completeAuthData()
|
||||||
|
input.DeviceID = ""
|
||||||
|
input.PassO = ""
|
||||||
|
|
||||||
|
client, err := NewClientWithAuthData(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if input.DeviceID != "" || input.PassO != "" {
|
||||||
|
t.Fatalf("caller auth data mutated: %#v", input)
|
||||||
|
}
|
||||||
|
stored := client.AuthData()
|
||||||
|
if stored.DeviceID == "" || stored.PassO == "" {
|
||||||
|
t.Fatalf("stored identity not generated: %#v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAuthDataChangedRejectsNil(t *testing.T) {
|
||||||
|
_, err := NewClientWithAuthData(AuthData{}, WithAuthDataChanged(nil))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "must not be nil") {
|
||||||
|
t.Fatalf("error = %v, want nil callback error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewClientWithAuthDataDoesNotAccessFilesystem(t *testing.T) {
|
func TestNewClientWithAuthDataDoesNotAccessFilesystem(t *testing.T) {
|
||||||
home := filepath.Join(t.TempDir(), "must-not-exist")
|
home := filepath.Join(t.TempDir(), "must-not-exist")
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
@@ -110,6 +136,43 @@ func TestMemoryRefreshCallbackCanReadCurrentAuth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMemoryRefreshCallbackCanAcquireApplicationLock(t *testing.T) {
|
||||||
|
var persistenceMu sync.Mutex
|
||||||
|
client, _, server := newMemoryRefreshClient(t, func(AuthData) error {
|
||||||
|
persistenceMu.Lock()
|
||||||
|
defer persistenceMu.Unlock()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := client.refreshToken(context.Background()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemoryRefreshPersistsGeneratedIdentity(t *testing.T) {
|
||||||
|
initial := completeAuthData()
|
||||||
|
initial.DeviceID = ""
|
||||||
|
initial.PassO = ""
|
||||||
|
var changed AuthData
|
||||||
|
client, err := NewClientWithAuthData(initial, WithAuthDataChanged(func(authData AuthData) error {
|
||||||
|
changed = authData.clone()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server := configureRefreshServer(t, client)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := client.refreshToken(context.Background()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if changed.DeviceID == "" || changed.PassO == "" {
|
||||||
|
t.Fatalf("persisted identity not generated: %#v", changed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFreshMemoryClientQRLoginPersistsThroughCallback(t *testing.T) {
|
func TestFreshMemoryClientQRLoginPersistsThroughCallback(t *testing.T) {
|
||||||
var changed AuthData
|
var changed AuthData
|
||||||
var server *httptest.Server
|
var server *httptest.Server
|
||||||
|
|||||||
Reference in New Issue
Block a user