Skip to content

Commit f64741b

Browse files
committed
tests: add tests for basic auth
1 parent d2609b8 commit f64741b

File tree

5 files changed

+179
-87
lines changed

5 files changed

+179
-87
lines changed

src/auth.js

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,51 @@
1-
import { WorkerError } from "./common.js";
1+
import { WorkerError } from "./common.js"
22

3-
function parseBasicAuth(request) {
4-
const Authorization = request.headers.get('Authorization');
5-
6-
const [scheme, encoded] = Authorization.split(' ');
7-
8-
// The Authorization header must start with Basic, followed by a space.
9-
if (!encoded || scheme !== 'Basic') {
10-
throw new WorkerError(400, 'malformed authorization header');
11-
}
12-
13-
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0))
14-
const decoded = new TextDecoder().decode(buffer).normalize();
15-
16-
const index = decoded.indexOf(':');
3+
// Encoding function
4+
export function encodeBasicAuth(username, password) {
5+
const credentials = `${username}:${password}`
6+
const encodedCredentials = Buffer.from(credentials).toString("base64")
7+
return `Basic ${encodedCredentials}`
8+
}
179

18-
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
19-
throw WorkerError(400, 'invalid authorization value');
10+
// Decoding function
11+
export function decodeBasicAuth(encodedString) {
12+
const [scheme, encodedCredentials] = encodedString.split(" ")
13+
if (scheme !== "Basic") {
14+
throw new WorkerError(400, "Invalid authentication scheme")
2015
}
21-
22-
return {
23-
user: decoded.substring(0, index),
24-
pass: decoded.substring(index + 1),
25-
};
16+
const credentials = Buffer.from(encodedCredentials, "base64").toString("utf-8")
17+
const [username, password] = credentials.split(":")
18+
return { username, password }
2619
}
2720

2821
// return true if auth passes or is not required,
2922
// return auth page if auth is required
3023
// throw WorkerError if auth failed
3124
export function verifyAuth(request, env) {
3225
// pass auth if 'BASIC_AUTH' is not present
33-
if (!('BASIC_AUTH' in env)) return null
26+
if (!env.BASIC_AUTH) return null
3427

35-
const passwdMap = new Map(Object.entries(env['BASIC_AUTH']))
28+
const passwdMap = new Map(Object.entries(env.BASIC_AUTH))
3629

3730
// pass auth if 'BASIC_AUTH' is empty
3831
if (passwdMap.size === 0) return null
3932

40-
if (request.headers.has('Authorization')) {
41-
const { user, pass } = parseBasicAuth(request)
42-
if (passwdMap.get(user) === undefined) {
33+
if (request.headers.has("Authorization")) {
34+
const { username, password } = decodeBasicAuth(request.headers.get("Authorization"))
35+
if (passwdMap.get(username) === undefined) {
4336
throw new WorkerError(401, "user not found for basic auth")
44-
} else if (passwdMap.get(user) !== pass) {
37+
} else if (passwdMap.get(username) !== password) {
4538
throw new WorkerError(401, "incorrect passwd for basic auth")
4639
} else {
4740
return null
4841
}
4942
} else {
50-
return new Response('HTTP basic auth is required', {
43+
return new Response("HTTP basic auth is required", {
5144
status: 401,
5245
headers: {
5346
// Prompts the user for credentials.
54-
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
47+
"WWW-Authenticate": "Basic charset=\"UTF-8\"",
5548
},
56-
});
49+
})
5750
}
5851
}

test/basicAuth.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { expect, test } from "vitest"
2+
import { areBlobsEqual, BASE_URL, createFormData, randomBlob, staticPages, workerFetchWithAuth } from "./testUtils.js"
3+
import { encodeBasicAuth, decodeBasicAuth } from "../src/auth.js"
4+
5+
test("basic auth encode and decode", async () => {
6+
const userPasswdPairs = [
7+
["user1", "passwd1"],
8+
["あおい", "まなか"],
9+
["1234#", "اهلا"],
10+
]
11+
for (const [user, passwd] of userPasswdPairs) {
12+
const encoded = encodeBasicAuth(user, passwd)
13+
const decoded = decodeBasicAuth(encoded)
14+
expect(decoded.username).toStrictEqual(user)
15+
expect(decoded.password).toStrictEqual(passwd)
16+
}
17+
})
18+
19+
test("basic auth", async () => {
20+
const usersKv = {
21+
"user1": "passwd1",
22+
"user2": "passwd2",
23+
}
24+
25+
// access index
26+
for (const page of staticPages) {
27+
expect((await workerFetchWithAuth(usersKv, `${BASE_URL}/${page}`, {})).status).toStrictEqual(401)
28+
}
29+
expect((await workerFetchWithAuth(usersKv, BASE_URL, {
30+
headers: { "Authorization": encodeBasicAuth("user1", usersKv["user1"]) },
31+
})).status).toStrictEqual(200)
32+
33+
// upload with no auth
34+
const blob1 = randomBlob(1024)
35+
const uploadResp = await workerFetchWithAuth(usersKv, BASE_URL, {
36+
method: "POST",
37+
body: createFormData({ c: blob1 }),
38+
})
39+
expect(uploadResp.status).toStrictEqual(401)
40+
41+
// upload with true auth
42+
const uploadResp1 = await workerFetchWithAuth(usersKv, BASE_URL, {
43+
method: "POST",
44+
body: createFormData({ c: blob1 }),
45+
headers: { "Authorization": encodeBasicAuth("user2", usersKv["user2"]) },
46+
})
47+
expect(uploadResp1.status).toStrictEqual(200)
48+
49+
// upload with wrong auth
50+
const uploadResp2 = await workerFetchWithAuth(usersKv, BASE_URL, {
51+
method: "POST",
52+
body: createFormData({ c: blob1 }),
53+
headers: { "Authorization": encodeBasicAuth("user1", "wrong-password") },
54+
})
55+
expect(uploadResp2.status).toStrictEqual(401)
56+
57+
// revisit without auth
58+
const uploadJson = JSON.parse(await uploadResp1.text())
59+
const url = uploadJson["url"]
60+
const revisitResp = await workerFetchWithAuth(usersKv, url)
61+
expect(revisitResp.status).toStrictEqual(200)
62+
expect(areBlobsEqual(await revisitResp.blob(), blob1)).toBeTruthy()
63+
64+
// update with no auth
65+
const blob2 = randomBlob(1024)
66+
const admin = uploadJson["admin"]
67+
const updateResp = await workerFetchWithAuth(usersKv, admin, {
68+
method: "PUT",
69+
body: createFormData({ c: blob2 }),
70+
})
71+
expect(updateResp.status).toStrictEqual(200)
72+
const revisitUpdatedResp = await workerFetchWithAuth(usersKv, url)
73+
expect(revisitUpdatedResp.status).toStrictEqual(200)
74+
expect(areBlobsEqual(await revisitUpdatedResp.blob(), blob2)).toBeTruthy()
75+
76+
// delete with no auth
77+
const deleteResp = await workerFetchWithAuth(usersKv, admin, {
78+
method: "DELETE",
79+
})
80+
expect(deleteResp.status).toStrictEqual(200)
81+
expect((await workerFetchWithAuth(usersKv, url)).status).toStrictEqual(404)
82+
})

test/formdata.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ test("basic formdata", async () => {
3232
// compare "s"
3333
const parsedSecret = new TextDecoder().decode(parts.get("s").content)
3434
expect(parsedSecret).toStrictEqual(secret)
35-
})
35+
})

