package session import ( "context" "net" "net/http" "testing" "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" objpb "github.com/zitadel/zitadel/pkg/grpc/object" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" ) var ( creationDate = time.Date(2023, 10, 10, 14, 15, 0, 0, time.UTC) ) func Test_sessionsToPb(t *testing.T) { now := time.Now() past := now.Add(-time.Hour) sessions := []*query.Session{ { // no factor, with user agent and expiration ID: "999", CreationDate: now, ChangeDate: now, Sequence: 123, State: domain.SessionStateActive, ResourceOwner: "me", Creator: "he", Metadata: map[string][]byte{"hello": []byte("world")}, UserAgent: domain.UserAgent{ FingerprintID: gu.Ptr("fingerprintID"), Description: gu.Ptr("description"), IP: net.IPv4(1, 2, 3, 4), Header: http.Header{"foo": []string{"foo", "bar"}}, }, Expiration: now, }, { // user factor ID: "999", CreationDate: now, ChangeDate: now, Sequence: 123, State: domain.SessionStateActive, ResourceOwner: "me", Creator: "he", UserFactor: query.SessionUserFactor{ UserID: "345", UserCheckedAt: past, LoginName: "donald", DisplayName: "donald duck", ResourceOwner: "org1", }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // password factor ID: "999", CreationDate: now, ChangeDate: now, Sequence: 123, State: domain.SessionStateActive, ResourceOwner: "me", Creator: "he", UserFactor: query.SessionUserFactor{ UserID: "345", UserCheckedAt: past, LoginName: "donald", DisplayName: "donald duck", ResourceOwner: "org1", }, PasswordFactor: query.SessionPasswordFactor{ PasswordCheckedAt: past, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // webAuthN factor ID: "999", CreationDate: now, ChangeDate: now, Sequence: 123, State: domain.SessionStateActive, ResourceOwner: "me", Creator: "he", UserFactor: query.SessionUserFactor{ UserID: "345", UserCheckedAt: past, LoginName: "donald", DisplayName: "donald duck", ResourceOwner: "org1", }, WebAuthNFactor: query.SessionWebAuthNFactor{ WebAuthNCheckedAt: past, UserVerified: true, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // totp factor ID: "999", CreationDate: now, ChangeDate: now, Sequence: 123, State: domain.SessionStateActive, ResourceOwner: "me", Creator: "he", UserFactor: query.SessionUserFactor{ UserID: "345", UserCheckedAt: past, LoginName: "donald", DisplayName: "donald duck", ResourceOwner: "org1", }, TOTPFactor: query.SessionTOTPFactor{ TOTPCheckedAt: past, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, } want := []*session.Session{ { // no factor, with user agent and expiration Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: nil, Metadata: map[string][]byte{"hello": []byte("world")}, UserAgent: &session.UserAgent{ FingerprintId: gu.Ptr("fingerprintID"), Description: gu.Ptr("description"), Ip: gu.Ptr("1.2.3.4"), Header: map[string]*session.UserAgent_HeaderValues{ "foo": {Values: []string{"foo", "bar"}}, }, }, ExpirationDate: timestamppb.New(now), }, { // user factor Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: &session.Factors{ User: &session.UserFactor{ VerifiedAt: timestamppb.New(past), Id: "345", LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", OrganizationId: "org1", }, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // password factor Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: &session.Factors{ User: &session.UserFactor{ VerifiedAt: timestamppb.New(past), Id: "345", LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", OrganizationId: "org1", }, Password: &session.PasswordFactor{ VerifiedAt: timestamppb.New(past), }, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // webAuthN factor Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: &session.Factors{ User: &session.UserFactor{ VerifiedAt: timestamppb.New(past), Id: "345", LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", OrganizationId: "org1", }, WebAuthN: &session.WebAuthNFactor{ VerifiedAt: timestamppb.New(past), UserVerified: true, }, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, { // totp factor Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: &session.Factors{ User: &session.UserFactor{ VerifiedAt: timestamppb.New(past), Id: "345", LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", OrganizationId: "org1", }, Totp: &session.TOTPFactor{ VerifiedAt: timestamppb.New(past), }, }, Metadata: map[string][]byte{"hello": []byte("world")}, }, } out := sessionsToPb(sessions) require.Len(t, out, len(want)) for i, got := range out { if !proto.Equal(got, want[i]) { t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want[i]) } } } func Test_userAgentToPb(t *testing.T) { type args struct { ua domain.UserAgent } tests := []struct { name string args args want *session.UserAgent }{ { name: "empty", args: args{domain.UserAgent{}}, }, { name: "fingerprint id and description", args: args{domain.UserAgent{ FingerprintID: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), }}, want: &session.UserAgent{ FingerprintId: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), }, }, { name: "with ip", args: args{domain.UserAgent{ FingerprintID: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), IP: net.IPv4(1, 2, 3, 4), }}, want: &session.UserAgent{ FingerprintId: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), Ip: gu.Ptr("1.2.3.4"), }, }, { name: "with header", args: args{domain.UserAgent{ FingerprintID: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), Header: http.Header{ "foo": []string{"foo", "bar"}, "hello": []string{"world"}, }, }}, want: &session.UserAgent{ FingerprintId: gu.Ptr("fingerPrintID"), Description: gu.Ptr("description"), Header: map[string]*session.UserAgent_HeaderValues{ "foo": {Values: []string{"foo", "bar"}}, "hello": {Values: []string{"world"}}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := userAgentToPb(tt.args.ua) assert.Equal(t, tt.want, got) }) } } func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery { q, err := query.NewTextQuery(column, value, compare) require.NoError(t, err) return q } func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery { q, err := query.NewListQuery(query.SessionColumnID, list, compare) require.NoError(t, err) return q } func mustNewTimestampQuery(t testing.TB, column query.Column, ts time.Time, compare query.TimestampComparison) query.SearchQuery { q, err := query.NewTimestampQuery(column, ts, compare) require.NoError(t, err) return q } func Test_listSessionsRequestToQuery(t *testing.T) { type args struct { ctx context.Context req *session.ListSessionsRequest } tests := []struct { name string args args want *query.SessionsSearchQueries wantErr error }{ { name: "default request", args: args{ ctx: authz.NewMockContext("123", "456", "789"), req: &session.ListSessionsRequest{}, }, want: &query.SessionsSearchQueries{ SearchRequest: query.SearchRequest{ Offset: 0, Limit: 0, Asc: false, }, Queries: []query.SearchQuery{ mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), }, }, }, { name: "default request with sorting column", args: args{ ctx: authz.NewMockContext("123", "456", "789"), req: &session.ListSessionsRequest{ SortingColumn: session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE, }, }, want: &query.SessionsSearchQueries{ SearchRequest: query.SearchRequest{ Offset: 0, Limit: 0, SortingColumn: query.SessionColumnCreationDate, Asc: false, }, Queries: []query.SearchQuery{ mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), }, }, }, { name: "with list query and sessions", args: args{ ctx: authz.NewMockContext("123", "456", "789"), req: &session.ListSessionsRequest{ Query: &object.ListQuery{ Offset: 10, Limit: 20, Asc: true, }, Queries: []*session.SearchQuery{ {Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"1", "2", "3"}, }, }}, {Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"4", "5", "6"}, }, }}, {Query: &session.SearchQuery_UserIdQuery{ UserIdQuery: &session.UserIDQuery{ Id: "10", }, }}, {Query: &session.SearchQuery_CreationDateQuery{ CreationDateQuery: &session.CreationDateQuery{ CreationDate: timestamppb.New(creationDate), Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER, }, }}, }, }, }, want: &query.SessionsSearchQueries{ SearchRequest: query.SearchRequest{ Offset: 10, Limit: 20, Asc: true, }, Queries: []query.SearchQuery{ mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn), mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals), mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater), mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), }, }, }, { name: "invalid argument error", args: args{ ctx: authz.NewMockContext("123", "456", "789"), req: &session.ListSessionsRequest{ Query: &object.ListQuery{ Offset: 10, Limit: 20, Asc: true, }, Queries: []*session.SearchQuery{ {Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"1", "2", "3"}, }, }}, {Query: nil}, }, }, }, wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) } } func Test_sessionQueriesToQuery(t *testing.T) { type args struct { ctx context.Context queries []*session.SearchQuery } tests := []struct { name string args args want []query.SearchQuery wantErr error }{ { name: "creator only", args: args{ ctx: authz.NewMockContext("123", "456", "789"), }, want: []query.SearchQuery{ mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), }, }, { name: "invalid argument", args: args{ ctx: authz.NewMockContext("123", "456", "789"), queries: []*session.SearchQuery{ {Query: nil}, }, }, wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), }, { name: "creator and sessions", args: args{ ctx: authz.NewMockContext("123", "456", "789"), queries: []*session.SearchQuery{ {Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"1", "2", "3"}, }, }}, {Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"4", "5", "6"}, }, }}, }, }, want: []query.SearchQuery{ mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn), mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) } } func Test_sessionQueryToQuery(t *testing.T) { type args struct { query *session.SearchQuery } tests := []struct { name string args args want query.SearchQuery wantErr error }{ { name: "invalid argument", args: args{&session.SearchQuery{ Query: nil, }}, wantErr: caos_errs.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), }, { name: "ids query", args: args{&session.SearchQuery{ Query: &session.SearchQuery_IdsQuery{ IdsQuery: &session.IDsQuery{ Ids: []string{"1", "2", "3"}, }, }, }}, want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), }, { name: "user id query", args: args{&session.SearchQuery{ Query: &session.SearchQuery_UserIdQuery{ UserIdQuery: &session.UserIDQuery{ Id: "10", }, }, }}, want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals), }, { name: "creation date query", args: args{&session.SearchQuery{ Query: &session.SearchQuery_CreationDateQuery{ CreationDateQuery: &session.CreationDateQuery{ CreationDate: timestamppb.New(creationDate), Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS, }, }, }}, want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess), }, { name: "creation date query with default method", args: args{&session.SearchQuery{ Query: &session.SearchQuery_CreationDateQuery{ CreationDateQuery: &session.CreationDateQuery{ CreationDate: timestamppb.New(creationDate), }, }, }}, want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := sessionQueryToQuery(tt.args.query) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) } } func Test_userCheck(t *testing.T) { type args struct { user *session.CheckUser } tests := []struct { name string args args want userSearch wantErr error }{ { name: "nil user", args: args{nil}, want: nil, }, { name: "by user id", args: args{&session.CheckUser{ Search: &session.CheckUser_UserId{ UserId: "foo", }, }}, want: userSearchByID{"foo"}, }, { name: "by user id", args: args{&session.CheckUser{ Search: &session.CheckUser_LoginName{ LoginName: "bar", }, }}, want: userSearchByLoginName{"bar"}, }, { name: "unimplemented error", args: args{&session.CheckUser{ Search: nil, }}, wantErr: caos_errs.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := userCheck(tt.args.user) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) } } func Test_userVerificationRequirementToDomain(t *testing.T) { type args struct { req session.UserVerificationRequirement } tests := []struct { args args want domain.UserVerificationRequirement }{ { args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED}, want: domain.UserVerificationRequirementUnspecified, }, { args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED}, want: domain.UserVerificationRequirementRequired, }, { args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED}, want: domain.UserVerificationRequirementPreferred, }, { args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED}, want: domain.UserVerificationRequirementDiscouraged, }, { args: args{999}, want: domain.UserVerificationRequirementUnspecified, }, } for _, tt := range tests { t.Run(tt.args.req.String(), func(t *testing.T) { got := userVerificationRequirementToDomain(tt.args.req) assert.Equal(t, tt.want, got) }) } } func Test_userAgentToCommand(t *testing.T) { type args struct { userAgent *session.UserAgent } tests := []struct { name string args args want *domain.UserAgent }{ { name: "nil", args: args{nil}, want: nil, }, { name: "all fields", args: args{&session.UserAgent{ FingerprintId: gu.Ptr("fp1"), Ip: gu.Ptr("1.2.3.4"), Description: gu.Ptr("firefox"), Header: map[string]*session.UserAgent_HeaderValues{ "hello": { Values: []string{"foo", "bar"}, }, }, }}, want: &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: net.ParseIP("1.2.3.4"), Description: gu.Ptr("firefox"), Header: http.Header{ "hello": []string{"foo", "bar"}, }, }, }, { name: "invalid ip", args: args{&session.UserAgent{ FingerprintId: gu.Ptr("fp1"), Ip: gu.Ptr("oops"), Description: gu.Ptr("firefox"), Header: map[string]*session.UserAgent_HeaderValues{ "hello": { Values: []string{"foo", "bar"}, }, }, }}, want: &domain.UserAgent{ FingerprintID: gu.Ptr("fp1"), IP: nil, Description: gu.Ptr("firefox"), Header: http.Header{ "hello": []string{"foo", "bar"}, }, }, }, { name: "nil fields", args: args{&session.UserAgent{}}, want: &domain.UserAgent{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := userAgentToCommand(tt.args.userAgent) assert.Equal(t, tt.want, got) }) } }