Skip to content

Commit 2d01f12

Browse files
CopilotCopilot
andcommitted
chore: add verification sample for PR #196
Verification sample: examples/verification/pr-196-connection-string-case-insensitive.ts Verifies that connection string property keys are parsed case-insensitively, matching Azure SDK conventions. Tests parsing with lowercase, uppercase, mixed-case, and PascalCase keys, plus an end-to-end orchestration using a lowercase connection string against the DTS emulator. Generated by pr-verification-agent Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5470410 commit 2d01f12

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// Verification sample for PR #196: Fix connection string property key lookup case sensitivity
2+
//
3+
// Customer scenario: A developer configures their Durable Task Scheduler connection
4+
// using a connection string copied from a config file or environment variable that
5+
// has lowercase or mixed-case property keys (e.g., "endpoint=..." instead of
6+
// "Endpoint=..."). This is common when connection strings are generated by scripts,
7+
// stored in environment variables by deployment tools, or copied from documentation
8+
// that uses different casing conventions.
9+
//
10+
// Before fix: The connection string parser would throw an error like
11+
// "The connection string must contain an Authentication property" because keys
12+
// were stored and looked up case-sensitively (e.g., "authentication" != "Authentication").
13+
//
14+
// After fix: Property keys are normalized to lowercase during parsing, so connection
15+
// strings work regardless of key casing — matching Azure SDK conventions.
16+
17+
import {
18+
OrchestrationContext,
19+
ActivityContext,
20+
OrchestrationStatus,
21+
TOrchestrator,
22+
TActivity,
23+
} from "@microsoft/durabletask-js";
24+
import {
25+
DurableTaskAzureManagedClientBuilder,
26+
DurableTaskAzureManagedWorkerBuilder,
27+
DurableTaskAzureManagedConnectionString,
28+
} from "@microsoft/durabletask-js-azuremanaged";
29+
30+
const endpoint = process.env.ENDPOINT || "localhost:8080";
31+
const taskHub = process.env.TASKHUB || "default";
32+
33+
// Step 1: Verify that connection strings with different key casings parse correctly.
34+
// This is the core behavior the PR fixes.
35+
function verifyConnectionStringParsing(): {
36+
passed: boolean;
37+
details: { casing: string; connectionString: string; result: string }[];
38+
} {
39+
const testCases = [
40+
{
41+
casing: "all-lowercase",
42+
connectionString: `endpoint=http://${endpoint};authentication=None;taskhub=${taskHub}`,
43+
},
44+
{
45+
casing: "all-uppercase",
46+
connectionString: `ENDPOINT=http://${endpoint};AUTHENTICATION=None;TASKHUB=${taskHub}`,
47+
},
48+
{
49+
casing: "mixed-case",
50+
connectionString: `endPoint=http://${endpoint};AUTHENTICATION=None;taskHub=${taskHub}`,
51+
},
52+
{
53+
casing: "PascalCase (standard)",
54+
connectionString: `Endpoint=http://${endpoint};Authentication=None;TaskHub=${taskHub}`,
55+
},
56+
{
57+
casing: "lowercase with optional ClientID",
58+
connectionString: `endpoint=http://${endpoint};authentication=None;taskhub=${taskHub};clientid=test-client-id`,
59+
},
60+
];
61+
62+
const details: { casing: string; connectionString: string; result: string }[] = [];
63+
let allPassed = true;
64+
65+
for (const tc of testCases) {
66+
try {
67+
const parsed = new DurableTaskAzureManagedConnectionString(tc.connectionString);
68+
// Verify that values are correctly extracted regardless of key casing
69+
const ep = parsed.getEndpoint();
70+
const auth = parsed.getAuthentication();
71+
const hub = parsed.getTaskHubName();
72+
73+
if (ep && auth && hub) {
74+
details.push({ casing: tc.casing, connectionString: tc.connectionString, result: "PASS" });
75+
} else {
76+
details.push({
77+
casing: tc.casing,
78+
connectionString: tc.connectionString,
79+
result: `FAIL: missing values (endpoint=${ep}, auth=${auth}, taskHub=${hub})`,
80+
});
81+
allPassed = false;
82+
}
83+
} catch (err: any) {
84+
details.push({
85+
casing: tc.casing,
86+
connectionString: tc.connectionString,
87+
result: `FAIL: ${err.message}`,
88+
});
89+
allPassed = false;
90+
}
91+
}
92+
93+
return { passed: allPassed, details };
94+
}
95+
96+
// Step 2: Verify that a lowercase connection string can be used to actually connect
97+
// to the DTS emulator and run an orchestration end-to-end.
98+
99+
// A simple order-processing orchestration that a customer might write
100+
const processOrder: TOrchestrator = async function* (ctx: OrchestrationContext, orderId: string): any {
101+
// Validate the order
102+
const isValid: boolean = yield ctx.callActivity(validateOrder, orderId);
103+
if (!isValid) {
104+
return { orderId, status: "rejected", reason: "validation failed" };
105+
}
106+
107+
// Process payment
108+
const paymentResult: string = yield ctx.callActivity(processPayment, orderId);
109+
110+
return { orderId, status: "completed", paymentResult };
111+
};
112+
113+
const validateOrder: TActivity<string, boolean> = async (
114+
_ctx: ActivityContext,
115+
orderId: string,
116+
): Promise<boolean> => {
117+
// Simulate order validation
118+
console.log(` [Activity] Validating order: ${orderId}`);
119+
return orderId.length > 0;
120+
};
121+
122+
const processPayment: TActivity<string, string> = async (
123+
_ctx: ActivityContext,
124+
orderId: string,
125+
): Promise<string> => {
126+
// Simulate payment processing
127+
console.log(` [Activity] Processing payment for order: ${orderId}`);
128+
return `payment-${orderId}-confirmed`;
129+
};
130+
131+
async function main() {
132+
console.log("=== PR #196 Verification: Connection String Case-Insensitive Keys ===\n");
133+
134+
// --- Part 1: Connection string parsing verification ---
135+
console.log("--- Part 1: Connection string parsing with various key casings ---\n");
136+
const parseResult = verifyConnectionStringParsing();
137+
138+
for (const detail of parseResult.details) {
139+
const icon = detail.result === "PASS" ? "✅" : "❌";
140+
console.log(` ${icon} ${detail.casing}: ${detail.result}`);
141+
}
142+
console.log();
143+
144+
if (!parseResult.passed) {
145+
console.log("=== VERIFICATION RESULT ===");
146+
console.log(
147+
JSON.stringify(
148+
{
149+
pr: 196,
150+
scenario: "connection-string-case-insensitive-parsing",
151+
passed: false,
152+
parseResults: parseResult.details,
153+
timestamp: new Date().toISOString(),
154+
},
155+
null,
156+
2,
157+
),
158+
);
159+
process.exit(1);
160+
}
161+
162+
// --- Part 2: End-to-end orchestration using lowercase connection string ---
163+
console.log("--- Part 2: End-to-end orchestration with lowercase connection string ---\n");
164+
165+
// Use an all-lowercase connection string — this is the exact scenario that
166+
// previously failed before the fix
167+
const lowercaseConnectionString = `endpoint=http://${endpoint};authentication=None;taskhub=${taskHub}`;
168+
console.log(` Connection string: ${lowercaseConnectionString}\n`);
169+
170+
const client = new DurableTaskAzureManagedClientBuilder()
171+
.connectionString(lowercaseConnectionString)
172+
.build();
173+
174+
const worker = new DurableTaskAzureManagedWorkerBuilder()
175+
.connectionString(lowercaseConnectionString)
176+
.addOrchestrator(processOrder)
177+
.addActivity(validateOrder)
178+
.addActivity(processPayment)
179+
.build();
180+
181+
await worker.start();
182+
console.log(" Worker started successfully with lowercase connection string\n");
183+
184+
const orderId = `order-${Date.now()}`;
185+
const id = await client.scheduleNewOrchestration(processOrder, { input: orderId });
186+
console.log(` Orchestration scheduled: ${id}`);
187+
188+
const state = await client.waitForOrchestrationCompletion(id, undefined, 30);
189+
190+
const expectedStatus = OrchestrationStatus.COMPLETED;
191+
const actualStatus = state?.runtimeStatus;
192+
const statusName = actualStatus !== undefined ? OrchestrationStatus[actualStatus] : "UNKNOWN";
193+
const passed = parseResult.passed && actualStatus === expectedStatus;
194+
195+
let output: any = undefined;
196+
try {
197+
output = state?.serializedOutput ? JSON.parse(state.serializedOutput) : undefined;
198+
} catch {
199+
output = state?.serializedOutput;
200+
}
201+
202+
console.log(`\n Orchestration completed:`);
203+
console.log(` Instance ID: ${id}`);
204+
console.log(` Status: ${statusName}`);
205+
console.log(` Output: ${JSON.stringify(output)}`);
206+
console.log();
207+
208+
console.log("=== VERIFICATION RESULT ===");
209+
console.log(
210+
JSON.stringify(
211+
{
212+
pr: 196,
213+
scenario: "connection-string-case-insensitive-keys",
214+
checks: [
215+
{
216+
name: "Parse lowercase keys",
217+
expected: "PASS",
218+
actual: parseResult.details.find((d) => d.casing === "all-lowercase")?.result,
219+
passed: parseResult.details.find((d) => d.casing === "all-lowercase")?.result === "PASS",
220+
},
221+
{
222+
name: "Parse uppercase keys",
223+
expected: "PASS",
224+
actual: parseResult.details.find((d) => d.casing === "all-uppercase")?.result,
225+
passed: parseResult.details.find((d) => d.casing === "all-uppercase")?.result === "PASS",
226+
},
227+
{
228+
name: "Parse mixed-case keys",
229+
expected: "PASS",
230+
actual: parseResult.details.find((d) => d.casing === "mixed-case")?.result,
231+
passed: parseResult.details.find((d) => d.casing === "mixed-case")?.result === "PASS",
232+
},
233+
{
234+
name: "E2E orchestration with lowercase connection string",
235+
expected: "COMPLETED",
236+
actual: statusName,
237+
passed: actualStatus === expectedStatus,
238+
},
239+
],
240+
instanceId: id,
241+
status: statusName,
242+
output,
243+
passed,
244+
timestamp: new Date().toISOString(),
245+
},
246+
null,
247+
2,
248+
),
249+
);
250+
251+
await worker.stop();
252+
await client.stop();
253+
254+
process.exit(passed ? 0 : 1);
255+
}
256+
257+
main().catch((err) => {
258+
console.error("Verification failed with error:", err);
259+
process.exit(1);
260+
});

0 commit comments

Comments
 (0)