Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ WALLET_B_PRIVATE_KEY=0x000000000000000000000000000000000000000000000000000000000
# the auth flow only does signature verification.
# AUTH_TEST_WALLET_PRIVATE_KEY=0x...

# Explorer build target for `@cross` flows — passed to mf as the install/run
# target. Use `dev` or any branch/PR carrying the ALTTESTER preprocessor +
# the OpenURL hook (unity-explorer branch
# `chore/expose-requestid-for-cross-tests`). Use an absolute path to a local
# `.app` to skip download + install (treated as `mf explorer test --local-build`).
# EXPLORER_BUILD_TARGET=dev

# RPC URLs for transaction broadcasting. Use your own provider keys
# (Alchemy, Infura, Ankr, ...) for production — defaults are public and rate-limited.
POLYGON_AMOY_RPC_URL=https://rpc.decentraland.org/amoy
Expand Down
25 changes: 0 additions & 25 deletions explorer/Tests/Tests/CrossPlatformVerificationTests.cs

This file was deleted.

178 changes: 178 additions & 0 deletions explorer/Tests/Tests/CrossWalletLoginTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
namespace ExplorerAutomation.Tests.Tests;

/// <summary>
/// Shared file-paths the cross-stack `@cross` flows use to communicate between
/// the Unity Explorer (writer) and the Playwright orchestrator (reader). Both
/// files live in Unity's `Application.persistentDataPath`, which resolves to
/// `~/Library/Application Support/Decentraland/Explorer/` on macOS — the same
/// directory the Unity client uses for its other runtime state (analytics
/// queue, sentry, profile cache, etc.).
///
/// VERIFIED AT RUNTIME — the folder is "Explorer", NOT "Decentraland Unity
/// Explorer". The companion unity-explorer PR (#8862, branch
/// `chore/expose-requestid-for-cross-tests`, which adds the OpenUrl hook these
/// flows depend on) documents the macOS path as
/// `.../Decentraland/Decentraland Unity Explorer/auth-url.txt` in its
/// "Coordination required" prose, but that is a documentation slip: the PR
/// touches only `UnityAppWebBrowser.cs` (it does not rename `productName` in
/// `ProjectSettings.asset`), and the hook it adds writes the real
/// `auth-url.txt` to `.../Decentraland/Explorer/`. The on-disk file, plus the
/// sibling `com.Decentraland.Explorer` PlayerPrefs plist dir, confirm
/// `productName == "Explorer"` for this build. Do not "align" these constants
/// to the PR prose — that would point the pollers at a directory Unity never
/// writes to and every `@cross` flow would time out.
///
/// TS-side mirror: `getAuthUrlPath()` + `getAuthVerificationCodePath()` in
/// `web/tests/auth/helpers/auth-request-bridge.ts` (which delegate to
/// `getCrossStackPath('explorer', …)` in `persistent-data-path.ts`).
/// </summary>
internal static class CrossPlatformPaths
{
/// <summary>
/// URL the Unity client opened for the wallet auth flow, written by the
/// `#if ALTTESTER` hook in `UnityAppWebBrowser.OpenUrl`. The Playwright
/// spec polls for this file's appearance and uses its contents to drive
/// its own browser to the same target.
/// </summary>
public static string AuthUrlPath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "Decentraland", "Explorer",
"auth-url.txt");

/// <summary>
/// Validation code surfaced on `Verification.Dapp.Screen`, written by
/// the C# <see cref="WalletLoginCodeRead"/> stage after the browser side
/// has reached `/auth/requests/<id>` (which triggers the auth-api
/// `request-validation-status` event that activates the Unity-side
/// screen). Playwright reads this and asserts it equals the code
/// rendered on the web RequestPage — the actual device-pairing safety
/// check end-users perform visually.
/// </summary>
public static string AuthVerificationCodePath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "Decentraland", "Explorer",
"auth-verification-code.txt");
}

