init commit
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'"
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
type UserGoogleInfo struct {
|
||||
Email string `json:"email"`
|
||||
Picture string `json:"picture"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user