Coverage for webapp/publisher/snaps/views.py: 72%

228 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-05 22:06 +0000

1# Packages 

2import bleach 

3import flask 

4from canonicalwebteam.store_api.dashboard import Dashboard 

5from canonicalwebteam.store_api.publishergw import PublisherGW 

6from canonicalwebteam.exceptions import ( 

7 StoreApiError, 

8 StoreApiResponseErrorList, 

9 StoreApiResponseError, 

10 StoreApiResourceNotFound, 

11) 

12from flask.json import jsonify 

13from talisker import logging 

14 

15# Local 

16from webapp import authentication 

17from webapp.helpers import api_publisher_session, launchpad 

18from webapp.api.exceptions import ApiError 

19from webapp.decorators import exchange_required, login_required 

20from webapp.publisher.cve import cve_views 

21from webapp.publisher.snaps import ( 

22 build_views, 

23 listing_views, 

24 logic, 

25 metrics_views, 

26 publicise_views, 

27 release_views, 

28 settings_views, 

29 collaboration_views, 

30) 

31from webapp.publisher.snaps.builds import map_snap_build_status 

32 

33dashboard = Dashboard(api_publisher_session) 

34publisher_gateway = PublisherGW("snap", api_publisher_session) 

35 

36 

37publisher_snaps = flask.Blueprint( 

38 "publisher_snaps", 

39 __name__, 

40 template_folder="/templates", 

41 static_folder="/static", 

42) 

43 

44# Listing views 

45publisher_snaps.add_url_rule( 

46 "/account/snaps/<snap_name>/market", 

47 view_func=listing_views.get_market_snap, 

48) 

49publisher_snaps.add_url_rule( 

50 "/account/snaps/<snap_name>/listing", 

51 view_func=listing_views.get_market_snap, 

52 methods=["GET"], 

53) 

54publisher_snaps.add_url_rule( 

55 "/account/snaps/<snap_name>/listing", 

56 view_func=listing_views.redirect_post_market_snap, 

57 methods=["POST"], 

58) 

59publisher_snaps.add_url_rule( 

60 "/<snap_name>/listing", 

61 view_func=listing_views.get_listing_snap, 

62 methods=["GET"], 

63) 

64publisher_snaps.add_url_rule( 

65 "/api/<snap_name>/listing", 

66 view_func=listing_views.get_listing_data, 

67 methods=["GET"], 

68) 

69publisher_snaps.add_url_rule( 

70 "/api/<snap_name>/listing", 

71 view_func=listing_views.post_listing_data, 

72 methods=["POST"], 

73) 

74publisher_snaps.add_url_rule( 

75 "/<snap_name>/preview", 

76 view_func=listing_views.post_preview, 

77 methods=["POST"], 

78) 

79publisher_snaps.add_url_rule( 

80 "/<snap_name>/collaboration", 

81 view_func=collaboration_views.get_collaboration_snap, 

82 methods=["GET"], 

83) 

84 

85# Build views 

86publisher_snaps.add_url_rule( 

87 "/<snap_name>/builds", 

88 view_func=build_views.get_snap_builds_page, 

89 methods=["GET"], 

90), 

91 

92publisher_snaps.add_url_rule( 

93 "/<snap_name>/builds/<build_id>", 

94 view_func=build_views.get_snap_build_page, 

95 methods=["GET"], 

96), 

97 

98publisher_snaps.add_url_rule( 

99 "/api/<snap_name>/repo", 

100 view_func=build_views.get_snap_repo, 

101 methods=["GET"], 

102) 

103publisher_snaps.add_url_rule( 

104 "/api/<snap_name>/builds", 

105 view_func=build_views.get_snap_builds, 

106 methods=["GET"], 

107) 

108publisher_snaps.add_url_rule( 

109 "/api/<snap_name>/builds", 

110 view_func=build_views.post_snap_builds, 

111 methods=["POST"], 

112) 

113publisher_snaps.add_url_rule( 

114 "/api/<snap_name>/builds/<build_id>", 

115 view_func=build_views.get_snap_build, 

116 methods=["GET"], 

117) 

