Skip to content

Commit b108d18

Browse files
Merge pull request #28 from upstash/DX-1393-auth-in-failure-function
Custom auth in failure function
2 parents e37c9b0 + 16b47d3 commit b108d18

File tree

20 files changed

+313
-58
lines changed

20 files changed

+313
-58
lines changed

examples/ci/app/ci/ci.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe("workflow integration tests", () => {
1010
await initiateTest(testConfig.route, testConfig.waitForSeconds)
1111
},
1212
{
13-
timeout: (testConfig.waitForSeconds + 10) * 1000
13+
timeout: (testConfig.waitForSeconds + 15) * 1000
1414
}
1515
)
1616
});

examples/ci/app/ci/constants.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@ export const TEST_ROUTES: Pick<TestConfig, "route" | "waitForSeconds">[] = [
3131
},
3232
{
3333
// checks auth
34-
route: "auth",
34+
route: "auth/success",
3535
waitForSeconds: 1
3636
},
3737
{
3838
// checks auth failing
39-
route: "auth-fail",
39+
route: "auth/fail",
4040
waitForSeconds: 0
4141
},
42+
{
43+
// checks custom auth
44+
route: "auth/custom/workflow",
45+
waitForSeconds: 5
46+
},
4247
{
4348
// checks context.call (sucess and fail case)
4449
route: "call/workflow",

examples/ci/app/ci/upstash/redis.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,21 @@ describe("redis", () => {
8080
).not.toThrow()
8181
})
8282
})
83+
84+
test("should fail if marked as failed", async () => {
85+
86+
const route = "fail-route"
87+
const randomId = `random-id-${nanoid()}`
88+
const result = `random-result-${nanoid()}`
89+
90+
// increment, save and check
91+
await redis.increment(route, randomId)
92+
await redis.saveResultsWithoutContext(route, randomId, result)
93+
await redis.checkRedisForResults(route, randomId, 1, result)
94+
95+
// mark as failed and check
96+
await redis.failWithoutContext(route, randomId)
97+
expect(redis.checkRedisForResults(route, randomId, 1, result)).rejects.toThrow(redis.FAILED_TEXT)
98+
99+
})
83100
})

examples/ci/app/ci/upstash/redis.ts

+40-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const redis = Redis.fromEnv();
88
const EXPIRE_IN_SECS = 60
99

