github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/cmd/snap/cmd_buy_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main_test 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "net/http" 26 27 "gopkg.in/check.v1" 28 29 snap "github.com/snapcore/snapd/cmd/snap" 30 ) 31 32 type BuySnapSuite struct { 33 BaseSnapSuite 34 } 35 36 var _ = check.Suite(&BuySnapSuite{}) 37 38 type expectedURL struct { 39 Body string 40 Checker func(r *http.Request) 41 42 callCount int 43 } 44 45 type expectedMethod map[string]*expectedURL 46 47 type expectedMethods map[string]*expectedMethod 48 49 type buyTestMockSnapServer struct { 50 ExpectedMethods expectedMethods 51 52 Checker *check.C 53 } 54 55 func (s *buyTestMockSnapServer) serveHttp(w http.ResponseWriter, r *http.Request) { 56 method := s.ExpectedMethods[r.Method] 57 if method == nil || len(*method) == 0 { 58 s.Checker.Fatalf("unexpected HTTP method %s", r.Method) 59 } 60 61 url := (*method)[r.URL.Path] 62 if url == nil { 63 s.Checker.Fatalf("unexpected URL %q", r.URL.Path) 64 } 65 66 if url.Checker != nil { 67 url.Checker(r) 68 } 69 fmt.Fprintln(w, url.Body) 70 url.callCount++ 71 } 72 73 func (s *buyTestMockSnapServer) checkCounts() { 74 for _, method := range s.ExpectedMethods { 75 for _, url := range *method { 76 s.Checker.Check(url.callCount, check.Equals, 1) 77 } 78 } 79 } 80 81 func (s *BuySnapSuite) SetUpTest(c *check.C) { 82 s.BaseSnapSuite.SetUpTest(c) 83 s.Login(c) 84 } 85 86 func (s *BuySnapSuite) TearDownTest(c *check.C) { 87 s.Logout(c) 88 s.BaseSnapSuite.TearDownTest(c) 89 } 90 91 func (s *BuySnapSuite) TestBuyHelp(c *check.C) { 92 _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy"}) 93 c.Assert(err, check.NotNil) 94 c.Check(err.Error(), check.Equals, "the required argument `<snap>` was not provided") 95 c.Check(s.Stdout(), check.Equals, "") 96 c.Check(s.Stderr(), check.Equals, "") 97 } 98 99 func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) { 100 _, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "a:b"}) 101 c.Assert(err, check.NotNil) 102 c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") 103 c.Check(s.Stdout(), check.Equals, "") 104 c.Check(s.Stderr(), check.Equals, "") 105 106 _, err = snap.Parser(snap.Client()).ParseArgs([]string{"buy", "c*d"}) 107 c.Assert(err, check.NotNil) 108 c.Check(err.Error(), check.Equals, "cannot buy snap: invalid characters in name") 109 c.Check(s.Stdout(), check.Equals, "") 110 c.Check(s.Stderr(), check.Equals, "") 111 } 112 113 const buyFreeSnapFailsFindJson = ` 114 { 115 "type": "sync", 116 "status-code": 200, 117 "status": "OK", 118 "result": [ 119 { 120 "channel": "stable", 121 "confinement": "strict", 122 "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", 123 "developer": "canonical", 124 "publisher": { 125 "id": "canonical", 126 "username": "canonical", 127 "display-name": "Canonical", 128 "validation": "verified" 129 }, 130 "download-size": 65536, 131 "icon": "", 132 "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", 133 "name": "hello", 134 "private": false, 135 "resource": "/v2/snaps/hello", 136 "revision": "1", 137 "status": "available", 138 "summary": "GNU Hello, the \"hello world\" snap", 139 "type": "app", 140 "version": "2.10" 141 } 142 ], 143 "sources": [ 144 "store" 145 ], 146 "suggested-currency": "GBP" 147 } 148 ` 149 150 func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) { 151 mockServer := &buyTestMockSnapServer{ 152 ExpectedMethods: expectedMethods{ 153 "GET": &expectedMethod{ 154 "/v2/find": &expectedURL{ 155 Body: buyFreeSnapFailsFindJson, 156 }, 157 }, 158 }, 159 Checker: c, 160 } 161 defer mockServer.checkCounts() 162 s.RedirectClientToTestServer(mockServer.serveHttp) 163 164 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 165 c.Assert(err, check.NotNil) 166 c.Check(err.Error(), check.Equals, "cannot buy snap: snap is free") 167 c.Assert(rest, check.DeepEquals, []string{"hello"}) 168 c.Check(s.Stdout(), check.Equals, "") 169 c.Check(s.Stderr(), check.Equals, "") 170 } 171 172 const buySnapFindJson = ` 173 { 174 "type": "sync", 175 "status-code": 200, 176 "status": "OK", 177 "result": [ 178 { 179 "channel": "stable", 180 "confinement": "strict", 181 "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", 182 "developer": "canonical", 183 "publisher": { 184 "id": "canonical", 185 "username": "canonical", 186 "display-name": "Canonical", 187 "validation": "verified" 188 }, 189 "download-size": 65536, 190 "icon": "", 191 "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", 192 "name": "hello", 193 "private": false, 194 "resource": "/v2/snaps/hello", 195 "revision": "1", 196 "status": "priced", 197 "summary": "GNU Hello, the \"hello world\" snap", 198 "type": "app", 199 "version": "2.10", 200 "prices": {"USD": 3.99, "GBP": 2.99} 201 } 202 ], 203 "sources": [ 204 "store" 205 ], 206 "suggested-currency": "GBP" 207 } 208 ` 209 210 func buySnapFindURL(c *check.C) *expectedURL { 211 return &expectedURL{ 212 Body: buySnapFindJson, 213 Checker: func(r *http.Request) { 214 c.Check(r.URL.Query().Get("name"), check.Equals, "hello") 215 }, 216 } 217 } 218 219 const buyReadyJson = ` 220 { 221 "type": "sync", 222 "status-code": 200, 223 "status": "OK", 224 "result": true, 225 "sources": [ 226 "store" 227 ], 228 "suggested-currency": "GBP" 229 } 230 ` 231 232 func buyReady(c *check.C) *expectedURL { 233 return &expectedURL{ 234 Body: buyReadyJson, 235 } 236 } 237 238 const buySnapJson = ` 239 { 240 "type": "sync", 241 "status-code": 200, 242 "status": "OK", 243 "result": { 244 "state": "Complete" 245 }, 246 "sources": [ 247 "store" 248 ], 249 "suggested-currency": "GBP" 250 } 251 ` 252 253 const loginJson = ` 254 { 255 "type": "sync", 256 "status-code": 200, 257 "status": "OK", 258 "result": { 259 "id": 1, 260 "username": "username", 261 "email": "hello@mail.com", 262 "macaroon": "1234abcd", 263 "discharges": ["a", "b", "c"] 264 }, 265 "sources": [ 266 "store" 267 ] 268 } 269 ` 270 271 func (s *BuySnapSuite) TestBuySnapSuccess(c *check.C) { 272 mockServer := &buyTestMockSnapServer{ 273 ExpectedMethods: expectedMethods{ 274 "GET": &expectedMethod{ 275 "/v2/find": buySnapFindURL(c), 276 "/v2/buy/ready": buyReady(c), 277 }, 278 "POST": &expectedMethod{ 279 "/v2/login": &expectedURL{ 280 Body: loginJson, 281 }, 282 "/v2/buy": &expectedURL{ 283 Body: buySnapJson, 284 Checker: func(r *http.Request) { 285 var postData struct { 286 SnapID string `json:"snap-id"` 287 Price float64 `json:"price"` 288 Currency string `json:"currency"` 289 } 290 decoder := json.NewDecoder(r.Body) 291 err := decoder.Decode(&postData) 292 c.Assert(err, check.IsNil) 293 294 c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") 295 c.Check(postData.Price, check.Equals, 2.99) 296 c.Check(postData.Currency, check.Equals, "GBP") 297 }, 298 }, 299 }, 300 }, 301 Checker: c, 302 } 303 defer mockServer.checkCounts() 304 s.RedirectClientToTestServer(mockServer.serveHttp) 305 306 // Confirm the purchase. 307 s.password = "the password" 308 309 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 310 c.Check(err, check.IsNil) 311 c.Check(rest, check.DeepEquals, []string{}) 312 c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" 313 for 2.99GBP. Press ctrl-c to cancel. 314 Password of "hello@mail.com": 315 Thanks for purchasing "hello". You may now install it on any of your devices 316 with 'snap install hello'. 317 `) 318 c.Check(s.Stderr(), check.Equals, "") 319 } 320 321 const buySnapPaymentDeclinedJson = ` 322 { 323 "type": "error", 324 "result": { 325 "message": "payment declined", 326 "kind": "payment-declined" 327 }, 328 "status-code": 400 329 } 330 ` 331 332 func (s *BuySnapSuite) TestBuySnapPaymentDeclined(c *check.C) { 333 mockServer := &buyTestMockSnapServer{ 334 ExpectedMethods: expectedMethods{ 335 "GET": &expectedMethod{ 336 "/v2/find": buySnapFindURL(c), 337 "/v2/buy/ready": buyReady(c), 338 }, 339 "POST": &expectedMethod{ 340 "/v2/login": &expectedURL{ 341 Body: loginJson, 342 }, 343 "/v2/buy": &expectedURL{ 344 Body: buySnapPaymentDeclinedJson, 345 Checker: func(r *http.Request) { 346 var postData struct { 347 SnapID string `json:"snap-id"` 348 Price float64 `json:"price"` 349 Currency string `json:"currency"` 350 } 351 decoder := json.NewDecoder(r.Body) 352 err := decoder.Decode(&postData) 353 c.Assert(err, check.IsNil) 354 355 c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") 356 c.Check(postData.Price, check.Equals, 2.99) 357 c.Check(postData.Currency, check.Equals, "GBP") 358 }, 359 }, 360 }, 361 }, 362 Checker: c, 363 } 364 defer mockServer.checkCounts() 365 s.RedirectClientToTestServer(mockServer.serveHttp) 366 367 // Confirm the purchase. 368 s.password = "the password" 369 370 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 371 c.Assert(err, check.NotNil) 372 c.Check(err.Error(), check.Equals, `Sorry, your payment method has been declined by the issuer. Please review your 373 payment details at https://my.ubuntu.com/payment/edit and try again.`) 374 c.Check(rest, check.DeepEquals, []string{"hello"}) 375 c.Check(s.Stdout(), check.Equals, `Please re-enter your Ubuntu One password to purchase "hello" from "canonical" 376 for 2.99GBP. Press ctrl-c to cancel. 377 Password of "hello@mail.com": 378 `) 379 c.Check(s.Stderr(), check.Equals, "") 380 } 381 382 const readyToBuyNoPaymentMethodJson = ` 383 { 384 "type": "error", 385 "result": { 386 "message": "no payment methods", 387 "kind": "no-payment-methods" 388 }, 389 "status-code": 400 390 }` 391 392 func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethod(c *check.C) { 393 mockServer := &buyTestMockSnapServer{ 394 ExpectedMethods: expectedMethods{ 395 "GET": &expectedMethod{ 396 "/v2/find": buySnapFindURL(c), 397 "/v2/buy/ready": &expectedURL{ 398 Body: readyToBuyNoPaymentMethodJson, 399 }, 400 }, 401 }, 402 Checker: c, 403 } 404 defer mockServer.checkCounts() 405 s.RedirectClientToTestServer(mockServer.serveHttp) 406 407 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 408 c.Assert(err, check.NotNil) 409 c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. 410 411 Once you’ve added your payment details, you just need to run 'snap buy hello' again.`) 412 c.Check(rest, check.DeepEquals, []string{"hello"}) 413 c.Check(s.Stdout(), check.Equals, "") 414 c.Check(s.Stderr(), check.Equals, "") 415 } 416 417 const readyToBuyNotAcceptedTermsJson = ` 418 { 419 "type": "error", 420 "result": { 421 "message": "terms of service not accepted", 422 "kind": "terms-not-accepted" 423 }, 424 "status-code": 400 425 }` 426 427 func (s *BuySnapSuite) TestBuySnapFailsNotAcceptedTerms(c *check.C) { 428 mockServer := &buyTestMockSnapServer{ 429 ExpectedMethods: expectedMethods{ 430 "GET": &expectedMethod{ 431 "/v2/find": buySnapFindURL(c), 432 "/v2/buy/ready": &expectedURL{ 433 Body: readyToBuyNotAcceptedTermsJson, 434 }, 435 }, 436 }, 437 Checker: c, 438 } 439 defer mockServer.checkCounts() 440 s.RedirectClientToTestServer(mockServer.serveHttp) 441 442 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 443 c.Assert(err, check.NotNil) 444 c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. 445 446 Once completed, return here and run 'snap buy hello' again.`) 447 c.Check(rest, check.DeepEquals, []string{"hello"}) 448 c.Check(s.Stdout(), check.Equals, "") 449 c.Check(s.Stderr(), check.Equals, "") 450 } 451 452 func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) { 453 // We don't login here 454 s.Logout(c) 455 456 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"buy", "hello"}) 457 c.Check(err, check.NotNil) 458 c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.") 459 c.Check(rest, check.DeepEquals, []string{"hello"}) 460 c.Check(s.Stdout(), check.Equals, "") 461 c.Check(s.Stderr(), check.Equals, "") 462 }