Compare commits

...

5 Commits

Author SHA1 Message Date
0db28a025f chore: add empty root-level Program.cs placeholder 2026-09-21 10:10:47 +02:00
6a321c1022 build(sln): register Tests.Live and Tests.NuGet projects; expand platform configurations
- Add DocumentService.Tests.Live (xUnit live tests, GUID CC5A7D7B) to solution
  under the existing Tests solution folder
- Add DocumentService.Tests.NuGet (smoke-test console app, GUID EE3E7C50) to
  solution under the Tests solution folder
- Expand GlobalSection(SolutionConfigurationPlatforms) to include x64 and x86
  variants for Debug and Release alongside the existing Any CPU entries
- Add corresponding ProjectConfigurationPlatforms entries for all projects
  (existing + new) so VS/MSBuild can resolve the correct output paths when
  targeting x64/x86 explicitly
2026-09-21 10:10:39 +02:00
dcb735f8c8 test(live): add live API integration test project (DocumentService.Tests.Live)
End-to-end tests that run against a real, deployed API instance to verify that
all controllers respond correctly over HTTP - bypassing WebApplicationFactory.

Project setup:
- Targets net8.0; xUnit 2.9.3 + FluentAssertions 7.0.0
- References DocumentService.Client, Application and Domain as project refs
- Embeds 4 test PDFs as EmbeddedResources:
    valid.pdf, pdfWithSwissQRCode.pdf, pdfWithMoreThanOneAttachment.pdf,
    encryptedPdf.pdf

LiveApiTestBase:
- Base URL defaults to http://172.24.12.39:9393, overridable via
  LIVE_API_BASE_URL env var
- Constructs typed clients (PdfValidationClient, PdfAttachmentClient,
  PdfOperationsClient, SwissQrCodeClient) backed by a single HttpClient
- LoadPdfBytes / LoadPdfStream helpers for EmbeddedResource access

Test coverage per file:
- PdfValidationLiveTests   : Stream + byte[] overloads for validate and
                             validate-pdfa; empty/corrupted inputs assert 4xx/5xx
- PdfAttachmentLiveTests   : check + extract endpoints; stream and byte[] variants
- PdfOperationsLiveTests   : merge and stamp endpoints
- SwissQrCodeLiveTests     : extract Swiss QR bill; invalid content asserts error

Tests are intentionally skipped in CI (no live server); run manually with
the LIVE_API_BASE_URL variable pointing to a running instance.
2026-09-21 10:10:28 +02:00
6527cdcbb0 test(nuget): add NuGet smoke-test console app (DocumentService.Tests.NuGet)
Standalone console application that references the published DocumentService.Client
NuGet package (v1.2.2) and verifies the full happy-path against a live API instance.

- Targets net8.0; references DocumentService.Client 1.2.2 via NuGet (not project ref)
- Embeds valid.pdf as an EmbeddedResource so no file-system path is required
- Calls Client.Validation.ValidateAsync(Stream) and prints PageCount, PdfVersion,
  IsEncrypted, AttachmentCount, FileSizeBytes to stdout
- Handles HttpRequestException and InvalidOperationException with descriptive output
- Intended to run once after a new package is pushed to confirm the .nupkg bundles
  Application and Domain DLLs correctly (validates the embedding targets added in
  the previous commit)
2026-09-21 10:10:09 +02:00
24db1b0d0a build(client): bump version to 1.2.2 and embed Application/Domain DLLs into NuGet package
- Bump Version, AssemblyVersion, FileVersion from 1.2.0 to 1.2.2
- Mark Application and Domain ProjectReferences as PrivateAssets=all so they
  are not propagated as transitive dependencies to consumers
- Add IncludeReferencedProjectDlls MSBuild target to pack Application.dll and
  Domain.dll into the correct lib/<tfm>/ path inside the .nupkg
- Add CopyProjectReferencesToPackage target (TargetsForTfmSpecificBuildOutput)
  as the per-TFM companion needed for multi-targeting builds to pick up both
  referenced project DLLs correctly
