|
1 | 1 | import { base32 } from "@scure/base"; |
2 | 2 | import { describe, expect, it } from "vitest"; |
3 | | -import Otp, { InvalidHashFunctionError, InvalidOtpLengthError, InvalidSecretError } from "./index"; |
| 3 | +import Otp, { InvalidHashFunctionError, InvalidIntervalError, InvalidOtpLengthError, InvalidSecretError } from "./index"; |
4 | 4 |
|
5 | 5 | describe("Otp", () => { |
6 | 6 | it("should generate a secret of default strength", () => { |
@@ -72,6 +72,13 @@ describe("Otp", () => { |
72 | 72 | expect(uri).toContain("issuer=app.example.com"); |
73 | 73 | }); |
74 | 74 |
|
| 75 | + it("should encode issuer with special characters in URI", () => { |
| 76 | + const secret = Otp.createSecret(); |
| 77 | + const uri = Otp.createTotpKeyUriForQrCode("My App/Service", "user@example.com", secret); |
| 78 | + expect(uri).toContain("otpauth://totp/My%20App%2FService:"); |
| 79 | + expect(uri).toContain("issuer=My%20App%2FService"); |
| 80 | + }); |
| 81 | + |
75 | 82 | it("should throw an error for invalid secret length", () => { |
76 | 83 | expect(() => { |
77 | 84 | Otp.generateTotp("shortsecret"); |
@@ -229,6 +236,46 @@ describe("Otp - 6 Character Length", () => { |
229 | 236 | expect(isValid).toBe(false); |
230 | 237 | }); |
231 | 238 |
|
| 239 | + it("should throw for zero interval", () => { |
| 240 | + const secret = Otp.createSecret(); |
| 241 | + expect(() => Otp.generateTotp(secret, undefined, 6, 0)).toThrow(InvalidIntervalError); |
| 242 | + }); |
| 243 | + |
| 244 | + it("should throw for negative interval", () => { |
| 245 | + const secret = Otp.createSecret(); |
| 246 | + expect(() => Otp.generateTotp(secret, undefined, 6, -30)).toThrow(InvalidIntervalError); |
| 247 | + }); |
| 248 | + |
| 249 | + it("should use symmetric window when lookAheadSteps is not provided", () => { |
| 250 | + const secret = Otp.createSecret(); |
| 251 | + const now = Math.floor(Date.now() / 1000); |
| 252 | + const interval = 30; |
| 253 | + const oneStepAhead = now + interval; |
| 254 | + const totp = Otp.generateTotp(secret, oneStepAhead); |
| 255 | + expect(Otp.verifyTotp(secret, totp, 0, undefined, now)).toBe(false); |
| 256 | + expect(Otp.verifyTotp(secret, totp, 1, undefined, now)).toBe(true); |
| 257 | + }); |
| 258 | + |
| 259 | + it("should allow asymmetric window when lookAheadSteps is explicit", () => { |
| 260 | + const secret = Otp.createSecret(); |
| 261 | + const now = Math.floor(Date.now() / 1000); |
| 262 | + const interval = 30; |
| 263 | + const oneStepAhead = now + interval; |
| 264 | + const totp = Otp.generateTotp(secret, oneStepAhead); |
| 265 | + expect(Otp.verifyTotp(secret, totp, 2, 0, now)).toBe(false); |
| 266 | + expect(Otp.verifyTotp(secret, totp, 0, 1, now)).toBe(true); |
| 267 | + }); |
| 268 | + |
| 269 | + it("should be strict with window of 0", () => { |
| 270 | + const secret = Otp.createSecret(); |
| 271 | + const now = Math.floor(Date.now() / 1000); |
| 272 | + const totp = Otp.generateTotp(secret, now); |
| 273 | + expect(Otp.verifyTotp(secret, totp, 0, undefined, now)).toBe(true); |
| 274 | + const oneStepAgo = now - 30; |
| 275 | + const oldTotp = Otp.generateTotp(secret, oneStepAgo); |
| 276 | + expect(Otp.verifyTotp(secret, oldTotp, 0, undefined, now)).toBe(false); |
| 277 | + }); |
| 278 | + |
232 | 279 | it("should verify RFC 6238 test vectors for SHA-1 with 6-character OTP", () => { |
233 | 280 | // const rfc6238TestKeySha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // Base32 encoding of '12345678901234567890' |
234 | 281 | const rfc6238TestKeySha1 = base32.encode(new TextEncoder().encode("12345678901234567890")).replace(/=+$/, ""); |
|
0 commit comments