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) } }) }