github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/namespace/schema_test.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package namespace
    22  
    23  import (
    24  	"fmt"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"strings"
    32  	"testing"
    33  
    34  	"github.com/m3db/m3/src/cluster/kv"
    35  	"github.com/m3db/m3/src/cluster/placementhandler/handleroptions"
    36  	nsproto "github.com/m3db/m3/src/dbnode/generated/proto/namespace"
    37  	"github.com/m3db/m3/src/dbnode/namespace"
    38  	"github.com/m3db/m3/src/dbnode/namespace/kvadmin"
    39  	"github.com/m3db/m3/src/x/instrument"
    40  	xjson "github.com/m3db/m3/src/x/json"
    41  
    42  	"github.com/golang/mock/gomock"
    43  	"github.com/stretchr/testify/assert"
    44  	"github.com/stretchr/testify/require"
    45  )
    46  
    47  const (
    48  	mainProtoStr = `syntax = "proto3";
    49  
    50  package mainpkg;
    51  
    52  import "mainpkg/imported.proto";
    53  
    54  message TestMessage {
    55    double latitude = 1;
    56    double longitude = 2;
    57    int64 epoch = 3;
    58    bytes deliveryID = 4;
    59    map<string, string> attributes = 5;
    60    ImportedMessage an_imported_message = 6;
    61  }
    62  `
    63  	importedProtoStr = `
    64  syntax = "proto3";
    65  
    66  package mainpkg;
    67  
    68  message ImportedMessage {
    69    double latitude = 1;
    70    double longitude = 2;
    71    int64 epoch = 3;
    72    bytes deliveryID = 4;
    73  }
    74  `
    75  	testSchemaJQ = `
    76  {
    77    name: "testNamespace",
    78    msgName: "mainpkg.TestMessage",
    79    protoName: "mainpkg/test.proto",
    80    protoMap:
    81      {
    82        "mainpkg/test.proto": $file1,
    83        "mainpkg/imported.proto": $file2
    84      }
    85  }
    86  `
    87  	testSchemaJSON = `
    88          {
    89            "name": "testNamespace",
    90            "msgName": "mainpkg.TestMessage",
    91            "protoName": "mainpkg/test.proto",
    92            "protoMap": {
    93              "mainpkg/test.proto": "syntax = \"proto3\";\n\npackage mainpkg;\n\nimport \"mainpkg/imported.proto\";\n\nmessage TestMessage {\n  double latitude = 1;\n  double longitude = 2;\n  int64 epoch = 3;\n  bytes deliveryID = 4;\n  map<string, string> attributes = 5;\n  ImportedMessage an_imported_message = 6;\n}",
    94              "mainpkg/imported.proto": "\nsyntax = \"proto3\";\n\npackage mainpkg;\n\nmessage ImportedMessage {\n  double latitude = 1;\n  double longitude = 2;\n  int64 epoch = 3;\n  bytes deliveryID = 4;\n}"
    95            }
    96          }
    97  `
    98  )
    99  
   100  var (
   101  	svcDefaults = handleroptions.ServiceNameAndDefaults{
   102  		ServiceName: "m3db",
   103  	}
   104  )
   105  
   106  func genTestJSON(t *testing.T) string {
   107  	tempDir, err := ioutil.TempDir("", "schema_deploy_test")
   108  	require.NoError(t, err)
   109  	defer os.RemoveAll(tempDir)
   110  
   111  	file1 := filepath.Join(tempDir, "test.proto")
   112  	file2 := filepath.Join(tempDir, "imported.proto")
   113  	require.NoError(t, ioutil.WriteFile(file1, []byte(mainProtoStr), 0644))
   114  	require.NoError(t, ioutil.WriteFile(file2, []byte(importedProtoStr), 0644))
   115  
   116  	jqfile := filepath.Join(tempDir, "test.jq")
   117  	require.NoError(t, ioutil.WriteFile(jqfile, []byte(testSchemaJQ), 0644))
   118  
   119  	file1var := "\"$(<" + file1 + ")\""
   120  	file2var := "\"$(<" + file2 + ")\""
   121  	cmd := exec.Command("sh", "-c", "jq -n --arg file1 "+file1var+
   122  		" --arg file2 "+file2var+" -f "+jqfile)
   123  	t.Logf("cmd args are %v\n", cmd.Args)
   124  	out, err := cmd.CombinedOutput()
   125  	require.NoError(t, err)
   126  	t.Logf("generated json is \n%s\n", string(out))
   127  	return string(out)
   128  }
   129  
   130  func TestGenTestJson(t *testing.T) {
   131  	t.Skip("skip for now as jq is not installed on buildkite")
   132  	genTestJSON(t)
   133  }
   134  
   135  func TestSchemaDeploy_KVKeyNotFound(t *testing.T) {
   136  	ctrl := gomock.NewController(t)
   137  	defer ctrl.Finish()
   138  
   139  	mockClient, mockKV := setupNamespaceTest(t, ctrl)
   140  	addHandler := NewSchemaHandler(mockClient, instrument.NewOptions())
   141  	mockClient.EXPECT().Store(gomock.Any()).Return(mockKV, nil)
   142  
   143  	// Error case where required fields are not set
   144  	w := httptest.NewRecorder()
   145  
   146  	jsonInput := xjson.Map{
   147  		"name": "testNamespace",
   148  	}
   149  
   150  	req := httptest.NewRequest("POST", "/schema",
   151  		xjson.MustNewTestReader(t, jsonInput))
   152  	require.NotNil(t, req)
   153  
   154  	mockKV.EXPECT().Get(M3DBNodeNamespacesKey).Return(nil, kv.ErrNotFound)
   155  	addHandler.ServeHTTP(svcDefaults, w, req)
   156  
   157  	resp := w.Result()
   158  	body, err := ioutil.ReadAll(resp.Body)
   159  	assert.NoError(t, err)
   160  	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
   161  	assert.JSONEq(t, `{"status":"error","error":"namespace is not found"}`, string(body))
   162  }
   163  
   164  func TestSchemaDeploy(t *testing.T) {
   165  	ctrl := gomock.NewController(t)
   166  	defer ctrl.Finish()
   167  
   168  	mockClient, mockKV := setupNamespaceTest(t, ctrl)
   169  	schemaHandler := NewSchemaHandler(mockClient, instrument.NewOptions())
   170  	mockClient.EXPECT().Store(gomock.Any()).Return(mockKV, nil)
   171  
   172  	mockAdminSvc := kvadmin.NewMockNamespaceMetadataAdminService(ctrl)
   173  	newAdminService = func(kv.Store, string, func() string) kvadmin.NamespaceMetadataAdminService { return mockAdminSvc }
   174  	defer func() { newAdminService = kvadmin.NewAdminService }()
   175  
   176  	mockAdminSvc.EXPECT().DeploySchema("testNamespace", "mainpkg/test.proto",
   177  		"mainpkg.TestMessage", gomock.Any()).Do(
   178  		func(name, file, msg string, protos map[string]string) {
   179  			schemaOpt, err := namespace.AppendSchemaOptions(nil, file, msg, protos, "first")
   180  			require.NoError(t, err)
   181  			require.Equal(t, "mainpkg.TestMessage", schemaOpt.DefaultMessageName)
   182  			sh, err := namespace.LoadSchemaHistory(schemaOpt)
   183  			require.NoError(t, err)
   184  			descr, ok := sh.GetLatest()
   185  			require.True(t, ok)
   186  			require.NotNil(t, descr)
   187  			require.Equal(t, "first", descr.DeployId())
   188  		}).Return("first", nil)
   189  
   190  	w := httptest.NewRecorder()
   191  
   192  	// TODO [haijun] buildkite does not have jq, so use the pre-generated json string.
   193  	//testSchemaJson := genTestJson(t)
   194  	req := httptest.NewRequest("POST", "/schema", strings.NewReader(testSchemaJSON))
   195  	require.NotNil(t, req)
   196  
   197  	schemaHandler.ServeHTTP(svcDefaults, w, req)
   198  
   199  	resp := w.Result()
   200  	body, _ := ioutil.ReadAll(resp.Body)
   201  	assert.Equal(t, http.StatusOK, resp.StatusCode)
   202  	assert.Equal(t, "{\"deployID\":\"first\"}", string(body))
   203  }
   204  
   205  func TestSchemaDeploy_NamespaceNotFound(t *testing.T) {
   206  	ctrl := gomock.NewController(t)
   207  	defer ctrl.Finish()
   208  
   209  	mockClient, mockKV := setupNamespaceTest(t, ctrl)
   210  	schemaHandler := NewSchemaHandler(mockClient, instrument.NewOptions())
   211  	mockClient.EXPECT().Store(gomock.Any()).Return(mockKV, nil)
   212  
   213  	jsonInput := xjson.Map{
   214  		"name": "no-such-namespace",
   215  	}
   216  
   217  	// Ensure adding to an non-existing namespace returns 404
   218  	req := httptest.NewRequest("POST", "/namespace",
   219  		xjson.MustNewTestReader(t, jsonInput))
   220  	require.NotNil(t, req)
   221  
   222  	registry := nsproto.Registry{
   223  		Namespaces: map[string]*nsproto.NamespaceOptions{
   224  			"testNamespace": {
   225  				BootstrapEnabled:  true,
   226  				FlushEnabled:      true,
   227  				SnapshotEnabled:   true,
   228  				WritesToCommitLog: true,
   229  				CleanupEnabled:    false,
   230  				RepairEnabled:     false,
   231  				RetentionOptions: &nsproto.RetentionOptions{
   232  					RetentionPeriodNanos:                     172800000000000,
   233  					BlockSizeNanos:                           7200000000000,
   234  					BufferFutureNanos:                        600000000000,
   235  					BufferPastNanos:                          600000000000,
   236  					BlockDataExpiry:                          true,
   237  					BlockDataExpiryAfterNotAccessPeriodNanos: 3600000000000,
   238  				},
   239  			},
   240  		},
   241  	}
   242  
   243  	mockValue := kv.NewMockValue(ctrl)
   244  	mockValue.EXPECT().Unmarshal(gomock.Any()).Return(nil).SetArg(0, registry)
   245  	mockValue.EXPECT().Version().Return(0)
   246  	mockKV.EXPECT().Get(M3DBNodeNamespacesKey).Return(mockValue, nil)
   247  
   248  	w := httptest.NewRecorder()
   249  	schemaHandler.ServeHTTP(svcDefaults, w, req)
   250  	resp := w.Result()
   251  	body, _ := ioutil.ReadAll(resp.Body)
   252  	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
   253  	assert.JSONEq(t, `{"status":"error","error":"namespace is not found"}`, string(body))
   254  }
   255  
   256  func TestSchemaReset(t *testing.T) {
   257  	ctrl := gomock.NewController(t)
   258  	defer ctrl.Finish()
   259  
   260  	mockClient, mockKV := setupNamespaceTest(t, ctrl)
   261  	schemaHandler := NewSchemaResetHandler(mockClient, instrument.NewOptions())
   262  	mockClient.EXPECT().Store(gomock.Any()).Return(mockKV, nil)
   263  
   264  	mockAdminSvc := kvadmin.NewMockNamespaceMetadataAdminService(ctrl)
   265  	newAdminService = func(kv.Store, string, func() string) kvadmin.NamespaceMetadataAdminService { return mockAdminSvc }
   266  	defer func() { newAdminService = kvadmin.NewAdminService }()
   267  
   268  	mockAdminSvc.EXPECT().ResetSchema("testNamespace").Return(nil)
   269  
   270  	w := httptest.NewRecorder()
   271  
   272  	jsonInput := xjson.Map{
   273  		"name": "testNamespace",
   274  	}
   275  
   276  	req := httptest.NewRequest("DELETE", "/schema",
   277  		xjson.MustNewTestReader(t, jsonInput))
   278  	require.NotNil(t, req)
   279  
   280  	schemaHandler.ServeHTTP(svcDefaults, w, req)
   281  
   282  	resp := w.Result()
   283  	body, _ := ioutil.ReadAll(resp.Body)
   284  	assert.Equal(t, http.StatusBadRequest, resp.StatusCode,
   285  		fmt.Sprintf("response: %s", body))
   286  
   287  	w = httptest.NewRecorder()
   288  	req = httptest.NewRequest("DELETE", "/schema",
   289  		xjson.MustNewTestReader(t, jsonInput))
   290  	require.NotNil(t, req)
   291  	req.Header.Add("Force", "true")
   292  
   293  	schemaHandler.ServeHTTP(svcDefaults, w, req)
   294  
   295  	resp = w.Result()
   296  	body, _ = ioutil.ReadAll(resp.Body)
   297  	assert.Equal(t, http.StatusOK, resp.StatusCode,
   298  		fmt.Sprintf("response: %s", body))
   299  	assert.Equal(t, "{}", string(body))
   300  }