|
1 | 1 | package e2e_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | +"bytes" |
| 5 | +"fmt" |
4 | 6 | "net/http" |
5 | 7 |
|
6 | 8 | "github.com/hookdeck/outpost/cmd/e2e/httpclient" |
7 | 9 | "github.com/hookdeck/outpost/internal/idgen" |
| 10 | +"github.com/stretchr/testify/require" |
8 | 11 | ) |
9 | 12 |
|
10 | 13 | func (suite*basicSuite)TestHealthzAPI() { |
@@ -267,10 +270,215 @@ func (suite *basicSuite) TestTenantsAPI() { |
267 | 270 | }, |
268 | 271 | }, |
269 | 272 | }, |
| 273 | +// Metadata tests |
| 274 | +{ |
| 275 | +Name:"PUT /:tenantID with metadata", |
| 276 | +Request:suite.AuthRequest(httpclient.Request{ |
| 277 | +Method:httpclient.MethodPUT, |
| 278 | +Path:"/"+tenantID, |
| 279 | +Body:map[string]interface{}{ |
| 280 | +"metadata":map[string]interface{}{ |
| 281 | +"environment":"production", |
| 282 | +"team":"platform", |
| 283 | +"region":"us-east-1", |
| 284 | +}, |
| 285 | +}, |
| 286 | +}), |
| 287 | +Expected:APITestExpectation{ |
| 288 | +Match:&httpclient.Response{ |
| 289 | +StatusCode:http.StatusOK, |
| 290 | +Body:map[string]interface{}{ |
| 291 | +"id":tenantID, |
| 292 | +"metadata":map[string]interface{}{ |
| 293 | +"environment":"production", |
| 294 | +"team":"platform", |
| 295 | +"region":"us-east-1", |
| 296 | +}, |
| 297 | +}, |
| 298 | +}, |
| 299 | +}, |
| 300 | +}, |
| 301 | +{ |
| 302 | +Name:"GET /:tenantID retrieves metadata", |
| 303 | +Request:suite.AuthRequest(httpclient.Request{ |
| 304 | +Method:httpclient.MethodGET, |
| 305 | +Path:"/"+tenantID, |
| 306 | +}), |
| 307 | +Expected:APITestExpectation{ |
| 308 | +Match:&httpclient.Response{ |
| 309 | +StatusCode:http.StatusOK, |
| 310 | +Body:map[string]interface{}{ |
| 311 | +"id":tenantID, |
| 312 | +"metadata":map[string]interface{}{ |
| 313 | +"environment":"production", |
| 314 | +"team":"platform", |
| 315 | +"region":"us-east-1", |
| 316 | +}, |
| 317 | +}, |
| 318 | +}, |
| 319 | +}, |
| 320 | +}, |
| 321 | +{ |
| 322 | +Name:"PUT /:tenantID replaces metadata (full replacement)", |
| 323 | +Request:suite.AuthRequest(httpclient.Request{ |
| 324 | +Method:httpclient.MethodPUT, |
| 325 | +Path:"/"+tenantID, |
| 326 | +Body:map[string]interface{}{ |
| 327 | +"metadata":map[string]interface{}{ |
| 328 | +"team":"engineering", |
| 329 | +"owner":"alice", |
| 330 | +}, |
| 331 | +}, |
| 332 | +}), |
| 333 | +Expected:APITestExpectation{ |
| 334 | +Match:&httpclient.Response{ |
| 335 | +StatusCode:http.StatusOK, |
| 336 | +Body:map[string]interface{}{ |
| 337 | +"id":tenantID, |
| 338 | +"metadata":map[string]interface{}{ |
| 339 | +"team":"engineering", |
| 340 | +"owner":"alice", |
| 341 | +// Note: environment and region are gone (full replacement) |
| 342 | +}, |
| 343 | +}, |
| 344 | +}, |
| 345 | +}, |
| 346 | +}, |
| 347 | +{ |
| 348 | +Name:"GET /:tenantID verifies metadata was replaced", |
| 349 | +Request:suite.AuthRequest(httpclient.Request{ |
| 350 | +Method:httpclient.MethodGET, |
| 351 | +Path:"/"+tenantID, |
| 352 | +}), |
| 353 | +Expected:APITestExpectation{ |
| 354 | +Match:&httpclient.Response{ |
| 355 | +StatusCode:http.StatusOK, |
| 356 | +Body:map[string]interface{}{ |
| 357 | +"id":tenantID, |
| 358 | +"metadata":map[string]interface{}{ |
| 359 | +"team":"engineering", |
| 360 | +"owner":"alice", |
| 361 | +}, |
| 362 | +}, |
| 363 | +}, |
| 364 | +}, |
| 365 | +}, |
| 366 | +{ |
| 367 | +Name:"PUT /:tenantID without metadata clears it", |
| 368 | +Request:suite.AuthRequest(httpclient.Request{ |
| 369 | +Method:httpclient.MethodPUT, |
| 370 | +Path:"/"+tenantID, |
| 371 | +Body:map[string]interface{}{}, |
| 372 | +}), |
| 373 | +Expected:APITestExpectation{ |
| 374 | +Match:&httpclient.Response{ |
| 375 | +StatusCode:http.StatusOK, |
| 376 | +}, |
| 377 | +}, |
| 378 | +}, |
| 379 | +{ |
| 380 | +Name:"GET /:tenantID verifies metadata is nil", |
| 381 | +Request:suite.AuthRequest(httpclient.Request{ |
| 382 | +Method:httpclient.MethodGET, |
| 383 | +Path:"/"+tenantID, |
| 384 | +}), |
| 385 | +Expected:APITestExpectation{ |
| 386 | +Match:&httpclient.Response{ |
| 387 | +StatusCode:http.StatusOK, |
| 388 | +Body:map[string]interface{}{ |
| 389 | +"id":tenantID, |
| 390 | +"destinations_count":0, |
| 391 | +"topics": []string{}, |
| 392 | +// metadata field should not be present (omitempty) |
| 393 | +}, |
| 394 | +}, |
| 395 | +}, |
| 396 | +}, |
| 397 | +{ |
| 398 | +Name:"Create new tenant with metadata", |
| 399 | +Request:suite.AuthRequest(httpclient.Request{ |
| 400 | +Method:httpclient.MethodPUT, |
| 401 | +Path:"/"+idgen.String(), |
| 402 | +Body:map[string]interface{}{ |
| 403 | +"metadata":map[string]interface{}{ |
| 404 | +"stage":"development", |
| 405 | +}, |
| 406 | +}, |
| 407 | +}), |
| 408 | +Expected:APITestExpectation{ |
| 409 | +Match:&httpclient.Response{ |
| 410 | +StatusCode:http.StatusCreated, |
| 411 | +Body:map[string]interface{}{ |
| 412 | +"metadata":map[string]interface{}{ |
| 413 | +"stage":"development", |
| 414 | +}, |
| 415 | +}, |
| 416 | +}, |
| 417 | +}, |
| 418 | +}, |
| 419 | +{ |
| 420 | +Name:"PUT /:tenantID with metadata value auto-converted (number to string)", |
| 421 | +Request:suite.AuthRequest(httpclient.Request{ |
| 422 | +Method:httpclient.MethodPUT, |
| 423 | +Path:"/"+idgen.String(), |
| 424 | +Body:map[string]interface{}{ |
| 425 | +"metadata":map[string]interface{}{ |
| 426 | +"count":42, |
| 427 | +"enabled":true, |
| 428 | +"ratio":3.14, |
| 429 | +}, |
| 430 | +}, |
| 431 | +}), |
| 432 | +Expected:APITestExpectation{ |
| 433 | +Match:&httpclient.Response{ |
| 434 | +StatusCode:http.StatusCreated, |
| 435 | +Body:map[string]interface{}{ |
| 436 | +"metadata":map[string]interface{}{ |
| 437 | +"count":"42", |
| 438 | +"enabled":"true", |
| 439 | +"ratio":"3.14", |
| 440 | +}, |
| 441 | +}, |
| 442 | +}, |
| 443 | +}, |
| 444 | +}, |
| 445 | +{ |
| 446 | +Name:"PUT /:tenantID with empty body (no metadata)", |
| 447 | +Request:suite.AuthRequest(httpclient.Request{ |
| 448 | +Method:httpclient.MethodPUT, |
| 449 | +Path:"/"+idgen.String(), |
| 450 | +Body:map[string]interface{}{}, |
| 451 | +}), |
| 452 | +Expected:APITestExpectation{ |
| 453 | +Match:&httpclient.Response{ |
| 454 | +StatusCode:http.StatusCreated, |
| 455 | +}, |
| 456 | +}, |
| 457 | +}, |
270 | 458 | } |
271 | 459 | suite.RunAPITests(suite.T(),tests) |
272 | 460 | } |
273 | 461 |
|
| 462 | +func (suite*basicSuite)TestTenantAPIInvalidJSON() { |
| 463 | +t:=suite.T() |
| 464 | +tenantID:=idgen.String() |
| 465 | +baseURL:=fmt.Sprintf("http://localhost:%d/api/v1",suite.config.APIPort) |
| 466 | + |
| 467 | +// Create tenant with malformed JSON (send raw bytes) |
| 468 | +jsonBody:= []byte(`{"metadata": invalid json}`) |
| 469 | +req,err:=http.NewRequest(httpclient.MethodPUT,baseURL+"/"+tenantID,bytes.NewReader(jsonBody)) |
| 470 | +require.NoError(t,err) |
| 471 | +req.Header.Set("Content-Type","application/json") |
| 472 | +req.Header.Set("Authorization","Bearer "+suite.config.APIKey) |
| 473 | + |
| 474 | +httpClient:=&http.Client{} |
| 475 | +resp,err:=httpClient.Do(req) |
| 476 | +require.NoError(t,err) |
| 477 | +deferresp.Body.Close() |
| 478 | + |
| 479 | +require.Equal(t,http.StatusBadRequest,resp.StatusCode,"Malformed JSON should return 400") |
| 480 | +} |
| 481 | + |
274 | 482 | func (suite*basicSuite)TestDestinationsAPI() { |
275 | 483 | tenantID:=idgen.String() |
276 | 484 | sampleDestinationID:=idgen.Destination() |
@@ -796,6 +1004,37 @@ func (suite *basicSuite) TestDestinationsAPI() { |
796 | 1004 | Validate:makeDestinationListValidator(2), |
797 | 1005 | }, |
798 | 1006 | }, |
| 1007 | +{ |
| 1008 | +Name:"POST /:tenantID/destinations with metadata auto-conversion", |
| 1009 | +Request:suite.AuthRequest(httpclient.Request{ |
| 1010 | +Method:httpclient.MethodPOST, |
| 1011 | +Path:"/"+tenantID+"/destinations", |
| 1012 | +Body:map[string]interface{}{ |
| 1013 | +"type":"webhook", |
| 1014 | +"topics":"*", |
| 1015 | +"config":map[string]interface{}{ |
| 1016 | +"url":"http://host.docker.internal:4444", |
| 1017 | +}, |
| 1018 | +"metadata":map[string]interface{}{ |
| 1019 | +"priority":10, |
| 1020 | +"enabled":true, |
| 1021 | +"version":1.5, |
| 1022 | +}, |
| 1023 | +}, |
| 1024 | +}), |
| 1025 | +Expected:APITestExpectation{ |
| 1026 | +Match:&httpclient.Response{ |
| 1027 | +StatusCode:http.StatusCreated, |
| 1028 | +Body:map[string]interface{}{ |
| 1029 | +"metadata":map[string]interface{}{ |
| 1030 | +"priority":"10", |
| 1031 | +"enabled":"true", |
| 1032 | +"version":"1.5", |
| 1033 | +}, |
| 1034 | +}, |
| 1035 | +}, |
| 1036 | +}, |
| 1037 | +}, |
799 | 1038 | } |
800 | 1039 | suite.RunAPITests(suite.T(),tests) |
801 | 1040 | } |
|