github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ServiceLane/ServiceLane.test.tsx (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 import { render } from '@testing-library/react'; 17 import { ServiceLane } from './ServiceLane'; 18 import { UpdateOverview } from '../../utils/store'; 19 import { Spy } from 'spy4js'; 20 import { 21 Application, 22 BatchAction, 23 Environment, 24 Environment_Application, 25 Priority, 26 Release, 27 UndeploySummary, 28 } from '../../../api/api'; 29 import { MemoryRouter } from 'react-router-dom'; 30 import { elementQuerySelectorSafe, makeRelease } from '../../../setupTests'; 31 32 const mock_ReleaseCard = Spy.mockReactComponents('../../components/ReleaseCard/ReleaseCard', 'ReleaseCard'); 33 const mock_addAction = Spy.mockModule('../../utils/store', 'addAction'); 34 35 const extendRelease = (props: Partial<Release>): Release => ({ 36 version: 123, 37 displayVersion: '123', 38 sourceCommitId: 'id', 39 sourceAuthor: 'author', 40 sourceMessage: 'source', 41 undeployVersion: false, 42 prNumber: 'pr', 43 ...props, 44 }); 45 46 describe('Service Lane', () => { 47 const getNode = (overrides: { application: Application }) => ( 48 <MemoryRouter> 49 <ServiceLane {...overrides} /> 50 </MemoryRouter> 51 ); 52 const getWrapper = (overrides: { application: Application }) => render(getNode(overrides)); 53 it('Renders a row of releases', () => { 54 // when 55 const sampleApp: Application = { 56 name: 'test2', 57 releases: [extendRelease({ version: 5 }), extendRelease({ version: 2 }), extendRelease({ version: 3 })], 58 sourceRepoUrl: 'http://test2.com', 59 team: 'example', 60 undeploySummary: UndeploySummary.NORMAL, 61 warnings: [], 62 }; 63 UpdateOverview.set({ 64 applications: { 65 test2: sampleApp, 66 }, 67 }); 68 getWrapper({ application: sampleApp }); 69 70 // then releases are sorted and Release card is called with props: 71 expect(mock_ReleaseCard.ReleaseCard.getCallArgument(0, 0)).toStrictEqual({ app: sampleApp.name, version: 5 }); 72 expect(mock_ReleaseCard.ReleaseCard.getCallArgument(1, 0)).toStrictEqual({ app: sampleApp.name, version: 3 }); 73 expect(mock_ReleaseCard.ReleaseCard.getCallArgument(2, 0)).toStrictEqual({ app: sampleApp.name, version: 2 }); 74 mock_ReleaseCard.ReleaseCard.wasCalled(3); 75 }); 76 }); 77 78 type TestData = { 79 name: string; 80 envs: Environment[]; 81 }; 82 83 type TestDataDiff = TestData & { diff: string; releases: Release[] }; 84 85 const data: TestDataDiff[] = [ 86 { 87 name: 'test same version', 88 diff: '-1', 89 releases: [makeRelease(1)], 90 envs: [ 91 { 92 name: 'foo', 93 applications: { 94 test2: { 95 version: 1, 96 name: '', 97 locks: {}, 98 queuedVersion: 0, 99 undeployVersion: false, 100 }, 101 }, 102 distanceToUpstream: 0, 103 priority: Priority.UPSTREAM, 104 locks: {}, 105 }, 106 { 107 name: 'foo2', 108 applications: { 109 test2: { 110 version: 1, 111 name: '', 112 locks: {}, 113 queuedVersion: 0, 114 undeployVersion: false, 115 }, 116 }, 117 distanceToUpstream: 0, 118 priority: Priority.UPSTREAM, 119 locks: {}, 120 }, 121 ], 122 }, 123 { 124 name: 'test no diff', 125 diff: '0', 126 releases: [makeRelease(1), makeRelease(2)], 127 envs: [ 128 { 129 name: 'foo', 130 applications: { 131 test2: { 132 version: 1, 133 name: '', 134 locks: {}, 135 queuedVersion: 0, 136 undeployVersion: false, 137 }, 138 }, 139 distanceToUpstream: 0, 140 priority: Priority.UPSTREAM, 141 locks: {}, 142 }, 143 { 144 name: 'foo2', 145 applications: { 146 test2: { 147 version: 2, 148 name: '', 149 locks: {}, 150 queuedVersion: 0, 151 undeployVersion: false, 152 }, 153 }, 154 distanceToUpstream: 0, 155 priority: Priority.UPSTREAM, 156 locks: {}, 157 }, 158 ], 159 }, 160 { 161 name: 'test diff by one', 162 diff: '1', 163 releases: [makeRelease(1), makeRelease(4), makeRelease(2)], 164 envs: [ 165 { 166 name: 'foo', 167 applications: { 168 test2: { 169 name: 'test2', 170 version: 1, 171 locks: {}, 172 queuedVersion: 0, 173 undeployVersion: false, 174 }, 175 }, 176 locks: {}, 177 distanceToUpstream: 0, 178 priority: Priority.UPSTREAM, 179 }, 180 { 181 name: 'foo2', 182 applications: { 183 test2: { 184 name: 'test2', 185 version: 4, 186 locks: {}, 187 queuedVersion: 0, 188 undeployVersion: false, 189 }, 190 }, 191 locks: {}, 192 distanceToUpstream: 0, 193 priority: Priority.UPSTREAM, 194 }, 195 ], 196 }, 197 { 198 name: 'test diff by two', 199 diff: '2', 200 releases: [makeRelease(2), makeRelease(4), makeRelease(3), makeRelease(5)], 201 envs: [ 202 { 203 name: 'foo', 204 applications: { 205 test2: { 206 version: 2, 207 name: '', 208 locks: {}, 209 queuedVersion: 0, 210 undeployVersion: false, 211 }, 212 }, 213 distanceToUpstream: 0, 214 priority: Priority.UPSTREAM, 215 locks: {}, 216 }, 217 { 218 name: 'foo2', 219 applications: { 220 test2: { 221 version: 5, 222 name: '', 223 locks: {}, 224 queuedVersion: 0, 225 undeployVersion: false, 226 }, 227 }, 228 distanceToUpstream: 0, 229 priority: Priority.UPSTREAM, 230 locks: {}, 231 }, 232 ], 233 }, 234 ]; 235 236 describe('Service Lane Diff', () => { 237 const getNode = (overrides: { application: Application }) => ( 238 <MemoryRouter> 239 <ServiceLane {...overrides} /> 240 </MemoryRouter> 241 ); 242 const getWrapper = (overrides: { application: Application }) => render(getNode(overrides)); 243 describe.each(data)('Service Lane diff number', (testcase) => { 244 it(testcase.name, () => { 245 UpdateOverview.set({ 246 applications: { 247 test2: { 248 releases: testcase.releases, 249 name: '', 250 team: '', 251 sourceRepoUrl: '', 252 undeploySummary: UndeploySummary.MIXED, 253 warnings: [], 254 }, 255 }, 256 environmentGroups: [ 257 { 258 environments: testcase.envs, 259 environmentGroupName: 'group1', 260 distanceToUpstream: 0, 261 priority: Priority.UNRECOGNIZED, 262 }, 263 ], 264 }); 265 const sampleApp: Application = { 266 undeploySummary: UndeploySummary.NORMAL, 267 name: 'test2', 268 releases: [], 269 sourceRepoUrl: 'http://test2.com', 270 team: 'example', 271 warnings: [], 272 }; 273 const { container } = getWrapper({ application: sampleApp }); 274 275 // check for the diff between versions 276 if (testcase.diff === '-1' || testcase.diff === '0') { 277 expect(document.querySelector('.service-lane__diff--number') === undefined); 278 } else { 279 expect(container.querySelector('.service-lane__diff--number')?.textContent).toContain(testcase.diff); 280 } 281 }); 282 }); 283 }); 284 285 type TestDataImportantRels = { name: string; releases: Release[]; currentlyDeployedVersion: number }; 286 287 const dataImportantRels: TestDataImportantRels[] = [ 288 { 289 name: 'Gets deployed release first and 5 trailing releases', 290 currentlyDeployedVersion: 9, 291 releases: [ 292 makeRelease(9), 293 makeRelease(7), 294 makeRelease(6), 295 makeRelease(5), 296 makeRelease(4), 297 makeRelease(3), 298 makeRelease(2), 299 makeRelease(1), // not important 300 ], 301 }, 302 { 303 name: 'Gets latest release first, then deployed release and 4 trailing releases', 304 currentlyDeployedVersion: 7, 305 releases: [ 306 makeRelease(9), 307 makeRelease(7), 308 makeRelease(6), 309 makeRelease(5), 310 makeRelease(4), 311 makeRelease(3), 312 makeRelease(2), 313 makeRelease(1), // not important 314 ], 315 }, 316 { 317 name: 'jumps over not important second release', 318 currentlyDeployedVersion: 6, 319 releases: [ 320 makeRelease(9), 321 makeRelease(7), // not important 322 makeRelease(6), 323 makeRelease(5), 324 makeRelease(4), 325 makeRelease(3), 326 makeRelease(2), 327 makeRelease(1), // not important 328 ], 329 }, 330 ]; 331 332 describe('Service Lane Important Releases', () => { 333 const getNode = (overrides: { application: Application }) => ( 334 <MemoryRouter> 335 <ServiceLane {...overrides} /> 336 </MemoryRouter> 337 ); 338 const getWrapper = (overrides: { application: Application }) => render(getNode(overrides)); 339 describe.each(dataImportantRels)('Service Lane important releases', (testcase) => { 340 it(testcase.name, () => { 341 // given 342 const sampleApp: Application = { 343 releases: testcase.releases, 344 name: 'test2', 345 team: 'test2', 346 sourceRepoUrl: 'test2', 347 undeploySummary: UndeploySummary.MIXED, 348 warnings: [], 349 }; 350 UpdateOverview.set({ 351 applications: { 352 test2: sampleApp, 353 }, 354 environmentGroups: [ 355 { 356 environments: [ 357 { 358 name: 'foo', 359 applications: { 360 test2: { 361 name: 'test2', 362 version: testcase.currentlyDeployedVersion, 363 locks: {}, 364 undeployVersion: false, 365 queuedVersion: 0, 366 }, 367 }, 368 distanceToUpstream: 0, 369 priority: Priority.UPSTREAM, 370 locks: {}, 371 }, 372 ], 373 environmentGroupName: 'group1', 374 distanceToUpstream: 0, 375 priority: Priority.UNRECOGNIZED, 376 }, 377 ], 378 }); 379 // when 380 getWrapper({ application: sampleApp }); 381 382 // then - the latest release is always important and is displayed first 383 expect(mock_ReleaseCard.ReleaseCard.getCallArgument(0)).toMatchObject({ 384 version: testcase.releases[0].version, 385 }); 386 if (testcase.currentlyDeployedVersion !== testcase.releases[0].version) { 387 // then - the currently deployed version always important and displayed second after latest 388 expect(mock_ReleaseCard.ReleaseCard.getCallArgument(1)).toMatchObject({ 389 version: testcase.currentlyDeployedVersion, 390 }); 391 } 392 if (testcase.releases[1].version > testcase.currentlyDeployedVersion) { 393 // then - second release not deployed and not latest -> not important 394 mock_ReleaseCard.ReleaseCard.wasNotCalledWith( 395 { app: 'test2', version: testcase.releases[1].version }, 396 Spy.IGNORE 397 ); 398 } 399 // then - the old release is not important and not displayed 400 mock_ReleaseCard.ReleaseCard.wasNotCalledWith( 401 { app: 'test2', version: testcase.releases[7].version }, 402 Spy.IGNORE 403 ); 404 }); 405 }); 406 }); 407 408 type TestDataUndeploy = TestData & { 409 renderedApp: Application; 410 expectedUndeployButton: string | null; 411 expectedAction: BatchAction; 412 }; 413 const dataUndeploy: TestDataUndeploy[] = (() => { 414 const result: TestDataUndeploy[] = [ 415 { 416 name: 'test no prepareUndeploy', 417 renderedApp: { 418 name: 'test1', 419 releases: [], 420 sourceRepoUrl: 'http://test2.com', 421 team: 'example', 422 undeploySummary: UndeploySummary.NORMAL, 423 warnings: [], 424 }, 425 envs: [ 426 { 427 name: 'foo2', 428 applications: {}, 429 distanceToUpstream: 0, 430 priority: Priority.UPSTREAM, 431 locks: {}, 432 }, 433 ], 434 expectedUndeployButton: '⋮', 435 expectedAction: { 436 action: { 437 $case: 'prepareUndeploy', 438 prepareUndeploy: { application: 'test1' }, 439 }, 440 }, 441 }, 442 { 443 name: 'test no undeploy', 444 renderedApp: { 445 name: 'test1', 446 releases: [], 447 sourceRepoUrl: 'http://test2.com', 448 team: 'example', 449 undeploySummary: UndeploySummary.UNDEPLOY, 450 warnings: [], 451 }, 452 envs: [ 453 { 454 name: 'foo2', 455 applications: {}, 456 distanceToUpstream: 0, 457 priority: Priority.UPSTREAM, 458 locks: {}, 459 }, 460 ], 461 expectedUndeployButton: '⋮', 462 expectedAction: { 463 action: { 464 $case: 'undeploy', 465 undeploy: { application: 'test1' }, 466 }, 467 }, 468 }, 469 ]; 470 return result; 471 })(); 472 473 describe('Service Lane ⋮ menu', () => { 474 const getNode = (overrides: { application: Application }) => ( 475 <MemoryRouter> 476 <ServiceLane {...overrides} /> 477 </MemoryRouter> 478 ); 479 const getWrapper = (overrides: { application: Application }) => render(getNode(overrides)); 480 describe.each(dataUndeploy)('Undeploy Buttons', (testcase) => { 481 it(testcase.name, () => { 482 mock_addAction.addAction.returns(undefined); 483 484 UpdateOverview.set({ 485 applications: { 486 test1: testcase.renderedApp, 487 }, 488 environmentGroups: [ 489 { 490 environments: testcase.envs, 491 environmentGroupName: 'group1', 492 distanceToUpstream: 0, 493 priority: Priority.UNRECOGNIZED, 494 }, 495 ], 496 }); 497 498 const { container } = getWrapper({ application: testcase.renderedApp }); 499 500 const undeployButton = elementQuerySelectorSafe(container, '.dots-menu-hidden'); 501 const label = elementQuerySelectorSafe(undeployButton, 'span'); 502 expect(label?.textContent).toBe(testcase.expectedUndeployButton); 503 504 mock_addAction.addAction.wasNotCalled(); 505 }); 506 }); 507 }); 508 509 type TestDataAppLockSummary = TestData & { 510 renderedApp: Application; 511 expected: string | undefined; 512 }; 513 const dataAppLockSummary: TestDataAppLockSummary[] = (() => { 514 const appWith1Lock: Environment_Application = { 515 name: 'test1', 516 version: 123, 517 queuedVersion: 0, 518 undeployVersion: false, 519 locks: { 520 l1: { message: 'test lock', lockId: '321' }, 521 }, 522 }; 523 const appWith2Locks: Environment_Application = { 524 name: 'test1', 525 version: 123, 526 queuedVersion: 0, 527 undeployVersion: false, 528 locks: { 529 l1: { message: 'test lock', lockId: '321' }, 530 l2: { message: 'test lock', lockId: '321' }, 531 }, 532 }; 533 const result: TestDataAppLockSummary[] = [ 534 { 535 name: 'test no prepareUndeploy', 536 renderedApp: { 537 name: 'test1', 538 releases: [], 539 sourceRepoUrl: 'http://test2.com', 540 team: 'example', 541 undeploySummary: UndeploySummary.NORMAL, 542 warnings: [], 543 }, 544 envs: [ 545 { 546 name: 'foo2', 547 applications: {}, 548 distanceToUpstream: 0, 549 priority: Priority.UPSTREAM, 550 locks: { 551 envLockThatDoesNotMatter: { 552 message: 'I am an env lock, I should not count', 553 lockId: '487329463874223', 554 }, 555 }, 556 }, 557 ], 558 expected: undefined, 559 }, 560 { 561 name: 'test one lock', 562 renderedApp: { 563 name: 'test1', 564 releases: [], 565 sourceRepoUrl: 'http://test2.com', 566 team: 'example', 567 undeploySummary: UndeploySummary.NORMAL, 568 warnings: [], 569 }, 570 envs: [ 571 { 572 name: 'foo2', 573 applications: { 574 foo2: appWith1Lock, 575 }, 576 distanceToUpstream: 0, 577 priority: Priority.UPSTREAM, 578 locks: {}, 579 }, 580 ], 581 expected: '"test1" has 1 application lock. Click on a tile to see details.', 582 }, 583 { 584 name: 'test two locks', 585 renderedApp: { 586 name: 'test1', 587 releases: [], 588 sourceRepoUrl: 'http://test2.com', 589 team: 'example', 590 undeploySummary: UndeploySummary.NORMAL, 591 warnings: [], 592 }, 593 envs: [ 594 { 595 name: 'foo2', 596 applications: { 597 foo2: appWith2Locks, 598 }, 599 distanceToUpstream: 0, 600 priority: Priority.UPSTREAM, 601 locks: {}, 602 }, 603 ], 604 expected: '"test1" has 2 application locks. Click on a tile to see details.', 605 }, 606 ]; 607 return result; 608 })(); 609 610 describe('Service Lane AppLockSummary', () => { 611 const getNode = (overrides: { application: Application }) => ( 612 <MemoryRouter> 613 <ServiceLane {...overrides} /> 614 </MemoryRouter> 615 ); 616 const getWrapper = (overrides: { application: Application }) => render(getNode(overrides)); 617 describe.each(dataAppLockSummary)('diff', (testcase) => { 618 it(testcase.name, () => { 619 mock_addAction.addAction.returns(undefined); 620 621 UpdateOverview.set({ 622 applications: { 623 test1: testcase.renderedApp, 624 }, 625 environmentGroups: [ 626 { 627 environments: testcase.envs, 628 environmentGroupName: 'group1', 629 distanceToUpstream: 0, 630 priority: Priority.UNRECOGNIZED, 631 }, 632 ], 633 }); 634 635 const { container } = getWrapper({ application: testcase.renderedApp }); 636 637 const appLockSummary = container.querySelector('.test-app-lock-summary div'); 638 expect(appLockSummary?.attributes.getNamedItem('title')?.value).toBe(testcase.expected); 639 }); 640 }); 641 });