1010
const getRedisKey = (
11-
kind: "increment" | "result",
11+
kind: "increment" | "result" | "fail",
1212
route: string,
1313
randomTestId: string
1414
): string => {
@@ -46,11 +46,7 @@ export const saveResultsWithoutContext = async (
4646

4747
// save result
4848
const key = getRedisKey("result", route, randomTestId)
49-
50-
const pipe = redis.pipeline()
51-
pipe.set<RedisResult>(key, { callCount, result, randomTestId })
52-
pipe.expire(key, EXPIRE_IN_SECS)
53-
await pipe.exec()
49+
await redis.set<RedisResult>(key, { callCount, result, randomTestId }, { ex: EXPIRE_IN_SECS })
5450
}
5551

5652
/**
@@ -80,6 +76,38 @@ export const saveResult = async (
8076
)
8177
}
8278

79+
export const failWithoutContext = async (
80+
route: string,
81+
randomTestId: string
82+
) => {
83+
const key = getRedisKey("fail", route, randomTestId)
84+
await redis.set<boolean>(key, true, { ex: EXPIRE_IN_SECS })
85+
}
86+
87+
/**
88+
* marks the workflow as failed
89+
*
90+
* @param context
91+
* @returns
92+
*/
93+
export const fail = async (
94+
context: WorkflowContext<unknown>,
95+
) => {
96+
const randomTestId = context.headers.get(CI_RANDOM_ID_HEADER)
97+
const route = context.headers.get(CI_ROUTE_HEADER)
98+
99+
if (randomTestId === null) {
100+
throw new Error("randomTestId can't be null.")
101+
}
102+
if (route === null) {
103+
throw new Error("route can't be null.")
104+
}
105+
106+
await failWithoutContext(route, randomTestId)
107+
}
108+
109+
export const FAILED_TEXT = "Test has failed because it was marked as failed with `fail` method."
110+
83111
export const checkRedisForResults = async (
84112
route: string,
85113
randomTestId: string,
@@ -101,6 +129,12 @@ export const checkRedisForResults = async (
101129
throw new Error(`result not found for route ${route} with randomTestId ${randomTestId}`)
102130
}
103131

132+
const failKey = getRedisKey("fail", route, randomTestId)
133+
const failed = await redis.get<boolean>(failKey)
134+
if (failed) {
135+
throw new Error(FAILED_TEXT)
136+
}
137+
104138
const { callCount, randomTestId: resultRandomTestId, result } = testResult
105139

106140
expect(resultRandomTestId, randomTestId)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
this directory has three tests
2+
- success: checking auth correctly
3+
- fail: auth failing
4+
- custom: define an workflow endpoint secured with custom auth (instead of receiver) and try to call it as if failure callback
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { WorkflowContext } from "@upstash/workflow";
2+
import { serve } from "@upstash/workflow/nextjs";
3+
import { fail } from "app/ci/upstash/redis";
4+
import { nanoid } from "app/ci/utils";
5+
6+
7+
export const { POST } = serve(async (context) => {
8+
if (context.headers.get("authorization") !== nanoid()) {
9+
return;
10+
};
11+
}, {
12+
receiver: undefined,
13+
async failureFunction({ context }) {
14+
await fail(context as WorkflowContext)
15+
},
16+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { serve } from "@upstash/workflow/nextjs";
2+
import { BASE_URL, CI_RANDOM_ID_HEADER, CI_ROUTE_HEADER, TEST_ROUTE_PREFIX } from "app/ci/constants";
3+
import { testServe, expect } from "app/ci/utils";
4+
import { FailureFunctionPayload, WorkflowContext } from "@upstash/workflow";
5+
import { saveResult } from "app/ci/upstash/redis";
6+
7+
const header = `test-header-foo`
8+
const headerValue = `header-bar`
9+
const authentication = `Bearer test-auth-super-secret`
10+
const payload = "my-payload"
11+
12+
const thirdPartyEndpoint = `${TEST_ROUTE_PREFIX}/auth/custom/target`
13+
14+
const makeCall = async (
15+
context: WorkflowContext,
16+
stepName: string,
17+
method: "GET" | "POST",
18+
expectedStatus: number,
19+
expectedBody: unknown
20+
) => {
21+
const randomId = context.headers.get(CI_RANDOM_ID_HEADER)
22+
const route = context.headers.get(CI_ROUTE_HEADER)
23+
24+
if (!randomId || !route) {
25+
throw new Error("randomId or route not found")
26+
}
27+
28+
const { status, body } = await context.call<FailureFunctionPayload>(stepName, {
29+
url: thirdPartyEndpoint,
30+
body:
31+
{
32+
status: 200,
33+
header: "",
34+
body: "",
35+
url: "",
36+
sourceHeader: {
37+
[CI_ROUTE_HEADER]: [route],
38+
[CI_RANDOM_ID_HEADER]: [randomId]
39+
},
40+
sourceBody: "",
41+
workflowRunId: "",
42+
sourceMessageId: "",
43+
},
44+
method,
45+
headers: {
46+
[ CI_RANDOM_ID_HEADER ]: randomId,
47+
[ CI_ROUTE_HEADER ]: route,
48+
"Upstash-Workflow-Is-Failure": "true"
49+
}
50+
})
51+
52+
expect(status, expectedStatus)
53+
54+
expect(typeof body, typeof expectedBody)
55+
expect(JSON.stringify(body), JSON.stringify(expectedBody))
56+
}
57+
58+
export const { POST, GET } = testServe(
59+
serve<string>(
60+
async (context) => {
61+
62+
expect(context.headers.get(header)!, headerValue)
63+
64+
await makeCall(
65+
context,
66+
"regular call should fail",
67+
"POST",
68+
500,
69+
{
70+
error: "WorkflowError",
71+
message: "Not authorized to run the failure function."
72+
}
73+
)
74+
75+
const input = context.requestPayload;
76+
expect(input, payload);
77+
78+
await saveResult(
79+
context,
80+
"not authorized for failure"
81+
)
82+
}, {
83+
baseUrl: BASE_URL,
84+
retries: 0,
85+
}
86+
), {
87+
expectedCallCount: 4,
88+
expectedResult: "not authorized for failure",
89+
payload,
90+
headers: {
91+
[ header ]: headerValue,
92+
"authentication": authentication
93+
}
94+
}
95+
)

examples/ci/app/test-routes/auth-fail/route.ts renamed to examples/ci/app/test-routes/auth/fail/route.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { serve } from "@upstash/workflow/nextjs";
22
import { BASE_URL } from "app/ci/constants";
33
import { testServe, expect } from "app/ci/utils";
4-
import { saveResult } from "app/ci/upstash/redis"
4+
import { fail, saveResult } from "app/ci/upstash/redis"
55

66
const header = `test-header-foo`
77
const headerValue = `header-bar`
@@ -28,10 +28,10 @@ export const { POST, GET } = testServe(
2828
return;
2929
}
3030

31-
throw new Error("shouldn't come here.")
31+
await fail(context)
3232
}, {
3333
baseUrl: BASE_URL,
34-
retries: 0
34+
retries: 1 // check with retries 1 to see if endpoint will retry
3535
}
3636
), {
3737
expectedCallCount: 1,

examples/ci/app/test-routes/call/third-party/route.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,15 @@ export const PATCH = async () => {
3030
headers: {
3131
[ FAILING_HEADER ]: FAILING_HEADER_VALUE
3232
}
33-
})
34-
}
33+
}
34+
)
35+
}
36+
37+
export const PUT = async () => {
38+
return new Response(
39+
undefined,
40+
{
41+
status: 300,
42+
}
43+
)
44+
}

