This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
The following commit(s) were added to refs/heads/main by this push:
new 0ff4c42 chore: Align our mock servers impl (#674)
0ff4c42 is described below
commit 0ff4c42d0373eadef7e2b00cf26cbc712202c4bd
Author: Xuanwo <[email protected]>
AuthorDate: Fri Dec 26 21:25:06 2025 +0800
chore: Align our mock servers impl (#674)
This PR will align our mock servers impl
---
**Parts of this PR were drafted with assistance from Codex (with
`gpt-5.2`) and fully reviewed and edited by me. I take full
responsibility for all changes.**
---
services/aws-v4/tests/mocks/ecs_mock_server.py | 8 +++-
services/azure-storage/src/sign_request.rs | 54 ++++++++++++++++++++++
services/azure-storage/src/user_delegation.rs | 5 +-
services/azure-storage/tests/README.md | 25 +++-------
services/azure-storage/tests/mocks/imds_mock.py | 15 ++++--
services/google/tests/README.md | 2 +
.../tests/credential_providers/vm_metadata.rs | 23 +++++++++
7 files changed, 107 insertions(+), 25 deletions(-)
diff --git a/services/aws-v4/tests/mocks/ecs_mock_server.py
b/services/aws-v4/tests/mocks/ecs_mock_server.py
index 5d8b88e..91d8040 100644
--- a/services/aws-v4/tests/mocks/ecs_mock_server.py
+++ b/services/aws-v4/tests/mocks/ecs_mock_server.py
@@ -15,12 +15,16 @@ class ECSHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/creds':
# Return mock credentials
+ last_updated = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
expiration = (datetime.utcnow() +
timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ')
response = {
+ 'RoleArn': 'arn:aws:iam::123456789012:role/test-ecs-role',
+ 'Type': 'AWS-HMAC',
'AccessKeyId': 'AKIAIOSFODNN7EXAMPLE',
'SecretAccessKey': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
'Token':
'IQoJb3JpZ2luX2VjEJv//////////wEaCXVzLXdlc3QtMiJGMEQCIDyJl0YXJwU8iBG4gLVxiNJTYfLp3oFxEOpGGHmQuWmFAiBHEK/GkClQFb0aQ/+kOZkzHKVAPItVJW/VEXAMPLE=',
- 'Expiration': expiration
+ 'Expiration': expiration,
+ 'LastUpdated': last_updated,
}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
@@ -40,4 +44,4 @@ if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else 51679
server = HTTPServer(('0.0.0.0', port), ECSHandler)
print(f'Mock ECS server running on port {port}')
- server.serve_forever()
\ No newline at end of file
+ server.serve_forever()
diff --git a/services/azure-storage/src/sign_request.rs
b/services/azure-storage/src/sign_request.rs
index 75dd702..ce0ed7f 100644
--- a/services/azure-storage/src/sign_request.rs
+++ b/services/azure-storage/src/sign_request.rs
@@ -813,6 +813,11 @@ mod tests {
&self,
req: http::Request<Bytes>,
) -> reqsign_core::Result<http::Response<Bytes>> {
+ if req.method() != http::Method::POST {
+ return Err(reqsign_core::Error::unexpected("unexpected request
method")
+ .with_context(req.method().to_string()));
+ }
+
let uri = req.uri().to_string();
if uri
!=
"https://account.blob.core.windows.net/?restype=service&comp=userdelegationkey"
@@ -822,6 +827,45 @@ mod tests {
);
}
+ let version = req
+ .headers()
+ .get("x-ms-version")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or_default()
+ .to_string();
+ if version != "2020-12-06" {
+ return Err(
+ reqsign_core::Error::unexpected("unexpected x-ms-version
header")
+ .with_context(version),
+ );
+ }
+
+ let date = req
+ .headers()
+ .get("x-ms-date")
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or_default()
+ .to_string();
+ if date != "Tue, 01 Mar 2022 08:12:34 GMT" {
+ return Err(
+ reqsign_core::Error::unexpected("unexpected x-ms-date
header")
+ .with_context(date),
+ );
+ }
+
+ let content_type = req
+ .headers()
+ .get(http::header::CONTENT_TYPE)
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or_default()
+ .to_string();
+ if content_type != "application/xml" {
+ return Err(
+ reqsign_core::Error::unexpected("unexpected content-type
header")
+ .with_context(content_type),
+ );
+ }
+
let auth = req
.headers()
.get("authorization")
@@ -835,6 +879,16 @@ mod tests {
);
}
+ let body = String::from_utf8_lossy(req.body()).to_string();
+ if !body.contains("<KeyInfo>")
+ || !body.contains("<Start>2022-03-01T08:12:34Z</Start>")
+ || !body.contains("<Expiry>2022-03-01T08:17:34Z</Expiry>")
+ {
+ return Err(
+ reqsign_core::Error::unexpected("unexpected request
body").with_context(body)
+ );
+ }
+
let body = r#"
<UserDelegationKey>
<SignedOid>oid</SignedOid>
diff --git a/services/azure-storage/src/user_delegation.rs
b/services/azure-storage/src/user_delegation.rs
index 5c3ccab..3306330 100644
--- a/services/azure-storage/src/user_delegation.rs
+++ b/services/azure-storage/src/user_delegation.rs
@@ -60,8 +60,11 @@ pub(crate) async fn get_user_delegation_key(
reqsign_core::Error::request_invalid("invalid user delegation key
URI").with_source(e)
})?;
+ // Azure Blob Storage expects a KeyInfo payload.
+ //
+ // Reference:
https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key
let body = format!(
-
"<UserDelegationKey><SignedStart>{}</SignedStart><SignedExpiry>{}</SignedExpiry></UserDelegationKey>",
+ "<KeyInfo><Start>{}</Start><Expiry>{}</Expiry></KeyInfo>",
request.start.format_rfc3339_zulu(),
request.expiry.format_rfc3339_zulu(),
);
diff --git a/services/azure-storage/tests/README.md
b/services/azure-storage/tests/README.md
index 84b636b..875ac4e 100644
--- a/services/azure-storage/tests/README.md
+++ b/services/azure-storage/tests/README.md
@@ -20,8 +20,7 @@ tests/
│ ├── shared_key.rs # SharedKey signature tests
│ └── sas_token.rs # SAS token handling tests
└── mocks/ # Mock servers for testing
- ├── imds_mock_server.py # Mock IMDS endpoint
- └── oauth_mock_server.py # Mock OAuth2 token endpoint
+ └── imds_mock.py # Mock IMDS endpoint
```
## Running Tests Locally
@@ -109,26 +108,14 @@ The test suite includes mock servers for testing without
Azure dependencies:
Simulates the Azure Instance Metadata Service:
```bash
-python3 tests/mocks/imds_mock_server.py 8080
+python3 tests/mocks/imds_mock.py 8080
```
Then run tests with:
```bash
-export REQSIGN_AZURE_STORAGE_TEST_IMDS_MOCK=on
-export AZURE_IMDS_ENDPOINT=http://localhost:8080
-cargo test credential_providers::imds::test_imds_provider_with_mock
-```
-
-### OAuth Mock Server
-
-Simulates Azure AD token endpoints:
-```bash
-python3 tests/mocks/oauth_mock_server.py 8081
-```
-
-Configure with:
-```bash
-export AZURE_AUTHORITY_HOST=http://localhost:8081
+export REQSIGN_AZURE_STORAGE_TEST_IMDS=on
+export AZURE_IMDS_ENDPOINT=http://localhost:8080/metadata/identity/oauth2/token
+cargo test credential_providers::imds:: --no-fail-fast
```
## GitHub Actions
@@ -196,4 +183,4 @@ openssl pkcs12 -info -in $AZURE_CLIENT_CERTIFICATE_PATH
3. Add environment variable flag if needed
4. Update GitHub Actions workflow
5. Update this README
-6. Add secrets to 1Password if required
\ No newline at end of file
+6. Add secrets to 1Password if required
diff --git a/services/azure-storage/tests/mocks/imds_mock.py
b/services/azure-storage/tests/mocks/imds_mock.py
index 59d92ad..f20f76c 100644
--- a/services/azure-storage/tests/mocks/imds_mock.py
+++ b/services/azure-storage/tests/mocks/imds_mock.py
@@ -18,13 +18,22 @@ class IMDSHandler(BaseHTTPRequestHandler):
return
if parsed.path == '/metadata/identity/oauth2/token':
- # Accept both API versions (2018-02-01 and 2019-08-01)
+ api_version = query.get('api-version', [''])[0]
+ if api_version not in ('2018-02-01', '2019-08-01'):
+ self.send_error(400, "unsupported api-version")
+ return
+
+ resource = query.get('resource', [''])[0]
+ if not resource:
+ self.send_error(400, "resource query required")
+ return
+
# Mock token response
token_response = {
"access_token":
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKTTBWYmJQWGhzMWpvT1ljYjh0WXhPXyIsImtpZCI6IjJaUXBKTTBWYmJQWGhzMWpvT1ljYjh0WXhPXyJ9.eyJhdWQiOiJodHRwczovL3N0b3JhZ2UuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMi8iLCJpYXQiOjE2MzM1MzY0MjIsIm5iZiI6MTYzMzUzNjQyMiwiZXhwIjoxNjMzNjIzMTIyLCJhaW8iOiJFMlpnWUxqL3Y3Ly9kWitQL0JBQSIsImFwcGlkIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MDEyIiwiYXBwaWRhY3IiOi
[...]
"expires_in": "3600",
"expires_on": str(int(time.time()) + 3600),
- "resource": query.get('resource', [''])[0],
+ "resource": resource,
"token_type": "Bearer"
}
self.send_response(200)
@@ -42,4 +51,4 @@ if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
server = HTTPServer(('', port), IMDSHandler)
print(f"IMDS mock server started on port {port}")
- server.serve_forever()
\ No newline at end of file
+ server.serve_forever()
diff --git a/services/google/tests/README.md b/services/google/tests/README.md
index bcabf08..798474d 100644
--- a/services/google/tests/README.md
+++ b/services/google/tests/README.md
@@ -34,6 +34,7 @@ Tests for request signing functionality:
- `REQSIGN_GOOGLE_TEST_IMPERSONATION_REAL`: Set to `on` to test with real
impersonation credentials
- `REQSIGN_GOOGLE_TEST_IMPERSONATION_DELEGATES`: Set to `on` to test
impersonation with delegation chain
- `REQSIGN_GOOGLE_TEST_VM_METADATA`: Set to `on` to enable
VmMetadataCredentialProvider tests (GCP VMs only)
+- `REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK`: Set to `on` to enable
VmMetadataCredentialProvider tests with the local mock server
### Google Cloud Configuration
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to credential JSON file (supports all
credential types)
@@ -108,6 +109,7 @@ The DefaultCredentialProvider automatically detects and
handles all these types.
## Notes
- The VmMetadataCredentialProvider tests are disabled by default in CI as they
require running on actual GCP VMs
+- CI runs a separate VmMetadataCredentialProvider test via the mock metadata
server
- External Account tests can run in GitHub Actions with proper Workload
Identity setup
- Impersonation tests require proper IAM permissions for the source credentials
- Tests use real GCS API endpoints to verify signature validity
diff --git a/services/google/tests/credential_providers/vm_metadata.rs
b/services/google/tests/credential_providers/vm_metadata.rs
index be14e6a..3f13b7d 100644
--- a/services/google/tests/credential_providers/vm_metadata.rs
+++ b/services/google/tests/credential_providers/vm_metadata.rs
@@ -45,6 +45,29 @@ async fn test_vm_metadata_credential_provider() ->
Result<()> {
Ok(())
}
+#[tokio::test]
+async fn test_vm_metadata_credential_provider_with_mock() -> Result<()> {
+ if env::var("REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK").unwrap_or_default() !=
"on" {
+ warn!("REQSIGN_GOOGLE_TEST_VM_METADATA_MOCK is not set, skipped");
+ return Ok(());
+ }
+
+ let ctx = create_test_context();
+
+ let provider = VmMetadataCredentialProvider::new();
+ let credential = provider
+ .provide_credential(&ctx)
+ .await?
+ .expect("credential must be provided with mock metadata server");
+
+ assert!(credential.has_token(), "Must have access token");
+ assert!(credential.has_valid_token(), "Token must be valid");
+ let token = credential.token.as_ref().unwrap();
+ assert!(!token.access_token.is_empty(), "Token must not be empty");
+
+ Ok(())
+}
+
#[tokio::test]
async fn test_vm_metadata_credential_provider_with_scope() -> Result<()> {
if env::var("REQSIGN_GOOGLE_TEST_VM_METADATA").unwrap_or_default() != "on"
{