- Fix stray UTF-8 BOM on the opening Project element
2026-09-21 10:09:56 +02:00
16 changed files with 1401 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
@@ -11,9 +11,9 @@
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/Rec.git</RepositoryUrl>
<PackageTags>digital data document service api client</PackageTags>
<Version>1.2.0</Version>
<AssemblyVersion>1.2.0.0</AssemblyVersion>
<FileVersion>1.2.0.0</FileVersion>
<Version>1.2.2</Version>
<AssemblyVersion>1.2.2.0</AssemblyVersion>
<FileVersion>1.2.2.0</FileVersion>
<Description></Description>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -44,7 +44,34 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj">
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<!-- Embed Application and Domain DLLs into the NuGet package lib folder -->
<Target Name="IncludeReferencedProjectDlls" BeforeTargets="_GetPackageFiles" Condition="'$(TargetFramework)' != ''">
<ItemGroup>
<None Include="..\DocumentService.Application\bin\$(Configuration)\$(TargetFramework)\DocumentService.Application.dll"
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
<None Include="..\DocumentService.Domain\bin\$(Configuration)\$(TargetFramework)\DocumentService.Domain.dll"
Pack="true" PackagePath="lib\$(TargetFramework)\" Visible="false" />
</ItemGroup>
</Target>
<!-- Trigger inner build per-TFM to pick up IncludeReferencedProjectDlls target -->
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
<ItemGroup>
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget','ProjectReference'))"
Condition="'%(Filename)' == 'DocumentService.Application' Or '%(Filename)' == 'DocumentService.Domain'" />
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,235 @@
using DocumentService.Application.Common.DTOs;
using FluentAssertions;
namespace DocumentService.Tests.Live.API;
/// <summary>
/// Live tests for PDF attachment endpoints (check + extract).
/// Uses PdfAttachmentClient — routes managed by the client.
/// Target: http://172.24.12.39:9393/
/// </summary>
[Collection("Live API")]
public class PdfAttachmentLiveTests : LiveApiTestBase
{
// ──────────────────────────────────────────────────────────────
// CheckAsync(Stream) → multipart → /api/pdf/attachments/check
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Check_Stream_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
{
using var stream = LoadPdfStream("valid.pdf");
var result = await AttachmentClient.CheckAsync(stream);
result.Should().NotBeNull();
result.HasAttachments.Should().BeFalse();
result.AttachmentCount.Should().Be(0);
result.Attachments.Should().BeEmpty();
}
[Fact]
public async Task Check_Stream_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
{
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
var result = await AttachmentClient.CheckAsync(stream);
result.HasAttachments.Should().BeTrue();
result.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
result.Attachments.Should().HaveCount(6);
foreach (var attachment in result.Attachments)
{
attachment.FileName.Should().NotBeNullOrEmpty();
attachment.MimeType.Should().NotBeNullOrEmpty();
attachment.Size.Should().BeGreaterThan(0);
}
}
[Fact]
public async Task Check_Stream_PdfWithSwissQrCode_HasNoAttachments()
{
// QR code PDF has no embedded file attachments
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
var result = await AttachmentClient.CheckAsync(stream);
result.HasAttachments.Should().BeFalse();
result.AttachmentCount.Should().Be(0);
}
[Fact]
public async Task Check_Stream_EmptyStream_Throws()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var act = async () => await AttachmentClient.CheckAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task Check_Stream_CorruptedContent_Throws()
{
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
using var stream = new MemoryStream(garbage);
var act = async () => await AttachmentClient.CheckAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>(
"corrupted PDF should return 500 → client throws");
}
// ──────────────────────────────────────────────────────────────
// CheckAsync(byte[]) → Base64 JSON → /api/pdf/attachments/check
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Check_Bytes_PdfWithoutAttachments_ReturnsHasAttachmentsFalse()
{
byte[] bytes = LoadPdfBytes("valid.pdf");
var result = await AttachmentClient.CheckAsync(bytes);
result.HasAttachments.Should().BeFalse();
result.AttachmentCount.Should().Be(0);
}
[Fact]
public async Task Check_Bytes_PdfWith6Attachments_ReturnsAllAttachmentsWithMetadata()
{
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
var result = await AttachmentClient.CheckAsync(bytes);
result.HasAttachments.Should().BeTrue();
result.AttachmentCount.Should().Be(6);
result.Attachments.Should().HaveCount(6);
foreach (var attachment in result.Attachments)
{
attachment.FileName.Should().NotBeNullOrEmpty();
attachment.MimeType.Should().NotBeNullOrEmpty();
attachment.Size.Should().BeGreaterThan(0);
}
}
[Fact]
public async Task Check_Bytes_EmptyArray_Throws()
{
var act = async () => await AttachmentClient.CheckAsync(Array.Empty<byte>());
await act.Should().ThrowAsync<HttpRequestException>();
}
// ──────────────────────────────────────────────────────────────
// ExtractAsync(Stream) → multipart → /api/pdf/attachments/extract
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Extract_Stream_PdfWith6Attachments_Returns6NamedStreams()
{
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
var attachments = await AttachmentClient.ExtractAsync(stream);
attachments.Should().HaveCount(6, "PDF contains 6 attachments");
foreach (var (name, content) in attachments)
{
name.Should().NotBeNullOrEmpty();
content.Length.Should().BeGreaterThan(0);
content.Dispose();
}
}
[Fact]
public async Task Extract_Stream_AttachmentNamesAreUnique()
{
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
var attachments = await AttachmentClient.ExtractAsync(stream);
try
{
attachments.Keys.Should().OnlyHaveUniqueItems(
"ZIP entry names must be unique within the archive");
}
finally
{
foreach (var s in attachments.Values) s.Dispose();
}
}
[Fact]
public async Task Extract_Stream_EmptyStream_Throws()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var act = async () => await AttachmentClient.ExtractAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task Extract_Stream_CorruptedContent_Throws()
{
byte[] garbage = "This is not a valid PDF content"u8.ToArray();
using var stream = new MemoryStream(garbage);
var act = async () => await AttachmentClient.ExtractAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>();
}
// ──────────────────────────────────────────────────────────────
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/attachments/extract
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Extract_Bytes_PdfWith6Attachments_Returns6NamedStreams()
{
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
var attachments = await AttachmentClient.ExtractAsync(bytes);
attachments.Should().HaveCount(6);
foreach (var (name, content) in attachments)
{
name.Should().NotBeNullOrEmpty();
content.Length.Should().BeGreaterThan(0);
content.Dispose();
}
}
[Fact]
public async Task Extract_Bytes_AttachmentContentsAreNonEmpty()
{
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
var attachments = await AttachmentClient.ExtractAsync(bytes);
try
{
foreach (var (_, content) in attachments)
{
((MemoryStream)content).ToArray().Should().NotBeEmpty(
"every extracted attachment must have content");
}
}
finally
{
foreach (var s in attachments.Values) s.Dispose();
}
}
[Fact]
public async Task Extract_Bytes_EmptyArray_Throws()
{
var act = async () => await AttachmentClient.ExtractAsync(Array.Empty<byte>());
await act.Should().ThrowAsync<HttpRequestException>();
}
}

