Bug 1036717 - Test webapp runtime update service. r=myk
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Sat, 12 Jul 2014 02:09:50 +0200
changeset 193739 89076de4e3c3e10ec5f22cdc85a9f737b65888f8
parent 193738 9f869623bd08d47db2f246c52ab05964642fadc6
child 193740 44776990ebd01fada385c16d9944337828032f54
push id7719
push usercbook@mozilla.com
push dateMon, 14 Jul 2014 13:24:23 +0000
treeherderfx-team@827f88a7eb65 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmyk
bugs1036717
milestone33.0a1
Bug 1036717 - Test webapp runtime update service. r=myk
toolkit/webapps/tests/app.sjs
toolkit/webapps/tests/chrome.ini
toolkit/webapps/tests/data/app/hosted_manifest.webapp
toolkit/webapps/tests/data/app/manifest.webapp
toolkit/webapps/tests/head.js
toolkit/webapps/tests/test_hosted_checkforupdates_from_webapp_runtime.xul
toolkit/webapps/tests/test_packaged_checkforupdates_from_webapp_runtime.xul
webapprt/WebappRT.jsm
webapprt/prefs.js
--- a/toolkit/webapps/tests/app.sjs
+++ b/toolkit/webapps/tests/app.sjs
@@ -1,19 +1,88 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
 function getQuery(request) {
   let query = {};
 
   request.queryString.split('&').forEach(function(val) {
     let [name, value] = val.split('=');
     query[name] = unescape(value);
   });
 
   return query;
 }
 
