Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0a8e8e5
Add SAS URL generation for Azure Blob Storage
Sep 15, 2025
bccc0eb
Replace integration test workflow with a scheduled unit test check fo…
Sep 15, 2025
8868fa5
Add workflow_dispatch trigger to allow manual execution of integratio…
Sep 15, 2025
5f1a61d
Update environment name in integration test workflow to MMI-Samples-D…
Sep 16, 2025
749decf
Update environment name in integration test workflow to MMI-Samples
Sep 16, 2025
1954c4e
Add debug step to output environment variables in integration test wo…
Sep 16, 2025
61c0890
Remove debug steps for environment variables and setting paths in int…
Sep 16, 2025
41c1f33
Add debug step to check presence of AZURE_CONTENT_UNDERSTANDING_ENDPO…
Sep 16, 2025
051d2c9
Add debug step to check AZURE_CONTENT_UNDERSTANDING_ENDPOINT host only
Sep 16, 2025
8cff142
Remove debug steps for AZURE_CONTENT_UNDERSTANDING_ENDPOINT from inte…
Sep 17, 2025
16e47b0
Add step to check presence of AZURE environment variables in workflow
Sep 17, 2025
b65a8a6
Add debug steps to check visibility of AZURE environment variables in…
Sep 17, 2025
e716d01
Refactor configuration handling to use AZURE_CONTENT_UNDERSTANDING_EN…
Sep 17, 2025
924b9a8
Add AZURE_SUBSCRIPTION_ID configuration to various integration tests
Sep 17, 2025
2b76d66
Refactor integration tests to retrieve AZURE_SUBSCRIPTION_ID from env…
Sep 17, 2025
2f9a0b8
Add debug logging for subscription key usage in AzureContentUnderstan…
Sep 17, 2025
c6d767d
Update subscription key references to use AZURE_CONTENT_UNDERSTANDING…
Sep 19, 2025
b8b67b5
Refactor AnalyzerTrainingIntegrationTest to define training data path…
Sep 19, 2025
62f9c76
Refactor AnalyzerTrainingIntegrationTest to streamline training data …
Sep 19, 2025
a14be3a
Comment out Fact and Trait attributes in BuildPersonDirectoryIntegrat…
Sep 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/Run Integration Test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Unit Test Check

on:
schedule:
- cron: '0 8 * * *' # Every day at 08:00 UTC
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch: # Allows manual run from GitHub UI

permissions:
id-token: write
contents: read

jobs:
test-apis:
runs-on: ubuntu-latest
environment: MMI-Samples
env:
AZURE_CONTENT_UNDERSTANDING_ENDPOINT: ${{ secrets.AZURE_CONTENT_UNDERSTANDING_ENDPOINT }}
TRAINING_DATA_STORAGE_ACCOUNT_NAME: ${{ secrets.TRAINING_DATA_STORAGE_ACCOUNT_NAME }}
TRAINING_DATA_CONTAINER_NAME: ${{ secrets.TRAINING_DATA_CONTAINER_NAME }}
REFERENCE_DOC_STORAGE_ACCOUNT_NAME: ${{ secrets.REFERENCE_DOC_STORAGE_ACCOUNT_NAME }}
REFERENCE_DOC_CONTAINER_NAME: ${{ secrets.REFERENCE_DOC_CONTAINER_NAME }}

steps:
- name: Checkout Repo
uses: actions/checkout@v3

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Pull LFS files
run: git lfs pull

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Run Tests
run: dotnet test --no-build --verbosity normal
45 changes: 0 additions & 45 deletions .github/workflows/run_integration_test.yml

This file was deleted.

21 changes: 20 additions & 1 deletion AnalyzerTraining/Interfaces/IAnalyzerTrainingService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
using System.Text.Json;
using Azure.Storage.Sas;
using System.Text.Json;

