License Key - Activate & Manage Tests

There are two test files, license.activate.spec.ts and license.manage.spec.ts

These test files each contain a test:
- Creates, activates, and deletes an online license for a self hosted account - license.activate.spec.ts
- Retrieves plans, creates checkout session, and updates license - license.manage.spec.ts

Updated and created API files
- StripeAPI
- LicenseAPI
- internal-api LicenseAPI
- index & AccountInternalAPI also updated to reflect API file changes
This commit is contained in:
Mitch-Budibase 2023-10-05 17:39:40 +01:00
parent 321003afec
commit 678033cc8b
7 changed files with 329 additions and 48 deletions

View File

@ -1,5 +1,5 @@
import AccountInternalAPIClient from "./AccountInternalAPIClient" import AccountInternalAPIClient from "./AccountInternalAPIClient"
import { AccountAPI, LicenseAPI, AuthAPI } from "./apis" import { AccountAPI, LicenseAPI, AuthAPI, StripeAPI } from "./apis"
import { State } from "../../types" import { State } from "../../types"
export default class AccountInternalAPI { export default class AccountInternalAPI {
@ -8,11 +8,13 @@ export default class AccountInternalAPI {
auth: AuthAPI auth: AuthAPI
accounts: AccountAPI accounts: AccountAPI
licenses: LicenseAPI licenses: LicenseAPI
stripe: StripeAPI
constructor(state: State) { constructor(state: State) {
this.client = new AccountInternalAPIClient(state) this.client = new AccountInternalAPIClient(state)
this.auth = new AuthAPI(this.client) this.auth = new AuthAPI(this.client)
this.accounts = new AccountAPI(this.client) this.accounts = new AccountAPI(this.client)
this.licenses = new LicenseAPI(this.client) this.licenses = new LicenseAPI(this.client)
this.stripe = new StripeAPI((this.client))
} }
} }

View File

@ -2,25 +2,23 @@ import AccountInternalAPIClient from "../AccountInternalAPIClient"
import { import {
Account, Account,
CreateOfflineLicenseRequest, CreateOfflineLicenseRequest,
GetLicenseKeyResponse,
GetOfflineLicenseResponse, GetOfflineLicenseResponse,
UpdateLicenseRequest, UpdateLicenseRequest,
} from "@budibase/types" } from "@budibase/types"
import { Response } from "node-fetch" import { Response } from "node-fetch"
import BaseAPI from "./BaseAPI" import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types" import { APIRequestOpts } from "../../../types"
export default class LicenseAPI extends BaseAPI { export default class LicenseAPI extends BaseAPI {
client: AccountInternalAPIClient client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) { constructor(client: AccountInternalAPIClient) {
super() super()
this.client = client this.client = client
} }
async updateLicense( async updateLicense(
accountId: string, accountId: string,
body: UpdateLicenseRequest, body: UpdateLicenseRequest,
opts: APIRequestOpts = { status: 200 } opts: APIRequestOpts = { status: 200 }
): Promise<[Response, Account]> { ): Promise<[Response, Account]> {
return this.doRequest(() => { return this.doRequest(() => {
return this.client.put(`/api/accounts/${accountId}/license`, { return this.client.put(`/api/accounts/${accountId}/license`, {
@ -29,44 +27,111 @@ export default class LicenseAPI extends BaseAPI {
}) })
}, opts) }, opts)
} }
// TODO: Better approach for setting tenant id header // TODO: Better approach for setting tenant id header
async createOfflineLicense( async createOfflineLicense(
accountId: string, accountId: string,
tenantId: string, tenantId: string,
body: CreateOfflineLicenseRequest, body: CreateOfflineLicenseRequest,
opts: { status?: number } = {} opts: { status?: number } = {}
): Promise<Response> { ): Promise<Response> {
const [response, json] = await this.client.post( const [response, json] = await this.client.post(
`/api/internal/accounts/${accountId}/license/offline`, `/api/internal/accounts/${accountId}/license/offline`,
{ {
body, body,
internal: true, internal: true,
headers: { headers: {
"x-budibase-tenant-id": tenantId, "x-budibase-tenant-id": tenantId,
},
}
)
expect(response.status).toBe(opts.status ? opts.status : 201)
return response
}
async getOfflineLicense(
accountId: string,
tenantId: string,
opts: { status?: number } = {}
): Promise<[Response, GetOfflineLicenseResponse]> {
const [response, json] = await this.client.get(
`/api/internal/accounts/${accountId}/license/offline`,
{
internal: true,
headers: {
"x-budibase-tenant-id": tenantId,
},
}
)
expect(response.status).toBe(opts.status ? opts.status : 200)
return [response, json]
}
async getLicenseKey(
opts: { status?: number } = {}
): Promise<[Response, GetLicenseKeyResponse]> {
const [response, json] = await this.client.get(`/api/license/key`)
expect(response.status).toBe(opts.status ? opts.status : 200)
return [response, json]
}
async activateLicense(
apiKey: string,
tenantId: string,
licenseKey: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/license/activate`, {
body: {
apiKey: apiKey,
tenantId: tenantId,
licenseKey: licenseKey,
}, },
} })
}, opts)
}
async regenerateLicenseKey(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/license/key/regenerate`, {})
}, opts)
}
async getPlans(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/plans`)
}, opts)
}
async updatePlan(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.put(`/api/license/plan`)
}, opts)
}
async refreshAccountLicense(
accountId: string,
opts: { status?: number } = {}
): Promise<Response> {
const [response, json] = await this.client.post(
`/api/accounts/${accountId}/license/refresh`,
{
internal: true,
}
) )
expect(response.status).toBe(opts.status ? opts.status : 201) expect(response.status).toBe(opts.status ? opts.status : 201)
return response return response
} }
async getOfflineLicense( async getLicenseUsage(opts: APIRequestOpts = { status: 200 }) {
accountId: string, return this.doRequest(() => {
tenantId: string, return this.client.get(`/api/license/usage`)
opts: { status?: number } = {} }, opts)
): Promise<[Response, GetOfflineLicenseResponse]> {
const [response, json] = await this.client.get(
`/api/internal/accounts/${accountId}/license/offline`,
{
internal: true,
headers: {
"x-budibase-tenant-id": tenantId,
},
}
)
expect(response.status).toBe(opts.status ? opts.status : 200)
return [response, json]
} }
}
async licenseUsageTriggered(
opts: { status?: number } = {}
): Promise<Response> {
const [response, json] = await this.client.post(
`/api/license/usage/triggered`
)
expect(response.status).toBe(opts.status ? opts.status : 201)
return response
}
}

View File

@ -0,0 +1,64 @@
import AccountInternalAPIClient from "../AccountInternalAPIClient"
import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types"
export default class StripeAPI extends BaseAPI {
client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) {
super()
this.client = client
}
async createCheckoutSession(
priceId: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/checkout-session`, {
body: { priceId },
})
}, opts)
}
async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/checkout-success`)
}, opts)
}
async createPortalSession(
stripeCustomerId: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/portal-session`, {
body: { stripeCustomerId },
})
}, opts)
}
async linkStripeCustomer(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/link`)
}, opts)
}
async getInvoices(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/invoices`)
}, opts)
}
async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/upcoming-invoice`)
}, opts)
}
async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/customers`)
}, opts)
}
}