View File

@@ -0,0 +1,532 @@
using DocumentService.Application.Common.DTOs.Requests;
using DocumentService.Domain.Models.ValueObjects;
using FluentAssertions;
namespace DocumentService.Tests.Live.API;
/// <summary>
/// Live tests for PDF operations: merge, annotate, stamp.
/// Uses PdfOperationsClient — routes managed by the client.
/// Target: http://172.24.12.39:9393/
/// </summary>
[Collection("Live API")]
public class PdfOperationsLiveTests : LiveApiTestBase
{
// ══════════════════════════════════════════════════════════════
// MERGE — MergeAsync(IEnumerable<Stream>) → multipart
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Merge_Streams_TwoPdfs_ReturnsMergedPdfStream()
{
using var pdf1 = LoadPdfStream("valid.pdf");
using var pdf2 = LoadPdfStream("valid.pdf");
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2 });
merged.Should().NotBeNull();
merged.Length.Should().BeGreaterThan(0);
// Merged must start with PDF magic bytes (%PDF = 0x25 0x50 0x44 0x46)
var header = new byte[4];
merged.ReadExactly(header, 0, 4);
header.Should().Equal(new byte[] { 0x25, 0x50, 0x44, 0x46 },
"merged result must be a valid PDF");
}
[Fact]
public async Task Merge_Streams_ThreePdfs_ReturnsMergedPdfStream()
{
using var pdf1 = LoadPdfStream("valid.pdf");
using var pdf2 = LoadPdfStream("valid.pdf");
using var pdf3 = LoadPdfStream("valid.pdf");
await using var merged = await OperationsClient.MergeAsync(new[] { pdf1, pdf2, pdf3 });
merged.Should().NotBeNull();
merged.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Merge_Streams_OnlyOnePdf_Throws()
{
using var pdf = LoadPdfStream("valid.pdf");
var act = async () =>
{
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
};
await act.Should().ThrowAsync<HttpRequestException>(
"at least 2 PDFs are required; single PDF should return 400");
}
[Fact]
public async Task Merge_Streams_CorruptedPdf_Throws()
{
byte[] garbage = "NOT A PDF"u8.ToArray();
using var corrupted = new MemoryStream(garbage);
using var valid = LoadPdfStream("valid.pdf");
var act = async () =>
{
await using var result = await OperationsClient.MergeAsync(new[] { corrupted, valid });
};
await act.Should().ThrowAsync<HttpRequestException>(
"corrupted PDF should return 500");
}
// ══════════════════════════════════════════════════════════════
// MERGE — MergeAsync(IEnumerable<byte[]>) → Base64 JSON
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Merge_Bytes_TwoPdfs_ReturnsMergedPdfStream()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
await using var merged = await OperationsClient.MergeAsync(new[] { pdf, pdf });
merged.Should().NotBeNull();
merged.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Merge_Bytes_WithPageRanges_ReturnsMergedPdfStream()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
await using var merged = await OperationsClient.MergeAsync(
new[] { pdf, pdf },
pageRanges: ["1", "1"]);
merged.Should().NotBeNull();
merged.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Merge_Bytes_OnlyOnePdf_Throws()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var act = async () =>
{
await using var result = await OperationsClient.MergeAsync(new[] { pdf });
};
await act.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task Merge_Bytes_PageRangeExceedsPageCount_Throws()
{
byte[] pdf = LoadPdfBytes("valid.pdf"); // 1-page PDF
var act = async () =>
{
await using var result = await OperationsClient.MergeAsync(
new[] { pdf, pdf },
pageRanges: ["999-1000", null]);
};
await act.Should().ThrowAsync<HttpRequestException>(
"out-of-bounds page range should return 400");
}
// ══════════════════════════════════════════════════════════════
// ANNOTATE — AnnotateAsync(Stream, ...) → multipart
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Annotate_Stream_TextMarkupHighlight_ReturnsAnnotatedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty, // not used in Stream overload
AnnotationType = AnnotationType.TextMarkup,
PageNumber = 1,
X1 = 100, Y1 = 100, X2 = 300, Y2 = 120,
TextMarkupStyle = TextMarkupStyle.Highlight,
Color = "FFFF00",
Author = "LiveTest"
};
await using var result = await OperationsClient.AnnotateAsync(stream, request);
result.Should().NotBeNull();
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Stream_Underline_ReturnsAnnotatedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.TextMarkup,
PageNumber = 1,
X1 = 50, Y1 = 700, X2 = 300, Y2 = 720,
TextMarkupStyle = TextMarkupStyle.Underline
};
await using var result = await OperationsClient.AnnotateAsync(stream, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Stream_Circle_ReturnsAnnotatedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.Circle,
PageNumber = 1,
X1 = 100, Y1 = 200, X2 = 200, Y2 = 300,
Color = "00FF00",
Content = "Circle annotation"
};
await using var result = await OperationsClient.AnnotateAsync(stream, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Stream_Square_ReturnsAnnotatedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.Square,
PageNumber = 1,
X1 = 250, Y1 = 250, X2 = 350, Y2 = 350,
Color = "0000FF"
};
await using var result = await OperationsClient.AnnotateAsync(stream, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Stream_InvalidPageNumber_Throws()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.FreeText,
PageNumber = 9999, // exceeds page count
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100,
Content = "Test"
};
var act = async () =>
{
await using var result = await OperationsClient.AnnotateAsync(stream, request);
};
await act.Should().ThrowAsync<HttpRequestException>(
"invalid page number should return 400");
}
// ══════════════════════════════════════════════════════════════
// ANNOTATE — AnnotateAsync(byte[], ...) → Base64 JSON
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Annotate_Bytes_FreeText_ReturnsAnnotatedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty, // client fills this in
AnnotationType = AnnotationType.FreeText,
PageNumber = 1,
X1 = 50, Y1 = 50, X2 = 200, Y2 = 100,
Content = "Free text from live test",
Author = "LiveTestRunner",
Color = "FF0000"
};
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
result.Should().NotBeNull();
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Bytes_StickyNote_ReturnsAnnotatedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.StickyNote,
PageNumber = 1,
X1 = 300, Y1 = 300, X2 = 320, Y2 = 320,
Content = "Sticky note from live test",
Author = "Reviewer"
};
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Bytes_TextMarkupStrikeout_ReturnsAnnotatedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.TextMarkup,
PageNumber = 1,
X1 = 60, Y1 = 600, X2 = 250, Y2 = 620,
TextMarkupStyle = TextMarkupStyle.Strikeout,
Color = "FF6600"
};
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Annotate_Bytes_FreeTextWithoutContent_Throws()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.FreeText,
PageNumber = 1,
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
// Content intentionally missing
};
var act = async () =>
{
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
};
await act.Should().ThrowAsync<HttpRequestException>(
"FreeText without content should return 400");
}
[Fact]
public async Task Annotate_Bytes_TextMarkupWithoutStyle_Throws()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddAnnotationBase64Request
{
Base64Pdf = string.Empty,
AnnotationType = AnnotationType.TextMarkup,
PageNumber = 1,
X1 = 0, Y1 = 0, X2 = 100, Y2 = 100
// TextMarkupStyle intentionally missing
};
var act = async () =>
{
await using var result = await OperationsClient.AnnotateAsync(pdf, request);
};
await act.Should().ThrowAsync<HttpRequestException>(
"TextMarkup without style should return 400");
}
// ══════════════════════════════════════════════════════════════
// STAMP — StampAsync(Stream, ...) → multipart
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Stamp_Stream_TextStamp_ReturnsStampedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
X = 100, Y = 400,
Text = "CONFIDENTIAL",
FontSize = 24,
Color = "FF0000",
Opacity = 0.5
};
await using var result = await OperationsClient.StampAsync(stream, request);
result.Should().NotBeNull();
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Stream_TextStamp_WithRotation_ReturnsStampedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
X = 300, Y = 400,
Text = "DRAFT",
Rotation = 45,
Opacity = 0.3,
Placement = StampPlacement.Background
};
await using var result = await OperationsClient.StampAsync(stream, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Stream_PredefinedStamp_Confidential_ReturnsStampedPdf()
{
using var stream = LoadPdfStream("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Predefined,
X = 100, Y = 400,
PredefinedType = PredefinedStampType.Confidential,
Opacity = 0.6
};
await using var result = await OperationsClient.StampAsync(stream, request);
result.Length.Should().BeGreaterThan(0);
}
// ══════════════════════════════════════════════════════════════
// STAMP — StampAsync(byte[], ...) → Base64 JSON
// ══════════════════════════════════════════════════════════════
[Fact]
public async Task Stamp_Bytes_TextStamp_ReturnsStampedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
X = 100, Y = 400,
Text = "APPROVED",
FontName = "Arial",
FontSize = 20,
Color = "008000",
Opacity = 0.7,
Placement = StampPlacement.Foreground
};
await using var result = await OperationsClient.StampAsync(pdf, request);
result.Should().NotBeNull();
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Bytes_WatermarkBackground_ReturnsStampedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
X = 150, Y = 350,
Text = "WATERMARK",
FontSize = 36,
Color = "AAAAAA",
Opacity = 0.2,
Rotation = 45,
Placement = StampPlacement.Background
};
await using var result = await OperationsClient.StampAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Bytes_PredefinedStamp_Approved_ReturnsStampedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Predefined,
X = 100, Y = 400,
PredefinedType = PredefinedStampType.Approved,
Opacity = 0.8
};
await using var result = await OperationsClient.StampAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Bytes_PredefinedStamp_Draft_ReturnsStampedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Predefined,
X = 200, Y = 500,
PredefinedType = PredefinedStampType.Draft,
Opacity = 0.5
};
await using var result = await OperationsClient.StampAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Bytes_TextStampOnSpecificPage_ReturnsStampedPdf()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
PageNumbers = [1],
X = 200, Y = 500,
Text = "PAGE 1 ONLY",
Opacity = 0.5
};
await using var result = await OperationsClient.StampAsync(pdf, request);
result.Length.Should().BeGreaterThan(0);
}
[Fact]
public async Task Stamp_Bytes_TextStampWithoutText_Throws()
{
byte[] pdf = LoadPdfBytes("valid.pdf");
var request = new AddStampBase64Request
{
Base64Pdf = string.Empty,
StampType = StampType.Text,
X = 100, Y = 400
// Text intentionally missing
};
var act = async () =>
{
await using var result = await OperationsClient.StampAsync(pdf, request);
};
await act.Should().ThrowAsync<HttpRequestException>(
"Text stamp without text should return 400");
}
}

