c441366f89
- mas_oidc_loopback_go_infra: OAuth2 PKCE + loopback HTTP for desktop login - keyring_token_store_go_infra: persist OAuth tokens in SO keychain - matrix_client_init_go_infra: init mautrix.Client from access_token + whoami Plus go.work workspace including matrix_client_pc sub-repo for shared import path during dev. All 3 fns tagged matrix-mas capability group. Tests: TestMasOidcLoopback (15 cases), TestKeyringTokenStore (5 cases, SKIP on headless), TestMatrixClientInit (6 cases) — all green/skip. Refs: dev/issues/0147-matrix-client-pc-scaffold.md Refs: dataforge/matrix_client_pc commit f28c2b1
127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
keyring "github.com/zalando/go-keyring"
|
|
)
|
|
|
|
func TestKeyringTokenStore(t *testing.T) {
|
|
// Probe whether the OS keyring is available. If not, skip gracefully
|
|
// (CI Linux headless, Docker containers without Secret Service).
|
|
probeService := fmt.Sprintf("fn_registry.test.probe.%d", time.Now().UnixNano())
|
|
probeErr := keyring.Set(probeService, "probe", "ok")
|
|
if probeErr != nil {
|
|
t.Skipf("keyring not available (headless/CI): %v", probeErr)
|
|
}
|
|
// Clean up the probe entry.
|
|
_ = keyring.Delete(probeService, "probe")
|
|
|
|
// Use a timestamped service name so parallel test runs don't collide.
|
|
service := fmt.Sprintf("fn_registry.test.%d", time.Now().UnixNano())
|
|
store := NewKeyringTokenStore(service)
|
|
|
|
sampleToken := Token{
|
|
AccessToken: "mxat_test_access",
|
|
RefreshToken: "mxrt_test_refresh",
|
|
ExpiresAt: time.Now().Add(time.Hour).UTC().Truncate(time.Second),
|
|
UserID: "@testuser:matrix.example.com",
|
|
DeviceID: "TESTDEV01",
|
|
HomeserverURL: "https://matrix.example.com",
|
|
Issuer: "https://auth.example.com/",
|
|
ClientID: "TESTCLIENT123",
|
|
}
|
|
|
|
t.Run("Save then Load returns matching token", func(t *testing.T) {
|
|
account := sampleToken.UserID
|
|
t.Cleanup(func() { _ = store.Delete(account) })
|
|
|
|
if err := store.Save(account, sampleToken); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
got, err := store.Load(account)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got.AccessToken != sampleToken.AccessToken {
|
|
t.Errorf("AccessToken: got %q, want %q", got.AccessToken, sampleToken.AccessToken)
|
|
}
|
|
if got.RefreshToken != sampleToken.RefreshToken {
|
|
t.Errorf("RefreshToken: got %q, want %q", got.RefreshToken, sampleToken.RefreshToken)
|
|
}
|
|
if !got.ExpiresAt.Equal(sampleToken.ExpiresAt) {
|
|
t.Errorf("ExpiresAt: got %v, want %v", got.ExpiresAt, sampleToken.ExpiresAt)
|
|
}
|
|
if got.UserID != sampleToken.UserID {
|
|
t.Errorf("UserID: got %q, want %q", got.UserID, sampleToken.UserID)
|
|
}
|
|
if got.DeviceID != sampleToken.DeviceID {
|
|
t.Errorf("DeviceID: got %q, want %q", got.DeviceID, sampleToken.DeviceID)
|
|
}
|
|
if got.HomeserverURL != sampleToken.HomeserverURL {
|
|
t.Errorf("HomeserverURL: got %q, want %q", got.HomeserverURL, sampleToken.HomeserverURL)
|
|
}
|
|
if got.Issuer != sampleToken.Issuer {
|
|
t.Errorf("Issuer: got %q, want %q", got.Issuer, sampleToken.Issuer)
|
|
}
|
|
if got.ClientID != sampleToken.ClientID {
|
|
t.Errorf("ClientID: got %q, want %q", got.ClientID, sampleToken.ClientID)
|
|
}
|
|
})
|
|
|
|
t.Run("Load nonexistent returns ErrNotFound", func(t *testing.T) {
|
|
_, err := store.Load("@nobody:missing.example.com")
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Save then Delete then Load returns ErrNotFound", func(t *testing.T) {
|
|
account := "@delete_me:matrix.example.com"
|
|
if err := store.Save(account, sampleToken); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
if err := store.Delete(account); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
_, err := store.Load(account)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("expected ErrNotFound after Delete, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Delete nonexistent is idempotent", func(t *testing.T) {
|
|
if err := store.Delete("@nonexistent:matrix.example.com"); err != nil {
|
|
t.Errorf("Delete of nonexistent should not error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Save twice overwrites with second token", func(t *testing.T) {
|
|
account := "@overwrite_me:matrix.example.com"
|
|
t.Cleanup(func() { _ = store.Delete(account) })
|
|
|
|
first := sampleToken
|
|
first.AccessToken = "mxat_first_version"
|
|
if err := store.Save(account, first); err != nil {
|
|
t.Fatalf("Save (first): %v", err)
|
|
}
|
|
|
|
second := sampleToken
|
|
second.AccessToken = "mxat_second_version"
|
|
if err := store.Save(account, second); err != nil {
|
|
t.Fatalf("Save (second): %v", err)
|
|
}
|
|
|
|
got, err := store.Load(account)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got.AccessToken != second.AccessToken {
|
|
t.Errorf("overwrite: got AccessToken %q, want %q", got.AccessToken, second.AccessToken)
|
|
}
|
|
})
|
|
}
|