Skip to content

Commit

Permalink
Modularize downloadWithCredentials in SolrResult
Browse files Browse the repository at this point in the history
- Separate out the logic for getting the data from DataONE from the logic for downloading that data onto the user's machine.
- Allows us to use the same data fetching logic in other parts of the app, i.e. the DataONEObjectView.
- Also separate out the logic for getting the file name.
- Add a tests for the new functions

Issue #1758
  • Loading branch information
robyngit committed Sep 12, 2024
1 parent 72b0634 commit 1f72aed
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 97 deletions.
175 changes: 84 additions & 91 deletions src/js/models/SolrResult.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
define(["jquery", "underscore", "backbone"], ($, _, Backbone) => {
/**
* @class SolrResult
* @classdesc A single result from the Solr search service
* @classcategory Models
* @extends Backbone.Model
*/
var SolrResult = Backbone.Model.extend(
const SolrResult = Backbone.Model.extend(
/** @lends SolrResult.prototype */ {
// This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
defaults: {
Expand Down Expand Up @@ -278,103 +278,96 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
);
},

/*
* This method will download this object while sending the user's auth token in the request.
/**
* Download this object while sending the user's auth token in the
* request.
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;

//Get info about this object
var url = this.get("url"),
model = this;

//Create an XHR
var xhr = new XMLHttpRequest();
async downloadWithCredentials() {
const model = this;

//Open and send the request with the user's auth token
xhr.open("GET", url);
// Call the new getBlob method and handle the response
const response = await this.fetchDataObjectWithCredentials();
const blob = await response.blob();
const filename = this.getFileNameFromResponse(response);

if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;

//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}

//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");

if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");

//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");

//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob

// Set the file name.
a.download = filename;

a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}

model.trigger("downloadComplete");

// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};

xhr.onerror = function (e) {
model.trigger("downloadError");
// For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
} else {
// Other browsers can download it via a link
const a = document.createElement("a");
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}

// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
// Track this event
model.trigger("downloadComplete");
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
},

xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
/**
* This method will fetch this object while sending the user's auth token
* in the request. The data can then be downloaded or displayed in the
* browser
* @returns {Promise} A promise that resolves when the data is fetched
* @since 0.0.0
*/
fetchDataObjectWithCredentials() {
const url = this.get("url");
const token = MetacatUI.appUserModel.get("token") || "";
const method = "GET";

return new Promise((resolve, reject) => {
const headers = {};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
};

xhr.responseType = "blob";

if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
fetch(url, { method, headers })
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
resolve(response);
})
.catch((error) => {
reject(error);
});
});
},

xhr.send();
/**
* Get the filename from the response headers or default to the model's
* title, id, or "download"
* @param {Response} response - The response object from the fetch request
* @returns {string} The filename to save the file as
* @since 0.0.0
*/
getFileNameFromResponse(response) {
const model = this;
let filename = response.headers.get("Content-Disposition");

if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else {
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
}
filename = filename.trim().replace(/ /g, "_");
return filename;
},

getInfo: function (fields) {
Expand Down
106 changes: 100 additions & 6 deletions test/js/specs/unit/models/SolrResult.spec.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,120 @@
define(["../../../../../../../../src/js/models/SolrResult"], function (
SolrResult,
) {
define(["models/SolrResult"], function (SolrResult) {
// Configure the Chai assertion library
var should = chai.should();
var expect = chai.expect;
let solrResult;
let solrResult, fetchStub;

describe("Search Test Suite", function () {
describe("SolrResult Test Suite", function () {
/* Set up */
beforeEach(function () {
solrResult = new SolrResult();
// Create a stub for the fetch API
fetchStub = sinon.stub(window, "fetch");
});

/* Tear down */
after(function () {
afterEach(function () {
solrResult = undefined;
fetchStub.restore();
});

describe("The SolrResult model", function () {
it("should be created", function () {
solrResult.should.be.instanceof(SolrResult);
});
});

describe("downloadWithCredentials", function () {
it("should download a file with valid credentials", async function () {
const mockBlob = new Blob(["test"], { type: "text/plain" });
const mockResponse = new Response(mockBlob, {
status: 200,
headers: {
"Content-Disposition": 'attachment; filename="testfile.txt"',
},
});

// Mock fetch response
fetchStub.resolves(mockResponse);

// Spy on model.trigger to check if the events are triggered
const triggerSpy = sinon.spy(solrResult, "trigger");

// Execute the downloadWithCredentials method
await solrResult.downloadWithCredentials();

// Ensure that fetch was called once
sinon.assert.calledOnce(fetchStub);

// Check if the downloadComplete event was triggered
sinon.assert.calledWith(triggerSpy, "downloadComplete");
});
});

describe("fetchDataObjectWithCredentials", function () {
it("should fetch the data object with valid credentials", async function () {
const mockResponse = new Response("{}", { status: 200 });

// Mock fetch response
fetchStub.resolves(mockResponse);

// Execute the fetchDataObjectWithCredentials method
const response = await solrResult.fetchDataObjectWithCredentials();

// Ensure that fetch was called once
sinon.assert.calledOnce(fetchStub);

// The response should be the mock response
response.status.should.equal(200);
});

it("should throw an error for a failed fetch", async function () {
// Mock a failed fetch response
fetchStub.rejects(new Error("Failed to fetch"));

try {
await solrResult.fetchDataObjectWithCredentials();
} catch (error) {
error.message.should.equal("Failed to fetch");
}

// Ensure that fetch was called once
sinon.assert.calledOnce(fetchStub);
});
});

describe("getFileNameFromResponse", function () {
it("should extract filename from Content-Disposition header", function () {
const mockResponse = new Response(null, {
headers: {
"Content-Disposition": 'attachment; filename="testfile.txt"',
},
});

// Execute getFileNameFromResponse
const filename = solrResult.getFileNameFromResponse(mockResponse);

// Ensure the filename is correct
filename.should.equal("testfile.txt");
});

it("should fall back to model attributes for filename", function () {
// Set model properties
sinon.stub(solrResult, "get").callsFake(function (attr) {
if (attr === "fileName") return "defaultFileName.txt";
return null;
});

const mockResponse = new Response(null, {
headers: {},
});

// Execute getFileNameFromResponse
const filename = solrResult.getFileNameFromResponse(mockResponse);

// Ensure the fallback filename is correct
filename.should.equal("defaultFileName.txt");
});
});
});
});

0 comments on commit 1f72aed

Please sign in to comment.