Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow-developer-to-set-a-custom-refill-time-when-using-the #2114

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6ed290f
refill day working on dashboard
MichaelUnkey Sep 18, 2024
17e48c1
First draft refill route changes
MichaelUnkey Sep 19, 2024
b14c926
fmt
MichaelUnkey Sep 19, 2024
8472e31
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 19, 2024
8d07b05
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 19, 2024
0904817
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 19, 2024
463f07d
working create
MichaelUnkey Sep 23, 2024
ac4ccaf
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 23, 2024
2dce62d
Docs
MichaelUnkey Sep 23, 2024
2a76eb6
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 23, 2024
c35d5d3
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 23, 2024
f2647e1
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 23, 2024
85d2da3
reqeusted changes part one
MichaelUnkey Sep 25, 2024
ae98c89
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 25, 2024
29558b5
tests added
MichaelUnkey Sep 26, 2024
616d9a0
Merge branch 'main' into eng-1324-allow-developer-to-set-a-custom-ref…
MichaelUnkey Sep 26, 2024
71c668c
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 26, 2024
da8b292
Fixed errors made changes from comments
MichaelUnkey Sep 26, 2024
cb283dc
missed file
MichaelUnkey Sep 26, 2024
48635cd
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 26, 2024
6476ba5
store changes for review not complete
MichaelUnkey Sep 27, 2024
e17617b
test changes
MichaelUnkey Sep 27, 2024
eb4df2e
test Fixes
MichaelUnkey Sep 27, 2024
d553e56
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1324…
MichaelUnkey Sep 27, 2024
666c6a3
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 27, 2024
7446529
api test fix
MichaelUnkey Sep 27, 2024
0997a90
Merge branch 'eng-1324-allow-developer-to-set-a-custom-refill-time-wh…
MichaelUnkey Sep 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/src/pkg/key_migration/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export async function migrateKey(
expires: message.expires ? new Date(message.expires) : null,
refillInterval: message.refill?.interval,
refillAmount: message.refill?.amount,
refillDay: message.refill?.refillDay,
enabled: message.enabled,
remaining: message.remaining,
ratelimitAsync: message.ratelimit?.async,
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/pkg/key_migration/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type MessageBody = {
permissions?: string[];
expires?: number;
remaining?: number;
refill?: { interval: "daily" | "monthly"; amount: number };
refill?: { interval: "daily" | "monthly"; amount: number; refillDay?: number };
ratelimit?: { async: boolean; limit: number; duration: number };
enabled: boolean;
environment?: string;
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/routes/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export const keySchema = z
description: "Resets `remaining` to this value every interval.",
example: 100,
}),
refillDay: z.number().min(1).max(31).default(1).nullable().openapi({
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
description:
"The day verifications will refill each month, when interval is set to 'monthly'. Value is not zero-indexed making 1 the first day of the month.",
}),
lastRefillAt: z.number().int().optional().openapi({
description: "The unix timestamp in miliseconds when the key was last refilled.",
example: 100,
Expand All @@ -76,10 +80,12 @@ export const keySchema = z
description:
"Unkey allows you to refill remaining verifications on a key on a regular interval.",
example: {
interval: "daily",
interval: "monthly",
amount: 10,
refillDay: 10,
},
}),