test/integration.spec.js

Lines changed: 17 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,13 @@
1-
import { SELF, env, ctx } from "cloudflare:test"
1+
import { env } from "cloudflare:test"
22
import { test, expect } from "vitest"
33

44
import { params, genRandStr } from "../src/common.js"
5-
6-
import * as crypto from "crypto"
7-
8-
// for auto reload
9-
import worker from "../src/index.js"
10-
11-
const BASE_URL = env["BASE_URL"]
12-
const RAND_NAME_REGEX = /^[ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678]+$/
13-
14-
async function workerFetch(req, options) {
15-
// we are not using SELF.fetch since it sometimes do not print worker log to console
16-
// return await SELF.fetch(req, options)
17-
return await worker.fetch(new Request(req, options), env, ctx)
18-
}
19-
20-
async function upload(kv) {
21-
const uploadResponse = await workerFetch(new Request(BASE_URL, {
22-
method: "POST",
23-
body: createFormData(kv),
24-
}))
25-
expect(uploadResponse.status).toStrictEqual(200)
26-
expect(uploadResponse.headers.get("Content-Type")).toStrictEqual("application/json;charset=UTF-8")
27-
return JSON.parse(await uploadResponse.text())
28-
}
29-
30-
function createFormData(kv) {
31-
const fd = new FormData()
32-
Object.entries(kv).forEach(([k, v]) => {
33-
if ((v === Object(v)) && "filename" in v && "value" in v) {
34-
fd.set(k, new File([v.value], v.filename))
35-
} else {
36-
fd.set(k, v)
37-
}
38-
})
39-
return fd
40-
}
41-
42-
function randomBlob(len) {
43-
const buf = Buffer.alloc(len)
44-
return new Blob([crypto.randomFillSync(buf, 0, len)])
45-
}
46-
47-
async function areBlobsEqual(blob1, blob2) {
48-
return Buffer.from(await blob1.arrayBuffer()).compare(
49-
Buffer.from(await blob2.arrayBuffer()),
50-
) === 0
51-
}
5+
import {
6+
randomBlob, areBlobsEqual, createFormData, workerFetch, upload,
7+
BASE_URL, RAND_NAME_REGEX, staticPages,
8+
} from "./testUtils.js"
529

