Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions MODERNIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@ _Date: 2026-07-17. Applies to repo `vso-testingbot-plugin` (marketplace: `testin
> drift resolved via `scripts/stamp-version.js`, immediate correctness bugs fixed.
> - **Phase 2 — DONE (PR #18):** tasks rewritten in TypeScript, runtime tunnel via
> `testingbot-tunnel-launcher` (jar deleted), secret pipeline variables, tests.
> - **Phase 3 — PENDING:** results-tab / SDK rewrite, including moving the `/mini`
> URL signing server-side so the secret leaves the browser (finding #1).
> - **Phase 3 — PARTIAL (PR #19):** results-tab hardening — unpkg.com CDN script
> removed, pagination + error-state bugs fixed, `embedDialog` URL allowlisted;
> manifest rebranded and scopes cut to `vso.build` + `vso.serviceendpoint_query`.
> **Still PENDING:** (a) moving the `/mini` share-URL signing server-side so the
> secret leaves the browser (finding #1) — **blocked**: the TestingBot public
> API exposes no endpoint that returns the `md5(key:secret:session_id)` share
> hash, so keeping the embedded `/mini` viewer requires the secret in the
> browser until TestingBot adds such an endpoint; (b) the `vss-web-extension-sdk`
> → `azure-devops-extension-sdk` / React port (large, and the old SDK keeps
> working, so deferred to its own PR).
> - **Phase 4 — PENDING:** CI/CD, lint, publishing.
>
> Items marked "PENDING" below are the genuinely outstanding work.
>
> **Release note for PR #19:** the scope reduction and tab changes are
> browser/manifest-side and cannot be exercised outside a live Azure DevOps org.
> Smoke-test the results tab (attachment load + API call) with the packaged vsix
> before publishing; the reduced scopes force installed users to re-authorize on
> upgrade.

## 1. Executive summary

Expand Down
1 change: 0 additions & 1 deletion tb-build-info/infoTab.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<head>
<title>TestingBot Info</title>
<script src="../lib/VSS.SDK.js"></script>
<script src="https://unpkg.com/text-encoding@0.6.1/lib/encoding.js"></script>
<script type="text/javascript">
VSS.init({ usePlatformScripts: true, moduleLoaderConfig: { paths: { "scripts": "tb-build-info/scripts" } } });
VSS.ready(function() {
Expand Down
25 changes: 21 additions & 4 deletions tb-build-info/scripts/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,25 @@ const getParams = query => {
return params;
}, { });
};
// Only ever frame an https TestingBot URL. The url comes in as a query
// parameter, so validate it before assigning it as the iframe source to avoid
// framing an attacker-supplied page.
const isAllowedUrl = value => {
try {
const parsed = new URL(value);
return parsed.protocol === 'https:' &&
(parsed.hostname === 'testingbot.com' || parsed.hostname.endsWith('.testingbot.com'));
} catch (err) {
return false;
}
};

const params = getParams(window.location.search);
var iframe = document.createElement('iframe');
iframe.src = params.url;
iframe.style = 'width: 100%; min-height: 100vh; border: 0';
document.body.appendChild(iframe);
if (isAllowedUrl(params.url)) {
var iframe = document.createElement('iframe');
iframe.src = params.url;
iframe.style = 'width: 100%; min-height: 100vh; border: 0';
document.body.appendChild(iframe);
} else {
document.body.textContent = 'Unable to display this test: the requested URL is not a valid TestingBot address.';
}
49 changes: 34 additions & 15 deletions tb-build-info/scripts/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,30 +213,41 @@ const getBuildResponse = async function (buildInformation, offset) {
return JSON.parse(json.result[0]);
}

const renderResults = function (buildInformation, buildFullJobs, buildFullMeta, currentPage) {
const renderError = function (message) {
const $buildinfo = $('.build-info').empty();
$buildinfo.append($('<h2>').text(message));
};

// pageSize is the stable number of results per API page, captured once from the
// first (offset 0) response. Using the *current* page's count as the divisor
// corrupts the page links on a partial last page, and a count of 0 would make
// the pagination loop spin forever.
const renderResults = function (buildInformation, buildFullJobs, buildFullMeta, currentOffset, pageSize) {
const $table = $('<table>');
$table.css('min-width', '800px');
$table.append('<thead><tr><th align="left">Test Name</th><th align="left">OS/Browser</th><th align="left">Pass/Fail</th></tr></thead>');

const $footer = $('<ul style="padding-left: 0">');
if (buildFullMeta && buildFullMeta.count < buildFullMeta.total) {
let paginationCount = buildFullMeta.count
for (let i = 0; i < buildFullMeta.total; i += paginationCount) {
if (i === currentPage) {
$footer.append($('<li style="display: inline; margin-right: 8px">' + ((i + paginationCount) / paginationCount) + '</li>'))
if (pageSize > 0 && buildFullMeta && buildFullMeta.total > pageSize) {
for (let offset = 0; offset < buildFullMeta.total; offset += pageSize) {
const pageNumber = (offset / pageSize) + 1;
if (offset === currentOffset) {
$footer.append($('<li style="display: inline; margin-right: 8px">').text(pageNumber));
continue;
}
$footer.append($('<li style="display: inline; margin-right: 8px">')
.append($('<a>')
.attr('href', '#')
.text((i + paginationCount) / paginationCount)
.text(pageNumber)
.click(async function(e) {
e.preventDefault();
const buildFullResponse = await getBuildResponse(buildInformation, i);
const buildFullJobs = buildFullResponse.data;
const buildFullMeta = buildFullResponse.meta;

renderResults(buildInformation, buildFullJobs, buildFullMeta, i);
try {
const buildFullResponse = await getBuildResponse(buildInformation, offset);
renderResults(buildInformation, buildFullResponse.data, buildFullResponse.meta, offset, pageSize);
} catch (err) {
console.error('error loading page', err);
renderError('Could not load TestingBot results for this page.');
}
})));
}
}
Expand Down Expand Up @@ -278,6 +289,11 @@ const renderResults = function (buildInformation, buildFullJobs, buildFullMeta,

sharedConfig.onBuildChanged(async function(build) {
try {
if (!build || !build.orchestrationPlan) {
renderError('No test results found');
return;
}

const taskAttachments = await taskRestClient.getPlanAttachments(
webContext.project.id,
'build',
Expand All @@ -286,8 +302,7 @@ sharedConfig.onBuildChanged(async function(build) {
);

if (taskAttachments.length < 1) {
const $buildinfo = $('.build-info').empty();
$buildinfo.append('<h2>No test results found</h2>');
renderError('No test results found');
return;
}
const buildInformation = await taskRestClient.getAttachmentContent(
Expand All @@ -306,8 +321,12 @@ sharedConfig.onBuildChanged(async function(build) {
const buildFullJobs = buildFullResponse.data;
const buildFullMeta = buildFullResponse.meta;

renderResults(buildInformation, buildFullJobs, buildFullMeta, 0)
// Capture the page size once, from the first response, and thread it through
// so pagination stays stable regardless of the current page's count.
const pageSize = buildFullMeta ? buildFullMeta.count : 0;
renderResults(buildInformation, buildFullJobs, buildFullMeta, 0, pageSize)
} catch (err) {
console.error('error', err);
renderError('Could not load TestingBot results.');
}
});
6 changes: 3 additions & 3 deletions vss-extension.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"manifestVersion": 1,
"id": "testingbot-tasks",
"name": "TestingBot for Visual Studio Team Services",
"name": "TestingBot for Azure DevOps",
"version": "0.1.0",
"publisher": "testingbot",
"public": true,
"description": "Integrate TestingBot's Automated Testing with VSTS",
"description": "Integrate TestingBot's automated testing with Azure DevOps",
"categories": ["Azure Pipelines", "Azure Test Plans"],
"content": {
"details": {
Expand All @@ -32,7 +32,7 @@
{ "path": "lib", "addressable": true }

],
"scopes": [ "vso.build_execute", "vso.test", "vso.test_write", "vso.serviceendpoint_manage" ],
"scopes": [ "vso.build", "vso.serviceendpoint_query" ],
"targets": [
{
"id": "Microsoft.VisualStudio.Services"
Expand Down