Skip to content

Commit c379d0e

Browse files
committed
added live tests
1 parent 8709791 commit c379d0e

File tree

3 files changed

+260
-0
lines changed

3 files changed

+260
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Live Contract
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 3 * * 1"
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
live-contract:
13+
name: Live API contract tests
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 20
16+
env:
17+
GIE_LIVE_TESTS: "1"
18+
GIE_API_KEY: ${{ secrets.GIE_API_KEY }}
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Setup Rust
25+
uses: dtolnay/rust-toolchain@stable
26+
with:
27+
toolchain: "stable"
28+
29+
- name: Cache
30+
uses: Swatinem/rust-cache@v2
31+
32+
- name: Live contract tests
33+
run: cargo test --test live_api_contract -- --ignored

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,19 @@ Environment variables used by examples:
109109
- `GIE_API_KEY`
110110
- `GIE_PROXY_URL`
111111
- `GIE_USER_AGENT`
112+
113+
## Live Contract Tests
114+
115+
Live contract tests hit the real GIE API and are intentionally excluded from the default CI test job.
116+
117+
Run public live tests locally:
118+
119+
```bash
120+
GIE_LIVE_TESTS=1 cargo test --test live_api_contract -- --ignored
121+
```
122+
123+
Run public + auth live tests locally:
124+
125+
```bash
126+
GIE_API_KEY=... GIE_LIVE_TESTS=1 cargo test --test live_api_contract -- --ignored
127+
```

