Skip to content

Commit 9fa7b5a

Browse files
committed
test cases added
1 parent 439cac4 commit 9fa7b5a

File tree

3 files changed

+247
-3
lines changed

3 files changed

+247
-3
lines changed

.github/workflows/go-test.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Go Tests
2+
3+
on:
4+
push:
5+
branches: [ 'main', 'develop' ]
6+
pull_request:
7+
branches: [ '*' ]
8+
9+
jobs:
10+
tests:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- name: Set up Go
15+
uses: actions/setup-go@v2
16+
with:
17+
go-version: 1.22
18+
- name: Download dependencies
19+
run: go mod tidy
20+
- name: Test
21+
run: go test -race ./...

main.go renamed to totp.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/*
2+
Package totp implements Time-based One-Time Password (TOTP) generation and validation.
3+
4+
TOTP is an algorithm that computes a one-time password from a shared secret key and the current time.
5+
This package provides functions to generate a TOTP secret, generate TOTP codes, and validate them.
6+
*/
17
package totp
28

39
import (
@@ -13,6 +19,8 @@ import (
1319
)
1420

1521
// GenerateSecret generates a random TOTP secret key in base32 encoding.
22+
// The length of the generated secret is fixed to 10 bytes.
23+
// It returns the base32-encoded secret and an error if the generation fails.
1624
func GenerateSecret() (string, error) {
1725

1826
secretLength := 10
@@ -32,7 +40,9 @@ func GenerateSecret() (string, error) {
3240
return secret, nil
3341
}
3442

35-
// TOTP generates a 6-digit TOTP code using the given base32-encoded secret and a time step of 30 seconds.
43+
// TOTP generates a 6-digit TOTP code using the given base32-encoded secret
44+
// and a time step in seconds (default is 30 seconds).
45+
// It returns the generated TOTP code and an error if the generation fails.
3646
func TOTP(secret string, duration int) (string, error) {
3747
// Decode the base32-encoded secret
3848
secret = strings.ToUpper(secret) // TOTP secrets are usually upper-case
@@ -43,7 +53,9 @@ func TOTP(secret string, duration int) (string, error) {
4353
return generateTOTP(secret, timestamp, duration)
4454
}
4555

46-
// ValidateTOTP checks if the provided code matches the generated TOTP code for the given secret
56+
// Validate checks if the provided TOTP code matches the generated TOTP code
57+
// for the given secret within a +/- 30-second time window.
58+
// It returns true if the code is valid, otherwise false.
4759
func Validate(secret string, duration int, code string) bool {
4860
if duration < 1 {
4961
duration = 30
@@ -68,7 +80,9 @@ func Validate(secret string, duration int, code string) bool {
6880
return false
6981
}
7082

71-
// generateTOTP generates a TOTP code for the current timestamp
83+
// generateTOTP generates a TOTP code for the specified timestamp.
84+
// It takes a base32-encoded secret, a timestamp, and a duration in seconds.
85+
// It returns the generated TOTP code and an error if the generation fails.
7286
func generateTOTP(secret string, timestamp int64, duration int) (string, error) {
7387
// Decode the base32-encoded secret
7488
secret = strings.ToUpper(secret) // TOTP secrets are usually upper-case

totp_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package totp
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestGenerateSecret(t *testing.T) {
9+
// Test generating a secret
10+
secret, err := GenerateSecret()
11+
if err != nil {
12+
t.Fatalf("Failed to generate secret: %v", err)
13+
}
14+
15+
// Check secret length (base32 encoded, should be around 16 characters)
16+
if len(secret) < 10 || len(secret) > 20 {
17+
t.Errorf("Generated secret length is invalid: %d", len(secret))
18+
}
19+
}
20+
21+
func TestTOTP(t *testing.T) {
22+
// Test scenarios
23+
testCases := []struct {
24+
name string
25+
duration int
26+
wantErr bool
27+
}{
28+
{"Standard 30-second interval", 30, false},
29+
{"Custom 60-second interval", 60, false},
30+
{"Invalid duration", -1, false},
31+
}
32+
33+
for _, tc := range testCases {
34+
t.Run(tc.name, func(t *testing.T) {
35+
// Generate a secret
36+
secret, err := GenerateSecret()
37+
if err != nil {
38+
t.Fatalf("Failed to generate secret: %v", err)
39+
}
40+
41+
// Generate TOTP
42+
code, err := TOTP(secret, tc.duration)
43+
if tc.wantErr && err == nil {
44+
t.Error("Expected an error, but got none")
45+
}
46+
47+
if err != nil && !tc.wantErr {
48+
t.Errorf("Unexpected error: %v", err)
49+
}
50+
51+
// Check code length
52+
if len(code) != 6 {
53+
t.Errorf("Invalid TOTP code length: %d", len(code))
54+
}
55+
})
56+
}
57+
}
58+
59+
// timeProvider is an interface to allow easier time mocking
60+
type timeProvider interface {
61+
Now() time.Time
62+
}
63+
64+
// realTimeProvider uses the actual system time
65+
type realTimeProvider struct{}
66+
67+
func (r realTimeProvider) Now() time.Time {
68+
return time.Now()
69+
}
70+
71+
// mockTimeProvider allows setting a fixed time for testing
72+
type mockTimeProvider struct {
73+
fixedTime time.Time
74+
}
75+
76+
func (m mockTimeProvider) Now() time.Time {
77+
return m.fixedTime
78+
}
79+
80+
// Global variable to hold current time provider
81+
var currentTimeProvider timeProvider = realTimeProvider{}
82+
83+
// SetTimeProvider allows setting a custom time provider (useful for testing)
84+
func SetTimeProvider(provider timeProvider) {
85+
currentTimeProvider = provider
86+
}
87+
88+
// Modified functions to use the time provider
89+
func nowFunc() time.Time {
90+
return currentTimeProvider.Now()
91+
}
92+
93+
94+
// Updated test file
95+
func TestValidate(t *testing.T) {
96+
// Reset time provider after the test
97+
defer SetTimeProvider(realTimeProvider{})
98+
99+
// Test validation scenarios
100+
testCases := []struct {
101+
name string
102+
duration int
103+
timeDiff time.Duration
104+
expected bool
105+
}{
106+
{"Current time", 30, 0, true},
107+
{"30 seconds before", 30, -30 * time.Second, true},
108+
{"30 seconds after", 30, 30 * time.Second, true},
109+
}
110+
111+
for _, tc := range testCases {
112+
t.Run(tc.name, func(t *testing.T) {
113+
// Create a fixed time and set it as the current time provider
114+
fixedTime := time.Now().Add(tc.timeDiff)
115+
SetTimeProvider(mockTimeProvider{fixedTime})
116+
117+
// Generate a secret
118+
secret, err := GenerateSecret()
119+
if err != nil {
120+
t.Fatalf("Failed to generate secret: %v", err)
121+
}
122+
123+
// Generate TOTP code
124+
code, err := TOTP(secret, tc.duration)
125+
if err != nil {
126+
t.Fatalf("Failed to generate TOTP: %v", err)
127+
}
128+
129+
// Validate the code
130+
isValid := Validate(secret, tc.duration, code)
131+
if isValid != tc.expected {
132+
t.Errorf("Validation result unexpected. Got %v, want %v", isValid, tc.expected)
133+
}
134+
})
135+
}
136+
}
137+
138+
func TestInvalidSecret(t *testing.T) {
139+
// Test with invalid secrets
140+
invalidSecrets := []string{
141+
"INVALID_SECRET",
142+
"12345",
143+
}
144+
145+
for _, secret := range invalidSecrets {
146+
t.Run("Invalid Secret: "+secret, func(t *testing.T) {
147+
// Try to generate TOTP with invalid secret
148+
_, err := TOTP(secret, 30)
149+
if err == nil {
150+
t.Error("Expected an error with invalid secret, but got none")
151+
}
152+
153+
// Try to validate with invalid secret
154+
isValid := Validate(secret, 30, "123456")
155+
if isValid {
156+
t.Error("Validation should fail with invalid secret")
157+
}
158+
})
159+
}
160+
}
161+
162+
// Example of resetting to real time provider
163+
func resetTimeProvider() {
164+
SetTimeProvider(realTimeProvider{})
165+
}
166+
167+
func BenchmarkGenerateSecret(b *testing.B) {
168+
for i := 0; i < b.N; i++ {
169+
_, _ = GenerateSecret()
170+
}
171+
}
172+
173+
func BenchmarkTOTP(b *testing.B) {
174+
secret, _ := GenerateSecret()
175+
b.ResetTimer()
176+
177+
for i := 0; i < b.N; i++ {
178+
_, _ = TOTP(secret, 30)
179+
}
180+
}
181+
182+
func BenchmarkValidate(b *testing.B) {
183+
secret, _ := GenerateSecret()
184+
code, _ := TOTP(secret, 30)
185+
b.ResetTimer()
186+
187+
for i := 0; i < b.N; i++ {
188+
_ = Validate(secret, 30, code)
189+
}
190+
}
191+
192+
// Example of how to use the package
193+
func ExampleTOTP() {
194+
// Generate a secret
195+
secret, err := GenerateSecret()
196+
if err != nil {
197+
panic(err)
198+
}
199+
200+
// Generate a TOTP code
201+
code, err := TOTP(secret, 30)
202+
if err != nil {
203+
panic(err)
204+
}
205+
206+
// Validate the code
207+
isValid := Validate(secret, 30, code)
208+
println("Code is valid:", isValid)
209+
}

0 commit comments

Comments
 (0)