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  }