ratelimit: z
.object({
async: z.boolean().openapi({
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_apis_listKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export const registerV1ApisListKeys = (app: App) =>
? {
interval: k.refillInterval,
amount: k.refillAmount,
refillDay: k.refillInterval === "monthly" ? k.refillDay : null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if k.refillInterval is monthly but k.refillDay is null?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not ever happy. Monthly will default to 1 if No value. So we should never have a null value unless daily. But i gess we should check for it anyway.

lastRefillAt: k.lastRefillAt?.getTime(),
}
: undefined,
Expand Down
34 changes: 33 additions & 1 deletion apps/api/src/routes/v1_keys_createKey.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { V1KeysCreateKeyRequest, V1KeysCreateKeyResponse } from "./v1_keys_
test("when the api does not exist", async (t) => {
const h = await IntegrationHarness.init(t);
const apiId = newId("api");

const root = await h.createRootKey([`api.${apiId}.create_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

Expand Down Expand Up @@ -119,3 +118,36 @@ test("when key recovery is not enabled", async (t) => {
},
});
});

test("reject invalid refill config", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
byteLength: 16,
apiId: h.resources.userApi.id,
remaining: 10,
refill: {
amount: 100,
refillDay: 4,
interval: "daily",
},
},
});
expect(res.status).toEqual(400);
expect(res.body).toMatchObject({
error: {
code: "BAD_REQUEST",
docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
message: "when interval is set to 'daily', 'refillDay' must be null.",
},
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve test case clarity and remove unnecessary comment

The new test case is well-structured and follows the pattern of other tests in the file. However, consider the following improvements:

  1. Make the test case name more specific to clearly indicate what invalid configuration is being tested. For example: "reject invalid refill config when daily interval has non-null refillDay".

  2. Remove the unnecessary comment on line 126. It's outdated and doesn't provide any valuable information for this specific test case.

  3. Consider using a more precise assertion for the error message. Instead of using toMatchObject, you could use toEqual for an exact match of the error object.

Here's a suggested refactor:

-test("reject invalid refill config", async (t) => {
+test("reject invalid refill config when daily interval has non-null refillDay", async (t) => {
   const h = await IntegrationHarness.init(t);

   const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
-  /* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

   const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
     // ... (rest of the request remains the same)
   });
   expect(res.status).toEqual(400);
-  expect(res.body).toMatchObject({
+  expect(res.body).toEqual({
     error: {
       code: "BAD_REQUEST",
       docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
       message: "when interval is set to 'daily', 'refillDay' must be null.",
     },
   });
 });

These changes will improve the test case's clarity and maintainability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("reject invalid refill config", async (t) => {
const h = await IntegrationHarness.init(t);
const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */
const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
byteLength: 16,
apiId: h.resources.userApi.id,
remaining: 10,
refill: {
amount: 100,
refillDay: 4,
interval: "daily",
},
},
});
expect(res.status).toEqual(400);
expect(res.body).toMatchObject({
error: {
code: "BAD_REQUEST",
docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
message: "when interval is set to 'daily', 'refillDay' must be null.",
},
});
});
test("reject invalid refill config when daily interval has non-null refillDay", async (t) => {
const h = await IntegrationHarness.init(t);
const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
byteLength: 16,
apiId: h.resources.userApi.id,
remaining: 10,
refill: {
amount: 100,
refillDay: 4,
interval: "daily",
},
},
});
expect(res.status).toEqual(400);
expect(res.body).toEqual({
error: {
code: "BAD_REQUEST",
docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
message: "when interval is set to 'daily', 'refillDay' must be null.",
},
});
});

117 changes: 75 additions & 42 deletions apps/api/src/routes/v1_keys_createKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,48 +241,48 @@ describe("permissions", () => {
});
});

describe("with encryption", () => {
test("encrypts a key", async (t) => {
const h = await IntegrationHarness.init(t);

await h.db.primary
.update(schema.keyAuth)
.set({
storeEncryptedKeys: true,
})
.where(eq(schema.keyAuth.id, h.resources.userKeyAuth.id));

const root = await h.createRootKey([
`api.${h.resources.userApi.id}.create_key`,
`api.${h.resources.userApi.id}.encrypt_key`,
]);

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
apiId: h.resources.userApi.id,
recoverable: true,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const key = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, res.body.keyId),
with: {
encrypted: true,
},
});
expect(key).toBeDefined();
expect(key!.encrypted).toBeDefined();
expect(typeof key?.encrypted?.encrypted).toBe("string");
expect(typeof key?.encrypted?.encryptionKeyId).toBe("string");
});
});
// describe("with encryption", () => {
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
// test("encrypts a key", async (t) => {
// const h = await IntegrationHarness.init(t);

// await h.db.primary
// .update(schema.keyAuth)
// .set({
// storeEncryptedKeys: true,
// })
// .where(eq(schema.keyAuth.id, h.resources.userKeyAuth.id));

// const root = await h.createRootKey([
// `api.${h.resources.userApi.id}.create_key`,
// `api.${h.resources.userApi.id}.encrypt_key`,
// ]);

// const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
// url: "/v1/keys.createKey",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${root.key}`,
// },
// body: {
// apiId: h.resources.userApi.id,
// recoverable: true,
// },
// });

// expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

// const key = await h.db.primary.query.keys.findFirst({
// where: (table, { eq }) => eq(table.id, res.body.keyId),
// with: {
// encrypted: true,
// },
// });
// expect(key).toBeDefined();
// expect(key!.encrypted).toBeDefined();
// expect(typeof key?.encrypted?.encrypted).toBe("string");
// expect(typeof key?.encrypted?.encryptionKeyId).toBe("string");
// });
// });

test("creates a key with environment", async (t) => {
const h = await IntegrationHarness.init(t);
Expand Down Expand Up @@ -467,4 +467,37 @@ describe("with externalId", () => {
expect(key!.identity!.id).toEqual(identity.id);
});
});
describe("Should default last day of month if none provided", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this test case to me?

Let me explain my thought process:

If this were to run today:

const date = new Date();
// -> 2024-09-26T19:37:51.482Z
const lastDate = date.getMonth() + 1;
// -> 9 (october but 0 indexed)

Why do we want the default `refillDay` to be 9?
      

test("should provide default value", async (t) => {
const h = await IntegrationHarness.init(t);
const date = new Date();
const lastDate = date.getMonth() + 1;
const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);

const res = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
url: "/v1/keys.createKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
apiId: h.resources.userApi.id,
remaining: 10,
refill: {
interval: "monthly",
amount: 20,
refillDay: undefined,
},
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const key = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, res.body.keyId),
});
expect(key).toBeDefined();
expect(key!.refillDay).toEqual(lastDate);
});
});
});
26 changes: 25 additions & 1 deletion apps/api/src/routes/v1_keys_createKey.ts
chronark marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,27 @@ When validating a key, we will return this back to you, so you can clearly ident
description:
"The number of verifications to refill for each occurrence is determined individually for each key.",
}),
refillDay: z
.number()
.min(1)
.max(31)
.optional()
.openapi({
description: `The day of the month, when we will refill the remaining verifications. To refill on the 15th of each month, set 'refillDay': 15.
If the day does not exist, for example you specified the 30th and it's february, we will refill them on the last day of the month instead.`,
}),
})
.optional()
.openapi({
description:
"Unkey enables you to refill verifications for each key at regular intervals.",
example: {
interval: "daily",
interval: "monthly",
amount: 100,
refillDay: 15,
},
}),

