Skip to content

Commit 2cfdee4

Browse files
authored
Merge branch 'main' into refactor-account-creation-1017
Signed-off-by: Mathew Joseph <[email protected]>
2 parents 41f7766 + 19a3a61 commit 2cfdee4

File tree

3 files changed

+299
-1
lines changed

3 files changed

+299
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1414
delegated spending behavior and key concepts. [#1202](https://github.com/hiero-ledger/hiero-sdk-python/issues/1202)
1515
- Added a GitHub Actions workflow to validate broken Markdown links in pull requests.
1616
- Added method chaining examples to the developer training guide (`docs/sdk_developers/training/coding_token_transactions.md`) (#1194)
17+
- Added documentation explaining how to pin GitHub Actions to specific commit SHAs (`docs/sdk_developers/how-to-pin-github-actions.md`)(#1211)
1718
- examples/mypy.ini for stricter type checking in example scripts
1819
- Added a GitHub Actions workflow that reminds contributors to link pull requests to issues.
1920
- Added `__str__` and `__repr__` methods to `AccountInfo` class for improved logging and debugging experience (#1098)
@@ -97,7 +98,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
9798
- Moved helpful references to Additional Context section and added clickable links.
9899
- Transformed `examples\tokens\custom_royalty_fee.py` to be an end-to-end example, that interacts with the Hedera network, rather than a static object demo.
99100
- Refactored `examples/tokens/custom_royalty_fee.py` by splitting monolithic function custom_royalty_fee_example() into modular functions create_royalty_fee_object(), create_token_with_fee(), verify_token_fee(), and main() to improve readability, cleaned up setup_client() (#1169)
100-
101+
- Added comprehensive unit tests for Timestamp class (#1158)
101102

102103

103104

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# How to Pin GitHub Actions to a Specific Commit Hash
2+
3+
## Overview
4+
5+
When creating or updating GitHub workflows in the Hiero Python SDK, **all GitHub Actions must be pinned to a specific commit hash** rather than using floating tags.
6+
7+
This document explains **why pinning is required**, **how to find the correct commit hash**, and **best practices** for maintaining pinned GitHub Actions.
8+
9+
---
10+
11+
## Why Pin GitHub Actions?
12+
13+
GitHub Actions can be referenced in multiple ways:
14+
15+
```yaml
16+
uses: owner/action@v1 # ❌ floating tag
17+
uses: owner/action@v4 # ⚠️ major version tag
18+
uses: owner/action@<commit> # ✅ pinned commit SHA (v2.14.0)
19+
```
20+
21+
### Security and supply-chain risk
22+
23+
GitHub Actions referenced using tags such as `latest`, `v1`, or `v4` are **mutable** and can be updated to point to a different commit at any time. This means the code executed by a workflow can change without any modification to the workflow file itself.
24+
25+
If an action repository is compromised or a malicious change is introduced upstream, workflows that rely on floating tags may unknowingly execute untrusted code. This represents a significant **software supply-chain risk**.
26+
27+
By pinning a GitHub Action to a specific commit SHA, the exact code being executed is known and cannot change unexpectedly. This makes workflows more secure, auditable, and resistant to supply-chain attacks.
28+
29+
For this reason, many security tools (for example, StepSecurity) require GitHub Actions to be pinned to commit SHAs.
30+
31+
### Reproducibility and stability
32+
33+
Pinning GitHub Actions to a specific commit SHA ensures workflows are fully reproducible. The same workflow will always execute the same version of an action, regardless of new releases or tag updates made upstream.
34+
35+
This prevents unexpected behavior changes caused by automatic updates to floating tags and makes CI runs more predictable.
36+
37+
Reproducible workflows are easier to debug, audit, and maintain, since any failures can be traced back to a known and immutable version of the action.
38+
39+
## Step-by-step: How to find the correct commit SHA
40+
41+
### Step 1: Open the action’s GitHub repository
42+
43+
Start with the official GitHub repository for the action. Prefer repositories linked from the GitHub Marketplace and ensure the project is actively maintained.
44+
45+
Examples:
46+
- actions/checkout
47+
- step-security/harden-runner
48+
49+
### Step 2: Go to the Releases page
50+
51+
In the repository, navigate to **Releases** and open the **Latest** release.
52+
53+
### Step 3: Open the release tag
54+
55+
Click the release version (for example `v2.14.0`) to open the release details page.
56+
57+
### Step 4: Copy the commit SHA
58+
59+
From the release page, open the commit associated with the release and copy the full 40-character commit SHA.
60+
61+
Example SHA: 20cf305ff2072d973412fa9b1e3a4f227bda3c76
62+
63+
### Step 5: Update the workflow
64+
65+
Replace any floating or version-based reference:
66+
67+
```yaml
68+
uses: step-security/harden-runner@v2
69+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
70+
```
71+
72+
## Best practices
73+
74+
- Always pin GitHub Actions to a full commit SHA
75+
- Avoid floating tags such as `latest`, `v1`, or `v4`
76+
- Keep a version comment (for example `# v2.14.0`)
77+
- Review release notes before updating pinned SHAs
78+
- Periodically update pinned actions to include security fixes
79+
- Avoid deprecated or archived action repositories

tests/unit/timestamp_test.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""
2+
Unit tests for the Timestamp class.
3+
4+
These tests validate correctness, edge cases, precision handling,
5+
serialization, arithmetic, comparison, and time-based behavior to
6+
ensure robust coverage of timestamp functionality.
7+
"""
8+
9+
import time
10+
import pytest
11+
from datetime import datetime, timezone
12+
13+
from hiero_sdk_python.timestamp import Timestamp
14+
from hiero_sdk_python.hapi.services.timestamp_pb2 import Timestamp as TimestampProto
15+
16+
pytestmark = pytest.mark.unit
17+
18+
19+
# Constructor and basic tests
20+
21+
22+
def test_init_and_attributes():
23+
"""Verify that Timestamp initializes correctly with seconds and nanoseconds."""
24+
ts = Timestamp(10, 500)
25+
assert ts.seconds == 10
26+
assert ts.nanos == 500
27+
28+
29+
def test_eq_and_hash():
30+
"""Ensure equality and hashing behave correctly for Timestamp instances."""
31+
ts1 = Timestamp(10, 500)
32+
ts2 = Timestamp(10, 500)
33+
ts3 = Timestamp(11, 0)
34+
35+
assert ts1 == ts2
36+
assert ts1 != ts3
37+
assert hash(ts1) == hash(ts2)
38+
39+
40+
def test_str_representation_zero_padded():
41+
"""Ensure string representation is zero-padded to 9 nanoseconds digits."""
42+
ts = Timestamp(10, 5)
43+
assert str(ts) == "10.000000005"
44+
45+
46+
# from_date() tests
47+
48+
49+
@pytest.mark.parametrize(
50+
"value",
51+
[
52+
datetime(1970, 1, 1, tzinfo=timezone.utc),
53+
int(time.time()),
54+
"1970-01-01T00:00:00+00:00",
55+
],
56+
)
57+
def test_from_date_valid_inputs(value):
58+
"""Test from_date with valid inputs: datetime, int, and ISO-8601 string."""
59+
ts = Timestamp.from_date(value)
60+
assert isinstance(ts, Timestamp)
61+
62+
63+
def test_from_date_unix_epoch():
64+
"""Test from_date with the Unix epoch (0 seconds)."""
65+
dt = datetime(1970, 1, 1, tzinfo=timezone.utc)
66+
ts = Timestamp.from_date(dt)
67+
assert ts.seconds == 0
68+
assert ts.nanos == 0
69+
70+
71+
def test_from_date_max_microseconds():
72+
"""Test from_date with maximum microseconds to ensure nanos calculation is correct."""
73+
dt = datetime(2020, 1, 1, 0, 0, 0, 999999, tzinfo=timezone.utc)
74+
ts = Timestamp.from_date(dt)
75+
76+
expected = 999_999_000
77+
assert abs(ts.nanos - expected) < 1_000
78+
79+
@pytest.mark.parametrize("bad_input", [None, [], {}, 3.14])
80+
def test_from_date_invalid_type(bad_input):
81+
"""Ensure from_date raises ValueError for invalid input types."""
82+
with pytest.raises(ValueError, match="Invalid type for 'date'"):
83+
Timestamp.from_date(bad_input)
84+
85+
86+
# to_date() tests
87+
88+
89+
def test_to_date_returns_utc_datetime():
90+
"""Verify that to_date returns a UTC datetime with correct seconds and microseconds."""
91+
ts = Timestamp(10, 500_000_000)
92+
dt = ts.to_date()
93+
94+
assert isinstance(dt, datetime)
95+
assert dt.tzinfo == timezone.utc
96+
assert dt.second == 10
97+
assert dt.microsecond == 500_000
98+
99+
100+
def test_to_date_truncates_nanoseconds():
101+
"""Ensure that nanoseconds are truncated (not rounded) when converting to datetime."""
102+
ts = Timestamp(0, 123_456_789)
103+
dt = ts.to_date()
104+
assert dt.microsecond == 123_456
105+
106+
107+
def test_datetime_round_trip_preserves_microseconds():
108+
"""Verify that datetime -> Timestamp -> datetime preserves microsecond precision."""
109+
original = datetime.now(timezone.utc).replace(microsecond=654321)
110+
ts = Timestamp.from_date(original)
111+
result = ts.to_date()
112+
assert original.replace(microsecond=result.microsecond) == result
113+
114+
115+
# plus_nanos() tests
116+
117+
118+
def test_plus_nanos_simple_add():
119+
"""Test simple addition of nanoseconds without overflow."""
120+
ts = Timestamp(1, 100)
121+
new_ts = ts.plus_nanos(200)
122+
assert new_ts.seconds == 1
123+
assert new_ts.nanos == 300
124+
125+
126+
def test_plus_nanos_carry_over():
127+
"""Test addition of nanoseconds causing a carry-over into seconds."""
128+
ts = Timestamp(1, 900_000_000)
129+
new_ts = ts.plus_nanos(200_000_000)
130+
assert new_ts.seconds == 2
131+
assert new_ts.nanos == 100_000_000
132+
133+
134+
def test_plus_nanos_multiple_seconds():
135+
"""Test addition of nanoseconds resulting in multiple seconds overflow."""
136+
ts = Timestamp(1, 0)
137+
new_ts = ts.plus_nanos(3_000_000_000)
138+
assert new_ts.seconds == 4
139+
assert new_ts.nanos == 0
140+
141+
142+
def test_plus_nanos_zero():
143+
"""Test adding zero nanoseconds returns the same Timestamp instance."""
144+
ts = Timestamp(5, 123)
145+
new_ts = ts.plus_nanos(0)
146+
assert new_ts == ts
147+
148+
149+
# compare() tests
150+
151+
152+
def test_compare_equal():
153+
"""Verify compare returns 0 for equal Timestamps."""
154+
ts1 = Timestamp(10, 0)
155+
ts2 = Timestamp(10, 0)
156+
assert ts1.compare(ts2) == 0
157+
158+
159+
def test_compare_less_than():
160+
"""Verify compare returns -1 when first Timestamp is earlier."""
161+
ts1 = Timestamp(9, 0)
162+
ts2 = Timestamp(10, 0)
163+
assert ts1.compare(ts2) == -1
164+
165+
166+
def test_compare_greater_than():
167+
"""Verify compare returns 1 when first Timestamp is later."""
168+
ts1 = Timestamp(10, 1)
169+
ts2 = Timestamp(10, 0)
170+
assert ts1.compare(ts2) == 1
171+
172+
173+
# generate() tests
174+
175+
176+
def test_generate_without_jitter():
177+
"""Ensure generate without jitter produces a timestamp close to current time."""
178+
ts = Timestamp.generate(has_jitter=False)
179+
delta = abs(ts.to_date().timestamp() - time.time())
180+
181+
assert delta < 0.1
182+
183+
184+
185+
def test_generate_with_jitter():
186+
"""Verify that generated timestamps with jitter remain close to the system time within a safe tolerance."""
187+
ts = Timestamp.generate(has_jitter=True)
188+
delta = time.time() - ts.to_date().timestamp()
189+
190+
# Jitter is explicitly 3-8 seconds backward
191+
assert 3.0 <= delta <= 9.0
192+
193+
# Protobuf serialization tests
194+
195+
196+
def test_to_protobuf():
197+
"""Ensure Timestamp converts correctly to protobuf representation."""
198+
ts = Timestamp(10, 500)
199+
proto = ts._to_protobuf()
200+
assert isinstance(proto, TimestampProto)
201+
assert proto.seconds == 10
202+
assert proto.nanos == 500
203+
204+
205+
def test_from_protobuf():
206+
"""Ensure Timestamp can be created from a protobuf object."""
207+
proto = TimestampProto(seconds=10, nanos=500)
208+
ts = Timestamp._from_protobuf(proto)
209+
assert ts.seconds == 10
210+
assert ts.nanos == 500
211+
212+
213+
def test_protobuf_round_trip():
214+
"""Verify that protobuf serialization and deserialization preserves the original Timestamp."""
215+
original = Timestamp(123, 456)
216+
proto = original._to_protobuf()
217+
restored = Timestamp._from_protobuf(proto)
218+
assert original == restored

0 commit comments

Comments
 (0)