lgtm-processor-test.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /**
  2. * Tests for lgtm-processor.js helper logic.
  3. *
  4. * Tests the pure CODEOWNERS parsing and file-pattern matching behaviour
  5. * that lives inside lgtm-processor.js, extracted here so they can run
  6. * without GitHub Actions context.
  7. *
  8. * Run with: node .github/scripts/lgtm-processor-test.js
  9. */
  10. import assert from 'node:assert/strict';
  11. // ---------------------------------------------------------------------------
  12. // Helpers duplicated from lgtm-processor.js for unit testing
  13. // (kept in sync manually; tests will catch drift)
  14. // ---------------------------------------------------------------------------
  15. function parseCodeowners(content, organization) {
  16. const codeownerMappings = [];
  17. let wildcardRoles = [];
  18. content.split('\n').forEach(line => {
  19. const trimmed = line.trim();
  20. if (!trimmed || trimmed.startsWith('#')) return;
  21. const match = trimmed.match(/^(\S+)\s+(.+)$/);
  22. if (match) {
  23. const [, pattern, roles] = match;
  24. const rolesList = roles.split(/\s+/).filter(r => r.startsWith('@'));
  25. if (pattern === '*') {
  26. wildcardRoles = rolesList;
  27. } else {
  28. codeownerMappings.push({ pattern, roles: rolesList });
  29. }
  30. }
  31. });
  32. const maintainerRoles = wildcardRoles.map(role => role.replace(`@${organization}/`, ''));
  33. return { codeownerMappings, wildcardRoles, maintainerRoles };
  34. }
  35. function fileMatchesPattern(file, pattern) {
  36. return file === pattern ||
  37. file.startsWith(pattern.endsWith('/') ? pattern : pattern + '/');
  38. }
  39. function getRequiredReviewerRoles(changedFiles, codeownerMappings, wildcardRoles) {
  40. const requiredReviewerRoles = new Set();
  41. let hasFilesWithoutSpecificOwners = false;
  42. changedFiles.forEach(file => {
  43. let hasSpecificOwner = false;
  44. codeownerMappings.forEach(({ pattern, roles }) => {
  45. if (fileMatchesPattern(file, pattern)) {
  46. roles.forEach(role => requiredReviewerRoles.add(role));
  47. hasSpecificOwner = true;
  48. }
  49. });
  50. if (!hasSpecificOwner) hasFilesWithoutSpecificOwners = true;
  51. });
  52. if (hasFilesWithoutSpecificOwners) {
  53. wildcardRoles.forEach(role => requiredReviewerRoles.add(role));
  54. }
  55. return requiredReviewerRoles;
  56. }
  57. // ---------------------------------------------------------------------------
  58. // Tests
  59. // ---------------------------------------------------------------------------
  60. const CODEOWNERS = `
  61. # Global owners
  62. * @external-secrets/maintainers
  63. pkg/provider/aws/ @external-secrets/aws-team
  64. pkg/provider/gcp/ @external-secrets/gcp-team
  65. docs/ @external-secrets/docs-team
  66. `;
  67. const ORG = 'external-secrets';
  68. // parseCodeowners: wildcard roles
  69. {
  70. const { wildcardRoles, maintainerRoles } = parseCodeowners(CODEOWNERS, ORG);
  71. assert.deepEqual(wildcardRoles, ['@external-secrets/maintainers']);
  72. assert.deepEqual(maintainerRoles, ['maintainers']);
  73. }
  74. // parseCodeowners: directory mappings
  75. {
  76. const { codeownerMappings } = parseCodeowners(CODEOWNERS, ORG);
  77. assert.equal(codeownerMappings.length, 3);
  78. assert.equal(codeownerMappings[0].pattern, 'pkg/provider/aws/');
  79. assert.deepEqual(codeownerMappings[0].roles, ['@external-secrets/aws-team']);
  80. }
  81. // parseCodeowners: comments and blank lines are ignored
  82. {
  83. const { codeownerMappings } = parseCodeowners('# comment\n\npkg/foo/ @org/team\n', ORG);
  84. assert.equal(codeownerMappings.length, 1);
  85. }
  86. // fileMatchesPattern: exact match
  87. assert.equal(fileMatchesPattern('pkg/provider/aws/s3.go', 'pkg/provider/aws/s3.go'), true);
  88. // fileMatchesPattern: directory prefix (with trailing slash)
  89. assert.equal(fileMatchesPattern('pkg/provider/aws/s3.go', 'pkg/provider/aws/'), true);
  90. // fileMatchesPattern: directory prefix (without trailing slash)
  91. assert.equal(fileMatchesPattern('pkg/provider/aws/s3.go', 'pkg/provider/aws'), true);
  92. // fileMatchesPattern: no match
  93. assert.equal(fileMatchesPattern('pkg/provider/gcp/storage.go', 'pkg/provider/aws/'), false);
  94. // fileMatchesPattern: partial prefix should not match
  95. assert.equal(fileMatchesPattern('pkg/provider/aws-extra/file.go', 'pkg/provider/aws'), false);
  96. // getRequiredReviewerRoles: file under specific owner
  97. {
  98. const { codeownerMappings, wildcardRoles } = parseCodeowners(CODEOWNERS, ORG);
  99. const roles = getRequiredReviewerRoles(['pkg/provider/aws/ec2.go'], codeownerMappings, wildcardRoles);
  100. assert.ok(roles.has('@external-secrets/aws-team'));
  101. assert.ok(!roles.has('@external-secrets/maintainers'));
  102. }
  103. // getRequiredReviewerRoles: file with no specific owner falls back to wildcard
  104. {
  105. const { codeownerMappings, wildcardRoles } = parseCodeowners(CODEOWNERS, ORG);
  106. const roles = getRequiredReviewerRoles(['some/unowned/file.go'], codeownerMappings, wildcardRoles);
  107. assert.ok(roles.has('@external-secrets/maintainers'));
  108. }
  109. // getRequiredReviewerRoles: mixed files collect all required roles
  110. {
  111. const { codeownerMappings, wildcardRoles } = parseCodeowners(CODEOWNERS, ORG);
  112. const roles = getRequiredReviewerRoles(
  113. ['pkg/provider/aws/ec2.go', 'pkg/provider/gcp/storage.go'],
  114. codeownerMappings, wildcardRoles
  115. );
  116. assert.ok(roles.has('@external-secrets/aws-team'));
  117. assert.ok(roles.has('@external-secrets/gcp-team'));
  118. }
  119. console.log('All tests passed.');