118publisher_snaps.add_url_rule( 

119 "/api/<snap_name>/builds/validate-repo", 

120 view_func=build_views.get_validate_repo, 

121 methods=["GET"], 

122) 

123publisher_snaps.add_url_rule( 

124 "/api/<snap_name>/builds/trigger-build", 

125 view_func=build_views.post_build, 

126 methods=["POST"], 

127) 

128publisher_snaps.add_url_rule( 

129 "/api/<snap_name>/builds/check-build-request/<build_id>", 

130 view_func=build_views.check_build_request, 

131 methods=["GET"], 

132) 

133publisher_snaps.add_url_rule( 

134 "/api/<snap_name>/webhook/notify", 

135 view_func=build_views.post_github_webhook, 

136 methods=["POST"], 

137) 

138# This route is to support previous webhooks from build.snapcraft.io 

139publisher_snaps.add_url_rule( 

140 "/api/<github_owner>/<github_repo>/webhook/notify", 

141 view_func=build_views.post_github_webhook, 

142 methods=["POST"], 

143) 

144publisher_snaps.add_url_rule( 

145 "/api/<snap_name>/builds/update-webhook", 

146 view_func=build_views.get_update_gh_webhooks, 

147 methods=["GET"], 

148) 

149publisher_snaps.add_url_rule( 

150 "/api/<snap_name>/builds/disconnect/", 

151 view_func=build_views.post_disconnect_repo, 

152 methods=["POST"], 

153) 

154 

155# Release views 

156publisher_snaps.add_url_rule( 

157 "/account/snaps/<snap_name>/release", 

158 view_func=release_views.redirect_get_release_history, 

159) 

160publisher_snaps.add_url_rule( 

161 "/<snap_name>/release", 

162 view_func=release_views.redirect_get_release_history, 

163) 

164publisher_snaps.add_url_rule( 

165 "/<snap_name>/releases", 

166 view_func=release_views.get_releases, 

167 methods=["GET"], 

168) 

169publisher_snaps.add_url_rule( 

170 "/api/<snap_name>/releases", 

171 view_func=release_views.get_release_history_data, 

172 methods=["GET"], 

173) 

174publisher_snaps.add_url_rule( 

175 "/account/snaps/<snap_name>/release", 

176 view_func=release_views.redirect_post_release, 

177 methods=["POST"], 

178) 

179publisher_snaps.add_url_rule( 

180 "/<snap_name>/release", 

181 view_func=release_views.redirect_post_release, 

182 methods=["POST"], 

183) 

184publisher_snaps.add_url_rule( 

185 "/<snap_name>/releases/json", 

186 view_func=release_views.get_release_history_json, 

187) 

188publisher_snaps.add_url_rule( 

189 "/<snap_name>/releases", 

190 view_func=release_views.post_release, 

191 methods=["POST"], 

192) 

193publisher_snaps.add_url_rule( 

194 "/<snap_name>/release/close-channel", 

195 view_func=release_views.redirect_post_close_channel, 

196 methods=["POST"], 

197) 

198publisher_snaps.add_url_rule( 

199 "/<snap_name>/releases/close-channel", 

200 view_func=release_views.post_close_channel, 

201 methods=["POST"], 

202) 

203publisher_snaps.add_url_rule( 

204 "/<snap_name>/releases/default-track", 

205 view_func=release_views.post_default_track, 

206 methods=["POST"], 

207) 

208publisher_snaps.add_url_rule( 

209 "/<snap_name>/releases/revision/<revision>", 

210 view_func=release_views.get_snap_revision_json, 

211) 

212 

213# Metrics views 

214publisher_snaps.add_url_rule( 

215 "/snaps/metrics/json", 

216 view_func=metrics_views.get_account_snaps_metrics, 

217 methods=["POST"], 

218) 

219publisher_snaps.add_url_rule( 

220 "/account/snaps/<snap_name>/measure", 

221 view_func=metrics_views.get_measure_snap, 

222) 

223publisher_snaps.add_url_rule( 

224 "/account/snaps/<snap_name>/metrics", 

225 view_func=metrics_views.get_measure_snap, 

226) 

227publisher_snaps.add_url_rule( 

228 "/<snap_name>/metrics", 

229 view_func=metrics_views.publisher_snap_metrics, 

230) 