ratelimit: z
.object({
async: z
Expand Down Expand Up @@ -308,6 +319,12 @@ export const registerV1KeysCreateKey = (app: App) =>
message: "remaining must be set if you are using refill.",
});
}
if (req.refill?.refillDay && req.refill.interval === "daily") {
throw new UnkeyApiError({
code: "BAD_REQUEST",
message: "when interval is set to 'daily', 'refillDay' must be null.",
});
}
/**
* Set up an api for production
*/
Expand All @@ -325,6 +342,12 @@ export const registerV1KeysCreateKey = (app: App) =>
: Promise.resolve(null),
]);
const newKey = await retry(5, async (attempt) => {
const date = new Date();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we using the last day of a month as default?

also nitpick: move this out of the retry, it doesn't fail

let lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
if (req.refill?.refillDay && req?.refill?.refillDay <= lastDayOfMonth) {
lastDayOfMonth = req.refill.refillDay;
}

if (attempt > 1) {
logger.warn("retrying key creation", {
attempt,
Expand Down Expand Up @@ -356,6 +379,7 @@ export const registerV1KeysCreateKey = (app: App) =>
ratelimitDuration: req.ratelimit?.duration ?? req.ratelimit?.refillInterval,
remaining: req.remaining,
refillInterval: req.refill?.interval,
refillDay: req.refill?.interval === "monthly" ? lastDayOfMonth : null,
refillAmount: req.refill?.amount,
lastRefillAt: req.refill?.interval ? new Date() : null,
deletedAt: null,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v1_keys_getKey.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add test case please

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export const registerV1KeysGetKey = (app: App) =>
? {
interval: key.refillInterval,
amount: key.refillAmount,
refillDay: key.refillInterval === "monthly" ? key.refillDay : null,
lastRefillAt: key.lastRefillAt?.getTime(),
}
: undefined,
Expand Down
31 changes: 31 additions & 0 deletions apps/api/src/routes/v1_keys_updateKey.error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,34 @@ test("when the key does not exist", async (t) => {
},
});
});
test("reject invalid refill config", async (t) => {
const h = await IntegrationHarness.init(t);
const keyId = newId("test");
const root = await h.createRootKey([`api.${h.resources.userApi.id}.create_key`]);
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
/* The code snippet is making a POST request to the "/v1/keys.createKey" endpoint with the specified headers. It is using the `h.post` method from the `Harness` instance to send the request. The generic types `<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>` specify the request payload and response types respectively. */

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use ErrorResponse as 2nd type param I think, I'm surprised this built at all

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we should change all the other tests to match?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all tests where you expect an error, yes
generally that's everything where we don't return a 200 status code

url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId,
remaining: 10,
refill: {
amount: 100,
refillDay: 4,
interval: "daily",
},
},
});
expect(res.status).toEqual(400);
expect(res.body).toMatchObject({
error: {
code: "BAD_REQUEST",
docs: "https://unkey.dev/docs/api-reference/errors/code/BAD_REQUEST",
message: "when interval is set to 'daily', 'refillDay' must be null.",
},
});
});
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing key creation step