namespace AnalyzerTraining.Interfaces
{
public interface IAnalyzerTrainingService
{
/// <summary>
/// Get a Shared Access Signature (SAS) URL for a specified Azure Blob Storage container.
/// </summary>
/// <remarks>The generated SAS URL grants access to the specified container with the specified
/// permissions for the specified duration. The method uses the Azure Identity library to authenticate and
/// generate the SAS token.</remarks>
/// <param name="accountName">The name of the Azure Storage account. This value cannot be null, empty, or whitespace.</param>
/// <param name="containerName">The name of the blob container for which the SAS URL is generated. This value cannot be null, empty, or
/// whitespace.</param>
/// <param name="permissions">The permissions to include in the SAS token. If not specified, the default permissions are <see
/// cref="BlobContainerSasPermissions.Read"/>, <see cref="BlobContainerSasPermissions.Write"/>, and <see
/// cref="BlobContainerSasPermissions.List"/>.</param>
/// <param name="expiryHours">The number of hours until the SAS token expires. The default value is 1 hour. Must be a positive integer.</param>
/// <returns>A string containing the SAS URL for the specified container, including the SAS token.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="accountName"/> or <paramref name="containerName"/> is null, empty, or consists
/// only of whitespace.</exception>
Task<string> GetTrainingContainerSasUrlAsync(string accountName, string containerName, BlobContainerSasPermissions? permissions = null, int expiryHours = 1);

/// <summary>
/// Uploads training data files, including labels and OCR results, from a local folder to a specified Azure Blob
/// Storage container.
Expand Down
11 changes: 8 additions & 3 deletions AnalyzerTraining/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ public static async Task Main(string[] args)
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
if (string.IsNullOrWhiteSpace(context.Configuration.GetValue<string>("AZURE_CU_CONFIG:Endpoint")))
string endpoint = context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_ENDPOINT") ?? string.Empty;
if (string.IsNullOrWhiteSpace(endpoint))
{
throw new ArgumentException("Endpoint must be provided in appsettings.json.");
}

if (string.IsNullOrWhiteSpace(context.Configuration.GetValue<string>("AZURE_CU_CONFIG:ApiVersion")))
string apiVersion = context.Configuration.GetValue<string>("AZURE_APIVERSION") ?? string.Empty;
if (string.IsNullOrWhiteSpace(apiVersion))
{
throw new ArgumentException("API version must be provided in appsettings.json.");
}

services.AddConfigurations(opts =>
{
context.Configuration.GetSection("AZURE_CU_CONFIG").Bind(opts);
opts.Endpoint = endpoint;
opts.ApiVersion = apiVersion;
opts.SubscriptionKey = context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_KEY") ?? string.Empty;

// This header is used for sample usage telemetry, please comment out this line if you want to opt out.
opts.UserAgent = "azure-ai-content-understanding-dotnet/analyzer_training";
});
Expand Down
26 changes: 26 additions & 0 deletions AnalyzerTraining/Services/AnalyzerTrainingService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using AnalyzerTraining.Interfaces;
using Azure.Storage.Blobs;
using Azure.Storage.Sas;
using ContentUnderstanding.Common;
using System.Text.Json;
using System.Threading.Tasks;

namespace AnalyzerTraining.Services
{
Expand All @@ -20,6 +22,30 @@ public AnalyzerTrainingService(AzureContentUnderstandingClient client)
}
}

/// <summary>
/// Get a Shared Access Signature (SAS) URL for a specified Azure Blob Storage container.
/// </summary>
/// <remarks>The generated SAS URL grants access to the specified container with the specified
/// permissions for the specified duration. The method uses the Azure Identity library to authenticate and
/// generate the SAS token.</remarks>
/// <param name="accountName">The name of the Azure Storage account. This value cannot be null, empty, or whitespace.</param>
/// <param name="containerName">The name of the blob container for which the SAS URL is generated. This value cannot be null, empty, or
/// whitespace.</param>
/// <param name="permissions">The permissions to include in the SAS token. If not specified, the default permissions are <see
/// cref="BlobContainerSasPermissions.Read"/>, <see cref="BlobContainerSasPermissions.Write"/>, and <see
/// cref="BlobContainerSasPermissions.List"/>.</param>
/// <param name="expiryHours">The number of hours until the SAS token expires. The default value is 1 hour. Must be a positive integer.</param>
/// <returns>A string containing the SAS URL for the specified container, including the SAS token.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="accountName"/> or <paramref name="containerName"/> is null, empty, or consists
/// only of whitespace.</exception>
public async Task<string> GetTrainingContainerSasUrlAsync(string accountName,
string containerName,
BlobContainerSasPermissions? permissions = null,
int expiryHours = 1)
{
return await _client.GenerateContainerSasUrlAsync(accountName, containerName, permissions, expiryHours);
}

/// <summary>
/// Uploads training data files, including labels and OCR results, from a local folder to a specified Azure Blob
/// Storage container.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,32 @@
using System.Text.Json;

namespace AzureAiContentUnderstanding.Tests
{
{
public class AnalyzerTrainingIntegrationTest
{
private readonly IAnalyzerTrainingService service;
// SAS URL for uploading training data to Azure Blob Storage
// Replace with your SAS URL for actual usage
private string trainingDataSasUrl = "https://<your_storage_account_name>.blob.core.windows.net/<your_container_name>?<your_sas_token>";
// Local directory for generated training data (dynamically named for each test run)
private string trainingDataPath = $"test_training_data_dotnet_{DateTime.Now.ToString("yyyyMMddHHmmss")}/";
// Local folder containing source documents for training
private const string trainingDocsFolder = "./data/document_training";
// SAS URL for the Azure Blob Storage container to upload training data
private string accountName = "";
private string containerName = "";

/// <summary>
/// Initializes test dependencies and AnalyzerTrainingService via dependency injection.
/// </summary>
public AnalyzerTrainingIntegrationTest()
{
var host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
})
.ConfigureServices((context, services) =>
{
// Load configuration from environment variables or appsettings.json
string? endpoint = Environment.GetEnvironmentVariable("AZURE_CU_CONFIG_Endpoint") ?? context.Configuration.GetValue<string>("AZURE_CU_CONFIG:Endpoint");
string? endpoint = Environment.GetEnvironmentVariable("AZURE_CONTENT_UNDERSTANDING_ENDPOINT") ?? context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_ENDPOINT");

// API version for Azure Content Understanding service
string? apiVersion = Environment.GetEnvironmentVariable("AZURE_CU_CONFIG_ApiVersion") ?? context.Configuration.GetValue<string>("AZURE_CU_CONFIG:ApiVersion");
string? apiVersion = Environment.GetEnvironmentVariable("AZURE_APIVERSION") ?? context.Configuration.GetValue<string>("AZURE_APIVERSION");

if (string.IsNullOrWhiteSpace(endpoint))
{
Expand All @@ -45,10 +46,28 @@ public AnalyzerTrainingIntegrationTest()
throw new ArgumentException("API version must be provided in environment variable or appsettings.json.");
}

// account name
accountName = Environment.GetEnvironmentVariable("TRAINING_DATA_STORAGE_ACCOUNT_NAME") ?? context.Configuration.GetValue<string>("TRAINING_DATA_STORAGE_ACCOUNT_NAME") ?? "";

// container name
containerName = Environment.GetEnvironmentVariable("TRAINING_DATA_CONTAINER_NAME") ?? context.Configuration.GetValue<string>("TRAINING_DATA_CONTAINER_NAME") ?? "";

if (string.IsNullOrWhiteSpace(accountName))
{
throw new ArgumentException("Storage account name must be provided in environment variable or appsettings.json.");
}

if (string.IsNullOrWhiteSpace(containerName))
{
throw new ArgumentException("Storage container name must be provided in environment variable or appsettings.json.");
}

services.AddConfigurations(opts =>
{
opts.Endpoint = endpoint;
opts.ApiVersion = apiVersion;
opts.SubscriptionKey = Environment.GetEnvironmentVariable("AZURE_CONTENT_UNDERSTANDING_KEY") ?? context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_KEY") ?? "";

// This header is used for sample usage telemetry, please comment out this line if you want to opt out.
opts.UserAgent = "azure-ai-content-understanding-dotnet/analyzer_training";
});
Expand All @@ -59,8 +78,6 @@ public AnalyzerTrainingIntegrationTest()
.Build();

service = host.Services.GetService<IAnalyzerTrainingService>()!;
// Optionally override SAS URL from environment variable
trainingDataSasUrl = Environment.GetEnvironmentVariable("TRAINING_DATA_SAS_URL") ?? trainingDataSasUrl;
}

/// <summary>
Expand All @@ -78,34 +95,24 @@ public async Task RunAsync()
Exception? serviceException = null;
JsonDocument? resultJson = null;
var analyzerId = string.Empty;
// Local directory for generated training data (dynamically named for each test run)
string trainingDataPath = $"test_training_data_dotnet_{DateTime.Now.ToString("yyyyMMddHHmmss")}/";
// Local folder containing source documents for training
string trainingDocsFolder = "./data/document_training";

try
{ // Step 1: Generate training data and upload to blob storage
await service.GenerateTrainingDataOnBlobAsync(trainingDocsFolder, trainingDataSasUrl, trainingDataPath);

// Step 2: Validate that all local files are uploaded to blob storage
var files = Directory.GetFiles(trainingDocsFolder, "*.*", SearchOption.AllDirectories).ToList().ToHashSet();
// check if the training data is uploaded to the blob storage
var blobClient = new BlobContainerClient(new Uri(trainingDataSasUrl));
var blobFiles = new HashSet<string>();
await foreach (BlobItem blobItem in blobClient.GetBlobsAsync(prefix: trainingDataPath))
{
var name = blobItem.Name.Substring(trainingDataPath.Length);
if (!string.IsNullOrEmpty(name) && !name.EndsWith("/"))
{
blobFiles.Add(name);
}
}
{
// Construct the SAS URL for the blob storage container
var trainingDataSasUrl = await service.GetTrainingContainerSasUrlAsync(accountName, containerName);

var fileNames = files.Select(f => Path.GetRelativePath(trainingDocsFolder, f)).ToHashSet();
// Assert: All local files are present in Blob
Assert.True(JsonSerializer.Serialize(fileNames) == JsonSerializer.Serialize(blobFiles), "Mismatch between local training data and uploaded blob files");
// Step 1: Generate training data and upload to blob storage
await service.GenerateTrainingDataOnBlobAsync(trainingDocsFolder, trainingDataSasUrl, trainingDataPath);

// Step 3: Create custom analyzer using training data and template
// Step 2: Create custom analyzer using training data and template
var analyzerTemplatePath = "./analyzer_templates/receipt.json";
analyzerId = await service.CreateAnalyzerAsync(analyzerTemplatePath, trainingDataSasUrl, trainingDataPath);

// Step 4: Analyze sample document with custom analyzer and verify output
// Step 3: Analyze sample document with custom analyzer and verify output
var customAnalyzerSampleFilePath = "./data/receipt.png";
resultJson = await service.AnalyzeDocumentWithCustomAnalyzerAsync(analyzerId, customAnalyzerSampleFilePath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ public class BuildPersonDirectoryIntegrationTest
/// Sets up dependency injection, configures the test host, and validates required configurations.
/// </summary>
/// <exception cref="ArgumentException">
/// Thrown if required configuration values for AZURE_CU_CONFIG:Endpoint or AZURE_CU_CONFIG:ApiVersion are missing.
/// Thrown if required configuration values for AZURE_CONTENT_UNDERSTANDING_ENDPOINT or AZURE_APIVERSION are missing.
/// </exception>
public BuildPersonDirectoryIntegrationTest()
{
var host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
})
.ConfigureServices((context, services) =>
{
// Load configuration from environment variables or appsettings.json
string? endpoint = Environment.GetEnvironmentVariable("AZURE_CU_CONFIG_Endpoint") ?? context.Configuration.GetValue<string>("AZURE_CU_CONFIG:Endpoint");
string? endpoint = Environment.GetEnvironmentVariable("AZURE_CONTENT_UNDERSTANDING_ENDPOINT") ?? context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_ENDPOINT");

// API version for Azure Content Understanding service
string? apiVersion = Environment.GetEnvironmentVariable("AZURE_CU_CONFIG_ApiVersion") ?? context.Configuration.GetValue<string>("AZURE_CU_CONFIG:ApiVersion");
string? apiVersion = Environment.GetEnvironmentVariable("AZURE_APIVERSION") ?? context.Configuration.GetValue<string>("AZURE_APIVERSION");

if (string.IsNullOrWhiteSpace(endpoint))
{
Expand All @@ -50,6 +55,8 @@ public BuildPersonDirectoryIntegrationTest()
{
opts.Endpoint = endpoint;
opts.ApiVersion = apiVersion;
opts.SubscriptionKey = Environment.GetEnvironmentVariable("AZURE_CONTENT_UNDERSTANDING_KEY") ?? context.Configuration.GetValue<string>("AZURE_CONTENT_UNDERSTANDING_KEY") ?? "";

// This header is used for sample usage telemetry, please comment out this line if you want to opt out.
opts.UserAgent = "azure-ai-content-understanding-dotnet/build_person_directory";
});
Expand All @@ -67,8 +74,8 @@ public BuildPersonDirectoryIntegrationTest()
/// Covers scenarios: directory creation, enrollment, identification, face management, metadata update, and cleanup.
/// Asserts success and validity at each step. Any unexpected exception is captured and asserted as null.
/// </summary>
[Fact(DisplayName = "Build Person Directory Integration Test")]
[Trait("Category", "Integration")]
// [Fact(DisplayName = "Build Person Directory Integration Test")]
// [Trait("Category", "Integration")]
public async Task RunAsync()
{
Exception? serviceException = null;
Expand Down
Loading