231 

232publisher_snaps.add_url_rule( 

233 "/<snap_name>/metrics/active-devices", 

234 view_func=metrics_views.get_active_devices, 

235) 

236 

237publisher_snaps.add_url_rule( 

238 "/<snap_name>/metrics/active-latest-devices", 

239 view_func=metrics_views.get_latest_active_devices, 

240) 

241 

242publisher_snaps.add_url_rule( 

243 "/<snap_name>/metrics/active-device-annotation", 

244 view_func=metrics_views.get_metric_annotaion, 

245) 

246 

247publisher_snaps.add_url_rule( 

248 "/<snap_name>/metrics/country-metric", 

249 view_func=metrics_views.get_country_metric, 

250) 

251 

252# Publice views 

253publisher_snaps.add_url_rule( 

254 "/<snap_name>/publicise", 

255 view_func=publicise_views.get_publicise, 

256) 

257publisher_snaps.add_url_rule( 

258 "/<snap_name>/publicise/badges", 

259 view_func=publicise_views.get_publicise, 

260) 

261publisher_snaps.add_url_rule( 

262 "/<snap_name>/publicise/cards", 

263 view_func=publicise_views.get_publicise, 

264) 

265publisher_snaps.add_url_rule( 

266 "/api/<snap_name>/publicise", 

267 view_func=publicise_views.get_publicise_data, 

268) 

269 

270# Settings views 

271publisher_snaps.add_url_rule( 

272 "/<snap_name>/settings", 

273 view_func=settings_views.get_settings, 

274) 

275publisher_snaps.add_url_rule( 

276 "/api/<snap_name>/settings", 

277 view_func=settings_views.post_settings_data, 

278 methods=["POST"], 

279) 

280publisher_snaps.add_url_rule( 

281 "/api/<snap_name>/settings", 

282 view_func=settings_views.get_settings_data, 

283) 

284 

285# CVE API 

286publisher_snaps.add_url_rule( 

287 "/api/<snap_name>/<revision>/cves", 

288 view_func=cve_views.get_cves, 

289) 

290 

291publisher_snaps.add_url_rule( 

292 "/api/<snap_name>/cves", 

293 view_func=cve_views.get_revisions_with_cves, 

294) 

295 

296 

297@publisher_snaps.route("/account/snaps") 

298@login_required 

299def redirect_get_account_snaps(): 

300 return flask.redirect(flask.url_for(".get_account_snaps")) 

301 

302 

303@publisher_snaps.route("/snaps") 

304@login_required 

305def get_account_snaps(): 

306 account_info = dashboard.get_account(flask.session) 

307 

308 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

309 

310 flask_user = flask.session["publisher"] 

311 

312 context = { 

313 "snaps": user_snaps, 

314 "current_user": flask_user["nickname"], 

315 "registered_snaps": registered_snaps, 

316 } 

317 

318 return flask.render_template("store/publisher.html", **context) 

319 

320 

321@publisher_snaps.route("/snaps.json") 

322@login_required 

323def get_user_snaps(): 

324 account_info = dashboard.get_account(flask.session) 

325 

326 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

327 

328 flask_user = flask.session["publisher"] 

329 

330 return flask.jsonify( 

331 { 

332 "snaps": user_snaps, 

333 "current_user": flask_user["nickname"], 

334 "registered_snaps": registered_snaps, 

335 } 

336 ) 

337 

338 

339@publisher_snaps.route("/snap-builds.json") 

340@login_required 

341def get_snap_build_status(): 

342 try: 

343 account_info = dashboard.get_account(flask.session) 

344 except (StoreApiError, ApiError) as api_error: 

345 logging.getLogger("talisker.wsgi").error( 

346 "Error with session: %s", api_error 

347 ) 

348 

349 return flask.jsonify({"error": "An unexpected error occurred"}), 400 

350 

351 response = [] 

352 user_snaps, _ = logic.get_snaps_account_info(account_info) 

353 

354 for snap_name in user_snaps: 

355 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

356 status = map_snap_build_status(snap_build_statuses) 

357 

358 response.append({"name": snap_name, "status": status}) 