View File

@@ -0,0 +1,196 @@
using DocumentService.Application.Common.DTOs;
using FluentAssertions;
namespace DocumentService.Tests.Live.API;
/// <summary>
/// Live tests for PDF validation endpoints.
/// Uses PdfValidationClient — routes are managed by the client, not the test.
/// Target: http://172.24.12.39:9393/
/// </summary>
[Collection("Live API")]
public class PdfValidationLiveTests : LiveApiTestBase
{
// ──────────────────────────────────────────────────────────────
// ValidateAsync(Stream) → multipart/form-data → /api/pdf/validation/validate
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Validate_Stream_ValidPdf_ReturnsMetadata()
{
using var stream = LoadPdfStream("valid.pdf");
var result = await ValidationClient.ValidateAsync(stream);
result.Should().NotBeNull();
result.PageCount.Should().BeGreaterThan(0);
result.FileSizeBytes.Should().BeGreaterThan(0);
result.PdfVersion.Should().NotBeNullOrEmpty();
result.HasAttachments.Should().BeFalse();
result.AttachmentCount.Should().Be(0);
}
[Fact]
public async Task Validate_Stream_PdfWithAttachments_ReturnsHasAttachmentsTrue()
{
using var stream = LoadPdfStream("pdfWithMoreThanOneAttachment.pdf");
var result = await ValidationClient.ValidateAsync(stream);
result.HasAttachments.Should().BeTrue();
result.AttachmentCount.Should().BeGreaterThan(0);
}
[Fact]
public async Task Validate_Stream_PdfWithSwissQrCode_ReturnsValidResult()
{
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
var result = await ValidationClient.ValidateAsync(stream);
result.Should().NotBeNull();
result.PageCount.Should().BeGreaterThan(0);
result.FileSizeBytes.Should().BeGreaterThan(0);
}
[Fact]
public async Task Validate_Stream_EmptyStream_Throws()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var act = async () => await ValidationClient.ValidateAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>(
"empty file should be rejected with 400");
}
[Fact]
public async Task Validate_Stream_CorruptedContent_Throws()
{
// Valid stream shape, but not a PDF
byte[] garbage = "THIS IS NOT A PDF FILE"u8.ToArray();
using var stream = new MemoryStream(garbage);
var act = async () => await ValidationClient.ValidateAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>(
"corrupted content should be rejected (500)");
}
// ──────────────────────────────────────────────────────────────
// ValidateAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Validate_Bytes_ValidPdf_ReturnsMetadata()
{
byte[] bytes = LoadPdfBytes("valid.pdf");
var result = await ValidationClient.ValidateAsync(bytes);
result.Should().NotBeNull();
result.PageCount.Should().BeGreaterThan(0);
result.FileSizeBytes.Should().BeGreaterThan(0);
result.PdfVersion.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task Validate_Bytes_PdfWithAttachments_ReturnsHasAttachmentsTrue()
{
byte[] bytes = LoadPdfBytes("pdfWithMoreThanOneAttachment.pdf");
var result = await ValidationClient.ValidateAsync(bytes);
result.HasAttachments.Should().BeTrue();
result.AttachmentCount.Should().BeGreaterThan(0);
}
[Fact]
public async Task Validate_Bytes_EmptyArray_Throws()
{
var act = async () => await ValidationClient.ValidateAsync(Array.Empty<byte>());
await act.Should().ThrowAsync<HttpRequestException>(
"empty byte array encodes to empty Base64, which should be rejected");
}
// ──────────────────────────────────────────────────────────────
// ValidatePdfAAsync(Stream) → multipart → /api/pdf/validation/validate-pdfa
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task ValidatePdfA_Stream_ValidPdf_ReturnsComplianceFields()
{
using var stream = LoadPdfStream("valid.pdf");
var result = await ValidationClient.ValidatePdfAAsync(stream);
result.Should().NotBeNull();
result.IsValid.Should().BeTrue();
result.PageCount.Should().BeGreaterThan(0);
result.PdfVersion.Should().NotBeNullOrEmpty();
result.FileSize.Should().BeGreaterThan(0);
result.Encrypted.Should().BeFalse();
// valid.pdf is a regular PDF, not PDF/A
result.PdfACompliant.Should().BeFalse();
result.PdfAVersion.Should().BeNullOrEmpty();
}
[Fact]
public async Task ValidatePdfA_Stream_EncryptedPdf_ReturnsEncryptedTrue_NotCompliant()
{
using var stream = LoadPdfStream("encryptedPdf.pdf");
var result = await ValidationClient.ValidatePdfAAsync(stream);
result.Should().NotBeNull();
result.Encrypted.Should().BeTrue();
result.PdfACompliant.Should().BeFalse("encrypted PDFs cannot be PDF/A compliant");
}
[Fact]
public async Task ValidatePdfA_Stream_EmptyStream_Throws()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var act = async () => await ValidationClient.ValidatePdfAAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>();
}
// ──────────────────────────────────────────────────────────────
// ValidatePdfAAsync(byte[]) → Base64 JSON → /api/pdf/validation/validate-pdfa
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task ValidatePdfA_Bytes_ValidPdf_ReturnsComplianceFields()
{
byte[] bytes = LoadPdfBytes("valid.pdf");
var result = await ValidationClient.ValidatePdfAAsync(bytes);
result.Should().NotBeNull();
result.IsValid.Should().BeTrue();
result.PdfACompliant.Should().BeFalse();
result.PageCount.Should().BeGreaterThan(0);
}
[Fact]
public async Task ValidatePdfA_Bytes_EncryptedPdf_ReturnsEncryptedTrue()
{
byte[] bytes = LoadPdfBytes("encryptedPdf.pdf");
var result = await ValidationClient.ValidatePdfAAsync(bytes);
result.Encrypted.Should().BeTrue();
result.PdfACompliant.Should().BeFalse();
}
[Fact]
public async Task ValidatePdfA_Bytes_EmptyArray_Throws()
{
var act = async () => await ValidationClient.ValidatePdfAAsync(Array.Empty<byte>());
await act.Should().ThrowAsync<HttpRequestException>();
}
}

