Skip to content

Commit

Permalink
feat(preconditions): add if-none-match precondition
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Mar 23, 2023
1 parent 2e4a850 commit f443b67
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 0 deletions.
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// This module is browser compatible.

export { conditionalRequest } from "./middleware.ts";
export { IfNoneMatch } from "./preconditions/if_none_match.ts";
44 changes: 44 additions & 0 deletions preconditions/if_none_match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
ConditionalHeader,
filterKeys,
isNull,
isRepresentationHeader,
isRetrieveMethod,
not,
RepresentationHeader,
Status,
} from "../deps.ts";
import type { Precondition } from "../types.ts";
import { ifNoneMatch, isBannedHeader } from "./utils.ts";

/** `If-None-Match` header field precondition. */
export class IfNoneMatch implements Precondition {
field = ConditionalHeader.IfNoneMatch;

evaluate(request: Request, response: Response): boolean | undefined {
const fieldValue = request.headers.get(ConditionalHeader.IfNoneMatch);
const etag = response.headers.get(RepresentationHeader.ETag);

if (isNull(fieldValue) || isNull(etag)) return;

return ifNoneMatch(fieldValue.trim(), etag.trim());
}

respond(
request: Request,
response: Response,
result: boolean,
): Response | undefined {
if (result) return;

if (isRetrieveMethod(request.method)) {
const headers = filterKeys(response.headers, not(isBannedHeader));

return new Response(null, { status: Status.NotModified, headers });
}

const headers = filterKeys(response.headers, not(isRepresentationHeader));

return new Response(null, { status: Status.PreconditionFailed, headers });
}
}
170 changes: 170 additions & 0 deletions preconditions/if_none_match_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
assert,
assertEquals,
ConditionalHeader,
describe,
equalsResponse,
it,
Method,
RepresentationHeader,
Status,
} from "../_dev_deps.ts";
import { IfNoneMatch } from "./if_none_match.ts";

describe("IfNoneMatch", () => {
it("should be if-none-match", () => {
assertEquals(new IfNoneMatch().field, ConditionalHeader.IfNoneMatch);
});

describe("evaluate", () => {
it("should return undefined if the request or response is invalid", () => {
const table: [Request, Response][] = [
[new Request("test:"), new Response()],
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: "" },
}),
new Response(),
],
[
new Request("test:"),
new Response(null, { headers: { [RepresentationHeader.ETag]: "" } }),
],
];

table.forEach(([request, response]) => {
assertEquals(new IfNoneMatch().evaluate(request, response), undefined);
});
});

it("should return true if the etag does not match", () => {
const table: [Request, Response][] = [
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: `"abc"` },
}),
new Response(null, {
headers: { [RepresentationHeader.ETag]: `""` },
}),
],
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: `"abc", "bcd"` },
}),
new Response(null, {
headers: { [RepresentationHeader.ETag]: `""` },
}),
],
];

table.forEach(([request, response]) => {
assert(new IfNoneMatch().evaluate(request, response));
});
});

it("should return false if the etag does not match", () => {
const table: [Request, Response][] = [
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: `*` },
}),
new Response(null, {
headers: { [RepresentationHeader.ETag]: `""` },
}),
],
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: `"abc", ""` },
}),
new Response(null, {
headers: { [RepresentationHeader.ETag]: `""` },
}),
],
[
new Request("test:", {
headers: { [ConditionalHeader.IfNoneMatch]: `"abc", W/""` },
}),
new Response(null, {
headers: { [RepresentationHeader.ETag]: `W/""` },
}),
],
];

table.forEach(([request, response]) => {
assertEquals(new IfNoneMatch().evaluate(request, response), false);
});
});
});

describe("respond", () => {
it("should return undefined if the result is true", () => {
assertEquals(
new IfNoneMatch().respond(new Request("test:"), new Response(), true),
undefined,
);
});

it("should return 314 response if the request is GET or HEAD", () => {
const table: [Request, Response, Response][] = [
[
new Request("test:"),
new Response(),
new Response(null, { status: Status.NotModified }),
],
[
new Request("test:", { method: Method.Head }),
new Response(),
new Response(null, { status: Status.NotModified }),
],
[
new Request("test:"),
new Response(null, {
headers: { "x-test": "test", "content-type": "" },
}),
new Response(null, {
status: Status.NotModified,
headers: { "x-test": "test" },
}),
],
];

table.map(async ([request, response, expected]) => {
const res = new IfNoneMatch().respond(request, response, false);

assert(res);
assert(
await equalsResponse(res, expected, true),
);
});
});

it("should return 412 response if the request is not GET or HEAD", () => {
const table: [Request, Response, Response][] = [
[
new Request("test:", { method: Method.Patch }),
new Response(),
new Response(null, { status: Status.PreconditionFailed }),
],
[
new Request("test:", { method: Method.Patch }),
new Response(null, {
headers: { [RepresentationHeader.ETag]: "", "x-test": "test" },
}),
new Response(null, {
status: Status.PreconditionFailed,
headers: { "x-test": "test" },
}),
],
];

table.map(async ([request, response, expected]) => {
const res = new IfNoneMatch().respond(request, response, false);

assert(res);
assert(
await equalsResponse(res, expected, true),
);
});
});
});
});

0 comments on commit f443b67

Please sign in to comment.