359 

360 return flask.jsonify(response) 

361 

362 

363@publisher_snaps.route("/account/register-snap") 

364def redirect_get_register_name(): 

365 return flask.redirect(flask.url_for(".get_register_name")) 

366 

367 

368@publisher_snaps.route("/register-snap") 

369@login_required 

370def get_register_name(): 

371 return flask.render_template("store/publisher.html") 

372 

373 

374@publisher_snaps.route("/account/register-snap", methods=["POST"]) 

375def redirect_post_register_name(): 

376 return flask.redirect(flask.url_for(".post_register_name"), 307) 

377 

378 

379@publisher_snaps.route("/api/register-snap", methods=["POST"]) 

380@login_required 

381def post_register_name(): 

382 snap_name = flask.request.form.get("snap_name") 

383 res = {} 

384 

385 if not snap_name: 

386 res["success"] = False 

387 res["message"] = "You must define a snap name" 

388 

389 return jsonify(res) 

390 

391 is_private = flask.request.form.get("is_private") == "private" 

392 store = flask.request.form.get("store") 

393 

394 try: 

395 dashboard.post_register_name( 

396 session=flask.session, 

397 snap_name=snap_name, 

398 registrant_comment=None, 

399 is_private=is_private, 

400 store=store, 

401 ) 

402 except StoreApiResponseErrorList as api_response_error_list: 

403 res = { 

404 "success": False, 

405 "data": { 

406 "is_private": is_private, 

407 "snap_name": snap_name, 

408 "store": store, 

409 }, 

410 } 

411 

412 if api_response_error_list.status_code == 409: 

413 for error in api_response_error_list.errors: 

414 res["data"]["error_code"] = error["code"] 

415 

416 return jsonify(res) 

417 

418 if api_response_error_list.status_code == 400: 

419 res["data"]["error_code"] = "no-permission" 

420 res[ 

421 "message" 

422 ] = """You don't have permission 

423 to register a snap in this store. 

424 Please see store administrator.""" 

425 

426 return jsonify(res) 

427 

428 res["message"] = "Unable to register snap name" 

429 res["data"] = { 

430 "snap_name": snap_name, 

431 "is_private": is_private, 

432 "store": store, 

433 } 

434 

435 return jsonify(res) 

436 

437 return jsonify({"success": True}) 

438 

439 

440@publisher_snaps.route("/api/packages/<snap_name>", methods=["GET"]) 

441@login_required 

442@exchange_required 

443def get_package_metadata(snap_name): 

444 try: 

445 package_metadata = publisher_gateway.get_package_metadata( 

446 flask.session, snap_name 

447 ) 

448 return jsonify({"data": package_metadata, "success": True}) 

449 except StoreApiResourceNotFound: 

450 return (jsonify({"error": "Package not found", "success": False}), 404) 

451 except StoreApiResponseErrorList as error: 

452 return ( 

453 jsonify( 

454 { 

455 "error": "Error occurred while fetching snap metadata.", 

456 "errors": error.errors, 

457 "success": False, 

458 } 

459 ), 

460 error.status_code, 

461 ) 

462 except StoreApiResponseError as error: 

463 return ( 

464 jsonify( 

465 { 

466 "error": "Error occurred while fetching snap metadata.", 

467 "success": False, 

468 } 

469 ), 

470 error.status_code, 

471 ) 

472 except StoreApiError: 

473 return ( 

474 jsonify( 

475 { 

476 "error": "Error occurred while fetching snap metadata.", 

477 "success": False, 

478 } 

479 ), 

480 500, 

481 ) 

482 except Exception: 

483 return (jsonify({"error": "Unexpected error", "success": False}), 500) 

484 

485 

486@publisher_snaps.route("/packages/<package_name>", methods=["DELETE"]) 

487@login_required 

488@exchange_required 

489def delete_package(package_name): 

490 response = publisher_gateway.unregister_package_name( 

491 flask.session, package_name 

492 ) 

493 

494 if response.status_code == 200: 

495 return ("", 200) 

496 return ( 

497 jsonify({"error": response.json()["error-list"][0]["message"]}), 

498 response.status_code, 

499 ) 

