218 lines
8.1 KiB
C#
218 lines
8.1 KiB
C#
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using StellaOps.Scheduler.ImpactIndex;
|
|
|
|
namespace StellaOps.Scheduler.Worker.Tests;
|
|
|
|
public sealed class ImpactTargetingServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task ResolveByPurlsAsync_DeduplicatesKeysAndInvokesIndex()
|
|
{
|
|
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
|
var expected = CreateEmptyImpactSet(selector, usageOnly: false);
|
|
IEnumerable<string>? capturedKeys = null;
|
|
|
|
var index = new StubImpactIndex
|
|
{
|
|
OnResolveByPurls = (purls, usageOnly, sel, _) =>
|
|
{
|
|
capturedKeys = purls.ToArray();
|
|
Assert.False(usageOnly);
|
|
Assert.Equal(selector, sel);
|
|
return ValueTask.FromResult(expected);
|
|
}
|
|
};
|
|
|
|
var service = new ImpactTargetingService(index);
|
|
|
|
var result = await service.ResolveByPurlsAsync(
|
|
new[] { "pkg:npm/a", "pkg:npm/A ", null!, "pkg:npm/b" },
|
|
usageOnly: false,
|
|
selector);
|
|
|
|
Assert.Equal(expected, result);
|
|
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b" }, capturedKeys);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveByVulnerabilitiesAsync_ReturnsEmptyWhenNoIds()
|
|
{
|
|
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
|
var index = new StubImpactIndex();
|
|
var service = new ImpactTargetingService(index);
|
|
|
|
var result = await service.ResolveByVulnerabilitiesAsync(Array.Empty<string>(), usageOnly: true, selector);
|
|
|
|
Assert.Empty(result.Images);
|
|
Assert.True(result.UsageOnly);
|
|
Assert.Null(index.LastVulnerabilityIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveAllAsync_DelegatesToIndex()
|
|
{
|
|
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
|
var expected = CreateEmptyImpactSet(selector, usageOnly: true);
|
|
|
|
var index = new StubImpactIndex
|
|
{
|
|
OnResolveAll = (sel, usageOnly, _) =>
|
|
{
|
|
Assert.Equal(selector, sel);
|
|
Assert.True(usageOnly);
|
|
return ValueTask.FromResult(expected);
|
|
}
|
|
};
|
|
|
|
var service = new ImpactTargetingService(index);
|
|
var result = await service.ResolveAllAsync(selector, usageOnly: true);
|
|
|
|
Assert.Equal(expected, result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveByPurlsAsync_DeduplicatesImpactImagesByDigest()
|
|
{
|
|
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
|
var indexResult = new ImpactSet(
|
|
selector,
|
|
new[]
|
|
{
|
|
new ImpactImage(
|
|
"sha256:111",
|
|
"registry-1",
|
|
"repo/app",
|
|
namespaces: new[] { "team-a" },
|
|
tags: new[] { "v1" },
|
|
usedByEntrypoint: false,
|
|
labels: new[] { KeyValuePair.Create("env", "prod") }),
|
|
new ImpactImage(
|
|
"sha256:111",
|
|
"registry-1",
|
|
"repo/app",
|
|
namespaces: new[] { "team-b" },
|
|
tags: new[] { "v2" },
|
|
usedByEntrypoint: true,
|
|
labels: new[]
|
|
{
|
|
KeyValuePair.Create("env", "prod"),
|
|
KeyValuePair.Create("component", "api")
|
|
})
|
|
},
|
|
usageOnly: false,
|
|
DateTimeOffset.UtcNow,
|
|
total: 2,
|
|
snapshotId: "snap-1",
|
|
schemaVersion: SchedulerSchemaVersions.ImpactSet);
|
|
|
|
var index = new StubImpactIndex
|
|
{
|
|
OnResolveByPurls = (_, _, _, _) => ValueTask.FromResult(indexResult)
|
|
};
|
|
|
|
var service = new ImpactTargetingService(index);
|
|
var result = await service.ResolveByPurlsAsync(new[] { "pkg:npm/a" }, usageOnly: false, selector);
|
|
|
|
Assert.Single(result.Images);
|
|
var image = result.Images[0];
|
|
Assert.Equal("sha256:111", image.ImageDigest);
|
|
Assert.Equal(new[] { "team-a", "team-b" }, image.Namespaces);
|
|
Assert.Equal(new[] { "v1", "v2" }, image.Tags);
|
|
Assert.True(image.UsedByEntrypoint);
|
|
Assert.Equal("registry-1", image.Registry);
|
|
Assert.Equal("repo/app", image.Repository);
|
|
Assert.Equal(2, result.Total);
|
|
Assert.Equal("prod", image.Labels["env"]);
|
|
Assert.Equal("api", image.Labels["component"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveByPurlsAsync_FiltersImagesBySelectorConstraints()
|
|
{
|
|
var selector = new Selector(
|
|
SelectorScope.ByNamespace,
|
|
tenantId: "tenant-alpha",
|
|
namespaces: new[] { "team-a" },
|
|
includeTags: new[] { "prod-*" },
|
|
labels: new[] { new LabelSelector("env", new[] { "prod" }) });
|
|
|
|
var matching = new ImpactImage(
|
|
"sha256:aaa",
|
|
"registry-1",
|
|
"repo/app",
|
|
namespaces: new[] { "team-a" },
|
|
tags: new[] { "prod-202510" },
|
|
usedByEntrypoint: true,
|
|
labels: new[] { KeyValuePair.Create("env", "prod") });
|
|
|
|
var nonMatching = new ImpactImage(
|
|
"sha256:bbb",
|
|
"registry-1",
|
|
"repo/app",
|
|
namespaces: new[] { "team-b" },
|
|
tags: new[] { "dev" },
|
|
usedByEntrypoint: false,
|
|
labels: new[] { KeyValuePair.Create("env", "dev") });
|
|
|
|
var indexResult = new ImpactSet(
|
|
selector,
|
|
new[] { matching, nonMatching },
|
|
usageOnly: true,
|
|
DateTimeOffset.UtcNow,
|
|
total: 2,
|
|
snapshotId: null,
|
|
schemaVersion: SchedulerSchemaVersions.ImpactSet);
|
|
|
|
var index = new StubImpactIndex
|
|
{
|
|
OnResolveByPurls = (_, _, _, _) => ValueTask.FromResult(indexResult)
|
|
};
|
|
|
|
var service = new ImpactTargetingService(index);
|
|
var result = await service.ResolveByPurlsAsync(new[] { "pkg:npm/a" }, usageOnly: true, selector);
|
|
|
|
Assert.Single(result.Images);
|
|
Assert.Equal("sha256:aaa", result.Images[0].ImageDigest);
|
|
}
|
|
|
|
private static ImpactSet CreateEmptyImpactSet(Selector selector, bool usageOnly)
|
|
=> new(
|
|
selector,
|
|
ImmutableArray<ImpactImage>.Empty,
|
|
usageOnly,
|
|
DateTimeOffset.UtcNow,
|
|
0,
|
|
snapshotId: null,
|
|
schemaVersion: SchedulerSchemaVersions.ImpactSet);
|
|
|
|
private sealed class StubImpactIndex : IImpactIndex
|
|
{
|
|
public Func<IEnumerable<string>, bool, Selector, CancellationToken, ValueTask<ImpactSet>>? OnResolveByPurls { get; set; }
|
|
|
|
public Func<IEnumerable<string>, bool, Selector, CancellationToken, ValueTask<ImpactSet>>? OnResolveByVulnerabilities { get; set; }
|
|
|
|
public Func<Selector, bool, CancellationToken, ValueTask<ImpactSet>>? OnResolveAll { get; set; }
|
|
|
|
public IEnumerable<string>? LastVulnerabilityIds { get; private set; }
|
|
|
|
public ValueTask<ImpactSet> ResolveByPurlsAsync(IEnumerable<string> purls, bool usageOnly, Selector selector, CancellationToken cancellationToken = default)
|
|
=> OnResolveByPurls?.Invoke(purls, usageOnly, selector, cancellationToken)
|
|
?? ValueTask.FromResult(CreateEmptyImpactSet(selector, usageOnly));
|
|
|
|
public ValueTask<ImpactSet> ResolveByVulnerabilitiesAsync(IEnumerable<string> vulnerabilityIds, bool usageOnly, Selector selector, CancellationToken cancellationToken = default)
|
|
{
|
|
LastVulnerabilityIds = vulnerabilityIds;
|
|
return OnResolveByVulnerabilities?.Invoke(vulnerabilityIds, usageOnly, selector, cancellationToken)
|
|
?? ValueTask.FromResult(CreateEmptyImpactSet(selector, usageOnly));
|
|
}
|
|
|
|
public ValueTask<ImpactSet> ResolveAllAsync(Selector selector, bool usageOnly, CancellationToken cancellationToken = default)
|
|
=> OnResolveAll?.Invoke(selector, usageOnly, cancellationToken)
|
|
?? ValueTask.FromResult(CreateEmptyImpactSet(selector, usageOnly));
|
|
}
|
|
}
|