View File

@@ -0,0 +1,129 @@
using DocumentService.Application.Common.DTOs;
using FluentAssertions;
namespace DocumentService.Tests.Live.API;
/// <summary>
/// Live tests for Swiss QR Code extraction endpoint.
/// Uses SwissQrCodeClient — routes managed by the client.
/// Target: http://172.24.12.39:9393/
/// </summary>
[Collection("Live API")]
public class SwissQrCodeLiveTests : LiveApiTestBase
{
// ──────────────────────────────────────────────────────────────
// ExtractAsync(Stream) → multipart → /api/pdf/qr-code/extract-swiss
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Extract_Stream_PdfWithQrCode_ReturnsBillDetails()
{
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
var result = await QrCodeClient.ExtractAsync(stream);
result.Should().NotBeNull();
result.Bill.Should().NotBeNull("bill data must be extracted");
result.RawLines.Should().NotBeEmpty("raw QR payload lines must be present");
// Swiss QR Bill Standard 2.0 payload always starts with "SPC"
result.RawLines.Should().Contain(line => line == "SPC",
"first SPC line is mandated by Swiss QR Bill Standard 2.0");
}
[Fact]
public async Task Extract_Stream_PdfWithQrCode_BillHasMandatoryFields()
{
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
var result = await QrCodeClient.ExtractAsync(stream);
var bill = result.Bill;
bill.Should().NotBeNull();
bill!.Account.Should().NotBeNullOrEmpty("IBAN/QR-IBAN is mandatory per standard");
bill.Creditor.Should().NotBeNull("Creditor is mandatory");
bill.Creditor!.Name.Should().NotBeNullOrEmpty("Creditor name is mandatory");
}
[Fact]
public async Task Extract_Stream_PdfWithoutQrCode_Throws404()
{
// valid.pdf has no QR code
using var stream = LoadPdfStream("valid.pdf");
var act = async () => await QrCodeClient.ExtractAsync(stream);
// Client calls EnsureSuccessStatusCode → HttpRequestException on 404
await act.Should().ThrowAsync<HttpRequestException>(
"PDF without QR code must return 404");
}
[Fact]
public async Task Extract_Stream_EmptyStream_Throws()
{
using var stream = new MemoryStream(Array.Empty<byte>());
var act = async () => await QrCodeClient.ExtractAsync(stream);
await act.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task Extract_Stream_WithRawFlag_ReturnsRawLines()
{
using var stream = LoadPdfStream("pdfWithSwissQRCode.pdf");
// raw=true → API returns full unprocessed payload lines
var result = await QrCodeClient.ExtractAsync(stream, raw: true);
result.Should().NotBeNull();
result.RawLines.Should().NotBeEmpty();
}
// ──────────────────────────────────────────────────────────────
// ExtractAsync(byte[]) → Base64 JSON → /api/pdf/qr-code/extract-swiss
// ──────────────────────────────────────────────────────────────
[Fact]
public async Task Extract_Bytes_PdfWithQrCode_ReturnsBillDetails()
{
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
var result = await QrCodeClient.ExtractAsync(bytes);
result.Should().NotBeNull();
result.Bill.Should().NotBeNull();
result.RawLines.Should().NotBeEmpty();
}
[Fact]
public async Task Extract_Bytes_PdfWithQrCode_BillHasMandatoryFields()
{
byte[] bytes = LoadPdfBytes("pdfWithSwissQRCode.pdf");
var result = await QrCodeClient.ExtractAsync(bytes);
result.Bill!.Account.Should().NotBeNullOrEmpty();
result.Bill.Creditor.Should().NotBeNull();
result.Bill.Creditor!.Name.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task Extract_Bytes_PdfWithoutQrCode_Throws404()
{
byte[] bytes = LoadPdfBytes("valid.pdf");
var act = async () => await QrCodeClient.ExtractAsync(bytes);
await act.Should().ThrowAsync<HttpRequestException>(
"PDF without QR code must return 404");
}
[Fact]
public async Task Extract_Bytes_EmptyArray_Throws()
{
var act = async () => await QrCodeClient.ExtractAsync(Array.Empty<byte>());
await act.Should().ThrowAsync<HttpRequestException>();
}
}

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<None Remove="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
<None Remove="TestData\Pdfs\valid.pdf" />
<None Remove="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
<None Remove="TestData\Pdfs\encryptedPdf.pdf" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestData\Pdfs\pdfWithSwissQRCode.pdf" />
<EmbeddedResource Include="TestData\Pdfs\valid.pdf" />
<EmbeddedResource Include="TestData\Pdfs\pdfWithMoreThanOneAttachment.pdf" />
<EmbeddedResource Include="TestData\Pdfs\encryptedPdf.pdf" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,64 @@
using DocumentService.Client.Clients;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
namespace DocumentService.Tests.Live;
/// <summary>
/// Shared base for all live API tests.
/// Uses the typed DocumentService.Client classes — no hardcoded routes.
///
/// Live API: http://172.24.12.39:9393/
/// Override via LIVE_API_BASE_URL environment variable if needed.
/// </summary>
public abstract class LiveApiTestBase : IDisposable
{
public static readonly string BaseUrl =
Environment.GetEnvironmentVariable("LIVE_API_BASE_URL")
?? "http://172.24.12.39:9393";
protected readonly HttpClient HttpClient;
// Typed clients (route-free — routes live inside the client implementations)
protected readonly PdfValidationClient ValidationClient;
protected readonly PdfAttachmentClient AttachmentClient;
protected readonly PdfOperationsClient OperationsClient;
protected readonly SwissQrCodeClient QrCodeClient;
protected LiveApiTestBase()
{
HttpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) };
ValidationClient = new PdfValidationClient(HttpClient, NullLogger<PdfValidationClient>.Instance);
AttachmentClient = new PdfAttachmentClient(HttpClient, NullLogger<PdfAttachmentClient>.Instance);
OperationsClient = new PdfOperationsClient(HttpClient, NullLogger<PdfOperationsClient>.Instance);
QrCodeClient = new SwissQrCodeClient(HttpClient, NullLogger<SwissQrCodeClient>.Instance);
}
// ──────────────────────────────────────────────────────────────
// Embedded-resource helpers
// ──────────────────────────────────────────────────────────────
protected static byte[] LoadPdfBytes(string fileName)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"DocumentService.Tests.Live.TestData.Pdfs.{fileName}";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new FileNotFoundException(
$"Embedded resource not found: '{resourceName}'. " +
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
protected static Stream LoadPdfStream(string fileName)
{
var bytes = LoadPdfBytes(fileName);
return new MemoryStream(bytes);
}
public void Dispose() => HttpClient.Dispose();
}

View File

@@ -0,0 +1,30 @@
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
4 0 obj
<< /Filter /Standard /V 2 /R 3 /Length 128 /P -3904 /O (OwnerHash12345678901234567890123456) /U (UserHash12345678901234567890123456) >>
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000206 00000 n
trailer
<< /Size 5 /Root 1 0 R /Encrypt 4 0 R >>
startxref
370
%%EOF

Binary file not shown.

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="valid.pdf" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DocumentService.Client" Version="1.2.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
using DocumentService.Client;
// 1. Configure the client
Client.Configure("http://172.24.12.39:9393/");
// 2. Load embedded PDF resource
var assembly = typeof(Program).Assembly;
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("valid.pdf", StringComparison.OrdinalIgnoreCase));
if (resourceName is null)
{
Console.WriteLine("❌ Embedded resource 'valid.pdf' not found.");
return;
}
try
{
using var stream = assembly.GetManifestResourceStream(resourceName)!;
var result = await Client.Validation.ValidateAsync(stream);
Console.WriteLine("✅ Connection successful!");
Console.WriteLine($" Page count : {result.PageCount}");
Console.WriteLine($" PDF version : {result.PdfVersion}");
Console.WriteLine($" Encrypted : {result.IsEncrypted}");
Console.WriteLine($" Attachments : {result.AttachmentCount}");
Console.WriteLine($" File size : {result.FileSizeMB:F2} MB");
}
catch (HttpRequestException ex)
{
Console.WriteLine("❌ HTTP error: " + ex.Message);
}
catch (InvalidOperationException ex)
{
Console.WriteLine("❌ Client error: " + ex.Message);
}

