atlassian-cli 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {atlassian_cli-0.12.0/src/atlassian_cli.egg-info → atlassian_cli-0.13.0}/PKG-INFO +21 -1
  2. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/README.md +20 -0
  3. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/pyproject.toml +2 -1
  4. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/config.py +24 -0
  5. atlassian_cli-0.13.0/src/atlassian_cli/org.py +117 -0
  6. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0/src/atlassian_cli.egg-info}/PKG-INFO +21 -1
  7. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/SOURCES.txt +2 -0
  8. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/entry_points.txt +1 -0
  9. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_config.py +43 -1
  10. atlassian_cli-0.13.0/tests/test_org.py +95 -0
  11. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/LICENSE +0 -0
  12. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/setup.cfg +0 -0
  13. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/__init__.py +0 -0
  14. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/adf.py +0 -0
  15. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/confluence.py +0 -0
  16. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/hints.py +0 -0
  17. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/http.py +0 -0
  18. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira.py +0 -0
  19. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira_assets.py +0 -0
  20. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/jira_issues.py +0 -0
  21. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/output.py +0 -0
  22. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli/update_check.py +0 -0
  23. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/dependency_links.txt +0 -0
  24. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/requires.txt +0 -0
  25. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/src/atlassian_cli.egg-info/top_level.txt +0 -0
  26. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_adf.py +0 -0
  27. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_confluence.py +0 -0
  28. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_http.py +0 -0
  29. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_jira_assets.py +0 -0
  30. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_jira_issues.py +0 -0
  31. {atlassian_cli-0.12.0 → atlassian_cli-0.13.0}/tests/test_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlassian-cli
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
5
5
  Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
6
6
  License-Expression: MIT
@@ -85,6 +85,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
85
85
 
86
86
  > Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
87
87
 
88
+ Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
89
+ ```
90
+ ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
91
+ ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
92
+ ```
93
+
88
94
  ## Confluence CLI
89
95
 
90
96
  Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
@@ -261,6 +267,18 @@ jira assets delete 123
261
267
  jira assets type-create 1 "Network Device" --description "Switches and routers"
262
268
  ```
263
269
 
270
+ ## Organisation admin CLI
271
+
272
+ Read-only access to the org admin API: audit log, managed users, audit action types.
273
+
274
+ ```bash
275
+ org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
276
+ org users [--org-id ID] [--dump FILE] # managed accounts with last_active
277
+ org event-actions [--org-id ID] [--dump FILE] # every audit event action type
278
+ ```
279
+
280
+ `--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
281
+
264
282
  ## `--json` flag
265
283
 
266
284
  Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
@@ -298,12 +316,14 @@ src/atlassian_cli/
298
316
  jira.py Jira CLI entry point (subparsers)
299
317
  jira_issues.py Jira issue commands (v3 API)
300
318
  jira_assets.py Jira Assets commands (Assets v1 API)
319
+ org.py Org admin commands (admin v1 API)
301
320
  ```
302
321
 
303
322
  **APIs used:**
304
323
  - Confluence Cloud REST API v2 (`/wiki/api/v2/`)
305
324
  - Jira Cloud REST API v3 (`/rest/api/3/`)
306
325
  - Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
326
+ - Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
307
327
 
308
328
  ## Development
309
329
 
@@ -54,6 +54,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
54
54
 
55
55
  > Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
56
56
 
57
+ Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
58
+ ```
59
+ ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
60
+ ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
61
+ ```
62
+
57
63
  ## Confluence CLI
58
64
 
59
65
  Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
@@ -230,6 +236,18 @@ jira assets delete 123
230
236
  jira assets type-create 1 "Network Device" --description "Switches and routers"
231
237
  ```
232
238
 
