Skip to content

Commit 0a4cca5

Browse files
authored
Merge pull request #131 from 23prime/feature/129-130-fix-licence-rate-limit-deserialization
fix: make optional fields in Licence struct and fix RateLimit response structure
2 parents 0a72df1 + e3fa860 commit 0a4cca5

10 files changed

Lines changed: 422 additions & 40 deletions

File tree

.cspell/dicts/project.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ burndown
3535
splitn
3636
hexdigit
3737
PRRT
38+
gantt
39+
sattg
40+
varname
41+
CMTS
42+
DOTS

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
.claude/worktrees
3+
smoke-test-result.txt

mise.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ description = "Run Rust tests"
110110
run = ["cargo test -- --nocapture"]
111111
alias = "rt"
112112

113+
[tasks.rs-run]
114+
description = "Run Rust application"
115+
run = "cargo run"
116+
alias = "rr"
117+
113118
[tasks.rs-build]
114119
description = "Build Rust application"
115120
run = "cargo build"

smoke-test.sh

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env bash
2+
# Smoke test: run all read-only bl commands against a real Backlog space.
3+
# Usage: ./smoke-test.sh [SPACE_KEY [PROJECT_KEY]]
4+
set -euo pipefail
5+
6+
SPACE_KEY="${1:-sattg}"
7+
PROJECT_KEY="${2:-DOTS_TEST}"
8+
BL="${BL:-$(command -v bl 2>/dev/null || echo "./target/debug/bl")}"
9+
PASS=0
10+
FAIL=0
11+
BL_OUT=$(mktemp)
12+
trap 'rm -f "$BL_OUT"' EXIT
13+
14+
# ── helpers ────────────────────────────────────────────────────────────────────
15+
16+
run() {
17+
local label="$1"; shift
18+
if "$BL" --space "$SPACE_KEY" "$@" > "$BL_OUT" 2>&1; then
19+
echo " PASS $label"
20+
PASS=$((PASS + 1))
21+
else
22+
echo " FAIL $label"
23+
sed 's/^/ /' "$BL_OUT"
24+
FAIL=$((FAIL + 1))
25+
fi
26+
}
27+
28+
# Run command and capture stdout for later use; still reports PASS/FAIL.
29+
run_capture() {
30+
local label="$1"; local varname="$2"; shift 2
31+
local output
32+
if "$BL" --space "$SPACE_KEY" "$@" > "$BL_OUT" 2>&1; then
33+
echo " PASS $label"
34+
PASS=$((PASS + 1))
35+
output="$(cat "$BL_OUT")"
36+
printf -v "$varname" '%s' "$output"
37+
else
38+
echo " FAIL $label"
39+
sed 's/^/ /' "$BL_OUT"
40+
FAIL=$((FAIL + 1))
41+
printf -v "$varname" ''
42+
fi
43+
}
44+
45+
section() { echo; echo "── $* ──────────────────────────────────────────"; }
46+
47+
jq_or_empty() {
48+
# Extract a value with jq; return empty string on error.
49+
local input="$1"; local query="$2"
50+
echo "$input" | jq -r "$query" 2>/dev/null || true
51+
}
52+
53+
# ── Phase 1: space (read-only) ─────────────────────────────────────────────────
54+
55+
section "auth"
56+
run "auth status" auth status
57+
58+
section "space"
59+
run "space show" space --json
60+
run "space activities" space activities --json
61+
run "space disk-usage" space disk-usage --json
62+
run "space notification" space notification --json
63+
run "space licence" space licence --json
64+
65+
section "master data"
66+
run "priority list" priority list --json
67+
run "resolution list" resolution list --json
68+
run "rate-limit" rate-limit --json
69+
70+
# ── Phase 2: project ──────────────────────────────────────────────────────────
71+
72+
section "project"
73+
run "project list" project list --json
74+
run_capture "project show" PROJ_JSON project show "$PROJECT_KEY" --json
75+
run "project activities" project activities "$PROJECT_KEY" --json
76+
run "project disk-usage" project disk-usage "$PROJECT_KEY" --json
77+
run "project user list" project user list "$PROJECT_KEY" --json
78+
run "project admin list" project admin list "$PROJECT_KEY" --json
79+
run "project status list" project status list "$PROJECT_KEY" --json
80+
run "project issue-type list" project issue-type list "$PROJECT_KEY" --json
81+
run "project category list" project category list "$PROJECT_KEY" --json
82+
run "project version list" project version list "$PROJECT_KEY" --json
83+
run "project team list" project team list "$PROJECT_KEY" --json
84+
run "project webhook list" project webhook list "$PROJECT_KEY" --json
85+
run "project custom-field list" project custom-field list "$PROJECT_KEY" --json
86+
87+
PROJECT_ID=$(jq_or_empty "$PROJ_JSON" '.id')
88+
89+
# ── Phase 3: issues ───────────────────────────────────────────────────────────
90+
91+
section "issue"
92+
if [[ -n "$PROJECT_ID" ]]; then
93+
run "issue list" issue list --project-id "$PROJECT_ID" --json
94+
run "issue count" issue count --project-id "$PROJECT_ID" --json
95+
run_capture "issue show (first)" ISSUE_JSON issue list --project-id "$PROJECT_ID" --count 1 --json
96+
ISSUE_KEY=$(jq_or_empty "$ISSUE_JSON" '.[0].issueKey // empty')
97+
else
98+
echo " SKIP issue list/count (project ID unavailable)"
99+
ISSUE_KEY=""
100+
fi
101+
102+
if [[ -n "$ISSUE_KEY" ]]; then
103+
run "issue show $ISSUE_KEY" issue show "$ISSUE_KEY" --json
104+
run "issue comment list" issue comment list "$ISSUE_KEY" --json
105+
run "issue comment count" issue comment count "$ISSUE_KEY" --json
106+
run "issue attachment list" issue attachment list "$ISSUE_KEY" --json
107+
run "issue participant list" issue participant list "$ISSUE_KEY" --json
108+
run "issue shared-file list" issue shared-file list "$ISSUE_KEY" --json
109+
110+
# comment show (first comment if any)
111+
run_capture "issue comment list (for id)" CMTS_JSON issue comment list "$ISSUE_KEY" --json
112+
COMMENT_ID=$(jq_or_empty "$CMTS_JSON" '.[0].id // empty')
113+
if [[ -n "$COMMENT_ID" ]]; then
114+
run "issue comment show $COMMENT_ID" issue comment show "$ISSUE_KEY" "$COMMENT_ID" --json
115+
run "issue comment notification list" issue comment notification list "$ISSUE_KEY" "$COMMENT_ID" --json
116+
else
117+
echo " SKIP issue comment show/notification (no comments)"
118+
fi
119+
else
120+
echo " SKIP issue subcommands (no issues found)"
121+
fi
122+
123+
# ── Phase 4: wiki ─────────────────────────────────────────────────────────────
124+
125+
section "wiki"
126+
run "wiki list" wiki list "$PROJECT_KEY" --json
127+
run "wiki count" wiki count "$PROJECT_KEY" --json
128+
run "wiki tag list" wiki tag list "$PROJECT_KEY" --json
129+
130+
run_capture "wiki list (for id)" WIKIS_JSON wiki list "$PROJECT_KEY" --json
131+
WIKI_ID=$(jq_or_empty "$WIKIS_JSON" '.[0].id // empty')
132+
133+
if [[ -n "$WIKI_ID" ]]; then
134+
run "wiki show $WIKI_ID" wiki show "$WIKI_ID" --json
135+
run "wiki history" wiki history "$WIKI_ID" --json
136+
run "wiki attachment list" wiki attachment list "$WIKI_ID" --json
137+
run "wiki shared-file list" wiki shared-file list "$WIKI_ID" --json
138+
run "wiki star list" wiki star list "$WIKI_ID" --json
139+
else
140+
echo " SKIP wiki show/history/attachment/shared-file/star (no wiki pages)"
141+
fi
142+
143+
# ── Phase 5: documents ────────────────────────────────────────────────────────
144+
145+
section "document"
146+
if [[ -n "$PROJECT_ID" ]]; then
147+
run "document list" document list --project-id "$PROJECT_ID" --json
148+
run "document tree" document tree "$PROJECT_KEY" --json
149+
150+
run_capture "document list (for id)" DOCS_JSON document list --project-id "$PROJECT_ID" --json
151+
DOC_ID=$(jq_or_empty "$DOCS_JSON" '.list[0].id // .[0].id // empty')
152+
if [[ -n "$DOC_ID" ]]; then
153+
run "document show $DOC_ID" document show "$DOC_ID" --json
154+
else
155+
echo " SKIP document show (no documents)"
156+
fi
157+
else
158+
echo " SKIP document (project ID unavailable)"
159+
fi
160+
161+
# ── Phase 6: shared files ─────────────────────────────────────────────────────
162+
163+
section "shared-file"
164+
run "shared-file list" shared-file list "$PROJECT_KEY" --json
165+
166+
# ── Phase 7: git / PR ─────────────────────────────────────────────────────────
167+
168+
section "git / pr"
169+
run_capture "git repo list" REPOS_JSON git repo list "$PROJECT_KEY" --json
170+
REPO_NAME=$(jq_or_empty "$REPOS_JSON" '.[0].name // empty')
171+
172+
if [[ -n "$REPO_NAME" ]]; then
173+
run "git repo show" git repo show "$PROJECT_KEY" "$REPO_NAME" --json
174+
run "pr list" pr list "$PROJECT_KEY" "$REPO_NAME" --json
175+
run "pr count" pr count "$PROJECT_KEY" "$REPO_NAME" --json
176+
177+
run_capture "pr list (for number)" PRS_JSON pr list "$PROJECT_KEY" "$REPO_NAME" --json
178+
PR_NUM=$(jq_or_empty "$PRS_JSON" '.[0].number // empty')
179+
if [[ -n "$PR_NUM" ]]; then
180+
run "pr show $PR_NUM" pr show "$PROJECT_KEY" "$REPO_NAME" "$PR_NUM" --json
181+
run "pr comment list" pr comment list "$PROJECT_KEY" "$REPO_NAME" "$PR_NUM" --json
182+
run "pr comment count" pr comment count "$PROJECT_KEY" "$REPO_NAME" "$PR_NUM" --json
183+
run "pr attachment list" pr attachment list "$PROJECT_KEY" "$REPO_NAME" "$PR_NUM" --json
184+
else
185+
echo " SKIP pr show/comment/attachment (no pull requests)"
186+
fi
187+
else
188+
echo " SKIP git repo show / pr (no git repositories)"
189+
fi
190+
191+
# ── Phase 8: user ─────────────────────────────────────────────────────────────
192+
193+
section "user"
194+
run "user list" user list --json
195+
run "user recently-viewed" user recently-viewed --json
196+
run "user recently-viewed-projects" user recently-viewed-projects --json
197+
run "user recently-viewed-wikis" user recently-viewed-wikis --json
198+
199+
run_capture "user list (for id)" USERS_JSON user list --json
200+
USER_ID=$(jq_or_empty "$USERS_JSON" '.[] | select(.userId != null) | .id' | head -1)
201+
202+
if [[ -n "$USER_ID" ]]; then
203+
run "user show $USER_ID" user show "$USER_ID" --json
204+
run "user activities" user activities "$USER_ID" --json
205+
run "user star list" user star list "$USER_ID" --json
206+
run "user star count" user star count "$USER_ID" --json
207+
run "watch list" watch list "$USER_ID" --json
208+
run "watch count" watch count "$USER_ID" --json
209+
else
210+
echo " SKIP user show/activities/star/watch (no users with userId)"
211+
fi
212+
213+
# ── Phase 9: team (space scope, read-only) ────────────────────────────────────
214+
215+
section "team"
216+
run_capture "team list" TEAMS_JSON team list --json
217+
TEAM_ID=$(jq_or_empty "$TEAMS_JSON" '.[0].id // empty')
218+
if [[ -n "$TEAM_ID" ]]; then
219+
run "team show $TEAM_ID" team show "$TEAM_ID" --json
220+
else
221+
echo " SKIP team show (no teams)"
222+
fi
223+
224+
# ── Phase 10: notification (space scope, read-only) ───────────────────────────
225+
226+
section "notification"
227+
run "notification list" notification list --json
228+
run "notification count" notification count --json
229+
230+
# ── summary ───────────────────────────────────────────────────────────────────
231+
232+
echo
233+
echo "════════════════════════════════════════"
234+
echo " PASS: $PASS FAIL: $FAIL"
235+
echo "════════════════════════════════════════"
236+
[[ "$FAIL" -eq 0 ]]