tests/live_api_contract.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use std::env;
2+
use std::time::Duration;
3+
4+
use gie_client::GiePage;
5+
use gie_client::GieQuery;
6+
use gie_client::agsi::AgsiClient;
7+
use gie_client::alsi::AlsiClient;
8+
use serde_json::Value;
9+
10+
const AGSI_API_URL: &str = "https://agsi.gie.eu/api";
11+
const ALSI_API_URL: &str = "https://alsi.gie.eu/api";
12+
const QUERY_SIZE: &str = "1";
13+
14+
#[test]
15+
#[ignore]
16+
fn agsi_public_contract() {
17+
if !live_tests_enabled() {
18+
eprintln!("Skipping live test: set GIE_LIVE_TESTS=1 to enable");
19+
return;
20+
}
21+
22+
let query = GieQuery::new()
23+
.country("DE")
24+
.try_size(1)
25+
.expect("failed to build AGSI live query");
26+
let page = AgsiClient::without_api_key()
27+
.fetch_page(&query)
28+
.expect("AGSI public typed request failed");
29+
30+
assert_typed_contract(&page);
31+
32+
let raw = fetch_raw_page(AGSI_API_URL, "DE", None);
33+
assert_envelope_core_shape(&raw);
34+
}
35+
36+
#[test]
37+
#[ignore]
38+
fn alsi_public_contract() {
39+
if !live_tests_enabled() {
40+
eprintln!("Skipping live test: set GIE_LIVE_TESTS=1 to enable");
41+
return;
42+
}
43+
44+
let query = GieQuery::new()
45+
.country("FR")
46+
.try_size(1)
47+
.expect("failed to build ALSI live query");
48+
let page = AlsiClient::without_api_key()
49+
.fetch_page(&query)
50+
.expect("ALSI public typed request failed");
51+
52+
assert_typed_contract(&page);
53+
54+
let raw = fetch_raw_page(ALSI_API_URL, "FR", None);
55+
assert_envelope_core_shape(&raw);
56+
}
57+
58+
#[test]
59+
#[ignore]
60+
fn agsi_auth_contract_optional() {
61+
if !live_tests_enabled() {
62+
eprintln!("Skipping live test: set GIE_LIVE_TESTS=1 to enable");
63+
return;
64+
}
65+
66+
let Some(api_key) = read_api_key_or_skip() else {
67+
return;
68+
};
69+
70+
let query = GieQuery::new()
71+
.country("DE")
72+
.try_size(1)
73+
.expect("failed to build AGSI auth query");
74+
let page = AgsiClient::new(api_key.as_str())
75+
.fetch_page(&query)
76+
.expect("AGSI auth typed request failed");
77+
78+
assert_typed_contract(&page);
79+
80+
let raw = fetch_raw_page(AGSI_API_URL, "DE", Some(api_key.as_str()));
81+
assert_envelope_core_shape(&raw);
82+
}
83+
84+
#[test]
85+
#[ignore]
86+
fn alsi_auth_contract_optional() {
87+
if !live_tests_enabled() {
88+
eprintln!("Skipping live test: set GIE_LIVE_TESTS=1 to enable");
89+
return;
90+
}
91+
92+
let Some(api_key) = read_api_key_or_skip() else {
93+
return;
94+
};
95+
96+
let query = GieQuery::new()
97+
.country("FR")
98+
.try_size(1)
99+
.expect("failed to build ALSI auth query");
100+
let page = AlsiClient::new(api_key.as_str())
101+
.fetch_page(&query)
102+
.expect("ALSI auth typed request failed");
103+
104+
assert_typed_contract(&page);
105+
106+
let raw = fetch_raw_page(ALSI_API_URL, "FR", Some(api_key.as_str()));
107+
assert_envelope_core_shape(&raw);
108+
}
109+
110+
fn live_tests_enabled() -> bool {
111+
normalized_env_var("GIE_LIVE_TESTS").is_some_and(|value| value == "1")
112+
}
113+
114+
fn read_api_key_or_skip() -> Option<String> {
115+
let Some(api_key) = normalized_env_var("GIE_API_KEY") else {
116+
eprintln!("Skipping auth live test: GIE_API_KEY is not set");
117+
return None;
118+
};
119+
120+
Some(api_key)
121+
}
122+
123+
fn normalized_env_var(name: &str) -> Option<String> {
124+
env::var(name).ok().and_then(|value| {
125+
let trimmed = value.trim();
126+
if trimmed.is_empty() {
127+
None
128+
} else {
129+
Some(trimmed.to_string())
130+
}
131+
})
132+
}
133+
134+
fn build_http_client() -> reqwest::blocking::Client {
135+
reqwest::blocking::Client::builder()
136+
.timeout(Duration::from_secs(20))
137+
.build()
138+
.expect("failed to build reqwest client for live tests")
139+
}
140+
141+
fn build_query_params(country: &str) -> [(&'static str, &str); 2] {
142+
[("country", country), ("size", QUERY_SIZE)]
143+
}
144+
145+
fn fetch_raw_page(url: &str, country: &str, api_key: Option<&str>) -> Value {
146+
let client = build_http_client();
147+
let mut request = client.get(url).query(&build_query_params(country));
148+
149+
if let Some(api_key) = api_key {
150+
request = request.header("x-key", api_key);
151+
}
152+
153+
let response = request.send().expect("failed to send raw contract request");
154+
let status = response.status();
155+
let body = response
156+
.text()
157+
.expect("failed to read raw contract response body");
158+
159+
assert!(
160+
status.is_success(),
161+
"expected successful HTTP status from {url}, got {status}: {body}"
162+
);
163+
164+
serde_json::from_str(&body).expect("failed to decode raw contract JSON response")
165+
}
166+
167+
fn assert_typed_contract<T>(page: &GiePage<T>) {
168+
assert!(
169+
page.last_page >= 1,
170+
"expected last_page >= 1, got {}",
171+
page.last_page
172+
);
173+
174+
let total = usize::try_from(page.total).expect("total did not fit in usize");
175+
assert!(
176+
total >= page.data.len(),
177+
"expected total >= data.len(), got total={} len={}",
178+
page.total,
179+
page.data.len()
180+
);
181+
assert!(!page.data.is_empty(), "expected non-empty data payload");
182+
}
183+
184+
fn assert_envelope_core_shape(raw: &Value) {
185+
let envelope = raw
186+
.as_object()
187+
.expect("expected top-level JSON object envelope");
188+
189+
for key in ["last_page", "total", "data"] {
190+
assert!(
191+
envelope.contains_key(key),
192+
"expected envelope to contain key {key}"
193+
);
194+
}
195+
196+
let data = envelope
197+
.get("data")
198+
.and_then(Value::as_array)
199+
.expect("expected envelope.data to be an array");
200+
201+
if let Some(first_record) = data.first()
202+
&& let Some(first_object) = first_record.as_object()
203+
{
204+
for key in ["name", "code", "type", "gasDayStart"] {
205+
assert!(
206+
first_object.contains_key(key),
207+
"expected first record to contain key {key}"
208+
);
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)