500 

501 

502@publisher_snaps.route("/snap_info/user_snap/<snap_name>", methods=["GET"]) 

503@login_required 

504def get_is_user_snap(snap_name): 

505 is_users_snap = False 

506 try: 

507 snap_info = dashboard.get_snap_info(flask.session, snap_name) 

508 except (StoreApiError, ApiError) as api_error: 

509 logging.getLogger("talisker.wsgi").error( 

510 "Error with session: %s", api_error 

511 ) 

512 

513 return flask.jsonify({"error": "An unexpected error occurred"}), 400 

514 

515 if authentication.is_authenticated(flask.session): 

516 publisher_info = flask.session.get("publisher", {}) 

517 if ( 

518 publisher_info.get("nickname") 

519 == snap_info["publisher"]["username"] 

520 ): 

521 is_users_snap = True 

522 

523 return {"is_users_snap": is_users_snap} 

524 

525 

526@publisher_snaps.route("/register-snap/json", methods=["POST"]) 

527@login_required 

528def post_register_name_json(): 

529 snap_name = flask.request.form.get("snap-name") 

530 

531 if not snap_name: 

532 return ( 

533 flask.jsonify({"errors": [{"message": "Snap name is required"}]}), 

534 400, 

535 ) 

536 

537 try: 

538 response = dashboard.post_register_name( 

539 session=flask.session, snap_name=snap_name 

540 ) 

541 except StoreApiResponseErrorList as api_response_error_list: 

542 for error in api_response_error_list.errors: 

543 # if snap name is already owned treat it as success 

544 if error["code"] == "already_owned": 

545 return flask.jsonify( 

546 {"code": error["code"], "snap_name": snap_name} 

547 ) 

548 return ( 

549 flask.jsonify({"errors": api_response_error_list.errors}), 

550 api_response_error_list.status_code, 

551 ) 

552 

553 response["code"] = "created" 

554 

555 return flask.jsonify(response) 

556 

557 

558@publisher_snaps.route("/register-name-dispute") 

559@login_required 

560def get_register_name_dispute(): 

561 snap_name = flask.request.args.get("snap-name") 

562 

563 if not snap_name: 

564 return flask.redirect( 

565 flask.url_for(".get_register_name", snap_name=snap_name) 

566 ) 

567 return flask.render_template( 

568 "store/publisher.html", 

569 ) 

570 

571 

572@publisher_snaps.route("/api/register-name-dispute", methods=["POST"]) 

573@login_required 

574def post_register_name_dispute(): 

575 try: 

576 claim = flask.json.loads(flask.request.data) 

577 snap_name = claim["snap-name"] 

578 claim_comment = claim["claim-comment"] 

579 dashboard.post_register_name_dispute( 

580 flask.session, 

581 bleach.clean(snap_name), 

582 bleach.clean(claim_comment), 

583 ) 

584 except StoreApiResponseErrorList as api_response_error_list: 

585 if api_response_error_list.status_code in [400, 409]: 

586 return jsonify( 

587 { 

588 "success": False, 

589 "data": api_response_error_list.errors, 

590 "message": api_response_error_list.errors[0]["message"], 

591 } 

592 ) 

593 return jsonify({"success": True}) 

594 

595 

596@publisher_snaps.route("/request-reserved-name") 

597@login_required 

598def get_request_reserved_name(): 

599 stores = dashboard.get_stores(flask.session) 

600 

601 snap_name = flask.request.args.get("snap_name") 

602 store_id = flask.request.args.get("store") 

603 store_name = logic.get_store_name(store_id, stores) 

604 

605 if not snap_name: 

606 return flask.redirect( 

607 flask.url_for( 

608 ".get_register_name", snap_name=snap_name, store=store_id 

609 ) 

610 ) 

611 return flask.render_template( 

612 "store/publisher.html", 

613 snap_name=snap_name, 

614 store=store_name, 

615 ) 

616 

617 

618@publisher_snaps.route("/snaps/api/snap-count") 

619@login_required 

620def snap_count(): 

621 account_info = dashboard.get_account(flask.session) 

622 

623 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

624 

625 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())} 

626 

627 return flask.jsonify(context)