+function getTestFile(aName) {
+  var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+  var path = "chrome/toolkit/webapps/tests/data/app/" + aName;
+
+  path.split("/").forEach(function(component) {
+    file.append(component);
+  });
+
+  return file;
+}
+
+function readFile(aFile) {
+  var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
+                createInstance(Ci.nsIFileInputStream);
+  fstream.init(aFile, -1, 0, 0);
+  var data = NetUtil.readInputStreamToString(fstream, fstream.available());
+  fstream.close();
+  return data;
+}
+
+function getHostedManifest(aVersion) {
+  return readFile(getTestFile("hosted_manifest.webapp")).
+           replace(/VERSION_TOKEN/g, aVersion);
+}
+
+function getManifest(aVersion) {
+  return readFile(getTestFile("manifest.webapp")).
+           replace(/VERSION_TOKEN/g, aVersion);
+}
+
+function buildAppPackage(aVersion) {
+  const PR_RDWR        = 0x04;
+  const PR_CREATE_FILE = 0x08;
+  const PR_TRUNCATE    = 0x20;
+
+  let zipFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+  zipFile.append("application.zip");
+
+  let zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
+  zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+  // Add index.html file to the zip file
+  zipWriter.addEntryFile("index.html",
+                         Ci.nsIZipWriter.COMPRESSION_NONE,
+                         getTestFile("index.html"),
+                         false);
+
+  // Add manifest to the zip file
+  var manStream = Cc["@mozilla.org/io/string-input-stream;1"].
+                  createInstance(Ci.nsIStringInputStream);
+  var manifest = getManifest(aVersion);
+  manStream.setData(manifest, manifest.length);
+  zipWriter.addEntryStream("manifest.webapp", Date.now(),
+                           Ci.nsIZipWriter.COMPRESSION_NONE,
+                           manStream, false);
+
+  zipWriter.close();
+
+  return readFile(zipFile);
+}
+
 function handleRequest(request, response) {
   response.setHeader("Cache-Control", "no-cache", false);
 
   let query = getQuery(request);
 
   if ("appreq" in query) {
     response.setHeader("Content-Type", "text/plain", false);
     response.write("Hello world!");
@@ -25,9 +94,58 @@ function handleRequest(request, response
 
   if ("testreq" in query) {
     response.setHeader("Content-Type", "text/plain", false);
 
     response.write(getState("appreq"));
 
     return;
   }
+
+  if ("setVersion" in query) {
+    setState("version", query.setVersion);
+    response.write("OK");
+    return;
+  }
+  var version = Number(getState("version"));
+
+  if ("getPackage" in query) {
+    response.setHeader("Content-Type", "application/zip", false);
+    response.write(buildAppPackage(version));
+
+    var getPackageQueries = Number(getState("getPackageQueries"));
+    setState("getPackageQueries", String(++getPackageQueries));
+
+    return;
+  }
+
+  if ("getPackageQueries" in query) {
+    response.setHeader("Content-Type", "text/plain", false);
+    response.write(String(Number(getState("getPackageQueries"))));
+    return;
+  }
+
+  if ("getManifest" in query) {
+    response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
+    response.write(getManifest(version));
+
+    var getManifestQueries = Number(getState("getManifestQueries"));
+    setState("getManifestQueries", String(++getManifestQueries));
+
+    return;
+  }
+
+  if ("getHostedManifest" in query) {
+    response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
+    response.write(getHostedManifest(version));
+
+    var getManifestQueries = Number(getState("getManifestQueries"));
+    setState("getManifestQueries", String(++getManifestQueries));
+
+    return;
+  }
+
+  if ("getManifestQueries" in query) {
+    response.setHeader("Content-Type", "text/plain", false);
+    response.write(String(Number(getState("getManifestQueries"))));
+    return;
+  }
 }
--- a/toolkit/webapps/tests/chrome.ini
+++ b/toolkit/webapps/tests/chrome.ini
@@ -23,10 +23,14 @@ skip-if = asan
 skip-if = os == "win" && os_version == "5.1" # see bug 981251
 [test_packaged_uninstall.xul]
 skip-if = os == "win" && os_version == "5.1" # see bug 981251
 [test_hosted_update_from_webapp_runtime.xul]
 skip-if = asan
 [test_packaged_update_from_webapp_runtime.xul]
 skip-if = asan
 [test_hosted_icons.xul]
+[test_hosted_checkforupdates_from_webapp_runtime.xul]
+skip-if = asan
 [test_packaged_icons.xul]
+[test_packaged_checkforupdates_from_webapp_runtime.xul]
+skip-if = asan
 [test_webapp_runtime_executable_update.xul]
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/data/app/hosted_manifest.webapp
@@ -0,0 +1,11 @@
+{
+  "name" : "Test app",
+  "version" : "VERSION_TOKEN",
+  "description": "Test app",
+  "launch_path": "/chrome/toolkit/webapps/tests/app.sjs?appreq",
+  "developer": {
+    "name": "marco",
+    "url": "http://www.example.com/"
+  },
+  "default_locale": "en-US"
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/data/app/manifest.webapp
@@ -0,0 +1,12 @@
+{
+  "name" : "Test app",
+  "version" : "VERSION_TOKEN",
+  "package_path": "app.sjs?getPackage=true",
+  "description": "Test app",
+  "launch_path": "/index.html",
+  "developer": {
+    "name": "marco",
+    "url": "http://www.example.com/"
+  },
+  "default_locale": "en-US"
+}
--- a/toolkit/webapps/tests/head.js
+++ b/toolkit/webapps/tests/head.js
@@ -445,35 +445,50 @@ function buildAppPackage(aManifest, aIco
                            false);
   }
 
   zipWriter.close();
 
   return zipFile.path;
 }
 
-function wasAppSJSAccessed() {
+function xhrRequest(aQueryString) {
   let deferred = Promise.defer();
 
   var xhr = new XMLHttpRequest();
 
   xhr.addEventListener("load", function() {
-    let ret = (xhr.responseText == "done") ? true : false;
-    deferred.resolve(ret);
+    deferred.resolve(xhr.responseText);
   });
 
   xhr.addEventListener("error", aError => deferred.reject(aError));
   xhr.addEventListener("abort", aError => deferred.reject(aError));
 
-  xhr.open('GET', 'http://test/chrome/toolkit/webapps/tests/app.sjs?testreq', true);
+  xhr.open('GET', 'http://test/chrome/toolkit/webapps/tests/app.sjs' + aQueryString, true);
   xhr.send();
 
   return deferred.promise;
 }
 
+function wasAppSJSAccessed() {
+  return xhrRequest('?testreq').then((aResponseText) => {
+    return (aResponseText == 'done') ? true : false;
+  });
+}
+
+function setState(aVar, aState) {
+  return xhrRequest('?set' + aVar + '=' + aState).then((aResponseText) => {
+    is(aResponseText, "OK", "set" + aVar + " OK");
+  });
+}
+
+function getState(aVar) {
+  return xhrRequest('?get' + aVar);
+}
+
 function generateDataURI(aFile) {
   var contentType = Cc["@mozilla.org/mime;1"].
                     getService(Ci.nsIMIMEService).
                     getTypeFromFile(aFile);
 
   var inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
                     createInstance(Ci.nsIFileInputStream);
   inputStream.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
@@ -512,8 +527,20 @@ let setMacRootInstallDir = Task.async(fu
                                                      "Applications");
   yield OS.File.makeDir(NativeApp.prototype._rootInstallDir,
                         { ignoreExisting: true });
 
   SimpleTest.registerCleanupFunction(function() {
     NativeApp.prototype._rootInstallDir = oldRootInstallDir;
   });
 });
+
+let writeToFile = Task.async(function*(aPath, aData) {
+  let data = new TextEncoder().encode(aData);
+
+  let file;
+  try {
+    file = yield OS.File.open(aPath, { truncate: true, write: true }, { unixMode: 0o777 });
+    yield file.write(data);
+  } finally {
+    yield file.close();
+  }
+});
copy from toolkit/webapps/tests/test_custom_origin.xul
copy to toolkit/webapps/tests/test_hosted_checkforupdates_from_webapp_runtime.xul
--- a/toolkit/webapps/tests/test_custom_origin.xul
+++ b/toolkit/webapps/tests/test_hosted_checkforupdates_from_webapp_runtime.xul
@@ -1,129 +1,111 @@
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/css" href="chrome://global/skin"?>
 <?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
 <!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1029674
+https://bugzilla.mozilla.org/show_bug.cgi?id=1036717
 -->
-<window title="Mozilla Bug 1029674"
+<window title="Mozilla Bug 1036717"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript"
           src="chrome://mochikit/content/chrome-harness.js"></script>
   <script type="application/javascript" src="head.js"/>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml">
-  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1029674"
-     target="_blank">Mozilla Bug 1029674</a>
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1036717"
+     target="_blank">Mozilla Bug 1036717</a>
   </body>
 
 <script type="application/javascript">
 <![CDATA[
 
-/** Test for Bug 1029674 **/
+/** Test for Bug 1036717 **/
 
 "use strict";
 
 SimpleTest.waitForExplicitFinish();
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NativeApp.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 let runTest = Task.async(function*() {
-  let manifest = yield readJSON(getTestFilePath("data/custom_origin.webapp"));
+  let manifest = yield readJSON(getTestFilePath("data/app/hosted_manifest.webapp"));
 
   let app = {
     name: manifest.name,
-    manifestURL: "http://test/chrome/toolkit/webapps/tests/data/custom_origin.webapp",
-    origin: "app://test.origin.privileged.app",
+    manifestURL: "http://127.0.0.1:8888/chrome/toolkit/webapps/tests/app.sjs?getHostedManifest=true",
+    origin: "http://127.0.0.1:8888",
   };
 
   let testAppInfo = new TestAppInfo(app, true);
 
   // Get to a clean state before the test
   yield testAppInfo.cleanup();
 
   SimpleTest.registerCleanupFunction(() => testAppInfo.cleanup());
 
   setDryRunPref();
 
-  // Use the test root certificate for the test
-  Cu.import("resource://gre/modules/StoreTrustAnchor.jsm");
-  let oldIndex = TrustedRootCertificate.index;
-  TrustedRootCertificate.index = Ci.nsIX509CertDB.AppXPCShellRoot;
-
-  SimpleTest.registerCleanupFunction(function() {
-    TrustedRootCertificate.index = oldIndex;
-  });
-
-  // Allow signed apps to be installed from the test origin
-  let oldSignedAppOrigins;
-  try {
-    oldSignedAppOrigins = Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from");
-  } catch (ex) {}
-
-  let newSignedAppOrigins = oldSignedAppOrigins.concat(",chrome://mochitests");
-  Services.prefs.setCharPref("dom.mozApps.signed_apps_installable_from", newSignedAppOrigins);
-
-  SimpleTest.registerCleanupFunction(function() {
-    Services.prefs.setCharPref("dom.mozApps.signed_apps_installable_from", oldSignedAppOrigins);
-  });
-
   // On Mac build servers, we don't have enough privileges to write to /Applications,
   // so we install apps in a user-owned directory.
   if (MAC) {
     yield setMacRootInstallDir(OS.Path.join(OS.Constants.Path.homeDir, "Applications"));
   }
 
   confirmNextInstall();
 
-  let request = navigator.mozApps.installPackage("http://test/chrome/toolkit/webapps/tests/data/custom_origin.webapp");
+  yield setState("Version", 1);
+
+  let request = navigator.mozApps.install(app.manifestURL);
 
   let (deferred = Promise.defer()) {
     request.onerror = function() {
       deferred.reject(this.error.name);
     };
     request.onsuccess = deferred.resolve;
     yield deferred.promise;
   }
 
   let appObject = request.result;
   ok(appObject, "app is non-null");
 
-  let (deferred = Promise.defer()) {
-    appObject.ondownloaderror = function() {
-      deferred.reject(this.error.name);
-    };
-    appObject.ondownloadapplied = deferred.resolve;
-    yield deferred.promise;
-  };
-
   while (!WebappOSUtils.isLaunchable(app)) {
     yield wait(1000);
   }
   ok(true, "App launchable");
 
-  let exeFile = getFile(testAppInfo.exePath);
+  // Force the update timer to fire in 1 second
+  let prefsFile = OS.Path.join(testAppInfo.profileDir.path, "prefs.js");
+  yield writeToFile(prefsFile, "user_pref(\"webapprt.app_update_interval\", 1);");
 
-  ok(exeFile.isExecutable(), "webapprt executable is executable");
+  // Update app version
+  yield setState("Version", 2);
+
+  // Launch the webapprt executable that will verify the presence of an update
+  let exeFile = getFile(testAppInfo.exePath);
 
   let appClosed = false;
 
   testAppInfo.appProcess.init(exeFile)
   testAppInfo.appProcess.runAsync([], 0, () => appClosed = true);
 
-  while (!(yield wasAppSJSAccessed()) && !appClosed) {
+  // If there was a second request to get the manifest, then the webapprt
+  // has searched for an update.
+  while (Number(yield getState("ManifestQueries")) < 2 &&
+         !appClosed) {
     yield wait(1000);
   }
   ok(!appClosed, "App was launched and is still running");
+  ok(Number(yield getState("ManifestQueries")) >= 2, "Two manifest requests");
 
   SimpleTest.finish();
 });
 
 runTest().catch((e) => {
   ok(false, "Error during test: " + e);
   SimpleTest.finish();
 });
copy from toolkit/webapps/tests/test_custom_origin.xul
copy to toolkit/webapps/tests/test_packaged_checkforupdates_from_webapp_runtime.xul
--- a/toolkit/webapps/tests/test_custom_origin.xul
+++ b/toolkit/webapps/tests/test_packaged_checkforupdates_from_webapp_runtime.xul
@@ -1,129 +1,122 @@
 <?xml version="1.0"?>
 <?xml-stylesheet type="text/css" href="chrome://global/skin"?>
 <?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
 <!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1029674
+https://bugzilla.mozilla.org/show_bug.cgi?id=1036717
 -->
-<window title="Mozilla Bug 1029674"
+<window title="Mozilla Bug 1036717"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript"
           src="chrome://mochikit/content/chrome-harness.js"></script>
   <script type="application/javascript" src="head.js"/>
 
   <!-- test results are displayed in the html:body -->
   <body xmlns="http://www.w3.org/1999/xhtml">
-  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1029674"
-     target="_blank">Mozilla Bug 1029674</a>
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1036717"
+     target="_blank">Mozilla Bug 1036717</a>
   </body>
 
 <script type="application/javascript">
 <![CDATA[
 
-/** Test for Bug 1029674 **/
+/** Test for Bug 1036717 **/
 
 "use strict";
 
 SimpleTest.waitForExplicitFinish();
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NativeApp.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 let runTest = Task.async(function*() {
-  let manifest = yield readJSON(getTestFilePath("data/custom_origin.webapp"));
+  let manifest = yield readJSON(getTestFilePath("data/app/manifest.webapp"));
 
   let app = {
     name: manifest.name,
-    manifestURL: "http://test/chrome/toolkit/webapps/tests/data/custom_origin.webapp",
-    origin: "app://test.origin.privileged.app",
+    manifestURL: "http://127.0.0.1:8888/chrome/toolkit/webapps/tests/app.sjs?getManifest=true",
+    origin: "app://test.app",
   };
 
   let testAppInfo = new TestAppInfo(app, true);
 
   // Get to a clean state before the test
   yield testAppInfo.cleanup();
 
   SimpleTest.registerCleanupFunction(() => testAppInfo.cleanup());
 
   setDryRunPref();
 
-  // Use the test root certificate for the test
-  Cu.import("resource://gre/modules/StoreTrustAnchor.jsm");
-  let oldIndex = TrustedRootCertificate.index;
-  TrustedRootCertificate.index = Ci.nsIX509CertDB.AppXPCShellRoot;
-
-  SimpleTest.registerCleanupFunction(function() {
-    TrustedRootCertificate.index = oldIndex;
-  });
-
-  // Allow signed apps to be installed from the test origin
-  let oldSignedAppOrigins;
-  try {
-    oldSignedAppOrigins = Services.prefs.getCharPref("dom.mozApps.signed_apps_installable_from");
-  } catch (ex) {}
-
-  let newSignedAppOrigins = oldSignedAppOrigins.concat(",chrome://mochitests");
-  Services.prefs.setCharPref("dom.mozApps.signed_apps_installable_from", newSignedAppOrigins);
-
-  SimpleTest.registerCleanupFunction(function() {
-    Services.prefs.setCharPref("dom.mozApps.signed_apps_installable_from", oldSignedAppOrigins);
-  });
-
   // On Mac build servers, we don't have enough privileges to write to /Applications,
   // so we install apps in a user-owned directory.
   if (MAC) {
     yield setMacRootInstallDir(OS.Path.join(OS.Constants.Path.homeDir, "Applications"));
   }
 
   confirmNextInstall();
 
-  let request = navigator.mozApps.installPackage("http://test/chrome/toolkit/webapps/tests/data/custom_origin.webapp");
+  yield setState("Version", 1);
+
+  let request = navigator.mozApps.installPackage(app.manifestURL);
 
   let (deferred = Promise.defer()) {
     request.onerror = function() {
       deferred.reject(this.error.name);
     };
     request.onsuccess = deferred.resolve;
     yield deferred.promise;
   }
 
   let appObject = request.result;
   ok(appObject, "app is non-null");
 
   let (deferred = Promise.defer()) {
     appObject.ondownloaderror = function() {
-      deferred.reject(this.error.name);
+      deferred.reject(appObject.downloadError.name);
     };
     appObject.ondownloadapplied = deferred.resolve;
     yield deferred.promise;
   };
 
   while (!WebappOSUtils.isLaunchable(app)) {
     yield wait(1000);
   }
   ok(true, "App launchable");
 
-  let exeFile = getFile(testAppInfo.exePath);
+  // Force the update timer to fire in 1 second
+  let prefsFile = OS.Path.join(testAppInfo.profileDir.path, "prefs.js");
+  yield writeToFile(prefsFile, "user_pref(\"webapprt.app_update_interval\", 1);");
 
-  ok(exeFile.isExecutable(), "webapprt executable is executable");
+  // Update app version
+  yield setState("Version", 2);
+
+  // Launch the webapprt executable that will verify the presence of an update
+  let exeFile = getFile(testAppInfo.exePath);
 
   let appClosed = false;
 
   testAppInfo.appProcess.init(exeFile)
   testAppInfo.appProcess.runAsync([], 0, () => appClosed = true);
 
-  while (!(yield wasAppSJSAccessed()) && !appClosed) {
+  // If there was a second request to get the manifest and the package,
+  // then the webapprt has searched for an update and has started to
+  // download it.
+  while ((Number(yield getState("PackageQueries")) < 2 ||
+          Number(yield getState("ManifestQueries")) < 2) &&
+         !appClosed) {
     yield wait(1000);
   }
   ok(!appClosed, "App was launched and is still running");
+  ok(Number(yield getState("ManifestQueries")) >= 2, "Two manifest requests");
+  ok(Number(yield getState("PackageQueries")) >= 2, "Two package requests");
 
   SimpleTest.finish();
 });
 
 runTest().catch((e) => {
   ok(false, "Error during test: " + e);
   SimpleTest.finish();
 });
--- a/webapprt/WebappRT.jsm
+++ b/webapprt/WebappRT.jsm
@@ -128,11 +128,11 @@ this.WebappRT = {
         };
 
         thisApp.ondownloadapplied = () => {
           // Application updated, nothing to do.
         };
 
         thisApp.checkForUpdate();
       }
-    }, 24 * 60 * 60);
+    }, Services.prefs.getIntPref("webapprt.app_update_interval"));
   },
 };
--- a/webapprt/prefs.js
+++ b/webapprt/prefs.js
@@ -1,12 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// Check for updates once a day.
+pref("webapprt.app_update_interval", 86400);
+
 pref("browser.chromeURL", "chrome://webapprt/content/webapp.xul");
 pref("browser.download.folderList", 1);
 
 // Disable all add-on locations other than the profile (which can't be disabled this way)
 pref("extensions.enabledScopes", 1);
 // Auto-disable any add-ons that are "dropped in" to the profile
 pref("extensions.autoDisableScopes", 1);
 // Disable add-on installation via the web-exposed APIs