|
| 1 | +//go:build cfenv_artifact |
| 2 | + |
| 3 | +// This test verifies a *necessary condition* for the buildpack's automatic "cloud" |
| 4 | +// Spring profile behaviour: the java-cfenv artifact the manifest ships must register |
| 5 | +// io.pivotal.cfenv.profile.CloudProfileApplicationListener as a Spring ApplicationListener |
| 6 | +// (via META-INF/spring.factories). That listener lives only in the java-cfenv-all module; |
| 7 | +// the bare java-cfenv core module does not carry it, which silently drops the "cloud" |
| 8 | +// profile (see cloudfoundry/java-buildpack#1349). |
| 9 | +// |
| 10 | +// It downloads each shipped jar, verifies its SHA-256 against the manifest (which also |
| 11 | +// guards against duplicate/mismatched mirror entries), then inspects spring.factories. |
| 12 | +// |
| 13 | +// It is network-bound, so it is excluded from the default unit suite by the cfenv_artifact |
| 14 | +// build tag. Run explicitly: |
| 15 | +// |
| 16 | +// go test -tags cfenv_artifact ./src/java/frameworks/ |
| 17 | +package frameworks_test |
| 18 | + |
| 19 | +import ( |
| 20 | + "archive/zip" |
| 21 | + "crypto/sha256" |
| 22 | + "encoding/hex" |
| 23 | + "io" |
| 24 | + "net/http" |
| 25 | + "os" |
| 26 | + "path/filepath" |
| 27 | + "strings" |
| 28 | + "time" |
| 29 | + |
| 30 | + . "github.com/onsi/ginkgo/v2" |
| 31 | + . "github.com/onsi/gomega" |
| 32 | + |
| 33 | + "gopkg.in/yaml.v2" |
| 34 | +) |
| 35 | + |
| 36 | +const cloudProfileListener = "io.pivotal.cfenv.profile.CloudProfileApplicationListener" |
| 37 | + |
| 38 | +type manifestDependency struct { |
| 39 | + Name string `yaml:"name"` |
| 40 | + Version string `yaml:"version"` |
| 41 | + URI string `yaml:"uri"` |
| 42 | + SHA256 string `yaml:"sha256"` |
| 43 | +} |
| 44 | + |
| 45 | +type manifestFile struct { |
| 46 | + Dependencies []manifestDependency `yaml:"dependencies"` |
| 47 | +} |
| 48 | + |
| 49 | +var _ = Describe("Java CF Env artifact", func() { |
| 50 | + var deps []manifestDependency |
| 51 | + |
| 52 | + BeforeEach(func() { |
| 53 | + deps = nil |
| 54 | + |
| 55 | + manifestPath, err := filepath.Abs(filepath.Join("..", "..", "..", "manifest.yml")) |
| 56 | + Expect(err).NotTo(HaveOccurred()) |
| 57 | + |
| 58 | + raw, err := os.ReadFile(manifestPath) |
| 59 | + Expect(err).NotTo(HaveOccurred()) |
| 60 | + |
| 61 | + var m manifestFile |
| 62 | + Expect(yaml.Unmarshal(raw, &m)).To(Succeed()) |
| 63 | + |
| 64 | + for _, d := range m.Dependencies { |
| 65 | + if d.Name == "java-cfenv" && d.URI != "" { |
| 66 | + deps = append(deps, d) |
| 67 | + } |
| 68 | + } |
| 69 | + Expect(deps).NotTo(BeEmpty(), "no java-cfenv dependency entries with a uri found in manifest.yml") |
| 70 | + }) |
| 71 | + |
| 72 | + It("ships a jar that registers the cloud-profile ApplicationListener", func() { |
| 73 | + for _, dep := range deps { |
| 74 | + By("checking java-cfenv " + dep.Version) |
| 75 | + |
| 76 | + jar := downloadToTemp(dep.URI) |
| 77 | + defer os.Remove(jar) |
| 78 | + |
| 79 | + Expect(fileSHA256(jar)).To(Equal(dep.SHA256), |
| 80 | + "java-cfenv %s: downloaded bytes do not match manifest sha256 (%s)", dep.Version, dep.URI) |
| 81 | + |
| 82 | + factories, ok := readZipEntry(jar, "META-INF/spring.factories") |
| 83 | + Expect(ok).To(BeTrue(), |
| 84 | + "java-cfenv %s: jar has no META-INF/spring.factories — this is the bare core module, "+ |
| 85 | + "which cannot auto-activate the 'cloud' profile; ship java-cfenv-all instead", dep.Version) |
| 86 | + |
| 87 | + Expect(factories).To(ContainSubstring(cloudProfileListener), |
| 88 | + "java-cfenv %s: spring.factories does not register %s — the 'cloud' profile will not be "+ |
| 89 | + "activated automatically; ship java-cfenv-all instead", dep.Version, cloudProfileListener) |
| 90 | + |
| 91 | + Expect(registersAsApplicationListener(factories, cloudProfileListener)).To(BeTrue(), |
| 92 | + "java-cfenv %s: %s is present but not registered under "+ |
| 93 | + "org.springframework.context.ApplicationListener", dep.Version, cloudProfileListener) |
| 94 | + } |
| 95 | + }) |
| 96 | +}) |
| 97 | + |
| 98 | +// registersAsApplicationListener reports whether class is listed under the |
| 99 | +// org.springframework.context.ApplicationListener key, accounting for backslash |
| 100 | +// line continuations used in spring.factories files. |
| 101 | +func registersAsApplicationListener(factories, class string) bool { |
| 102 | + const key = "org.springframework.context.ApplicationListener" |
| 103 | + joined := strings.ReplaceAll(factories, "\\\n", "") |
| 104 | + for _, line := range strings.Split(joined, "\n") { |
| 105 | + line = strings.TrimSpace(line) |
| 106 | + if strings.HasPrefix(line, key+"=") { |
| 107 | + return strings.Contains(line, class) |
| 108 | + } |
| 109 | + } |
| 110 | + return false |
| 111 | +} |
| 112 | + |
| 113 | +func downloadToTemp(uri string) string { |
| 114 | + GinkgoHelper() |
| 115 | + |
| 116 | + client := &http.Client{Timeout: 60 * time.Second} |
| 117 | + resp, err := client.Get(uri) |
| 118 | + Expect(err).NotTo(HaveOccurred(), "download %s", uri) |
| 119 | + defer resp.Body.Close() |
| 120 | + Expect(resp.StatusCode).To(Equal(http.StatusOK), "download %s returned %d", uri, resp.StatusCode) |
| 121 | + |
| 122 | + f, err := os.CreateTemp("", "java-cfenv-*.jar") |
| 123 | + Expect(err).NotTo(HaveOccurred()) |
| 124 | + defer f.Close() |
| 125 | + |
| 126 | + _, err = io.Copy(f, resp.Body) |
| 127 | + Expect(err).NotTo(HaveOccurred()) |
| 128 | + |
| 129 | + return f.Name() |
| 130 | +} |
| 131 | + |
| 132 | +func fileSHA256(path string) string { |
| 133 | + GinkgoHelper() |
| 134 | + |
| 135 | + f, err := os.Open(path) |
| 136 | + Expect(err).NotTo(HaveOccurred()) |
| 137 | + defer f.Close() |
| 138 | + |
| 139 | + h := sha256.New() |
| 140 | + _, err = io.Copy(h, f) |
| 141 | + Expect(err).NotTo(HaveOccurred()) |
| 142 | + |
| 143 | + return hex.EncodeToString(h.Sum(nil)) |
| 144 | +} |
| 145 | + |
| 146 | +func readZipEntry(jarPath, entry string) (string, bool) { |
| 147 | + GinkgoHelper() |
| 148 | + |
| 149 | + r, err := zip.OpenReader(jarPath) |
| 150 | + Expect(err).NotTo(HaveOccurred()) |
| 151 | + defer r.Close() |
| 152 | + |
| 153 | + for _, f := range r.File { |
| 154 | + if f.Name == entry { |
| 155 | + rc, err := f.Open() |
| 156 | + Expect(err).NotTo(HaveOccurred()) |
| 157 | + defer rc.Close() |
| 158 | + |
| 159 | + data, err := io.ReadAll(rc) |
| 160 | + Expect(err).NotTo(HaveOccurred()) |
| 161 | + return string(data), true |
| 162 | + } |
| 163 | + } |
| 164 | + return "", false |
| 165 | +} |
0 commit comments