5310
test("static page", async () => {
54-
const staticPages = ["", "index.html", "index", "tos", "tos.html", "api", "api.html"]
5511
for (const page of staticPages) {
5612
expect((await workerFetch(`${BASE_URL}/${page}`)).status).toStrictEqual(200)
5713
}
@@ -133,7 +89,7 @@ test("basic", async () => {
13389

13490
// check delete
13591
const deleteResponse = await workerFetch(new Request(admin, { method: "DELETE" }))
136-
expect(putResponse.status).toStrictEqual(200)
92+
expect(deleteResponse.status).toStrictEqual(200)
13793

13894
// check visit modified
13995
const revisitDeletedResponse = await workerFetch(url)
@@ -181,7 +137,7 @@ test("expire", async () => {
181137
await testExpireParse("1M", 18144000)
182138
await testExpireParse("100 m", 6000)
183139

184-
const testFailParse = async (expire, expireSecs) => {
140+
const testFailParse = async (expire) => {
185141
const uploadResponse = await workerFetch(new Request(BASE_URL, {
186142
method: "POST",
187143
body: createFormData({ "c": blob1, "e": expire }),
@@ -375,10 +331,18 @@ test("cache control", async () => {
375331
const uploadResp = await upload(({ "c": randomBlob(1024) }))
376332
const url = uploadResp["url"]
377333
const resp = await workerFetch(url)
378-
expect(resp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_PASTE_AGE}`)
334+
if ("CACHE_PASTE_AGE" in env) {
335+
expect(resp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_PASTE_AGE}`)
336+
} else {
337+
expect(resp.headers.get("Cache-Control")).toBeUndefined()
338+
}
379339

380340
const indexResp = await workerFetch(BASE_URL)
381-
expect(indexResp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_STATIC_PAGE_AGE}`)
341+
if ("CACHE_STATIC_PAGE_AGE" in env) {
342+
expect(indexResp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_STATIC_PAGE_AGE}`)
343+
} else {
344+
expect(indexResp.headers.get("Cache-Control")).toBeUndefined()
345+
}
382346

383347
const staleResp = await workerFetch(url, {
384348
headers: {
@@ -388,6 +352,4 @@ test("cache control", async () => {
388352
expect(staleResp.status).toStrictEqual(304)
389353
})
390354

391-
// TODO: add tests for basic auth
392-
393355
// TODO: add tests for CORS

test/testUtils.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { env, ctx } from "cloudflare:test"
2+
3+
import { expect } from "vitest"
4+
import crypto from "crypto"
5+
import worker from "../src/index.js"
6+
7+
export const BASE_URL = env["BASE_URL"]
8+
export const RAND_NAME_REGEX = /^[ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678]+$/
9+
10+
export const staticPages = ["", "index.html", "index", "tos", "tos.html", "api", "api.html"]
11+
12+
export async function workerFetch(req, options) {
13+
// we are not using SELF.fetch since it sometimes do not print worker log to console
14+
// return await SELF.fetch(req, options)
15+
return await worker.fetch(new Request(req, options), env, ctx)
16+
}
17+
18+
export async function workerFetchWithAuth(usersKv, req, options) {
19+
const newEnv = Object.assign({ BASIC_AUTH: usersKv }, env)
20+
return await worker.fetch(new Request(req, options), newEnv, ctx)
21+
}
22+
23+
export async function upload(kv) {
24+
const uploadResponse = await workerFetch(new Request(BASE_URL, {
25+
method: "POST",
26+
body: createFormData(kv),
27+
}))
28+
expect(uploadResponse.status).toStrictEqual(200)
29+
expect(uploadResponse.headers.get("Content-Type")).toStrictEqual("application/json;charset=UTF-8")
30+
return JSON.parse(await uploadResponse.text())
31+
}
32+
33+
export function createFormData(kv) {
34+
const fd = new FormData()
35+
Object.entries(kv).forEach(([k, v]) => {
36+
if ((v === Object(v)) && "filename" in v && "value" in v) {
37+
fd.set(k, new File([v.value], v.filename))
38+
} else {
39+
fd.set(k, v)
40+
}
41+
})
42+
return fd
43+
}
44+
45+
export function randomBlob(len) {
46+
const buf = Buffer.alloc(len)
47+
return new Blob([crypto.randomFillSync(buf, 0, len)])
48+
}
49+
50+
export async function areBlobsEqual(blob1, blob2) {
51+
return Buffer.from(await blob1.arrayBuffer()).compare(
52+
Buffer.from(await blob2.arrayBuffer()),
53+
) === 0
54+
}
55+

0 commit comments

Comments
 (0)