Skip to content

Commit 0a7b689

Browse files
committed
Add generate-lease-files.js script and tests with uppercase orgName support
1 parent 5f07db4 commit 0a7b689

2 files changed

Lines changed: 891 additions & 0 deletions

File tree

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
#!/usr/bin/env node
2+
// Tests for generate-lease-files.js
3+
4+
const path = require("path");
5+
const fs = require("fs");
6+
const {
7+
parseInputLine,
8+
generateLeaseYaml,
9+
getLeasePath,
10+
validateStartDate,
11+
validateDuration,
12+
validateRpNamespace,
13+
validateOrgName,
14+
validateReviewer,
15+
getTodayDate,
16+
} = require("../generate-lease-files.js");
17+
18+
function assert(condition, message) {
19+
if (!condition) throw new Error(`Test failed: ${message}`);
20+
}
21+
22+
function testParseInputLine() {
23+
// Test without service names
24+
const result1 = parseInputLine("storage, Microsoft.Storage");
25+
assert(result1.orgName === "storage", "Org name should be storage");
26+
assert(
27+
result1.rpNamespace === "Microsoft.Storage",
28+
"RP should be Microsoft.Storage",
29+
);
30+
assert(result1.serviceNames.length === 0, "Should have no service names");
31+
32+
// Test with service names
33+
const result2 = parseInputLine(
34+
"compute, Microsoft.Compute, [ComputeRP, DiskRP]",
35+
);
36+
assert(result2.orgName === "compute", "Org name should be compute");
37+
assert(
38+
result2.rpNamespace === "Microsoft.Compute",
39+
"RP should be Microsoft.Compute",
40+
);
41+
assert(result2.serviceNames.length === 2, "Should have 2 service names");
42+
assert(
43+
result2.serviceNames[0] === "ComputeRP",
44+
"First name should be ComputeRP",
45+
);
46+
assert(result2.serviceNames[1] === "DiskRP", "Second name should be DiskRP");
47+
48+
// Test empty line
49+
const result3 = parseInputLine("");
50+
assert(result3 === null, "Empty line should return null");
51+
52+
// Test comment
53+
const result4 = parseInputLine("# This is a comment");
54+
assert(result4 === null, "Comment should return null");
55+
56+
console.log("✓ parseInputLine tests passed");
57+
}
58+
59+
function testGenerateLeaseYaml() {
60+
const yaml = generateLeaseYaml(
61+
"Microsoft.Test",
62+
"2026-06-01",
63+
"P180D",
64+
"@johnDoe",
65+
);
66+
67+
assert(
68+
yaml.includes("resource-provider: Microsoft.Test"),
69+
"Should include resource provider",
70+
);
71+
assert(yaml.includes('startdate: "2026-06-01"'), "Should include startdate with quotes");
72+
assert(
73+
yaml.includes("duration: P180D"),
74+
"Should include duration (not duration-days)",
75+
);
76+
assert(
77+
!yaml.includes("duration-days:"),
78+
"Should NOT use duration-days field",
79+
);
80+
assert(yaml.includes('reviewer: "@johnDoe"'), "Should include reviewer with quotes");
81+
assert(yaml.endsWith("\n\n"), "Should have trailing blank line (6th line)");
82+
83+
console.log("✓ generateLeaseYaml tests passed");
84+
}
85+
86+
function testGetLeasePath() {
87+
const repoRoot = "/repo";
88+
89+
// Without service name (use path.join for cross-platform)
90+
const path1 = getLeasePath(repoRoot, "storage", "Microsoft.Storage");
91+
const expected1 = path.join(
92+
repoRoot,
93+
".github",
94+
"arm-leases",
95+
"storage",
96+
"Microsoft.Storage",
97+
"lease.yaml",
98+
);
99+
assert(path1 === expected1, "Path without service name should be correct");
100+
101+
// With service name
102+
const path2 = getLeasePath(
103+
repoRoot,
104+
"compute",
105+
"Microsoft.Compute",
106+
"DiskRP",
107+
);
108+
const expected2 = path.join(
109+
repoRoot,
110+
".github",
111+
"arm-leases",
112+
"compute",
113+
"Microsoft.Compute",
114+
"DiskRP",
115+
"lease.yaml",
116+
);
117+
assert(path2 === expected2, "Path with service name should be correct");
118+
119+
console.log("✓ getLeasePath tests passed");
120+
}
121+
122+
function testValidateStartDate() {
123+
const today = getTodayDate();
124+
125+
// Valid date (today)
126+
try {
127+
validateStartDate(today);
128+
console.log(" ✓ Today date is valid");
129+
} catch (error) {
130+
throw new Error("Today should be valid");
131+
}
132+
133+
// Valid date (future)
134+
try {
135+
validateStartDate("2027-12-31");
136+
console.log(" ✓ Future date is valid");
137+
} catch (error) {
138+
throw new Error("Future date should be valid");
139+
}
140+
141+
// Valid date (within grace period - 5 days ago)
142+
const fiveDaysAgo = new Date();
143+
fiveDaysAgo.setDate(fiveDaysAgo.getDate() - 5);
144+
const fiveDaysAgoStr = fiveDaysAgo.toISOString().split("T")[0];
145+
try {
146+
validateStartDate(fiveDaysAgoStr);
147+
console.log(" ✓ Date within 10-day grace period is valid");
148+
} catch (error) {
149+
throw new Error(
150+
`Date within 10-day grace period should be valid: ${error.message}`,
151+
);
152+
}
153+
154+
// Invalid format
155+
try {
156+
validateStartDate("12/31/2026");
157+
throw new Error("Should reject invalid format");
158+
} catch (error) {
159+
if (error.message.includes("Invalid date format")) {
160+
console.log(" ✓ Rejects invalid date format");
161+
} else {
162+
throw error;
163+
}
164+
}
165+
166+
// Past date (beyond grace period)
167+
try {
168+
validateStartDate("2020-01-01");
169+
throw new Error("Should reject date too far in the past");
170+
} catch (error) {
171+
if (error.message.includes("past")) {
172+
console.log(" ✓ Rejects date too far in the past");
173+
} else {
174+
throw error;
175+
}
176+
}
177+
178+
console.log("✓ validateStartDate tests passed");
179+
}
180+
181+
function testValidateDuration() {
182+
// Valid durations
183+
assert(validateDuration("P180D") === "P180D", "P180D should be valid");
184+
assert(validateDuration("P90D") === "P90D", "P90D should be valid");
185+
assert(validateDuration("P1D") === "P1D", "P1D should be valid");
186+
assert(validateDuration("p30d") === "P30D", "Should convert to uppercase");
187+
188+
// Invalid format
189+
try {
190+
validateDuration("180 days");
191+
throw new Error("Should reject invalid format");
192+
} catch (error) {
193+
if (error.message.includes("Invalid duration format")) {
194+
console.log(" ✓ Rejects invalid duration format");
195+
} else {
196+
throw error;
197+
}
198+
}
199+
200+
// Over 180 days
201+
try {
202+
validateDuration("P200D");
203+
throw new Error("Should reject over 180 days");
204+
} catch (error) {
205+
if (error.message.includes("between 1 and 180")) {
206+
console.log(" ✓ Rejects duration over 180 days");
207+
} else {
208+
throw error;
209+
}
210+
}
211+
212+
// Zero days
213+
try {
214+
validateDuration("P0D");
215+
throw new Error("Should reject 0 days");
216+
} catch (error) {
217+
if (error.message.includes("between 1 and 180")) {
218+
console.log(" ✓ Rejects zero duration");
219+
} else {
220+
throw error;
221+
}
222+
}
223+
224+
console.log("✓ validateDuration tests passed");
225+
}
226+
227+
function testValidateRpNamespace() {
228+
// Valid RPs
229+
validateRpNamespace("Microsoft.Test");
230+
validateRpNamespace("Azure.Widget");
231+
validateRpNamespace("Contoso.Manager");
232+
233+
// Invalid RPs
234+
try {
235+
validateRpNamespace("microsoft.Test");
236+
throw new Error("Should reject lowercase start");
237+
} catch (error) {
238+
if (error.message.includes("capital letter")) {
239+
console.log(" ✓ Rejects lowercase start");
240+
} else {
241+
throw error;
242+
}
243+
}
244+
245+
console.log("✓ validateRpNamespace tests passed");
246+
}
247+
248+
function testValidateOrgName() {
249+
// Valid org names
250+
validateOrgName("storage");
251+
validateOrgName("compute");
252+
validateOrgName("testservice123");
253+
validateOrgName("TestService"); // uppercase is now valid
254+
validateOrgName("BakeryRP");
255+
256+
// Invalid org names (hyphen not allowed)
257+
try {
258+
validateOrgName("test-service");
259+
throw new Error("Should reject hyphen");
260+
} catch (error) {
261+
if (error.message.includes("alphanumeric")) {
262+
console.log(" ✓ Rejects hyphen");
263+
} else {
264+
throw error;
265+
}
266+
}
267+
268+
console.log("✓ validateOrgName tests passed");
269+
}
270+
271+
function testValidateReviewer() {
272+
// Valid reviewers (must start with @)
273+
assert(
274+
validateReviewer("@githubUser") === "@githubUser",
275+
"Valid @ reviewer should pass",
276+
);
277+
assert(
278+
validateReviewer("@johnDoe") === "@johnDoe",
279+
"Valid @ reviewer should pass",
280+
);
281+
assert(
282+
validateReviewer(" @trimmed ") === "@trimmed",
283+
"Should trim whitespace",
284+
);
285+
286+
// Invalid: missing @ prefix
287+
try {
288+
validateReviewer("githubUser");
289+
throw new Error("Should reject reviewer without @ prefix");
290+
} catch (error) {
291+
if (error.message.includes("@")) {
292+
console.log(" ✓ Rejects reviewer without @ prefix");
293+
} else {
294+
throw error;
295+
}
296+
}
297+
298+
// Invalid: only @
299+
try {
300+
validateReviewer("@");
301+
throw new Error("Should reject @ only");
302+
} catch (error) {
303+
if (error.message.includes("@")) {
304+
console.log(" ✓ Rejects @ only");
305+
} else {
306+
throw error;
307+
}
308+
}
309+
310+
// Invalid: empty string
311+
try {
312+
validateReviewer("");
313+
throw new Error("Should reject empty reviewer");
314+
} catch (error) {
315+
if (error.message.includes("required")) {
316+
console.log(" ✓ Rejects empty reviewer");
317+
} else {
318+
throw error;
319+
}
320+
}
321+
322+
console.log("✓ validateReviewer tests passed");
323+
}
324+
325+
function testGetTodayDate() {
326+
const today = getTodayDate();
327+
assert(
328+
/^\d{4}-\d{2}-\d{2}$/.test(today),
329+
"Today should be in YYYY-MM-DD format",
330+
);
331+
console.log(`✓ getTodayDate tests passed (today: ${today})`);
332+
}
333+
334+
function runTests() {
335+
console.log("Running tests for generate-lease-files.js...\n");
336+
337+
try {
338+
testParseInputLine();
339+
testGenerateLeaseYaml();
340+
testGetLeasePath();
341+
testValidateStartDate();
342+
testValidateDuration();
343+
testValidateRpNamespace();
344+
testValidateOrgName();
345+
testValidateReviewer();
346+
testGetTodayDate();
347+
348+
console.log("\n✅ All tests passed!");
349+
return 0;
350+
} catch (error) {
351+
console.error(`\n❌ ${error.message}`);
352+
console.error(error.stack);
353+
return 1;
354+
}
355+
}
356+
357+
if (require.main === module) {
358+
process.exit(runTests());
359+
}
360+
361+
module.exports = { runTests };

0 commit comments

Comments
 (0)