src/api/licence.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ use super::deserialize;
88
#[derive(Debug, Clone, Serialize, Deserialize)]
99
#[serde(rename_all = "camelCase")]
1010
pub struct Licence {
11-
pub start_date: String,
11+
pub start_date: Option<String>,
1212
pub contract_type: Option<String>,
13-
pub storage_limit: u64,
14-
pub storage_usage: u64,
13+
pub storage_limit: Option<u64>,
14+
pub storage_usage: Option<u64>,
1515
#[serde(flatten)]
1616
pub extra: BTreeMap<String, serde_json::Value>,
1717
}
@@ -50,10 +50,27 @@ mod tests {
5050

5151
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
5252
let l = client.get_space_licence().unwrap();
53-
assert_eq!(l.start_date, "2020-01-01");
53+
assert_eq!(l.start_date, Some("2020-01-01".to_string()));
5454
assert_eq!(l.contract_type, Some("premium".to_string()));
55-
assert_eq!(l.storage_limit, 1073741824);
56-
assert_eq!(l.storage_usage, 5242880);
55+
assert_eq!(l.storage_limit, Some(1073741824));
56+
assert_eq!(l.storage_usage, Some(5242880));
57+
}
58+
59+
#[test]
60+
fn get_space_licence_without_start_date() {
61+
let server = MockServer::start();
62+
server.mock(|when, then| {
63+
when.method(GET).path("/space/licence");
64+
then.status(200).json_body(json!({
65+
"contractType": "premium",
66+
"storageLimit": 1073741824u64
67+
}));
68+
});
69+
70+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
71+
let l = client.get_space_licence().unwrap();
72+
assert_eq!(l.start_date, None);
73+
assert_eq!(l.storage_usage, None);
5774
}
5875