View File

@ -1,3 +1,4 @@
export { default as AuthAPI } from "./AuthAPI" export { default as AuthAPI } from "./AuthAPI"
export { default as AccountAPI } from "./AccountAPI" export { default as AccountAPI } from "./AccountAPI"
export { default as LicenseAPI } from "./LicenseAPI" export { default as LicenseAPI } from "./LicenseAPI"
export { default as StripeAPI } from "./StripeAPI"

View File

@ -0,0 +1,74 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixures from "../../fixtures"
import { Feature, Hosting } from "@budibase/types"
describe("license activation", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("creates, activates and deletes online license - self host", async () => {
// Remove existing license key
await config.internalApi.license.deleteLicenseKey()
// Verify license key not found
await config.internalApi.license.getLicenseKey({ status: 404 })
// Create self host account
const createAccountRequest = fixures.accounts.generateAccount({
hosting: Hosting.SELF,
})
const [createAccountRes, account] =
await config.accountsApi.accounts.create(createAccountRequest, { autoVerify: true })
let licenseKey: string = " "
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
// Retrieve license key
const [res, body] =
await config.accountsApi.licenses.getLicenseKey()
licenseKey = body.licenseKey
})
const accountId = account.accountId!
// Update license to have paid feature
const [res, acc] = await config.accountsApi.licenses.updateLicense(
accountId,
{
overrides: {
features: [Feature.APP_BACKUPS],
},
}
)
// Activate license key
await config.internalApi.license.activateLicenseKey({licenseKey})
// Verify license updated with new feature
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
const [selfRes, body] = await config.api.accounts.self()
expect(body.license.features[0]).toBe("appBackups")
})
// Remove license key
await config.internalApi.license.deleteLicenseKey()
// Verify license key not found
await config.internalApi.license.getLicenseKey({ status: 404 })
// Verify user downgraded to free license
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
const [selfRes, body] = await config.api.accounts.self()
expect(body.license.plan.type).toBe("free")
})
})
})

