github.com/cloudfoundry/cli@v7.1.0+incompatible/cf/api/applicationbits/application_bits_test.go (about)

     1  package applicationbits_test
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	testapi "code.cloudfoundry.org/cli/cf/api/apifakes"
    17  	"code.cloudfoundry.org/cli/cf/api/resources"
    18  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    19  	"code.cloudfoundry.org/cli/cf/net"
    20  	"code.cloudfoundry.org/cli/cf/terminal/terminalfakes"
    21  	testconfig "code.cloudfoundry.org/cli/cf/util/testhelpers/configuration"
    22  	testnet "code.cloudfoundry.org/cli/cf/util/testhelpers/net"
    23  
    24  	. "code.cloudfoundry.org/cli/cf/api/applicationbits"
    25  	"code.cloudfoundry.org/cli/cf/trace/tracefakes"
    26  	. "github.com/onsi/ginkgo"
    27  	. "github.com/onsi/gomega"
    28  )
    29  
    30  var _ = Describe("CloudControllerApplicationBitsRepository", func() {
    31  	var (
    32  		fixturesDir string
    33  		repo        Repository
    34  		file1       resources.AppFileResource
    35  		file2       resources.AppFileResource
    36  		file3       resources.AppFileResource
    37  		file4       resources.AppFileResource
    38  		testServer  *httptest.Server
    39  		configRepo  coreconfig.ReadWriter
    40  	)
    41  
    42  	BeforeEach(func() {
    43  		cwd, err := os.Getwd()
    44  		Expect(err).NotTo(HaveOccurred())
    45  		fixturesDir = filepath.Join(cwd, "../../../fixtures/applications")
    46  
    47  		configRepo = testconfig.NewRepositoryWithDefaults()
    48  
    49  		gateway := net.NewCloudControllerGateway(configRepo, time.Now, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
    50  		gateway.PollingThrottle = time.Duration(0)
    51  
    52  		repo = NewCloudControllerApplicationBitsRepository(configRepo, gateway)
    53  
    54  		file1 = resources.AppFileResource{Path: "app.rb", Sha1: "2474735f5163ba7612ef641f438f4b5bee00127b", Size: 51}
    55  		file2 = resources.AppFileResource{Path: "config.ru", Sha1: "f097424ce1fa66c6cb9f5e8a18c317376ec12e05", Size: 70}
    56  		file3 = resources.AppFileResource{Path: "Gemfile", Sha1: "d9c3a51de5c89c11331d3b90b972789f1a14699a", Size: 59, Mode: "0750"}
    57  		file4 = resources.AppFileResource{Path: "Gemfile.lock", Sha1: "345f999aef9070fb9a608e65cf221b7038156b6d", Size: 229, Mode: "0600"}
    58  	})
    59  
    60  	setupTestServer := func(reqs ...testnet.TestRequest) {
    61  		testServer, _ = testnet.NewServer(reqs)
    62  		configRepo.SetAPIEndpoint(testServer.URL)
    63  	}
    64  
    65  	Describe(".UploadBits", func() {
    66  		var uploadFile *os.File
    67  		var err error
    68  
    69  		BeforeEach(func() {
    70  			uploadFile, err = os.Open(filepath.Join(fixturesDir, "ignored_and_resource_matched_example_app.zip"))
    71  			if err != nil {
    72  				log.Fatal(err)
    73  			}
    74  		})
    75  
    76  		AfterEach(func() {
    77  			testServer.Close()
    78  		})
    79  
    80  		It("uploads zip files", func() {
    81  			setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{
    82  				Method:  "PUT",
    83  				Path:    "/v2/apps/my-cool-app-guid/bits",
    84  				Matcher: uploadBodyMatcher(defaultZipCheck),
    85  				Response: testnet.TestResponse{
    86  					Status: http.StatusCreated,
    87  					Body: `
    88  					{
    89  						"metadata":{
    90  							"guid": "my-job-guid",
    91  							"url": "/v2/jobs/my-job-guid"
    92  						}
    93  					}`,
    94  				},
    95  			}),
    96  				createProgressEndpoint("running"),
    97  				createProgressEndpoint("finished"),
    98  			)
    99  
   100  			apiErr := repo.UploadBits("my-cool-app-guid", uploadFile, []resources.AppFileResource{file1, file2})
   101  			Expect(apiErr).NotTo(HaveOccurred())
   102  		})
   103  
   104  		It("returns a failure when uploading bits fails", func() {
   105  			setupTestServer(testapi.NewCloudControllerTestRequest(testnet.TestRequest{
   106  				Method:  "PUT",
   107  				Path:    "/v2/apps/my-cool-app-guid/bits",
   108  				Matcher: uploadBodyMatcher(defaultZipCheck),
   109  				Response: testnet.TestResponse{
   110  					Status: http.StatusCreated,
   111  					Body: `
   112  					{
   113  						"metadata":{
   114  							"guid": "my-job-guid",
   115  							"url": "/v2/jobs/my-job-guid"
   116  						}
   117  					}`,
   118  				},
   119  			}),
   120  				createProgressEndpoint("running"),
   121  				createProgressEndpoint("failed"),
   122  			)
   123  			apiErr := repo.UploadBits("my-cool-app-guid", uploadFile, []resources.AppFileResource{file1, file2})
   124  
   125  			Expect(apiErr).To(HaveOccurred())
   126  		})
   127  
   128  		Context("when there are no files to upload", func() {
   129  			It("makes a request without a zipfile", func() {
   130  				setupTestServer(
   131  					testapi.NewCloudControllerTestRequest(testnet.TestRequest{
   132  						Method: "PUT",
   133  						Path:   "/v2/apps/my-cool-app-guid/bits",
   134  						Matcher: func(request *http.Request) {
   135  							err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes)
   136  							Expect(err).NotTo(HaveOccurred())
   137  							defer request.MultipartForm.RemoveAll()
   138  
   139  							Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value")
   140  							valuePart, ok := request.MultipartForm.Value["resources"]
   141  
   142  							Expect(ok).To(BeTrue(), "Resource manifest not present")
   143  							Expect(valuePart).To(Equal([]string{"[]"}))
   144  							Expect(request.MultipartForm.File).To(BeEmpty())
   145  						},
   146  						Response: testnet.TestResponse{
   147  							Status: http.StatusCreated,
   148  							Body: `
   149  					{
   150  						"metadata":{
   151  							"guid": "my-job-guid",
   152  							"url": "/v2/jobs/my-job-guid"
   153  						}
   154  					}`,
   155  						},
   156  					}),
   157  					createProgressEndpoint("running"),
   158  					createProgressEndpoint("finished"),
   159  				)
   160  
   161  				apiErr := repo.UploadBits("my-cool-app-guid", nil, []resources.AppFileResource{})
   162  				Expect(apiErr).NotTo(HaveOccurred())
   163  			})
   164  		})
   165  
   166  		It("marshals a nil presentFiles parameter into an empty array", func() {
   167  			setupTestServer(
   168  				testapi.NewCloudControllerTestRequest(testnet.TestRequest{
   169  					Method: "PUT",
   170  					Path:   "/v2/apps/my-cool-app-guid/bits",
   171  					Matcher: func(request *http.Request) {
   172  						err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes)
   173  						Expect(err).NotTo(HaveOccurred())
   174  						defer request.MultipartForm.RemoveAll()
   175  
   176  						Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value")
   177  						valuePart, ok := request.MultipartForm.Value["resources"]
   178  
   179  						Expect(ok).To(BeTrue(), "Resource manifest not present")
   180  						Expect(valuePart).To(Equal([]string{"[]"}))
   181  						Expect(request.MultipartForm.File).To(BeEmpty())
   182  					},
   183  					Response: testnet.TestResponse{
   184  						Status: http.StatusCreated,
   185  						Body: `
   186  					{
   187  						"metadata":{
   188  							"guid": "my-job-guid",
   189  							"url": "/v2/jobs/my-job-guid"
   190  						}
   191  					}`,
   192  					},
   193  				}),
   194  				createProgressEndpoint("running"),
   195  				createProgressEndpoint("finished"),
   196  			)
   197  
   198  			apiErr := repo.UploadBits("my-cool-app-guid", nil, nil)
   199  			Expect(apiErr).NotTo(HaveOccurred())
   200  		})
   201  	})
   202  
   203  	Describe(".GetApplicationFiles", func() {
   204  		It("accepts a slice of files and returns a slice of the files that it already has", func() {
   205  			setupTestServer(matchResourceRequest)
   206  			matchedFiles, err := repo.GetApplicationFiles([]resources.AppFileResource{file1, file2, file3, file4})
   207  			Expect(matchedFiles).To(Equal([]resources.AppFileResource{file3, file4}))
   208  			Expect(err).NotTo(HaveOccurred())
   209  		})
   210  
   211  		It("excludes files that were in the response but not in the request", func() {
   212  			setupTestServer(matchResourceRequestImbalanced)
   213  			matchedFiles, err := repo.GetApplicationFiles([]resources.AppFileResource{file1, file4})
   214  			Expect(matchedFiles).To(Equal([]resources.AppFileResource{file4}))
   215  			Expect(err).NotTo(HaveOccurred())
   216  		})
   217  	})
   218  })
   219  
   220  var matchedResources = testnet.RemoveWhiteSpaceFromBody(`[
   221  	{
   222          "sha1": "d9c3a51de5c89c11331d3b90b972789f1a14699a",
   223          "size": 59
   224      },
   225      {
   226          "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d",
   227          "size": 229
   228      }
   229  ]`)
   230  
   231  var unmatchedResources = testnet.RemoveWhiteSpaceFromBody(`[
   232  	{
   233          "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b",
   234          "size": 51,
   235          "fn": "app.rb",
   236  				"mode":""
   237      },
   238      {
   239          "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05",
   240          "size": 70,
   241          "fn": "config.ru",
   242  				"mode":""
   243      }
   244  ]`)
   245  
   246  func uploadApplicationRequest(zipCheck func(*zip.Reader)) testnet.TestRequest {
   247  	return testapi.NewCloudControllerTestRequest(testnet.TestRequest{
   248  		Method:  "PUT",
   249  		Path:    "/v2/apps/my-cool-app-guid/bits",
   250  		Matcher: uploadBodyMatcher(zipCheck),
   251  		Response: testnet.TestResponse{
   252  			Status: http.StatusCreated,
   253  			Body: `
   254  {
   255  	"metadata":{
   256  		"guid": "my-job-guid",
   257  		"url": "/v2/jobs/my-job-guid"
   258  	}
   259  }
   260  	`},
   261  	})
   262  }
   263  
   264  var matchResourceRequest = testnet.TestRequest{
   265  	Method: "PUT",
   266  	Path:   "/v2/resource_match",
   267  	Matcher: testnet.RequestBodyMatcher(testnet.RemoveWhiteSpaceFromBody(`[
   268  	{
   269          "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b",
   270          "size": 51
   271      },
   272      {
   273          "sha1": "f097424ce1fa66c6cb9f5e8a18c317376ec12e05",
   274          "size": 70
   275      },
   276      {
   277          "sha1": "d9c3a51de5c89c11331d3b90b972789f1a14699a",
   278          "size": 59
   279      },
   280      {
   281          "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d",
   282          "size": 229
   283      }
   284  ]`)),
   285  	Response: testnet.TestResponse{
   286  		Status: http.StatusOK,
   287  		Body:   matchedResources,
   288  	},
   289  }
   290  
   291  var matchResourceRequestImbalanced = testnet.TestRequest{
   292  	Method: "PUT",
   293  	Path:   "/v2/resource_match",
   294  	Matcher: testnet.RequestBodyMatcher(testnet.RemoveWhiteSpaceFromBody(`[
   295  	{
   296          "sha1": "2474735f5163ba7612ef641f438f4b5bee00127b",
   297          "size": 51
   298      },
   299      {
   300          "sha1": "345f999aef9070fb9a608e65cf221b7038156b6d",
   301          "size": 229
   302      }
   303  ]`)),
   304  	Response: testnet.TestResponse{
   305  		Status: http.StatusOK,
   306  		Body:   matchedResources,
   307  	},
   308  }
   309  
   310  var defaultZipCheck = func(zipReader *zip.Reader) {
   311  	Expect(len(zipReader.File)).To(Equal(2), "Wrong number of files in zip")
   312  
   313  	var expectedPermissionBits os.FileMode
   314  	if runtime.GOOS == "windows" {
   315  		expectedPermissionBits = 0111
   316  	} else {
   317  		expectedPermissionBits = 0755
   318  	}
   319  
   320  	Expect(zipReader.File[0].Name).To(Equal("app.rb"))
   321  	Expect(executableBits(zipReader.File[0].Mode())).To(Equal(executableBits(expectedPermissionBits)))
   322  
   323  nextFile:
   324  	for _, f := range zipReader.File {
   325  		for _, expected := range expectedApplicationContent {
   326  			if f.Name == expected {
   327  				continue nextFile
   328  			}
   329  		}
   330  		Fail("Expected " + f.Name + " but did not find it")
   331  	}
   332  }
   333  
   334  var defaultRequests = []testnet.TestRequest{
   335  	uploadApplicationRequest(defaultZipCheck),
   336  	createProgressEndpoint("running"),
   337  	createProgressEndpoint("finished"),
   338  }
   339  
   340  var expectedApplicationContent = []string{"app.rb", "config.ru"}
   341  
   342  const maxMultipartResponseSizeInBytes = 4096
   343  
   344  func uploadBodyMatcher(zipChecks func(zipReader *zip.Reader)) func(*http.Request) {
   345  	return func(request *http.Request) {
   346  		defer GinkgoRecover()
   347  		err := request.ParseMultipartForm(maxMultipartResponseSizeInBytes)
   348  		if err != nil {
   349  			Fail(fmt.Sprintf("Failed parsing multipart form %v", err))
   350  			return
   351  		}
   352  		defer request.MultipartForm.RemoveAll()
   353  
   354  		Expect(len(request.MultipartForm.Value)).To(Equal(1), "Should have 1 value")
   355  		valuePart, ok := request.MultipartForm.Value["resources"]
   356  		Expect(ok).To(BeTrue(), "Resource manifest not present")
   357  		Expect(len(valuePart)).To(Equal(1), "Wrong number of values")
   358  
   359  		resourceManifest := valuePart[0]
   360  		chompedResourceManifest := strings.Replace(resourceManifest, "\n", "", -1)
   361  		Expect(chompedResourceManifest).To(Equal(unmatchedResources), "Resources do not match")
   362  
   363  		Expect(len(request.MultipartForm.File)).To(Equal(1), "Wrong number of files")
   364  
   365  		fileHeaders, ok := request.MultipartForm.File["application"]
   366  		Expect(ok).To(BeTrue(), "Application file part not present")
   367  		Expect(len(fileHeaders)).To(Equal(1), "Wrong number of files")
   368  
   369  		applicationFile := fileHeaders[0]
   370  		Expect(applicationFile.Filename).To(Equal("application.zip"), "Wrong file name")
   371  
   372  		file, err := applicationFile.Open()
   373  		if err != nil {
   374  			Fail(fmt.Sprintf("Cannot get multipart file %v", err.Error()))
   375  			return
   376  		}
   377  
   378  		length, err := strconv.ParseInt(applicationFile.Header.Get("content-length"), 10, 64)
   379  		if err != nil {
   380  			Fail(fmt.Sprintf("Cannot convert content-length to int %v", err.Error()))
   381  			return
   382  		}
   383  
   384  		if zipChecks != nil {
   385  			zipReader, err := zip.NewReader(file, length)
   386  			if err != nil {
   387  				Fail(fmt.Sprintf("Error reading zip content %v", err.Error()))
   388  				return
   389  			}
   390  
   391  			zipChecks(zipReader)
   392  		}
   393  	}
   394  }
   395  
   396  func createProgressEndpoint(status string) (req testnet.TestRequest) {
   397  	body := fmt.Sprintf(`
   398  	{
   399  		"entity":{
   400  			"status":"%s"
   401  		}
   402  	}`, status)
   403  
   404  	req.Method = "GET"
   405  	req.Path = "/v2/jobs/my-job-guid"
   406  	req.Response = testnet.TestResponse{
   407  		Status: http.StatusCreated,
   408  		Body:   body,
   409  	}
   410  
   411  	return
   412  }
   413  
   414  var matchExcludedResourceRequest = testnet.TestRequest{
   415  	Method: "PUT",
   416  	Path:   "/v2/resource_match",
   417  	Matcher: testnet.RequestBodyMatcher(testnet.RemoveWhiteSpaceFromBody(`[
   418      {
   419          "fn": ".svn",
   420          "sha1": "0",
   421          "size": 0
   422      },
   423      {
   424          "fn": ".svn/test",
   425          "sha1": "456b1d3f7cfbadc66d390de79cbbb6e6a10662da",
   426          "size": 12
   427      },
   428      {
   429          "fn": "_darcs",
   430          "sha1": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
   431          "size": 4
   432      }
   433  ]`)),
   434  	Response: testnet.TestResponse{
   435  		Status: http.StatusOK,
   436  		Body:   matchedResources,
   437  	},
   438  }
   439  
   440  func executableBits(mode os.FileMode) os.FileMode {
   441  	return mode & 0111
   442  }