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