Skip to content

Commit 9ca0872

Browse files
committed
add token tests
1 parent c0952c1 commit 9ca0872

File tree

6 files changed

+248
-36
lines changed

6 files changed

+248
-36
lines changed

src/routes/generateToken.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
4444
</h1>
4545
`);
4646
}
47+
} else {
48+
return res.sendStatus(403);
4749
}
4850
}

src/routes/verifyToken.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { config } from "../config";
44
import { privateDB } from "../databases/databases";
55
import { Logger } from "../utils/logger";
66
import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils";
7-
import FormData from "form-data";
87

98
interface VerifyTokenRequest extends Request {
109
query: {
@@ -13,7 +12,7 @@ interface VerifyTokenRequest extends Request {
1312
}
1413

1514
export const validatelicenseKeyRegex = (token: string) =>
16-
new RegExp(/[A-Za-z0-9]{40}/).test(token);
15+
new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token);
1716

1817
export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise<Response> {
1918
const { query: { licenseKey } } = req;
@@ -26,12 +25,6 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
2625
allowed: false
2726
});
2827
}
29-
const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/);
30-
if (!licenseRegex.test(licenseKey)) {
31-
return res.status(200).send({
32-
allowed: false
33-
});
34-
}
3528

3629
const tokens = (await privateDB.prepare("get", `SELECT "accessToken", "refreshToken", "expiresIn" from "oauthLicenseKeys" WHERE "licenseKey" = ?`
3730
, [licenseKey])) as {accessToken: string, refreshToken: string, expiresIn: number};
@@ -42,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
4235
refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error);
4336
}
4437

38+
/* istanbul ignore else */
4539
if (identity) {
4640
const membership = identity.included?.[0]?.attributes;
4741
const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0)
@@ -73,20 +67,13 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
7367
async function checkAllGumroadProducts(licenseKey: string): Promise<boolean> {
7468
for (const link of config.gumroad.productPermalinks) {
7569
try {
76-
const formData = new FormData();
77-
formData.append("product_permalink", link);
78-
formData.append("license_key", licenseKey);
79-
80-
const result = await axios.request({
81-
url: "https://api.gumroad.com/v2/licenses/verify",
82-
data: formData,
83-
method: "POST",
84-
headers: formData.getHeaders()
70+
const result = await axios.post("https://api.gumroad.com/v2/licenses/verify", {
71+
params: { product_permalink: link, license_key: licenseKey }
8572
});
8673

8774
const allowed = result.status === 200 && result.data?.success;
8875
if (allowed) return allowed;
89-
} catch (e) {
76+
} catch (e) /* istanbul ignore next */ {
9077
Logger.error(`Gumroad fetch for ${link} failed: ${e}`);
9178
}
9279
}

test/cases/generateVerifyToken.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import assert from "assert";
2+
import { config } from "../../src/config";
3+
import axios from "axios";
4+
import { createAndSaveToken, TokenType } from "../../src/utils/tokenUtils";
5+
import MockAdapter from "axios-mock-adapter";
6+
let mock: MockAdapter;
7+
import * as patreon from "../mocks/patreonMock";
8+
import * as gumroad from "../mocks/gumroadMock";
9+
import { client } from "../utils/httpClient";
10+
import { validatelicenseKeyRegex } from "../../src/routes/verifyToken";
11+
12+
const generateEndpoint = "/api/generateToken";
13+
const getGenerateToken = (type: string, code: string | null, adminUserID: string | null) => client({
14+
url: `${generateEndpoint}/${type}`,
15+
params: { code, adminUserID }
16+
});
17+
18+
const verifyEndpoint = "/api/verifyToken";
19+
const getVerifyToken = (licenseKey: string | null) => client({
20+
url: verifyEndpoint,
21+
params: { licenseKey }
22+
});
23+
24+
let patreonLicense: string;
25+
let localLicense: string;
26+
const gumroadLicense = gumroad.generateLicense();
27+
28+
const extractLicenseKey = (data: string) => {
29+
const regex = /([A-Za-z0-9]{40})/;
30+
const match = data.match(regex);
31+
if (!match) throw new Error("Failed to extract license key");
32+
return match[1];
33+
};
34+
35+
describe("generateToken test", function() {
36+
37+
before(function() {
38+
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
39+
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
40+
});
41+
42+
after(function () {
43+
mock.restore();
44+
});
45+
46+
it("Should be able to create patreon token for active patron", function (done) {
47+
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
48+
if (!config?.patreon) this.skip();
49+
getGenerateToken("patreon", "patreon_code", "").then(res => {
50+
patreonLicense = extractLicenseKey(res.data);
51+
assert.ok(validatelicenseKeyRegex(patreonLicense));
52+
done();
53+
});
54+
});
55+
56+
it("Should be able to create new local token", function (done) {
57+
createAndSaveToken(TokenType.local).then((licenseKey) => {
58+
assert.ok(validatelicenseKeyRegex(licenseKey));
59+
localLicense = licenseKey;
60+
done();
61+
});
62+
});
63+
64+
it("Should return 400 if missing code parameter", function (done) {
65+
getGenerateToken("patreon", null, "").then(res => {
66+
assert.strictEqual(res.status, 400);
67+
done();
68+
});
69+
});
70+
71+
it("Should return 403 if missing adminuserID parameter", function (done) {
72+
getGenerateToken("local", "fake-code", null).then(res => {
73+
assert.strictEqual(res.status, 403);
74+
done();
75+
});
76+
});
77+
78+
it("Should return 403 for invalid adminuserID parameter", function (done) {
79+
getGenerateToken("local", "fake-code", "fakeAdminID").then(res => {
80+
assert.strictEqual(res.status, 403);
81+
done();
82+
});
83+
});
84+
});
85+
86+
describe("verifyToken static tests", function() {
87+
it("Should fast reject invalid token", function (done) {
88+
getVerifyToken("00000").then(res => {
89+
assert.strictEqual(res.status, 200);
90+
assert.ok(!res.data.allowed);
91+
done();
92+
}).catch(err => done(err));
93+
});
94+
95+
it("Should return 400 if missing code token", function (done) {
96+
getVerifyToken(null).then(res => {
97+
assert.strictEqual(res.status, 400);
98+
done();
99+
});
100+
});
101+
});
102+
103+
describe("verifyToken mock tests", function() {
104+
105+
beforeEach(function() {
106+
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
107+
});
108+
109+
afterEach(function () {
110+
mock.restore();
111+
});
112+
113+
it("Should accept current patron", function (done) {
114+
if (!config?.patreon) this.skip();
115+
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
116+
getVerifyToken(patreonLicense).then(res => {
117+
assert.strictEqual(res.status, 200);
118+
assert.ok(res.data.allowed);
119+
done();
120+
}).catch(err => done(err));
121+
});
122+
123+
it("Should reject nonexistent patron", function (done) {
124+
if (!config?.patreon) this.skip();
125+
mock.onGet(/identity/).reply(200, patreon.invalidIdentity);
126+
getVerifyToken(patreonLicense).then(res => {
127+
assert.strictEqual(res.status, 200);
128+
assert.ok(!res.data.allowed);
129+
done();
130+
}).catch(err => done(err));
131+
});
132+
133+
it("Should accept qualitying former patron", function (done) {
134+
if (!config?.patreon) this.skip();
135+
mock.onGet(/identity/).reply(200, patreon.formerIdentitySucceed);
136+
getVerifyToken(patreonLicense).then(res => {
137+
assert.strictEqual(res.status, 200);
138+
assert.ok(res.data.allowed);
139+
done();
140+
}).catch(err => done(err));
141+
});
142+
143+
it("Should reject unqualitifed former patron", function (done) {
144+
if (!config?.patreon) this.skip();
145+
mock.onGet(/identity/).reply(200, patreon.formerIdentityFail);
146+
getVerifyToken(patreonLicense).then(res => {
147+
assert.strictEqual(res.status, 200);
148+
assert.ok(!res.data.allowed);
149+
done();
150+
}).catch(err => done(err));
151+
});
152+
153+
it("Should accept real gumroad key", function (done) {
154+
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseSuccess);
155+
getVerifyToken(gumroadLicense).then(res => {
156+
assert.strictEqual(res.status, 200);
157+
assert.ok(res.data.allowed);
158+
done();
159+
}).catch(err => done(err));
160+
});
161+
162+
it("Should reject fake gumroad key", function (done) {
163+
mock.onPost("https://api.gumroad.com/v2/licenses/verify").reply(200, gumroad.licenseFail);
164+
getVerifyToken(gumroadLicense).then(res => {
165+
assert.strictEqual(res.status, 200);
166+
assert.ok(!res.data.allowed);
167+
done();
168+
}).catch(err => done(err));
169+
});
170+
171+
it("Should validate local license", function (done) {
172+
getVerifyToken(localLicense).then(res => {
173+
assert.strictEqual(res.status, 200);
174+
assert.ok(res.data.allowed);
175+
done();
176+
}).catch(err => done(err));
177+
});
178+
});

test/cases/tokenUtils.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,12 @@ let mock: MockAdapter;
88
import * as patreon from "../mocks/patreonMock";
99

1010
const validateToken = validatelicenseKeyRegex;
11-
const fakePatreonIdentity = {
12-
data: {},
13-
links: {},
14-
included: [
15-
{
16-
attributes: {
17-
is_monthly: true,
18-
currently_entitled_amount_cents: 100,
19-
patron_status: "active_patron",
20-
},
21-
id: "id",
22-
type: "campaign"
23-
}
24-
],
25-
};
2611

2712
describe("tokenUtils test", function() {
2813
before(function() {
2914
mock = new MockAdapter(axios, { onNoMatch: "throwException" });
3015
mock.onPost("https://www.patreon.com/api/oauth2/token").reply(200, patreon.fakeOauth);
31-
mock.onGet(/identity/).reply(200, patreon.fakeIdentity);
16+
mock.onGet(/identity/).reply(200, patreon.activeIdentity);
3217
});
3318

3419
it("Should be able to create patreon token", function (done) {
@@ -47,7 +32,7 @@ describe("tokenUtils test", function() {
4732
it("Should be able to get patreon identity", function (done) {
4833
if (!config?.patreon) this.skip();
4934
tokenUtils.getPatreonIdentity("fake_access_token").then((result) => {
50-
assert.deepEqual(result, patreon.fakeIdentity);
35+
assert.deepEqual(result, patreon.activeIdentity);
5136
done();
5237
});
5338
});

test/mocks/gumroadMock.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const licenseSuccess = {
2+
success: true,
3+
uses: 4,
4+
purchase: {}
5+
};
6+
7+
export const licenseFail = {
8+
success: false,
9+
message: "That license does not exist for the provided product."
10+
};
11+
12+
13+
const subCode = (length = 8) => {
14+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
15+
let result = "";
16+
for (let i = 0; i < length; i++) {
17+
result += characters[(Math.floor(Math.random() * characters.length))];
18+
}
19+
return result;
20+
}
21+
22+
export const generateLicense = (): string => `${subCode()}-${subCode()}-${subCode()}-${subCode()}`;

test/mocks/patreonMock.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const fakeIdentity = {
1+
export const activeIdentity = {
22
data: {},
33
links: {},
44
included: [
@@ -14,6 +14,44 @@ export const fakeIdentity = {
1414
],
1515
};
1616

17+
export const invalidIdentity = {
18+
data: {},
19+
links: {},
20+
included: [{}],
21+
};
22+
23+
export const formerIdentitySucceed = {
24+
data: {},
25+
links: {},
26+
included: [
27+
{
28+
attributes: {
29+
is_monthly: true,
30+
campaign_lifetime_support_cents: 500,
31+
patron_status: "former_patron",
32+
},
33+
id: "id",
34+
type: "campaign"
35+
}
36+
],
37+
};
38+
39+
export const formerIdentityFail = {
40+
data: {},
41+
links: {},
42+
included: [
43+
{
44+
attributes: {
45+
is_monthly: true,
46+
campaign_lifetime_support_cents: 1,
47+
patron_status: "former_patron",
48+
},
49+
id: "id",
50+
type: "campaign"
51+
}
52+
],
53+
};
54+
1755
export const fakeOauth = {
1856
access_token: "test_access_token",
1957
refresh_token: "test_refresh_token",

0 commit comments

Comments
 (0)