fix(httpapi): return request not found errors (#28693)
This commit is contained in:
parent
76d9c2cd76
commit
4ce247eaba
15 changed files with 213 additions and 56 deletions
|
|
@ -100,6 +100,10 @@ export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("Permiss
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Permission.NotFoundError", {
|
||||||
|
requestID: PermissionID,
|
||||||
|
}) {}
|
||||||
|
|
||||||
export type Error = DeniedError | RejectedError | CorrectedError
|
export type Error = DeniedError | RejectedError | CorrectedError
|
||||||
|
|
||||||
export const AskInput = Schema.Struct({
|
export const AskInput = Schema.Struct({
|
||||||
|
|
@ -117,7 +121,7 @@ export type ReplyInput = Schema.Schema.Type<typeof ReplyInput>
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly ask: (input: AskInput) => Effect.Effect<void, Error>
|
readonly ask: (input: AskInput) => Effect.Effect<void, Error>
|
||||||
readonly reply: (input: ReplyInput) => Effect.Effect<void>
|
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
|
||||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +213,7 @@ export const layer = Layer.effect(
|
||||||
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
|
const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) {
|
||||||
const { approved, pending } = yield* InstanceState.get(state)
|
const { approved, pending } = yield* InstanceState.get(state)
|
||||||
const existing = pending.get(input.requestID)
|
const existing = pending.get(input.requestID)
|
||||||
if (!existing) return
|
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||||
|
|
||||||
pending.delete(input.requestID)
|
pending.delete(input.requestID)
|
||||||
yield* bus.publish(Event.Replied, {
|
yield* bus.publish(Event.Replied, {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,10 @@ export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Que
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Question.NotFoundError", {
|
||||||
|
requestID: QuestionID,
|
||||||
|
}) {}
|
||||||
|
|
||||||
interface PendingEntry {
|
interface PendingEntry {
|
||||||
info: Request
|
info: Request
|
||||||
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
|
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
|
||||||
|
|
@ -115,8 +119,8 @@ export interface Interface {
|
||||||
questions: ReadonlyArray<Info>
|
questions: ReadonlyArray<Info>
|
||||||
tool?: Tool
|
tool?: Tool
|
||||||
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
|
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
|
||||||
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void>
|
readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void, NotFoundError>
|
||||||
readonly reject: (requestID: QuestionID) => Effect.Effect<void>
|
readonly reject: (requestID: QuestionID) => Effect.Effect<void, NotFoundError>
|
||||||
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +184,7 @@ export const layer = Layer.effect(
|
||||||
const existing = pending.get(input.requestID)
|
const existing = pending.get(input.requestID)
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
log.warn("reply for unknown request", { requestID: input.requestID })
|
log.warn("reply for unknown request", { requestID: input.requestID })
|
||||||
return
|
return yield* new NotFoundError({ requestID: input.requestID })
|
||||||
}
|
}
|
||||||
pending.delete(input.requestID)
|
pending.delete(input.requestID)
|
||||||
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
||||||
|
|
@ -197,7 +201,7 @@ export const layer = Layer.effect(
|
||||||
const existing = pending.get(requestID)
|
const existing = pending.get(requestID)
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
log.warn("reject for unknown request", { requestID })
|
log.warn("reject for unknown request", { requestID })
|
||||||
return
|
return yield* new NotFoundError({ requestID })
|
||||||
}
|
}
|
||||||
pending.delete(requestID)
|
pending.delete(requestID)
|
||||||
log.info("rejected", { requestID })
|
log.info("rejected", { requestID })
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,24 @@ export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>(
|
||||||
{ httpApiStatus: 409 },
|
{ httpApiStatus: 409 },
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
|
||||||
|
"QuestionNotFoundError",
|
||||||
|
{
|
||||||
|
requestID: Schema.String,
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
{ httpApiStatus: 404 },
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionNotFoundError>()(
|
||||||
|
"PermissionNotFoundError",
|
||||||
|
{
|
||||||
|
requestID: Schema.String,
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
{ httpApiStatus: 404 },
|
||||||
|
) {}
|
||||||
|
|
||||||
export class ApiNotFoundError extends Schema.ErrorClass<ApiNotFoundError>("NotFoundError")(
|
export class ApiNotFoundError extends Schema.ErrorClass<ApiNotFoundError>("NotFoundError")(
|
||||||
{
|
{
|
||||||
name: Schema.Literal("NotFoundError"),
|
name: Schema.Literal("NotFoundError"),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Permission } from "@/permission"
|
||||||
import { PermissionID } from "@/permission/schema"
|
import { PermissionID } from "@/permission/schema"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { PermissionNotFoundError } from "../errors"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
|
|
@ -32,7 +33,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ReplyPayload,
|
payload: ReplyPayload,
|
||||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, PermissionNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "permission.reply",
|
identifier: "permission.reply",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Question } from "@/question"
|
||||||
import { QuestionID } from "@/question/schema"
|
import { QuestionID } from "@/question/schema"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { QuestionNotFoundError } from "../errors"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
|
|
@ -33,7 +34,7 @@ export const QuestionApi = HttpApi.make("question")
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ReplyPayload,
|
payload: ReplyPayload,
|
||||||
success: described(Schema.Boolean, "Question answered successfully"),
|
success: described(Schema.Boolean, "Question answered successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, QuestionNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "question.reply",
|
identifier: "question.reply",
|
||||||
|
|
@ -45,7 +46,7 @@ export const QuestionApi = HttpApi.make("question")
|
||||||
params: { requestID: QuestionID },
|
params: { requestID: QuestionID },
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Question rejected successfully"),
|
success: described(Schema.Boolean, "Question rejected successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, QuestionNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "question.reject",
|
identifier: "question.reject",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
WorkspaceRoutingQuery,
|
WorkspaceRoutingQuery,
|
||||||
WorkspaceRoutingQueryFields,
|
WorkspaceRoutingQueryFields,
|
||||||
} from "../middleware/workspace-routing"
|
} from "../middleware/workspace-routing"
|
||||||
import { ApiNotFoundError, SessionBusyError } from "../errors"
|
import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
import { QueryBoolean } from "./query"
|
import { QueryBoolean } from "./query"
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ export const SessionApi = HttpApi.make("session")
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: PermissionResponsePayload,
|
payload: PermissionResponsePayload,
|
||||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError, PermissionNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "permission.respond",
|
identifier: "permission.respond",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { PermissionID } from "@/permission/schema"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../api"
|
import { InstanceHttpApi } from "../api"
|
||||||
|
import { PermissionNotFoundError } from "../errors"
|
||||||
|
|
||||||
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) =>
|
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -16,11 +17,22 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
|
||||||
params: { requestID: PermissionID }
|
params: { requestID: PermissionID }
|
||||||
payload: Permission.ReplyBody
|
payload: Permission.ReplyBody
|
||||||
}) {
|
}) {
|
||||||
yield* svc.reply({
|
yield* svc
|
||||||
requestID: ctx.params.requestID,
|
.reply({
|
||||||
reply: ctx.payload.reply,
|
requestID: ctx.params.requestID,
|
||||||
message: ctx.payload.message,
|
reply: ctx.payload.reply,
|
||||||
})
|
message: ctx.payload.message,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.catchTag("Permission.NotFoundError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new PermissionNotFoundError({
|
||||||
|
requestID: String(error.requestID),
|
||||||
|
message: `Permission request not found: ${error.requestID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { QuestionID } from "@/question/schema"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../api"
|
import { InstanceHttpApi } from "../api"
|
||||||
|
import { QuestionNotFoundError } from "../errors"
|
||||||
|
|
||||||
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
|
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -16,15 +17,35 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question"
|
||||||
params: { requestID: QuestionID }
|
params: { requestID: QuestionID }
|
||||||
payload: Question.Reply
|
payload: Question.Reply
|
||||||
}) {
|
}) {
|
||||||
yield* svc.reply({
|
yield* svc
|
||||||
requestID: ctx.params.requestID,
|
.reply({
|
||||||
answers: ctx.payload.answers,
|
requestID: ctx.params.requestID,
|
||||||
})
|
answers: ctx.payload.answers,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.catchTag("Question.NotFoundError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new QuestionNotFoundError({
|
||||||
|
requestID: String(error.requestID),
|
||||||
|
message: `Question request not found: ${error.requestID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
|
const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
|
||||||
yield* svc.reject(ctx.params.requestID)
|
yield* svc.reject(ctx.params.requestID).pipe(
|
||||||
|
Effect.catchTag("Question.NotFoundError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new QuestionNotFoundError({
|
||||||
|
requestID: String(error.requestID),
|
||||||
|
message: `Question request not found: ${error.requestID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
SummarizePayload,
|
SummarizePayload,
|
||||||
UpdatePayload,
|
UpdatePayload,
|
||||||
} from "../groups/session"
|
} from "../groups/session"
|
||||||
|
import { PermissionNotFoundError } from "../errors"
|
||||||
import * as SessionError from "./session-errors"
|
import * as SessionError from "./session-errors"
|
||||||
|
|
||||||
const tryParseJson = (text: string) =>
|
const tryParseJson = (text: string) =>
|
||||||
|
|
@ -356,7 +357,16 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
payload: typeof PermissionResponsePayload.Type
|
payload: typeof PermissionResponsePayload.Type
|
||||||
}) {
|
}) {
|
||||||
yield* requireSession(ctx.params.sessionID)
|
yield* requireSession(ctx.params.sessionID)
|
||||||
yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
|
yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response }).pipe(
|
||||||
|
Effect.catchTag("Permission.NotFoundError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new PermissionNotFoundError({
|
||||||
|
requestID: String(error.requestID),
|
||||||
|
message: `Permission request not found: ${error.requestID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1057,10 +1057,14 @@ it.instance(
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"reply - does nothing for unknown requestID",
|
"reply - fails for unknown requestID",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" })
|
const exit = yield* reply({ requestID: PermissionID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
|
||||||
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
|
if (Exit.isFailure(exit)) {
|
||||||
|
expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Permission.NotFoundError", requestID: "per_unknown" })
|
||||||
|
}
|
||||||
expect(yield* list()).toHaveLength(0)
|
expect(yield* list()).toHaveLength(0)
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
|
|
|
||||||
|
|
@ -184,11 +184,17 @@ it.instance(
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"reply - does nothing for unknown requestID",
|
"reply - fails for unknown requestID",
|
||||||
() =>
|
() =>
|
||||||
replyEffect({
|
Effect.gen(function* () {
|
||||||
requestID: QuestionID.make("que_unknown"),
|
const exit = yield* replyEffect({
|
||||||
answers: [["Option 1"]],
|
requestID: QuestionID.make("que_unknown"),
|
||||||
|
answers: [["Option 1"]],
|
||||||
|
}).pipe(Effect.exit)
|
||||||
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
|
if (Exit.isFailure(exit)) {
|
||||||
|
expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Question.NotFoundError", requestID: "que_unknown" })
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
@ -253,9 +259,18 @@ it.instance(
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("reject - does nothing for unknown requestID", () => rejectEffect(QuestionID.make("que_unknown")), {
|
it.instance(
|
||||||
git: true,
|
"reject - fails for unknown requestID",
|
||||||
})
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const exit = yield* rejectEffect(QuestionID.make("que_unknown")).pipe(Effect.exit)
|
||||||
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
|
if (Exit.isFailure(exit)) {
|
||||||
|
expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Question.NotFoundError", requestID: "que_unknown" })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
// multiple questions tests
|
// multiple questions tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -224,9 +224,7 @@ const scenarios: Scenario[] = [
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: { reply: "once" },
|
body: { reply: "once" },
|
||||||
}))
|
}))
|
||||||
.json(200, (body) => {
|
.json(404, object, "status"),
|
||||||
check(body === true, "permission reply should return true even when request is no longer pending")
|
|
||||||
}),
|
|
||||||
http.protected.get("/question", "question.list").json(200, array),
|
http.protected.get("/question", "question.list").json(200, array),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/question/{requestID}/reply", "question.reply.invalid")
|
.post("/question/{requestID}/reply", "question.reply.invalid")
|
||||||
|
|
@ -243,18 +241,14 @@ const scenarios: Scenario[] = [
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: { answers: [["Yes"]] },
|
body: { answers: [["Yes"]] },
|
||||||
}))
|
}))
|
||||||
.json(200, (body) => {
|
.json(404, object, "status"),
|
||||||
check(body === true, "question reply should return true even when request is no longer pending")
|
|
||||||
}),
|
|
||||||
http.protected
|
http.protected
|
||||||
.post("/question/{requestID}/reject", "question.reject")
|
.post("/question/{requestID}/reject", "question.reject")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/question/{requestID}/reject", { requestID: "que_httpapi_reject" }),
|
path: route("/question/{requestID}/reject", { requestID: "que_httpapi_reject" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
}))
|
}))
|
||||||
.json(200, (body) => {
|
.json(404, object, "status"),
|
||||||
check(body === true, "question reject should return true even when request is no longer pending")
|
|
||||||
}),
|
|
||||||
http.protected
|
http.protected
|
||||||
.get("/file", "file.list")
|
.get("/file", "file.list")
|
||||||
.seeded((ctx) => ctx.file("hello.txt", "hello\n"))
|
.seeded((ctx) => ctx.file("hello.txt", "hello\n"))
|
||||||
|
|
@ -1249,9 +1243,7 @@ const scenarios: Scenario[] = [
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: { response: "once" },
|
body: { response: "once" },
|
||||||
}))
|
}))
|
||||||
.json(200, (body) => {
|
.json(404, object, "status"),
|
||||||
check(body === true, "deprecated permission response should return true")
|
|
||||||
}),
|
|
||||||
http.protected
|
http.protected
|
||||||
.post("/session/{sessionID}/share", "session.share")
|
.post("/session/{sessionID}/share", "session.share")
|
||||||
.mutating()
|
.mutating()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { WorkspaceID } from "../../src/control-plane/schema"
|
||||||
import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
|
import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
|
||||||
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
|
import { PermissionID } from "../../src/permission/schema"
|
||||||
|
import { QuestionID } from "../../src/question/schema"
|
||||||
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
|
||||||
import { HEADER as FenceHeader } from "../../src/server/shared/fence"
|
import { HEADER as FenceHeader } from "../../src/server/shared/fence"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
|
|
@ -151,6 +153,58 @@ describe("instance HttpApi", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("returns typed not found bodies for missing permission and question requests", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
const request = (path: string, init?: RequestInit) =>
|
||||||
|
Effect.promise(() =>
|
||||||
|
HttpApiApp.webHandler().handler(
|
||||||
|
new Request(`http://localhost${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: { "x-opencode-directory": dir, "content-type": "application/json", ...init?.headers },
|
||||||
|
}),
|
||||||
|
handlerContext,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const permissionID = PermissionID.ascending()
|
||||||
|
const questionReplyID = QuestionID.ascending()
|
||||||
|
const questionRejectID = QuestionID.ascending()
|
||||||
|
const [permission, questionReply, questionReject] = yield* Effect.all(
|
||||||
|
[
|
||||||
|
request(`/permission/${permissionID}/reply`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ reply: "once" }),
|
||||||
|
}),
|
||||||
|
request(`/question/${questionReplyID}/reply`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ answers: [["Yes"]] }),
|
||||||
|
}),
|
||||||
|
request(`/question/${questionRejectID}/reject`, { method: "POST" }),
|
||||||
|
],
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(permission.status).toBe(404)
|
||||||
|
expect(yield* Effect.promise(() => permission.json())).toEqual({
|
||||||
|
_tag: "PermissionNotFoundError",
|
||||||
|
requestID: permissionID,
|
||||||
|
message: `Permission request not found: ${permissionID}`,
|
||||||
|
})
|
||||||
|
expect(questionReply.status).toBe(404)
|
||||||
|
expect(yield* Effect.promise(() => questionReply.json())).toEqual({
|
||||||
|
_tag: "QuestionNotFoundError",
|
||||||
|
requestID: questionReplyID,
|
||||||
|
message: `Question request not found: ${questionReplyID}`,
|
||||||
|
})
|
||||||
|
expect(questionReject.status).toBe(404)
|
||||||
|
expect(yield* Effect.promise(() => questionReject.json())).toEqual({
|
||||||
|
_tag: "QuestionNotFoundError",
|
||||||
|
requestID: questionRejectID,
|
||||||
|
message: `Question request not found: ${questionRejectID}`,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("serves path and VCS read endpoints", () =>
|
it.live("serves path and VCS read endpoints", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped({ git: true })
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
|
|
||||||
|
|
@ -157,4 +157,20 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("documents permission and question not-found errors", () => {
|
||||||
|
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||||
|
|
||||||
|
expect(componentName(responseRef(spec.paths["/permission/{requestID}/reply"]?.post?.responses?.["404"]) ?? "")).toBe(
|
||||||
|
"PermissionNotFoundError",
|
||||||
|
)
|
||||||
|
for (const route of [
|
||||||
|
["post", "/question/{requestID}/reply"],
|
||||||
|
["post", "/question/{requestID}/reject"],
|
||||||
|
] as const) {
|
||||||
|
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
|
||||||
|
"QuestionNotFoundError",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -821,19 +821,24 @@ describe("session HttpApi", () => {
|
||||||
}),
|
}),
|
||||||
).toMatchObject({ id: session.id })
|
).toMatchObject({ id: session.id })
|
||||||
|
|
||||||
expect(
|
const permissionID = String(PermissionID.ascending())
|
||||||
yield* requestJson<boolean>(
|
const permission = yield* request(
|
||||||
pathFor(SessionPaths.permissions, {
|
pathFor(SessionPaths.permissions, {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
permissionID: String(PermissionID.ascending()),
|
permissionID,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ response: "once" }),
|
body: JSON.stringify({ response: "once" }),
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
).toBe(true)
|
expect(permission.status).toBe(404)
|
||||||
|
expect(yield* responseJson(permission)).toEqual({
|
||||||
|
_tag: "PermissionNotFoundError",
|
||||||
|
requestID: permissionID,
|
||||||
|
message: `Permission request not found: ${permissionID}`,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
{ git: true, config: { formatter: false, lsp: false } },
|
{ git: true, config: { formatter: false, lsp: false } },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue