gitlab.com/SiaPrime/SiaPrime@v1.4.1/node/api/server_helpers_test.go (about)

     1  package api
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"path/filepath"
    11  	"runtime/debug"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"gitlab.com/NebulousLabs/errors"
    17  	"gitlab.com/NebulousLabs/threadgroup"
    18  
    19  	"gitlab.com/SiaPrime/SiaPrime/build"
    20  	"gitlab.com/SiaPrime/SiaPrime/config"
    21  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    22  	"gitlab.com/SiaPrime/SiaPrime/modules"
    23  	"gitlab.com/SiaPrime/SiaPrime/modules/consensus"
    24  	"gitlab.com/SiaPrime/SiaPrime/modules/explorer"
    25  	"gitlab.com/SiaPrime/SiaPrime/modules/gateway"
    26  	"gitlab.com/SiaPrime/SiaPrime/modules/host"
    27  	"gitlab.com/SiaPrime/SiaPrime/modules/index"
    28  	"gitlab.com/SiaPrime/SiaPrime/modules/miner"
    29  	"gitlab.com/SiaPrime/SiaPrime/modules/miningpool"
    30  	"gitlab.com/SiaPrime/SiaPrime/modules/renter"
    31  	"gitlab.com/SiaPrime/SiaPrime/modules/transactionpool"
    32  	"gitlab.com/SiaPrime/SiaPrime/modules/wallet"
    33  	"gitlab.com/SiaPrime/SiaPrime/persist"
    34  	"gitlab.com/SiaPrime/SiaPrime/types"
    35  )
    36  
    37  // A Server is a collection of siad modules that can be communicated with over
    38  // an http api.
    39  type Server struct {
    40  	api               *API
    41  	apiServer         *http.Server
    42  	listener          net.Listener
    43  	requiredUserAgent string
    44  	tg                threadgroup.ThreadGroup
    45  }
    46  
    47  // panicClose will close a Server, panicking if there is an error upon close.
    48  func (srv *Server) panicClose() {
    49  	err := srv.Close()
    50  	if err != nil {
    51  		// Print the stack.
    52  		debug.PrintStack()
    53  		panic(err)
    54  	}
    55  }
    56  
    57  // Close closes the Server's listener, causing the HTTP server to shut down.
    58  func (srv *Server) Close() error {
    59  	err := srv.listener.Close()
    60  	err = errors.Extend(err, srv.tg.Stop())
    61  
    62  	// Safely close each module.
    63  	mods := []struct {
    64  		name string
    65  		c    io.Closer
    66  	}{
    67  		{"explorer", srv.api.explorer},
    68  		{"host", srv.api.host},
    69  		{"renter", srv.api.renter},
    70  		{"miner", srv.api.miner},
    71  		{"stratumminer", srv.api.stratumminer},
    72  		{"wallet", srv.api.wallet},
    73  		{"tpool", srv.api.tpool},
    74  		{"consensus", srv.api.cs},
    75  		{"gateway", srv.api.gateway},
    76  	}
    77  	for _, mod := range mods {
    78  		if mod.c != nil {
    79  			if closeErr := mod.c.Close(); closeErr != nil {
    80  				err = errors.Extend(err, fmt.Errorf("%v.Close failed: %v", mod.name, closeErr))
    81  			}
    82  		}
    83  	}
    84  	return errors.AddContext(err, "error while closing server")
    85  }
    86  
    87  // Serve listens for and handles API calls. It is a blocking function.
    88  func (srv *Server) Serve() error {
    89  	err := srv.tg.Add()
    90  	if err != nil {
    91  		return errors.AddContext(err, "unable to initialize server")
    92  	}
    93  	defer srv.tg.Done()
    94  
    95  	// The server will run until an error is encountered or the listener is
    96  	// closed, via either the Close method or by signal handling.  Closing the
    97  	// listener will result in the benign error handled below.
    98  	err = srv.apiServer.Serve(srv.listener)
    99  	if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {
   100  		return err
   101  	}
   102  	return nil
   103  }
   104  
   105  // NewServer creates a new API server from the provided modules. The API will
   106  // require authentication using HTTP basic auth if the supplied password is not
   107  // the empty string. Usernames are ignored for authentication. This type of
   108  // authentication sends passwords in plaintext and should therefore only be
   109  // used if the APIaddr is localhost.
   110  func NewServer(dir string, APIaddr string, requiredUserAgent string, requiredPassword string, cs modules.ConsensusSet, e modules.Explorer, g modules.Gateway, h modules.Host, m modules.Miner, r modules.Renter, tp modules.TransactionPool, w modules.Wallet, mp modules.Pool, sm modules.StratumMiner, i modules.Index) (*Server, error) {
   111  	listener, err := net.Listen("tcp", APIaddr)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	// Load the config file.
   117  	cfg, err := modules.NewConfig(filepath.Join(dir, "siad.config"))
   118  	if err != nil {
   119  		return nil, errors.AddContext(err, "failed to load siad config")
   120  	}
   121  
   122  	api := New(cfg, requiredUserAgent, requiredPassword, cs, e, g, h, m, r, tp, w, mp, sm, i)
   123  	srv := &Server{
   124  		api: api,
   125  		apiServer: &http.Server{
   126  			Handler: api,
   127  		},
   128  		listener:          listener,
   129  		requiredUserAgent: requiredUserAgent,
   130  	}
   131  	return srv, nil
   132  }
   133  
   134  // serverTester contains a server and a set of channels for keeping all of the
   135  // modules synchronized during testing.
   136  type serverTester struct {
   137  	cs        modules.ConsensusSet
   138  	explorer  modules.Explorer
   139  	gateway   modules.Gateway
   140  	host      modules.Host
   141  	miner     modules.TestMiner
   142  	renter    modules.Renter
   143  	tpool     modules.TransactionPool
   144  	wallet    modules.Wallet
   145  	walletKey crypto.CipherKey
   146  
   147  	server *Server
   148  
   149  	dir string
   150  }
   151  
   152  // assembleServerTester creates a bunch of modules and assembles them into a
   153  // server tester, without creating any directories or mining any blocks.
   154  func assembleServerTester(key crypto.CipherKey, testdir string) (*serverTester, error) {
   155  	// assembleServerTester should not get called during short tests, as it
   156  	// takes a long time to run.
   157  	if testing.Short() {
   158  		panic("assembleServerTester called during short tests")
   159  	}
   160  
   161  	// Create the modules.
   162  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	encrypted, err := w.Encrypted()
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	if !encrypted {
   183  		_, err = w.Encrypt(key)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  	}
   188  	err = w.Unlock(key)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	h, err := host.New(cs, g, tp, w, "localhost:0", filepath.Join(testdir, modules.HostDir))
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	r, err := renter.New(g, cs, w, tp, filepath.Join(testdir, modules.RenterDir))
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	srv, err := NewServer(testdir, "localhost:0", "SiaPrime-Agent", "", cs, nil, g, h, m, r, tp, w, nil, nil, nil)
   205  
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	// Assemble the serverTester.
   211  	st := &serverTester{
   212  		cs:        cs,
   213  		gateway:   g,
   214  		host:      h,
   215  		miner:     m,
   216  		renter:    r,
   217  		tpool:     tp,
   218  		wallet:    w,
   219  		walletKey: key,
   220  
   221  		server: srv,
   222  
   223  		dir: testdir,
   224  	}
   225  
   226  	// TODO: A more reasonable way of listening for server errors.
   227  	go func() {
   228  		listenErr := srv.Serve()
   229  		if listenErr != nil && !strings.Contains(listenErr.Error(), "ThreadGroup already stopped") {
   230  			panic(listenErr)
   231  		}
   232  	}()
   233  	return st, nil
   234  }
   235  
   236  // assembleAuthenticatedServerTester creates a bunch of modules and assembles
   237  // them into a server tester that requires authentication with the given
   238  // requiredPassword. No directories are created and no blocks are mined.
   239  func assembleAuthenticatedServerTester(requiredPassword string, key crypto.CipherKey, testdir string, withPool bool) (*serverTester, error) {
   240  	// assembleAuthenticatedServerTester should not get called during short
   241  	// tests, as it takes a long time to run.
   242  	if testing.Short() {
   243  		panic("assembleServerTester called during short tests")
   244  	}
   245  
   246  	// Create the modules.
   247  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	encrypted, err := w.Encrypted()
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	if !encrypted {
   268  		_, err = w.Encrypt(key)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  	}
   273  	err = w.Unlock(key)
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	h, err := host.New(cs, g, tp, w, "localhost:0", filepath.Join(testdir, modules.HostDir))
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	r, err := renter.New(g, cs, w, tp, filepath.Join(testdir, modules.RenterDir))
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	var mp *pool.Pool
   291  	var idx *index.Index
   292  	if withPool {
   293  		mp, err = pool.New(cs, tp, g, w, filepath.Join(testdir, modules.PoolDir), config.MiningPoolConfig{})
   294  		if err != nil {
   295  			return nil, err
   296  		}
   297  		idx, err = index.New(cs, tp, g, w, filepath.Join(testdir, modules.IndexDir), config.IndexConfig{})
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  	}
   302  	srv, err := NewServer(testdir, "localhost:0", "SiaPrime-Agent", requiredPassword, cs, nil, g, h, m, r, tp, w, mp, nil, idx)
   303  
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	// Assemble the serverTester.
   309  	st := &serverTester{
   310  		cs:        cs,
   311  		gateway:   g,
   312  		host:      h,
   313  		miner:     m,
   314  		renter:    r,
   315  		tpool:     tp,
   316  		wallet:    w,
   317  		walletKey: key,
   318  
   319  		server: srv,
   320  
   321  		dir: testdir,
   322  	}
   323  
   324  	// TODO: A more reasonable way of listening for server errors.
   325  	go func() {
   326  		listenErr := srv.Serve()
   327  		if listenErr != nil {
   328  			panic(listenErr)
   329  		}
   330  	}()
   331  	return st, nil
   332  }
   333  
   334  // assembleExplorerServerTester creates all the explorer dependencies and
   335  // explorer module without creating any directories. The user agent requirement
   336  // is disabled.
   337  func assembleExplorerServerTester(testdir string) (*serverTester, error) {
   338  	// assembleExplorerServerTester should not get called during short tests,
   339  	// as it takes a long time to run.
   340  	if testing.Short() {
   341  		panic("assembleServerTester called during short tests")
   342  	}
   343  
   344  	// Create the modules.
   345  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	e, err := explorer.New(cs, filepath.Join(testdir, modules.ExplorerDir))
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	srv, err := NewServer(testdir, "localhost:0", "", "", cs, e, g, nil, nil, nil, nil, nil, nil, nil, nil)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	// Assemble the serverTester.
   363  	st := &serverTester{
   364  		cs:       cs,
   365  		explorer: e,
   366  		gateway:  g,
   367  
   368  		server: srv,
   369  
   370  		dir: testdir,
   371  	}
   372  
   373  	// TODO: A more reasonable way of listening for server errors.
   374  	go func() {
   375  		listenErr := srv.Serve()
   376  		if listenErr != nil {
   377  			panic(listenErr)
   378  		}
   379  	}()
   380  	return st, nil
   381  }
   382  
   383  // blankServerTester creates a server tester object that is ready for testing,
   384  // without mining any blocks.
   385  func blankServerTester(name string) (*serverTester, error) {
   386  	// createServerTester is expensive, and therefore should not be called
   387  	// during short tests.
   388  	if testing.Short() {
   389  		panic("blankServerTester called during short tests")
   390  	}
   391  
   392  	// Create the server tester with key.
   393  	testdir := build.TempDir("api", name)
   394  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
   395  	st, err := assembleServerTester(key, testdir)
   396  	if err != nil {
   397  		return nil, err
   398  	}
   399  	return st, nil
   400  }
   401  
   402  // createServerTester creates a server tester object that is ready for testing,
   403  // including money in the wallet and all modules initialized.
   404  func createServerTester(name string) (*serverTester, error) {
   405  	// createServerTester is expensive, and therefore should not be called
   406  	// during short tests.
   407  	if testing.Short() {
   408  		panic("createServerTester called during short tests")
   409  	}
   410  
   411  	// Create the testing directory.
   412  	testdir := build.TempDir("api", name)
   413  
   414  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
   415  	st, err := assembleServerTester(key, testdir)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	// Mine blocks until the wallet has confirmed money.
   421  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
   422  		_, err := st.miner.AddBlock()
   423  		if err != nil {
   424  			return nil, err
   425  		}
   426  	}
   427  
   428  	return st, nil
   429  }
   430  
   431  // createAuthenticatedServerTester creates an authenticated server tester
   432  // object that is ready for testing, including money in the wallet and all
   433  // modules initialized.
   434  func createAuthenticatedServerTester(name string, password string, withPool bool) (*serverTester, error) {
   435  	// createAuthenticatedServerTester should not get called during short
   436  	// tests, as it takes a long time to run.
   437  	if testing.Short() {
   438  		panic("assembleServerTester called during short tests")
   439  	}
   440  
   441  	// Create the testing directory.
   442  	testdir := build.TempDir("authenticated-api", name)
   443  
   444  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
   445  	st, err := assembleAuthenticatedServerTester(password, key, testdir, withPool)
   446  
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	// Mine blocks until the wallet has confirmed money.
   452  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
   453  		_, err := st.miner.AddBlock()
   454  		if err != nil {
   455  			return nil, err
   456  		}
   457  	}
   458  
   459  	return st, nil
   460  }
   461  
   462  // createExplorerServerTester creates a server tester object containing only
   463  // the explorer and some presets that match standard explorer setups.
   464  func createExplorerServerTester(name string) (*serverTester, error) {
   465  	testdir := build.TempDir("api", name)
   466  	st, err := assembleExplorerServerTester(testdir)
   467  	if err != nil {
   468  		return nil, err
   469  	}
   470  	return st, nil
   471  }
   472  
   473  // decodeError returns the api.Error from a API response. This method should
   474  // only be called if the response's status code is non-2xx. The error returned
   475  // may not be of type api.Error in the event of an error unmarshalling the
   476  // JSON.
   477  func decodeError(resp *http.Response) error {
   478  	var apiErr Error
   479  	err := json.NewDecoder(resp.Body).Decode(&apiErr)
   480  	if err != nil {
   481  		return err
   482  	}
   483  	return apiErr
   484  }
   485  
   486  // non2xx returns true for non-success HTTP status codes.
   487  func non2xx(code int) bool {
   488  	return code < 200 || code > 299
   489  }
   490  
   491  // panicClose attempts to close a serverTester. If it fails, panic is called
   492  // with the error.
   493  func (st *serverTester) panicClose() {
   494  	st.server.panicClose()
   495  }
   496  
   497  // reloadedServerTester creates a server tester where all of the persistent
   498  // data has been copied to a new folder and all of the modules re-initialized
   499  // on the new folder. This gives an opportunity to see how modules will behave
   500  // when they are relying on their persistent structures.
   501  func (st *serverTester) reloadedServerTester() (*serverTester, error) {
   502  	// Copy the testing directory.
   503  	copiedDir := st.dir + " - " + persist.RandomSuffix()
   504  	err := build.CopyDir(st.dir, copiedDir)
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	copyST, err := assembleServerTester(st.walletKey, copiedDir)
   509  	if err != nil {
   510  		return nil, err
   511  	}
   512  	return copyST, nil
   513  }
   514  
   515  // netAddress returns the NetAddress of the caller.
   516  func (st *serverTester) netAddress() modules.NetAddress {
   517  	return st.server.api.gateway.Address()
   518  }
   519  
   520  // coinAddress returns a coin address that the caller is able to spend from.
   521  func (st *serverTester) coinAddress() string {
   522  	var addr struct {
   523  		Address string
   524  	}
   525  	st.getAPI("/wallet/address", &addr)
   526  	return addr.Address
   527  }
   528  
   529  // acceptContracts instructs the host to begin accepting contracts.
   530  func (st *serverTester) acceptContracts() error {
   531  	settingsValues := url.Values{}
   532  	settingsValues.Set("acceptingcontracts", "true")
   533  	return st.stdPostAPI("/host", settingsValues)
   534  }
   535  
   536  // setHostStorage adds a storage folder to the host.
   537  func (st *serverTester) setHostStorage() error {
   538  	values := url.Values{}
   539  	values.Set("path", st.dir)
   540  	values.Set("size", "1048576")
   541  	return st.stdPostAPI("/host/storage/folders/add", values)
   542  }
   543  
   544  // announceHost announces the host, mines a block, and waits for the
   545  // announcement to register.
   546  func (st *serverTester) announceHost() error {
   547  	// Check how many hosts there are to begin with.
   548  	var hosts HostdbActiveGET
   549  	err := st.getAPI("/hostdb/active", &hosts)
   550  	if err != nil {
   551  		return err
   552  	}
   553  	initialHosts := len(hosts.Hosts)
   554  
   555  	// Set the host to be accepting contracts.
   556  	acceptingContractsValues := url.Values{}
   557  	acceptingContractsValues.Set("acceptingcontracts", "true")
   558  	err = st.stdPostAPI("/host", acceptingContractsValues)
   559  	if err != nil {
   560  		return build.ExtendErr("couldn't make an api call to the host:", err)
   561  	}
   562  
   563  	announceValues := url.Values{}
   564  	announceValues.Set("address", string(st.host.ExternalSettings().NetAddress))
   565  	err = st.stdPostAPI("/host/announce", announceValues)
   566  	if err != nil {
   567  		return err
   568  	}
   569  	// mine block
   570  	_, err = st.miner.AddBlock()
   571  	if err != nil {
   572  		return err
   573  	}
   574  	// wait for announcement
   575  	err = st.getAPI("/hostdb/active", &hosts)
   576  	if err != nil {
   577  		return err
   578  	}
   579  	for i := 0; i < 50 && len(hosts.Hosts) <= initialHosts; i++ {
   580  		time.Sleep(100 * time.Millisecond)
   581  		err = st.getAPI("/hostdb/active", &hosts)
   582  		if err != nil {
   583  			return err
   584  		}
   585  	}
   586  	if len(hosts.Hosts) <= initialHosts {
   587  		return errors.New("host announcement not seen")
   588  	}
   589  	return nil
   590  }
   591  
   592  // getAPI makes an API call and decodes the response.
   593  func (st *serverTester) getAPI(call string, obj interface{}) error {
   594  	resp, err := HttpGET("http://" + st.server.listener.Addr().String() + call)
   595  	if err != nil {
   596  		return err
   597  	}
   598  	defer resp.Body.Close()
   599  
   600  	if non2xx(resp.StatusCode) {
   601  		return decodeError(resp)
   602  	}
   603  
   604  	// Return early because there is no content to decode.
   605  	if resp.StatusCode == http.StatusNoContent {
   606  		return nil
   607  	}
   608  
   609  	// Decode the response into 'obj'.
   610  	err = json.NewDecoder(resp.Body).Decode(obj)
   611  	if err != nil {
   612  		return err
   613  	}
   614  	return nil
   615  }
   616  
   617  // postAPI makes an API call and decodes the response.
   618  func (st *serverTester) postAPI(call string, values url.Values, obj interface{}) error {
   619  	resp, err := HttpPOST("http://"+st.server.listener.Addr().String()+call, values.Encode())
   620  	if err != nil {
   621  		return err
   622  	}
   623  	defer resp.Body.Close()
   624  
   625  	if non2xx(resp.StatusCode) {
   626  		return decodeError(resp)
   627  	}
   628  
   629  	// Return early because there is no content to decode.
   630  	if resp.StatusCode == http.StatusNoContent {
   631  		return nil
   632  	}
   633  
   634  	// Decode the response into 'obj'.
   635  	err = json.NewDecoder(resp.Body).Decode(obj)
   636  	if err != nil {
   637  		return err
   638  	}
   639  	return nil
   640  }
   641  
   642  // stdGetAPI makes an API call and discards the response.
   643  func (st *serverTester) stdGetAPI(call string) error {
   644  	resp, err := HttpGET("http://" + st.server.listener.Addr().String() + call)
   645  	if err != nil {
   646  		return err
   647  	}
   648  	defer resp.Body.Close()
   649  
   650  	if non2xx(resp.StatusCode) {
   651  		return decodeError(resp)
   652  	}
   653  	return nil
   654  }
   655  
   656  // stdGetAPIUA makes an API call with a custom user agent.
   657  func (st *serverTester) stdGetAPIUA(call string, userAgent string) error {
   658  	req, err := http.NewRequest("GET", "http://"+st.server.listener.Addr().String()+call, nil)
   659  	if err != nil {
   660  		return err
   661  	}
   662  	req.Header.Set("User-Agent", userAgent)
   663  	resp, err := http.DefaultClient.Do(req)
   664  	if err != nil {
   665  		return err
   666  	}
   667  	defer resp.Body.Close()
   668  
   669  	if non2xx(resp.StatusCode) {
   670  		return decodeError(resp)
   671  	}
   672  	return nil
   673  }
   674  
   675  // stdPostAPI makes an API call and discards the response.
   676  func (st *serverTester) stdPostAPI(call string, values url.Values) error {
   677  	resp, err := HttpPOST("http://"+st.server.listener.Addr().String()+call, values.Encode())
   678  	if err != nil {
   679  		return err
   680  	}
   681  	defer resp.Body.Close()
   682  
   683  	if non2xx(resp.StatusCode) {
   684  		return decodeError(resp)
   685  	}
   686  	return nil
   687  }