examples/ci/app/test-routes/call/workflow-with-failureFunction/route.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { serve } from "@upstash/workflow/nextjs";
22
import { BASE_URL, TEST_ROUTE_PREFIX } from "app/ci/constants";
33
import { testServe, expect } from "app/ci/utils";
4-
import { saveResult } from "app/ci/upstash/redis"
4+
import { fail, saveResult } from "app/ci/upstash/redis"
55
import { FAILING_HEADER, FAILING_HEADER_VALUE } from "../constants";
6+
import { WorkflowContext } from "@upstash/workflow";
67

78
const testHeader = `test-header-foo`
89
const headerValue = `header-foo`
@@ -31,8 +32,8 @@ export const { POST, GET } = testServe(
3132
}, {
3233
baseUrl: BASE_URL,
3334
retries: 0,
34-
failureFunction() {
35-
console.log("SHOULDNT RUN");
35+
async failureFunction({ context }) {
36+
await fail(context as WorkflowContext)
3637
},
3738
}
3839
), {

examples/ci/app/test-routes/call/workflow/route.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ export const { POST, GET } = testServe(
4545

4646
await context.sleep("sleep 1", 2);
4747

48-
const { body: getResult, header: getHeaders, status: getStatus } = await context.call("get call", {
48+
const { body: getResult, header: getHeaders, status: getStatus } = await context.call<string>("get call", {
4949
url: thirdPartyEndpoint,
5050
headers: getHeader,
5151
});
5252

5353
expect(getStatus, 200)
5454
expect(getHeaders[GET_HEADER][0], GET_HEADER_VALUE)
55-
expect(getResult as string, "called GET 'third-party-result' 'get-header-value-x'");
55+
expect(getResult, "called GET 'third-party-result' 'get-header-value-x'");
5656

57-
const { body: patchResult, status, header } = await context.call("get call", {
57+
const { body: patchResult, status, header } = await context.call("patch call", {
5858
url: thirdPartyEndpoint,
5959
headers: getHeader,
6060
method: "PATCH",
@@ -65,16 +65,26 @@ export const { POST, GET } = testServe(
6565
expect(patchResult as string, "failing request");
6666
expect(header[FAILING_HEADER][0], FAILING_HEADER_VALUE)
6767

68+
// put will return with an empty body. should return "" as body in that case.
69+
const { body: putBody, status: putStatus } = await context.call<string>("put call", {
70+
url: thirdPartyEndpoint,
71+
method: "PUT",
72+
retries: 0
73+
})
74+
75+
expect(putStatus, 300)
76+
expect(putBody, "");
77+
6878
await saveResult(
6979
context,
70-
getResult as string
80+
getResult
7181
)
7282
}, {
7383
baseUrl: BASE_URL,
7484
retries: 0
7585
}
7686
), {
77-
expectedCallCount: 10,
87+
expectedCallCount: 12,
7888
expectedResult: "called GET 'third-party-result' 'get-header-value-x'",
7989
payload,
8090
headers: {

examples/cloudflare-workers-hono/ci.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const testEndpoint = ({
9696
expect(result?.secret).toBe(secret)
9797
expect(result?.result).toBe(expectedResult)
9898
}, {
99-
timeout: 8000
99+
timeout: 15000
100100
})
101101
}
102102

examples/nextjs-pages/ci.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const testEndpoint = ({
107107
expect(result?.secret).toBe(secret)
108108
expect(result?.result).toBe(expectedResult)
109109
}, {
110-
timeout: 9000
110+
timeout: 15000
111111
})
112112
}
113113

src/serve/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const serve = <
9797
requestPayload,
9898
qstashClient,
9999
initialPayloadParser,
100+
routeFunction,
100101
failureFunction
101102
);
102103
if (failureCheck.isErr()) {

0 commit comments

Comments
 (0)