239
+ ## Organisation admin CLI
240
+
241
+ Read-only access to the org admin API: audit log, managed users, audit action types.
242
+
243
+ ```bash
244
+ org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
245
+ org users [--org-id ID] [--dump FILE] # managed accounts with last_active
246
+ org event-actions [--org-id ID] [--dump FILE] # every audit event action type
247
+ ```
248
+
249
+ `--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
250
+
233
251
  ## `--json` flag
234
252
 
235
253
  Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
@@ -267,12 +285,14 @@ src/atlassian_cli/
267
285
  jira.py Jira CLI entry point (subparsers)
268
286
  jira_issues.py Jira issue commands (v3 API)
269
287
  jira_assets.py Jira Assets commands (Assets v1 API)
288
+ org.py Org admin commands (admin v1 API)
270
289
  ```
271
290
 
272
291
  **APIs used:**
273
292
  - Confluence Cloud REST API v2 (`/wiki/api/v2/`)
274
293
  - Jira Cloud REST API v3 (`/rest/api/3/`)
275
294
  - Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
295
+ - Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
276
296
 
277
297
  ## Development
278
298
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "atlassian-cli"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -45,6 +45,7 @@ Issues = "https://github.com/catapultcx/atlassian-cli/issues"
45
45
  [project.scripts]
46
46
  confluence = "atlassian_cli.confluence:main"
47
47
  jira = "atlassian_cli.jira:main"
48
+ org = "atlassian_cli.org:main"
48
49
 
49
50
  [tool.setuptools.packages.find]
50
51
  where = ["src"]
@@ -77,3 +77,27 @@ def setup():
77
77
  url, email, token = get_config()
78
78
  session = get_session(email, token)
79
79
  return session, url
80
+
81
+
82
+ ORG_API = 'https://api.atlassian.com/admin/v1/orgs'
83
+
84
+
85
+ def get_org_config(org_id=None):
86
+ env = load_env()
87
+ api_key = env.get('ATLASSIAN_ORG_API_KEY', os.environ.get('ATLASSIAN_ORG_API_KEY'))
88
+ org_id = org_id or env.get('ATLASSIAN_ORG_ID', os.environ.get('ATLASSIAN_ORG_ID'))
89
+ if not api_key:
90
+ sys.stderr.write('ERR Missing ATLASSIAN_ORG_API_KEY (organisation admin API key)\n'
91
+ 'Set it in .env or as an environment variable.\n')
92
+ sys.exit(1)
93
+ if not org_id:
94
+ sys.stderr.write('ERR Missing ATLASSIAN_ORG_ID: set it in .env or pass --org-id.\n')
95
+ sys.exit(1)
96
+ return api_key.strip(), org_id.strip()
97
+
98
+
99
+ def org_setup(org_id=None):
100
+ api_key, org_id = get_org_config(org_id)
101
+ session = requests.Session()
102
+ session.headers.update({'Authorization': f'Bearer {api_key}', 'Accept': 'application/json'})
103
+ return session, f'{ORG_API}/{org_id}'
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Atlassian organisation admin CLI: audit log events, managed users, event action types.
3
+
4
+ Uses the organisation admin API (api.atlassian.com/admin/v1/orgs/{orgId}) with an org API key,
5
+ which is separate from the site credentials the confluence and jira commands use.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+
12
+ from atlassian_cli.config import org_setup
13
+ from atlassian_cli.http import APIError, _retry
14
+ from atlassian_cli.output import emit, emit_error, is_json_mode, set_json_mode
15
+
16
+
17
+ def _get(session, url, params=None):
18
+ response = _retry(session.get, url, params=params)
19
+ if response.ok:
20
+ return response.json()
21
+ raise APIError(response.status_code, response.text)
22
+
23
+
24
+ def fetch_events(session, base, start_ms, end_ms):
25
+ # events-stream drops out of the window if a cursor is sent without from/to.
26
+ window = {'from': start_ms, 'to': end_ms}
27
+ params = dict(window)
28
+ events = []
29
+ while True:
30
+ data = _get(session, f'{base}/events-stream', params)
31
+ batch = data.get('data') or []
32
+ events.extend(batch)
33
+ cursor = (data.get('meta') or {}).get('next')
34
+ if not batch or not cursor:
35
+ return events
36
+ params = dict(window, cursor=cursor)
37
+
38
+
39
+ def fetch_list(session, url):
40
+ items = []
41
+ while url:
42
+ data = _get(session, url)
43
+ items.extend(data.get('data') or [])
44
+ url = (data.get('links') or {}).get('next')
45
+ return items
46
+
47
+
48
+ def _finish(args, key, items, line):
49
+ if args.dump:
50
+ with open(args.dump, 'w') as fh:
51
+ json.dump({'total': len(items), key: items}, fh, indent=2)
52
+ elif not is_json_mode():
53
+ for item in items:
54
+ print(line(item))
55
+ emit('DONE', f'{len(items)} {key}', data={'total': len(items)})
56
+
57
+
58
+ def cmd_events(args):
59
+ session, base = org_setup(args.org_id)
60
+ events = fetch_events(session, base, args.start_ms, args.end_ms)
61
+ _finish(args, 'events', events, lambda e: ' '.join([
62
+ e['attributes'].get('time', ''), e['attributes'].get('action', ''),
63
+ (e['attributes'].get('actor') or {}).get('name', '')]))
64
+
65
+
66
+ def cmd_users(args):
67
+ session, base = org_setup(args.org_id)
68
+ users = fetch_list(session, f'{base}/users')
69
+ _finish(args, 'users', users,
70
+ lambda u: f"{u.get('account_id', '')} {u.get('email', '')} {u.get('last_active', '')}")
71
+
72
+
73
+ def cmd_event_actions(args):
74
+ session, base = org_setup(args.org_id)
75
+ actions = fetch_list(session, f'{base}/event-actions')
76
+ _finish(args, 'actions', actions, lambda a: a.get('id', ''))
77
+
78
+
79
+ def main():
80
+ from atlassian_cli.update_check import check_for_update
81
+ check_for_update()
82
+
83
+ if '--json' in sys.argv:
84
+ sys.argv.remove('--json')
85
+ set_json_mode(True)
86
+
87
+ common = argparse.ArgumentParser(add_help=False)
88
+ common.add_argument('--org-id', help='Organisation id (default: ATLASSIAN_ORG_ID from config)')
89
+ common.add_argument('--dump', metavar='FILE', help='Save the full data to a JSON file')
90
+
91
+ parser = argparse.ArgumentParser(prog='org',
92
+ description='Atlassian organisation admin: audit events, users, event actions')
93
+ sub = parser.add_subparsers(dest='command', required=True)
94
+
95
+ p = sub.add_parser('events', parents=[common], help='Audit log events in a time window')
96
+ p.add_argument('--from', dest='start_ms', type=int, required=True, help='Window start, epoch milliseconds')
97
+ p.add_argument('--to', dest='end_ms', type=int, required=True, help='Window end, epoch milliseconds')
98
+ p.set_defaults(func=cmd_events)
99
+
100
+ p = sub.add_parser('users', parents=[common], help='Managed accounts with last-active dates')
101
+ p.set_defaults(func=cmd_users)
102
+
103
+ p = sub.add_parser('event-actions', parents=[common], help='Every audit event action type')
104
+ p.set_defaults(func=cmd_event_actions)
105
+
106
+ args = parser.parse_args()
107
+ try:
108
+ args.func(args)
109
+ except APIError as e:
110
+ emit_error(str(e))
111
+ sys.exit(1)
112
+ except KeyboardInterrupt:
113
+ sys.exit(130)
114
+
115
+
116
+ if __name__ == '__main__':
117
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlassian-cli
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Fast CLI tools for Atlassian Cloud (Confluence + Jira) — optimized for AI agents
5
5
  Author-email: Alex Fishlock <alex.fishlock@catapult.cx>
6
6
  License-Expression: MIT
@@ -85,6 +85,12 @@ Get your API token at https://id.atlassian.com/manage-profile/security/api-token
85
85
 
86
86
  > Legacy `CONFLUENCE_URL` / `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` are also supported.
87
87
 
88
+ Organisation admin commands (`org`) use a separate org API key, read from the same config file or env:
89
+ ```
90
+ ATLASSIAN_ORG_API_KEY=ATCTT... # admin.atlassian.com > org > Settings > API keys (personal ATATT tokens get 401)
91
+ ATLASSIAN_ORG_ID=your-org-id # or pass --org-id
92
+ ```
93
+
88
94
  ## Confluence CLI
89
95
 
90
96
  Manages Confluence pages as local JSON files in ADF (Atlassian Document Format). No markdown — ADF preserves every macro, panel, and table perfectly.
@@ -261,6 +267,18 @@ jira assets delete 123
261
267
  jira assets type-create 1 "Network Device" --description "Switches and routers"
262
268
  ```
263
269
 
270
+ ## Organisation admin CLI
271
+
272
+ Read-only access to the org admin API: audit log, managed users, audit action types.
273
+
274
+ ```bash
275
+ org events --from <epoch ms> --to <epoch ms> [--org-id ID] [--dump FILE] # org audit log events
276
+ org users [--org-id ID] [--dump FILE] # managed accounts with last_active
277
+ org event-actions [--org-id ID] [--dump FILE] # every audit event action type
278
+ ```
279
+
280
+ `--dump` writes `{"total": N, "<key>": [...]}`. Audit retention is about 180 days.
281
+
264
282
  ## `--json` flag
265
283
 
266
284
  Both CLIs accept a global `--json` flag that switches all output to machine-readable JSON. Perfect for piping into `jq` or parsing from code.
@@ -298,12 +316,14 @@ src/atlassian_cli/
298
316
  jira.py Jira CLI entry point (subparsers)
299
317
  jira_issues.py Jira issue commands (v3 API)
300
318
  jira_assets.py Jira Assets commands (Assets v1 API)
319
+ org.py Org admin commands (admin v1 API)
301
320
  ```
302
321
 
303
322
  **APIs used:**
304
323
  - Confluence Cloud REST API v2 (`/wiki/api/v2/`)
305
324
  - Jira Cloud REST API v3 (`/rest/api/3/`)
306
325
  - Jira Assets REST API v1 (`api.atlassian.com/jsm/assets/workspace/{id}/v1`)
326
+ - Organisation admin API v1 (`api.atlassian.com/admin/v1/orgs/{orgId}`)
307
327
 
308
328
  ## Development
309
329
 
@@ -10,6 +10,7 @@ src/atlassian_cli/http.py
10
10
  src/atlassian_cli/jira.py
11
11
  src/atlassian_cli/jira_assets.py
12
12
  src/atlassian_cli/jira_issues.py
13
+ src/atlassian_cli/org.py
13
14
  src/atlassian_cli/output.py
14
15
  src/atlassian_cli/update_check.py
15
16
  src/atlassian_cli.egg-info/PKG-INFO
@@ -24,4 +25,5 @@ tests/test_confluence.py
24
25
  tests/test_http.py
25
26
  tests/test_jira_assets.py
26
27
  tests/test_jira_issues.py
28
+ tests/test_org.py
27
29
  tests/test_output.py
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  confluence = atlassian_cli.confluence:main
3
3
  jira = atlassian_cli.jira:main
4
+ org = atlassian_cli.org:main
@@ -2,7 +2,7 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from atlassian_cli.config import get_config, get_session, load_env, setup
5
+ from atlassian_cli.config import get_config, get_org_config, get_session, load_env, org_setup, setup
6
6
 
7
7
 
8
8
  class TestLoadEnv:
@@ -130,3 +130,45 @@ class TestSetup:
130
130
  session, base = setup()
131
131
  assert base == "https://test.atlassian.net"
132
132
  assert session.auth is not None
133
+
134
+
135
+ class TestGetOrgConfig:
136
+ @pytest.fixture(autouse=True)
137
+ def _isolate(self, monkeypatch, tmp_path):
138
+ monkeypatch.chdir(tmp_path)
139
+ monkeypatch.setenv("HOME", str(tmp_path))
140
+ monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
141
+ monkeypatch.delenv("ATLASSIAN_CLI_CONFIG", raising=False)
142
+ monkeypatch.delenv("ATLASSIAN_ORG_API_KEY", raising=False)
143
+ monkeypatch.delenv("ATLASSIAN_ORG_ID", raising=False)
144
+
145
+ def test_reads_key_and_org_id_from_config(self, tmp_path):
146
+ (tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\nATLASSIAN_ORG_ID=org-1\n")
147
+ assert get_org_config() == ("ATCTT-k", "org-1")
148
+
149
+ def test_explicit_org_id_wins(self, tmp_path):
150
+ (tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\nATLASSIAN_ORG_ID=org-1\n")
151
+ assert get_org_config("org-2") == ("ATCTT-k", "org-2")
152
+
153
+ def test_env_var_fallback(self, monkeypatch):
154
+ monkeypatch.setenv("ATLASSIAN_ORG_API_KEY", "ATCTT-e")
155
+ monkeypatch.setenv("ATLASSIAN_ORG_ID", "org-e")
156
+ assert get_org_config() == ("ATCTT-e", "org-e")
157
+
158
+ def test_missing_key_exits_naming_it(self, capsys):
159
+ with pytest.raises(SystemExit):
160
+ get_org_config("org-1")
161
+ assert "ATLASSIAN_ORG_API_KEY" in capsys.readouterr().err
162
+
163
+ def test_missing_org_id_exits_naming_it(self, tmp_path, capsys):
164
+ (tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\n")
165
+ with pytest.raises(SystemExit):
166
+ get_org_config()
167
+ assert "ATLASSIAN_ORG_ID" in capsys.readouterr().err
168
+
169
+ def test_org_setup_uses_bearer_auth(self, tmp_path):
170
+ (tmp_path / ".env").write_text("ATLASSIAN_ORG_API_KEY=ATCTT-k\n")
171
+ session, base = org_setup("org-1")
172
+ assert session.headers["Authorization"] == "Bearer ATCTT-k"
173
+ assert session.auth is None
174
+ assert base == "https://api.atlassian.com/admin/v1/orgs/org-1"
@@ -0,0 +1,95 @@
1
+ """Tests for atlassian_cli.org."""
2
+
3
+ import json
4
+ from argparse import Namespace
5
+ from urllib.parse import parse_qsl, urlparse
6
+
7
+ import pytest
8
+
9
+ from atlassian_cli import org
10
+ from atlassian_cli.http import APIError
11
+ from atlassian_cli.output import set_json_mode
12
+
13
+ ORG_BASE = "https://api.atlassian.com/admin/v1/orgs/org-1"
14
+ EVENTS = f"{ORG_BASE}/events-stream"
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _patch_setup(monkeypatch, mock_session):
19
+ monkeypatch.setattr("atlassian_cli.org.org_setup", lambda org_id=None: (mock_session, ORG_BASE))
20
+ set_json_mode(False)
21
+
22
+
23
+ @pytest.fixture
24
+ def slept(monkeypatch):
25
+ calls = []
26
+ monkeypatch.setattr("atlassian_cli.http.time.sleep", calls.append)
27
+ return calls
28
+
29
+
30
+ def _params(call):
31
+ return dict(parse_qsl(urlparse(call.request.url).query))
32
+
33
+
34
+ class TestFetchEvents:
35
+ def test_pages_by_cursor_keeping_the_window(self, mock_session, mocked_responses):
36
+ mocked_responses.get(EVENTS, json={"data": [{"id": "1"}, {"id": "2"}], "meta": {"next": "c1"}})
37
+ mocked_responses.get(EVENTS, json={"data": [{"id": "3"}], "meta": {"next": "c2"}})
38
+ mocked_responses.get(EVENTS, json={"data": [], "meta": {}})
39
+ events = org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
40
+ assert [e["id"] for e in events] == ["1", "2", "3"]
41
+ params = [_params(c) for c in mocked_responses.calls]
42
+ assert all(p["from"] == "1000" and p["to"] == "2000" for p in params)
43
+ assert [p.get("cursor") for p in params] == [None, "c1", "c2"]
44
+
45
+ def test_retries_429_with_the_same_request(self, mock_session, mocked_responses, slept):
46
+ mocked_responses.get(EVENTS, json={"data": [{"id": "1"}], "meta": {"next": "c1"}})
47
+ mocked_responses.get(EVENTS, status=429, headers={"Retry-After": "3"})
48
+ mocked_responses.get(EVENTS, json={"data": [{"id": "2"}], "meta": {}})
49
+ events = org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
50
+ assert [e["id"] for e in events] == ["1", "2"]
51
+ params = [_params(c) for c in mocked_responses.calls]
52
+ assert [p.get("cursor") for p in params] == [None, "c1", "c1"]
53
+ assert all(p["from"] == "1000" and p["to"] == "2000" for p in params)
54
+ assert len(slept) == 1 and 2.1 <= slept[0] <= 3.9
55
+
56
+ def test_http_error_raises(self, mock_session, mocked_responses):
57
+ mocked_responses.get(EVENTS, status=401, body="unauthorised")
58
+ with pytest.raises(APIError):
59
+ org.fetch_events(mock_session, ORG_BASE, 1000, 2000)
60
+
61
+
62
+ class TestFetchList:
63
+ def test_follows_absolute_next_links(self, mock_session, mocked_responses):
64
+ mocked_responses.get(f"{ORG_BASE}/users", json={"data": [{"account_id": "a"}], "links": {"next": "https://n.example/p2"}})
65
+ mocked_responses.get("https://n.example/p2", json={"data": [{"account_id": "b"}], "links": {}})
66
+ assert [u["account_id"] for u in org.fetch_list(mock_session, f"{ORG_BASE}/users")] == ["a", "b"]
67
+
68
+
69
+ class TestCommands:
70
+ def test_events_writes_the_dump(self, mocked_responses, tmp_path):
71
+ mocked_responses.get(EVENTS, json={"data": [{"id": "1"}], "meta": {}})
72
+ dump = tmp_path / "events.json"
73
+ org.cmd_events(Namespace(org_id="org-1", start_ms=1000, end_ms=2000, dump=str(dump)))
74
+ assert json.loads(dump.read_text()) == {"total": 1, "events": [{"id": "1"}]}
75
+
76
+ def test_users_writes_the_dump(self, mocked_responses, tmp_path):
77
+ mocked_responses.get(f"{ORG_BASE}/users", json={"data": [{"account_id": "a", "last_active": "x"}], "links": {}})
78
+ dump = tmp_path / "users.json"
79
+ org.cmd_users(Namespace(org_id=None, dump=str(dump)))
80
+ assert json.loads(dump.read_text())["users"] == [{"account_id": "a", "last_active": "x"}]
81
+
82
+ def test_event_actions_writes_the_dump(self, mocked_responses, tmp_path):
83
+ mocked_responses.get(f"{ORG_BASE}/event-actions", json={"data": [{"id": "x"}], "links": {}})
84
+ dump = tmp_path / "actions.json"
85
+ org.cmd_event_actions(Namespace(org_id=None, dump=str(dump)))
86
+ assert json.loads(dump.read_text())["actions"] == [{"id": "x"}]
87
+
88
+ def test_main_parses_the_window_and_org_id(self, monkeypatch):
89
+ seen = []
90
+ monkeypatch.setattr("atlassian_cli.update_check.check_for_update", lambda: None)
91
+ monkeypatch.setattr("atlassian_cli.org.cmd_events", seen.append)
92
+ monkeypatch.setattr("sys.argv", ["org", "events", "--from", "1", "--to", "2",
93
+ "--org-id", "org-9", "--dump", "f.json", "--json"])
94
+ org.main()
95
+ assert (seen[0].start_ms, seen[0].end_ms, seen[0].org_id, seen[0].dump) == (1, 2, "org-9", "f.json")
File without changes
File without changes