Skip to content

Commit 435f119

Browse files
committed
feat: add URLCrypto service for encoding and decoding URLs in base64url format
1 parent 8b1bc41 commit 435f119

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

lib/url-transformer.service.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createHash } from "crypto"
2+
3+
export class URLCrypto {
4+
private static readonly SEPARATOR = "-"
5+
6+
static toBase64Url(url: string): string {
7+
try {
8+
new URL(url)
9+
} catch (error) {
10+
throw new Error(`URL invalide: ${url}`)
11+
}
12+
13+
const hash = createHash("sha256").update(url).digest("base64url").slice(0, 8)
14+
const encoded = Buffer.from(url).toString("base64url")
15+
16+
return `${encoded}${this.SEPARATOR}${hash}`
17+
}
18+
19+
static fromBase64Url(encoded: string): string {
20+
try {
21+
const [urlPart, hashPart] = encoded.split(this.SEPARATOR)
22+
if (!urlPart || !hashPart) {
23+
throw new Error("Format invalide")
24+
}
25+
26+
const decodedUrl = Buffer.from(urlPart, "base64url").toString()
27+
28+
const expectedHash = createHash("sha256")
29+
.update(decodedUrl)
30+
.digest("base64url")
31+
.slice(0, 8)
32+
33+
if (hashPart !== expectedHash) {
34+
throw new Error("Hash de vérification invalide")
35+
}
36+
37+
new URL(decodedUrl)
38+
39+
return decodedUrl
40+
} catch (error) {
41+
console.error(error)
42+
throw new Error(`Décodage impossible`)
43+
}
44+
}
45+
46+
static isValid(encoded: string): boolean {
47+
try {
48+
this.fromBase64Url(encoded)
49+
return true
50+
} catch {
51+
return false
52+
}
53+
}
54+
}

lib/url-transformer.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { URLCrypto } from "./url-transformer.service"
2+
3+
describe("URLCrypto", () => {
4+
// Tests d'encodage
5+
describe("toBase64Url", () => {
6+
test("devrait encoder une URL valide", () => {
7+
const url = "https://www.alextraveylan.fr/fr"
8+
const encoded = URLCrypto.toBase64Url(url)
9+
10+
expect(encoded).toBeDefined()
11+
expect(encoded).toContain("-")
12+
expect(encoded.split("-")).toHaveLength(2)
13+
})
14+
15+
test("devrait rejeter une URL invalide", () => {
16+
const invalidUrl = "not-a-url"
17+
18+
expect(() => {
19+
URLCrypto.toBase64Url(invalidUrl)
20+
}).toThrow("URL invalide")
21+
})
22+
23+
test("devrait gérer les URLs avec des caractères spéciaux", () => {
24+
const urlWithSpecialChars = "https://example.com/path?param=value&special=@#$"
25+
const encoded = URLCrypto.toBase64Url(urlWithSpecialChars)
26+
27+
expect(encoded).toBeDefined()
28+
expect(URLCrypto.fromBase64Url(encoded)).toBe(urlWithSpecialChars)
29+
})
30+
})
31+
32+
// Tests de décodage
33+
describe("fromBase64Url", () => {
34+
test("devrait décoder une string encodée valide", () => {
35+
const originalUrl = "https://www.alextraveylan.fr/fr"
36+
const encoded = URLCrypto.toBase64Url(originalUrl)
37+
const decoded = URLCrypto.fromBase64Url(encoded)
38+
39+
expect(decoded).toBe(originalUrl)
40+
})
41+
42+
test("devrait rejeter une string encodée invalide", () => {
43+
const invalidEncoded = "invalid-encoded-string"
44+
45+
expect(() => {
46+
URLCrypto.fromBase64Url(invalidEncoded)
47+
}).toThrow("Décodage impossible")
48+
})
49+
50+
test("devrait rejeter une string avec un hash incorrect", () => {
51+
const encoded = URLCrypto.toBase64Url("https://www.alextraveylan.fr/fr")
52+
const corruptedEncoded = encoded.slice(0, -1) + "X"
53+
54+
expect(() => {
55+
URLCrypto.fromBase64Url(corruptedEncoded)
56+
}).toThrow("Décodage impossible")
57+
})
58+
})
59+
60+
// Tests de validation
61+
describe("isValid", () => {
62+
test("devrait retourner true pour une string encodée valide", () => {
63+
const url = "https://www.alextraveylan.fr/fr"
64+
const encoded = URLCrypto.toBase64Url(url)
65+
66+
expect(URLCrypto.isValid(encoded)).toBe(true)
67+
})
68+
69+
test("devrait retourner false pour une string encodée invalide", () => {
70+
expect(URLCrypto.isValid("invalid-string")).toBe(false)
71+
})
72+
})
73+
74+
// Tests de cas limites
75+
describe("edge cases", () => {
76+
test("devrait gérer les URLs très longues", () => {
77+
const longUrl = "https://example.com/" + "a".repeat(1000)
78+
const encoded = URLCrypto.toBase64Url(longUrl)
79+
const decoded = URLCrypto.fromBase64Url(encoded)
80+
81+
expect(decoded).toBe(longUrl)
82+
})
83+
84+
test("devrait gérer les URLs avec des fragments", () => {
85+
const urlWithFragment = "https://example.com/page#section"
86+
const encoded = URLCrypto.toBase64Url(urlWithFragment)
87+
const decoded = URLCrypto.fromBase64Url(encoded)
88+
89+
expect(decoded).toBe(urlWithFragment)
90+
})
91+
92+
test("devrait gérer les URLs avec des query params complexes", () => {
93+
const urlWithParams = "https://example.com/search?q=test&filter[]=1&filter[]=2"
94+
const encoded = URLCrypto.toBase64Url(urlWithParams)
95+
const decoded = URLCrypto.fromBase64Url(encoded)
96+
97+
expect(decoded).toBe(urlWithParams)
98+
})
99+
})
100+
101+
// Test de round-trip
102+
describe("round-trip", () => {
103+
test("devrait préserver l'URL après encodage/décodage multiple", () => {
104+
const originalUrl = "https://www.alextraveylan.fr/fr"
105+
const encoded1 = URLCrypto.toBase64Url(originalUrl)
106+
const decoded1 = URLCrypto.fromBase64Url(encoded1)
107+
const encoded2 = URLCrypto.toBase64Url(decoded1)
108+
const decoded2 = URLCrypto.fromBase64Url(encoded2)
109+
110+
expect(decoded2).toBe(originalUrl)
111+
})
112+
})
113+
})

0 commit comments

Comments
 (0)