Compare commits
12 Commits
copierutil
...
copierutil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f713e489b | ||
|
|
ace6bd4237 | ||
|
|
3ec9bbbd27 | ||
|
|
faa5817857 | ||
|
|
89a69a9d4d | ||
|
|
63789d090d | ||
|
|
06c29c57d2 | ||
|
|
5faa4e06f7 | ||
|
|
5abe88a0d9 | ||
|
|
028dd8dd19 | ||
|
|
50161f8c8a | ||
|
|
e686e7189c |
@@ -42,7 +42,31 @@ var TimestamppbToTimeConverter = copier.TypeConverter{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeTypeConverter(srcType, dstType interface{}, fn func(src interface{}) (interface{}, error)) copier.TypeConverter {
|
func TimeToString(tm *time.Time) *string {
|
||||||
|
return timeutil.TimeToString(tm, timeutil.ISO8601)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeStringConverterPair() []copier.TypeConverter {
|
||||||
|
srcType := &time.Time{}
|
||||||
|
dstType := trans.Ptr("")
|
||||||
|
|
||||||
|
fromFn := TimeToString
|
||||||
|
toFn := timeutil.StringTimeToTime
|
||||||
|
|
||||||
|
return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeTimestamppbConverterPair() []copier.TypeConverter {
|
||||||
|
srcType := &time.Time{}
|
||||||
|
dstType := ×tamppb.Timestamp{}
|
||||||
|
|
||||||
|
fromFn := timeutil.TimeToTimestamppb
|
||||||
|
toFn := timeutil.TimestamppbToTime
|
||||||
|
|
||||||
|
return NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTypeConverter(srcType, dstType interface{}, fn func(src interface{}) (interface{}, error)) copier.TypeConverter {
|
||||||
return copier.TypeConverter{
|
return copier.TypeConverter{
|
||||||
SrcType: srcType,
|
SrcType: srcType,
|
||||||
DstType: dstType,
|
DstType: dstType,
|
||||||
@@ -50,7 +74,7 @@ func MakeTypeConverter(srcType, dstType interface{}, fn func(src interface{}) (i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeTypeConverterPair(srcType, dstType interface{}, fromFn, toFn func(src interface{}) (interface{}, error)) []copier.TypeConverter {
|
func NewTypeConverterPair(srcType, dstType interface{}, fromFn, toFn func(src interface{}) (interface{}, error)) []copier.TypeConverter {
|
||||||
return []copier.TypeConverter{
|
return []copier.TypeConverter{
|
||||||
{
|
{
|
||||||
SrcType: srcType,
|
SrcType: srcType,
|
||||||
@@ -65,7 +89,7 @@ func MakeTypeConverterPair(srcType, dstType interface{}, fromFn, toFn func(src i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) B, toFn func(src B) A) []copier.TypeConverter {
|
func NewGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) B, toFn func(src B) A) []copier.TypeConverter {
|
||||||
return []copier.TypeConverter{
|
return []copier.TypeConverter{
|
||||||
{
|
{
|
||||||
SrcType: srcType,
|
SrcType: srcType,
|
||||||
@@ -84,7 +108,7 @@ func MakeGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstTy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeErrorHandlingTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) (B, error), toFn func(src B) (A, error)) []copier.TypeConverter {
|
func NewErrorHandlingGenericTypeConverterPair[A interface{}, B interface{}](srcType A, dstType B, fromFn func(src A) (B, error), toFn func(src B) (A, error)) []copier.TypeConverter {
|
||||||
return []copier.TypeConverter{
|
return []copier.TypeConverter{
|
||||||
{
|
{
|
||||||
SrcType: srcType,
|
SrcType: srcType,
|
||||||
|
|||||||
@@ -4,35 +4,33 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/tx7do/go-utils/timeutil"
|
"github.com/tx7do/go-utils/timeutil"
|
||||||
"github.com/tx7do/go-utils/trans"
|
"github.com/tx7do/go-utils/trans"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMakeTypeConverter(t *testing.T) {
|
func TestNewTypeConverter(t *testing.T) {
|
||||||
srcType := &time.Time{}
|
srcType := &time.Time{}
|
||||||
dstType := trans.Ptr("")
|
dstType := trans.Ptr("")
|
||||||
fn := func(src interface{}) (interface{}, error) {
|
fn := func(src interface{}) (interface{}, error) {
|
||||||
return timeutil.TimeToTimeString(src.(*time.Time)), nil
|
return timeutil.TimeToTimeString(src.(*time.Time)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
converter := MakeTypeConverter(srcType, dstType, fn)
|
converter := NewTypeConverter(srcType, dstType, fn)
|
||||||
|
|
||||||
// 验证转换器的类型
|
// 验证转换器的类型
|
||||||
if converter.SrcType != srcType || converter.DstType != dstType {
|
assert.IsType(t, srcType, converter.SrcType)
|
||||||
t.Errorf("converter types mismatch")
|
assert.IsType(t, dstType, converter.DstType)
|
||||||
}
|
|
||||||
|
|
||||||
// 验证转换器的功能
|
// 验证转换器的功能
|
||||||
result, err := converter.Fn(&time.Time{})
|
result, err := converter.Fn(&time.Time{})
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("converter function failed: %v", err)
|
assert.IsType(t, dstType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*string); !ok {
|
|
||||||
t.Errorf("converter result type mismatch")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeTypeConverterPair(t *testing.T) {
|
func TestNewTypeConverterPair(t *testing.T) {
|
||||||
srcType := &time.Time{}
|
srcType := &time.Time{}
|
||||||
dstType := trans.Ptr("")
|
dstType := trans.Ptr("")
|
||||||
fromFn := func(src interface{}) (interface{}, error) {
|
fromFn := func(src interface{}) (interface{}, error) {
|
||||||
@@ -42,75 +40,49 @@ func TestMakeTypeConverterPair(t *testing.T) {
|
|||||||
return timeutil.StringTimeToTime(src.(*string)), nil
|
return timeutil.StringTimeToTime(src.(*string)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
converters := MakeTypeConverterPair(srcType, dstType, fromFn, toFn)
|
converters := NewTypeConverterPair(srcType, dstType, fromFn, toFn)
|
||||||
|
assert.Len(t, converters, 2, "expected 2 converters")
|
||||||
if len(converters) != 2 {
|
|
||||||
t.Fatalf("expected 2 converters, got %d", len(converters))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第一个转换器
|
// 验证第一个转换器
|
||||||
if converters[0].SrcType != srcType || converters[0].DstType != dstType {
|
assert.IsType(t, srcType, converters[0].SrcType)
|
||||||
t.Errorf("first converter types mismatch")
|
assert.IsType(t, dstType, converters[0].DstType)
|
||||||
}
|
|
||||||
result, err := converters[0].Fn(&time.Time{})
|
result, err := converters[0].Fn(&time.Time{})
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("first converter function failed: %v", err)
|
assert.IsType(t, dstType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*string); !ok {
|
|
||||||
t.Errorf("first converter result type mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第二个转换器
|
// 验证第二个转换器
|
||||||
if converters[1].SrcType != dstType || converters[1].DstType != srcType {
|
assert.IsType(t, dstType, converters[1].SrcType)
|
||||||
t.Errorf("second converter types mismatch")
|
assert.IsType(t, srcType, converters[1].DstType)
|
||||||
}
|
|
||||||
result, err = converters[1].Fn(trans.Ptr(""))
|
result, err = converters[1].Fn(trans.Ptr(""))
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("second converter function failed: %v", err)
|
assert.IsType(t, srcType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*time.Time); !ok {
|
|
||||||
t.Errorf("second converter result type mismatch")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeGenericTypeConverterPair(t *testing.T) {
|
func TestNewGenericTypeConverterPair(t *testing.T) {
|
||||||
srcType := &time.Time{}
|
srcType := &time.Time{}
|
||||||
dstType := trans.Ptr("")
|
dstType := trans.Ptr("")
|
||||||
fromFn := timeutil.TimeToTimeString
|
fromFn := timeutil.TimeToTimeString
|
||||||
toFn := timeutil.StringTimeToTime
|
toFn := timeutil.StringTimeToTime
|
||||||
|
|
||||||
converters := MakeGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
|
converters := NewGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
|
||||||
|
assert.Len(t, converters, 2, "expected 2 converters")
|
||||||
if len(converters) != 2 {
|
|
||||||
t.Fatalf("expected 2 converters, got %d", len(converters))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第一个转换器
|
// 验证第一个转换器
|
||||||
if converters[0].SrcType != srcType || converters[0].DstType != dstType {
|
assert.IsType(t, srcType, converters[0].SrcType)
|
||||||
t.Errorf("first converter types mismatch")
|
assert.IsType(t, dstType, converters[0].DstType)
|
||||||
}
|
|
||||||
result, err := converters[0].Fn(&time.Time{})
|
result, err := converters[0].Fn(&time.Time{})
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("first converter function failed: %v", err)
|
assert.IsType(t, dstType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*string); !ok {
|
|
||||||
t.Errorf("first converter result type mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第二个转换器
|
// 验证第二个转换器
|
||||||
if converters[1].SrcType != dstType || converters[1].DstType != srcType {
|
assert.IsType(t, dstType, converters[1].SrcType)
|
||||||
t.Errorf("second converter types mismatch")
|
assert.IsType(t, srcType, converters[1].DstType)
|
||||||
}
|
|
||||||
result, err = converters[1].Fn(trans.Ptr(""))
|
result, err = converters[1].Fn(trans.Ptr(""))
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("second converter function failed: %v", err)
|
assert.IsType(t, srcType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*time.Time); !ok {
|
|
||||||
t.Errorf("second converter result type mismatch")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMakeErrorHandlingTypeConverterPair(t *testing.T) {
|
func TestNewErrorHandlingGenericTypeConverterPair(t *testing.T) {
|
||||||
srcType := &time.Time{}
|
srcType := &time.Time{}
|
||||||
dstType := trans.Ptr("")
|
dstType := trans.Ptr("")
|
||||||
fromFn := func(src *time.Time) (*string, error) {
|
fromFn := func(src *time.Time) (*string, error) {
|
||||||
@@ -120,33 +92,62 @@ func TestMakeErrorHandlingTypeConverterPair(t *testing.T) {
|
|||||||
return timeutil.StringTimeToTime(src), nil
|
return timeutil.StringTimeToTime(src), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
converters := MakeErrorHandlingTypeConverterPair(srcType, dstType, fromFn, toFn)
|
converters := NewErrorHandlingGenericTypeConverterPair(srcType, dstType, fromFn, toFn)
|
||||||
|
assert.Len(t, converters, 2, "expected 2 converters")
|
||||||
if len(converters) != 2 {
|
|
||||||
t.Fatalf("expected 2 converters, got %d", len(converters))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第一个转换器
|
// 验证第一个转换器
|
||||||
if converters[0].SrcType != srcType || converters[0].DstType != dstType {
|
assert.IsType(t, srcType, converters[0].SrcType)
|
||||||
t.Errorf("first converter types mismatch")
|
assert.IsType(t, dstType, converters[0].DstType)
|
||||||
}
|
|
||||||
result, err := converters[0].Fn(&time.Time{})
|
result, err := converters[0].Fn(&time.Time{})
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("first converter function failed: %v", err)
|
assert.IsType(t, dstType, result)
|
||||||
}
|
|
||||||
if _, ok := result.(*string); !ok {
|
|
||||||
t.Errorf("first converter result type mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证第二个转换器
|
// 验证第二个转换器
|
||||||
if converters[1].SrcType != dstType || converters[1].DstType != srcType {
|
assert.IsType(t, dstType, converters[1].SrcType)
|
||||||
t.Errorf("second converter types mismatch")
|
assert.IsType(t, srcType, converters[1].DstType)
|
||||||
}
|
|
||||||
result, err = converters[1].Fn(trans.Ptr(""))
|
result, err = converters[1].Fn(trans.Ptr(""))
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Errorf("second converter function failed: %v", err)
|
assert.IsType(t, srcType, result)
|
||||||
}
|
}
|
||||||
if _, ok := result.(*time.Time); !ok {
|
|
||||||
t.Errorf("second converter result type mismatch")
|
func TestNewTimeStringConverterPair(t *testing.T) {
|
||||||
}
|
converters := NewTimeStringConverterPair()
|
||||||
|
assert.Len(t, converters, 2, "expected 2 converters")
|
||||||
|
|
||||||
|
// 验证第一个转换器
|
||||||
|
srcType := &time.Time{}
|
||||||
|
dstType := trans.Ptr("")
|
||||||
|
assert.IsType(t, srcType, converters[0].SrcType)
|
||||||
|
assert.IsType(t, dstType, converters[0].DstType)
|
||||||
|
result, err := converters[0].Fn(&time.Time{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, dstType, result)
|
||||||
|
|
||||||
|
// 验证第二个转换器
|
||||||
|
assert.IsType(t, dstType, converters[1].SrcType)
|
||||||
|
assert.IsType(t, srcType, converters[1].DstType)
|
||||||
|
result, err = converters[1].Fn(trans.Ptr(""))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, srcType, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTimeTimestamppbConverterPair(t *testing.T) {
|
||||||
|
converters := NewTimeTimestamppbConverterPair()
|
||||||
|
assert.Len(t, converters, 2, "expected 2 converters")
|
||||||
|
|
||||||
|
// 验证第一个转换器
|
||||||
|
srcType := &time.Time{}
|
||||||
|
dstType := ×tamppb.Timestamp{}
|
||||||
|
assert.IsType(t, srcType, converters[0].SrcType)
|
||||||
|
assert.IsType(t, dstType, converters[0].DstType)
|
||||||
|
result, err := converters[0].Fn(&time.Time{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, dstType, result)
|
||||||
|
|
||||||
|
// 验证第二个转换器
|
||||||
|
assert.IsType(t, dstType, converters[1].SrcType)
|
||||||
|
assert.IsType(t, srcType, converters[1].DstType)
|
||||||
|
result, err = converters[1].Fn(×tamppb.Timestamp{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, srcType, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,19 @@ toolchain go1.24.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/tx7do/go-utils v1.1.22
|
github.com/tx7do/go-utils v1.1.27
|
||||||
)
|
)
|
||||||
|
|
||||||
require google.golang.org/protobuf v1.36.6
|
require (
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
google.golang.org/protobuf v1.36.6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
replace github.com/tx7do/go-utils => ../
|
replace github.com/tx7do/go-utils => ../
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/go-kratos/kratos/v2 v2.8.4
|
github.com/go-kratos/kratos/v2 v2.8.4
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/tx7do/go-utils v1.1.22
|
github.com/tx7do/go-utils v1.1.27
|
||||||
go.opentelemetry.io/otel v1.36.0
|
go.opentelemetry.io/otel v1.36.0
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
)
|
)
|
||||||
@@ -23,7 +23,7 @@ require (
|
|||||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/inflect v0.21.2 // indirect
|
github.com/go-openapi/inflect v0.21.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYq
|
|||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM=
|
github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM=
|
||||||
|
|||||||
21
jwtutil/go.mod
Normal file
21
jwtutil/go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module github.com/tx7do/go-utils/jwtutil
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-kratos/kratos/v2 v2.8.4
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.1.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/tx7do/go-utils => ../
|
||||||
28
jwtutil/go.sum
Normal file
28
jwtutil/go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
|
||||||
|
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
355
jwtutil/jwt.go
Normal file
355
jwtutil/jwt.go
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
package jwtutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-kratos/kratos/v2/encoding"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseJWTPayload 使用 github.com/golang-jwt/jwt/v5 从 JWT 中解析出 payload
|
||||||
|
func ParseJWTPayload(tokenString string) (jwt.MapClaims, error) {
|
||||||
|
// 不验证签名,仅解析
|
||||||
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
|
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWTClaimsToStruct 解析 JWT 的负载部分,不验证签名,仅解析。
|
||||||
|
func ParseJWTClaimsToStruct[T any](tokenString string) (*T, error) {
|
||||||
|
// 不验证签名,仅解析
|
||||||
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
|
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
codec := encoding.GetCodec("json")
|
||||||
|
|
||||||
|
// 将 claims 转换为目标类型
|
||||||
|
claimsBytes, err := codec.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal claims: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret T
|
||||||
|
err = codec.Unmarshal(claimsBytes, &ret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal claims: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyJWT 验证 JWT 的签名
|
||||||
|
func VerifyJWT(tokenString string, secretKey []byte) (jwt.MapClaims, error) {
|
||||||
|
// 解析并验证 JWT
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
// 确保使用的是 HMAC 签名方法
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return secretKey, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 token 是否有效
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJWTClaims(tokenString string) (map[string]interface{}, error) {
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWT 生成 JWT
|
||||||
|
func GenerateJWT(mapClaims jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod) (string, error) {
|
||||||
|
// 检查密钥是否为空,密钥不能为空,否则会有安全隐患。
|
||||||
|
if len(secretKey) == 0 {
|
||||||
|
return "", fmt.Errorf("secret key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的 JWT Token
|
||||||
|
token := jwt.NewWithClaims(signingMethod, mapClaims)
|
||||||
|
|
||||||
|
// 使用 HMAC 签名方法签名 token
|
||||||
|
signedToken, err := token.SignedString(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateGenericJWT 生成 JWT
|
||||||
|
func GenerateGenericJWT[T any](payload T, secretKey []byte, signingMethod jwt.SigningMethod) (string, error) {
|
||||||
|
// 检查密钥是否为空,密钥不能为空,否则会有安全隐患。
|
||||||
|
if len(secretKey) == 0 {
|
||||||
|
return "", fmt.Errorf("secret key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将泛型 payload 转换为 jwt.MapClaims
|
||||||
|
claimsMap, err := ToMapClaims(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to convert payload to claims: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的 JWT Token
|
||||||
|
token := jwt.NewWithClaims(signingMethod, claimsMap)
|
||||||
|
|
||||||
|
// 使用 HMAC 签名方法签名 token
|
||||||
|
signedToken, err := token.SignedString(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMapClaims 将泛型 payload 转换为 jwt.MapClaims
|
||||||
|
func ToMapClaims[T any](payload T) (jwt.MapClaims, error) {
|
||||||
|
codec := encoding.GetCodec("json")
|
||||||
|
|
||||||
|
payloadBytes, err := codec.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims jwt.MapClaims
|
||||||
|
err = codec.Unmarshal(payloadBytes, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshJWT 刷新JWT
|
||||||
|
func RefreshJWT(tokenString string, secretKey []byte, newExpiration time.Time) (string, error) {
|
||||||
|
// 解析 JWT 的 payload
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新过期时间
|
||||||
|
claims["exp"] = newExpiration.Unix()
|
||||||
|
|
||||||
|
// 生成新的 JWT
|
||||||
|
newToken, err := GenerateJWT(claims, secretKey, jwt.SigningMethodHS256)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWTWithHeader 生成带自定义头部的JWT
|
||||||
|
func GenerateJWTWithHeader(payload jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod, customHeader map[string]interface{}) (string, error) {
|
||||||
|
// 检查密钥是否为空
|
||||||
|
if len(secretKey) == 0 {
|
||||||
|
return "", fmt.Errorf("secret key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的 JWT Token
|
||||||
|
token := jwt.NewWithClaims(signingMethod, payload)
|
||||||
|
|
||||||
|
// 添加自定义头部
|
||||||
|
for key, value := range customHeader {
|
||||||
|
token.Header[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用密钥签名生成 JWT
|
||||||
|
signedToken, err := token.SignedString(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractJWTFromRequest 从请求中提取JWT
|
||||||
|
func ExtractJWTFromRequest(r *http.Request) (string, error) {
|
||||||
|
// 从 Authorization Header 中提取 JWT
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return "", fmt.Errorf("authorization header is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否以 "Bearer " 开头
|
||||||
|
const bearerPrefix = "Bearer "
|
||||||
|
if len(authHeader) <= len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
|
||||||
|
return "", fmt.Errorf("invalid authorization header format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 JWT 部分
|
||||||
|
token := authHeader[len(bearerPrefix):]
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateShortLivedJWT 生成短期有效的JWT
|
||||||
|
func GenerateShortLivedJWT(payload jwt.MapClaims, secretKey []byte, signingMethod jwt.SigningMethod, duration time.Duration) (string, error) {
|
||||||
|
// 检查密钥是否为空
|
||||||
|
if len(secretKey) == 0 {
|
||||||
|
return "", fmt.Errorf("secret key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
expirationTime := time.Now().Add(duration).Unix()
|
||||||
|
payload["exp"] = expirationTime
|
||||||
|
|
||||||
|
// 创建一个新的 JWT Token
|
||||||
|
token := jwt.NewWithClaims(signingMethod, payload)
|
||||||
|
|
||||||
|
// 使用密钥签名生成 JWT
|
||||||
|
signedToken, err := token.SignedString(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWTAudience 验证JWT受众
|
||||||
|
func ValidateJWTAudience(tokenString string, expectedAudience string) (bool, error) {
|
||||||
|
// 解析 JWT 的 payload
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 `aud` 字段
|
||||||
|
audience, err := claims.GetAudience()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 `aud` 是否包含预期值
|
||||||
|
for _, aud := range audience {
|
||||||
|
if aud == expectedAudience {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWTAlgorithm 验证JWT算法
|
||||||
|
func ValidateJWTAlgorithm(tokenString string, expectedAlgorithm string) (bool, error) {
|
||||||
|
// 使用 jwt.Parser 解析 JWT,不验证签名
|
||||||
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
|
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 `alg` 字段
|
||||||
|
alg, ok := token.Header["alg"].(string)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("invalid or missing algorithm in token header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证算法是否符合预期
|
||||||
|
if alg != expectedAlgorithm {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsJWTExpired 检查JWT是否过期
|
||||||
|
func IsJWTExpired(tokenString string) (bool, error) {
|
||||||
|
// 解析 JWT 的 payload
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 `exp` 字段
|
||||||
|
exp, err := claims.GetExpirationTime()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前时间是否超过 `exp`
|
||||||
|
if time.Now().After(exp.Time) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJWTHeader 获取JWT头部
|
||||||
|
func GetJWTHeader(tokenString string) (map[string]interface{}, error) {
|
||||||
|
// 使用 jwt.Parser 解析 JWT,不验证签名
|
||||||
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
|
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.Header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWTIssuer 验证JWT发行者
|
||||||
|
func ValidateJWTIssuer(tokenString string, expectedIssuer string) (bool, error) {
|
||||||
|
// 解析 JWT 的 payload
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 `iss` 字段
|
||||||
|
issuer, err := claims.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 `iss` 是否符合预期
|
||||||
|
if issuer != expectedIssuer {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJWTIssuedAt 获取JWT签发时间
|
||||||
|
func GetJWTIssuedAt(tokenString string) (*time.Time, error) {
|
||||||
|
claims, err := ParseJWTPayload(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iat, err := claims.GetIssuedAt()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iat.Time, nil
|
||||||
|
}
|
||||||
519
jwtutil/jwt_test.go
Normal file
519
jwtutil/jwt_test.go
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
package jwtutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
_ "github.com/go-kratos/kratos/v2/encoding/json"
|
||||||
|
_ "github.com/go-kratos/kratos/v2/encoding/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseJWTPayload(t *testing.T) {
|
||||||
|
var secretKey = []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的 JWT 负载解析
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": 1672728000,
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseJWTPayload(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, "userId", result["sub"])
|
||||||
|
assert.Equal(t, float64(1672728000), result["exp"]) // 注意:JSON 解码后数字为 float64
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
result, err = ParseJWTPayload(invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
result, err = ParseJWTPayload("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseJWTClaimsToStruct(t *testing.T) {
|
||||||
|
// 测试有效的 JWT 负载解析
|
||||||
|
type Payload struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = []byte("secret")
|
||||||
|
|
||||||
|
// 验证签名以生成有效的 token
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": 1672728000,
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseJWTClaimsToStruct[Payload](token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, "userId", result.Sub)
|
||||||
|
assert.Equal(t, int64(1672728000), result.Exp)
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
result, err = ParseJWTClaimsToStruct[Payload](invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
result, err = ParseJWTClaimsToStruct[Payload]("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyJWT(t *testing.T) {
|
||||||
|
// 测试有效的 JWT 验证
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := VerifyJWT(token, secretKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, "userId", result["sub"])
|
||||||
|
|
||||||
|
// 测试无效的签名
|
||||||
|
invalidSecretKey := []byte("invalid_secret")
|
||||||
|
result, err = VerifyJWT(token, invalidSecretKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试无效的 JWT 格式
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
result, err = VerifyJWT(invalidToken, secretKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
result, err = VerifyJWT("", secretKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateGenericJWT(t *testing.T) {
|
||||||
|
// 定义测试用的 payload 和密钥
|
||||||
|
type Payload struct {
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
payload := Payload{
|
||||||
|
Sub: "userId",
|
||||||
|
Exp: 1672728000,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试生成有效的 JWT
|
||||||
|
token, err := GenerateGenericJWT(payload, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// 验证生成的 JWT 是否正确
|
||||||
|
parsedPayload, err := ParseJWTClaimsToStruct[Payload](token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, parsedPayload)
|
||||||
|
assert.Equal(t, payload.Sub, parsedPayload.Sub)
|
||||||
|
assert.Equal(t, payload.Exp, parsedPayload.Exp)
|
||||||
|
|
||||||
|
// 测试使用空密钥生成 JWT
|
||||||
|
emptySecretKey := []byte("")
|
||||||
|
token, err = GenerateGenericJWT(payload, emptySecretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJWT(t *testing.T) {
|
||||||
|
// 定义测试用的 payload 和密钥
|
||||||
|
payload := jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": 1672728000,
|
||||||
|
}
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试生成有效的 JWT
|
||||||
|
token, err := GenerateJWT(payload, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// 验证生成的 JWT 是否正确
|
||||||
|
parsedPayload, err := ParseJWTPayload(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, parsedPayload)
|
||||||
|
assert.Equal(t, payload["sub"], parsedPayload["sub"])
|
||||||
|
assert.Equal(t, float64(payload["exp"].(int)), parsedPayload["exp"].(float64)) // 注意:JSON 解码后数字为 float64
|
||||||
|
|
||||||
|
// 测试使用空密钥生成 JWT
|
||||||
|
emptySecretKey := []byte("")
|
||||||
|
token, err = GenerateJWT(payload, emptySecretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshJWT(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 创建一个初始的 JWT
|
||||||
|
originalToken, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新 JWT,设置新的过期时间
|
||||||
|
newExpiration := time.Now().Add(2 * time.Hour)
|
||||||
|
refreshedToken, err := RefreshJWT(originalToken, secretKey, newExpiration)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, refreshedToken)
|
||||||
|
|
||||||
|
// 验证刷新后的 JWT 是否包含新的过期时间
|
||||||
|
claims, err := ParseJWTPayload(refreshedToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, claims)
|
||||||
|
|
||||||
|
exp, ok := claims["exp"].(float64)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, newExpiration.Unix(), int64(exp))
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
_, err = RefreshJWT(invalidToken, secretKey, newExpiration)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
_, err = RefreshJWT("", secretKey, newExpiration)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJWTWithHeader(t *testing.T) {
|
||||||
|
// 定义测试用的 payload、密钥和自定义头部
|
||||||
|
payload := jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
|
}
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
customHeader := map[string]interface{}{
|
||||||
|
"kid": "key-id-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试生成有效的 JWT
|
||||||
|
token, err := GenerateJWTWithHeader(payload, secretKey, jwt.SigningMethodHS256, customHeader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// 验证生成的 JWT 是否包含自定义头部
|
||||||
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
|
parsedToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, parsedToken)
|
||||||
|
|
||||||
|
// 检查头部是否包含自定义字段
|
||||||
|
assert.Equal(t, "key-id-123", parsedToken.Header["kid"])
|
||||||
|
|
||||||
|
// 测试使用空密钥生成 JWT
|
||||||
|
emptySecretKey := []byte("")
|
||||||
|
token, err = GenerateJWTWithHeader(payload, emptySecretKey, jwt.SigningMethodHS256, customHeader)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractJWTFromRequest(t *testing.T) {
|
||||||
|
// 测试有效的 Authorization Header
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer valid.jwt.token")
|
||||||
|
token, err := ExtractJWTFromRequest(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "valid.jwt.token", token)
|
||||||
|
|
||||||
|
// 测试缺少 Authorization Header
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
token, err = ExtractJWTFromRequest(req)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, "authorization header is missing", err.Error())
|
||||||
|
assert.Empty(t, token)
|
||||||
|
|
||||||
|
// 测试无效的 Authorization Header 格式
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "InvalidFormat")
|
||||||
|
token, err = ExtractJWTFromRequest(req)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, "invalid authorization header format", err.Error())
|
||||||
|
assert.Empty(t, token)
|
||||||
|
|
||||||
|
// 测试空的 Bearer Token
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer ")
|
||||||
|
token, err = ExtractJWTFromRequest(req)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateShortLivedJWT(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
payload := jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试生成短期有效的 JWT
|
||||||
|
duration := 1 * time.Hour
|
||||||
|
token, err := GenerateShortLivedJWT(payload, secretKey, jwt.SigningMethodHS256, duration)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// 验证生成的 JWT 是否包含正确的过期时间
|
||||||
|
claims, err := ParseJWTPayload(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, claims)
|
||||||
|
|
||||||
|
exp, ok := claims["exp"].(float64)
|
||||||
|
assert.True(t, ok)
|
||||||
|
expectedExp := time.Now().Add(duration).Unix()
|
||||||
|
assert.InDelta(t, expectedExp, int64(exp), 5) // 允许 5 秒的时间误差
|
||||||
|
|
||||||
|
// 测试空密钥
|
||||||
|
emptySecretKey := []byte("")
|
||||||
|
token, err = GenerateShortLivedJWT(payload, emptySecretKey, jwt.SigningMethodHS256, duration)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateJWTAudience(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 创建一个包含受众的 JWT
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"aud": []string{"audience1", "audience2"},
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// 测试受众验证成功
|
||||||
|
valid, err := ValidateJWTAudience(token, "audience1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, valid)
|
||||||
|
|
||||||
|
// 测试受众验证失败
|
||||||
|
valid, err = ValidateJWTAudience(token, "audience3")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
valid, err = ValidateJWTAudience(invalidToken, "audience1")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
valid, err = ValidateJWTAudience("", "audience1")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateJWTAlgorithm(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的 JWT 算法验证
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
valid, err := ValidateJWTAlgorithm(token, "HS256")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, valid)
|
||||||
|
|
||||||
|
// 测试无效的算法
|
||||||
|
valid, err = ValidateJWTAlgorithm(token, "RS256")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
valid, err = ValidateJWTAlgorithm(invalidToken, "HS256")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
valid, err = ValidateJWTAlgorithm("", "HS256")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsJWTExpired(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试未过期的 JWT
|
||||||
|
notExpiredToken, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, notExpiredToken)
|
||||||
|
|
||||||
|
isExpired, err := IsJWTExpired(notExpiredToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isExpired)
|
||||||
|
|
||||||
|
// 测试已过期的 JWT
|
||||||
|
expiredToken, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": time.Now().Add(-1 * time.Hour).Unix(),
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, expiredToken)
|
||||||
|
|
||||||
|
isExpired, err = IsJWTExpired(expiredToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isExpired)
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
isExpired, err = IsJWTExpired(invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, isExpired)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
isExpired, err = IsJWTExpired("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, isExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJWTHeader(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的 JWT 头部解析
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
header, err := GetJWTHeader(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, header)
|
||||||
|
assert.Equal(t, "JWT", header["typ"])
|
||||||
|
assert.Equal(t, "HS256", header["alg"])
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
header, err = GetJWTHeader(invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, header)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
header, err = GetJWTHeader("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateJWTIssuer(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的发行者验证
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"iss": "trusted-issuer",
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
valid, err := ValidateJWTIssuer(token, "trusted-issuer")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, valid)
|
||||||
|
|
||||||
|
// 测试无效的发行者
|
||||||
|
valid, err = ValidateJWTIssuer(token, "untrusted-issuer")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
valid, err = ValidateJWTIssuer(invalidToken, "trusted-issuer")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
valid, err = ValidateJWTIssuer("", "trusted-issuer")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJWTIssuedAt(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的 JWT 签发时间
|
||||||
|
issuedAt := time.Now().Add(-1 * time.Hour).Unix()
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"iat": issuedAt,
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
iat, err := GetJWTIssuedAt(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, iat)
|
||||||
|
assert.Equal(t, issuedAt, iat.Unix())
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
iat, err = GetJWTIssuedAt(invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, iat)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
iat, err = GetJWTIssuedAt("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, iat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJWTClaims(t *testing.T) {
|
||||||
|
secretKey := []byte("secret")
|
||||||
|
|
||||||
|
// 测试有效的 JWT
|
||||||
|
token, err := GenerateJWT(jwt.MapClaims{
|
||||||
|
"sub": "userId",
|
||||||
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
|
}, secretKey, jwt.SigningMethodHS256)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, token)
|
||||||
|
|
||||||
|
claims, err := GetJWTClaims(token)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, claims)
|
||||||
|
assert.Equal(t, "userId", claims["sub"])
|
||||||
|
|
||||||
|
// 测试无效的 JWT
|
||||||
|
invalidToken := "invalid.token.string"
|
||||||
|
claims, err = GetJWTClaims(invalidToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, claims)
|
||||||
|
|
||||||
|
// 测试空字符串
|
||||||
|
claims, err = GetJWTClaims("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, claims)
|
||||||
|
}
|
||||||
7
tag.bat
7
tag.bat
@@ -1,11 +1,12 @@
|
|||||||
git tag v1.1.22
|
git tag v1.1.27
|
||||||
|
|
||||||
git tag bank_card/v1.1.5
|
git tag bank_card/v1.1.5
|
||||||
git tag geoip/v1.1.5
|
git tag geoip/v1.1.5
|
||||||
git tag translator/v1.1.2
|
git tag translator/v1.1.2
|
||||||
git tag copierutil/v0.0.2
|
git tag copierutil/v0.0.5
|
||||||
|
git tag jwtutil/v0.0.2
|
||||||
|
|
||||||
git tag entgo/v1.1.28
|
git tag entgo/v1.1.30
|
||||||
git tag gorm/v1.1.6
|
git tag gorm/v1.1.6
|
||||||
|
|
||||||
git push origin --tags
|
git push origin --tags
|
||||||
|
|||||||
@@ -10,4 +10,52 @@ const (
|
|||||||
DefaultTimeLocationName = "Asia/Shanghai"
|
DefaultTimeLocationName = "Asia/Shanghai"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// More predefined layouts for use in Time.Format and time.Parse.
|
||||||
|
const (
|
||||||
|
DT14 = "20060102150405"
|
||||||
|
DT8 = "20060102"
|
||||||
|
DT8MDY = "01022006"
|
||||||
|
DT6 = "200601"
|
||||||
|
MonthDay = "1/2"
|
||||||
|
DIN5008FullDate = "02.01.2006" // German DIN 5008 standard
|
||||||
|
DIN5008Date = "02.01.06"
|
||||||
|
RFC3339FullDate = time.DateOnly
|
||||||
|
RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
|
||||||
|
RFC3339Dash = "2006-01-02T15-04-05Z07-00"
|
||||||
|
ISO8601 = "2006-01-02T15:04:05Z0700"
|
||||||
|
ISO8601TZHour = "2006-01-02T15:04:05Z07"
|
||||||
|
ISO8601NoTZ = "2006-01-02T15:04:05"
|
||||||
|
ISO8601MilliNoTZ = "2006-01-02T15:04:05.999"
|
||||||
|
ISO8601Milli = "2006-01-02T15:04:05.999Z0700"
|
||||||
|
ISO8601CompactZ = "20060102T150405Z0700"
|
||||||
|
ISO8601CompactNoTZ = "20060102T150405"
|
||||||
|
ISO8601YM = "2006-01"
|
||||||
|
ISO9075 = time.DateTime // ISO/IEC 9075 used by MySQL, BigQuery, etc.
|
||||||
|
ISO9075MicroTZ = "2006-01-02 15:04:05.999999-07" // ISO/IEC 9075 used by PostgreSQL
|
||||||
|
RFC5322 = "Mon, 2 Jan 2006 15:04:05 -0700" // RFC5322 = "Mon Jan 02 15:04:05 -0700 2006"
|
||||||
|
SQLTimestamp = ISO9075
|
||||||
|
SQLTimestampMinutes = "2006-01-02 15:04"
|
||||||
|
Ruby = "2006-01-02 15:04:05 -0700" // Ruby Time.now.to_s
|
||||||
|
InsightlyAPIQuery = "_1/_2/2006 _3:04:05 PM"
|
||||||
|
DateMDY = "1/2/2006" // an underscore results in a space.
|
||||||
|
DateMDYSlash = "01/02/2006"
|
||||||
|
DateDMYDash = "_2-01-2006" // Jira XML Date format
|
||||||
|
DateDMYHM2 = "02:01:06 15:04" // GMT time in format dd:mm:yy hh:mm
|
||||||
|
DateYMD = RFC3339FullDate
|
||||||
|
DateTextUS = "January 2, 2006"
|
||||||
|
DateTextUSAbbr3 = "Jan 2, 2006"
|
||||||
|
DateTextEU = "2 January 2006"
|
||||||
|
DateTextEUAbbr3 = "2 Jan 2006"
|
||||||
|
MonthAbbrYear = "Jan 2006"
|
||||||
|
MonthYear = "January 2006"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RFC3339Min = "0000-01-01T00:00:00Z"
|
||||||
|
RFC3339Max = "9999-12-31T23:59:59Z"
|
||||||
|
RFC3339Zero = "0001-01-01T00:00:00Z" // Golang zero value
|
||||||
|
RFC3339ZeroUnix = "1970-01-01T00:00:00Z"
|
||||||
|
RFC3339YMDZeroUnix = int64(-62135596800)
|
||||||
|
)
|
||||||
|
|
||||||
var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60))
|
var ReferenceTimeValue time.Time = time.Date(2006, 1, 2, 15, 4, 5, 999999999, time.FixedZone("MST", -7*60*60))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package timeutil
|
package timeutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -43,3 +45,291 @@ func DurationHMS(d time.Duration) (int, int, int) {
|
|||||||
|
|
||||||
return int(h), int(m), int(s)
|
return int(h), int(m), int(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reformat a time string from one format to another
|
||||||
|
// Deprecated...
|
||||||
|
func FromTo(value, fromLayout, toLayout string) (string, error) {
|
||||||
|
t, err := time.Parse(fromLayout, strings.TrimSpace(value))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return t.Format(toLayout), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reformat a time string from one format to another
|
||||||
|
func FromTo2(fromLayout, toLayout, value string) (string, error) {
|
||||||
|
t, err := time.Parse(fromLayout, strings.TrimSpace(value))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return t.Format(toLayout), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromToFirstValueOrEmpty(fromLayout, toLayout string, values []string) string {
|
||||||
|
dtString, err := FromToFirstValue(fromLayout, toLayout, values)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dtString
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromToFirstValue(fromLayout, toLayout string, values []string) (string, error) {
|
||||||
|
for _, val := range values {
|
||||||
|
dt, err := time.Parse(fromLayout, val)
|
||||||
|
if err == nil {
|
||||||
|
return dt.Format(toLayout), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("no match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFirstValueOrZero(layout string, values []string) time.Time {
|
||||||
|
dt, err := ParseFirstValue(layout, values)
|
||||||
|
if err != nil {
|
||||||
|
return TimeZeroRFC3339()
|
||||||
|
}
|
||||||
|
return dt
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFirstValue(layout string, values []string) (time.Time, error) {
|
||||||
|
for _, val := range values {
|
||||||
|
try, err := time.Parse(layout, val)
|
||||||
|
if err == nil {
|
||||||
|
return try, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numVals := len(values)
|
||||||
|
if numVals == 0 {
|
||||||
|
return time.Now(), errors.New("no time values supplied")
|
||||||
|
}
|
||||||
|
return time.Now(), fmt.Errorf("no valid string of [%v] supplied values", strconv.Itoa(numVals))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOrZero returns a parsed time.Time or the RFC-3339 zero time.
|
||||||
|
func ParseOrZero(layout, value string) time.Time {
|
||||||
|
t, err := time.Parse(layout, value)
|
||||||
|
if err != nil {
|
||||||
|
return TimeZeroRFC3339()
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFirst attempts to parse a string with a set of layouts.
|
||||||
|
func ParseFirst(layouts []string, value string) (time.Time, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) == 0 || len(layouts) == 0 {
|
||||||
|
return time.Now(), fmt.Errorf(
|
||||||
|
"requires value [%v] and at least one layout [%v]", value, strings.Join(layouts, ","))
|
||||||
|
}
|
||||||
|
for _, layout := range layouts {
|
||||||
|
layout = strings.TrimSpace(layout)
|
||||||
|
if len(layout) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dt, err := time.Parse(layout, value); err == nil {
|
||||||
|
return dt, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Now(), fmt.Errorf("cannot parse time [%v] with layouts [%v]",
|
||||||
|
value, strings.Join(layouts, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
var FormatMap = map[string]string{
|
||||||
|
"RFC3339": time.RFC3339,
|
||||||
|
"RFC3339YMD": RFC3339FullDate,
|
||||||
|
"ISO8601YM": ISO8601YM,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFormat(formatName string) (string, error) {
|
||||||
|
format, ok := FormatMap[strings.TrimSpace(formatName)]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("format Not Found: %v", format)
|
||||||
|
}
|
||||||
|
return format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeMinRFC3339() time.Time {
|
||||||
|
t0, _ := time.Parse(time.RFC3339, RFC3339Min)
|
||||||
|
return t0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeZeroRFC3339() time.Time {
|
||||||
|
t0, _ := time.Parse(time.RFC3339, RFC3339Zero)
|
||||||
|
return t0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeZeroUnix() time.Time {
|
||||||
|
t0, _ := time.Parse(time.RFC3339, RFC3339ZeroUnix)
|
||||||
|
return t0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZeroAny(u time.Time) bool {
|
||||||
|
return u.Equal(TimeZeroRFC3339()) ||
|
||||||
|
u.Equal(TimeMinRFC3339()) ||
|
||||||
|
u.Equal(TimeZeroUnix())
|
||||||
|
}
|
||||||
|
|
||||||
|
type RFC3339YMDTime struct{ time.Time }
|
||||||
|
|
||||||
|
type ISO8601NoTzMilliTime struct{ time.Time }
|
||||||
|
|
||||||
|
func (t *RFC3339YMDTime) UnmarshalJSON(buf []byte) error {
|
||||||
|
tt, isNil, err := timeUnmarshalJSON(buf, RFC3339FullDate)
|
||||||
|
if err != nil || isNil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Time = tt
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t RFC3339YMDTime) MarshalJSON() ([]byte, error) {
|
||||||
|
return timeMarshalJSON(t.Time, RFC3339FullDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ISO8601NoTzMilliTime) UnmarshalJSON(buf []byte) error {
|
||||||
|
tt, isNil, err := timeUnmarshalJSON(buf, ISO8601MilliNoTZ)
|
||||||
|
if err != nil || isNil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Time = tt
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ISO8601NoTzMilliTime) MarshalJSON() ([]byte, error) {
|
||||||
|
return timeMarshalJSON(t.Time, ISO8601MilliNoTZ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeUnmarshalJSON(buf []byte, layout string) (time.Time, bool, error) {
|
||||||
|
str := string(buf)
|
||||||
|
isNil := true
|
||||||
|
if str == "null" || str == "\"\"" {
|
||||||
|
return time.Time{}, isNil, nil
|
||||||
|
}
|
||||||
|
tt, err := time.Parse(layout, strings.Trim(str, `"`))
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false, err
|
||||||
|
}
|
||||||
|
return tt, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeMarshalJSON(t time.Time, layout string) ([]byte, error) {
|
||||||
|
return []byte(`"` + t.Format(layout) + `"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSlice(layout string, strings []string) ([]time.Time, error) {
|
||||||
|
var times []time.Time
|
||||||
|
for _, raw := range strings {
|
||||||
|
t, err := time.Parse(layout, raw)
|
||||||
|
if err != nil {
|
||||||
|
return times, err
|
||||||
|
}
|
||||||
|
times = append(times, t)
|
||||||
|
}
|
||||||
|
return times, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTimeMulti formats a `time.Time` object or
|
||||||
|
// an epoch number. It is adapted from `github.com/wcharczuk/go-chart`.
|
||||||
|
func FormatTimeMulti(dateFormat string, v any) string {
|
||||||
|
if typed, isTyped := v.(time.Time); isTyped {
|
||||||
|
return typed.Format(dateFormat)
|
||||||
|
}
|
||||||
|
if typed, isTyped := v.(int64); isTyped {
|
||||||
|
return time.Unix(0, typed).Format(dateFormat)
|
||||||
|
}
|
||||||
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
|
return time.Unix(0, int64(typed)).Format(dateFormat)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatTimeToString(format string) func(time.Time) string {
|
||||||
|
return func(dt time.Time) string {
|
||||||
|
return dt.Format(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OffsetFormat converts an integer offset value to a string
|
||||||
|
// value to use in string time formats. Note: RFC-3339 times
|
||||||
|
// use colons and the UTC "Z" offset.
|
||||||
|
func OffsetFormat(offset int, useColon, useZ bool) string {
|
||||||
|
offsetStr := "+0000"
|
||||||
|
if offset == 0 {
|
||||||
|
if useZ {
|
||||||
|
offsetStr = "Z"
|
||||||
|
} else if useColon {
|
||||||
|
offsetStr = "+00:00"
|
||||||
|
}
|
||||||
|
} else if offset > 0 {
|
||||||
|
if useColon {
|
||||||
|
hr := offset / 100
|
||||||
|
mn := offset - (hr * 100)
|
||||||
|
offsetStr = "+" + fmt.Sprintf("%02d:%02d", hr, mn)
|
||||||
|
} else {
|
||||||
|
offsetStr = "+" + fmt.Sprintf("%04d", offset)
|
||||||
|
}
|
||||||
|
} else if offset < 0 {
|
||||||
|
if useColon {
|
||||||
|
offsetPositive := -1 * offset
|
||||||
|
hr := offsetPositive / 100
|
||||||
|
mn := offsetPositive - (hr * 100)
|
||||||
|
offsetStr = "-" + fmt.Sprintf("%02d:%02d", hr, mn)
|
||||||
|
} else {
|
||||||
|
offsetStr = "-" + fmt.Sprintf("%04d", -1*offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offsetStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// var rxSQLTimestamp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$`)
|
||||||
|
|
||||||
|
func ParseTimeUsingOffset(format, raw, sep string, offset int, useColon, useZ bool) (time.Time, error) {
|
||||||
|
return time.Parse(format, raw+sep+OffsetFormat(offset, useColon, useZ))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTimeSQLTimestampUsingOffset converts a SQL timestamp without timezone
|
||||||
|
// adding in a manual integer timezone.
|
||||||
|
func ParseTimeSQLTimestampUsingOffset(timeStr string, offset int) (time.Time, error) {
|
||||||
|
return ParseTimeUsingOffset(Ruby, timeStr, " ", offset, false, false)
|
||||||
|
/*
|
||||||
|
timeStr = strings.TrimSpace(timeStr)
|
||||||
|
if !rxSQLTimestamp.MatchString(timeStr) {
|
||||||
|
return time.Now(), fmt.Errorf("E_INVALID_SQL_TIMESTAMP [%v]", timeStr)
|
||||||
|
}
|
||||||
|
offsetStr := OffsetFormat(offset, useColon, useZ)
|
||||||
|
timeStr += " " + offsetStr
|
||||||
|
dt, err := time.Parse(Ruby, timeStr)
|
||||||
|
return dt, err
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LayoutNameDT4 = "dt4"
|
||||||
|
LayoutNameDT6 = "dt6"
|
||||||
|
LayoutNameDT8 = "dt8"
|
||||||
|
LayoutNameDT14 = "dt14"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsDTX returns the dtx format if conformant to various DTX values (dt4, dt6, dt8, dt14).
|
||||||
|
func IsDTX(d int) (string, error) {
|
||||||
|
switch len(strconv.Itoa(d)) {
|
||||||
|
case 4:
|
||||||
|
return LayoutNameDT4, nil
|
||||||
|
case 6:
|
||||||
|
m := d - ((d / 100) * 100)
|
||||||
|
if m < 1 || m > 12 {
|
||||||
|
return LayoutNameDT6, errors.New("dt6 length month value is out of bounds")
|
||||||
|
}
|
||||||
|
return LayoutNameDT6, nil
|
||||||
|
case 8:
|
||||||
|
dy := d - ((d / 100) * 100)
|
||||||
|
if dy < 1 || dy > 31 {
|
||||||
|
return LayoutNameDT6, errors.New("dt8 day value is out of bounds")
|
||||||
|
}
|
||||||
|
return LayoutNameDT8, nil
|
||||||
|
case 14:
|
||||||
|
return LayoutNameDT14, nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("length mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
132
timeutil/format_test.go
Normal file
132
timeutil/format_test.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package timeutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dmyhm2ParseTests = []struct {
|
||||||
|
v string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"02:01:06 15:04", "2006-01-02T15:04:00Z"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone.
|
||||||
|
func TestDMYHM2ParseTime(t *testing.T) {
|
||||||
|
for _, tt := range dmyhm2ParseTests {
|
||||||
|
got, err := FromTo(tt.v, DateDMYHM2, time.RFC3339)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("time.Parse(DateDMYHM2) Error: with %v, want %v, err %v", tt.v, tt.want, err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("time.Parse(\"%v\", DateDMYHM2) Mismatch: want %v, got %v", tt.v, tt.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rfc3339YMDTimeTests = []struct {
|
||||||
|
v string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{`{"MyTime":"2001-02-03"}`, `{"MyTime":"2001-02-03"}`},
|
||||||
|
{`{"MyTime":"0001-01-01"}`, `{"MyTime":"0001-01-01"}`},
|
||||||
|
{`{"MyTime":""}`, `{"MyTime":"0001-01-01"}`},
|
||||||
|
{`{}`, `{"MyTime":"0001-01-01"}`}}
|
||||||
|
|
||||||
|
type myStruct struct {
|
||||||
|
MyTime RFC3339YMDTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRfc3339YMDTime(t *testing.T) {
|
||||||
|
for _, tt := range rfc3339YMDTimeTests {
|
||||||
|
my := myStruct{}
|
||||||
|
//fmt.Println(tt.v)
|
||||||
|
err := json.Unmarshal([]byte(tt.v), &my)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Rfc3339YMDTime.Unmarshal: with %v, want %v, err %v", tt.v, tt.want, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(my)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Rfc3339YMDTime.Marshal: with %v, want %v, err %v", tt.v, tt.want, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := string(bytes)
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Rfc3339YMDTime(%v): want %v, got %v", tt.v, tt.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetFormatTests = []struct {
|
||||||
|
input int
|
||||||
|
useColon bool
|
||||||
|
useZ bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, false, false, "+0000"},
|
||||||
|
{0, true, false, "+00:00"},
|
||||||
|
{0, true, true, "Z"},
|
||||||
|
{400, false, false, "+0400"},
|
||||||
|
{-400, false, false, "-0400"},
|
||||||
|
{530, false, false, "+0530"},
|
||||||
|
{-530, false, false, "-0530"},
|
||||||
|
{700, true, false, "+07:00"},
|
||||||
|
{-700, true, false, "-07:00"},
|
||||||
|
{845, true, false, "+08:45"},
|
||||||
|
{-845, true, false, "-08:45"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOffsetFormat(t *testing.T) {
|
||||||
|
for _, tt := range offsetFormatTests {
|
||||||
|
got := OffsetFormat(tt.input, tt.useColon, tt.useZ)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("time.OffsetFormat(\"%v\",%v,%v) Mismatch: want [%v], got [%v]", tt.input, tt.useColon, tt.useZ, tt.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDTXTests = []struct {
|
||||||
|
v int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{2004, LayoutNameDT4},
|
||||||
|
{200401, LayoutNameDT6},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsDTX(t *testing.T) {
|
||||||
|
for _, tt := range isDTXTests {
|
||||||
|
got, err := IsDTX(tt.v)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("time.IsDTX(%d) Error: want (%s), err (%v)", tt.v, tt.want, err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("time.IsDTX(%d) Mismatch: want (%s), got (%s)", tt.v, tt.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatsTests = []struct {
|
||||||
|
v string
|
||||||
|
format string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"2023-06-18T00:00:00Z", DateTextUS, "June 18, 2023"},
|
||||||
|
{"2023-06-18T00:00:00Z", DateTextUSAbbr3, "Jun 18, 2023"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormats(t *testing.T) {
|
||||||
|
for _, tt := range formatsTests {
|
||||||
|
dt, err := time.Parse(time.RFC3339, tt.v)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("time.Parse(time.RFC3339, %s) Error: err (%s)", tt.v, err.Error())
|
||||||
|
}
|
||||||
|
got := dt.Format(tt.format)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("time.Format(%s) dt (%s) Mismatch: want (%s), got (%s)", tt.format, tt.v, tt.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,14 @@ func GetDefaultTimeLocation() *time.Location {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnixMilliToStringPtr 毫秒时间戳 -> 字符串
|
// UnixMilliToStringPtr 毫秒时间戳 -> 字符串
|
||||||
func UnixMilliToStringPtr(tm *int64) *string {
|
func UnixMilliToStringPtr(milli *int64) *string {
|
||||||
if tm == nil {
|
if milli == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
str := time.UnixMilli(*tm).Format(TimeLayout)
|
|
||||||
|
tm := time.UnixMilli(*milli)
|
||||||
|
|
||||||
|
str := tm.In(GetDefaultTimeLocation()).Format(TimeLayout)
|
||||||
return &str
|
return &str
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +47,18 @@ func StringToUnixMilliInt64Ptr(tm *string) *int64 {
|
|||||||
if theTime == nil {
|
if theTime == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unixTime := theTime.UnixMilli()
|
unixTime := theTime.UnixMilli()
|
||||||
return &unixTime
|
return &unixTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnixMilliToTimePtr 毫秒时间戳 -> 时间
|
// UnixMilliToTimePtr 毫秒时间戳 -> 时间
|
||||||
func UnixMilliToTimePtr(tm *int64) *time.Time {
|
func UnixMilliToTimePtr(milli *int64) *time.Time {
|
||||||
if tm == nil {
|
if milli == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unixMilli := time.UnixMilli(*tm)
|
unixMilli := time.UnixMilli(*milli)
|
||||||
return &unixMilli
|
return &unixMilli
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +73,13 @@ func TimeToUnixMilliInt64Ptr(tm *time.Time) *int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnixSecondToTimePtr 秒时间戳 -> 时间
|
// UnixSecondToTimePtr 秒时间戳 -> 时间
|
||||||
func UnixSecondToTimePtr(tm *int64) *time.Time {
|
func UnixSecondToTimePtr(second *int64) *time.Time {
|
||||||
if tm == nil {
|
if second == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unixMilli := time.Unix(*tm, 0)
|
unixMilli := time.Unix(*second, 0)
|
||||||
|
|
||||||
return &unixMilli
|
return &unixMilli
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,18 +105,19 @@ func StringTimeToTime(str *string) *time.Time {
|
|||||||
var err error
|
var err error
|
||||||
var theTime time.Time
|
var theTime time.Time
|
||||||
|
|
||||||
theTime, err = time.ParseInLocation(TimeLayout, *str, GetDefaultTimeLocation())
|
if theTime, err = time.ParseInLocation(TimeLayout, *str, GetDefaultTimeLocation()); err == nil {
|
||||||
if err == nil {
|
|
||||||
return &theTime
|
return &theTime
|
||||||
}
|
}
|
||||||
|
|
||||||
theTime, err = time.ParseInLocation(DateLayout, *str, GetDefaultTimeLocation())
|
if theTime, err = time.ParseInLocation(DateLayout, *str, GetDefaultTimeLocation()); err == nil {
|
||||||
if err == nil {
|
|
||||||
return &theTime
|
return &theTime
|
||||||
}
|
}
|
||||||
|
|
||||||
theTime, err = time.ParseInLocation(ClockLayout, *str, GetDefaultTimeLocation())
|
if theTime, err = time.ParseInLocation(ClockLayout, *str, GetDefaultTimeLocation()); err == nil {
|
||||||
if err == nil {
|
return &theTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if theTime, err = time.ParseInLocation(ISO9075MicroTZ, *str, GetDefaultTimeLocation()); err == nil {
|
||||||
return &theTime
|
return &theTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +129,8 @@ func TimeToTimeString(tm *time.Time) *string {
|
|||||||
if tm == nil {
|
if tm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return trans.String(tm.Format(TimeLayout))
|
|
||||||
|
return trans.String(tm.In(GetDefaultTimeLocation()).Format(TimeLayout))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringDateToTime 字符串 -> 时间
|
// StringDateToTime 字符串 -> 时间
|
||||||
@@ -161,13 +168,41 @@ func TimeToDateString(tm *time.Time) *string {
|
|||||||
if tm == nil {
|
if tm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return trans.String(tm.Format(DateLayout))
|
|
||||||
|
return trans.String(tm.In(GetDefaultTimeLocation()).Format(DateLayout))
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToTime(str *string, layout string) *time.Time {
|
||||||
|
if str == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(*str) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var theTime time.Time
|
||||||
|
|
||||||
|
theTime, err = time.ParseInLocation(layout, *str, GetDefaultTimeLocation())
|
||||||
|
if err == nil {
|
||||||
|
return &theTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeToString(tm *time.Time, layout string) *string {
|
||||||
|
if tm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return trans.String(tm.Format(layout))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimestamppbToTime timestamppb.Timestamp -> time.Time
|
// TimestamppbToTime timestamppb.Timestamp -> time.Time
|
||||||
func TimestamppbToTime(tm *timestamppb.Timestamp) *time.Time {
|
func TimestamppbToTime(timestamp *timestamppb.Timestamp) *time.Time {
|
||||||
if tm != nil {
|
if timestamp != nil {
|
||||||
return trans.Ptr(tm.AsTime())
|
return trans.Ptr(timestamp.AsTime())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -236,7 +271,7 @@ func DurationpbToDuration(duration *durationpb.Duration) *time.Duration {
|
|||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
||||||
func DurationpbSecond(duration *durationpb.Duration) *float64 {
|
func DurationpbToSecond(duration *durationpb.Duration) *float64 {
|
||||||
if duration == nil {
|
if duration == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -244,3 +279,20 @@ func DurationpbSecond(duration *durationpb.Duration) *float64 {
|
|||||||
secondsInt64 := seconds
|
secondsInt64 := seconds
|
||||||
return &secondsInt64
|
return &secondsInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringToDurationpb(in *string) *durationpb.Duration {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, _ := time.ParseDuration(*in)
|
||||||
|
return durationpb.New(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DurationpbToString(in *durationpb.Duration) *string {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return trans.Ptr(in.AsDuration().String())
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package timeutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/tx7do/go-utils/trans"
|
"github.com/tx7do/go-utils/trans"
|
||||||
|
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnixMilliToStringPtr(t *testing.T) {
|
func TestUnixMilliToStringPtr(t *testing.T) {
|
||||||
@@ -148,6 +149,49 @@ func TestTimeToDateString(t *testing.T) {
|
|||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTimeToString(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
fmt.Println(*TimeToString(&now, DateLayout))
|
||||||
|
fmt.Println(*TimeToString(&now, ISO8601))
|
||||||
|
fmt.Println(*TimeToString(&now, ISO8601TZHour))
|
||||||
|
fmt.Println(*TimeToString(&now, ISO8601NoTZ))
|
||||||
|
|
||||||
|
layout := "2006-01-02"
|
||||||
|
expected := now.Format(layout)
|
||||||
|
result := TimeToString(&now, layout)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expected, *result)
|
||||||
|
|
||||||
|
// 测试空输入
|
||||||
|
result = TimeToString(nil, layout)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToTime(t *testing.T) {
|
||||||
|
// 测试有效时间字符串输入
|
||||||
|
input := "2023-03-09 12:34:56"
|
||||||
|
layout := "2006-01-02 15:04:05"
|
||||||
|
expected := time.Date(2023, 3, 9, 12, 34, 56, 0, GetDefaultTimeLocation())
|
||||||
|
result := StringToTime(&input, layout)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expected, *result)
|
||||||
|
|
||||||
|
// 测试无效时间字符串输入
|
||||||
|
invalidInput := "invalid-date"
|
||||||
|
result = StringToTime(&invalidInput, layout)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试空字符串输入
|
||||||
|
emptyInput := ""
|
||||||
|
result = StringToTime(&emptyInput, layout)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
// 测试空指针输入
|
||||||
|
result = StringToTime(nil, layout)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTimestamppbToTime(t *testing.T) {
|
func TestTimestamppbToTime(t *testing.T) {
|
||||||
// 测试有效输入
|
// 测试有效输入
|
||||||
timestamp := timestamppb.Now()
|
timestamp := timestamppb.Now()
|
||||||
@@ -288,21 +332,21 @@ func TestSecondToDurationpb(t *testing.T) {
|
|||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDurationpbSecond(t *testing.T) {
|
func TestDurationpbToSecond(t *testing.T) {
|
||||||
// 测试非空输入
|
// 测试非空输入
|
||||||
duration := durationpb.New(5 * time.Second)
|
duration := durationpb.New(5 * time.Second)
|
||||||
result := DurationpbSecond(duration)
|
result := DurationpbToSecond(duration)
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
assert.Equal(t, 5.0, *result, "应返回正确的秒数")
|
assert.Equal(t, 5.0, *result, "应返回正确的秒数")
|
||||||
|
|
||||||
// 测试零输入
|
// 测试零输入
|
||||||
duration = durationpb.New(0)
|
duration = durationpb.New(0)
|
||||||
result = DurationpbSecond(duration)
|
result = DurationpbToSecond(duration)
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
assert.Equal(t, 0.0, *result, "应返回零秒")
|
assert.Equal(t, 0.0, *result, "应返回零秒")
|
||||||
|
|
||||||
// 测试空输入
|
// 测试空输入
|
||||||
result = DurationpbSecond(nil)
|
result = DurationpbToSecond(nil)
|
||||||
assert.Nil(t, result, "空输入应返回nil")
|
assert.Nil(t, result, "空输入应返回nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,3 +397,48 @@ func TestTimeToUnixSecondInt64Ptr(t *testing.T) {
|
|||||||
result = TimeToUnixSecondInt64Ptr(nil)
|
result = TimeToUnixSecondInt64Ptr(nil)
|
||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringToDurationpb(t *testing.T) {
|
||||||
|
// 测试有效输入
|
||||||
|
validInput := "1h30m"
|
||||||
|
expected := durationpb.New(90 * time.Minute)
|
||||||
|
result := StringToDurationpb(&validInput)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
|
||||||
|
// 测试无效输入
|
||||||
|
invalidInput := "invalid-duration"
|
||||||
|
result = StringToDurationpb(&invalidInput)
|
||||||
|
assert.NotNil(t, result) // 即使输入无效,time.ParseDuration 返回零值
|
||||||
|
assert.Equal(t, durationpb.New(0), result)
|
||||||
|
|
||||||
|
// 测试空字符串输入
|
||||||
|
emptyInput := ""
|
||||||
|
result = StringToDurationpb(&emptyInput)
|
||||||
|
assert.NotNil(t, result) // 空字符串解析为零值
|
||||||
|
assert.Equal(t, durationpb.New(0), result)
|
||||||
|
|
||||||
|
// 测试空指针输入
|
||||||
|
result = StringToDurationpb(nil)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationpbToString(t *testing.T) {
|
||||||
|
// 测试有效输入
|
||||||
|
duration := durationpb.New(90 * time.Second) // 90秒
|
||||||
|
expected := "1m30s"
|
||||||
|
result := DurationpbToString(duration)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expected, *result)
|
||||||
|
|
||||||
|
// 测试零值输入
|
||||||
|
duration = durationpb.New(0)
|
||||||
|
expected = "0s"
|
||||||
|
result = DurationpbToString(duration)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expected, *result)
|
||||||
|
|
||||||
|
// 测试空输入
|
||||||
|
result = DurationpbToString(nil)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user