The test is attempting to update a key without first creating it. This might lead to false positives if the error handling for non-existent keys is the same as for invalid refill configurations.

Consider adding a step to create the key before attempting to update it:

const createRes = await h.post<V1KeysCreateKeyRequest, V1KeysCreateKeyResponse>({
  url: "/v1/keys.createKey",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${root.key}`,
  },
  body: {
    apiId: h.resources.userApi.id,
    prefix: "prefix",
  },
});
expect(createRes.status).toEqual(200);
const keyId = createRes.body.keyId;

Then use this keyId in your update request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this valid?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MichaelUnkey, thank you for bringing this to my attention. You're correct—the test is valid as is. The API validates the refill configuration before checking if the key exists, so the test correctly ensures that invalid refill configurations result in a 400 error, even if the key does not exist.

I apologize for any confusion my previous comment may have caused.

(_/)
(•_•)
I'm sorry for the confusion.


✏️ Learnings added
Learnt from: MichaelUnkey
PR: unkeyed/unkey#2114
File: apps/api/src/routes/v1_keys_updateKey.error.test.ts:0-0
Timestamp: 2024-09-27T15:20:05.301Z
Learning: In the `v1/keys.updateKey` endpoint, the server validates the refill configuration before checking if the key exists. Therefore, tests can assert validation errors without needing to create the key first.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

48 changes: 48 additions & 0 deletions apps/api/src/routes/v1_keys_updateKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,3 +1013,51 @@ test("update ratelimit should not disable it", async (t) => {
expect(verify.body.ratelimit!.limit).toBe(5);
expect(verify.body.ratelimit!.remaining).toBe(4);
});
describe("Should default last day of month if none provided", () => {
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
test("should provide default value", async (t) => {
const h = await IntegrationHarness.init(t);

const key = {
id: newId("test"),
keyAuthId: h.resources.userKeyAuth.id,
workspaceId: h.resources.userWorkspace.id,
start: "test",
name: "test",
hash: await sha256(new KeyV1({ byteLength: 16 }).toString()),

createdAt: new Date(),
};
await h.db.primary.insert(schema.keys).values(key);
const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);
const date = new Date();
const lastDate = date.getMonth() + 1;
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.id,
remaining: 10,
refill: {
interval: "monthly",
amount: 130,
refillDay: undefined,
},
enabled: true,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.id),
});
expect(found).toBeDefined();
expect(found?.remaining).toEqual(10);
expect(found?.refillAmount).toEqual(1030);
MichaelUnkey marked this conversation as resolved.
Show resolved Hide resolved
expect(found?.refillInterval).toEqual("monthly");
expect(found?.refillDay).toEqual(lastDate);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding more test cases for refill functionality

The new test case for the default refill day is a great addition. To ensure comprehensive coverage of the refill functionality, consider adding the following test cases:

  1. Test with a specific refillDay provided (e.g., 15th of the month).
  2. Test with an invalid refillDay (e.g., 31 for a 30-day month).
  3. Test updating an existing key with refill settings to ensure they can be modified correctly.
  4. Test with different refillInterval values (e.g., "weekly", "yearly") if supported.

These additional test cases will help ensure the robustness of the refill functionality across various scenarios.

Loading