Initial commit (history squashed)
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Build Test Deploy / build-test (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
417
src/StellaOps.Cli.Tests/Services/BackendOperationsClientTests.cs
Normal file
417
src/StellaOps.Cli.Tests/Services/BackendOperationsClientTests.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.Transport;
|
||||
using StellaOps.Cli.Tests.Testing;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Services;
|
||||
|
||||
public sealed class BackendOperationsClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DownloadScannerAsync_VerifiesDigestAndWritesMetadata()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes("scanner-blob");
|
||||
var digestHex = Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant();
|
||||
|
||||
var handler = new StubHttpMessageHandler((request, _) =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(contentBytes),
|
||||
RequestMessage = request
|
||||
};
|
||||
|
||||
response.Headers.Add("X-StellaOps-Digest", $"sha256:{digestHex}");
|
||||
response.Content.Headers.LastModified = DateTimeOffset.UtcNow;
|
||||
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
ScannerCacheDirectory = temp.Path,
|
||||
ScannerDownloadAttempts = 1
|
||||
};
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");
|
||||
var result = await client.DownloadScannerAsync("stable", targetPath, overwrite: false, verbose: true, CancellationToken.None);
|
||||
|
||||
Assert.False(result.FromCache);
|
||||
Assert.True(File.Exists(targetPath));
|
||||
|
||||
var metadataPath = targetPath + ".metadata.json";
|
||||
Assert.True(File.Exists(metadataPath));
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(metadataPath));
|
||||
Assert.Equal($"sha256:{digestHex}", document.RootElement.GetProperty("digest").GetString());
|
||||
Assert.Equal("stable", document.RootElement.GetProperty("channel").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadScannerAsync_ThrowsOnDigestMismatch()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var contentBytes = Encoding.UTF8.GetBytes("scanner-data");
|
||||
var handler = new StubHttpMessageHandler((request, _) =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(contentBytes),
|
||||
RequestMessage = request
|
||||
};
|
||||
response.Headers.Add("X-StellaOps-Digest", "sha256:deadbeef");
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
ScannerCacheDirectory = temp.Path,
|
||||
ScannerDownloadAttempts = 1
|
||||
};
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.DownloadScannerAsync("stable", targetPath, overwrite: true, verbose: false, CancellationToken.None));
|
||||
Assert.False(File.Exists(targetPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DownloadScannerAsync_RetriesOnFailure()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var successBytes = Encoding.UTF8.GetBytes("success");
|
||||
var digestHex = Convert.ToHexString(SHA256.HashData(successBytes)).ToLowerInvariant();
|
||||
var attempts = 0;
|
||||
|
||||
var handler = new StubHttpMessageHandler(
|
||||
(request, _) =>
|
||||
{
|
||||
attempts++;
|
||||
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = new StringContent("error")
|
||||
};
|
||||
},
|
||||
(request, _) =>
|
||||
{
|
||||
attempts++;
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = new ByteArrayContent(successBytes)
|
||||
};
|
||||
response.Headers.Add("X-StellaOps-Digest", $"sha256:{digestHex}");
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
ScannerCacheDirectory = temp.Path,
|
||||
ScannerDownloadAttempts = 3
|
||||
};
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");
|
||||
var result = await client.DownloadScannerAsync("stable", targetPath, overwrite: false, verbose: false, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, attempts);
|
||||
Assert.False(result.FromCache);
|
||||
Assert.True(File.Exists(targetPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadScanResultsAsync_RetriesOnRetryAfter()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var filePath = Path.Combine(temp.Path, "scan.json");
|
||||
await File.WriteAllTextAsync(filePath, "{}");
|
||||
|
||||
var attempts = 0;
|
||||
var handler = new StubHttpMessageHandler(
|
||||
(request, _) =>
|
||||
{
|
||||
attempts++;
|
||||
var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = new StringContent("busy")
|
||||
};
|
||||
response.Headers.Add("Retry-After", "1");
|
||||
return response;
|
||||
},
|
||||
(request, _) =>
|
||||
{
|
||||
attempts++;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
RequestMessage = request
|
||||
};
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
ScanUploadAttempts = 3
|
||||
};
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
await client.UploadScanResultsAsync(filePath, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, attempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadScanResultsAsync_ThrowsAfterMaxAttempts()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var filePath = Path.Combine(temp.Path, "scan.json");
|
||||
await File.WriteAllTextAsync(filePath, "{}");
|
||||
|
||||
var attempts = 0;
|
||||
var handler = new StubHttpMessageHandler(
|
||||
(request, _) =>
|
||||
{
|
||||
attempts++;
|
||||
return new HttpResponseMessage(HttpStatusCode.BadGateway)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = new StringContent("bad gateway")
|
||||
};
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
ScanUploadAttempts = 2
|
||||
};
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.UploadScanResultsAsync(filePath, CancellationToken.None));
|
||||
Assert.Equal(2, attempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerJobAsync_ReturnsAcceptedResult()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler((request, _) =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.Accepted)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = JsonContent.Create(new JobRunResponse
|
||||
{
|
||||
RunId = Guid.NewGuid(),
|
||||
Status = "queued",
|
||||
Kind = "export:json",
|
||||
Trigger = "cli",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
})
|
||||
};
|
||||
response.Headers.Location = new Uri("/jobs/export:json/runs/123", UriKind.Relative);
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions { BackendUrl = "https://feedser.example" };
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
var result = await client.TriggerJobAsync("export:json", new Dictionary<string, object?>(), CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("Accepted", result.Message);
|
||||
Assert.Equal("/jobs/export:json/runs/123", result.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerJobAsync_ReturnsFailureMessage()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler((request, _) =>
|
||||
{
|
||||
var problem = new
|
||||
{
|
||||
title = "Job already running",
|
||||
detail = "export job active"
|
||||
};
|
||||
|
||||
var response = new HttpResponseMessage(HttpStatusCode.Conflict)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = JsonContent.Create(problem)
|
||||
};
|
||||
return response;
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions { BackendUrl = "https://feedser.example" };
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());
|
||||
|
||||
var result = await client.TriggerJobAsync("export:json", new Dictionary<string, object?>(), CancellationToken.None);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Job already running", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TriggerJobAsync_UsesAuthorityTokenWhenConfigured()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
|
||||
var handler = new StubHttpMessageHandler((request, _) =>
|
||||
{
|
||||
Assert.NotNull(request.Headers.Authorization);
|
||||
Assert.Equal("Bearer", request.Headers.Authorization!.Scheme);
|
||||
Assert.Equal("token-123", request.Headers.Authorization.Parameter);
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.Accepted)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = JsonContent.Create(new JobRunResponse
|
||||
{
|
||||
RunId = Guid.NewGuid(),
|
||||
Kind = "test",
|
||||
Status = "Pending",
|
||||
Trigger = "cli",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
var httpClient = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://feedser.example")
|
||||
};
|
||||
|
||||
var options = new StellaOpsCliOptions
|
||||
{
|
||||
BackendUrl = "https://feedser.example",
|
||||
Authority =
|
||||
{
|
||||
Url = "https://authority.example",
|
||||
ClientId = "cli",
|
||||
ClientSecret = "secret",
|
||||
Scope = "feedser.jobs.trigger",
|
||||
TokenCacheDirectory = temp.Path
|
||||
}
|
||||
};
|
||||
|
||||
var tokenClient = new StubTokenClient();
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>(), tokenClient);
|
||||
|
||||
var result = await client.TriggerJobAsync("test", new Dictionary<string, object?>(), CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("Accepted", result.Message);
|
||||
Assert.True(tokenClient.Requests > 0);
|
||||
}
|
||||
|
||||
private sealed class StubTokenClient : IStellaOpsTokenClient
|
||||
{
|
||||
private readonly StellaOpsTokenResult _tokenResult;
|
||||
|
||||
public int Requests { get; private set; }
|
||||
|
||||
public StubTokenClient()
|
||||
{
|
||||
_tokenResult = new StellaOpsTokenResult(
|
||||
"token-123",
|
||||
"Bearer",
|
||||
DateTimeOffset.UtcNow.AddMinutes(5),
|
||||
new[] { StellaOpsScopes.FeedserJobsTrigger });
|
||||
}
|
||||
|
||||
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
|
||||
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new JsonWebKeySet("{\"keys\":[]}"));
|
||||
|
||||
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);
|
||||
|
||||
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Requests++;
|
||||
return Task.FromResult(_tokenResult);
|
||||
}
|
||||
|
||||
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Requests++;
|
||||
return Task.FromResult(_tokenResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user