Skip to content
Closed
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
148 changes: 148 additions & 0 deletions tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ public List<ConnectionStatus> CaptureStatusChanges()
_client.StatusChanged += (_, s) => changes.Add(s);
return changes;
}

public bool GetAuthFailedFlag() =>
GetPrivateField<bool>("_authFailed");

public List<string> CaptureAuthenticationFailedEvents()
{
var events = new List<string>();
_client.AuthenticationFailed += (_, msg) => events.Add(msg);
return events;
}
}

private class TestLogger : IOpenClawLogger
Expand Down Expand Up @@ -1626,4 +1636,142 @@ public void HandleRequestError_UnknownMethod_NodeList_SetsUnsupportedFlag()
var flags = helper.GetUnsupportedMethodFlags();
Assert.True(flags.NodeList);
}

// --- HandleRequestError: terminal auth errors (PR #206 fix) ---

[Theory]
[InlineData("token mismatch")]
[InlineData("origin not allowed")]
[InlineData("too many failed attempts")]
public void HandleRequestError_TerminalAuthError_SetsAuthFailedFlag(string errorMessage)
{
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-auth-1", "connect");

helper.ProcessRawMessage($$"""
{
"type": "res",
"id": "req-auth-1",
"ok": false,
"error": "{{errorMessage}}"
}
""");

Assert.True(helper.GetAuthFailedFlag());
}

[Fact]
public void HandleRequestError_TerminalAuthError_RaisesAuthenticationFailedEvent()
{
var helper = new GatewayClientTestHelper();
var authEvents = helper.CaptureAuthenticationFailedEvents();
helper.TrackPendingRequest("req-auth-2", "connect");

helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-2",
"ok": false,
"error": "token mismatch β€” reconnect rejected"
}
""");

Assert.Single(authEvents);
Assert.Contains("token mismatch", authEvents[0], StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void HandleRequestError_TerminalAuthError_RaisesErrorStatus()
{
var helper = new GatewayClientTestHelper();
var statusChanges = helper.CaptureStatusChanges();
helper.TrackPendingRequest("req-auth-3", "connect");

helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-3",
"ok": false,
"error": "origin not allowed"
}
""");

Assert.Contains(ConnectionStatus.Error, statusChanges);
}

[Fact]
public void HandleRequestError_TerminalAuthError_OnNonConnectMethod_DoesNotSetAuthFailed()
{
// Terminal auth check only applies to "connect" method β€” other methods must not set the flag
var helper = new GatewayClientTestHelper();
helper.TrackPendingRequest("req-auth-4", "sessions.list");

helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-4",
"ok": false,
"error": "token mismatch"
}
""");

Assert.False(helper.GetAuthFailedFlag());
}

[Fact]
public void HandleHelloOk_AfterAuthFailed_ClearsAuthFailedFlag()
{
var helper = new GatewayClientTestHelper();

// First, trigger auth failure
helper.TrackPendingRequest("req-auth-5", "connect");
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-auth-5",
"ok": false,
"error": "token mismatch"
}
""");
Assert.True(helper.GetAuthFailedFlag());

// Now receive hello-ok β€” flag must be cleared
helper.ProcessRawMessage("""
{
"type": "res",
"id": "req-hello-1",
"payload": {
"type": "hello-ok"
}
}
""");

Assert.False(helper.GetAuthFailedFlag());
}

[Fact]
public void HandleRequestError_AllDeviceSignatureModesExhausted_SetsAuthFailed()
{
var logger = new TestLogger();
var helper = new GatewayClientTestHelper(logger);
var authEvents = helper.CaptureAuthenticationFailedEvents();

// Cycle through all 4 signature modes by sending 4 successive rejections
for (int i = 1; i <= 4; i++)
{
helper.TrackPendingRequest($"req-sig-exhaust-{i}", "connect");
helper.ProcessRawMessage($$"""
{
"type": "res",
"id": "req-sig-exhaust-{{i}}",
"ok": false,
"error": "device signature invalid"
}
""");
}

Assert.True(helper.GetAuthFailedFlag());
Assert.Single(authEvents);
Assert.Contains("device signature", authEvents[0], StringComparison.OrdinalIgnoreCase);
}
}