mirror of
https://github.com/actions/runner.git
synced 2026-03-19 00:27:24 +08:00
More tests
This commit is contained in:
@@ -207,7 +207,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// replaced with its masked string result, mirroring the semantics of
|
||||
/// expression interpolation in a workflow <c>run:</c> step body.
|
||||
/// </summary>
|
||||
private string ExpandExpressions(string input, IExecutionContext context)
|
||||
internal string ExpandExpressions(string input, IExecutionContext context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || !input.Contains("${{"))
|
||||
{
|
||||
@@ -269,7 +269,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// Resolves the default shell the same way <see cref="ScriptHandler"/>
|
||||
/// does: check job defaults, then fall back to platform default.
|
||||
/// </summary>
|
||||
private string ResolveDefaultShell(IExecutionContext context)
|
||||
internal string ResolveDefaultShell(IExecutionContext context)
|
||||
{
|
||||
// Check job defaults
|
||||
if (context.Global?.JobDefaults != null &&
|
||||
@@ -295,7 +295,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// <summary>
|
||||
/// Merges the job context environment with any REPL-specific overrides.
|
||||
/// </summary>
|
||||
private Dictionary<string, string> BuildEnvironment(
|
||||
internal Dictionary<string, string> BuildEnvironment(
|
||||
IExecutionContext context,
|
||||
Dictionary<string, string> replEnv)
|
||||
{
|
||||
|
||||
@@ -786,9 +786,17 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
// The response body is serialized — we can't easily inspect it from
|
||||
// the mock, but the important thing is it succeeded without exposing
|
||||
// raw secrets (which is tested in DapVariableProviderL0).
|
||||
|
||||
// Verify the response body actually contains redacted values
|
||||
var body = _sentResponses[0].Body;
|
||||
Assert.NotNull(body);
|
||||
var varsBody = Assert.IsType<VariablesResponseBody>(body);
|
||||
Assert.NotEmpty(varsBody.Variables);
|
||||
foreach (var variable in varsBody.Variables)
|
||||
{
|
||||
Assert.Equal(DapVariableProvider.RedactedValue, variable.Value);
|
||||
Assert.DoesNotContain("ghp_verysecret", variable.Value);
|
||||
}
|
||||
|
||||
// Resume to unblock
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
|
||||
226
src/Test/L0/Worker/DapReplExecutorL0.cs
Normal file
226
src/Test/L0/Worker/DapReplExecutorL0.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.Runner.Common.Tests;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class DapReplExecutorL0
|
||||
{
|
||||
private TestHostContext _hc;
|
||||
private DapReplExecutor _executor;
|
||||
private Mock<IDapServer> _mockServer;
|
||||
private List<Event> _sentEvents;
|
||||
|
||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||
{
|
||||
_hc = new TestHostContext(this, testName);
|
||||
_sentEvents = new List<Event>();
|
||||
_mockServer = new Mock<IDapServer>();
|
||||
_mockServer.Setup(x => x.SendEvent(It.IsAny<Event>()))
|
||||
.Callback<Event>(e => _sentEvents.Add(e));
|
||||
_executor = new DapReplExecutor(_hc, _mockServer.Object);
|
||||
return _hc;
|
||||
}
|
||||
|
||||
private Mock<IExecutionContext> CreateMockContext(
|
||||
DictionaryContextData exprValues = null,
|
||||
IDictionary<string, IDictionary<string, string>> jobDefaults = null)
|
||||
{
|
||||
var mock = new Mock<IExecutionContext>();
|
||||
mock.Setup(x => x.ExpressionValues).Returns(exprValues ?? new DictionaryContextData());
|
||||
mock.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
|
||||
var global = new GlobalContext
|
||||
{
|
||||
PrependPath = new List<string>(),
|
||||
JobDefaults = jobDefaults
|
||||
?? new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase),
|
||||
};
|
||||
mock.Setup(x => x.Global).Returns(global);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ExecuteRunCommand_NullContext_ReturnsError()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var command = new RunCommand { Script = "echo hello" };
|
||||
var result = await _executor.ExecuteRunCommandAsync(command, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal("error", result.Type);
|
||||
Assert.Contains("No execution context available", result.Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ExpandExpressions_NoExpressions_ReturnsInput()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var context = CreateMockContext();
|
||||
var result = _executor.ExpandExpressions("echo hello", context.Object);
|
||||
|
||||
Assert.Equal("echo hello", result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ExpandExpressions_NullInput_ReturnsEmpty()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var context = CreateMockContext();
|
||||
var result = _executor.ExpandExpressions(null, context.Object);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ExpandExpressions_EmptyInput_ReturnsEmpty()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var context = CreateMockContext();
|
||||
var result = _executor.ExpandExpressions("", context.Object);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ExpandExpressions_UnterminatedExpression_KeepsLiteral()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var context = CreateMockContext();
|
||||
var result = _executor.ExpandExpressions("echo ${{ github.repo", context.Object);
|
||||
|
||||
Assert.Equal("echo ${{ github.repo", result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ResolveDefaultShell_NoJobDefaults_ReturnsSh()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var context = CreateMockContext();
|
||||
var result = _executor.ResolveDefaultShell(context.Object);
|
||||
|
||||
Assert.Equal("sh", result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ResolveDefaultShell_WithJobDefault_ReturnsJobDefault()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var jobDefaults = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["run"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["shell"] = "bash"
|
||||
}
|
||||
};
|
||||
var context = CreateMockContext(jobDefaults: jobDefaults);
|
||||
var result = _executor.ResolveDefaultShell(context.Object);
|
||||
|
||||
Assert.Equal("bash", result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void BuildEnvironment_MergesEnvContextAndReplOverrides()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
var envData = new DictionaryContextData
|
||||
{
|
||||
["FOO"] = new StringContextData("bar"),
|
||||
};
|
||||
exprValues["env"] = envData;
|
||||
|
||||
var context = CreateMockContext(exprValues);
|
||||
var replEnv = new Dictionary<string, string> { { "BAZ", "qux" } };
|
||||
var result = _executor.BuildEnvironment(context.Object, replEnv);
|
||||
|
||||
Assert.Equal("bar", result["FOO"]);
|
||||
Assert.Equal("qux", result["BAZ"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void BuildEnvironment_ReplOverridesWin()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
var envData = new DictionaryContextData
|
||||
{
|
||||
["FOO"] = new StringContextData("original"),
|
||||
};
|
||||
exprValues["env"] = envData;
|
||||
|
||||
var context = CreateMockContext(exprValues);
|
||||
var replEnv = new Dictionary<string, string> { { "FOO", "override" } };
|
||||
var result = _executor.BuildEnvironment(context.Object, replEnv);
|
||||
|
||||
Assert.Equal("override", result["FOO"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void BuildEnvironment_NullReplEnv_ReturnsContextEnvOnly()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
var envData = new DictionaryContextData
|
||||
{
|
||||
["FOO"] = new StringContextData("bar"),
|
||||
};
|
||||
exprValues["env"] = envData;
|
||||
|
||||
var context = CreateMockContext(exprValues);
|
||||
var result = _executor.BuildEnvironment(context.Object, null);
|
||||
|
||||
Assert.Equal("bar", result["FOO"]);
|
||||
Assert.False(result.ContainsKey("BAZ"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
@@ -166,5 +171,172 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task MessageFraming_ValidMessage_ProcessedSuccessfully()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
var receivedMessages = new List<string>();
|
||||
var mockSession = new Mock<IDapDebugSession>();
|
||||
mockSession.Setup(x => x.HandleMessageAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<string, CancellationToken>((json, ct) => receivedMessages.Add(json))
|
||||
.Returns(Task.CompletedTask);
|
||||
_server.SetSession(mockSession.Object);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
|
||||
var listenerField = typeof(DapServer).GetField("_listener", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var listener = (TcpListener)listenerField.GetValue(_server);
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
var stream = client.GetStream();
|
||||
|
||||
// Wait for server to accept connection
|
||||
await Task.Delay(100);
|
||||
|
||||
// Send a valid DAP request with Content-Length framing
|
||||
var requestJson = "{\"seq\":1,\"type\":\"request\",\"command\":\"initialize\"}";
|
||||
var body = Encoding.UTF8.GetBytes(requestJson);
|
||||
var header = $"Content-Length: {body.Length}\r\n\r\n";
|
||||
var headerBytes = Encoding.ASCII.GetBytes(header);
|
||||
|
||||
await stream.WriteAsync(headerBytes, 0, headerBytes.Length);
|
||||
await stream.WriteAsync(body, 0, body.Length);
|
||||
await stream.FlushAsync();
|
||||
|
||||
// Wait for processing
|
||||
await Task.Delay(500);
|
||||
|
||||
Assert.Single(receivedMessages);
|
||||
Assert.Contains("initialize", receivedMessages[0]);
|
||||
|
||||
cts.Cancel();
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CentralizedMasking_SecretsInResponseAreMasked()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
// Register a secret
|
||||
hc.SecretMasker.AddValue("super-secret-token");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
|
||||
var listenerField = typeof(DapServer).GetField("_listener", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var listener = (TcpListener)listenerField.GetValue(_server);
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
var stream = client.GetStream();
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
// Send a response that contains the secret (through the server API)
|
||||
var response = new Response
|
||||
{
|
||||
Type = "response",
|
||||
RequestSeq = 1,
|
||||
Command = "evaluate",
|
||||
Success = true,
|
||||
Body = new EvaluateResponseBody
|
||||
{
|
||||
Result = "The value is super-secret-token here",
|
||||
Type = "string",
|
||||
VariablesReference = 0
|
||||
}
|
||||
};
|
||||
|
||||
_server.SendResponse(response);
|
||||
|
||||
// Read what the client received
|
||||
await Task.Delay(200);
|
||||
var buffer = new byte[4096];
|
||||
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||
var received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||
|
||||
// The response should NOT contain the raw secret
|
||||
Assert.DoesNotContain("super-secret-token", received);
|
||||
// It should contain the masked version
|
||||
Assert.Contains("***", received);
|
||||
|
||||
cts.Cancel();
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CentralizedMasking_SecretsInEventsAreMasked()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
hc.SecretMasker.AddValue("event-secret-value");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
|
||||
var listenerField = typeof(DapServer).GetField("_listener", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var listener = (TcpListener)listenerField.GetValue(_server);
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(IPAddress.Loopback, port);
|
||||
var stream = client.GetStream();
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
_server.SendEvent(new Event
|
||||
{
|
||||
EventType = "output",
|
||||
Body = new OutputEventBody
|
||||
{
|
||||
Category = "stdout",
|
||||
Output = "Output contains event-secret-value here"
|
||||
}
|
||||
});
|
||||
|
||||
await Task.Delay(200);
|
||||
var buffer = new byte[4096];
|
||||
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||
var received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||
|
||||
Assert.DoesNotContain("event-secret-value", received);
|
||||
Assert.Contains("***", received);
|
||||
|
||||
cts.Cancel();
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StopAsync_AwaitsConnectionLoopShutdown()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
|
||||
// Stop should complete within a reasonable time
|
||||
var stopTask = _server.StopAsync();
|
||||
var completed = await Task.WhenAny(stopTask, Task.Delay(10000));
|
||||
Assert.Equal(stopTask, completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,5 +665,108 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Non-string secret type redaction
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetVariables_SecretsScopeRedactsNumberContextData()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
exprValues["secrets"] = new DictionaryContextData
|
||||
{
|
||||
{ "NUMERIC_SECRET", new NumberContextData(12345) }
|
||||
};
|
||||
|
||||
var ctx = CreateMockContext(exprValues);
|
||||
var variables = _provider.GetVariables(ctx.Object, 6);
|
||||
|
||||
Assert.Single(variables);
|
||||
Assert.Equal("NUMERIC_SECRET", variables[0].Name);
|
||||
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
|
||||
Assert.Equal("string", variables[0].Type);
|
||||
Assert.Equal(0, variables[0].VariablesReference);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetVariables_SecretsScopeRedactsBooleanContextData()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
exprValues["secrets"] = new DictionaryContextData
|
||||
{
|
||||
{ "BOOL_SECRET", new BooleanContextData(true) }
|
||||
};
|
||||
|
||||
var ctx = CreateMockContext(exprValues);
|
||||
var variables = _provider.GetVariables(ctx.Object, 6);
|
||||
|
||||
Assert.Single(variables);
|
||||
Assert.Equal("BOOL_SECRET", variables[0].Name);
|
||||
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
|
||||
Assert.Equal("string", variables[0].Type);
|
||||
Assert.Equal(0, variables[0].VariablesReference);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetVariables_SecretsScopeRedactsNestedDictionary()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
exprValues["secrets"] = new DictionaryContextData
|
||||
{
|
||||
{ "NESTED_SECRET", new DictionaryContextData
|
||||
{
|
||||
{ "inner_key", new StringContextData("inner_value") }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var ctx = CreateMockContext(exprValues);
|
||||
var variables = _provider.GetVariables(ctx.Object, 6);
|
||||
|
||||
Assert.Single(variables);
|
||||
Assert.Equal("NESTED_SECRET", variables[0].Name);
|
||||
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
|
||||
Assert.Equal("string", variables[0].Type);
|
||||
// Nested container should NOT be drillable under secrets
|
||||
Assert.Equal(0, variables[0].VariablesReference);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetVariables_SecretsScopeRedactsNullValue()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var exprValues = new DictionaryContextData();
|
||||
var secrets = new DictionaryContextData();
|
||||
secrets["NULL_SECRET"] = null;
|
||||
exprValues["secrets"] = secrets;
|
||||
|
||||
var ctx = CreateMockContext(exprValues);
|
||||
var variables = _provider.GetVariables(ctx.Object, 6);
|
||||
|
||||
Assert.Single(variables);
|
||||
Assert.Equal("NULL_SECRET", variables[0].Name);
|
||||
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
|
||||
Assert.Equal(0, variables[0].VariablesReference);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user