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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 40 additions & 6 deletions
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)
Lines changed: 4 additions & 0 deletions
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
Lines changed: 16 additions & 0 deletions
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+
})
Lines changed: 95 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
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,
File renamed without changes.

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

Lines changed: 12 additions & 2 deletions
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+
}

0 commit comments

Comments
 (0)