/// <summary>
/// Stage 1 of the cross-stack wallet-login flow — fire the wallet auth flow
/// and confirm the client opened the system-browser URL (intercepted under
/// `#if ALTTESTER` to a file). Runs against a freshly-launched (logged-out)
/// Explorer. Extends <see cref="LoggedOutAuthBaseTest"/> so its
/// `EnsureInWorld` override lands the client at the LoginSelection screen
/// regardless of starting state.
///
/// Clicks the Metamask button. The client opens a SocketIO websocket to
/// auth-api, posts a `dcl_personal_sign` request, receives back
/// `{ requestId, code }`, and calls `webBrowser.OpenUrl(...)`. In ALTTESTER
/// builds the URL is written to <see cref="CrossPlatformPaths.AuthUrlPath"/>
/// instead of opening the system browser. This fixture asserts the file
/// appears within a generous timeout — the Playwright spec then reads the
/// file, derives the requestId, and drives its own browser to the same
/// `/auth/requests/<id>` URL.
///
/// We intentionally do NOT wait for `Verification.Dapp.Screen` here: that
/// screen only appears AFTER the server emits a `request-validation-status`
/// SocketIO event, which only happens once the browser side reaches the
/// requests page. Waiting on it would deadlock with the Playwright
/// orchestrator (which is itself waiting for this stage to complete).
/// Reading the code is the job of <see cref="WalletLoginCodeRead"/>.
/// </summary>
[AllureSuite("Wallet Login")]
[Category("CrossVerify")]
[Order(16)]
public class WalletLoginCapture : LoggedOutAuthBaseTest
{
[Test]
public void TestCaptureWalletAuthHandoff()
{
// Clean any stale URL file from a prior run.
if (File.Exists(CrossPlatformPaths.AuthUrlPath))
File.Delete(CrossPlatformPaths.AuthUrlPath);

Assert.That(Views.AuthenticationMainScreen.LoginSelectionScreen.IsPresent(), Is.True,
"Expected the LoginSelection sub-screen after LoggedOutAuthBaseTest.EnsureInWorld.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] The path components "Decentraland", "Explorer" may not match Unity's actual Application.persistentDataPath, which uses companyName/productName from ProjectSettings.asset. PR #8862's description states the macOS path is ~/Library/Application Support/Decentraland/Decentraland Unity Explorer/. If the product name is indeed "Decentraland Unity Explorer" (not just "Explorer"), this path will never find the file Unity writes.

Please verify by logging Application.persistentDataPath at runtime and aligning this constant.


Views.AuthenticationMainScreen.MetamaskButton.Click();
Reporter.Log("Metamask button clicked — waiting for auth-url.txt to appear");

// Poll for the URL file. The UnityAppWebBrowser.OpenUrl hook writes
// it the moment Application.OpenURL would be called — typically 1-3
// seconds after the click (auth-api websocket connect + dcl_personal_sign
// round trip). Generous 30s timeout for slow CI / cold caches.
var deadline = DateTime.UtcNow.AddSeconds(30);
while (DateTime.UtcNow < deadline && !File.Exists(CrossPlatformPaths.AuthUrlPath))
{
Thread.Sleep(250);
}

Assert.That(File.Exists(CrossPlatformPaths.AuthUrlPath), Is.True,
$"Expected the OpenURL hook to write {CrossPlatformPaths.AuthUrlPath} within 30s. " +
"Either the click didn't trigger the auth flow, the auth-api request failed, " +
"or the running Explorer build doesn't have the ALTTESTER-gated OpenURL hook " +
"(needs unity-explorer branch `chore/expose-requestid-for-cross-tests` or later).");

var url = File.ReadAllText(CrossPlatformPaths.AuthUrlPath).Trim();
Assert.That(url, Does.StartWith("http"),
$"auth-url.txt should hold an http(s) URL, got: {url}");
Reporter.Log($"Auth URL captured (Playwright will read this from disk): {url}");
}
}

/// <summary>
/// Stage 2 of the cross-stack wallet-login flow — read the validation code
/// from Unity's `Verification.Dapp.Screen` once the auth-api has emitted the
/// `request-validation-status` event (i.e. once the browser side has reached
/// `/auth/requests/<id>`). Writes the code to
/// <see cref="CrossPlatformPaths.AuthVerificationCodePath"/> so Playwright can
/// read and compare it against the code rendered on the web RequestPage.
///
/// This stage exists *because* the device-pairing safety check is the actual
/// product feature being tested: client and web display the same code, the
/// user verifies they match before approving. Skipping it (clicking Approve
/// immediately) makes the test green but doesn't validate the security
/// property real users rely on.
/// </summary>
[AllureSuite("Wallet Login")]
[Category("CrossVerify")]
[Order(17)]
public class WalletLoginCodeRead : LoggedOutAuthBaseTest
{
/// <summary>
/// EnsureInWorld override: when the dapp verification screen is active
/// (post-Metamask, post-browser-reach), neither the LoginSelection nor
/// the OtpVerification sub-screens are visible, and the cached-account
/// "Use a Different Account" button is missing — so the parent class's
/// fallback would throw. We just return without navigation: the test
/// expects the verification screen to be already present, and any
/// state-recovery would defeat the purpose of reading the code on it.
/// </summary>
protected override void EnsureInWorld()
{
// No-op: the orchestrator (Playwright spec) ensures the verification
// screen is up before invoking this stage.
}

[Test]
public void TestReadVerificationCode()
{
// Clean any stale file before writing.
if (File.Exists(CrossPlatformPaths.AuthVerificationCodePath))
File.Delete(CrossPlatformPaths.AuthVerificationCodePath);

// Wait for the verification screen to surface its code. The screen
// appears once auth-api sends "request-validation-status" — which
// requires the browser side to be on /auth/requests/<id>. 60s timeout
// covers the browser navigation + auth-api round-trip.
var code = Views.VerificationDappAuthScreen.CodeText.GetText(60);
Assert.That(code, Is.Not.Empty.And.Matches("^[0-9]+$"),
"Expected Verification.Dapp.Screen Code element to render a numeric code");
Reporter.Log($"Unity-side verification code: {code}");

Directory.CreateDirectory(Path.GetDirectoryName(CrossPlatformPaths.AuthVerificationCodePath)!);
File.WriteAllText(CrossPlatformPaths.AuthVerificationCodePath, code);
Reporter.Log($"Wrote code to {CrossPlatformPaths.AuthVerificationCodePath}");
}
}
49 changes: 49 additions & 0 deletions explorer/Tests/Tests/CrossWalletLoginTestsWithEmoteRun.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace ExplorerAutomation.Tests.Tests;

/// <summary>
/// Convergence stage for the wallet-login-with-emote scenario. Shelled out
/// by the Playwright orchestrator (`explorer-wallet-login.spec.ts`) via
/// `runExplorerTest('TestInWorldAndRunEmote')` once the client has accepted
/// the device-pairing approval and transitioned in-world. Asserts the
/// player-facing HUD is up and exercises a Fist Pump emote — catches
/// regressions in both the auth handshake and the post-login in-world
/// experience.
///
/// Inherits from <see cref="BaseTest"/> so its `OneTimeSetUp` waits through
/// splash → loading → main menu (handles the post-sign-in transition).
/// </summary>
[AllureSuite("Wallet Login")]
[Category("CrossVerify")]
[Order(18)]
public class WalletLoginInWorldEmote : BaseTest
{
[Test]
public void TestInWorldAndRunEmote()
{
Assert.That(Views.MainMenu.IsPresent(), Is.True,
"Main menu (sidebar) should be visible after the wallet-login handoff completes.");

// Equip Fist Pump to slot 0 — mirrors BackpackEmotesTests.TestSearchAndEquipFistPump
// verbatim, then triggers the equipped emote with the slot-0 hotkey (`1`).
PressKey(AltKeyCode.I);
Views.ExplorePanel.WaitFor();
Views.ExplorePanel.Backpack.EmotesTabButton.Click();

Views.ExplorePanel.Backpack.SearchBar.SetText("Fist Pump");
Wait(2);

Views.ExplorePanel.Backpack.Emotes.UnequipEmoteIfPresent(0);
Views.ExplorePanel.Backpack.Emotes.SetEmote(0, 0);
Reporter.Log("Fist Pump equipped to slot 0");

Views.ExplorePanel.Close();
Views.ExplorePanel.WaitForGone();

// Trigger slot 0. The avatar animation is not observable via AltTester reliably,
// so the assertion is implicit: the hotkey doesn't throw, the backpack stays
// closed, the client stays connected.
PressKey(AltKeyCode.Alpha1);
Wait(3);
Reporter.Log("Slot 0 emote triggered");
}
}
126 changes: 126 additions & 0 deletions explorer/Tests/Tests/CrossWebFirstLoginTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace ExplorerAutomation.Tests.Tests;

/// <summary>
/// Shared file path used by the cross-stack web-first signup flow (Flow 2)
/// to communicate the expected in-world username from the Playwright
/// orchestrator (writer) to this C# fixture (reader).
///
/// Lives in `~/Library/Application Support/DecentralandLauncherLight/` —
/// the same directory the desktop launcher uses for its own state and
/// where it consumes `auth-token-bridge.txt`. Keeping all cross-stack
/// communication files in one well-known dir mirrors what Flow 1 does
/// with the Explorer's `Application.persistentDataPath` for its own
/// `auth-url.txt` / `auth-verification-code.txt` pair.
///
/// TS-side mirror: `getExpectedUsernamePath()` /
/// `writeExpectedUsername()` in
/// `web/tests/auth/helpers/token-bridge.ts`.
/// </summary>
internal static class CrossWebFirstLoginPaths
{
public static string ExpectedUsernamePath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "DecentralandLauncherLight",
"expected-username.txt");
}