5976
#[test]

src/api/rate_limit.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ use super::deserialize;
66

77
#[derive(Debug, Clone, Serialize, Deserialize)]
88
#[serde(rename_all = "camelCase")]
9-
pub struct RateLimitInfo {
9+
pub struct RateLimitCategory {
1010
pub limit: u64,
1111
pub remaining: u64,
1212
pub reset: u64,
1313
}
1414

15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
pub struct RateLimitInfo {
18+
pub read: RateLimitCategory,
19+
pub update: RateLimitCategory,
20+
pub search: RateLimitCategory,
21+
pub icon: Option<RateLimitCategory>,
22+
}
23+
1524
#[derive(Debug, Clone, Serialize, Deserialize)]
1625
#[serde(rename_all = "camelCase")]
1726
pub struct RateLimit {
@@ -34,9 +43,10 @@ mod tests {
3443
fn rate_limit_json() -> serde_json::Value {
3544
json!({
3645
"rateLimit": {
37-
"limit": 600,
38-
"remaining": 599,
39-
"reset": 1698230400
46+
"read": {"limit": 600, "remaining": 591, "reset": 1774268714},
47+
"update": {"limit": 150, "remaining": 150, "reset": 1774268655},
48+
"search": {"limit": 150, "remaining": 150, "reset": 1774268655},
49+
"icon": {"limit": 60, "remaining": 60, "reset": 1774268655}
4050
}
4151
})
4252
}
@@ -51,9 +61,30 @@ mod tests {
5161

5262
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
5363
let rl = client.get_rate_limit().unwrap();
54-
assert_eq!(rl.rate_limit.limit, 600);
55-
assert_eq!(rl.rate_limit.remaining, 599);
56-
assert_eq!(rl.rate_limit.reset, 1698230400);
64+
assert_eq!(rl.rate_limit.read.limit, 600);
65+
assert_eq!(rl.rate_limit.read.remaining, 591);
66+
assert_eq!(rl.rate_limit.update.limit, 150);
67+
assert_eq!(rl.rate_limit.search.limit, 150);
68+
assert_eq!(rl.rate_limit.icon.unwrap().limit, 60);
69+
}
70+
71+
#[test]
72+
fn get_rate_limit_without_icon() {
73+
let server = MockServer::start();
74+
server.mock(|when, then| {
75+
when.method(GET).path("/rateLimit");
76+
then.status(200).json_body(json!({
77+
"rateLimit": {
78+
"read": {"limit": 600, "remaining": 600, "reset": 1774268714},
79+
"update": {"limit": 150, "remaining": 150, "reset": 1774268655},
80+
"search": {"limit": 150, "remaining": 150, "reset": 1774268655}
81+
}
82+
}));
83+
});
84+
85+
let client = BacklogClient::new_with(&server.base_url(), "test-key").unwrap();
86+
let rl = client.get_rate_limit().unwrap();
87+
assert!(rl.rate_limit.icon.is_none());
5788
}
5889

5990
#[test]

0 commit comments

Comments
 (0)