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  }