View File

@ -0,0 +1,57 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Hosting, PlanType } from "@budibase/types"
describe("license management", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("retrieves plans, creates checkout session, and updates license", async () => {
// Create cloud account
const createAccountRequest = fixtures.accounts.generateAccount({
hosting: Hosting.CLOUD,
})
// Self response has free license
const [selfRes, selfBody] = await config.api.accounts.self()
expect(selfBody.license.plan.type).toBe(PlanType.FREE)
// Retrieve plans
const [plansRes, planBody] = await config.api.licenses.getPlans()
// Select priceId from premium plan
let premiumPriceId = null
for (const plan of planBody) {
if (plan.type === PlanType.PREMIUM) {
premiumPriceId = plan.prices[0].priceId
break
}
}
// Create checkout session for price
const checkoutSessionRes = await config.api.stripe.createCheckoutSession(
premiumPriceId
)
const checkoutSessionUrl = checkoutSessionRes[1].url
expect(checkoutSessionUrl).toContain("checkout.stripe.com")
// TODO: Mimic checkout success
// Create stripe customer
// Create subscription for premium plan
// Assert license updated from free to premium
// Create portal session
//await config.api.stripe.createPortalSession()
// Update from free to business license
// License updated
})
})

View File

@ -1,45 +1,63 @@
import { Response } from "node-fetch" import { Response } from "node-fetch"
import { import {
ActivateLicenseKeyRequest,
ActivateOfflineLicenseTokenRequest, ActivateOfflineLicenseTokenRequest,
GetLicenseKeyResponse,
GetOfflineIdentifierResponse, GetOfflineIdentifierResponse,
GetOfflineLicenseTokenResponse, GetOfflineLicenseTokenResponse,
} from "@budibase/types" } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient" import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI" import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types"
export default class LicenseAPI extends BaseAPI { export default class LicenseAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) { constructor(client: BudibaseInternalAPIClient) {
super(client) super(client)
} }
async getOfflineLicenseToken( async getOfflineLicenseToken(
opts: { status?: number } = {} opts: { status?: number } = {}
): Promise<[Response, GetOfflineLicenseTokenResponse]> { ): Promise<[Response, GetOfflineLicenseTokenResponse]> {
const [response, body] = await this.get( const [response, body] = await this.get(
`/global/license/offline`, `/global/license/offline`,
opts.status opts.status
) )
return [response, body] return [response, body]
} }
async deleteOfflineLicenseToken(): Promise<[Response]> { async deleteOfflineLicenseToken(): Promise<[Response]> {
const [response] = await this.del(`/global/license/offline`, 204) const [response] = await this.del(`/global/license/offline`, 204)
return [response] return [response]
} }
async activateOfflineLicenseToken( async activateOfflineLicenseToken(
body: ActivateOfflineLicenseTokenRequest body: ActivateOfflineLicenseTokenRequest
): Promise<[Response]> { ): Promise<[Response]> {
const [response] = await this.post(`/global/license/offline`, body) const [response] = await this.post(`/global/license/offline`, body)
return [response] return [response]
} }
async getOfflineIdentifier(): Promise< async getOfflineIdentifier(): Promise<
[Response, GetOfflineIdentifierResponse] [Response, GetOfflineIdentifierResponse]
> { > {
const [response, body] = await this.get( const [response, body] = await this.get(
`/global/license/offline/identifier` `/global/license/offline/identifier`
) )
return [response, body] return [response, body]
} }
}
async getLicenseKey(
opts: { status?: number } = {}
): Promise<[Response, GetLicenseKeyResponse]> {
const [response, body] = await this.get(`/global/license/key`, opts.status)
return [response, body]
}
async activateLicenseKey(
body: ActivateLicenseKeyRequest
): Promise<[Response]> {
const [response] = await this.post(`/global/license/key`, body)
return [response]
}
async deleteLicenseKey(): Promise<[Response]> {
const [response] = await this.del(`/global/license/key`, 204)
return [response]
}
}