github.com/juju/charmrepo/v7@v7.0.1/testing/mockstore.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the LGPLv3, see LICENCE file for details. 3 4 package testing // import "github.com/juju/charmrepo/v7/testing" 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "io" 10 "net" 11 "net/http" 12 "os" 13 "strconv" 14 "strings" 15 16 "github.com/juju/charm/v9" 17 "github.com/juju/loggo" 18 "github.com/juju/utils/v3" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/charmrepo/v7" 22 ) 23 24 var logger = loggo.GetLogger("juju.charm.testing.mockstore") 25 26 // MockStore provides a mock charm store implementation useful when testing. 27 type MockStore struct { 28 mux *http.ServeMux 29 listener net.Listener 30 archiveBytes []byte 31 // ArchiveSHA256 holds the hex-encoded SHA256 checksum 32 // of the charm archive served by the mock store. 33 ArchiveSHA256 string 34 Downloads []*charm.URL 35 DownloadsNoStats []*charm.URL 36 Authorizations []string 37 Metadata []string 38 InfoRequestCount int 39 InfoRequestCountNoStats int 40 DefaultSeries string 41 42 charms map[string]int 43 } 44 45 // NewMockStore creates a mock charm store containing the specified charms. 46 func NewMockStore(c *gc.C, repo *Repo, charms map[string]int) *MockStore { 47 s := &MockStore{charms: charms, DefaultSeries: "precise"} 48 f, err := os.Open(repo.CharmArchivePath(c.MkDir(), "dummy")) 49 c.Assert(err, gc.IsNil) 50 defer f.Close() 51 buf := &bytes.Buffer{} 52 s.ArchiveSHA256, _, err = utils.ReadSHA256(io.TeeReader(f, buf)) 53 c.Logf("ArchiveSHA256: %v", s.ArchiveSHA256) 54 55 c.Assert(err, gc.IsNil) 56 s.archiveBytes = buf.Bytes() 57 c.Assert(err, gc.IsNil) 58 s.mux = http.NewServeMux() 59 s.mux.HandleFunc("/charm-info", s.serveInfo) 60 s.mux.HandleFunc("/charm-event", s.serveEvent) 61 s.mux.HandleFunc("/charm/", s.serveCharm) 62 lis, err := net.Listen("tcp", "127.0.0.1:0") 63 c.Assert(err, gc.IsNil) 64 s.listener = lis 65 go http.Serve(s.listener, s) 66 return s 67 } 68 69 // Close closes the mock store's socket. 70 func (s *MockStore) Close() { 71 s.listener.Close() 72 } 73 74 // Address returns the URL used to make requests to the mock store. 75 func (s *MockStore) Address() string { 76 return "http://" + s.listener.Addr().String() 77 } 78 79 // UpdateStoreRevision sets the revision of the specified charm to rev. 80 func (s *MockStore) UpdateStoreRevision(ch string, rev int) { 81 s.charms[ch] = rev 82 } 83 84 // ServeHTTP implements http.ServeHTTP 85 func (s *MockStore) ServeHTTP(w http.ResponseWriter, r *http.Request) { 86 s.mux.ServeHTTP(w, r) 87 } 88 89 func (s *MockStore) serveInfo(w http.ResponseWriter, r *http.Request) { 90 if metadata := r.Header.Get("Juju-Metadata"); metadata != "" { 91 s.Metadata = append(s.Metadata, metadata) 92 logger.Infof("Juju metadata: " + metadata) 93 } 94 95 r.ParseForm() 96 if r.Form.Get("stats") == "0" { 97 s.InfoRequestCountNoStats += 1 98 } else { 99 s.InfoRequestCount += 1 100 } 101 102 response := map[string]*charmrepo.InfoResponse{} 103 for _, url := range r.Form["charms"] { 104 cr := &charmrepo.InfoResponse{} 105 response[url] = cr 106 charmURL, err := charm.ParseURL(url) 107 if err != nil { 108 panic(err) 109 } 110 if charmURL.Series == "" { 111 charmURL.Series = s.DefaultSeries 112 } 113 switch charmURL.Name { 114 case "borken": 115 cr.Errors = append(cr.Errors, "badness") 116 case "terracotta": 117 cr.Errors = append(cr.Errors, "cannot get revision") 118 case "unwise": 119 cr.Warnings = append(cr.Warnings, "foolishness") 120 fallthrough 121 default: 122 if rev, ok := s.charms[charmURL.WithRevision(-1).String()]; ok { 123 if charmURL.Revision == -1 { 124 cr.Revision = rev 125 } else { 126 cr.Revision = charmURL.Revision 127 } 128 cr.Sha256 = s.ArchiveSHA256 129 cr.CanonicalURL = charmURL.String() 130 } else { 131 cr.Errors = append(cr.Errors, "entry not found") 132 } 133 } 134 } 135 data, err := json.Marshal(response) 136 if err != nil { 137 panic(err) 138 } 139 w.Header().Set("Content-Type", "application/json") 140 _, err = w.Write(data) 141 if err != nil { 142 panic(err) 143 } 144 } 145 146 func (s *MockStore) serveEvent(w http.ResponseWriter, r *http.Request) { 147 r.ParseForm() 148 response := map[string]*charmrepo.EventResponse{} 149 for _, url := range r.Form["charms"] { 150 digest := "" 151 if i := strings.Index(url, "@"); i >= 0 { 152 digest = url[i+1:] 153 url = url[:i] 154 } 155 er := &charmrepo.EventResponse{} 156 response[url] = er 157 if digest != "" && digest != "the-digest" { 158 er.Kind = "not-found" 159 er.Errors = []string{"entry not found"} 160 continue 161 } 162 charmURL := charm.MustParseURL(url) 163 switch charmURL.Name { 164 case "borken": 165 er.Kind = "publish-error" 166 er.Errors = append(er.Errors, "badness") 167 case "unwise": 168 er.Warnings = append(er.Warnings, "foolishness") 169 fallthrough 170 default: 171 if rev, ok := s.charms[charmURL.WithRevision(-1).String()]; ok { 172 er.Kind = "published" 173 er.Revision = rev 174 er.Digest = "the-digest" 175 } else { 176 er.Kind = "not-found" 177 er.Errors = []string{"entry not found"} 178 } 179 } 180 } 181 data, err := json.Marshal(response) 182 if err != nil { 183 panic(err) 184 } 185 w.Header().Set("Content-Type", "application/json") 186 _, err = w.Write(data) 187 if err != nil { 188 panic(err) 189 } 190 } 191 192 func (s *MockStore) serveCharm(w http.ResponseWriter, r *http.Request) { 193 charmURL := charm.MustParseURL("cs:" + r.URL.Path[len("/charm/"):]) 194 195 r.ParseForm() 196 if r.Form.Get("stats") == "0" { 197 s.DownloadsNoStats = append(s.DownloadsNoStats, charmURL) 198 } else { 199 s.Downloads = append(s.Downloads, charmURL) 200 } 201 202 if auth := r.Header.Get("Authorization"); auth != "" { 203 s.Authorizations = append(s.Authorizations, auth) 204 } 205 206 w.Header().Set("Connection", "close") 207 w.Header().Set("Content-Type", "application/octet-stream") 208 w.Header().Set("Content-Length", strconv.Itoa(len(s.archiveBytes))) 209 _, err := w.Write(s.archiveBytes) 210 if err != nil { 211 panic(err) 212 } 213 }