github.com/mailgun/holster/v4@v4.20.0/election/rpc_test.go (about)

     1  package election_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"testing"
    12  
    13  	"github.com/mailgun/holster/v4/election"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestRPCRequest(t *testing.T) {
    19  	for _, tt := range []struct {
    20  		name string
    21  		in   election.RPCRequest
    22  		out  string
    23  	}{
    24  		{
    25  			name: "heartbeat",
    26  			in: election.RPCRequest{
    27  				RPC: election.HeartBeatRPC,
    28  				Request: election.HeartBeatReq{
    29  					From: "node1",
    30  					Term: 1,
    31  				},
    32  			},
    33  			out: `{"rpc":"heartbeat","request":{"from":"node1","term":1}}`,
    34  		},
    35  		{
    36  			name: "vote",
    37  			in: election.RPCRequest{
    38  				RPC: election.VoteRPC,
    39  				Request: election.VoteReq{
    40  					Candidate: "node1",
    41  					Term:      1,
    42  				},
    43  			},
    44  			out: `{"rpc":"vote","request":{"candidate":"node1","term":1}}`,
    45  		},
    46  		{
    47  			name: "reset",
    48  			in: election.RPCRequest{
    49  				RPC:     election.ResetElectionRPC,
    50  				Request: election.ResetElectionReq{},
    51  			},
    52  			out: `{"rpc":"reset-election","request":{}}`,
    53  		},
    54  		{
    55  			name: "resign",
    56  			in: election.RPCRequest{
    57  				RPC:     election.ResignRPC,
    58  				Request: election.ResignReq{},
    59  			},
    60  			out: `{"rpc":"resign","request":{}}`,
    61  		},
    62  		{
    63  			name: "set-peers",
    64  			in: election.RPCRequest{
    65  				RPC:     election.SetPeersRPC,
    66  				Request: election.SetPeersReq{Peers: []string{"n0", "n1"}},
    67  			},
    68  			out: `{"rpc":"set-peers","request":{"peers":["n0","n1"]}}`,
    69  		},
    70  		{
    71  			name: "get-state",
    72  			in: election.RPCRequest{
    73  				RPC:     election.GetStateRPC,
    74  				Request: election.GetStateReq{},
    75  			},
    76  			out: `{"rpc":"get-state","request":{}}`,
    77  		},
    78  	} {
    79  		t.Run(tt.name, func(t *testing.T) {
    80  			b, err := json.Marshal(tt.in)
    81  			require.NoError(t, err)
    82  			assert.Equal(t, tt.out, string(b))
    83  
    84  			var in election.RPCRequest
    85  			err = json.Unmarshal(b, &in)
    86  			require.NoError(t, err)
    87  			assert.Equal(t, tt.in, in)
    88  		})
    89  	}
    90  
    91  }
    92  
    93  func TestRPCResponse(t *testing.T) {
    94  	for _, tt := range []struct {
    95  		name string
    96  		in   election.RPCResponse
    97  		out  string
    98  	}{
    99  		{
   100  			name: "heartbeat",
   101  			in: election.RPCResponse{
   102  				RPC: election.HeartBeatRPC,
   103  				Response: election.HeartBeatResp{
   104  					From: "node1",
   105  					Term: 1,
   106  				},
   107  			},
   108  			out: `{"rpc":"heartbeat","response":{"from":"node1","term":1}}`,
   109  		},
   110  		{
   111  			name: "vote",
   112  			in: election.RPCResponse{
   113  				RPC: election.VoteRPC,
   114  				Response: election.VoteResp{
   115  					Candidate: "node1",
   116  					Term:      1,
   117  					Granted:   true,
   118  				},
   119  			},
   120  			out: `{"rpc":"vote","response":{"candidate":"node1","term":1,"granted":true}}`,
   121  		},
   122  		{
   123  			name: "reset",
   124  			in: election.RPCResponse{
   125  				RPC:      election.ResetElectionRPC,
   126  				Response: election.ResetElectionResp{},
   127  			},
   128  			out: `{"rpc":"reset-election","response":{}}`,
   129  		},
   130  		{
   131  			name: "resign",
   132  			in: election.RPCResponse{
   133  				RPC: election.ResignRPC,
   134  				Response: election.ResignResp{
   135  					Success: true,
   136  				},
   137  			},
   138  			out: `{"rpc":"resign","response":{"success":true}}`,
   139  		},
   140  		{
   141  			name: "set-peers",
   142  			in: election.RPCResponse{
   143  				RPC:      election.SetPeersRPC,
   144  				Response: election.SetPeersResp{},
   145  			},
   146  			out: `{"rpc":"set-peers","response":{}}`,
   147  		},
   148  		{
   149  			name: "get-state",
   150  			in: election.RPCResponse{
   151  				RPC: election.GetStateRPC,
   152  				Response: election.GetStateResp{
   153  					Leader: "n0",
   154  					Peers:  []string{"n0", "n1"},
   155  					State:  "follower",
   156  				},
   157  			},
   158  			out: `{"rpc":"get-state","response":{"leader":"n0","state":"follower","peers":["n0","n1"]}}`,
   159  		},
   160  	} {
   161  		t.Run(tt.name, func(t *testing.T) {
   162  			b, err := json.Marshal(tt.in)
   163  			require.NoError(t, err)
   164  			assert.Equal(t, tt.out, string(b))
   165  
   166  			var in election.RPCResponse
   167  			err = json.Unmarshal(b, &in)
   168  			require.NoError(t, err)
   169  			assert.Equal(t, tt.in, in)
   170  
   171  		})
   172  	}
   173  }
   174  
   175  func TestHTTPServer(t *testing.T) {
   176  	ctx := context.Background()
   177  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   178  		var in election.RPCRequest
   179  
   180  		b, err := io.ReadAll(r.Body)
   181  		require.NoError(t, err)
   182  		require.NoError(t, json.Unmarshal(b, &in))
   183  		var resp election.RPCResponse
   184  
   185  		switch in.RPC {
   186  		case election.HeartBeatRPC:
   187  			resp = election.RPCResponse{
   188  				RPC: election.HeartBeatRPC,
   189  				Response: election.HeartBeatResp{
   190  					From: "node1",
   191  					Term: 10,
   192  				},
   193  			}
   194  		default:
   195  			resp = election.RPCResponse{
   196  				Error: fmt.Sprintf("invalid rpc request '%s'", in.RPC),
   197  			}
   198  		}
   199  		out, err := json.Marshal(resp)
   200  		require.NoError(t, err)
   201  		_, err = w.Write(out)
   202  		require.NoError(t, err)
   203  	}))
   204  	defer ts.Close()
   205  
   206  	// Marshall our request
   207  	b, err := json.Marshal(election.RPCRequest{
   208  		RPC: election.HeartBeatRPC,
   209  		Request: election.HeartBeatReq{
   210  			Term: 10,
   211  			From: "node10",
   212  		},
   213  	})
   214  	require.NoError(t, err)
   215  
   216  	// Send the request to the server
   217  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, ts.URL, bytes.NewBuffer(b))
   218  	require.NoError(t, err)
   219  	resp, err := http.DefaultClient.Do(req)
   220  	require.NoError(t, err)
   221  	defer func() {
   222  		err := resp.Body.Close()
   223  		require.NoError(t, err)
   224  	}()
   225  
   226  	// Unmarshall the response
   227  	var rpcResp election.RPCResponse
   228  	b, err = io.ReadAll(resp.Body)
   229  	require.NoError(t, err)
   230  	err = json.Unmarshal(b, &rpcResp)
   231  	require.NoError(t, err)
   232  
   233  	// Should have the response we expect
   234  	hb := rpcResp.Response.(election.HeartBeatResp)
   235  	assert.Equal(t, uint64(10), hb.Term)
   236  	assert.Equal(t, "node1", hb.From)
   237  
   238  	// Send an unknown rpc request to the server
   239  	req, err = http.NewRequestWithContext(ctx, http.MethodPost, ts.URL, bytes.NewBuffer([]byte(`{"rpc":"unknown"}`)))
   240  	require.NoError(t, err)
   241  	resp, err = http.DefaultClient.Do(req)
   242  	require.NoError(t, err)
   243  	defer func() {
   244  		err := resp.Body.Close()
   245  		require.NoError(t, err)
   246  	}()
   247  
   248  	// Unmarshall the response
   249  	b, err = io.ReadAll(resp.Body)
   250  	require.NoError(t, err)
   251  	err = json.Unmarshal(b, &rpcResp)
   252  	require.NoError(t, err)
   253  
   254  	// Should have the response we expect
   255  	assert.Equal(t, "invalid rpc request 'unknown'", rpcResp.Error)
   256  }