Sync src/WorkflowTest.gs
This commit is contained in:
@@ -0,0 +1,700 @@
|
|||||||
|
/**
|
||||||
|
* 🧪 WORKFLOW INTEGRATION TESTS
|
||||||
|
* =============================================================================
|
||||||
|
* This file contains manual test helpers to verify the connection between
|
||||||
|
* SchoolHub and external workflow tools (Vikunja & Gitea).
|
||||||
|
*
|
||||||
|
* INSTRUCTIONS:
|
||||||
|
* 1. Select the desired test function from the toolbar dropdown.
|
||||||
|
* 2. Click "Run".
|
||||||
|
* 3. Check the "Execution Log" for the results.
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 1: Verify Script Properties are readable.
|
||||||
|
* Goal: Ensure VIKUNJA_BASE_URL, VIKUNJA_TOKEN, etc., are set.
|
||||||
|
*/
|
||||||
|
function testWorkflowConfig() {
|
||||||
|
console.log('--- Starting Test: Workflow Config ---');
|
||||||
|
try {
|
||||||
|
const vikunja = getVikunjaConfig();
|
||||||
|
const gitea = getGiteaConfig();
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
vikunjaUrl: !!vikunja.baseUrl,
|
||||||
|
vikunjaToken: !!vikunja.token,
|
||||||
|
giteaUrl: !!gitea.baseUrl,
|
||||||
|
giteaToken: !!gitea.token,
|
||||||
|
giteaOwner: !!gitea.owner,
|
||||||
|
giteaRepo: !!gitea.repo
|
||||||
|
};
|
||||||
|
|
||||||
|
const allPassed = Object.values(results).every(val => val === true);
|
||||||
|
|
||||||
|
if (allPassed) {
|
||||||
|
console.log('✅ PASS: All required Script Properties are present.');
|
||||||
|
} else {
|
||||||
|
console.error('❌ FAIL: Some properties are missing:');
|
||||||
|
console.log(JSON.stringify(results, null, 2));
|
||||||
|
}
|
||||||
|
return { ok: allPassed, data: results };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ ERROR: ' + e.message);
|
||||||
|
return { ok: false, message: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 2: Verify Gitea Connectivity.
|
||||||
|
* Goal: Perform a read-only request to the repository root.
|
||||||
|
*/
|
||||||
|
function testGiteaConnection() {
|
||||||
|
console.log('--- Starting Test: Gitea Connection ---');
|
||||||
|
const res = getGiteaRepo();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log('✅ PASS: Gitea authenticated successfully.');
|
||||||
|
console.log('Repo Name: ' + res.data.name);
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
} else {
|
||||||
|
console.error('❌ FAIL: Gitea connection failed: ' + res.message);
|
||||||
|
return { ok: false, message: res.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 3: Verify Vikunja Connectivity using a Task ID.
|
||||||
|
* @param {string} taskId - Replace with a valid Task ID from your Vikunja instance.
|
||||||
|
*/
|
||||||
|
function testVikunjaRead() {
|
||||||
|
const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID';
|
||||||
|
if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') {
|
||||||
|
console.warn('⚠️ SKIP: Please provide a valid taskId in the code first.');
|
||||||
|
return { ok: false, message: 'No Task ID provided' };
|
||||||
|
}
|
||||||
|
console.log(`--- Starting Test: Vikunja Read (Task: ${taskId}) ---`);
|
||||||
|
const res = getVikunjaTask(taskId);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log('✅ PASS: Vikunja authenticated and task retrieved.');
|
||||||
|
console.log('Task Title: ' + (res.data.title || 'No Title'));
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
} else {
|
||||||
|
console.error('❌ FAIL: Vikunja read failed: ' + res.message);
|
||||||
|
if (res.rawBody) console.log('Raw Response: ' + res.rawBody);
|
||||||
|
return { ok: false, message: res.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 4: Verify Vikunja Write Operation.
|
||||||
|
* Goal: Add a clearly marked test comment.
|
||||||
|
* @param {string} taskId - Replace with a valid Task ID.
|
||||||
|
*/
|
||||||
|
function testVikunjaComment() {
|
||||||
|
const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID';
|
||||||
|
|
||||||
|
if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') {
|
||||||
|
console.warn('⚠️ SKIP: Please provide a valid taskId in the code first.');
|
||||||
|
return { ok: false, message: 'No Task ID provided' };
|
||||||
|
}
|
||||||
|
console.log(`--- Starting Test: Vikunja Write (Task: ${taskId}) ---`);
|
||||||
|
const testMessage = `[TEST] Integration check from SchoolHub at ${new Date().toISOString()}`;
|
||||||
|
const res = createVikunjaComment(taskId, testMessage);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log('✅ PASS: Test comment successfully created.');
|
||||||
|
return { ok: true };
|
||||||
|
} else {
|
||||||
|
console.error('❌ FAIL: Vikunja write failed: ' + res.message);
|
||||||
|
return { ok: false, message: res.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Test 6: Automated Vikunja Lifecycle Test
|
||||||
|
Goal: Create -> Read -> Comment -> Hook -> Delete (in Test Project)
|
||||||
|
*/
|
||||||
|
function testVikunjaLifecycle() {
|
||||||
|
console.log('--- Starting Test: Vikunja Lifecycle (Auto) ---');
|
||||||
|
|
||||||
|
const config = getTestProjectConfig();
|
||||||
|
const testProjectId = config.testProjectId;
|
||||||
|
|
||||||
|
if (!testProjectId) {
|
||||||
|
console.warn('⚠️ SKIP: VIKUNJA_TEST_PROJECT_ID not found in Script Properties.');
|
||||||
|
return { ok: false, message: 'Missing test project ID' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let createdTaskId = null;
|
||||||
|
const steps = [];
|
||||||
|
let cleanedUp = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Step 1: Creating temporary task...');
|
||||||
|
const createData = {
|
||||||
|
title: `[TEST][AUTO][SchoolHub] Lifecycle Test ${new Date().getTime()}`,
|
||||||
|
projectId: testProjectId
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRes = createVikunjaTask(createData);
|
||||||
|
if (!createRes.ok || !createRes.data || !createRes.data.id) {
|
||||||
|
throw new Error(`Task Creation Failed: ${createRes.message || 'No ID returned'}. Raw: ${createRes.rawBody || 'N/A'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createdTaskId = createRes.data.id;
|
||||||
|
steps.push('TASK_CREATED');
|
||||||
|
console.log(`✅ Task created with ID: ${createdTaskId}`);
|
||||||
|
|
||||||
|
console.log('Step 2: Reading task back...');
|
||||||
|
const readRes = getVikunjaTask(createdTaskId);
|
||||||
|
if (!readRes.ok) throw new Error(`Read failed: ${readRes.message}`);
|
||||||
|
|
||||||
|
steps.push('TASK_READ');
|
||||||
|
console.log(`✅ Task verified: ${readRes.data.title}`);
|
||||||
|
|
||||||
|
console.log('Step 3: Adding test comment...');
|
||||||
|
const commentRes = createVikunjaComment(createdTaskId, '[TEST] Automated lifecycle comment check.');
|
||||||
|
if (!commentRes.ok) {
|
||||||
|
throw new Error(`Comment failed (Code: ${commentRes.code}): ${commentRes.message}. Raw: ${commentRes.rawBody}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push('COMMENT_ADDED');
|
||||||
|
console.log('✅ Comment added successfully');
|
||||||
|
|
||||||
|
console.log('Step 4: Running workflow hook...');
|
||||||
|
const hookRes = workflowStartFeature('TEST-AUTO', createdTaskId);
|
||||||
|
if (!hookRes.ok) throw new Error(`Hook failed: ${hookRes.message}`);
|
||||||
|
|
||||||
|
steps.push('HOOK_EXECUTED');
|
||||||
|
console.log('✅ Workflow hook executed successfully');
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
createdTaskId,
|
||||||
|
cleanedUp: cleanedUp,
|
||||||
|
steps,
|
||||||
|
message: 'Full lifecycle completed successfully'
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ Lifecycle Test Failed at step ${steps.length + 1}: ${e.message}`);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
createdTaskId,
|
||||||
|
cleanedUp: cleanedUp,
|
||||||
|
steps,
|
||||||
|
message: e.message
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (createdTaskId) {
|
||||||
|
console.log('Final Step: Cleaning up temporary task...');
|
||||||
|
const delRes = deleteVikunjaTask(createdTaskId);
|
||||||
|
|
||||||
|
if (delRes.ok) {
|
||||||
|
console.log('✅ Temporary task deleted.');
|
||||||
|
cleanedUp = true;
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Cleanup failed for task ${createdTaskId}: ${delRes.message}`);
|
||||||
|
cleanedUp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMarkWorkflowTaskDone() {
|
||||||
|
console.log('--- Starting Test: markWorkflowTaskDone ---');
|
||||||
|
|
||||||
|
const featureMeta = {
|
||||||
|
featureCode: `DONE-TEST-${Date.now()}`,
|
||||||
|
title: 'Automated Done Status Test',
|
||||||
|
type: 'feature',
|
||||||
|
module: 'general'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Ensure task exists
|
||||||
|
const ensureRes = ensureWorkflowTask(featureMeta);
|
||||||
|
const taskId = ensureRes.taskId;
|
||||||
|
console.log(`Step 1: Task created/resolved: ${taskId}`);
|
||||||
|
|
||||||
|
// 2. Mark as done
|
||||||
|
markWorkflowTaskDone_(featureMeta, true);
|
||||||
|
console.log('Step 2: markWorkflowTaskDone_ executed');
|
||||||
|
|
||||||
|
// 3. Verify status
|
||||||
|
const taskRes = getVikunjaTask(taskId);
|
||||||
|
if (!taskRes.ok) throw new Error(`Read failed: ${taskRes.message}`);
|
||||||
|
|
||||||
|
const task = taskRes.data;
|
||||||
|
assertEqual_('Task is marked done', task.done, true);
|
||||||
|
|
||||||
|
// done_at is optional depending on server-side trigger/version
|
||||||
|
if (task.done_at) {
|
||||||
|
assertTrue_('Task has done_at timestamp', !!task.done_at);
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Note: Server did not return done_at, but task.done is true.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('--- Test Passed: markWorkflowTaskDone ---');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
deleteVikunjaTask(taskId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ Test Failed: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testVikunjaCreateOnly() {
|
||||||
|
const config = getTestProjectConfig();
|
||||||
|
if (!config.testProjectId) return { ok: false, message: 'No test project ID' };
|
||||||
|
|
||||||
|
const res = createVikunjaTask({
|
||||||
|
title: `[DEBUG] Create Test ${new Date().getTime()}`,
|
||||||
|
projectId: config.testProjectId,
|
||||||
|
priority: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(res.ok ? `✅ Created: ${res.data && res.data.id}` : `❌ Failed: ${res.message}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASSERTION HELPERS
|
||||||
|
*/
|
||||||
|
function assertEqual_(name, actual, expected) {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(`[ASSERT FAILED] ${name}: Expected ${expected}, but got ${actual}`);
|
||||||
|
}
|
||||||
|
console.log(`✅ ${name}: ${actual} == ${expected}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertArrayEqual_(name, actual, expected) {
|
||||||
|
const a = JSON.stringify(actual);
|
||||||
|
const e = JSON.stringify(expected);
|
||||||
|
if (a !== e) {
|
||||||
|
throw new Error(`[ASSERT FAILED] ${name}: Expected ${e}, but got ${a}`);
|
||||||
|
}
|
||||||
|
console.log(`✅ ${name}: ${a} == ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertTrue_(name, condition) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(`[ASSERT FAILED] ${name}: Expected true, but got false`);
|
||||||
|
}
|
||||||
|
console.log(`✅ ${name}: true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: Workflow Priority Rules
|
||||||
|
* Goal: Validate that the priority mapping (5..1) follows business rules.
|
||||||
|
*/
|
||||||
|
function testWorkflowPriorityRules() {
|
||||||
|
console.log('--- Starting Test: Workflow Priority Rules ---');
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
name: "Critical severity overrides all",
|
||||||
|
input: { type: 'feature', module: 'general', severity: 'critical' },
|
||||||
|
expected: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth module gets highest priority",
|
||||||
|
input: { type: 'feature', module: 'auth', severity: 'medium' },
|
||||||
|
expected: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bugfix gets priority 4",
|
||||||
|
input: { type: 'bugfix', module: 'student', severity: 'medium' },
|
||||||
|
expected: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "High severity gets priority 4",
|
||||||
|
input: { type: 'feature', module: 'student', severity: 'high' },
|
||||||
|
expected: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Feature gets priority 3",
|
||||||
|
input: { type: 'feature', module: 'student', severity: 'medium' },
|
||||||
|
expected: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Improvement gets priority 3",
|
||||||
|
input: { type: 'improvement', module: 'teacher', severity: 'medium' },
|
||||||
|
expected: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Refactor gets priority 2",
|
||||||
|
input: { type: 'refactor', module: 'general', severity: 'medium' },
|
||||||
|
expected: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Docs gets priority 2",
|
||||||
|
input: { type: 'docs', module: 'general', severity: 'medium' },
|
||||||
|
expected: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Low severity gets priority 2",
|
||||||
|
input: { type: 'chore', module: 'general', severity: 'low' },
|
||||||
|
expected: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fallback gets priority 1",
|
||||||
|
input: { type: 'chore', module: 'general', severity: 'medium' },
|
||||||
|
expected: 1
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(tc => {
|
||||||
|
const actual = deriveWorkflowPriority_(tc.input);
|
||||||
|
assertEqual_(tc.name, actual, tc.expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Workflow Priority Rules ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: Workflow Label Rules
|
||||||
|
* Goal: Validate that labels are correctly derived from metadata.
|
||||||
|
*/
|
||||||
|
function testWorkflowLabelRules() {
|
||||||
|
console.log('--- Starting Test: Workflow Label Rules ---');
|
||||||
|
|
||||||
|
const fullInput = {
|
||||||
|
type: 'bugfix',
|
||||||
|
module: 'auth',
|
||||||
|
impactArea: 'student',
|
||||||
|
severity: 'high'
|
||||||
|
};
|
||||||
|
const expectedFull = ['type:bugfix', 'mod:auth', 'impact:student', 'sev:high'];
|
||||||
|
assertArrayEqual_('Full metadata labels', deriveWorkflowLabels_(fullInput), expectedFull);
|
||||||
|
|
||||||
|
const minimalInput = { type: 'feature' };
|
||||||
|
const expectedMinimal = ['type:feature'];
|
||||||
|
assertArrayEqual_('Minimal metadata labels', deriveWorkflowLabels_(minimalInput), expectedMinimal);
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Workflow Label Rules ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test: Ensure Workflow Task Integration
|
||||||
|
* Goal: Verify the full cycle: ensure task -> verify in Vikunja -> delete.
|
||||||
|
*/
|
||||||
|
function testEnsureWorkflowTask() {
|
||||||
|
console.log('--- Starting Test: Ensure Workflow Task ---');
|
||||||
|
|
||||||
|
const featureMeta = {
|
||||||
|
featureCode: `TEST-AUTO-PRIO-${Date.now()}`,
|
||||||
|
title: 'Automated ensureWorkflowTask priority and label check',
|
||||||
|
type: 'bugfix',
|
||||||
|
module: 'auth',
|
||||||
|
impactArea: 'student',
|
||||||
|
severity: 'critical',
|
||||||
|
requestedBy: 'Automated Test',
|
||||||
|
source: 'WorkflowTest'
|
||||||
|
};
|
||||||
|
|
||||||
|
let createdTaskId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ensureRes = ensureWorkflowTask(featureMeta);
|
||||||
|
console.log('Ensure Result: ' + JSON.stringify(ensureRes));
|
||||||
|
|
||||||
|
assertTrue_('Ensure task ok', ensureRes.ok);
|
||||||
|
assertTrue_('Task ID exists', !!ensureRes.taskId);
|
||||||
|
assertTrue_('Task state is valid', ['CREATED', 'EXISTING'].includes(ensureRes.state));
|
||||||
|
|
||||||
|
createdTaskId = ensureRes.taskId;
|
||||||
|
|
||||||
|
const taskRes = getVikunjaTask(createdTaskId);
|
||||||
|
assertTrue_('Task fetch ok', taskRes.ok);
|
||||||
|
assertTrue_('Task data exists', !!taskRes.data);
|
||||||
|
|
||||||
|
const task = taskRes.data;
|
||||||
|
assertTrue_('Title contains feature code', task.title.includes(`[${featureMeta.featureCode}]`));
|
||||||
|
assertEqual_('Task priority matches rule', task.priority, deriveWorkflowPriority_(featureMeta));
|
||||||
|
|
||||||
|
const expectedLabels = deriveWorkflowLabels_(featureMeta);
|
||||||
|
if (Array.isArray(task.labels)) {
|
||||||
|
const actualLabels = task.labels.map(l => l.title || l.name);
|
||||||
|
expectedLabels.forEach(el => {
|
||||||
|
assertTrue_(`Label ${el} present`, actualLabels.includes(el));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Labels not returned as array; verification skipped for this instance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Ensure Workflow Task ---');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ Test Failed: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (createdTaskId) {
|
||||||
|
const delRes = deleteVikunjaTask(createdTaskId);
|
||||||
|
if (delRes.ok) console.log('✅ Cleanup: Temporary task deleted.');
|
||||||
|
else console.warn('⚠️ Cleanup: Failed to delete temporary task.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 5: Workflow Hook Dry Run.
|
||||||
|
* Goal: Verify the orchestrator can call the client.
|
||||||
|
* @param {string} taskId - Replace with a valid Task ID.
|
||||||
|
*/
|
||||||
|
function testWorkflowHooksDryRun() {
|
||||||
|
const taskId = 'REPLACE_WITH_ACTUAL_TASK_ID';
|
||||||
|
|
||||||
|
if (taskId === 'REPLACE_WITH_ACTUAL_TASK_ID') {
|
||||||
|
console.warn('⚠️ SKIP: Please provide a valid taskId in the code first.');
|
||||||
|
return { ok: false, message: 'No Task ID provided' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`--- Starting Test: Workflow Hook Dry Run (Task: ${taskId}) ---`);
|
||||||
|
const res = workflowStartFeature('TEST-001', taskId);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log('✅ PASS: workflowStartFeature executed successfully.');
|
||||||
|
return { ok: true };
|
||||||
|
} else {
|
||||||
|
console.error(`❌ FAIL: Workflow hook failed: ${res.message}`);
|
||||||
|
return { ok: false, message: res.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 🧪 GITEA SYNC INTEGRATION TESTS
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function testGiteaPathMapping() {
|
||||||
|
console.log('--- Starting Test: Gitea Path Mapping ---');
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ input: { name: 'appsscript', type: 'JSON' }, expected: 'appsscript.json' },
|
||||||
|
{ input: { name: 'Code', type: 'SERVER_JS' }, expected: 'src/Code.gs' },
|
||||||
|
{ input: { name: 'Index', type: 'HTML' }, expected: 'src/Index.html' },
|
||||||
|
{ input: { name: 'styles', type: 'HTML' }, expected: 'src/styles.html' },
|
||||||
|
{ input: { name: 'Unknown', type: 'OTHER' }, expected: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(function(tc) {
|
||||||
|
const actual = mapAppsScriptFileToRepoPath(tc.input);
|
||||||
|
assertEqual_(
|
||||||
|
`Path mapping for ${tc.input.name} (${tc.input.type})`,
|
||||||
|
actual,
|
||||||
|
tc.expected
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Gitea Path Mapping ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGiteaBranchNaming() {
|
||||||
|
console.log('--- Starting Test: Gitea Branch Naming ---');
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ code: 'feat/login', expectedBase: 'feat-login' },
|
||||||
|
{ code: 'BUG #123', expectedBase: 'bug-123' },
|
||||||
|
{ code: '---cool---feature---', expectedBase: 'cool-feature' },
|
||||||
|
{ code: null, expectedBase: 'manual' },
|
||||||
|
{ code: '', expectedBase: 'manual' }
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(function(tc) {
|
||||||
|
const branch = buildSyncBranchName(tc.code);
|
||||||
|
|
||||||
|
assertTrue_(
|
||||||
|
`Branch starts with sync/ for input: ${tc.code}`,
|
||||||
|
branch.indexOf('sync/') === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue_(
|
||||||
|
`Branch contains sanitized base "${tc.expectedBase}"`,
|
||||||
|
branch.indexOf(tc.expectedBase) !== -1
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue_(
|
||||||
|
`Branch ends with timestamp pattern for input: ${tc.code}`,
|
||||||
|
/\d{8}-\d{4}$/.test(branch)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue_(
|
||||||
|
`Branch has no duplicate hyphens for input: ${tc.code}`,
|
||||||
|
!/--/.test(branch)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Gitea Branch Naming ---');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convenience runner for the Workflow Rules Suite.
|
||||||
|
* Runs priority, label, and task assurance tests.
|
||||||
|
*/
|
||||||
|
function testWorkflowRulesSuite() {
|
||||||
|
console.log('=== Starting Workflow Rules Suite ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
testWorkflowPriorityRules();
|
||||||
|
testWorkflowLabelRules();
|
||||||
|
testEnsureWorkflowTask();
|
||||||
|
console.log('=== Workflow Rules Suite Passed ===');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('=== Workflow Rules Suite FAILED ===');
|
||||||
|
console.error(e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPrepareGitSyncPayload() {
|
||||||
|
console.log('--- Starting Test: Prepare Git Sync Payload ---');
|
||||||
|
|
||||||
|
const mockFiles = [
|
||||||
|
{ name: 'appsscript', type: 'JSON', source: '{ "timeZone": "UTC" }' },
|
||||||
|
{ name: 'Code', type: 'SERVER_JS', source: 'function test() {}' },
|
||||||
|
{ name: 'Index', type: 'HTML', source: '<html></html>' },
|
||||||
|
{ name: 'InvalidFile', type: 'UNKNOWN', source: 'junk' },
|
||||||
|
{ name: 'MissingSource', type: 'SERVER_JS' }, // valid file type mapping, missing source resolves to empty string
|
||||||
|
null
|
||||||
|
];
|
||||||
|
|
||||||
|
const payload = prepareGitSyncPayload('test-feature', mockFiles);
|
||||||
|
|
||||||
|
assertTrue_('Base branch is set', !!payload.baseBranch);
|
||||||
|
assertTrue_('Sync branch starts with sync/', payload.syncBranch.indexOf('sync/') === 0);
|
||||||
|
assertTrue_('Sync branch contains feature code', payload.syncBranch.indexOf('test-feature') !== -1);
|
||||||
|
|
||||||
|
assertEqual_('Valid files mapped correctly', payload.files.length, 4);
|
||||||
|
assertEqual_('JSON mapped to appsscript.json', payload.files[0].repoPath, 'appsscript.json');
|
||||||
|
assertEqual_('SERVER_JS mapped to src/Code.gs', payload.files[1].repoPath, 'src/Code.gs');
|
||||||
|
assertEqual_('HTML mapped to src/Index.html', payload.files[2].repoPath, 'src/Index.html');
|
||||||
|
assertEqual_('SERVER_JS missing source mapped to src/MissingSource.gs', payload.files[3].repoPath, 'src/MissingSource.gs');
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Prepare Git Sync Payload ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGiteaSyncHelpers() {
|
||||||
|
console.log('--- Starting Test: Gitea Sync Helpers ---');
|
||||||
|
|
||||||
|
const url = getGiteaSyncApiUrl('/test/path');
|
||||||
|
assertTrue_('URL ends with /api/v1/test/path', url.indexOf('/api/v1/test/path') !== -1);
|
||||||
|
|
||||||
|
const opts = getGiteaSyncRequestOptions('POST', { key: 'value' });
|
||||||
|
assertEqual_('Method is POST', opts.method, 'POST');
|
||||||
|
assertTrue_('Has Authorization header', !!opts.headers['Authorization']);
|
||||||
|
assertEqual_('contentType is application/json', opts.contentType, 'application/json');
|
||||||
|
assertEqual_('Payload is stringified', opts.payload, '{"key":"value"}');
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Gitea Sync Helpers ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGiteaBranchCreationPayload() {
|
||||||
|
console.log('--- Starting Test: Gitea Branch Creation Payload ---');
|
||||||
|
|
||||||
|
const baseBranch = 'main';
|
||||||
|
const newBranch = 'sync/feat-test';
|
||||||
|
const payload = {
|
||||||
|
new_branch_name: newBranch,
|
||||||
|
old_ref_name: baseBranch
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEqual_('Payload uses old_ref_name instead of old_branch_name', payload.old_ref_name, 'main');
|
||||||
|
assertTrue_('old_branch_name is undefined', payload.old_branch_name === undefined);
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Gitea Branch Creation Payload ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSyncSummaryFormat() {
|
||||||
|
console.log('--- Starting Test: Sync Summary Format ---');
|
||||||
|
|
||||||
|
const mockResults = {
|
||||||
|
created: 2,
|
||||||
|
updated: 1,
|
||||||
|
errors: 0,
|
||||||
|
details: [
|
||||||
|
{ file: "src/Code.gs", ok: true, message: "" },
|
||||||
|
{ file: "appsscript.json", ok: true, message: "" },
|
||||||
|
{ file: "sync-manifest.json", ok: true, message: "" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
assertTrue_('Summary has created count', mockResults.created === 2);
|
||||||
|
assertTrue_('Summary has updated count', mockResults.updated === 1);
|
||||||
|
assertTrue_('Summary has errors count', mockResults.errors === 0);
|
||||||
|
assertEqual_('Summary details length is correct', mockResults.details.length, 3);
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Sync Summary Format ---');
|
||||||
|
}
|
||||||
|
function testGetAppsScriptProjectContentErrorFormat() {
|
||||||
|
console.log('--- Starting Test: Get Apps Script Project Content Error Format ---');
|
||||||
|
|
||||||
|
const mockErrorCode = 500;
|
||||||
|
const result = { ok: false, message: "Failed to fetch project content. Code: " + mockErrorCode };
|
||||||
|
|
||||||
|
assertEqual_('Result ok is false', result.ok, false);
|
||||||
|
assertEqual_('Message formatting correct', result.message, "Failed to fetch project content. Code: 500");
|
||||||
|
|
||||||
|
const result403 = {
|
||||||
|
ok: false,
|
||||||
|
message: "HTTP 403 Forbidden. Ensure Google Apps Script API is enabled in user settings (https://script.google.com/home/usersettings) and OAuth scopes are correctly configured.",
|
||||||
|
code: 403,
|
||||||
|
rawBody: "Mock raw body content"
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEqual_('403 Result ok is false', result403.ok, false);
|
||||||
|
assertEqual_('403 Result code is 403', result403.code, 403);
|
||||||
|
assertTrue_('Message contains HTTP 403 Forbidden', result403.message.indexOf("HTTP 403 Forbidden") !== -1);
|
||||||
|
assertEqual_('rawBody is preserved', result403.rawBody, "Mock raw body content");
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Get Apps Script Project Content Error Format ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testScriptIdFallback() {
|
||||||
|
console.log('--- Starting Test: Script ID Fallback ---');
|
||||||
|
|
||||||
|
const currentScriptId = ScriptApp.getScriptId();
|
||||||
|
const config = getSyncConfig();
|
||||||
|
|
||||||
|
assertTrue_('getSyncConfig provides a scriptId', !!config.scriptId);
|
||||||
|
console.log('Current ScriptApp ID: ' + currentScriptId);
|
||||||
|
console.log('Config Script ID: ' + config.scriptId);
|
||||||
|
|
||||||
|
if (config.scriptId === currentScriptId) {
|
||||||
|
console.log('✅ Fallback working or explicitly matched.');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Configured scriptId is different from current ScriptApp ID.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Script ID Fallback ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEmptyRepoErrorDetection() {
|
||||||
|
console.log('--- Starting Test: Empty Repo Error Detection ---');
|
||||||
|
|
||||||
|
const mockCreateRes = {
|
||||||
|
ok: false,
|
||||||
|
message: "Branch creation failed: Git Repository is empty.",
|
||||||
|
code: 409
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRepoEmptyError = !mockCreateRes.ok && mockCreateRes.message && mockCreateRes.message.toLowerCase().indexOf('empty') !== -1;
|
||||||
|
assertTrue_('Detects empty repository error message', isRepoEmptyError);
|
||||||
|
|
||||||
|
const mockSuccessRes = {
|
||||||
|
ok: true,
|
||||||
|
data: { name: "sync/feat-test" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSuccessEmptyError = !mockSuccessRes.ok && mockSuccessRes.message && mockSuccessRes.message.toLowerCase().indexOf('empty') !== -1;
|
||||||
|
assertEqual_('Does not falsely detect empty repo on success', isSuccessEmptyError, false);
|
||||||
|
|
||||||
|
const mockBootstrapFail = {
|
||||||
|
ok: false,
|
||||||
|
message: 'Git Repository is empty and bootstrap failed: 404 Not Found. Harap inisialisasi repositori secara manual (misal: centang "Initialize Repository" saat membuat repo).'
|
||||||
|
};
|
||||||
|
const detectsManualInit = !mockBootstrapFail.ok && mockBootstrapFail.message.toLowerCase().indexOf('manual') !== -1;
|
||||||
|
assertTrue_('Detects manual initialization instruction on bootstrap failure', detectsManualInit);
|
||||||
|
|
||||||
|
console.log('--- Test Passed: Empty Repo Error Detection ---');
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user