-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(preconditions): add if-none-match precondition
- Loading branch information
1 parent
2e4a850
commit f443b67
Showing
3 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |