github.com/core-coin/go-core/v2@v2.1.9/signer/rules/rules_test.go (about)

     1  // Copyright 2018 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-core library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-core library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package rules
    18  
    19  import (
    20  	"fmt"
    21  	"math/big"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/core-coin/go-core/v2/internal/xcbapi"
    26  
    27  	"github.com/core-coin/go-core/v2/accounts"
    28  	"github.com/core-coin/go-core/v2/common"
    29  	"github.com/core-coin/go-core/v2/common/hexutil"
    30  	"github.com/core-coin/go-core/v2/core/types"
    31  	"github.com/core-coin/go-core/v2/signer/core"
    32  	"github.com/core-coin/go-core/v2/signer/storage"
    33  )
    34  
    35  const JS = `
    36  /**
    37  This is an example implementation of a Javascript rule file.
    38  
    39  When the signer receives a request over the external API, the corresponding method is evaluated.
    40  Three things can happen:
    41  
    42  1. The method returns "Approve". This means the operation is permitted.
    43  2. The method returns "Reject". This means the operation is rejected.
    44  3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
    45  that the operation will continue to manual processing, via the regular UI method chosen by the user.
    46  
    47  [*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
    48  only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
    49  accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
    50  
    51  **/
    52  
    53  function ApproveListing(request){
    54  	console.log("In js approve listing");
    55  	console.log(request.accounts[3].Address)
    56  	console.log(request.meta.Remote)
    57  	return "Approve"
    58  }
    59  
    60  function ApproveTx(request){
    61  	console.log("test");
    62  	console.log("from");
    63  	return "Reject";
    64  }
    65  
    66  function test(thing){
    67  	console.log(thing.String())
    68  }
    69  
    70  `
    71  
    72  type alwaysDenyUI struct{}
    73  
    74  func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
    75  	return core.UserInputResponse{}, nil
    76  }
    77  func (alwaysDenyUI) RegisterUIServer(api *core.UIServerAPI) {
    78  }
    79  
    80  func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
    81  }
    82  
    83  func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
    84  	return core.SignTxResponse{Transaction: request.Transaction, Approved: false}, nil
    85  }
    86  
    87  func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
    88  	return core.SignDataResponse{Approved: false}, nil
    89  }
    90  
    91  func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
    92  	return core.ListResponse{Accounts: nil}, nil
    93  }
    94  
    95  func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
    96  	return core.NewAccountResponse{Approved: false}, nil
    97  }
    98  
    99  func (alwaysDenyUI) ShowError(message string) {
   100  	panic("implement me")
   101  }
   102  
   103  func (alwaysDenyUI) ShowInfo(message string) {
   104  	panic("implement me")
   105  }
   106  
   107  func (alwaysDenyUI) OnApprovedTx(tx xcbapi.SignTransactionResult) {
   108  	panic("implement me")
   109  }
   110  
   111  func initRuleEngine(js string) (*rulesetUI, error) {
   112  	r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage())
   113  	if err != nil {
   114  		return nil, fmt.Errorf("failed to create js engine: %v", err)
   115  	}
   116  	if err = r.Init(js); err != nil {
   117  		return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
   118  	}
   119  	return r, nil
   120  }
   121  
   122  func TestListRequest(t *testing.T) {
   123  	accs := make([]accounts.Account, 5)
   124  
   125  	for i := range accs {
   126  		addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
   127  		acc := accounts.Account{
   128  			Address: common.BytesToAddress(common.Hex2Bytes(addr)),
   129  			URL:     accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
   130  		}
   131  		accs[i] = acc
   132  	}
   133  
   134  	js := `function ApproveListing(){ return "Approve" }`
   135  
   136  	r, err := initRuleEngine(js)
   137  	if err != nil {
   138  		t.Errorf("Couldn't create evaluator %v", err)
   139  		return
   140  	}
   141  	resp, _ := r.ApproveListing(&core.ListRequest{
   142  		Accounts: accs,
   143  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   144  	})
   145  	if len(resp.Accounts) != len(accs) {
   146  		t.Errorf("Expected check to resolve to 'Approve'")
   147  	}
   148  }
   149  
   150  func TestSignTxRequest(t *testing.T) {
   151  
   152  	js := `
   153  	function ApproveTx(r){
   154  		console.log("transaction.from", r.transaction.from);
   155  		console.log("transaction.to", r.transaction.to);
   156  		console.log("transaction.value", r.transaction.value);
   157  		console.log("transaction.nonce", r.transaction.nonce);
   158  		if(r.transaction.from.toLowerCase()=="cb390000000000000000000000000000000000001337"){ return "Approve"}
   159  		if(r.transaction.from.toLowerCase()=="cb79000000000000000000000000000000000000dead"){ return "Reject"}
   160  	}`
   161  
   162  	r, err := initRuleEngine(js)
   163  	if err != nil {
   164  		t.Errorf("Couldn't create evaluator %v", err)
   165  		return
   166  	}
   167  	to, err := common.HexToAddress("cb79000000000000000000000000000000000000dead")
   168  	if err != nil {
   169  		t.Error(err)
   170  		return
   171  	}
   172  	from, err := common.HexToAddress("cb390000000000000000000000000000000000001337")
   173  
   174  	if err != nil {
   175  		t.Error(err)
   176  		return
   177  	}
   178  	t.Logf("to %v", to.String())
   179  	resp, err := r.ApproveTx(&core.SignTxRequest{
   180  		Transaction: core.SendTxArgs{
   181  			From: from,
   182  			To:   &to},
   183  		Callinfo: nil,
   184  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   185  	})
   186  	if err != nil {
   187  		t.Errorf("Unexpected error %v", err)
   188  	}
   189  	if !resp.Approved {
   190  		t.Errorf("Expected check to resolve to 'Approve'")
   191  	}
   192  }
   193  
   194  type dummyUI struct {
   195  	calls []string
   196  }
   197  
   198  func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) {
   199  	panic("implement me")
   200  }
   201  
   202  func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
   203  	d.calls = append(d.calls, "OnInputRequired")
   204  	return core.UserInputResponse{}, nil
   205  }
   206  
   207  func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
   208  	d.calls = append(d.calls, "ApproveTx")
   209  	return core.SignTxResponse{}, core.ErrRequestDenied
   210  }
   211  
   212  func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
   213  	d.calls = append(d.calls, "ApproveSignData")
   214  	return core.SignDataResponse{}, core.ErrRequestDenied
   215  }
   216  
   217  func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
   218  	d.calls = append(d.calls, "ApproveListing")
   219  	return core.ListResponse{}, core.ErrRequestDenied
   220  }
   221  
   222  func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
   223  	d.calls = append(d.calls, "ApproveNewAccount")
   224  	return core.NewAccountResponse{}, core.ErrRequestDenied
   225  }
   226  
   227  func (d *dummyUI) ShowError(message string) {
   228  	d.calls = append(d.calls, "ShowError")
   229  }
   230  
   231  func (d *dummyUI) ShowInfo(message string) {
   232  	d.calls = append(d.calls, "ShowInfo")
   233  }
   234  
   235  func (d *dummyUI) OnApprovedTx(tx xcbapi.SignTransactionResult) {
   236  	d.calls = append(d.calls, "OnApprovedTx")
   237  }
   238  
   239  func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
   240  }
   241  
   242  // TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
   243  func TestForwarding(t *testing.T) {
   244  
   245  	js := ""
   246  	ui := &dummyUI{make([]string, 0)}
   247  	jsBackend := storage.NewEphemeralStorage()
   248  	r, err := NewRuleEvaluator(ui, jsBackend)
   249  	if err != nil {
   250  		t.Fatalf("Failed to create js engine: %v", err)
   251  	}
   252  	if err = r.Init(js); err != nil {
   253  		t.Fatalf("Failed to load bootstrap js: %v", err)
   254  	}
   255  	r.ApproveSignData(nil)
   256  	r.ApproveTx(nil)
   257  	r.ApproveNewAccount(nil)
   258  	r.ApproveListing(nil)
   259  	r.ShowError("test")
   260  	r.ShowInfo("test")
   261  
   262  	//This one is not forwarded
   263  	r.OnApprovedTx(xcbapi.SignTransactionResult{})
   264  
   265  	expCalls := 6
   266  	if len(ui.calls) != expCalls {
   267  
   268  		t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
   269  
   270  	}
   271  
   272  }
   273  
   274  func TestMissingFunc(t *testing.T) {
   275  	r, err := initRuleEngine(JS)
   276  	if err != nil {
   277  		t.Errorf("Couldn't create evaluator %v", err)
   278  		return
   279  	}
   280  
   281  	_, err = r.execute("MissingMethod", "test")
   282  
   283  	if err == nil {
   284  		t.Error("Expected error")
   285  	}
   286  
   287  	approved, err := r.checkApproval("MissingMethod", nil, nil)
   288  	if err == nil {
   289  		t.Errorf("Expected missing method to yield error'")
   290  	}
   291  	if approved {
   292  		t.Errorf("Expected missing method to cause non-approval")
   293  	}
   294  	t.Logf("Err %v", err)
   295  
   296  }
   297  func TestStorage(t *testing.T) {
   298  
   299  	js := `
   300  	function testStorage(){
   301  		storage.put("mykey", "myvalue")
   302  		a = storage.get("mykey")
   303  
   304  		storage.put("mykey", ["a", "list"])  	// Should result in "a,list"
   305  		a += storage.get("mykey")
   306  
   307  
   308  		storage.put("mykey", {"an": "object"}) 	// Should result in "[object Object]"
   309  		a += storage.get("mykey")
   310  
   311  
   312  		storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
   313  		a += storage.get("mykey")
   314  
   315  		a += storage.get("missingkey")		//Missing keys should result in empty string
   316  		storage.put("","missing key==noop") // Can't store with 0-length key
   317  		a += storage.get("")				// Should result in ''
   318  
   319  		var b = new BigNumber(2)
   320  		var c = new BigNumber(16)//"0xf0",16)
   321  		var d = b.plus(c)
   322  		console.log(d)
   323  		return a
   324  	}
   325  `
   326  	r, err := initRuleEngine(js)
   327  	if err != nil {
   328  		t.Errorf("Couldn't create evaluator %v", err)
   329  		return
   330  	}
   331  
   332  	v, err := r.execute("testStorage", nil)
   333  
   334  	if err != nil {
   335  		t.Errorf("Unexpected error %v", err)
   336  	}
   337  	retval := v.ToString().String()
   338  
   339  	if err != nil {
   340  		t.Errorf("Unexpected error %v", err)
   341  	}
   342  	exp := `myvaluea,list[object Object]{"an":"object"}`
   343  	if retval != exp {
   344  		t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
   345  	}
   346  	t.Logf("Err %v", err)
   347  
   348  }
   349  
   350  const ExampleTxWindow = `
   351  	function big(str){
   352  		if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
   353  		return new BigNumber(str)
   354  	}
   355  
   356  	// Time window: 1 week
   357  	var window = 1000* 3600*24*7;
   358  
   359  	// Limit : 1 core
   360  	var limit = new BigNumber("1e18");
   361  
   362  	function isLimitOk(transaction){
   363  		var value = big(transaction.value)
   364  		// Start of our window function
   365  		var windowstart = new Date().getTime() - window;
   366  
   367  		var txs = [];
   368  		var stored = storage.get('txs');
   369  
   370  		if(stored != ""){
   371  			txs = JSON.parse(stored)
   372  		}
   373  		// First, remove all that have passed out of the time-window
   374  		var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
   375  		console.log(txs, newtxs.length);
   376  
   377  		// Secondly, aggregate the current sum
   378  		sum = new BigNumber(0)
   379  
   380  		sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
   381  		console.log("ApproveTx > Sum so far", sum);
   382  		console.log("ApproveTx > Requested", value.toNumber());
   383  
   384  		// Would we exceed weekly limit ?
   385  		return sum.plus(value).lt(limit)
   386  
   387  	}
   388  	function ApproveTx(r){
   389  		console.log(r)
   390  		console.log(typeof(r))
   391  		if (isLimitOk(r.transaction)){
   392  			return "Approve"
   393  		}
   394  		return "Nope"
   395  	}
   396  
   397  	/**
   398  	* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
   399   	* 'response_str' contains the return value that will be sent to the external caller.
   400  	* The return value from this method is ignore - the reason for having this callback is to allow the
   401  	* ruleset to keep track of approved transactions.
   402  	*
   403  	* When implementing rate-limited rules, this callback should be used.
   404  	* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
   405  	* then accepts the transaction, this method will be called.
   406  	*
   407  	* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
   408  	*/
   409   	function OnApprovedTx(resp){
   410  		var value = big(resp.tx.value)
   411  		var txs = []
   412  		// Load stored transactions
   413  		var stored = storage.get('txs');
   414  		if(stored != ""){
   415  			txs = JSON.parse(stored)
   416  		}
   417  		// Add this to the storage
   418  		txs.push({tstamp: new Date().getTime(), value: value});
   419  		storage.put("txs", JSON.stringify(txs));
   420  	}
   421  
   422  `
   423  
   424  func dummyTx(value hexutil.Big) *core.SignTxRequest {
   425  	to, err := common.HexToAddress("cb79000000000000000000000000000000000000dead")
   426  	if err != nil {
   427  		panic(err)
   428  	}
   429  	from, err := common.HexToAddress("cb79000000000000000000000000000000000000dead")
   430  	if err != nil {
   431  		panic(err)
   432  	}
   433  	n := hexutil.Uint64(3)
   434  	energy := hexutil.Uint64(21000)
   435  	energyPrice := hexutil.Big(*big.NewInt(2000000))
   436  
   437  	return &core.SignTxRequest{
   438  		Transaction: core.SendTxArgs{
   439  			From:        from,
   440  			To:          &to,
   441  			Value:       value,
   442  			Nonce:       n,
   443  			EnergyPrice: energyPrice,
   444  			Energy:      energy,
   445  		},
   446  		Callinfo: []core.ValidationInfo{
   447  			{Typ: "Warning", Message: "All your base are bellong to us"},
   448  		},
   449  		Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   450  	}
   451  }
   452  
   453  func dummyTxWithV(value uint64) *core.SignTxRequest {
   454  	v := big.NewInt(0).SetUint64(value)
   455  	h := hexutil.Big(*v)
   456  	return dummyTx(h)
   457  }
   458  
   459  func dummySigned(value *big.Int) *types.Transaction {
   460  	to, err := common.HexToAddress("cb79000000000000000000000000000000000000dead")
   461  	if err != nil {
   462  		panic(err)
   463  	}
   464  	energy := uint64(21000)
   465  	energyPrice := big.NewInt(2000000)
   466  	data := make([]byte, 0)
   467  	return types.NewTransaction(3, to, value, energy, energyPrice, data)
   468  }
   469  
   470  func TestLimitWindow(t *testing.T) {
   471  	r, err := initRuleEngine(ExampleTxWindow)
   472  	if err != nil {
   473  		t.Errorf("Couldn't create evaluator %v", err)
   474  		return
   475  	}
   476  	// 0.3 core: 429D069189E0000 ore
   477  	v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
   478  	h := hexutil.Big(*v)
   479  	// The first three should succeed
   480  	for i := 0; i < 3; i++ {
   481  		unsigned := dummyTx(h)
   482  		resp, err := r.ApproveTx(unsigned)
   483  		if err != nil {
   484  			t.Errorf("Unexpected error %v", err)
   485  		}
   486  		if !resp.Approved {
   487  			t.Errorf("Expected check to resolve to 'Approve'")
   488  		}
   489  		// Create a dummy signed transaction
   490  
   491  		response := xcbapi.SignTransactionResult{
   492  			Tx:  dummySigned(v),
   493  			Raw: common.Hex2Bytes("deadbeef"),
   494  		}
   495  		r.OnApprovedTx(response)
   496  	}
   497  	// Fourth should fail
   498  	resp, _ := r.ApproveTx(dummyTx(h))
   499  	if resp.Approved {
   500  		t.Errorf("Expected check to resolve to 'Reject'")
   501  	}
   502  }
   503  
   504  // dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
   505  type dontCallMe struct {
   506  	t *testing.T
   507  }
   508  
   509  func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
   510  	d.t.Fatalf("Did not expect next-handler to be called")
   511  	return core.UserInputResponse{}, nil
   512  }
   513  
   514  func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) {
   515  }
   516  
   517  func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
   518  }
   519  
   520  func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
   521  	d.t.Fatalf("Did not expect next-handler to be called")
   522  	return core.SignTxResponse{}, core.ErrRequestDenied
   523  }
   524  
   525  func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
   526  	d.t.Fatalf("Did not expect next-handler to be called")
   527  	return core.SignDataResponse{}, core.ErrRequestDenied
   528  }
   529  
   530  func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
   531  	d.t.Fatalf("Did not expect next-handler to be called")
   532  	return core.ListResponse{}, core.ErrRequestDenied
   533  }
   534  
   535  func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
   536  	d.t.Fatalf("Did not expect next-handler to be called")
   537  	return core.NewAccountResponse{}, core.ErrRequestDenied
   538  }
   539  
   540  func (d *dontCallMe) ShowError(message string) {
   541  	d.t.Fatalf("Did not expect next-handler to be called")
   542  }
   543  
   544  func (d *dontCallMe) ShowInfo(message string) {
   545  	d.t.Fatalf("Did not expect next-handler to be called")
   546  }
   547  
   548  func (d *dontCallMe) OnApprovedTx(tx xcbapi.SignTransactionResult) {
   549  	d.t.Fatalf("Did not expect next-handler to be called")
   550  }
   551  
   552  // TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
   553  // if it does, that would be bad since developers may rely on that to store data,
   554  // instead of using the disk-based data storage
   555  func TestContextIsCleared(t *testing.T) {
   556  
   557  	js := `
   558  	function ApproveTx(){
   559  		if (typeof foobar == 'undefined') {
   560  			foobar = "Approve"
   561   		}
   562  		console.log(foobar)
   563  		if (foobar == "Approve"){
   564  			foobar = "Reject"
   565  		}else{
   566  			foobar = "Approve"
   567  		}
   568  		return foobar
   569  	}
   570  	`
   571  	ui := &dontCallMe{t}
   572  	r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage())
   573  	if err != nil {
   574  		t.Fatalf("Failed to create js engine: %v", err)
   575  	}
   576  	if err = r.Init(js); err != nil {
   577  		t.Fatalf("Failed to load bootstrap js: %v", err)
   578  	}
   579  	tx := dummyTxWithV(0)
   580  	r1, _ := r.ApproveTx(tx)
   581  	r2, _ := r.ApproveTx(tx)
   582  	if r1.Approved != r2.Approved {
   583  		t.Errorf("Expected execution context to be cleared between executions")
   584  	}
   585  }
   586  
   587  func TestSignData(t *testing.T) {
   588  
   589  	js := `function ApproveListing(){
   590      return "Approve"
   591  }
   592  function ApproveSignData(r){
   593      if( r.address.toLowerCase() == "cb27de521e43741cf785cbad450d5649187b9612018f")
   594      {
   595          if(r.messages[0].value.indexOf("bazonk") >= 0){
   596              return "Approve"
   597          }
   598          return "Reject"
   599      }
   600      // Otherwise goes to manual processing
   601  }`
   602  	r, err := initRuleEngine(js)
   603  	if err != nil {
   604  		t.Errorf("Couldn't create evaluator %v", err)
   605  		return
   606  	}
   607  	message := "baz bazonk foo"
   608  	hash, rawdata := accounts.TextAndHash([]byte(message))
   609  	addr, err := common.HexToAddress("cb27de521e43741cf785cbad450d5649187b9612018f")
   610  	if err != nil {
   611  		t.Error(err)
   612  	}
   613  	nvt := []*core.NameValueType{
   614  		{
   615  			Name:  "message",
   616  			Typ:   "text/plain",
   617  			Value: message,
   618  		},
   619  	}
   620  	resp, err := r.ApproveSignData(&core.SignDataRequest{
   621  		Address:  addr,
   622  		Messages: nvt,
   623  		Hash:     hash,
   624  		Meta:     core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
   625  		Rawdata:  []byte(rawdata),
   626  	})
   627  	if err != nil {
   628  		t.Fatalf("Unexpected error %v", err)
   629  	}
   630  	if !resp.Approved {
   631  		t.Fatalf("Expected approved")
   632  	}
   633  }