github.com/influxdata/influxdb/v2@v2.7.6/dashboards/transport/http_test.go (about) 1 package transport 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "strconv" 12 "testing" 13 "time" 14 15 "github.com/go-chi/chi" 16 "github.com/google/go-cmp/cmp" 17 "github.com/influxdata/influxdb/v2" 18 "github.com/influxdata/influxdb/v2/dashboards" 19 dashboardstesting "github.com/influxdata/influxdb/v2/dashboards/testing" 20 ihttp "github.com/influxdata/influxdb/v2/http" 21 "github.com/influxdata/influxdb/v2/kit/platform" 22 "github.com/influxdata/influxdb/v2/kit/platform/errors" 23 "github.com/influxdata/influxdb/v2/kv" 24 "github.com/influxdata/influxdb/v2/label" 25 "github.com/influxdata/influxdb/v2/mock" 26 "github.com/influxdata/influxdb/v2/tenant" 27 itesting "github.com/influxdata/influxdb/v2/testing" 28 "github.com/yudai/gojsondiff" 29 "github.com/yudai/gojsondiff/formatter" 30 "go.uber.org/zap" 31 "go.uber.org/zap/zaptest" 32 ) 33 34 func newDashboardHandler(log *zap.Logger, opts ...option) *DashboardHandler { 35 deps := dashboardDependencies{ 36 dashboardService: mock.NewDashboardService(), 37 userService: mock.NewUserService(), 38 orgService: mock.NewOrganizationService(), 39 labelService: mock.NewLabelService(), 40 urmService: mock.NewUserResourceMappingService(), 41 } 42 43 for _, opt := range opts { 44 opt(&deps) 45 } 46 47 return NewDashboardHandler( 48 log, 49 deps.dashboardService, 50 deps.labelService, 51 deps.userService, 52 deps.orgService, 53 tenant.NewURMHandler( 54 log.With(zap.String("handler", "urm")), 55 influxdb.DashboardsResourceType, 56 "id", 57 deps.userService, 58 deps.urmService, 59 ), 60 label.NewHTTPEmbeddedHandler( 61 log.With(zap.String("handler", "label")), 62 influxdb.DashboardsResourceType, 63 deps.labelService, 64 ), 65 ) 66 } 67 68 func TestService_handleGetDashboards(t *testing.T) { 69 type fields struct { 70 DashboardService influxdb.DashboardService 71 LabelService influxdb.LabelService 72 } 73 type args struct { 74 queryParams map[string][]string 75 } 76 type wants struct { 77 statusCode int 78 contentType string 79 body string 80 } 81 82 tests := []struct { 83 name string 84 fields fields 85 args args 86 wants wants 87 }{ 88 { 89 name: "get all dashboards", 90 fields: fields{ 91 &mock.DashboardService{ 92 FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { 93 return []*influxdb.Dashboard{ 94 { 95 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 96 OrganizationID: 1, 97 Name: "hello", 98 Description: "oh hello there!", 99 Meta: influxdb.DashboardMeta{ 100 CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), 101 UpdatedAt: time.Date(2009, time.November, 10, 24, 0, 0, 0, time.UTC), 102 }, 103 Cells: []*influxdb.Cell{ 104 { 105 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 106 CellProperty: influxdb.CellProperty{ 107 X: 1, 108 Y: 2, 109 W: 3, 110 H: 4, 111 }, 112 }, 113 }, 114 }, 115 { 116 ID: dashboardstesting.MustIDBase16("0ca2204eca2204e0"), 117 OrganizationID: 1, 118 Meta: influxdb.DashboardMeta{ 119 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 120 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 121 }, 122 Name: "example", 123 }, 124 }, 2, nil 125 }, 126 }, 127 &mock.LabelService{ 128 FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { 129 labels := []*influxdb.Label{ 130 { 131 ID: dashboardstesting.MustIDBase16("fc3dc670a4be9b9a"), 132 Name: "label", 133 Properties: map[string]string{ 134 "color": "fff000", 135 }, 136 }, 137 } 138 return labels, nil 139 }, 140 }, 141 }, 142 args: args{}, 143 wants: wants{ 144 statusCode: http.StatusOK, 145 contentType: "application/json; charset=utf-8", 146 body: ` 147 { 148 "links": { 149 "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0" 150 }, 151 "dashboards": [ 152 { 153 "id": "da7aba5e5d81e550", 154 "orgID": "0000000000000001", 155 "name": "hello", 156 "description": "oh hello there!", 157 "labels": [ 158 { 159 "id": "fc3dc670a4be9b9a", 160 "name": "label", 161 "properties": { 162 "color": "fff000" 163 } 164 } 165 ], 166 "meta": { 167 "createdAt": "2009-11-10T23:00:00Z", 168 "updatedAt": "2009-11-11T00:00:00Z" 169 }, 170 "cells": [ 171 { 172 "id": "da7aba5e5d81e550", 173 "x": 1, 174 "y": 2, 175 "w": 3, 176 "h": 4, 177 "links": { 178 "self": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550", 179 "view": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550/view" 180 } 181 } 182 ], 183 "links": { 184 "self": "/api/v2/dashboards/da7aba5e5d81e550", 185 "org": "/api/v2/orgs/0000000000000001", 186 "members": "/api/v2/dashboards/da7aba5e5d81e550/members", 187 "owners": "/api/v2/dashboards/da7aba5e5d81e550/owners", 188 "cells": "/api/v2/dashboards/da7aba5e5d81e550/cells", 189 "labels": "/api/v2/dashboards/da7aba5e5d81e550/labels" 190 } 191 }, 192 { 193 "id": "0ca2204eca2204e0", 194 "orgID": "0000000000000001", 195 "name": "example", 196 "description": "", 197 "labels": [ 198 { 199 "id": "fc3dc670a4be9b9a", 200 "name": "label", 201 "properties": { 202 "color": "fff000" 203 } 204 } 205 ], 206 "meta": { 207 "createdAt": "2012-11-10T23:00:00Z", 208 "updatedAt": "2012-11-11T00:00:00Z" 209 }, 210 "cells": [], 211 "links": { 212 "self": "/api/v2/dashboards/0ca2204eca2204e0", 213 "org": "/api/v2/orgs/0000000000000001", 214 "members": "/api/v2/dashboards/0ca2204eca2204e0/members", 215 "owners": "/api/v2/dashboards/0ca2204eca2204e0/owners", 216 "cells": "/api/v2/dashboards/0ca2204eca2204e0/cells", 217 "labels": "/api/v2/dashboards/0ca2204eca2204e0/labels" 218 } 219 } 220 ] 221 } 222 `, 223 }, 224 }, 225 { 226 name: "get all dashboards when there are none", 227 fields: fields{ 228 &mock.DashboardService{ 229 FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { 230 return []*influxdb.Dashboard{}, 0, nil 231 }, 232 }, 233 &mock.LabelService{ 234 FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { 235 return []*influxdb.Label{}, nil 236 }, 237 }, 238 }, 239 args: args{}, 240 wants: wants{ 241 statusCode: http.StatusOK, 242 contentType: "application/json; charset=utf-8", 243 body: ` 244 { 245 "links": { 246 "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0" 247 }, 248 "dashboards": [] 249 }`, 250 }, 251 }, 252 { 253 name: "get all dashboards belonging to org 1", 254 fields: fields{ 255 &mock.DashboardService{ 256 FindDashboardsF: func(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { 257 return []*influxdb.Dashboard{ 258 { 259 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 260 OrganizationID: 1, 261 Name: "hello", 262 Description: "oh hello there!", 263 Meta: influxdb.DashboardMeta{ 264 CreatedAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), 265 UpdatedAt: time.Date(2009, time.November, 10, 24, 0, 0, 0, time.UTC), 266 }, 267 Cells: []*influxdb.Cell{ 268 { 269 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 270 CellProperty: influxdb.CellProperty{ 271 X: 1, 272 Y: 2, 273 W: 3, 274 H: 4, 275 }, 276 }, 277 }, 278 }, 279 }, 1, nil 280 }, 281 }, 282 &mock.LabelService{ 283 FindResourceLabelsFn: func(ctx context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { 284 labels := []*influxdb.Label{ 285 { 286 ID: dashboardstesting.MustIDBase16("fc3dc670a4be9b9a"), 287 Name: "label", 288 Properties: map[string]string{ 289 "color": "fff000", 290 }, 291 }, 292 } 293 return labels, nil 294 }, 295 }, 296 }, 297 args: args{ 298 map[string][]string{ 299 "orgID": {"0000000000000001"}, 300 }, 301 }, 302 wants: wants{ 303 statusCode: http.StatusOK, 304 contentType: "application/json; charset=utf-8", 305 body: ` 306 { 307 "links": { 308 "self": "/api/v2/dashboards?descending=false&limit=` + strconv.Itoa(influxdb.DefaultPageSize) + `&offset=0&orgID=0000000000000001" 309 }, 310 "dashboards": [ 311 { 312 "id": "da7aba5e5d81e550", 313 "orgID": "0000000000000001", 314 "name": "hello", 315 "description": "oh hello there!", 316 "meta": { 317 "createdAt": "2009-11-10T23:00:00Z", 318 "updatedAt": "2009-11-11T00:00:00Z" 319 }, 320 "labels": [ 321 { 322 "id": "fc3dc670a4be9b9a", 323 "name": "label", 324 "properties": { 325 "color": "fff000" 326 } 327 } 328 ], 329 "cells": [ 330 { 331 "id": "da7aba5e5d81e550", 332 "x": 1, 333 "y": 2, 334 "w": 3, 335 "h": 4, 336 "links": { 337 "self": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550", 338 "view": "/api/v2/dashboards/da7aba5e5d81e550/cells/da7aba5e5d81e550/view" 339 } 340 } 341 ], 342 "links": { 343 "self": "/api/v2/dashboards/da7aba5e5d81e550", 344 "org": "/api/v2/orgs/0000000000000001", 345 "members": "/api/v2/dashboards/da7aba5e5d81e550/members", 346 "owners": "/api/v2/dashboards/da7aba5e5d81e550/owners", 347 "cells": "/api/v2/dashboards/da7aba5e5d81e550/cells", 348 "labels": "/api/v2/dashboards/da7aba5e5d81e550/labels" 349 } 350 } 351 ] 352 } 353 `, 354 }, 355 }, 356 } 357 358 for _, tt := range tests { 359 t.Run(tt.name, func(t *testing.T) { 360 log := zaptest.NewLogger(t) 361 h := newDashboardHandler( 362 log, 363 withDashboardService(tt.fields.DashboardService), 364 withLabelService(tt.fields.LabelService), 365 ) 366 367 r := httptest.NewRequest("GET", "http://any.url", nil) 368 369 qp := r.URL.Query() 370 for k, vs := range tt.args.queryParams { 371 for _, v := range vs { 372 qp.Add(k, v) 373 } 374 } 375 r.URL.RawQuery = qp.Encode() 376 377 w := httptest.NewRecorder() 378 379 h.handleGetDashboards(w, r) 380 381 res := w.Result() 382 content := res.Header.Get("Content-Type") 383 body, _ := io.ReadAll(res.Body) 384 385 if res.StatusCode != tt.wants.statusCode { 386 t.Errorf("%q. handleGetDashboards() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 387 } 388 if tt.wants.contentType != "" && content != tt.wants.contentType { 389 t.Errorf("%q. handleGetDashboards() = %v, want %v", tt.name, content, tt.wants.contentType) 390 } 391 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 392 t.Errorf("%q, handleGetDashboards(). error unmarshalling json %v", tt.name, err) 393 } else if tt.wants.body != "" && !eq { 394 t.Errorf("%q. handleGetDashboards() = ***%s***", tt.name, diff) 395 } 396 }) 397 } 398 } 399 400 func TestService_handleGetDashboard(t *testing.T) { 401 type fields struct { 402 DashboardService influxdb.DashboardService 403 } 404 type args struct { 405 id string 406 queryString map[string]string 407 } 408 type wants struct { 409 statusCode int 410 contentType string 411 body string 412 } 413 tests := []struct { 414 name string 415 fields fields 416 args args 417 wants wants 418 }{ 419 { 420 name: "get a dashboard by id with view properties", 421 fields: fields{ 422 &mock.DashboardService{ 423 GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) { 424 return &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}}, nil 425 }, 426 FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 427 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 428 return &influxdb.Dashboard{ 429 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 430 OrganizationID: 1, 431 Meta: influxdb.DashboardMeta{ 432 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 433 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 434 }, 435 Name: "hello", 436 Cells: []*influxdb.Cell{ 437 { 438 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 439 CellProperty: influxdb.CellProperty{ 440 X: 1, 441 Y: 2, 442 W: 3, 443 H: 4, 444 }, 445 View: &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}}, 446 }, 447 }, 448 }, nil 449 } 450 451 return nil, fmt.Errorf("not found") 452 }, 453 }, 454 }, 455 args: args{ 456 id: "020f755c3c082000", 457 queryString: map[string]string{ 458 "include": "properties", 459 }, 460 }, 461 wants: wants{ 462 statusCode: http.StatusOK, 463 contentType: "application/json; charset=utf-8", 464 body: ` 465 { 466 "id": "020f755c3c082000", 467 "orgID": "0000000000000001", 468 "name": "hello", 469 "description": "", 470 "labels": [], 471 "meta": { 472 "createdAt": "2012-11-10T23:00:00Z", 473 "updatedAt": "2012-11-11T00:00:00Z" 474 }, 475 "cells": [ 476 { 477 "id": "da7aba5e5d81e550", 478 "x": 1, 479 "y": 2, 480 "w": 3, 481 "h": 4, 482 "name": "the cell name", 483 "properties": { 484 "shape": "chronograf-v2", 485 "axes": null, 486 "colors": null, 487 "geom": "", 488 "staticLegend": {}, 489 "position": "", 490 "note": "", 491 "queries": null, 492 "shadeBelow": false, 493 "hoverDimension": "", 494 "showNoteWhenEmpty": false, 495 "timeFormat": "", 496 "type": "xy", 497 "xColumn": "", 498 "generateXAxisTicks": null, 499 "xTotalTicks": 0, 500 "xTickStart": 0, 501 "xTickStep": 0, 502 "yColumn": "", 503 "generateYAxisTicks": null, 504 "yTotalTicks": 0, 505 "yTickStart": 0, 506 "yTickStep": 0, 507 "legendColorizeRows": false, 508 "legendHide": false, 509 "legendOpacity": 0, 510 "legendOrientationThreshold": 0 511 }, 512 "links": { 513 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 514 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 515 } 516 } 517 ], 518 "links": { 519 "self": "/api/v2/dashboards/020f755c3c082000", 520 "org": "/api/v2/orgs/0000000000000001", 521 "members": "/api/v2/dashboards/020f755c3c082000/members", 522 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 523 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 524 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 525 } 526 } 527 `, 528 }, 529 }, 530 { 531 name: "get a dashboard by id with view properties, but a cell doesnt exist", 532 fields: fields{ 533 &mock.DashboardService{ 534 GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) { 535 return nil, nil 536 }, 537 FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 538 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 539 return &influxdb.Dashboard{ 540 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 541 OrganizationID: 1, 542 Meta: influxdb.DashboardMeta{ 543 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 544 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 545 }, 546 Name: "hello", 547 Cells: []*influxdb.Cell{ 548 { 549 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 550 CellProperty: influxdb.CellProperty{ 551 X: 1, 552 Y: 2, 553 W: 3, 554 H: 4, 555 }, 556 }, 557 }, 558 }, nil 559 } 560 561 return nil, fmt.Errorf("not found") 562 }, 563 }, 564 }, 565 args: args{ 566 id: "020f755c3c082000", 567 queryString: map[string]string{ 568 "include": "properties", 569 }, 570 }, 571 wants: wants{ 572 statusCode: http.StatusOK, 573 contentType: "application/json; charset=utf-8", 574 body: ` 575 { 576 "id": "020f755c3c082000", 577 "orgID": "0000000000000001", 578 "name": "hello", 579 "description": "", 580 "labels": [], 581 "meta": { 582 "createdAt": "2012-11-10T23:00:00Z", 583 "updatedAt": "2012-11-11T00:00:00Z" 584 }, 585 "cells": [ 586 { 587 "id": "da7aba5e5d81e550", 588 "x": 1, 589 "y": 2, 590 "w": 3, 591 "h": 4, 592 "links": { 593 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 594 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 595 } 596 } 597 ], 598 "links": { 599 "self": "/api/v2/dashboards/020f755c3c082000", 600 "org": "/api/v2/orgs/0000000000000001", 601 "members": "/api/v2/dashboards/020f755c3c082000/members", 602 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 603 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 604 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 605 } 606 } 607 `, 608 }, 609 }, 610 { 611 name: "get a dashboard by id doesnt return cell properties if they exist by default", 612 fields: fields{ 613 &mock.DashboardService{ 614 GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*influxdb.View, error) { 615 return &influxdb.View{ViewContents: influxdb.ViewContents{Name: "the cell name"}, Properties: influxdb.XYViewProperties{Type: influxdb.ViewPropertyTypeXY}}, nil 616 }, 617 FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 618 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 619 return &influxdb.Dashboard{ 620 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 621 OrganizationID: 1, 622 Meta: influxdb.DashboardMeta{ 623 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 624 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 625 }, 626 Name: "hello", 627 Cells: []*influxdb.Cell{ 628 { 629 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 630 CellProperty: influxdb.CellProperty{ 631 X: 1, 632 Y: 2, 633 W: 3, 634 H: 4, 635 }, 636 }, 637 }, 638 }, nil 639 } 640 641 return nil, fmt.Errorf("not found") 642 }, 643 }, 644 }, 645 args: args{ 646 id: "020f755c3c082000", 647 queryString: map[string]string{}, 648 }, 649 wants: wants{ 650 statusCode: http.StatusOK, 651 contentType: "application/json; charset=utf-8", 652 body: ` 653 { 654 "id": "020f755c3c082000", 655 "orgID": "0000000000000001", 656 "name": "hello", 657 "description": "", 658 "labels": [], 659 "meta": { 660 "createdAt": "2012-11-10T23:00:00Z", 661 "updatedAt": "2012-11-11T00:00:00Z" 662 }, 663 "cells": [ 664 { 665 "id": "da7aba5e5d81e550", 666 "x": 1, 667 "y": 2, 668 "w": 3, 669 "h": 4, 670 "links": { 671 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 672 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 673 } 674 } 675 ], 676 "links": { 677 "self": "/api/v2/dashboards/020f755c3c082000", 678 "org": "/api/v2/orgs/0000000000000001", 679 "members": "/api/v2/dashboards/020f755c3c082000/members", 680 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 681 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 682 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 683 } 684 } 685 `, 686 }, 687 }, 688 { 689 name: "get a dashboard by id", 690 fields: fields{ 691 &mock.DashboardService{ 692 FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 693 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 694 return &influxdb.Dashboard{ 695 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 696 OrganizationID: 1, 697 Meta: influxdb.DashboardMeta{ 698 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 699 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 700 }, 701 Name: "hello", 702 Cells: []*influxdb.Cell{ 703 { 704 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 705 CellProperty: influxdb.CellProperty{ 706 X: 1, 707 Y: 2, 708 W: 3, 709 H: 4, 710 }, 711 }, 712 }, 713 }, nil 714 } 715 716 return nil, fmt.Errorf("not found") 717 }, 718 }, 719 }, 720 args: args{ 721 id: "020f755c3c082000", 722 }, 723 wants: wants{ 724 statusCode: http.StatusOK, 725 contentType: "application/json; charset=utf-8", 726 body: ` 727 { 728 "id": "020f755c3c082000", 729 "orgID": "0000000000000001", 730 "name": "hello", 731 "description": "", 732 "labels": [], 733 "meta": { 734 "createdAt": "2012-11-10T23:00:00Z", 735 "updatedAt": "2012-11-11T00:00:00Z" 736 }, 737 "cells": [ 738 { 739 "id": "da7aba5e5d81e550", 740 "x": 1, 741 "y": 2, 742 "w": 3, 743 "h": 4, 744 "links": { 745 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 746 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 747 } 748 } 749 ], 750 "links": { 751 "self": "/api/v2/dashboards/020f755c3c082000", 752 "org": "/api/v2/orgs/0000000000000001", 753 "members": "/api/v2/dashboards/020f755c3c082000/members", 754 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 755 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 756 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 757 } 758 } 759 `, 760 }, 761 }, 762 { 763 name: "not found", 764 fields: fields{ 765 &mock.DashboardService{ 766 FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*influxdb.Dashboard, error) { 767 return nil, &errors.Error{ 768 Code: errors.ENotFound, 769 Msg: influxdb.ErrDashboardNotFound, 770 } 771 }, 772 }, 773 }, 774 args: args{ 775 id: "020f755c3c082000", 776 }, 777 wants: wants{ 778 statusCode: http.StatusNotFound, 779 }, 780 }, 781 } 782 783 for _, tt := range tests { 784 t.Run(tt.name, func(t *testing.T) { 785 h := newDashboardHandler( 786 zaptest.NewLogger(t), 787 withDashboardService(tt.fields.DashboardService), 788 ) 789 790 r := httptest.NewRequest("GET", "http://any.url", nil) 791 792 urlQuery := r.URL.Query() 793 794 for k, v := range tt.args.queryString { 795 urlQuery.Add(k, v) 796 } 797 798 r.URL.RawQuery = urlQuery.Encode() 799 800 rctx := chi.NewRouteContext() 801 rctx.URLParams.Add("id", tt.args.id) 802 r = r.WithContext(context.WithValue( 803 context.Background(), 804 chi.RouteCtxKey, 805 rctx), 806 ) 807 808 w := httptest.NewRecorder() 809 810 h.handleGetDashboard(w, r) 811 812 res := w.Result() 813 content := res.Header.Get("Content-Type") 814 body, _ := io.ReadAll(res.Body) 815 if res.StatusCode != tt.wants.statusCode { 816 t.Errorf("%q. handleGetDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 817 } 818 if tt.wants.contentType != "" && content != tt.wants.contentType { 819 t.Errorf("%q. handleGetDashboard() = %v, want %v", tt.name, content, tt.wants.contentType) 820 } 821 if eq, diff, err := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && err != nil { 822 t.Errorf("%q, handleGetDashboard(). error unmarshalling json %v", tt.name, err) 823 } else if tt.wants.body != "" && !eq { 824 t.Errorf("%q. handleGetDashboard() = ***%s***", tt.name, diff) 825 } 826 }) 827 } 828 } 829 830 func TestService_handlePostDashboard(t *testing.T) { 831 type fields struct { 832 DashboardService influxdb.DashboardService 833 } 834 type args struct { 835 dashboard *influxdb.Dashboard 836 } 837 type wants struct { 838 statusCode int 839 contentType string 840 body string 841 } 842 843 tests := []struct { 844 name string 845 fields fields 846 args args 847 wants wants 848 }{ 849 { 850 name: "create a new dashboard", 851 fields: fields{ 852 &mock.DashboardService{ 853 CreateDashboardF: func(ctx context.Context, c *influxdb.Dashboard) error { 854 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 855 c.Meta = influxdb.DashboardMeta{ 856 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 857 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 858 } 859 return nil 860 }, 861 }, 862 }, 863 args: args{ 864 dashboard: &influxdb.Dashboard{ 865 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 866 OrganizationID: 1, 867 Name: "hello", 868 Description: "howdy there", 869 Cells: []*influxdb.Cell{ 870 { 871 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 872 CellProperty: influxdb.CellProperty{ 873 X: 1, 874 Y: 2, 875 W: 3, 876 H: 4, 877 }, 878 }, 879 }, 880 }, 881 }, 882 wants: wants{ 883 statusCode: http.StatusCreated, 884 contentType: "application/json; charset=utf-8", 885 body: ` 886 { 887 "id": "020f755c3c082000", 888 "orgID": "0000000000000001", 889 "name": "hello", 890 "description": "howdy there", 891 "labels": [], 892 "meta": { 893 "createdAt": "2012-11-10T23:00:00Z", 894 "updatedAt": "2012-11-11T00:00:00Z" 895 }, 896 "cells": [ 897 { 898 "id": "da7aba5e5d81e550", 899 "x": 1, 900 "y": 2, 901 "w": 3, 902 "h": 4, 903 "links": { 904 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 905 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 906 } 907 } 908 ], 909 "links": { 910 "self": "/api/v2/dashboards/020f755c3c082000", 911 "org": "/api/v2/orgs/0000000000000001", 912 "members": "/api/v2/dashboards/020f755c3c082000/members", 913 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 914 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 915 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 916 } 917 }`, 918 }, 919 }, 920 { 921 name: "create a new dashboard with cell view properties", 922 fields: fields{ 923 &mock.DashboardService{ 924 CreateDashboardF: func(ctx context.Context, c *influxdb.Dashboard) error { 925 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 926 c.Meta = influxdb.DashboardMeta{ 927 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 928 UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), 929 } 930 c.Cells = []*influxdb.Cell{ 931 { 932 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 933 CellProperty: influxdb.CellProperty{ 934 X: 1, 935 Y: 2, 936 W: 3, 937 H: 4, 938 }, 939 View: &influxdb.View{ 940 ViewContents: influxdb.ViewContents{ 941 Name: "hello a view", 942 }, 943 Properties: influxdb.XYViewProperties{ 944 Type: influxdb.ViewPropertyTypeXY, 945 Note: "note", 946 }, 947 }, 948 }, 949 } 950 return nil 951 }, 952 }, 953 }, 954 args: args{ 955 dashboard: &influxdb.Dashboard{ 956 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 957 OrganizationID: 1, 958 Name: "hello", 959 Description: "howdy there", 960 Cells: []*influxdb.Cell{ 961 { 962 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 963 CellProperty: influxdb.CellProperty{ 964 X: 1, 965 Y: 2, 966 W: 3, 967 H: 4, 968 }, 969 View: &influxdb.View{ 970 ViewContents: influxdb.ViewContents{ 971 Name: "hello a view", 972 }, 973 Properties: struct { 974 influxdb.XYViewProperties 975 Shape string 976 }{ 977 XYViewProperties: influxdb.XYViewProperties{ 978 Note: "note", 979 Type: influxdb.ViewPropertyTypeXY, 980 }, 981 Shape: "chronograf-v2", 982 }, 983 }, 984 }, 985 }, 986 }, 987 }, 988 wants: wants{ 989 statusCode: http.StatusCreated, 990 contentType: "application/json; charset=utf-8", 991 body: ` 992 { 993 "id": "020f755c3c082000", 994 "orgID": "0000000000000001", 995 "name": "hello", 996 "description": "howdy there", 997 "labels": [], 998 "meta": { 999 "createdAt": "2012-11-10T23:00:00Z", 1000 "updatedAt": "2012-11-11T00:00:00Z" 1001 }, 1002 "cells": [ 1003 { 1004 "id": "da7aba5e5d81e550", 1005 "x": 1, 1006 "y": 2, 1007 "w": 3, 1008 "h": 4, 1009 "name": "hello a view", 1010 "properties": { 1011 "shape": "chronograf-v2", 1012 "axes": null, 1013 "colors": null, 1014 "geom": "", 1015 "staticLegend": {}, 1016 "note": "note", 1017 "position": "", 1018 "queries": null, 1019 "shadeBelow": false, 1020 "hoverDimension": "", 1021 "showNoteWhenEmpty": false, 1022 "timeFormat": "", 1023 "type": "", 1024 "xColumn": "", 1025 "generateXAxisTicks": null, 1026 "xTotalTicks": 0, 1027 "xTickStart": 0, 1028 "xTickStep": 0, 1029 "yColumn": "", 1030 "generateYAxisTicks": null, 1031 "yTotalTicks": 0, 1032 "yTickStart": 0, 1033 "yTickStep": 0, 1034 "type": "xy", 1035 "legendColorizeRows": false, 1036 "legendHide": false, 1037 "legendOpacity": 0, 1038 "legendOrientationThreshold": 0 1039 }, 1040 "links": { 1041 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 1042 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 1043 } 1044 } 1045 ], 1046 "links": { 1047 "self": "/api/v2/dashboards/020f755c3c082000", 1048 "org": "/api/v2/orgs/0000000000000001", 1049 "members": "/api/v2/dashboards/020f755c3c082000/members", 1050 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 1051 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 1052 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 1053 } 1054 }`, 1055 }, 1056 }, 1057 } 1058 1059 for _, tt := range tests { 1060 t.Run(tt.name, func(t *testing.T) { 1061 h := newDashboardHandler( 1062 zaptest.NewLogger(t), 1063 withDashboardService(tt.fields.DashboardService), 1064 ) 1065 1066 b, err := json.Marshal(tt.args.dashboard) 1067 if err != nil { 1068 t.Fatalf("failed to unmarshal dashboard: %v", err) 1069 } 1070 1071 r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b)) 1072 w := httptest.NewRecorder() 1073 1074 h.handlePostDashboard(w, r) 1075 1076 res := w.Result() 1077 content := res.Header.Get("Content-Type") 1078 body, _ := io.ReadAll(res.Body) 1079 1080 if res.StatusCode != tt.wants.statusCode { 1081 t.Errorf("%q. handlePostDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1082 } 1083 if tt.wants.contentType != "" && content != tt.wants.contentType { 1084 t.Errorf("%q. handlePostDashboard() = %v, want %v", tt.name, content, tt.wants.contentType) 1085 } 1086 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 1087 t.Errorf("%q, handlePostDashboard(). error unmarshalling json %v", tt.name, err) 1088 } else if tt.wants.body != "" && !eq { 1089 t.Errorf("%q. handlePostDashboard() = ***%s***", tt.name, diff) 1090 } 1091 }) 1092 } 1093 } 1094 1095 func TestService_handleDeleteDashboard(t *testing.T) { 1096 type fields struct { 1097 DashboardService influxdb.DashboardService 1098 } 1099 type args struct { 1100 id string 1101 } 1102 type wants struct { 1103 statusCode int 1104 contentType string 1105 body string 1106 } 1107 1108 tests := []struct { 1109 name string 1110 fields fields 1111 args args 1112 wants wants 1113 }{ 1114 { 1115 name: "remove a dashboard by id", 1116 fields: fields{ 1117 &mock.DashboardService{ 1118 DeleteDashboardF: func(ctx context.Context, id platform.ID) error { 1119 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 1120 return nil 1121 } 1122 1123 return fmt.Errorf("wrong id") 1124 }, 1125 }, 1126 }, 1127 args: args{ 1128 id: "020f755c3c082000", 1129 }, 1130 wants: wants{ 1131 statusCode: http.StatusNoContent, 1132 }, 1133 }, 1134 { 1135 name: "dashboard not found", 1136 fields: fields{ 1137 &mock.DashboardService{ 1138 DeleteDashboardF: func(ctx context.Context, id platform.ID) error { 1139 return &errors.Error{ 1140 Code: errors.ENotFound, 1141 Msg: influxdb.ErrDashboardNotFound, 1142 } 1143 }, 1144 }, 1145 }, 1146 args: args{ 1147 id: "020f755c3c082000", 1148 }, 1149 wants: wants{ 1150 statusCode: http.StatusNotFound, 1151 }, 1152 }, 1153 } 1154 1155 for _, tt := range tests { 1156 t.Run(tt.name, func(t *testing.T) { 1157 h := newDashboardHandler( 1158 zaptest.NewLogger(t), 1159 withDashboardService(tt.fields.DashboardService), 1160 ) 1161 1162 r := httptest.NewRequest("GET", "http://any.url", nil) 1163 1164 rctx := chi.NewRouteContext() 1165 rctx.URLParams.Add("id", tt.args.id) 1166 r = r.WithContext(context.WithValue( 1167 context.Background(), 1168 chi.RouteCtxKey, 1169 rctx), 1170 ) 1171 w := httptest.NewRecorder() 1172 1173 h.handleDeleteDashboard(w, r) 1174 1175 res := w.Result() 1176 content := res.Header.Get("Content-Type") 1177 body, _ := io.ReadAll(res.Body) 1178 1179 if res.StatusCode != tt.wants.statusCode { 1180 t.Errorf("%q. handleDeleteDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1181 } 1182 if tt.wants.contentType != "" && content != tt.wants.contentType { 1183 t.Errorf("%q. handleDeleteDashboard() = %v, want %v", tt.name, content, tt.wants.contentType) 1184 } 1185 if tt.wants.body != "" { 1186 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 1187 t.Errorf("%q, handleDeleteDashboard(). error unmarshalling json %v", tt.name, err) 1188 } else if !eq { 1189 t.Errorf("%q. handleDeleteDashboard() = ***%s***", tt.name, diff) 1190 } 1191 } 1192 }) 1193 } 1194 } 1195 1196 func TestService_handlePatchDashboard(t *testing.T) { 1197 type fields struct { 1198 DashboardService influxdb.DashboardService 1199 } 1200 type args struct { 1201 id string 1202 name string 1203 } 1204 type wants struct { 1205 statusCode int 1206 contentType string 1207 body string 1208 } 1209 1210 tests := []struct { 1211 name string 1212 fields fields 1213 args args 1214 wants wants 1215 }{ 1216 { 1217 name: "update a dashboard name", 1218 fields: fields{ 1219 &mock.DashboardService{ 1220 UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) { 1221 if id == dashboardstesting.MustIDBase16("020f755c3c082000") { 1222 d := &influxdb.Dashboard{ 1223 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 1224 OrganizationID: 1, 1225 Name: "hello", 1226 Meta: influxdb.DashboardMeta{ 1227 CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), 1228 UpdatedAt: time.Date(2012, time.November, 10, 25, 0, 0, 0, time.UTC), 1229 }, 1230 Cells: []*influxdb.Cell{ 1231 { 1232 ID: dashboardstesting.MustIDBase16("da7aba5e5d81e550"), 1233 CellProperty: influxdb.CellProperty{ 1234 X: 1, 1235 Y: 2, 1236 W: 3, 1237 H: 4, 1238 }, 1239 }, 1240 }, 1241 } 1242 1243 if upd.Name != nil { 1244 d.Name = *upd.Name 1245 } 1246 1247 return d, nil 1248 } 1249 1250 return nil, fmt.Errorf("not found") 1251 }, 1252 }, 1253 }, 1254 args: args{ 1255 id: "020f755c3c082000", 1256 name: "example", 1257 }, 1258 wants: wants{ 1259 statusCode: http.StatusOK, 1260 contentType: "application/json; charset=utf-8", 1261 body: ` 1262 { 1263 "id": "020f755c3c082000", 1264 "orgID": "0000000000000001", 1265 "name": "example", 1266 "description": "", 1267 "labels": [], 1268 "meta": { 1269 "createdAt": "2012-11-10T23:00:00Z", 1270 "updatedAt": "2012-11-11T01:00:00Z" 1271 }, 1272 "cells": [ 1273 { 1274 "id": "da7aba5e5d81e550", 1275 "x": 1, 1276 "y": 2, 1277 "w": 3, 1278 "h": 4, 1279 "links": { 1280 "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", 1281 "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" 1282 } 1283 } 1284 ], 1285 "links": { 1286 "self": "/api/v2/dashboards/020f755c3c082000", 1287 "org": "/api/v2/orgs/0000000000000001", 1288 "members": "/api/v2/dashboards/020f755c3c082000/members", 1289 "owners": "/api/v2/dashboards/020f755c3c082000/owners", 1290 "cells": "/api/v2/dashboards/020f755c3c082000/cells", 1291 "labels": "/api/v2/dashboards/020f755c3c082000/labels" 1292 } 1293 } 1294 `, 1295 }, 1296 }, 1297 { 1298 name: "update a dashboard with empty request body", 1299 fields: fields{ 1300 &mock.DashboardService{ 1301 UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) { 1302 return nil, fmt.Errorf("not found") 1303 }, 1304 }, 1305 }, 1306 args: args{ 1307 id: "020f755c3c082000", 1308 }, 1309 wants: wants{ 1310 statusCode: http.StatusBadRequest, 1311 }, 1312 }, 1313 { 1314 name: "dashboard not found", 1315 fields: fields{ 1316 &mock.DashboardService{ 1317 UpdateDashboardF: func(ctx context.Context, id platform.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) { 1318 return nil, &errors.Error{ 1319 Code: errors.ENotFound, 1320 Msg: influxdb.ErrDashboardNotFound, 1321 } 1322 }, 1323 }, 1324 }, 1325 args: args{ 1326 id: "020f755c3c082000", 1327 name: "hello", 1328 }, 1329 wants: wants{ 1330 statusCode: http.StatusNotFound, 1331 }, 1332 }, 1333 } 1334 1335 for _, tt := range tests { 1336 t.Run(tt.name, func(t *testing.T) { 1337 h := newDashboardHandler( 1338 zaptest.NewLogger(t), 1339 withDashboardService(tt.fields.DashboardService), 1340 ) 1341 1342 upd := influxdb.DashboardUpdate{} 1343 if tt.args.name != "" { 1344 upd.Name = &tt.args.name 1345 } 1346 1347 b, err := json.Marshal(upd) 1348 if err != nil { 1349 t.Fatalf("failed to unmarshal dashboard update: %v", err) 1350 } 1351 1352 r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b)) 1353 1354 rctx := chi.NewRouteContext() 1355 rctx.URLParams.Add("id", tt.args.id) 1356 r = r.WithContext(context.WithValue( 1357 context.Background(), 1358 chi.RouteCtxKey, 1359 rctx), 1360 ) 1361 1362 w := httptest.NewRecorder() 1363 1364 h.handlePatchDashboard(w, r) 1365 1366 res := w.Result() 1367 content := res.Header.Get("Content-Type") 1368 body, _ := io.ReadAll(res.Body) 1369 1370 if res.StatusCode != tt.wants.statusCode { 1371 t.Errorf("%q. handlePatchDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1372 } 1373 if tt.wants.contentType != "" && content != tt.wants.contentType { 1374 t.Errorf("%q. handlePatchDashboard() = %v, want %v", tt.name, content, tt.wants.contentType) 1375 } 1376 if tt.wants.body != "" { 1377 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 1378 t.Errorf("%q, handlePatchDashboard(). error unmarshalling json %v", tt.name, err) 1379 } else if !eq { 1380 t.Errorf("%q. handlePatchDashboard() = ***%s***", tt.name, diff) 1381 } 1382 } 1383 }) 1384 } 1385 } 1386 1387 func TestService_handlePostDashboardCell(t *testing.T) { 1388 type fields struct { 1389 DashboardService influxdb.DashboardService 1390 } 1391 type args struct { 1392 id string 1393 body string 1394 } 1395 type wants struct { 1396 statusCode int 1397 contentType string 1398 body string 1399 } 1400 1401 tests := []struct { 1402 name string 1403 fields fields 1404 args args 1405 wants wants 1406 }{ 1407 { 1408 name: "empty body", 1409 fields: fields{ 1410 &mock.DashboardService{ 1411 AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error { 1412 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 1413 return nil 1414 }, 1415 }, 1416 }, 1417 args: args{ 1418 id: "020f755c3c082000", 1419 }, 1420 wants: wants{ 1421 statusCode: http.StatusBadRequest, 1422 contentType: "application/json; charset=utf-8", 1423 body: `{"code":"invalid","message":"bad request json body: EOF"}`, 1424 }, 1425 }, 1426 { 1427 name: "no properties", 1428 fields: fields{ 1429 &mock.DashboardService{ 1430 AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error { 1431 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 1432 return nil 1433 }, 1434 }, 1435 }, 1436 args: args{ 1437 id: "020f755c3c082000", 1438 body: `{"bad":1}`, 1439 }, 1440 wants: wants{ 1441 statusCode: http.StatusBadRequest, 1442 contentType: "application/json; charset=utf-8", 1443 body: ` 1444 { 1445 "code": "invalid", 1446 "message": "req body is empty" 1447 }`, 1448 }, 1449 }, 1450 { 1451 name: "bad dash id", 1452 fields: fields{ 1453 &mock.DashboardService{ 1454 AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error { 1455 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 1456 return nil 1457 }, 1458 }, 1459 }, 1460 args: args{ 1461 id: "fff", 1462 body: `{}`, 1463 }, 1464 wants: wants{ 1465 statusCode: http.StatusBadRequest, 1466 contentType: "application/json; charset=utf-8", 1467 body: ` 1468 { 1469 "code": "invalid", 1470 "message": "id must have a length of 16 bytes" 1471 }`, 1472 }, 1473 }, 1474 { 1475 name: "general create a dashboard cell", 1476 fields: fields{ 1477 &mock.DashboardService{ 1478 AddDashboardCellF: func(ctx context.Context, id platform.ID, c *influxdb.Cell, opt influxdb.AddDashboardCellOptions) error { 1479 c.ID = dashboardstesting.MustIDBase16("020f755c3c082000") 1480 return nil 1481 }, 1482 GetDashboardCellViewF: func(ctx context.Context, id1, id2 platform.ID) (*influxdb.View, error) { 1483 return &influxdb.View{ 1484 ViewContents: influxdb.ViewContents{ 1485 ID: dashboardstesting.MustIDBase16("020f755c3c082001"), 1486 }}, nil 1487 }, 1488 }, 1489 }, 1490 args: args{ 1491 id: "020f755c3c082000", 1492 body: `{"x":10,"y":11,"name":"name1","usingView":"020f755c3c082001"}`, 1493 }, 1494 wants: wants{ 1495 statusCode: http.StatusCreated, 1496 contentType: "application/json; charset=utf-8", 1497 body: ` 1498 { 1499 "id": "020f755c3c082000", 1500 "x": 10, 1501 "y": 11, 1502 "w": 0, 1503 "h": 0, 1504 "links": { 1505 "self": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000", 1506 "view": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000/view" 1507 } 1508 } 1509 `, 1510 }, 1511 }, 1512 } 1513 1514 for _, tt := range tests { 1515 t.Run(tt.name, func(t *testing.T) { 1516 h := newDashboardHandler( 1517 zaptest.NewLogger(t), 1518 withDashboardService(tt.fields.DashboardService), 1519 ) 1520 1521 buf := new(bytes.Buffer) 1522 _, _ = buf.WriteString(tt.args.body) 1523 r := httptest.NewRequest("POST", "http://any.url", buf) 1524 1525 rctx := chi.NewRouteContext() 1526 rctx.URLParams.Add("id", tt.args.id) 1527 r = r.WithContext(context.WithValue( 1528 context.Background(), 1529 chi.RouteCtxKey, 1530 rctx), 1531 ) 1532 1533 w := httptest.NewRecorder() 1534 1535 h.handlePostDashboardCell(w, r) 1536 1537 res := w.Result() 1538 content := res.Header.Get("Content-Type") 1539 body, _ := io.ReadAll(res.Body) 1540 1541 if res.StatusCode != tt.wants.statusCode { 1542 t.Errorf("%q. handlePostDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1543 } 1544 if tt.wants.contentType != "" && content != tt.wants.contentType { 1545 t.Errorf("%q. handlePostDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType) 1546 } 1547 if tt.wants.body != "" { 1548 if eq, diff, err := jsonEqual(tt.wants.body, string(body)); err != nil { 1549 t.Errorf("%q, handlePostDashboardCell(). error unmarshalling json %v", tt.name, err) 1550 } else if !eq { 1551 t.Errorf("%q. handlePostDashboardCell() = ***%s***", tt.name, diff) 1552 } 1553 } 1554 }) 1555 } 1556 } 1557 1558 func TestService_handleDeleteDashboardCell(t *testing.T) { 1559 type fields struct { 1560 DashboardService influxdb.DashboardService 1561 } 1562 type args struct { 1563 id string 1564 cellID string 1565 } 1566 type wants struct { 1567 statusCode int 1568 contentType string 1569 body string 1570 } 1571 1572 tests := []struct { 1573 name string 1574 fields fields 1575 args args 1576 wants wants 1577 }{ 1578 { 1579 name: "remove a dashboard cell", 1580 fields: fields{ 1581 &mock.DashboardService{ 1582 RemoveDashboardCellF: func(ctx context.Context, id platform.ID, cellID platform.ID) error { 1583 return nil 1584 }, 1585 }, 1586 }, 1587 args: args{ 1588 id: "020f755c3c082000", 1589 cellID: "020f755c3c082000", 1590 }, 1591 wants: wants{ 1592 statusCode: http.StatusNoContent, 1593 }, 1594 }, 1595 } 1596 1597 for _, tt := range tests { 1598 t.Run(tt.name, func(t *testing.T) { 1599 h := newDashboardHandler( 1600 zaptest.NewLogger(t), 1601 withDashboardService(tt.fields.DashboardService), 1602 ) 1603 1604 r := httptest.NewRequest("GET", "http://any.url", nil) 1605 1606 rctx := chi.NewRouteContext() 1607 rctx.URLParams.Add("id", tt.args.id) 1608 rctx.URLParams.Add("cellID", tt.args.cellID) 1609 r = r.WithContext(context.WithValue( 1610 context.Background(), 1611 chi.RouteCtxKey, 1612 rctx), 1613 ) 1614 1615 w := httptest.NewRecorder() 1616 1617 h.handleDeleteDashboardCell(w, r) 1618 1619 res := w.Result() 1620 content := res.Header.Get("Content-Type") 1621 body, _ := io.ReadAll(res.Body) 1622 1623 if res.StatusCode != tt.wants.statusCode { 1624 t.Errorf("%q. handleDeleteDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1625 } 1626 if tt.wants.contentType != "" && content != tt.wants.contentType { 1627 t.Errorf("%q. handleDeleteDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType) 1628 } 1629 if tt.wants.body != "" { 1630 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 1631 t.Errorf("%q, handleDeleteDashboardCell(). error unmarshalling json %v", tt.name, err) 1632 } else if !eq { 1633 t.Errorf("%q. handleDeleteDashboardCell() = ***%s***", tt.name, diff) 1634 } 1635 } 1636 }) 1637 } 1638 } 1639 1640 func TestService_handlePatchDashboardCell(t *testing.T) { 1641 type fields struct { 1642 DashboardService influxdb.DashboardService 1643 } 1644 type args struct { 1645 id string 1646 cellID string 1647 x int32 1648 y int32 1649 w int32 1650 h int32 1651 } 1652 type wants struct { 1653 statusCode int 1654 contentType string 1655 body string 1656 } 1657 1658 tests := []struct { 1659 name string 1660 fields fields 1661 args args 1662 wants wants 1663 }{ 1664 { 1665 name: "update a dashboard cell", 1666 fields: fields{ 1667 &mock.DashboardService{ 1668 UpdateDashboardCellF: func(ctx context.Context, id, cellID platform.ID, upd influxdb.CellUpdate) (*influxdb.Cell, error) { 1669 cell := &influxdb.Cell{ 1670 ID: dashboardstesting.MustIDBase16("020f755c3c082000"), 1671 } 1672 1673 if err := upd.Apply(cell); err != nil { 1674 return nil, err 1675 } 1676 1677 return cell, nil 1678 }, 1679 }, 1680 }, 1681 args: args{ 1682 id: "020f755c3c082000", 1683 cellID: "020f755c3c082000", 1684 x: 10, 1685 y: 11, 1686 }, 1687 wants: wants{ 1688 statusCode: http.StatusOK, 1689 contentType: "application/json; charset=utf-8", 1690 body: ` 1691 { 1692 "id": "020f755c3c082000", 1693 "x": 10, 1694 "y": 11, 1695 "w": 0, 1696 "h": 0, 1697 "links": { 1698 "self": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000", 1699 "view": "/api/v2/dashboards/020f755c3c082000/cells/020f755c3c082000/view" 1700 } 1701 } 1702 `, 1703 }, 1704 }, 1705 } 1706 1707 for _, tt := range tests { 1708 t.Run(tt.name, func(t *testing.T) { 1709 h := newDashboardHandler( 1710 zaptest.NewLogger(t), 1711 withDashboardService(tt.fields.DashboardService), 1712 ) 1713 1714 upd := influxdb.CellUpdate{} 1715 if tt.args.x != 0 { 1716 upd.X = &tt.args.x 1717 } 1718 if tt.args.y != 0 { 1719 upd.Y = &tt.args.y 1720 } 1721 if tt.args.w != 0 { 1722 upd.W = &tt.args.w 1723 } 1724 if tt.args.h != 0 { 1725 upd.H = &tt.args.h 1726 } 1727 1728 b, err := json.Marshal(upd) 1729 if err != nil { 1730 t.Fatalf("failed to unmarshal cell: %v", err) 1731 } 1732 1733 r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b)) 1734 1735 rctx := chi.NewRouteContext() 1736 rctx.URLParams.Add("id", tt.args.id) 1737 rctx.URLParams.Add("cellID", tt.args.cellID) 1738 r = r.WithContext(context.WithValue( 1739 context.Background(), 1740 chi.RouteCtxKey, 1741 rctx), 1742 ) 1743 w := httptest.NewRecorder() 1744 1745 h.handlePatchDashboardCell(w, r) 1746 1747 res := w.Result() 1748 content := res.Header.Get("Content-Type") 1749 body, _ := io.ReadAll(res.Body) 1750 1751 if res.StatusCode != tt.wants.statusCode { 1752 t.Errorf("%q. handlePatchDashboardCell() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) 1753 } 1754 if tt.wants.contentType != "" && content != tt.wants.contentType { 1755 t.Errorf("%q. handlePatchDashboardCell() = %v, want %v", tt.name, content, tt.wants.contentType) 1756 } 1757 if tt.wants.body != "" { 1758 if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil { 1759 t.Errorf("%q, handlePatchDashboardCell(). error unmarshalling json %v", tt.name, err) 1760 } else if !eq { 1761 t.Errorf("%q. handlePatchDashboardCell() = ***%s***", tt.name, diff) 1762 } 1763 } 1764 }) 1765 } 1766 } 1767 1768 func Test_dashboardCellIDPath(t *testing.T) { 1769 t.Parallel() 1770 dashboard, err := platform.IDFromString("deadbeefdeadbeef") 1771 if err != nil { 1772 t.Fatal(err) 1773 } 1774 1775 cell, err := platform.IDFromString("cade9a7ecade9a7e") 1776 if err != nil { 1777 t.Fatal(err) 1778 } 1779 1780 want := "/api/v2/dashboards/deadbeefdeadbeef/cells/cade9a7ecade9a7e" 1781 if got := dashboardCellIDPath(*dashboard, *cell); got != want { 1782 t.Errorf("dashboardCellIDPath() = got: %s want: %s", got, want) 1783 } 1784 } 1785 1786 func initDashboardService(f dashboardstesting.DashboardFields, t *testing.T) (influxdb.DashboardService, string, func()) { 1787 t.Helper() 1788 log := zaptest.NewLogger(t) 1789 store := itesting.NewTestInmemStore(t) 1790 1791 kvsvc := kv.NewService(log, store, &mock.OrganizationService{}) 1792 kvsvc.IDGenerator = f.IDGenerator 1793 1794 svc := dashboards.NewService( 1795 store, 1796 kvsvc, // operation log storage 1797 ) 1798 1799 svc.IDGenerator = f.IDGenerator 1800 1801 ctx := context.Background() 1802 1803 for _, d := range f.Dashboards { 1804 if err := svc.PutDashboard(ctx, d); err != nil { 1805 t.Fatalf("failed to populate dashboard") 1806 } 1807 } 1808 1809 h := newDashboardHandler( 1810 log, 1811 withDashboardService(svc), 1812 ) 1813 1814 r := chi.NewRouter() 1815 r.Mount(h.Prefix(), h) 1816 server := httptest.NewServer(r) 1817 1818 httpClient, err := ihttp.NewHTTPClient(server.URL, "", false) 1819 if err != nil { 1820 t.Fatal(err) 1821 } 1822 1823 client := DashboardService{Client: httpClient} 1824 1825 return &client, "", server.Close 1826 } 1827 1828 func TestDashboardService(t *testing.T) { 1829 t.Parallel() 1830 dashboardstesting.DeleteDashboard(initDashboardService, t) 1831 } 1832 1833 func TestService_handlePostDashboardLabel(t *testing.T) { 1834 type fields struct { 1835 LabelService influxdb.LabelService 1836 } 1837 type args struct { 1838 labelMapping *influxdb.LabelMapping 1839 dashboardID platform.ID 1840 } 1841 type wants struct { 1842 statusCode int 1843 contentType string 1844 body string 1845 } 1846 1847 tests := []struct { 1848 name string 1849 fields fields 1850 args args 1851 wants wants 1852 }{ 1853 { 1854 name: "add label to dashboard", 1855 fields: fields{ 1856 LabelService: &mock.LabelService{ 1857 FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*influxdb.Label, error) { 1858 return &influxdb.Label{ 1859 ID: 1, 1860 Name: "label", 1861 Properties: map[string]string{ 1862 "color": "fff000", 1863 }, 1864 }, nil 1865 }, 1866 CreateLabelMappingFn: func(ctx context.Context, m *influxdb.LabelMapping) error { return nil }, 1867 }, 1868 }, 1869 args: args{ 1870 labelMapping: &influxdb.LabelMapping{ 1871 ResourceID: 100, 1872 LabelID: 1, 1873 }, 1874 dashboardID: 100, 1875 }, 1876 wants: wants{ 1877 statusCode: http.StatusCreated, 1878 contentType: "application/json; charset=utf-8", 1879 body: ` 1880 { 1881 "label": { 1882 "id": "0000000000000001", 1883 "name": "label", 1884 "properties": { 1885 "color": "fff000" 1886 } 1887 }, 1888 "links": { 1889 "self": "/api/v2/labels/0000000000000001" 1890 } 1891 } 1892 `, 1893 }, 1894 }, 1895 } 1896 1897 for _, tt := range tests { 1898 t.Run(tt.name, func(t *testing.T) { 1899 h := newDashboardHandler( 1900 zaptest.NewLogger(t), 1901 withLabelService(tt.fields.LabelService), 1902 withDashboardService(&mock.DashboardService{ 1903 FindDashboardByIDF: func(_ context.Context, id platform.ID) (*influxdb.Dashboard, error) { 1904 return &influxdb.Dashboard{ 1905 ID: id, 1906 OrganizationID: platform.ID(25), 1907 }, nil 1908 }, 1909 }), 1910 ) 1911 1912 router := chi.NewRouter() 1913 router.Mount(h.Prefix(), h) 1914 1915 b, err := json.Marshal(tt.args.labelMapping) 1916 if err != nil { 1917 t.Fatalf("failed to unmarshal label mapping: %v", err) 1918 } 1919 1920 url := fmt.Sprintf("http://localhost:8086/api/v2/dashboards/%s/labels", tt.args.dashboardID) 1921 r := httptest.NewRequest("POST", url, bytes.NewReader(b)) 1922 w := httptest.NewRecorder() 1923 1924 router.ServeHTTP(w, r) 1925 1926 res := w.Result() 1927 content := res.Header.Get("Content-Type") 1928 body, _ := io.ReadAll(res.Body) 1929 1930 if res.StatusCode != tt.wants.statusCode { 1931 t.Errorf("got %v, want %v", res.StatusCode, tt.wants.statusCode) 1932 } 1933 if tt.wants.contentType != "" && content != tt.wants.contentType { 1934 t.Errorf("got %v, want %v", content, tt.wants.contentType) 1935 } 1936 if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { 1937 t.Errorf("Diff\n%s", diff) 1938 } 1939 }) 1940 } 1941 } 1942 1943 func jsonEqual(s1, s2 string) (eq bool, diff string, err error) { 1944 if s1 == s2 { 1945 return true, "", nil 1946 } 1947 1948 if s1 == "" { 1949 return false, s2, fmt.Errorf("s1 is empty") 1950 } 1951 1952 if s2 == "" { 1953 return false, s1, fmt.Errorf("s2 is empty") 1954 } 1955 1956 var o1 interface{} 1957 if err = json.Unmarshal([]byte(s1), &o1); err != nil { 1958 return 1959 } 1960 1961 var o2 interface{} 1962 if err = json.Unmarshal([]byte(s2), &o2); err != nil { 1963 return 1964 } 1965 1966 differ := gojsondiff.New() 1967 d, err := differ.Compare([]byte(s1), []byte(s2)) 1968 if err != nil { 1969 return 1970 } 1971 1972 config := formatter.AsciiFormatterConfig{} 1973 1974 formatter := formatter.NewAsciiFormatter(o1, config) 1975 diff, err = formatter.Format(d) 1976 1977 return cmp.Equal(o1, o2), diff, err 1978 } 1979 1980 type dashboardDependencies struct { 1981 dashboardService influxdb.DashboardService 1982 userService influxdb.UserService 1983 orgService influxdb.OrganizationService 1984 labelService influxdb.LabelService 1985 urmService influxdb.UserResourceMappingService 1986 } 1987 1988 type option func(*dashboardDependencies) 1989 1990 func withDashboardService(svc influxdb.DashboardService) option { 1991 return func(d *dashboardDependencies) { 1992 d.dashboardService = svc 1993 } 1994 } 1995 1996 func withLabelService(svc influxdb.LabelService) option { 1997 return func(d *dashboardDependencies) { 1998 d.labelService = svc 1999 } 2000 }