init commit

This commit is contained in:
2025-11-25 15:12:31 +08:00
commit 052c7e0cca
63 changed files with 8828 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
package models
import "time"
type LogEventParams struct {
ID int `json:"id"`
UserID *string `json:"user_id"`
ParticipantID *string `json:"participant_id"`
ActivityType int `json:"activity_type"`
IPAddress string `json:"ip_address"`
FieldUpdated interface{} `json:"field_updated"`
Time *time.Time `json:"time"`
ErrorMessage string `json:"error_message"`
}
type UserAccessLog struct {
ID int `json:"id"`
UserID *string `json:"user_id"`
ParticipantID *string `json:"participant_id"`
ActivityType int `json:"activity_type"`
IPAddress string `json:"ip_address"`
FieldUpdated interface{} `json:"field_updated"`
Time time.Time `json:"time"`
}
+350
View File
@@ -0,0 +1,350 @@
package models
import (
"encoding/json"
"testing"
"time"
)
func TestLogEventParamsCreation(t *testing.T) {
userID := "user-123"
participantID := "participant-456"
now := time.Now()
params := LogEventParams{
ID: 1,
UserID: &userID,
ParticipantID: &participantID,
ActivityType: 10,
IPAddress: "192.168.1.1",
FieldUpdated: map[string]string{"field": "value"},
Time: &now,
ErrorMessage: "",
}
if params.FieldUpdated == nil {
t.Error("Expected FieldUpdated to not be nil")
}
if params.Time == nil {
t.Error("Expected Time to not be nil")
}
if params.ErrorMessage != "" {
t.Errorf("Expected empty ErrorMessage, got '%s'", params.ErrorMessage)
}
if params.ID != 1 {
t.Errorf("Expected ID 1, got %d", params.ID)
}
if *params.UserID != "user-123" {
t.Errorf("Expected UserID 'user-123', got '%s'", *params.UserID)
}
if *params.ParticipantID != "participant-456" {
t.Errorf("Expected ParticipantID 'participant-456', got '%s'", *params.ParticipantID)
}
if params.ActivityType != 10 {
t.Errorf("Expected ActivityType 10, got %d", params.ActivityType)
}
if params.IPAddress != "192.168.1.1" {
t.Errorf("Expected IPAddress '192.168.1.1', got '%s'", params.IPAddress)
}
}
func TestLogEventParamsNullableFields(t *testing.T) {
params := LogEventParams{
ID: 2,
UserID: nil,
ParticipantID: nil,
ActivityType: 5,
IPAddress: LocalNetwork,
FieldUpdated: nil,
Time: nil,
ErrorMessage: "Test error",
}
if params.ID != 2 {
t.Errorf("Expected ID 2, got %d", params.ID)
}
if params.ActivityType != 5 {
t.Errorf("Expected ActivityType 5, got %d", params.ActivityType)
}
if params.IPAddress != LocalNetwork {
t.Errorf("Expected IPAddress '10.0.0.1', got '%s'", params.IPAddress)
}
if params.UserID != nil {
t.Error("Expected UserID to be nil")
}
if params.ParticipantID != nil {
t.Error("Expected ParticipantID to be nil")
}
if params.Time != nil {
t.Error("Expected Time to be nil")
}
if params.FieldUpdated != nil {
t.Error("Expected FieldUpdated to be nil")
}
if params.ErrorMessage != "Test error" {
t.Errorf("Expected ErrorMessage 'Test error', got '%s'", params.ErrorMessage)
}
}
func TestLogEventParamsFieldUpdatedInterface(t *testing.T) {
// Test with map
mapData := map[string]interface{}{"key": "value", "count": 42}
params1 := LogEventParams{
FieldUpdated: mapData,
}
if params1.FieldUpdated == nil {
t.Error("Expected FieldUpdated to not be nil")
}
// Test with string
params2 := LogEventParams{
FieldUpdated: "simple string value",
}
if params2.FieldUpdated != "simple string value" {
t.Errorf("Expected FieldUpdated 'simple string value', got '%v'", params2.FieldUpdated)
}
// Test with int
params3 := LogEventParams{
FieldUpdated: 123,
}
if params3.FieldUpdated != 123 {
t.Errorf("Expected FieldUpdated 123, got %v", params3.FieldUpdated)
}
}
func TestLogEventParamsJSONMarshaling(t *testing.T) {
userID := "user-789"
now := time.Now()
params := LogEventParams{
ID: 3,
UserID: &userID,
ActivityType: 15,
IPAddress: "172.16.0.1",
Time: &now,
ErrorMessage: "",
}
if params.ActivityType != 15 {
t.Errorf("Expected ActivityType 15, got %d", params.ActivityType)
}
if params.IPAddress != "172.16.0.1" {
t.Errorf("Expected IPAddress '172.16.0.1', got '%s'", params.IPAddress)
}
jsonData, err := json.Marshal(params)
if err != nil {
t.Fatalf("Failed to marshal LogEventParams: %v", err)
}
if len(jsonData) == 0 {
t.Error("Expected non-empty JSON data")
}
// Unmarshal back
var unmarshaled LogEventParams
err = json.Unmarshal(jsonData, &unmarshaled)
if err != nil {
t.Fatalf("Failed to unmarshal LogEventParams: %v", err)
}
if unmarshaled.ID != params.ID {
t.Errorf("Expected ID %d, got %d", params.ID, unmarshaled.ID)
}
if *unmarshaled.UserID != *params.UserID {
t.Errorf("Expected UserID '%s', got '%s'", *params.UserID, *unmarshaled.UserID)
}
}
func TestUserAccessLogCreation(t *testing.T) {
userID := "user-abc"
now := time.Now()
accessLog := UserAccessLog{
ID: 100,
UserID: &userID,
ParticipantID: nil,
ActivityType: 20,
IPAddress: "203.0.113.1",
FieldUpdated: "login",
Time: now,
}
if accessLog.ParticipantID != nil {
t.Error("Expected ParticipantID to be nil")
}
if accessLog.FieldUpdated != "login" {
t.Errorf("Expected FieldUpdated 'login', got '%v'", accessLog.FieldUpdated)
}
if accessLog.Time.IsZero() {
t.Error("Expected Time to be set")
}
if accessLog.ID != 100 {
t.Errorf("Expected ID 100, got %d", accessLog.ID)
}
if *accessLog.UserID != "user-abc" {
t.Errorf("Expected UserID 'user-abc', got '%s'", *accessLog.UserID)
}
if accessLog.ActivityType != 20 {
t.Errorf("Expected ActivityType 20, got %d", accessLog.ActivityType)
}
if accessLog.IPAddress != "203.0.113.1" {
t.Errorf("Expected IPAddress '203.0.113.1', got '%s'", accessLog.IPAddress)
}
}
func TestUserAccessLogTimeNotNullable(t *testing.T) {
now := time.Now()
accessLog := UserAccessLog{
ID: 1,
IPAddress: Localhost,
Time: now,
}
if accessLog.ID != 1 {
t.Errorf("Expected ID 1, got %d", accessLog.ID)
}
if accessLog.IPAddress != Localhost {
t.Errorf("Expected IPAddress '127.0.0.1', got '%s'", accessLog.IPAddress)
}
// Time should always have a value (not pointer in UserAccessLog)
if accessLog.Time.IsZero() {
t.Error("Expected Time to be set, got zero value")
}
if !accessLog.Time.Equal(now) {
t.Error("Expected Time to match the set value")
}
}
func TestUserAccessLogActivityTypes(t *testing.T) {
testCases := []struct {
name string
activityType int
}{
{"Login", 1},
{"Logout", 2},
{"Create", 3},
{"Update", 4},
{"Delete", 5},
{"View", 6},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
accessLog := UserAccessLog{
ActivityType: tc.activityType,
}
if accessLog.ActivityType != tc.activityType {
t.Errorf("Expected ActivityType %d, got %d", tc.activityType, accessLog.ActivityType)
}
})
}
}
func TestUserAccessLogIPAddressValidation(t *testing.T) {
testCases := []struct {
name string
ipAddress string
}{
{"IPv4", "192.168.1.1"},
{"IPv4 Loopback", Localhost},
{"IPv4 Private", LocalNetwork},
{"IPv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
{"IPv6 Loopback", "::1"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
accessLog := UserAccessLog{
IPAddress: tc.ipAddress,
}
if accessLog.IPAddress != tc.ipAddress {
t.Errorf("Expected IPAddress '%s', got '%s'", tc.ipAddress, accessLog.IPAddress)
}
})
}
}
func TestUserAccessLogJSONMarshaling(t *testing.T) {
userID := "user-json-test"
now := time.Now()
accessLog := UserAccessLog{
ID: 50,
UserID: &userID,
ActivityType: 10,
IPAddress: "192.0.2.1",
FieldUpdated: map[string]string{"action": "test"},
Time: now,
}
jsonData, err := json.Marshal(accessLog)
if err != nil {
t.Fatalf("Failed to marshal UserAccessLog: %v", err)
}
var unmarshaled UserAccessLog
err = json.Unmarshal(jsonData, &unmarshaled)
if err != nil {
t.Fatalf("Failed to unmarshal UserAccessLog: %v", err)
}
if unmarshaled.ID != accessLog.ID {
t.Errorf("Expected ID %d, got %d", accessLog.ID, unmarshaled.ID)
}
if *unmarshaled.UserID != *accessLog.UserID {
t.Errorf("Expected UserID '%s', got '%s'", *accessLog.UserID, *unmarshaled.UserID)
}
}
func TestLogEventParamsErrorMessage(t *testing.T) {
params := LogEventParams{
ErrorMessage: "Database connection failed",
}
if params.ErrorMessage != "Database connection failed" {
t.Errorf("Expected ErrorMessage 'Database connection failed', got '%s'", params.ErrorMessage)
}
// Empty error message
params2 := LogEventParams{
ErrorMessage: "",
}
if params2.ErrorMessage != "" {
t.Errorf("Expected empty ErrorMessage, got '%s'", params2.ErrorMessage)
}
}
+9
View File
@@ -0,0 +1,9 @@
package models
const (
Localhost = "127.0.0.1"
LocalNetwork = "10.0.0.1"
TestEmail = "test@example.com"
SessionID = "session-123"
ErrorMessageFormat = "Expected ID '%s', got '%s'"
)
+6
View File
@@ -0,0 +1,6 @@
package models
type UserGoogleInfo struct {
Email string `json:"email"`
Picture string `json:"picture"`
}
+187
View File
@@ -0,0 +1,187 @@
package models
import (
"encoding/json"
"testing"
)
func TestUserGoogleInfo_Creation(t *testing.T) {
userInfo := UserGoogleInfo{
Email: "user@gmail.com",
Picture: "https://example.com/picture.jpg",
}
if userInfo.Email != "user@gmail.com" {
t.Errorf("Expected email 'user@gmail.com', got '%s'", userInfo.Email)
}
if userInfo.Picture != "https://example.com/picture.jpg" {
t.Errorf("Expected picture URL 'https://example.com/picture.jpg', got '%s'", userInfo.Picture)
}
}
func TestUserGoogleInfo_EmptyFields(t *testing.T) {
userInfo := UserGoogleInfo{}
if userInfo.Email != "" {
t.Errorf("Expected empty email, got '%s'", userInfo.Email)
}
if userInfo.Picture != "" {
t.Errorf("Expected empty picture, got '%s'", userInfo.Picture)
}
}
func TestUserGoogleInfo_JSONMarshaling(t *testing.T) {
userInfo := UserGoogleInfo{
Email: "test@example.com",
Picture: "https://example.com/photo.jpg",
}
// Marshal to JSON
jsonData, err := json.Marshal(userInfo)
if err != nil {
t.Fatalf("Failed to marshal UserGoogleInfo: %v", err)
}
expectedJSON := `{"email":"test@example.com","picture":"https://example.com/photo.jpg"}`
if string(jsonData) != expectedJSON {
t.Errorf("Expected JSON '%s', got '%s'", expectedJSON, string(jsonData))
}
}
func TestUserGoogleInfo_JSONUnmarshaling(t *testing.T) {
jsonData := []byte(`{"email":"unmarshaled@example.com","picture":"https://example.com/image.png"}`)
var userInfo UserGoogleInfo
err := json.Unmarshal(jsonData, &userInfo)
if err != nil {
t.Fatalf("Failed to unmarshal UserGoogleInfo: %v", err)
}
if userInfo.Email != "unmarshaled@example.com" {
t.Errorf("Expected email 'unmarshaled@example.com', got '%s'", userInfo.Email)
}
if userInfo.Picture != "https://example.com/image.png" {
t.Errorf("Expected picture 'https://example.com/image.png', got '%s'", userInfo.Picture)
}
}
func TestUserGoogleInfo_PartialData(t *testing.T) {
// Test with only email
userInfo1 := UserGoogleInfo{
Email: "onlyemail@example.com",
}
if userInfo1.Email != "onlyemail@example.com" {
t.Errorf("Expected email 'onlyemail@example.com', got '%s'", userInfo1.Email)
}
if userInfo1.Picture != "" {
t.Errorf("Expected empty picture, got '%s'", userInfo1.Picture)
}
// Test with only picture
userInfo2 := UserGoogleInfo{
Picture: "https://example.com/only-picture.jpg",
}
if userInfo2.Email != "" {
t.Errorf("Expected empty email, got '%s'", userInfo2.Email)
}
if userInfo2.Picture != "https://example.com/only-picture.jpg" {
t.Errorf("Expected picture 'https://example.com/only-picture.jpg', got '%s'", userInfo2.Picture)
}
}
func TestUserGoogleInfo_ValidEmailFormat(t *testing.T) {
testCases := []struct {
name string
email string
valid bool
}{
{"Valid Gmail", "user@gmail.com", true},
{"Valid Custom Domain", "user@example.com", true},
{"Invalid No At", "usergmail.com", false},
{"Invalid No Domain", "user@", false},
{"Invalid Empty", "", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
userInfo := UserGoogleInfo{Email: tc.email}
// Basic email validation (contains @)
hasAt := false
for _, char := range userInfo.Email {
if char == '@' {
hasAt = true
break
}
}
if tc.valid && !hasAt && tc.email != "" {
t.Errorf("Expected valid email format for '%s'", tc.email)
}
if !tc.valid && hasAt && tc.email != "" {
// This is fine, we're just checking structure
}
})
}
}
func TestUserGoogleInfo_PictureURLValidation(t *testing.T) {
testCases := []struct {
name string
picture string
isHTTPS bool
}{
{"HTTPS URL", "https://example.com/pic.jpg", true},
{"HTTP URL", "http://example.com/pic.jpg", false},
{"No Protocol", "example.com/pic.jpg", false},
{"Empty", "", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
userInfo := UserGoogleInfo{Picture: tc.picture}
hasHTTPS := len(userInfo.Picture) >= 8 && userInfo.Picture[:8] == "https://"
if tc.isHTTPS != hasHTTPS {
t.Errorf("Expected HTTPS=%v for '%s', got %v", tc.isHTTPS, tc.picture, hasHTTPS)
}
})
}
}
func TestUserGoogleInfo_CopyValues(t *testing.T) {
original := UserGoogleInfo{
Email: "original@example.com",
Picture: "https://example.com/original.jpg",
}
// Copy values
copied := UserGoogleInfo{
Email: original.Email,
Picture: original.Picture,
}
if copied.Email != original.Email {
t.Error("Copied email should match original")
}
if copied.Picture != original.Picture {
t.Error("Copied picture should match original")
}
// Modify copy
copied.Email = "modified@example.com"
if copied.Email == original.Email {
t.Error("Modified copy should not affect original")
}
}
+26
View File
@@ -0,0 +1,26 @@
package models
import "net/http"
// FlusherPreservingResponseWriter wraps http.ResponseWriter and preserves http.Flusher for SSE endpoints.
type FlusherPreservingResponseWriter struct {
http.ResponseWriter
}
func (w *FlusherPreservingResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// ResponseWriter wraps http.ResponseWriter to track response size for metrics
type ResponseWriter struct {
http.ResponseWriter
Size int
}
func (rw *ResponseWriter) Write(b []byte) (int, error) {
size, err := rw.ResponseWriter.Write(b)
rw.Size += size
return size, err
}
+326
View File
@@ -0,0 +1,326 @@
package models
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestFlusherPreservingResponseWriter_Creation(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &FlusherPreservingResponseWriter{
ResponseWriter: recorder,
}
if writer.ResponseWriter == nil {
t.Error("Expected ResponseWriter to be set")
}
}
func TestFlusherPreservingResponseWriter_Write(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &FlusherPreservingResponseWriter{
ResponseWriter: recorder,
}
testData := []byte("Hello, World!")
n, err := writer.Write(testData)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if n != len(testData) {
t.Errorf("Expected to write %d bytes, wrote %d", len(testData), n)
}
if recorder.Body.String() != "Hello, World!" {
t.Errorf("Expected body 'Hello, World!', got '%s'", recorder.Body.String())
}
}
func TestFlusherPreservingResponseWriter_Flush(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &FlusherPreservingResponseWriter{
ResponseWriter: recorder,
}
// Flush should not panic even if underlying writer doesn't support it
writer.Flush()
// Write something
writer.Write([]byte("test data"))
// Flush again
writer.Flush()
if recorder.Body.String() != "test data" {
t.Errorf("Expected body 'test data', got '%s'", recorder.Body.String())
}
}
func TestFlusherPreservingResponseWriter_Header(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &FlusherPreservingResponseWriter{
ResponseWriter: recorder,
}
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("X-Custom-Header", "test-value")
if recorder.Header().Get("Content-Type") != "application/json" {
t.Error("Expected Content-Type header to be set")
}
if recorder.Header().Get("X-Custom-Header") != "test-value" {
t.Error("Expected X-Custom-Header to be set")
}
}
func TestFlusherPreservingResponseWriter_WriteHeader(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &FlusherPreservingResponseWriter{
ResponseWriter: recorder,
}
writer.WriteHeader(http.StatusCreated)
if recorder.Code != http.StatusCreated {
t.Errorf("Expected status code %d, got %d", http.StatusCreated, recorder.Code)
}
}
func TestResponseWriter_Creation(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
if writer.ResponseWriter == nil {
t.Error("Expected ResponseWriter to be set")
}
if writer.Size != 0 {
t.Errorf("Expected initial Size 0, got %d", writer.Size)
}
}
func TestResponseWriter_Write(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
testData := []byte("Test response data")
n, err := writer.Write(testData)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if n != len(testData) {
t.Errorf("Expected to write %d bytes, wrote %d", len(testData), n)
}
if writer.Size != len(testData) {
t.Errorf("Expected Size %d, got %d", len(testData), writer.Size)
}
if recorder.Body.String() != "Test response data" {
t.Errorf("Expected body 'Test response data', got '%s'", recorder.Body.String())
}
}
func TestResponseWriter_MultipleWrites(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
data1 := []byte("First write. ")
data2 := []byte("Second write. ")
data3 := []byte("Third write.")
writer.Write(data1)
writer.Write(data2)
writer.Write(data3)
expectedSize := len(data1) + len(data2) + len(data3)
if writer.Size != expectedSize {
t.Errorf("Expected total Size %d, got %d", expectedSize, writer.Size)
}
expectedBody := "First write. Second write. Third write."
if recorder.Body.String() != expectedBody {
t.Errorf("Expected body '%s', got '%s'", expectedBody, recorder.Body.String())
}
}
func TestResponseWriter_EmptyWrite(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
emptyData := []byte("")
n, err := writer.Write(emptyData)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if n != 0 {
t.Errorf("Expected to write 0 bytes, wrote %d", n)
}
if writer.Size != 0 {
t.Errorf("Expected Size 0 after empty write, got %d", writer.Size)
}
}
func TestResponseWriter_Header(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
}
writer.Header().Set("Content-Type", "text/plain")
writer.Header().Set("Cache-Control", "no-cache")
if recorder.Header().Get("Content-Type") != "text/plain" {
t.Error("Expected Content-Type header to be set")
}
if recorder.Header().Get("Cache-Control") != "no-cache" {
t.Error("Expected Cache-Control header to be set")
}
}
func TestResponseWriter_WriteHeader(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
}
writer.WriteHeader(http.StatusNotFound)
if recorder.Code != http.StatusNotFound {
t.Errorf("Expected status code %d, got %d", http.StatusNotFound, recorder.Code)
}
}
func TestResponseWriter_SizeTracking(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
testCases := []struct {
name string
data string
}{
{"Small", "a"},
{"Medium", "This is a medium-sized response"},
{"Large", "This is a much larger response with lots of content to test size tracking across multiple writes"},
}
totalSize := 0
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data := []byte(tc.data)
n, err := writer.Write(data)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
totalSize += n
if writer.Size != totalSize {
t.Errorf("Expected cumulative Size %d, got %d", totalSize, writer.Size)
}
})
}
}
func TestResponseWriter_LargeWrite(t *testing.T) {
recorder := httptest.NewRecorder()
writer := &ResponseWriter{
ResponseWriter: recorder,
Size: 0,
}
// Create a large payload (10KB)
largeData := make([]byte, 10*1024)
for i := range largeData {
largeData[i] = byte('A' + (i % 26))
}
n, err := writer.Write(largeData)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if n != len(largeData) {
t.Errorf("Expected to write %d bytes, wrote %d", len(largeData), n)
}
if writer.Size != len(largeData) {
t.Errorf("Expected Size %d, got %d", len(largeData), writer.Size)
}
}
func TestFlusherPreservingResponseWriter_WithHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
w.Write([]byte("data: test event\n\n"))
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
})
req := httptest.NewRequest("GET", "/sse", nil)
recorder := httptest.NewRecorder()
wrapper := &FlusherPreservingResponseWriter{ResponseWriter: recorder}
handler.ServeHTTP(wrapper, req)
if recorder.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, recorder.Code)
}
if recorder.Header().Get("Content-Type") != "text/event-stream" {
t.Error("Expected Content-Type header for SSE")
}
}
func TestResponseWriter_WithHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"success"}`))
})
req := httptest.NewRequest("GET", "/api/test", nil)
recorder := httptest.NewRecorder()
wrapper := &ResponseWriter{ResponseWriter: recorder, Size: 0}
handler.ServeHTTP(wrapper, req)
if recorder.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, recorder.Code)
}
expectedSize := len(`{"message":"success"}`)
if wrapper.Size != expectedSize {
t.Errorf("Expected Size %d, got %d", expectedSize, wrapper.Size)
}
}
+32
View File
@@ -0,0 +1,32 @@
package models
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
type AccessToken struct {
Email string `json:"email"`
SessionID string `json:"session_id"`
Exp int64 `json:"exp"`
jwt.RegisteredClaims
}
type JWTSession struct {
ID string `json:"id" db:"id"`
UserID string `json:"user_id" db:"user_id"`
RefreshTokenHash string `json:"refresh_token_hash" db:"refresh_token_hash"`
UserAgent string `json:"user_agent" db:"user_agent"`
IPAddress string `json:"ip_address" db:"ip_address"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
IsRevoked bool `json:"is_revoked" db:"is_revoked"`
}
type ExpiredSession struct {
ID string
UserID string
RefreshTokenHash string
}
+290
View File
@@ -0,0 +1,290 @@
package models
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestAccessTokenCreation(t *testing.T) {
token := &AccessToken{
Email: TestEmail,
SessionID: SessionID,
Exp: time.Now().Add(15 * time.Minute).Unix(),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
},
}
if token.Email != TestEmail {
t.Errorf("Expected email 'test@example.com', got '%s'", token.Email)
}
if token.SessionID != SessionID {
t.Errorf("Expected session ID 'session-123', got '%s'", token.SessionID)
}
if token.Exp == 0 {
t.Error("Expected Exp to be set, got 0")
}
}
func TestAccessTokenExpiration(t *testing.T) {
expTime := time.Now().Add(15 * time.Minute)
token := &AccessToken{
Exp: expTime.Unix(),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expTime),
},
}
// Check if token is not expired
if time.Now().Unix() > token.Exp {
t.Error("Token should not be expired")
}
// Test expired token
expiredToken := &AccessToken{
Email: TestEmail,
SessionID: "session-456",
Exp: time.Now().Add(-1 * time.Hour).Unix(),
}
if expiredToken.Email != TestEmail {
t.Errorf("Expected email 'test@example.com', got '%s'", expiredToken.Email)
}
if expiredToken.SessionID != "session-456" {
t.Errorf("Expected session ID 'session-456', got '%s'", expiredToken.SessionID)
}
if time.Now().Unix() <= expiredToken.Exp {
t.Error("Token should be expired")
}
}
func TestJWTSessionCreation(t *testing.T) {
now := time.Now()
session := &JWTSession{
ID: "session-id-123",
UserID: "user-456",
RefreshTokenHash: "hash123",
UserAgent: "Mozilla/5.0",
IPAddress: "192.168.1.1",
CreatedAt: now,
UpdatedAt: now,
ExpiresAt: now.Add(7 * 24 * time.Hour),
IsRevoked: false,
}
if session.ID != "session-id-123" {
t.Errorf("Expected session ID 'session-id-123', got '%s'", session.ID)
}
if session.UserID != "user-456" {
t.Errorf("Expected user ID 'user-456', got '%s'", session.UserID)
}
if session.RefreshTokenHash != "hash123" {
t.Errorf("Expected refresh token hash 'hash123', got '%s'", session.RefreshTokenHash)
}
if session.UserAgent != "Mozilla/5.0" {
t.Errorf("Expected user agent 'Mozilla/5.0', got '%s'", session.UserAgent)
}
if session.IPAddress != "192.168.1.1" {
t.Errorf("Expected IP address '192.168.1.1', got '%s'", session.IPAddress)
}
if session.CreatedAt.IsZero() {
t.Error("Expected CreatedAt to be set")
}
if session.UpdatedAt.IsZero() {
t.Error("Expected UpdatedAt to be set")
}
if session.ExpiresAt.IsZero() {
t.Error("Expected ExpiresAt to be set")
}
if session.IsRevoked {
t.Error("Expected session to not be revoked")
}
}
func TestJWTSessionIsExpired(t *testing.T) {
now := time.Now()
// Active session
activeSession := &JWTSession{
ID: "active-session",
ExpiresAt: now.Add(1 * time.Hour),
IsRevoked: false,
}
if activeSession.ID != "active-session" {
t.Errorf("Expected ID 'active-session', got '%s'", activeSession.ID)
}
if activeSession.IsRevoked {
t.Error("Active session should not be revoked")
}
if activeSession.ExpiresAt.Before(now) {
t.Error("Active session should not be expired")
}
// Expired session
expiredSession := &JWTSession{
ID: "expired-session",
ExpiresAt: now.Add(-1 * time.Hour),
IsRevoked: false,
}
if expiredSession.ID != "expired-session" {
t.Errorf("Expected ID 'expired-session', got '%s'", expiredSession.ID)
}
if expiredSession.IsRevoked {
t.Error("Expired session should not be marked as revoked initially")
}
if !expiredSession.ExpiresAt.Before(now) {
t.Error("Expired session should be marked as expired")
}
}
func TestJWTSessionRevokedStatus(t *testing.T) {
session := &JWTSession{
ID: "test-session",
IsRevoked: false,
}
if session.ID != "test-session" {
t.Errorf("Expected ID 'test-session', got '%s'", session.ID)
}
if session.IsRevoked {
t.Error("New session should not be revoked")
}
// Simulate revocation
session.IsRevoked = true
if !session.IsRevoked {
t.Error("Session should be revoked after setting IsRevoked to true")
}
}
func TestExpiredSessionCreation(t *testing.T) {
expiredSession := ExpiredSession{
ID: "expired-id-123",
UserID: "user-789",
RefreshTokenHash: "expired-hash",
}
if expiredSession.ID != "expired-id-123" {
t.Errorf("Expected ID 'expired-id-123', got '%s'", expiredSession.ID)
}
if expiredSession.UserID != "user-789" {
t.Errorf("Expected UserID 'user-789', got '%s'", expiredSession.UserID)
}
if expiredSession.RefreshTokenHash != "expired-hash" {
t.Errorf("Expected RefreshTokenHash 'expired-hash', got '%s'", expiredSession.RefreshTokenHash)
}
}
func TestJWTSessionUpdateActivity(t *testing.T) {
now := time.Now()
session := &JWTSession{
ID: SessionID,
CreatedAt: now,
UpdatedAt: now,
}
if session.ID != SessionID {
t.Errorf("expected session ID %s, got %s", SessionID, session.ID)
}
// Simulate activity update
time.Sleep(10 * time.Millisecond)
newUpdateTime := time.Now()
session.UpdatedAt = newUpdateTime
if !session.UpdatedAt.After(session.CreatedAt) {
t.Error("UpdatedAt should be after CreatedAt after activity update")
}
if session.UpdatedAt.Before(now) {
t.Error("UpdatedAt should be more recent than original time")
}
}
func TestJWTSessionSecurityFields(t *testing.T) {
session := &JWTSession{
ID: SessionID,
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
IPAddress: "192.168.1.100",
}
if session.ID != SessionID {
t.Errorf("Expected ID %s", session.ID)
}
// Test user agent validation
expectedUserAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
if session.UserAgent != expectedUserAgent {
t.Errorf("Expected user agent '%s', got '%s'", expectedUserAgent, session.UserAgent)
}
// Test IP address validation
expectedIP := "192.168.1.100"
if session.IPAddress != expectedIP {
t.Errorf("Expected IP address '%s', got '%s'", expectedIP, session.IPAddress)
}
// Test mismatch scenario
if session.UserAgent == "Different Browser" {
t.Error("User agent should not match different value")
}
if session.IPAddress == "10.0.0.1" {
t.Error("IP address should not match different value")
}
}
func TestJWTSessionTimeValidation(t *testing.T) {
now := time.Now()
futureTime := now.Add(7 * 24 * time.Hour)
session := &JWTSession{
ID: SessionID,
CreatedAt: now,
UpdatedAt: now,
ExpiresAt: futureTime,
}
if session.ID != SessionID {
t.Errorf("Expected ID %s, got %s", SessionID, session.ID)
}
// CreatedAt should not be after UpdatedAt
if session.CreatedAt.After(session.UpdatedAt) {
t.Error("CreatedAt should not be after UpdatedAt")
}
// ExpiresAt should be after CreatedAt
if !session.ExpiresAt.After(session.CreatedAt) {
t.Error("ExpiresAt should be after CreatedAt")
}
// Session should not be expired
if session.ExpiresAt.Before(now) {
t.Error("Session should not be expired yet")
}
}