Binary file not shown.

View File

@@ -40,36 +40,116 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Domain", "D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentService.Tests\DocumentService.Tests.csproj", "{ECD5BC60-7009-4099-7193-526FDC6E8D99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.Live", "DocumentService.Tests.Live\DocumentService.Tests.Live.csproj", "{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests.NuGet", "DocumentService.Tests.NuGet\DocumentService.Tests.NuGet.csproj", "{EE3E7C50-E044-4F77-BB19-74286DDA43F3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.ActiveCfg = Debug|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x64.Build.0 = Debug|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.ActiveCfg = Debug|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|x86.Build.0 = Debug|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.ActiveCfg = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x64.Build.0 = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.ActiveCfg = Release|Any CPU
{626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|x86.Build.0 = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.ActiveCfg = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x64.Build.0 = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.ActiveCfg = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|x86.Build.0 = Debug|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|Any CPU.Build.0 = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.ActiveCfg = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x64.Build.0 = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.ActiveCfg = Release|Any CPU
{3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Release|x86.Build.0 = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.ActiveCfg = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x64.Build.0 = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.ActiveCfg = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Debug|x86.Build.0 = Debug|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|Any CPU.Build.0 = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.ActiveCfg = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x64.Build.0 = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.ActiveCfg = Release|Any CPU
{A5AB3D2B-D4A8-1BE2-CACE-B5DBF9A40E51}.Release|x86.Build.0 = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.ActiveCfg = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x64.Build.0 = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.ActiveCfg = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Debug|x86.Build.0 = Debug|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|Any CPU.Build.0 = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.ActiveCfg = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x64.Build.0 = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.ActiveCfg = Release|Any CPU
{459EDCC5-240E-A2BC-1330-B5B2326B679C}.Release|x86.Build.0 = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.ActiveCfg = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x64.Build.0 = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.ActiveCfg = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Debug|x86.Build.0 = Debug|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|Any CPU.Build.0 = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.ActiveCfg = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x64.Build.0 = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.ActiveCfg = Release|Any CPU
{2F2C937F-3732-4E6C-9720-FAB62DC33C63}.Release|x86.Build.0 = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.ActiveCfg = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x64.Build.0 = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.ActiveCfg = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Debug|x86.Build.0 = Debug|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|Any CPU.Build.0 = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.ActiveCfg = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x64.Build.0 = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.ActiveCfg = Release|Any CPU
{ECD5BC60-7009-4099-7193-526FDC6E8D99}.Release|x86.Build.0 = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.ActiveCfg = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x64.Build.0 = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.ActiveCfg = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Debug|x86.Build.0 = Debug|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|Any CPU.Build.0 = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.ActiveCfg = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x64.Build.0 = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.ActiveCfg = Release|Any CPU
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487}.Release|x86.Build.0 = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.ActiveCfg = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x64.Build.0 = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.ActiveCfg = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Debug|x86.Build.0 = Debug|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|Any CPU.Build.0 = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.ActiveCfg = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x64.Build.0 = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.ActiveCfg = Release|Any CPU
{EE3E7C50-E044-4F77-BB19-74286DDA43F3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -85,6 +165,8 @@ Global
{459EDCC5-240E-A2BC-1330-B5B2326B679C} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
{2F2C937F-3732-4E6C-9720-FAB62DC33C63} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
{ECD5BC60-7009-4099-7193-526FDC6E8D99} = {D64772B8-6213-4031-8064-2567F1E724F6}
{CC5A7D7B-E998-44B2-98CB-B2CF53D4A487} = {D64772B8-6213-4031-8064-2567F1E724F6}
{EE3E7C50-E044-4F77-BB19-74286DDA43F3} = {D64772B8-6213-4031-8064-2567F1E724F6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}

0
Program.cs Normal file
View File