/// <summary>
/// Flow 2 — Stage 5a, identity guard. The launcher consumes
/// <c>auth-token-bridge.txt</c> on startup and the Explorer authenticates
/// against catalysts. This fixture reads the expected username the
/// Playwright spec wrote to <see cref="CrossWebFirstLoginPaths.ExpectedUsernamePath"/>
/// before launching the client, polls until the chat-panel titlebar's
/// <c>[TXT] UserName</c> label matches it (the catalyst profile fetch is
/// asynchronous; the label updates only after auth + profile resolve), and
/// fails fast with diagnostic output on timeout.
///
/// The assertion is load-bearing: a raw "Explorer reached in-world" alone
/// can pass on a launcher that silently consumed a stale bridge file or
/// booted a profile cache from a prior run with a different identity. Both
/// manifest as "main menu visible + emote plays" with the wrong account,
/// masking real auth-handoff regressions. Comparing the displayed username
/// to the QuickSetup username is the cheap, deterministic way to catch
/// that class of cache-pollution bugs.
///
/// Inherits from <see cref="BaseTest"/> but overrides
/// <see cref="BaseTest.EnsureInWorld"/> to avoid the default implementation's
/// immediate-click on <c>JumpIntoWorldButton</c> when it sees the auth
/// screen — that would ride past whatever state we need to read. Our
/// override only waits for splash to clear; the test body owns the polling.
///
/// The subsequent <c>WalletLoginInWorldEmote</c> fixture (Order 18, separate
/// <c>dotnet test</c> invocation) inherits the standard <c>EnsureInWorld</c>
/// which clicks Jump and rides to in-world if needed.
/// </summary>
[AllureSuite("Web-First Login")]
[Category("CrossVerify")]
[Order(17)]
public class WebFirstLoginUsernameAssert : BaseTest
{
/// <summary>
/// Wait for splash only; defer screen-state assertions to the [Test]
/// body. We can't pin to a single screen here because Flow 2's
/// token-bridge boot can legitimately land on either the cached-account
/// screen ("Welcome &lt;name&gt;" + JumpIntoWorld button) OR auto-ride
/// past it depending on launcher behavior and account state.
/// </summary>
protected override void EnsureInWorld()
{
if (Views.SplashScreen.IsPresent())
{
Reporter.Log("Splash screen detected — waiting for it to clear");
Views.SplashScreen.WaitForGone(60);
}
}

[Test]
public void TestInWorldUsernameMatches()
{
Assert.That(File.Exists(CrossWebFirstLoginPaths.ExpectedUsernamePath), Is.True,
$"Expected the Playwright orchestrator to write the expected username to " +
$"{CrossWebFirstLoginPaths.ExpectedUsernamePath} before launching the client. " +
"Without it, this fixture has nothing to compare against.");

var expected = File.ReadAllText(CrossWebFirstLoginPaths.ExpectedUsernamePath).Trim();
Assert.That(expected, Is.Not.Empty,
"expected-username.txt was written but is empty.");

// The Lobby.ExistingAccount.Screen `Title` element renders either
// "Welcome <username>" (first-launch / new account just minted via
// the auth-token-bridge) or "Welcome back <username>" (returning
// account whose profile already exists on catalysts). Both forms
// are valid evidence the bridge token landed on the right identity.
// Catalyst profile fetch is asynchronous, so the label only
// resolves once the profile data is back — 180s covers cold-cache
// lookups for brand-new accounts on CI.
var newUserGreeting = $"Welcome {expected}";
var returningGreeting = $"Welcome back {expected}";
var deadline = DateTime.UtcNow.AddSeconds(180);
string lastSeen = null;
while (DateTime.UtcNow < deadline)
{
try { lastSeen = Views.AuthenticationMainScreen.UsernameLabel.GetText(2); }
catch { /* element not yet in scene — keep polling */ }

var trimmed = lastSeen?.Trim();
if (string.Equals(trimmed, newUserGreeting, StringComparison.Ordinal) ||
string.Equals(trimmed, returningGreeting, StringComparison.Ordinal))
{
Reporter.Log($"Welcome-screen greeting matched '{trimmed}'");
return;
}
Thread.Sleep(1000);
}

throw new AssertionException(
$"Welcome-screen greeting was '{lastSeen}', expected '{newUserGreeting}' or " +
$"'{returningGreeting}' within 180s. " +
"Likely causes: (a) the launcher consumed a stale auth-token-bridge.txt from a " +
"prior run, (b) the Explorer's Thirdweb identity cache shadowed the bridge token " +
"(spec should clearExplorerIdentityCache + launch Explorer freshly), " +
"(c) the dapp minted a download URL that didn't carry the just-signed-up identity, " +
"or (d) catalyst profile resolution is slow / failing. " +
"Run DiagnoseAuthScreenUsernameLabel to inspect what's actually rendered.");
}
}
Loading