diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79f8582..5218e55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,13 +19,36 @@ jobs: - ubuntu-latest arch: - x64 + + services: + azurite: + image: mcr.microsoft.com/azure-storage/azurite:3.35.0 + ports: + - 10000:10000 # Blob service + - 10001:10001 # Queue service + - 10002:10002 # Table service + options: >- + --health-cmd "nc -z 127.0.0.1 10000" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + + env: + AZURE_TEST_MODE: local + AZURE_STORAGE_CONNECTION_STRING: UseDevelopmentStorage=true + AZURITE_BLOB_ENDPOINT: http://127.0.0.1:10000/devstoreaccount1 + AZURITE_QUEUE_ENDPOINT: http://127.0.0.1:10001/devstoreaccount1 + AZURITE_TABLE_ENDPOINT: http://127.0.0.1:10002/devstoreaccount1 + AZURITE_ACCOUNT_NAME: devstoreaccount1 + AZURITE_ACCOUNT_KEY: Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v3 env: cache-name: cache-artifacts with: @@ -35,9 +58,16 @@ jobs: ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- + + # Wait for Azurite to be ready + - name: Wait for Azurite + run: | + timeout 30 bash -c 'until nc -z 127.0.0.1 10000; do sleep 1; done' + echo "Azurite is ready" + - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: file: lcov.info diff --git a/.gitignore b/.gitignore index b0b5752..ffec272 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.jl.*.cov *.jl.mem openapi-generator-cli.jar +Manifest.toml \ No newline at end of file diff --git a/Project.toml b/Project.toml index 6c51ff6..2036a9d 100644 --- a/Project.toml +++ b/Project.toml @@ -11,24 +11,26 @@ Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" Mmap = "a63ad114-7e13-5084-954f-fe012c677804" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" -XMLDict = "228000da-037f-5747-90a9-8195ccbf91a5" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +XMLDict = "228000da-037f-5747-90a9-8195ccbf91a5" [compat] Downloads = "1" -OpenAPI = "0.1" -XMLDict = "0.3,0.4" JSON = "0.21" MbedTLS = "0.6.8, 0.7, 1" +OpenAPI = "0.1,0.2" TimeZones = "1" URIs = "1.3, 1.4" +XMLDict = "0.3,0.4" julia = "1.6" [extras] +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test", "HTTP", "JSON3"] diff --git a/src/StorageServices/common.jl b/src/StorageServices/common.jl index 20770e8..e9182f4 100644 --- a/src/StorageServices/common.jl +++ b/src/StorageServices/common.jl @@ -27,7 +27,7 @@ function extract_account_and_key(ctx, subscription_id::String, resource_group_na account = String(split(split(uri, "/")[3], '.'; limit=2)[1]) # get keys for it - allkeys = storageAccountsListKeys(Azure.api(ctx, StorageAccountsApi), resource_group_name, account, apiver(StorageAccountsApi), subscription_id) + allkeys = storageAccountsListKeys(Azure.api(ctx, StorageManagementClient.StorageAccountsApi), resource_group_name, account, apiver(StorageManagementClient.StorageAccountsApi), subscription_id) key = (allkeys.keys[1]).value AccountKey(account, key) diff --git a/test/.env.example b/test/.env.example new file mode 100644 index 0000000..6f6d775 --- /dev/null +++ b/test/.env.example @@ -0,0 +1,26 @@ +# Azure.jl Environment Configuration + +# Testing Mode: "local" for Azurite emulator, "azure" for real Azure services +AZURE_TEST_MODE=local + +# Local Development with Azurite +AZURE_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true +AZURITE_BLOB_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 +AZURITE_QUEUE_ENDPOINT=http://127.0.0.1:10001/devstoreaccount1 +AZURITE_TABLE_ENDPOINT=http://127.0.0.1:10002/devstoreaccount1 +# Note: Azurite does not support File Storage service +AZURITE_ACCOUNT_NAME=devstoreaccount1 +AZURITE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + +# Real Azure Services (when AZURE_TEST_MODE=azure) +# AZURE_TENANT_ID=your-tenant-id +# AZURE_CLIENT_ID=your-client-id +# AZURE_CLIENT_SECRET=your-client-secret +# AZURE_SUBSCRIPTION_ID=your-subscription-id +# AZURE_RESOURCE_GROUP=your-resource-group +# AZURE_STORAGE_ACCOUNT=your-storage-account +# AZURE_STORAGE_KEY=your-storage-key + +# HTTP Mocking (for management API testing) +AZURE_MOCK_SERVER=false +AZURE_MOCK_PORT=8080 \ No newline at end of file diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +.env diff --git a/test/MockAzure.jl b/test/MockAzure.jl new file mode 100644 index 0000000..9b79cfe --- /dev/null +++ b/test/MockAzure.jl @@ -0,0 +1,278 @@ +""" +MockAzure.jl - HTTP mocking framework for Azure.jl + +Provides mocking capabilities for Azure management APIs during testing. +Inspired by MockAWS.jl but designed specifically for Azure services. +""" +module MockAzure + +using HTTP +using JSON3 +using URIs + +export MockAzureServer, start_mock_server, stop_mock_server, add_mock_response, @azure_mock + +""" +Simple HTTP mock server for Azure API responses. +""" +mutable struct MockAzureServer + server::Union{HTTP.Server, Nothing} + port::Int + responses::Dict{String, Any} + running::Bool + + function MockAzureServer(port::Int = 8080) + new(nothing, port, Dict{String, Any}(), false) + end +end + +""" +Start the mock server on the specified port. +""" +function start_mock_server(mock_server::MockAzureServer) + if mock_server.running + @warn "Mock server already running on port $(mock_server.port)" + return + end + + # Create HTTP handler + function handler(req::HTTP.Request) + method = String(req.method) + uri = String(req.target) + + # Create a key for matching requests + key = "$(method) $(uri)" + + @info "Mock request received" method=method uri=uri key=key + + if haskey(mock_server.responses, key) + response_data = mock_server.responses[key] + + # Return mock response + status = get(response_data, "status", 200) + headers = get(response_data, "headers", ["Content-Type" => "application/json"]) + body = get(response_data, "body", "{}") + + if isa(body, Dict) || isa(body, Vector) + body = JSON3.write(body) + end + + @info "Mock response sent" status=status + return HTTP.Response(status, headers, body) + else + # Default 404 response for unmocked endpoints + @warn "No mock response found for: $key" + return HTTP.Response(404, ["Content-Type" => "application/json"], + JSON3.write(Dict("error" => "Mock not found", "request" => key))) + end + end + + # Start server + mock_server.server = HTTP.serve!(handler, "127.0.0.1", mock_server.port; verbose=false) + mock_server.running = true + + @info "Mock Azure server started" port=mock_server.port +end + +""" +Stop the mock server. +""" +function stop_mock_server(mock_server::MockAzureServer) + if mock_server.running && mock_server.server !== nothing + close(mock_server.server) + mock_server.server = nothing + mock_server.running = false + @info "Mock Azure server stopped" + end +end + +""" +Add a mock response for a specific HTTP method and URI pattern. + +# Arguments +- `mock_server::MockAzureServer`: The mock server instance +- `method::String`: HTTP method (GET, POST, PUT, DELETE, etc.) +- `uri_pattern::String`: URI pattern to match +- `response::Dict`: Response configuration with keys: status, headers, body + +# Example +```julia +add_mock_response(server, "GET", "/subscriptions/123/resourceGroups", + Dict("status" => 200, + "body" => Dict("value" => [Dict("name" => "test-rg")]))) +``` +""" +function add_mock_response(mock_server::MockAzureServer, method::String, uri_pattern::String, response::Dict) + key = "$(uppercase(method)) $(uri_pattern)" + mock_server.responses[key] = response + @info "Mock response added" key=key +end + +""" +Macro for convenient Azure API mocking setup. + +# Example +```julia +@azure_mock begin + GET "/subscriptions/123/resourceGroups" => Dict( + "status" => 200, + "body" => Dict("value" => [Dict("name" => "test-rg")]) + ) + + POST "/subscriptions/123/resourceGroups/test/providers/Microsoft.Storage/storageAccounts" => Dict( + "status" => 201, + "body" => Dict("name" => "teststorage", "location" => "eastus") + ) +end +``` +""" +macro azure_mock(block) + quote + server = MockAzureServer() + start_mock_server(server) + + try + $(esc(block)) + server + catch e + stop_mock_server(server) + rethrow(e) + end + end +end + +""" +Common Azure API mock responses for testing. +""" +module CommonMocks + +using JSON3 + +export resource_group_list, storage_account_list, virtual_machine_list, virtual_network_list, subscription_info, error_response + +function resource_group_list(subscription_id::String, resource_groups::Vector{String}) + return Dict( + "status" => 200, + "body" => Dict( + "value" => [ + Dict( + "id" => "/subscriptions/$subscription_id/resourceGroups/$rg", + "name" => rg, + "location" => "eastus", + "properties" => Dict("provisioningState" => "Succeeded") + ) for rg in resource_groups + ] + ) + ) +end + +function storage_account_list(subscription_id::String, resource_group::String, accounts::Vector{String}) + return Dict( + "status" => 200, + "body" => Dict( + "value" => [ + Dict( + "id" => "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Storage/storageAccounts/$acc", + "name" => acc, + "location" => "eastus", + "kind" => "StorageV2", + "properties" => Dict( + "provisioningState" => "Succeeded", + "primaryEndpoints" => Dict( + "blob" => "https://$acc.blob.core.windows.net/", + "queue" => "https://$acc.queue.core.windows.net/", + "table" => "https://$acc.table.core.windows.net/" + ) + ) + ) for acc in accounts + ] + ) + ) +end + +function virtual_machine_list(subscription_id::String, resource_group::String, vms::Vector{String}) + return Dict( + "status" => 200, + "body" => Dict( + "value" => [ + Dict( + "id" => "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Compute/virtualMachines/$vm", + "name" => vm, + "location" => "eastus", + "properties" => Dict( + "provisioningState" => "Succeeded", + "hardwareProfile" => Dict("vmSize" => "Standard_DS1_v2"), + "storageProfile" => Dict( + "osDisk" => Dict("osType" => "Linux") + ) + ) + ) for vm in vms + ] + ) + ) +end + +function virtual_network_list(subscription_id::String, resource_group::String, vnets::Vector{String}) + return Dict( + "status" => 200, + "body" => Dict( + "value" => [ + Dict( + "id" => "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Network/virtualNetworks/$vnet", + "name" => vnet, + "location" => "eastus", + "properties" => Dict( + "provisioningState" => "Succeeded", + "addressSpace" => Dict( + "addressPrefixes" => ["10.0.0.0/16"] + ), + "subnets" => [ + Dict( + "name" => "default", + "properties" => Dict( + "addressPrefix" => "10.0.0.0/24", + "provisioningState" => "Succeeded" + ) + ) + ] + ) + ) for vnet in vnets + ] + ) + ) +end + +function subscription_info(subscription_id::String, display_name::String = "Test Subscription") + return Dict( + "status" => 200, + "body" => Dict( + "id" => "/subscriptions/$subscription_id", + "subscriptionId" => subscription_id, + "displayName" => display_name, + "state" => "Enabled", + "subscriptionPolicies" => Dict( + "locationPlacementId" => "Internal_2014-09-01", + "quotaId" => "Internal_2014-09-01", + "spendingLimit" => "Off" + ), + "authorizationSource" => "RoleBased" + ) + ) +end + +function error_response(status_code::Int, error_code::String, message::String) + return Dict( + "status" => status_code, + "body" => Dict( + "error" => Dict( + "code" => error_code, + "message" => message, + "details" => [] + ) + ) + ) +end + +end # module CommonMocks + +end # module MockAzure \ No newline at end of file diff --git a/test/docker-compose.yml b/test/docker-compose.yml new file mode 100644 index 0000000..8bfd40c --- /dev/null +++ b/test/docker-compose.yml @@ -0,0 +1,23 @@ +services: + azurite: + image: mcr.microsoft.com/azure-storage/azurite:3.35.0 + container_name: azurite-storage + ports: + - "10000:10000" # Blob service + - "10001:10001" # Queue service + - "10002:10002" # Table service + volumes: + - azurite-data:/data + command: "azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --location /data --debug /data/debug.log" + environment: + - AZURITE_ACCOUNTS=devstoreaccount1:Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "10000"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + azurite-data: + driver: local \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c32274c..d098a2a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,35 @@ using Azure using Test -# on public CI, just testing whether package loads at the moment +include("test_blobs.jl") +include("test_files.jl") +include("test_sas.jl") +include("test_mock_azure.jl") + +@testset "Azure.jl Tests" begin + + @testset "Package Loading" begin + @test isa(Azure, Module) + @info "Azure.jl package loaded successfully" + end + + @testset "Local Blob Storage Tests" begin + @info "Running local blob storage tests" + TestBlobs.test_blobs() + end + + @testset "Azure Files Tests" begin + @info "Running Azure Files tests" + TestFiles.test_files() + end + + @testset "SAS Token Tests" begin + @info "Running SAS token tests" + TestSAS.test_sas() + end + + @testset "Mock Azure API Tests" begin + @info "Running mock Azure API tests" + TestMockAzure.example_usage() + end +end diff --git a/test/test_blobs.jl b/test/test_blobs.jl index e105a26..6acc59c 100644 --- a/test/test_blobs.jl +++ b/test/test_blobs.jl @@ -1,69 +1,154 @@ +module TestBlobs + using Azure using Azure.StorageServices using Test -function test_blobs(tenant_id, appid, password, subscription_id, resource_group_name, storage) - creds = AzureCredentials(tenant_id, appid, password) - ctx = AzureContext(creds) - - resource_group_name = "testresgroup" - testcontainer = storage * "testcontainer" - testblob1 = testcontainer * "/testblob1" - testblob2 = testcontainer * "/testblob2" - - @test createContainer(ctx, subscription_id, resource_group_name, testcontainer) - - result = listContainers(ctx, subscription_id, resource_group_name, storage) - @test haskey(result, "Containers") - @test haskey(result["Containers"], "Container") - containers = result["Containers"]["Container"] +# Include the test configuration module +include("test_config.jl") +using .TestConfig - container = containers - if isa(containers, Vector) - # iterate through multiple containers - for c in containers - if c["Name"] == "testcontainer" - container = c +""" +Test blob operations with support for both Azurite emulator and real Azure services. +Set AZURE_TEST_MODE=local for Azurite, AZURE_TEST_MODE=azure for real Azure. +""" +function test_blobs() + @testset "Blob Storage Tests (Local)" begin + + # Skip if Azurite is not available in local mode + if TestConfig.is_local_testing() + config = TestConfig.get_storage_config() + account_key = TestConfig.create_storage_account_key() + base_url = TestConfig.get_storage_base_url() + + # Test container and blob names + test_container_name = "testcontainer" * string(Int(floor(time()))) + test_container_url = base_url * "/" * test_container_name + test_blob1_url = test_container_url * "/testblob1" + test_blob2_url = test_container_url * "/testblob2" + + @info "Testing with Azurite emulator" base_url test_container_name + + try + # Create container + @test createContainer(nothing, "", "", test_container_url, account_key) + @info "Container created successfully" + + # List containers to verify creation + containers_result = listContainers(nothing, "", "", base_url, account_key) + @test haskey(containers_result, "Containers") + @info "Containers listed successfully" + + # Add a string as a blob + @test putBlob(nothing, "", "", test_blob1_url, "BlockBlob", account_key; + block_blob_contents="hello world") + @info "String blob uploaded successfully" + + # Streaming a file as a blob + mktemp() do path, wio + println(wio, "hello world from file") + close(wio) + open(path) do rio + @test putBlob(nothing, "", "", test_blob2_url, "BlockBlob", account_key; + block_blob_contents=rio, content_length=filesize(rio)) + end + end + @info "File blob uploaded successfully" + + # List blobs to verify upload + blobs_result = listBlobs(nothing, "", "", test_container_url, account_key) + @test haskey(blobs_result, "Blobs") + if haskey(blobs_result["Blobs"], "Blob") + blobs = blobs_result["Blobs"]["Blob"] + if isa(blobs, Vector) + @test length(blobs) >= 2 + blob_names = Set(b["Name"] for b in blobs) + @test "testblob1" in blob_names + @test "testblob2" in blob_names + else + # Single blob case + @test haskey(blobs, "Name") + end + end + @info "Blobs listed successfully" + + # Read blobs + blob_bytes1 = getBlob(nothing, "", "", test_blob1_url, account_key) + @test String(blob_bytes1) == "hello world" + @info "String blob downloaded successfully" + + blob_bytes2 = getBlob(nothing, "", "", test_blob2_url, account_key) + blob_str2 = String(blob_bytes2) + @test strip(blob_str2) == "hello world from file" + @info "File blob downloaded successfully" + + finally + # Clean up - Delete blobs and container + try + deleteBlob(nothing, "", "", test_blob1_url, account_key) + deleteBlob(nothing, "", "", test_blob2_url, account_key) + deleteContainer(nothing, "", "", test_container_url, account_key) + @info "Cleanup completed successfully" + catch e + @warn "Cleanup failed" exception=e + end + end + + else + # Real Azure testing - use original test pattern but with environment variables + @info "Testing with real Azure services" + config = TestConfig.get_azure_config() + ctx = TestConfig.create_test_context() + + storage_url = "https://$(config.storage_account).blob.core.windows.net/" + test_container = storage_url * "testcontainer" + test_blob1 = test_container * "/testblob1" + test_blob2 = test_container * "/testblob2" + + try + @test createContainer(ctx, config.subscription_id, config.resource_group, test_container) + + result = listContainers(ctx, config.subscription_id, config.resource_group, storage_url) + @test haskey(result, "Containers") + + @test putBlob(ctx, config.subscription_id, config.resource_group, test_blob1, "BlockBlob"; + block_blob_contents="hello world") + + mktemp() do path, wio + println(wio, "hello world") + close(wio) + open(path) do rio + @test putBlob(ctx, config.subscription_id, config.resource_group, test_blob2, "BlockBlob"; + block_blob_contents=rio, content_length=filesize(rio)) + end + end + + result = listBlobs(ctx, config.subscription_id, config.resource_group, test_container) + @test haskey(result, "Blobs") + + blob_bytes1 = getBlob(ctx, config.subscription_id, config.resource_group, test_blob1) + @test String(blob_bytes1) == "hello world" + + blob_bytes2 = getBlob(ctx, config.subscription_id, config.resource_group, test_blob2) + blob_str2 = String(blob_bytes2) + @test strip(blob_str2) == "hello world" + + finally + try + deleteBlob(ctx, config.subscription_id, config.resource_group, test_blob1) + deleteBlob(ctx, config.subscription_id, config.resource_group, test_blob2) + deleteContainer(ctx, config.subscription_id, config.resource_group, test_container) + catch e + @warn "Cleanup failed" exception=e + end end end end +end - @test haskey(container, "Name") - @test container["Name"] == "testcontainer" - - # add a string as a blob - @test putBlob(ctx, subscription_id, resource_group_name, testblob1, "BlockBlob"; block_blob_contents="hello world") - - # streaming a file as a blob - mktemp() do path, wio - println(wio, "hello world") - close(wio) - open(path) do rio - @test putBlob(ctx, subscription_id, resource_group_name, testblob2, "BlockBlob"; block_blob_contents=rio, content_length=filesize(rio)) - end - end - - # read blobs - result = listBlobs(ctx, subscription_id, resource_group_name, testcontainer) - @test haskey(result, "Blobs") - @test haskey(result["Blobs"], "Blob") - blobs = result["Blobs"]["Blob"] - @test length(blobs) == 2 - blobnames = Set(b["Name"] for b in blobs) - @test "testblob1" in blobnames - @test "testblob2" in blobnames - - blobbytes1 = getBlob(ctx, subscription_id, resource_group_name, testblob1) - @test String(blobbytes1) == "hello world" - - blobbytes2 = getBlob(ctx, subscription_id, resource_group_name, testblob2) - blobstr2 = String(blobbytes2) - @test strip(blobstr2) == "hello world" - @test length(blobstr2) > length("hello world") - - # delete blobs - @test deleteBlob(ctx, subscription_id, resource_group_name, testblob1) - @test deleteBlob(ctx, subscription_id, resource_group_name, testblob2) +end # module TestBlobs - @test deleteContainer(ctx, subscription_id, resource_group_name, testcontainer) -end +# Run tests if called directly +if abspath(PROGRAM_FILE) == @__FILE__ + TestBlobs.test_blobs() +end \ No newline at end of file diff --git a/test/test_blobs_real_azure.jl b/test/test_blobs_real_azure.jl new file mode 100644 index 0000000..e105a26 --- /dev/null +++ b/test/test_blobs_real_azure.jl @@ -0,0 +1,69 @@ +using Azure +using Azure.StorageServices +using Test + +function test_blobs(tenant_id, appid, password, subscription_id, resource_group_name, storage) + creds = AzureCredentials(tenant_id, appid, password) + ctx = AzureContext(creds) + + resource_group_name = "testresgroup" + testcontainer = storage * "testcontainer" + testblob1 = testcontainer * "/testblob1" + testblob2 = testcontainer * "/testblob2" + + @test createContainer(ctx, subscription_id, resource_group_name, testcontainer) + + result = listContainers(ctx, subscription_id, resource_group_name, storage) + @test haskey(result, "Containers") + @test haskey(result["Containers"], "Container") + containers = result["Containers"]["Container"] + + container = containers + if isa(containers, Vector) + # iterate through multiple containers + for c in containers + if c["Name"] == "testcontainer" + container = c + end + end + end + + @test haskey(container, "Name") + @test container["Name"] == "testcontainer" + + # add a string as a blob + @test putBlob(ctx, subscription_id, resource_group_name, testblob1, "BlockBlob"; block_blob_contents="hello world") + + # streaming a file as a blob + mktemp() do path, wio + println(wio, "hello world") + close(wio) + open(path) do rio + @test putBlob(ctx, subscription_id, resource_group_name, testblob2, "BlockBlob"; block_blob_contents=rio, content_length=filesize(rio)) + end + end + + # read blobs + result = listBlobs(ctx, subscription_id, resource_group_name, testcontainer) + @test haskey(result, "Blobs") + @test haskey(result["Blobs"], "Blob") + blobs = result["Blobs"]["Blob"] + @test length(blobs) == 2 + blobnames = Set(b["Name"] for b in blobs) + @test "testblob1" in blobnames + @test "testblob2" in blobnames + + blobbytes1 = getBlob(ctx, subscription_id, resource_group_name, testblob1) + @test String(blobbytes1) == "hello world" + + blobbytes2 = getBlob(ctx, subscription_id, resource_group_name, testblob2) + blobstr2 = String(blobbytes2) + @test strip(blobstr2) == "hello world" + @test length(blobstr2) > length("hello world") + + # delete blobs + @test deleteBlob(ctx, subscription_id, resource_group_name, testblob1) + @test deleteBlob(ctx, subscription_id, resource_group_name, testblob2) + + @test deleteContainer(ctx, subscription_id, resource_group_name, testcontainer) +end diff --git a/test/test_config.jl b/test/test_config.jl new file mode 100644 index 0000000..7cf9a61 --- /dev/null +++ b/test/test_config.jl @@ -0,0 +1,117 @@ +# Test configuration module for Azure.jl +# Handles switching between local Azurite and real Azure services + +module TestConfig + +using Azure +using Azure.REST + +export get_storage_config, is_local_testing, create_test_context + +""" +Get storage configuration based on environment variables. +Returns appropriate endpoints and credentials for either Azurite or real Azure. +""" +function get_storage_config() + test_mode = get(ENV, "AZURE_TEST_MODE", "local") + + if test_mode == "local" + return get_azurite_config() + else + return get_azure_config() + end +end + +""" +Check if we're running in local testing mode with Azurite. +""" +function is_local_testing() + return get(ENV, "AZURE_TEST_MODE", "local") == "local" +end + +""" +Get Azurite local emulator configuration. +""" +function get_azurite_config() + return ( + blob_endpoint = get(ENV, "AZURITE_BLOB_ENDPOINT", "http://127.0.0.1:10000/devstoreaccount1"), + queue_endpoint = get(ENV, "AZURITE_QUEUE_ENDPOINT", "http://127.0.0.1:10001/devstoreaccount1"), + table_endpoint = get(ENV, "AZURITE_TABLE_ENDPOINT", "http://127.0.0.1:10002/devstoreaccount1"), + account_name = get(ENV, "AZURITE_ACCOUNT_NAME", "devstoreaccount1"), + account_key = get(ENV, "AZURITE_ACCOUNT_KEY", "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="), + connection_string = get(ENV, "AZURE_STORAGE_CONNECTION_STRING", "UseDevelopmentStorage=true") + ) +end + +""" +Get real Azure service configuration from environment variables. +""" +function get_azure_config() + # Validate required environment variables + required_vars = ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", + "AZURE_SUBSCRIPTION_ID", "AZURE_RESOURCE_GROUP", "AZURE_STORAGE_ACCOUNT"] + + for var in required_vars + if !haskey(ENV, var) + throw(ArgumentError("Required environment variable $var not found. Set AZURE_TEST_MODE=local to use Azurite emulator.")) + end + end + + return ( + tenant_id = ENV["AZURE_TENANT_ID"], + client_id = ENV["AZURE_CLIENT_ID"], + client_secret = ENV["AZURE_CLIENT_SECRET"], + subscription_id = ENV["AZURE_SUBSCRIPTION_ID"], + resource_group = ENV["AZURE_RESOURCE_GROUP"], + storage_account = ENV["AZURE_STORAGE_ACCOUNT"], + storage_key = get(ENV, "AZURE_STORAGE_KEY", nothing) + ) +end + +""" +Create an Azure context for testing based on configuration. +""" +function create_test_context() + if is_local_testing() + # For Azurite, we primarily use direct REST calls with the known account key + # No Azure authentication needed for local emulator + return nothing # Will use AccountKey directly in tests + else + config = get_azure_config() + creds = AzureCredentials(config.tenant_id, config.client_id, config.client_secret) + return AzureContext(creds) + end +end + +""" +Create an AccountKey for storage operations based on configuration. +""" +function create_storage_account_key() + config = get_storage_config() + + if is_local_testing() + return Azure.REST.AccountKey(config.account_name, config.account_key) + else + # For real Azure, would need to fetch keys via management API + if config.storage_key !== nothing + return Azure.REST.AccountKey(config.storage_account, config.storage_key) + else + throw(ArgumentError("AZURE_STORAGE_KEY must be provided when AZURE_TEST_MODE=azure")) + end + end +end + +""" +Get base storage URL for blob operations. +""" +function get_storage_base_url() + config = get_storage_config() + + if is_local_testing() + return config.blob_endpoint + else + return "https://$(config.storage_account).blob.core.windows.net" + end +end + +end # module TestConfig \ No newline at end of file diff --git a/test/test_files.jl b/test/test_files.jl new file mode 100644 index 0000000..8dc269a --- /dev/null +++ b/test/test_files.jl @@ -0,0 +1,59 @@ +module TestFiles + +using Azure +using Azure.StorageServices +using Test + +# Include the test configuration module +include("test_config.jl") +using .TestConfig + +""" +Test Azure Files operations with support for both Azurite emulator and real Azure services. +Set AZURE_TEST_MODE=local for Azurite, AZURE_TEST_MODE=azure for real Azure. +""" +function test_files() + @testset "Azure Files Tests (Local)" begin + + if TestConfig.is_local_testing() + @info "Azure Files testing with Azurite" + @warn "Azurite does not support File Storage - skipping local tests" + @test_skip "Azurite emulator does not support Azure Files service" + + else + # Real Azure testing + @info "Testing with real Azure File Storage services" + config = TestConfig.get_azure_config() + ctx = TestConfig.create_test_context() + + file_storage_url = "https://$(config.storage_account).file.core.windows.net/" + test_share = file_storage_url * "testshare" + + try + @test createShare(ctx, config.subscription_id, config.resource_group, test_share) + + success, properties = getShareProperties(ctx, config.subscription_id, config.resource_group, test_share) + @test success + + @test setShareProperties(ctx, config.subscription_id, config.resource_group, test_share, "100") + + metadata = Dict("environment" => "test") + @test setShareMetadata(ctx, config.subscription_id, config.resource_group, test_share, metadata) + + finally + try + deleteShare(ctx, config.subscription_id, config.resource_group, test_share) + catch e + @warn "Share cleanup failed" exception=e + end + end + end + end +end + +end # module TestFiles + +# Run tests if called directly +if abspath(PROGRAM_FILE) == @__FILE__ + TestFiles.test_files() +end \ No newline at end of file diff --git a/test/test_mock_azure.jl b/test/test_mock_azure.jl new file mode 100644 index 0000000..2a8c82a --- /dev/null +++ b/test/test_mock_azure.jl @@ -0,0 +1,252 @@ +module TestMockAzure + +using Test +using Azure +using HTTP +using JSON3 + +# Include the MockAzure module +include("MockAzure.jl") +using .MockAzure +using .MockAzure.CommonMocks + +@testset "MockAzure Tests" begin + + @testset "Basic Mock Server" begin + server = MockAzureServer(8081) + + try + # Start server + start_mock_server(server) + @test server.running == true + + # Add mock response + add_mock_response(server, "GET", "/test", + Dict("status" => 200, "body" => Dict("message" => "Hello World"))) + + # Test the mock response + response = HTTP.get("http://127.0.0.1:8081/test") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test body.message == "Hello World" + + finally + stop_mock_server(server) + @test server.running == false + end + end + + @testset "Azure Resource Group Mock" begin + server = MockAzureServer(8082) + + try + start_mock_server(server) + + # Mock Azure Resource Groups API + subscription_id = "12345678-1234-1234-1234-123456789012" + uri = "/subscriptions/$subscription_id/resourceGroups" + + mock_response = CommonMocks.resource_group_list(subscription_id, ["test-rg-1", "test-rg-2"]) + add_mock_response(server, "GET", uri, mock_response) + + # Test the mock + response = HTTP.get("http://127.0.0.1:8082$uri") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test haskey(body, :value) + @test length(body.value) == 2 + @test body.value[1].name == "test-rg-1" + @test body.value[2].name == "test-rg-2" + + finally + stop_mock_server(server) + end + end + + @testset "Azure Storage Account Mock" begin + server = MockAzureServer(8083) + + try + start_mock_server(server) + + subscription_id = "12345678-1234-1234-1234-123456789012" + resource_group = "test-rg" + uri = "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Storage/storageAccounts" + + mock_response = CommonMocks.storage_account_list(subscription_id, resource_group, ["teststorage1", "teststorage2"]) + add_mock_response(server, "GET", uri, mock_response) + + response = HTTP.get("http://127.0.0.1:8083$uri") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test haskey(body, :value) + @test length(body.value) == 2 + @test body.value[1].name == "teststorage1" + @test haskey(body.value[1].properties, :primaryEndpoints) + + finally + stop_mock_server(server) + end + end + + @testset "Unknown Endpoint Returns 404" begin + server = MockAzureServer(8084) + + try + start_mock_server(server) + + # Request unmocked endpoint + response = HTTP.get("http://127.0.0.1:8084/unknown/endpoint"; status_exception=false) + @test response.status == 404 + + body = JSON3.read(String(response.body)) + @test haskey(body, :error) + @test body.error == "Mock not found" + + finally + stop_mock_server(server) + end + end + + @testset "Virtual Network Mock" begin + server = MockAzureServer(8085) + + try + start_mock_server(server) + + subscription_id = "12345678-1234-1234-1234-123456789012" + resource_group = "test-rg" + uri = "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Network/virtualNetworks" + + mock_response = CommonMocks.virtual_network_list(subscription_id, resource_group, ["vnet1", "vnet2"]) + add_mock_response(server, "GET", uri, mock_response) + + response = HTTP.get("http://127.0.0.1:8085$uri") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test haskey(body, :value) + @test length(body.value) == 2 + @test body.value[1].name == "vnet1" + @test haskey(body.value[1].properties, :addressSpace) + + finally + stop_mock_server(server) + end + end + + @testset "Virtual Machine Mock" begin + server = MockAzureServer(8086) + + try + start_mock_server(server) + + subscription_id = "12345678-1234-1234-1234-123456789012" + resource_group = "test-rg" + uri = "/subscriptions/$subscription_id/resourceGroups/$resource_group/providers/Microsoft.Compute/virtualMachines" + + mock_response = CommonMocks.virtual_machine_list(subscription_id, resource_group, ["vm1", "vm2"]) + add_mock_response(server, "GET", uri, mock_response) + + response = HTTP.get("http://127.0.0.1:8086$uri") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test haskey(body, :value) + @test length(body.value) == 2 + @test body.value[1].name == "vm1" + @test haskey(body.value[1].properties, :hardwareProfile) + + finally + stop_mock_server(server) + end + end + + @testset "Subscription Info Mock" begin + server = MockAzureServer(8087) + + try + start_mock_server(server) + + subscription_id = "12345678-1234-1234-1234-123456789012" + uri = "/subscriptions/$subscription_id" + + mock_response = CommonMocks.subscription_info(subscription_id, "My Test Subscription") + add_mock_response(server, "GET", uri, mock_response) + + response = HTTP.get("http://127.0.0.1:8087$uri") + @test response.status == 200 + + body = JSON3.read(String(response.body)) + @test body.subscriptionId == subscription_id + @test body.displayName == "My Test Subscription" + @test body.state == "Enabled" + + finally + stop_mock_server(server) + end + end + + @testset "Error Response Mock" begin + server = MockAzureServer(8088) + + try + start_mock_server(server) + + # Mock a 404 Not Found error + uri = "/subscriptions/invalid/resourceGroups" + mock_response = CommonMocks.error_response(404, "ResourceGroupNotFound", "Resource group 'invalid' was not found.") + add_mock_response(server, "GET", uri, mock_response) + + response = HTTP.get("http://127.0.0.1:8088$uri"; status_exception=false) + @test response.status == 404 + + body = JSON3.read(String(response.body)) + @test haskey(body, :error) + @test body.error.code == "ResourceGroupNotFound" + @test contains(body.error.message, "was not found") + + finally + stop_mock_server(server) + end + end +end + +# Example usage function +function example_usage() + @info "MockAzure Example Usage" + + server = MockAzureServer(9000) + start_mock_server(server) + + try + # Add some mock responses + add_mock_response(server, "GET", "/subscriptions/test/resourceGroups", + CommonMocks.resource_group_list("test", ["rg1", "rg2"])) + + add_mock_response(server, "GET", "/subscriptions/test/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts", + CommonMocks.storage_account_list("test", "rg1", ["storage1"])) + + # Test requests + @info "Testing resource groups..." + resp1 = HTTP.get("http://127.0.0.1:9000/subscriptions/test/resourceGroups") + println("Resource Groups: ", String(resp1.body)) + + @info "Testing storage accounts..." + resp2 = HTTP.get("http://127.0.0.1:9000/subscriptions/test/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts") + println("Storage Accounts: ", String(resp2.body)) + + finally + stop_mock_server(server) + end +end + +end # module TestMockAzure + +# Run example if script is executed directly +if abspath(PROGRAM_FILE) == @__FILE__ + TestMockAzure.example_usage() +end \ No newline at end of file diff --git a/test/test_sas.jl b/test/test_sas.jl new file mode 100644 index 0000000..6b52d11 --- /dev/null +++ b/test/test_sas.jl @@ -0,0 +1,134 @@ +module TestSAS + +using Azure +using Azure.StorageServices +using Test +using Dates +using URIs + +# Include the test configuration module +include("test_config.jl") +using .TestConfig + +""" +Test SAS (Shared Access Signature) token generation and functionality. +Tests both local and Azure environments. +""" +function test_sas() + @testset "SAS Token Tests" begin + + if TestConfig.is_local_testing() + config = TestConfig.get_storage_config() + account_key = TestConfig.create_storage_account_key() + base_url = TestConfig.get_storage_base_url() + + @info "Testing SAS tokens with Azurite emulator" + + # Create a test container and blob for SAS testing + test_container_name = "sastest" * string(Int(floor(time()))) + test_container_url = base_url * "/" * test_container_name + test_blob_url = test_container_url * "/testblob.txt" + + try + # Create container and blob first + @test createContainer(nothing, "", "", test_container_url, account_key) + @test putBlob(nothing, "", "", test_blob_url, "BlockBlob", account_key; + block_blob_contents="Hello SAS World!") + + @testset "Basic SAS Token Generation" begin + # SAS token generation doesn't work with Azurite URLs + # The sas_canonicalize function expects Azure URLs like account.blob.core.windows.net + # but Azurite uses 127.0.0.1:10000 format + @test_skip "SAS token generation not compatible with Azurite URL format" + @info "SAS token generation skipped for Azurite (URL format incompatible)" + end + + @testset "SAS Permission Combinations" begin + @test_skip "SAS token generation not compatible with Azurite URL format" + @info "SAS permission testing skipped for Azurite" + end + + @testset "SAS Time-based Parameters" begin + @test_skip "SAS token generation not compatible with Azurite URL format" + @info "SAS time-based testing skipped for Azurite" + end + + @testset "SAS Content Headers" begin + @test_skip "SAS token generation not compatible with Azurite URL format" + @info "SAS content header testing skipped for Azurite" + end + + @testset "SAS Helper Functions" begin + # Test SAS helper functions + test_url = "https://myaccount.blob.core.windows.net/container/file.txt" + + # Test filename extraction + filename = Azure.StorageServices.sas_filename(test_url) + @test filename == "file.txt" + + # Test content disposition + content_disp = Azure.StorageServices.sas_content_disposition(test_url) + @test content_disp == "attachment; filename=file.txt" + + # Test canonicalize function + canonical_url, account, restype = Azure.StorageServices.sas_canonicalize(test_url) + @test account == "myaccount" + @test restype == "blob" + @test startswith(canonical_url, "/blob/myaccount/") + + @info "SAS helper functions work correctly" + end + + finally + # Clean up + try + deleteBlob(nothing, "", "", test_blob_url, account_key) + deleteContainer(nothing, "", "", test_container_url, account_key) + @info "SAS test cleanup completed successfully" + catch e + @warn "SAS test cleanup failed" exception=e + end + end + + else + # Real Azure testing + @info "Testing SAS tokens with real Azure services" + config = TestConfig.get_azure_config() + ctx = TestConfig.create_test_context() + + storage_url = "https://$(config.storage_account).blob.core.windows.net/" + test_container = storage_url * "sastest" + test_blob = test_container * "/testblob.txt" + + try + @test createContainer(ctx, config.subscription_id, config.resource_group, test_container) + @test putBlob(ctx, config.subscription_id, config.resource_group, test_blob, "BlockBlob"; + block_blob_contents="Hello SAS World!") + + # Generate SAS token for real Azure + sas_url = appendSAS(ctx, config.subscription_id, config.resource_group, test_blob; + permissions=Azure.StorageServices.SASPermission.READ, + expiry_hours=1) + + @test isa(sas_url, String) + @test contains(sas_url, "sig=") + @info "Real Azure SAS token generated successfully" + + finally + try + deleteBlob(ctx, config.subscription_id, config.resource_group, test_blob) + deleteContainer(ctx, config.subscription_id, config.resource_group, test_container) + catch e + @warn "Real Azure SAS cleanup failed" exception=e + end + end + end + end +end + +end # module TestSAS + +# Run tests if called directly +if abspath(PROGRAM_FILE) == @__FILE__ + TestSAS.test_sas() +end \ No newline at end of file