Skip to content

Commit

Permalink
test cases added
Browse files Browse the repository at this point in the history
  • Loading branch information
mrasif committed Nov 8, 2024
1 parent 439cac4 commit 9fa7b5a
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 3 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/go-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Go Tests

on:
push:
branches: [ 'main', 'develop' ]
pull_request:
branches: [ '*' ]

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.22
- name: Download dependencies
run: go mod tidy
- name: Test
run: go test -race ./...
20 changes: 17 additions & 3 deletions main.go → totp.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Package totp implements Time-based One-Time Password (TOTP) generation and validation.
TOTP is an algorithm that computes a one-time password from a shared secret key and the current time.
This package provides functions to generate a TOTP secret, generate TOTP codes, and validate them.
*/
package totp

import (
Expand All @@ -13,6 +19,8 @@ import (
)

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

secretLength := 10
Expand All @@ -32,7 +40,9 @@ func GenerateSecret() (string, error) {
return secret, nil
}

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

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

// generateTOTP generates a TOTP code for the current timestamp
// generateTOTP generates a TOTP code for the specified timestamp.
// It takes a base32-encoded secret, a timestamp, and a duration in seconds.
// It returns the generated TOTP code and an error if the generation fails.
func generateTOTP(secret string, timestamp int64, duration int) (string, error) {
// Decode the base32-encoded secret
secret = strings.ToUpper(secret) // TOTP secrets are usually upper-case
Expand Down
209 changes: 209 additions & 0 deletions totp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package totp

import (
"testing"
"time"
)

func TestGenerateSecret(t *testing.T) {
// Test generating a secret
secret, err := GenerateSecret()
if err != nil {
t.Fatalf("Failed to generate secret: %v", err)
}

// Check secret length (base32 encoded, should be around 16 characters)
if len(secret) < 10 || len(secret) > 20 {
t.Errorf("Generated secret length is invalid: %d", len(secret))
}
}

func TestTOTP(t *testing.T) {
// Test scenarios
testCases := []struct {
name string
duration int
wantErr bool
}{
{"Standard 30-second interval", 30, false},
{"Custom 60-second interval", 60, false},
{"Invalid duration", -1, false},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Generate a secret
secret, err := GenerateSecret()
if err != nil {
t.Fatalf("Failed to generate secret: %v", err)
}

// Generate TOTP
code, err := TOTP(secret, tc.duration)
if tc.wantErr && err == nil {
t.Error("Expected an error, but got none")
}

if err != nil && !tc.wantErr {
t.Errorf("Unexpected error: %v", err)
}

// Check code length
if len(code) != 6 {
t.Errorf("Invalid TOTP code length: %d", len(code))
}
})
}
}

// timeProvider is an interface to allow easier time mocking
type timeProvider interface {
Now() time.Time
}

// realTimeProvider uses the actual system time
type realTimeProvider struct{}

func (r realTimeProvider) Now() time.Time {
return time.Now()
}

// mockTimeProvider allows setting a fixed time for testing
type mockTimeProvider struct {
fixedTime time.Time
}

func (m mockTimeProvider) Now() time.Time {
return m.fixedTime
}

// Global variable to hold current time provider
var currentTimeProvider timeProvider = realTimeProvider{}

// SetTimeProvider allows setting a custom time provider (useful for testing)
func SetTimeProvider(provider timeProvider) {
currentTimeProvider = provider
}

// Modified functions to use the time provider
func nowFunc() time.Time {
return currentTimeProvider.Now()
}


// Updated test file
func TestValidate(t *testing.T) {
// Reset time provider after the test
defer SetTimeProvider(realTimeProvider{})

// Test validation scenarios
testCases := []struct {
name string
duration int
timeDiff time.Duration
expected bool
}{
{"Current time", 30, 0, true},
{"30 seconds before", 30, -30 * time.Second, true},
{"30 seconds after", 30, 30 * time.Second, true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a fixed time and set it as the current time provider
fixedTime := time.Now().Add(tc.timeDiff)
SetTimeProvider(mockTimeProvider{fixedTime})

// Generate a secret
secret, err := GenerateSecret()
if err != nil {
t.Fatalf("Failed to generate secret: %v", err)
}

// Generate TOTP code
code, err := TOTP(secret, tc.duration)
if err != nil {
t.Fatalf("Failed to generate TOTP: %v", err)
}

// Validate the code
isValid := Validate(secret, tc.duration, code)
if isValid != tc.expected {
t.Errorf("Validation result unexpected. Got %v, want %v", isValid, tc.expected)
}
})
}
}

func TestInvalidSecret(t *testing.T) {
// Test with invalid secrets
invalidSecrets := []string{
"INVALID_SECRET",
"12345",
}

for _, secret := range invalidSecrets {
t.Run("Invalid Secret: "+secret, func(t *testing.T) {
// Try to generate TOTP with invalid secret
_, err := TOTP(secret, 30)
if err == nil {
t.Error("Expected an error with invalid secret, but got none")
}

// Try to validate with invalid secret
isValid := Validate(secret, 30, "123456")
if isValid {
t.Error("Validation should fail with invalid secret")
}
})
}
}

// Example of resetting to real time provider
func resetTimeProvider() {
SetTimeProvider(realTimeProvider{})
}

func BenchmarkGenerateSecret(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = GenerateSecret()
}
}

func BenchmarkTOTP(b *testing.B) {
secret, _ := GenerateSecret()
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, _ = TOTP(secret, 30)
}
}

func BenchmarkValidate(b *testing.B) {
secret, _ := GenerateSecret()
code, _ := TOTP(secret, 30)
b.ResetTimer()

for i := 0; i < b.N; i++ {
_ = Validate(secret, 30, code)
}
}

// Example of how to use the package
func ExampleTOTP() {
// Generate a secret
secret, err := GenerateSecret()
if err != nil {
panic(err)
}

// Generate a TOTP code
code, err := TOTP(secret, 30)
if err != nil {
panic(err)
}

// Validate the code
isValid := Validate(secret, 30, code)
println("Code is valid:", isValid)
}

0 comments on commit 9fa7b5a

Please sign in to comment.