Remove app purchase/unpurchase

This commit is contained in:
Johannes Zellner
2025-05-13 11:46:13 +02:00
parent f77aeded6f
commit b8dcfcf900
4 changed files with 1 additions and 165 deletions

View File

@@ -1474,8 +1474,6 @@ async function install(data, auditSource) {
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
await purchaseApp({ appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' });
const task = {
args: { restoreConfig: null, skipDnsSetup, overwriteDns },
values: { },
@@ -2376,17 +2374,6 @@ async function exportApp(app, data, auditSource) {
return { taskId };
}
async function purchaseApp(data) {
assert.strictEqual(typeof data, 'object');
const [purchaseError] = await safe(appstore.purchaseApp(data));
if (!purchaseError) return;
await del(data.appId);
throw purchaseError;
}
async function clone(app, data, user, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof data, 'object');
@@ -2458,8 +2445,6 @@ async function clone(app, data, user, auditSource) {
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
await purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' });
const restoreConfig = { remotePath: backupInfo.remotePath, backupFormat: backupInfo.format };
const task = {
args: { restoreConfig, overwriteDns, skipDnsSetup, oldManifest: null },
@@ -2535,8 +2520,6 @@ async function unarchive(archive, data, auditSource) {
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
await purchaseApp({ appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' });
const task = {
args: { restoreConfig, overwriteDns },
values: {},
@@ -2564,8 +2547,6 @@ async function uninstall(app, auditSource) {
const error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL);
if (error) throw error;
await appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' });
const task = {
args: {},
values: {},

View File

@@ -18,9 +18,6 @@ exports = module.exports = {
registerCloudronWithLogin,
updateCloudron,
purchaseApp,
unpurchaseApp,
getSubscription,
isFreePlan,
@@ -171,60 +168,6 @@ function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
async function purchaseApp(data) {
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId }
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof data.appId, 'string');
const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/cloudronapps`)
.send(data)
.query({ accessToken: token })
.timeout(60 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, 'appstoreId does not exist');
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
if (response.status === 402) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
// 200 if already purchased, 201 is newly purchased
if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `App purchase failed. ${response.status} ${JSON.stringify(response.body)}`);
}
async function unpurchaseApp(appId, data) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId }
assert(data.appstoreId || data.manifestId);
const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const url = `${await getApiServerOrigin()}/api/v1/cloudronapps/${appId}`;
let [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(60 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 404) return; // was never purchased
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `App unpurchase failed to get app. status:${response.status}`);
[error, response] = await safe(superagent.del(url)
.send(data)
.query({ accessToken: token })
.timeout(60 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid appstore token');
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `App unpurchase failed. status:${response.status}`);
}
async function getBoxUpdate(options) {
assert.strictEqual(typeof options, 'object');
@@ -327,11 +270,6 @@ async function registerCloudron3(domain, version) {
await settings.set(settings.APPSTORE_API_TOKEN_KEY, response.body.token);
debug(`registerCloudron: Cloudron registered with id ${response.body.id}`);
// // app could already have been installed if we deleted the cloudron.io record and user re-registers
// for (const app of await apps.list()) {
// await safe(purchaseApp({ appId: app.id, appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }), { debug });
// }
}
async function registerCloudron(data) {
@@ -355,11 +293,6 @@ async function registerCloudron(data) {
await settings.set(settings.APPSTORE_API_TOKEN_KEY, response.body.cloudronToken);
debug(`registerCloudron: Cloudron registered with id ${response.body.cloudronId}`);
// app could already have been installed if we deleted the cloudron.io record and user re-registers
for (const app of await apps.list()) {
await safe(purchaseApp({ appId: app.id, appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }), { debug });
}
}
async function updateCloudron(data) {

View File

@@ -396,22 +396,8 @@ xdescribe('App API', function () {
});
});
it('app install fails due to purchase failure', function (done) {
const fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
superagent.post(SERVER_URL + '/api/v1/apps')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, subdomain: APP_SUBDOMAIN, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.status).to.equal(402);
expect(fake1.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', async function () {
it('app install succeeds', async function () {
const fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
const fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
await settings.setAppstoreApiToken(USER_1_APPSTORE_TOKEN);
@@ -423,7 +409,6 @@ xdescribe('App API', function () {
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
taskId = res.body.taskId;
});

View File

@@ -1,63 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
/* global beforeEach:false */
'use strict';
const appstore = require('../appstore.js'),
common = require('./common.js'),
expect = require('expect.js'),
nock = require('nock');
const APP_ID = 'appid';
const APPSTORE_APP_ID = 'appstoreappid';
describe('Appstore', function () {
const { setup, cleanup, appstoreToken, mockApiServerOrigin } = common;
before(setup);
before(() => { if (!nock.isActive()) nock.activate(); });
after(cleanup);
beforeEach(nock.cleanAll);
it('can purchase an app', async function () {
const scope1 = nock(mockApiServerOrigin)
.post(`/api/v1/cloudronapps?accessToken=${appstoreToken}`, function () { return true; })
.reply(201, {});
await appstore.purchaseApp({ appId: APP_ID, appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID });
expect(scope1.isDone()).to.be.ok();
});
it('unpurchase succeeds if app was never purchased', async function () {
const scope1 = nock(mockApiServerOrigin)
.get(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`)
.reply(404, {});
const scope2 = nock(mockApiServerOrigin)
.delete(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`, function () { return true; })
.reply(204, {});
await appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID });
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.not.be.ok();
});
it('can unpurchase an app', async function () {
const scope1 = nock(mockApiServerOrigin)
.get(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`)
.reply(200, {});
const scope2 = nock(mockApiServerOrigin)
.delete(`/api/v1/cloudronapps/${APP_ID}?accessToken=${appstoreToken}`, function () { return true; })
.reply(204, {});
await appstore.unpurchaseApp(APP_ID, { appstoreId: APPSTORE_APP_ID, manifestId: APPSTORE_APP_ID });
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
});
});