audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
24
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/AGENTS.md
Normal file
24
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX3 Tests Charter
|
||||
|
||||
## Mission
|
||||
- Validate SPDX 3.0.1 parsing, validation, and context resolution.
|
||||
|
||||
## Responsibilities
|
||||
- Cover parser, validator, and version detection behaviors.
|
||||
- Exercise offline and embedded context resolution paths.
|
||||
- Guard determinism and error handling.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/sbom-service/architecture.md
|
||||
- docs/modules/sbom-service/spdx3-profile-support.md
|
||||
|
||||
## Working Agreement
|
||||
- Use fixed times and IDs in fixtures.
|
||||
- Avoid network access in tests.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for parser, validator, and version detection.
|
||||
- Determinism tests for ordering and serialized output.
|
||||
272
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ModelTests.cs
Normal file
272
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ModelTests.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
// <copyright file="ModelTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 model classes.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Package_Equality_Works()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg3 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "other-package",
|
||||
PackageVersion = "2.0.0"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(pkg1, pkg2);
|
||||
Assert.NotEqual(pkg1, pkg3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Relationship_TypeMapping_Works()
|
||||
{
|
||||
// Arrange
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3RelationshipType.DependsOn, relationship.RelationshipType);
|
||||
Assert.Single(relationship.To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_NormalizesValue()
|
||||
{
|
||||
// Arrange
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
hash.NormalizedHashValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesHex()
|
||||
{
|
||||
// Arrange
|
||||
var validHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
};
|
||||
|
||||
var invalidHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "xyz-not-hex!"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(validHash.IsValidHex());
|
||||
Assert.False(invalidHash.IsValidHex());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesLength()
|
||||
{
|
||||
// Arrange
|
||||
var validSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" // 64 chars
|
||||
};
|
||||
|
||||
var invalidSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef" // too short
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(validSha256.IsValidLength());
|
||||
Assert.False(invalidSha256.IsValidLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, 128)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha3_256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, 32)]
|
||||
public void Spdx3Hash_GetExpectedLength_ReturnsCorrectLength(Spdx3HashAlgorithm algorithm, int expected)
|
||||
{
|
||||
// Arrange
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = algorithm,
|
||||
HashValue = new string('a', expected)
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, hash.GetExpectedLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, false)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, false)]
|
||||
public void HashAlgorithm_IsRecommended_ReturnsCorrectValue(Spdx3HashAlgorithm algorithm, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsRecommended());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, false)]
|
||||
public void HashAlgorithm_IsDeprecated_ReturnsCorrectValue(Spdx3HashAlgorithm algorithm, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsDeprecated());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_ParseUri_Works()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Software));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Core, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Core));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Build));
|
||||
Assert.Null(Spdx3ProfileUris.ParseUri("https://unknown.example.com"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_Parse_WorksWithNames()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("Software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.Parse("BUILD"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_GetUri_Works()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileUris.Software, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Software));
|
||||
Assert.Equal(Spdx3ProfileUris.Core, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Core));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalIdentifierExtensions_GetPurl_Works()
|
||||
{
|
||||
// Arrange
|
||||
var identifiers = new[]
|
||||
{
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/lodash@4.17.21"
|
||||
},
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.Cpe23,
|
||||
Identifier = "cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*"
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", identifiers.GetPurl());
|
||||
Assert.Equal("cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*", identifiers.GetCpe23());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3CreationInfo_IsValidSpecVersion_Works()
|
||||
{
|
||||
// Arrange
|
||||
var valid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "3.0.1",
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var invalid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "2.3",
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(valid.IsValidSpecVersion());
|
||||
Assert.False(invalid.IsValidSpecVersion());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Document_ConformsTo_Works()
|
||||
{
|
||||
// Arrange
|
||||
var packages = new[] { new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "test" } };
|
||||
var profiles = new[] { Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core };
|
||||
var doc = new Spdx3Document(packages, [], profiles);
|
||||
|
||||
// Assert
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Software));
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Core));
|
||||
Assert.False(doc.ConformsTo(Spdx3ProfileIdentifier.Build));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Document_GetRootPackage_Works()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "root" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:pkg2", Name = "dep" };
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.Contains
|
||||
};
|
||||
|
||||
var doc = new Spdx3Document(
|
||||
[pkg1, pkg2, relationship],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var root = doc.GetRootPackage();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal("root", root.Name);
|
||||
}
|
||||
}
|
||||
265
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ParserTests.cs
Normal file
265
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ParserTests.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
// <copyright file="ParserTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 parser.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ParserTests : IDisposable
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
private readonly MemoryCache _cache;
|
||||
|
||||
public ParserTests()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
var options = Options.Create(new Spdx3ContextResolverOptions { AllowRemoteContexts = false });
|
||||
var resolver = new Spdx3ContextResolver(
|
||||
httpClientFactory.Object,
|
||||
_cache,
|
||||
NullLogger<Spdx3ContextResolver>.Instance,
|
||||
options,
|
||||
TimeProvider.System);
|
||||
|
||||
_parser = new Spdx3Parser(resolver, NullLogger<Spdx3Parser>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidSoftwareProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success, string.Join(", ", result.Errors.Select(e => e.Message)));
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.True(result.Document.Packages.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidLiteProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-lite-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Lite, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidBuildProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-build-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Build, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_InvalidNoContext_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "invalid-no-context.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_CONTEXT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsPackages()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var packages = result.Document.Packages;
|
||||
Assert.Equal(2, packages.Length);
|
||||
|
||||
var mainPackage = packages.FirstOrDefault(p => p.Name == "example-app");
|
||||
Assert.NotNull(mainPackage);
|
||||
Assert.Equal("1.0.0", mainPackage.PackageVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsRelationships()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var relationships = result.Document.Relationships;
|
||||
Assert.Equal(2, relationships.Length);
|
||||
|
||||
var dependsOn = relationships.FirstOrDefault(r => r.RelationshipType == Spdx3RelationshipType.DependsOn);
|
||||
Assert.NotNull(dependsOn);
|
||||
Assert.Equal("urn:spdx:example:package-1", dependsOn.From);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsCreationInfo()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Software, result.Document.Profiles);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Core, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsPurl()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var purls = result.Document.GetAllPurls().ToList();
|
||||
Assert.Contains("pkg:npm/example-app@1.0.0", purls);
|
||||
Assert.Contains("pkg:npm/lodash@4.17.21", purls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_NonexistentFile_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync("nonexistent-file.json", ct);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "FILE_NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseFromJsonAsync_ValidJson_Parses()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:test:pkg1",
|
||||
"name": "test-package",
|
||||
"packageVersion": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseFromJsonAsync(json, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Single(result.Document.Packages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_DocumentGetById_ReturnsElement()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var pkg = result.Document.GetById<Model.Software.Spdx3Package>("urn:spdx:example:package-1");
|
||||
Assert.NotNull(pkg);
|
||||
Assert.Equal("example-app", pkg.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_DocumentGetDependencies_ReturnsDeps()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var deps = result.Document.GetDependencies("urn:spdx:example:package-1").ToList();
|
||||
Assert.Single(deps);
|
||||
Assert.Equal("lodash", deps[0].Name);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"invalid": "json",
|
||||
"no_context": true,
|
||||
"spdxVersion": "SPDX-2.3"
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_SpdxDocument",
|
||||
"spdxId": "urn:spdx:build:document-1",
|
||||
"name": "Build Profile SBOM with Attestation",
|
||||
"creationInfo": {
|
||||
"@id": "_:creationInfoBuild",
|
||||
"@type": "CreationInfo",
|
||||
"specVersion": "3.0.1",
|
||||
"created": "2026-01-07T14:00:00Z",
|
||||
"createdBy": ["urn:spdx:build:ci-system"],
|
||||
"createdUsing": ["urn:spdx:build:stellaops-attestor"],
|
||||
"profile": ["Build", "Software", "Core"],
|
||||
"dataLicense": "CC0-1.0"
|
||||
},
|
||||
"rootElement": ["urn:spdx:build:package-artifact"]
|
||||
},
|
||||
{
|
||||
"@type": "Tool",
|
||||
"spdxId": "urn:spdx:build:ci-system",
|
||||
"name": "GitHub Actions"
|
||||
},
|
||||
{
|
||||
"@type": "Tool",
|
||||
"spdxId": "urn:spdx:build:stellaops-attestor",
|
||||
"name": "StellaOps Attestor v1.2.0"
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:build:package-artifact",
|
||||
"creationInfo": "_:creationInfoBuild",
|
||||
"name": "stellaops-scanner",
|
||||
"packageVersion": "1.5.0",
|
||||
"primaryPurpose": "Application",
|
||||
"downloadLocation": "https://github.com/stellaops/scanner/releases/download/v1.5.0/scanner-linux-amd64",
|
||||
"buildTime": "2026-01-07T13:45:00Z",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:github/stellaops/scanner@v1.5.0"
|
||||
},
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "GitOid",
|
||||
"identifier": "abc123def456789012345678901234567890abcd"
|
||||
}
|
||||
],
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
|
||||
},
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha512",
|
||||
"hashValue": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
}
|
||||
],
|
||||
"externalRef": [
|
||||
{
|
||||
"@type": "ExternalRef",
|
||||
"externalRefType": "SecurityAdvisory",
|
||||
"locator": ["https://github.com/stellaops/scanner/security/advisories"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:build:package-source",
|
||||
"creationInfo": "_:creationInfoBuild",
|
||||
"name": "stellaops-scanner-source",
|
||||
"packageVersion": "1.5.0",
|
||||
"primaryPurpose": "Source",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "Swhid",
|
||||
"identifier": "swh:1:cnt:abc123456789abcdef0123456789abcdef01234567"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:build:rel-generated",
|
||||
"creationInfo": "_:creationInfoBuild",
|
||||
"from": "urn:spdx:build:package-artifact",
|
||||
"to": ["urn:spdx:build:package-source"],
|
||||
"relationshipType": "GeneratedFrom"
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:build:rel-tool",
|
||||
"creationInfo": "_:creationInfoBuild",
|
||||
"from": "urn:spdx:build:package-artifact",
|
||||
"to": ["urn:spdx:build:ci-system"],
|
||||
"relationshipType": "BuildToolOf"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_SpdxDocument",
|
||||
"spdxId": "urn:spdx:lite:document-1",
|
||||
"name": "Lite Profile SBOM",
|
||||
"creationInfo": {
|
||||
"@id": "_:creationInfoLite",
|
||||
"@type": "CreationInfo",
|
||||
"specVersion": "3.0.1",
|
||||
"created": "2026-01-07T12:00:00Z",
|
||||
"createdBy": ["urn:spdx:lite:scanner"],
|
||||
"profile": ["Lite", "Core"],
|
||||
"dataLicense": "CC0-1.0"
|
||||
},
|
||||
"rootElement": ["urn:spdx:lite:package-main"]
|
||||
},
|
||||
{
|
||||
"@type": "Tool",
|
||||
"spdxId": "urn:spdx:lite:scanner",
|
||||
"name": "StellaOps CI Scanner"
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:lite:package-main",
|
||||
"creationInfo": "_:creationInfoLite",
|
||||
"name": "my-service",
|
||||
"packageVersion": "2.1.0",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:docker/my-service@2.1.0"
|
||||
}
|
||||
],
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "sha256:aabbccdd1122334455667788990011223344556677889900aabbccdd11223344"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:lite:package-dep1",
|
||||
"creationInfo": "_:creationInfoLite",
|
||||
"name": "express",
|
||||
"packageVersion": "4.18.2",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/express@4.18.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:lite:package-dep2",
|
||||
"creationInfo": "_:creationInfoLite",
|
||||
"name": "typescript",
|
||||
"packageVersion": "5.3.3",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/typescript@5.3.3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:lite:rel-1",
|
||||
"creationInfo": "_:creationInfoLite",
|
||||
"from": "urn:spdx:lite:package-main",
|
||||
"to": ["urn:spdx:lite:package-dep1"],
|
||||
"relationshipType": "DependsOn"
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:lite:rel-2",
|
||||
"creationInfo": "_:creationInfoLite",
|
||||
"from": "urn:spdx:lite:package-main",
|
||||
"to": ["urn:spdx:lite:package-dep2"],
|
||||
"relationshipType": "DependsOn"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_SpdxDocument",
|
||||
"spdxId": "urn:spdx:security:document-1",
|
||||
"name": "Security Profile SBOM with Vulnerability Data",
|
||||
"creationInfo": {
|
||||
"@id": "_:creationInfoSec",
|
||||
"@type": "CreationInfo",
|
||||
"specVersion": "3.0.1",
|
||||
"created": "2026-01-07T16:00:00Z",
|
||||
"createdBy": ["urn:spdx:security:scanner"],
|
||||
"profile": ["Security", "Software", "Core"],
|
||||
"dataLicense": "CC0-1.0"
|
||||
},
|
||||
"rootElement": ["urn:spdx:security:package-main"]
|
||||
},
|
||||
{
|
||||
"@type": "Tool",
|
||||
"spdxId": "urn:spdx:security:scanner",
|
||||
"name": "StellaOps Vulnerability Scanner v2.0.0"
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:security:package-main",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"name": "vulnerable-app",
|
||||
"packageVersion": "1.0.0",
|
||||
"primaryPurpose": "Application",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/vulnerable-app@1.0.0"
|
||||
}
|
||||
],
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "abc123def456789012345678901234567890abcdef123456789012345678901234"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:security:package-vulnerable-dep",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"name": "lodash",
|
||||
"packageVersion": "4.17.15",
|
||||
"primaryPurpose": "Library",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/lodash@4.17.15"
|
||||
},
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "Cpe23",
|
||||
"identifier": "cpe:2.3:a:lodash:lodash:4.17.15:*:*:*:*:*:*:*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "security_Vulnerability",
|
||||
"spdxId": "urn:spdx:security:vuln-cve-2020-8203",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"name": "CVE-2020-8203",
|
||||
"summary": "Prototype Pollution in lodash",
|
||||
"description": "Prototype pollution in zipObjectDeep in lodash before 4.17.20 allows an attacker to modify the prototype of Object.prototype.",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "Cve",
|
||||
"identifier": "CVE-2020-8203"
|
||||
}
|
||||
],
|
||||
"externalRef": [
|
||||
{
|
||||
"@type": "ExternalRef",
|
||||
"externalRefType": "SecurityAdvisory",
|
||||
"locator": ["https://nvd.nist.gov/vuln/detail/CVE-2020-8203"]
|
||||
}
|
||||
],
|
||||
"publishedTime": "2020-07-15T00:00:00Z",
|
||||
"modifiedTime": "2023-01-20T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"@type": "security_VulnAssessmentRelationship",
|
||||
"spdxId": "urn:spdx:security:assessment-1",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"from": "urn:spdx:security:vuln-cve-2020-8203",
|
||||
"to": ["urn:spdx:security:package-vulnerable-dep"],
|
||||
"relationshipType": "AffectsElement",
|
||||
"assessedElement": "urn:spdx:security:package-vulnerable-dep",
|
||||
"suppliedBy": "urn:spdx:security:scanner",
|
||||
"publishedTime": "2026-01-07T16:00:00Z"
|
||||
},
|
||||
{
|
||||
"@type": "security_VexVulnAssessmentRelationship",
|
||||
"spdxId": "urn:spdx:security:vex-1",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"from": "urn:spdx:security:vuln-cve-2020-8203",
|
||||
"to": ["urn:spdx:security:package-main"],
|
||||
"relationshipType": "HasAssessmentFor",
|
||||
"vexVersion": "1.0.0",
|
||||
"statusNotes": "The vulnerable function is not called in this application",
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_in_execute_path"
|
||||
},
|
||||
{
|
||||
"@type": "security_CvssV3VulnAssessmentRelationship",
|
||||
"spdxId": "urn:spdx:security:cvss-1",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"from": "urn:spdx:security:vuln-cve-2020-8203",
|
||||
"to": ["urn:spdx:security:package-vulnerable-dep"],
|
||||
"relationshipType": "HasAssessmentFor",
|
||||
"score": 7.4,
|
||||
"severity": "High",
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H"
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:security:rel-depends",
|
||||
"creationInfo": "_:creationInfoSec",
|
||||
"from": "urn:spdx:security:package-main",
|
||||
"to": ["urn:spdx:security:package-vulnerable-dep"],
|
||||
"relationshipType": "DependsOn"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_SpdxDocument",
|
||||
"spdxId": "urn:spdx:example:document-1",
|
||||
"name": "Example SPDX 3.0.1 Document",
|
||||
"creationInfo": {
|
||||
"@id": "_:creationInfo1",
|
||||
"@type": "CreationInfo",
|
||||
"specVersion": "3.0.1",
|
||||
"created": "2026-01-07T10:00:00Z",
|
||||
"createdBy": ["urn:spdx:example:stellaops-tool"],
|
||||
"profile": ["Software", "Core"],
|
||||
"dataLicense": "CC0-1.0"
|
||||
},
|
||||
"rootElement": ["urn:spdx:example:package-1"],
|
||||
"element": [
|
||||
"urn:spdx:example:package-1",
|
||||
"urn:spdx:example:package-2",
|
||||
"urn:spdx:example:file-1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Tool",
|
||||
"spdxId": "urn:spdx:example:stellaops-tool",
|
||||
"name": "StellaOps Scanner v1.0.0"
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:example:package-1",
|
||||
"creationInfo": "_:creationInfo1",
|
||||
"name": "example-app",
|
||||
"packageVersion": "1.0.0",
|
||||
"downloadLocation": "https://example.com/example-app-1.0.0.tar.gz",
|
||||
"homePage": "https://example.com",
|
||||
"primaryPurpose": "Application",
|
||||
"copyrightText": "Copyright 2026 Example Inc.",
|
||||
"suppliedBy": "urn:spdx:example:org-1",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/example-app@1.0.0"
|
||||
}
|
||||
],
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:spdx:example:package-2",
|
||||
"creationInfo": "_:creationInfo1",
|
||||
"name": "lodash",
|
||||
"packageVersion": "4.17.21",
|
||||
"primaryPurpose": "Library",
|
||||
"externalIdentifier": [
|
||||
{
|
||||
"@type": "ExternalIdentifier",
|
||||
"externalIdentifierType": "PackageUrl",
|
||||
"identifier": "pkg:npm/lodash@4.17.21"
|
||||
}
|
||||
],
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "software_File",
|
||||
"spdxId": "urn:spdx:example:file-1",
|
||||
"creationInfo": "_:creationInfo1",
|
||||
"name": "index.js",
|
||||
"contentType": "application/javascript",
|
||||
"verifiedUsing": [
|
||||
{
|
||||
"@type": "Hash",
|
||||
"algorithm": "sha256",
|
||||
"hashValue": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:example:rel-1",
|
||||
"creationInfo": "_:creationInfo1",
|
||||
"from": "urn:spdx:example:package-1",
|
||||
"to": ["urn:spdx:example:package-2"],
|
||||
"relationshipType": "DependsOn"
|
||||
},
|
||||
{
|
||||
"@type": "Relationship",
|
||||
"spdxId": "urn:spdx:example:rel-2",
|
||||
"creationInfo": "_:creationInfo1",
|
||||
"from": "urn:spdx:example:package-1",
|
||||
"to": ["urn:spdx:example:file-1"],
|
||||
"relationshipType": "Contains"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"spdxId": "urn:spdx:example:org-1",
|
||||
"name": "Example Inc."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Spdx3\StellaOps.Spdx3.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Samples\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
292
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ValidatorTests.cs
Normal file
292
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ValidatorTests.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
// <copyright file="ValidatorTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 validator.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ValidatorTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidDocument_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyDocument_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var doc = new Spdx3Document([], [], []);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_DOCUMENT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DuplicateSpdxId_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg1" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg2" };
|
||||
var doc = new Spdx3Document([pkg1, pkg2], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DUPLICATE_SPDX_ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DanglingRelationship_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:nonexistent"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid); // Warnings don't fail validation by default
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DANGLING_RELATIONSHIP_TO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyRelationshipTo_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = [],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_RELATIONSHIP_TO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidPurl_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "not-a-valid-purl"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Warnings, w => w.Code == "INVALID_PURL_FORMAT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidHashFormat_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "not-hex-value!"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "INVALID_HASH_FORMAT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DeprecatedHashAlgorithm_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RequiredProfileMissing_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
var options = new Spdx3ValidationOptions
|
||||
{
|
||||
RequiredProfiles = [Spdx3ProfileIdentifier.Build]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_REQUIRED_PROFILE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TreatWarningsAsErrors_ConvertsWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
var options = new Spdx3ValidationOptions { TreatWarningsAsErrors = true };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Info_ContainsStats()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Info, i => i.Code == "DOCUMENT_STATS");
|
||||
}
|
||||
|
||||
private static Spdx3Document CreateValidDocument()
|
||||
{
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "root-package",
|
||||
PackageVersion = "1.0.0",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "dep-package",
|
||||
PackageVersion = "2.0.0",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/dep-package@2.0.0"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
return new Spdx3Document(
|
||||
[pkg1, pkg2, rel],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// <copyright file="VersionDetectorTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX version detection.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VersionDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Detect_Spdx301JsonLd_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": []
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
Assert.True(result.IsJsonLd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_Spdx23_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx23, result.Version);
|
||||
Assert.False(result.IsJsonLd);
|
||||
Assert.Equal("SPDX-2.3", result.VersionString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_Spdx22_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"SPDXID": "SPDXRef-DOCUMENT"
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx22, result.Version);
|
||||
Assert.False(result.IsJsonLd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_Unknown_ReturnsUnknown()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"random": "data",
|
||||
"notSpdx": true
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Unknown, result.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_ContextWithArray_DetectsVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": [
|
||||
"https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
{ "custom": "http://example.org/custom" }
|
||||
],
|
||||
"@graph": []
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
Assert.True(result.IsJsonLd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_SpecVersionInGraph_DetectsVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://example.com/context",
|
||||
"@graph": [
|
||||
{
|
||||
"creationInfo": {
|
||||
"specVersion": "3.0.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx22, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx23, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx301, "Use Spdx3Parser (SPDX 3.0.1 parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Unknown, "Unknown format - manual inspection required")]
|
||||
public void GetParserRecommendation_ReturnsCorrectRecommendation(
|
||||
Spdx3VersionDetector.SpdxVersion version,
|
||||
string expected)
|
||||
{
|
||||
// Act
|
||||
var recommendation = Spdx3VersionDetector.GetParserRecommendation(version);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, recommendation);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user