1// Copyright 2016 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// TODO(jba): test that OnError is getting called appropriately.
16
17package logadmin
18
19import (
20	"context"
21	"flag"
22	"log"
23	"net/http"
24	"net/url"
25	"os"
26	"testing"
27	"time"
28
29	"cloud.google.com/go/internal/testutil"
30	"cloud.google.com/go/logging"
31	ltesting "cloud.google.com/go/logging/internal/testing"
32	"github.com/golang/protobuf/proto"
33	"github.com/golang/protobuf/ptypes"
34	durpb "github.com/golang/protobuf/ptypes/duration"
35	structpb "github.com/golang/protobuf/ptypes/struct"
36	"github.com/google/go-cmp/cmp/cmpopts"
37	"google.golang.org/api/option"
38	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
39	audit "google.golang.org/genproto/googleapis/cloud/audit"
40	logtypepb "google.golang.org/genproto/googleapis/logging/type"
41	logpb "google.golang.org/genproto/googleapis/logging/v2"
42	"google.golang.org/grpc"
43)
44
45var (
46	client        *Client
47	testProjectID string
48)
49
50var (
51	// If true, this test is using the production service, not a fake.
52	integrationTest bool
53
54	newClient func(ctx context.Context, projectID string) *Client
55)
56
57func TestMain(m *testing.M) {
58	flag.Parse() // needed for testing.Short()
59	ctx := context.Background()
60	testProjectID = testutil.ProjID()
61	if testProjectID == "" || testing.Short() {
62		integrationTest = false
63		if testProjectID != "" {
64			log.Print("Integration tests skipped in short mode (using fake instead)")
65		}
66		testProjectID = "PROJECT_ID"
67		addr, err := ltesting.NewServer()
68		if err != nil {
69			log.Fatalf("creating fake server: %v", err)
70		}
71		newClient = func(ctx context.Context, projectID string) *Client {
72			conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
73			if err != nil {
74				log.Fatalf("dialing %q: %v", addr, err)
75			}
76			c, err := NewClient(ctx, projectID, option.WithGRPCConn(conn))
77			if err != nil {
78				log.Fatalf("creating client for fake at %q: %v", addr, err)
79			}
80			return c
81		}
82	} else {
83		integrationTest = true
84		ts := testutil.TokenSource(ctx, logging.AdminScope)
85		if ts == nil {
86			log.Fatal("The project key must be set. See CONTRIBUTING.md for details")
87		}
88		log.Printf("running integration tests with project %s", testProjectID)
89		newClient = func(ctx context.Context, projectID string) *Client {
90			c, err := NewClient(ctx, projectID, option.WithTokenSource(ts),
91				option.WithGRPCDialOption(grpc.WithBlock()))
92			if err != nil {
93				log.Fatalf("creating prod client: %v", err)
94			}
95			return c
96		}
97	}
98	client = newClient(ctx, testProjectID)
99	initMetrics(ctx)
100	cleanup := initSinks(ctx)
101	exit := m.Run()
102	cleanup()
103	client.Close()
104	os.Exit(exit)
105}
106
107// EntryIterator and DeleteLog are tested in the logging package.
108
109func TestClientClose(t *testing.T) {
110	c := newClient(context.Background(), testProjectID)
111	if err := c.Close(); err != nil {
112		t.Errorf("want got %v, want nil", err)
113	}
114}
115
116func TestFromLogEntry(t *testing.T) {
117	now := time.Now()
118	res := &mrpb.MonitoredResource{Type: "global"}
119	ts, err := ptypes.TimestampProto(now)
120	if err != nil {
121		t.Fatal(err)
122	}
123	logEntry := logpb.LogEntry{
124		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
125		Resource:  res,
126		Payload:   &logpb.LogEntry_TextPayload{TextPayload: "hello"},
127		Timestamp: ts,
128		Severity:  logtypepb.LogSeverity_INFO,
129		InsertId:  "123",
130		HttpRequest: &logtypepb.HttpRequest{
131			RequestMethod:                  "GET",
132			RequestUrl:                     "http:://example.com/path?q=1",
133			RequestSize:                    100,
134			Status:                         200,
135			ResponseSize:                   25,
136			Latency:                        &durpb.Duration{Seconds: 100},
137			UserAgent:                      "user-agent",
138			RemoteIp:                       "127.0.0.1",
139			ServerIp:                       "127.0.0.1",
140			Referer:                        "referer",
141			CacheLookup:                    true,
142			CacheHit:                       true,
143			CacheValidatedWithOriginServer: true,
144			CacheFillBytes:                 2048,
145		},
146		Labels: map[string]string{
147			"a": "1",
148			"b": "two",
149			"c": "true",
150		},
151		SourceLocation: &logpb.LogEntrySourceLocation{
152			File:     "some_file.go",
153			Line:     1,
154			Function: "someFunction",
155		},
156	}
157	u, err := url.Parse("http:://example.com/path?q=1")
158	if err != nil {
159		t.Fatal(err)
160	}
161	want := &logging.Entry{
162		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
163		Resource:  res,
164		Timestamp: now.In(time.UTC),
165		Severity:  logging.Info,
166		Payload:   "hello",
167		Labels: map[string]string{
168			"a": "1",
169			"b": "two",
170			"c": "true",
171		},
172		InsertID: "123",
173		HTTPRequest: &logging.HTTPRequest{
174			Request: &http.Request{
175				Method: "GET",
176				URL:    u,
177				Header: map[string][]string{
178					"User-Agent": {"user-agent"},
179					"Referer":    {"referer"},
180				},
181			},
182			RequestSize:                    100,
183			Status:                         200,
184			ResponseSize:                   25,
185			Latency:                        100 * time.Second,
186			LocalIP:                        "127.0.0.1",
187			RemoteIP:                       "127.0.0.1",
188			CacheLookup:                    true,
189			CacheHit:                       true,
190			CacheValidatedWithOriginServer: true,
191			CacheFillBytes:                 2048,
192		},
193		SourceLocation: &logpb.LogEntrySourceLocation{
194			File:     "some_file.go",
195			Line:     1,
196			Function: "someFunction",
197		},
198	}
199	got, err := fromLogEntry(&logEntry)
200	if err != nil {
201		t.Fatal(err)
202	}
203	if diff := testutil.Diff(got, want, cmpopts.IgnoreUnexported(http.Request{})); diff != "" {
204		t.Errorf("FullEntry:\n%s", diff)
205	}
206
207	// Proto payload.
208	alog := &audit.AuditLog{
209		ServiceName:  "svc",
210		MethodName:   "method",
211		ResourceName: "shelves/S/books/B",
212	}
213	any, err := ptypes.MarshalAny(alog)
214	if err != nil {
215		t.Fatal(err)
216	}
217	logEntry = logpb.LogEntry{
218		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
219		Resource:  res,
220		Timestamp: ts,
221		Payload:   &logpb.LogEntry_ProtoPayload{ProtoPayload: any},
222	}
223	got, err = fromLogEntry(&logEntry)
224	if err != nil {
225		t.Fatal(err)
226	}
227	if !ltesting.PayloadEqual(got.Payload, alog) {
228		t.Errorf("got %+v, want %+v", got.Payload, alog)
229	}
230
231	// JSON payload.
232	jstruct := &structpb.Struct{Fields: map[string]*structpb.Value{
233		"f": {Kind: &structpb.Value_NumberValue{NumberValue: 3.1}},
234	}}
235	logEntry = logpb.LogEntry{
236		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
237		Resource:  res,
238		Timestamp: ts,
239		Payload:   &logpb.LogEntry_JsonPayload{JsonPayload: jstruct},
240	}
241	got, err = fromLogEntry(&logEntry)
242	if err != nil {
243		t.Fatal(err)
244	}
245	if !ltesting.PayloadEqual(got.Payload, jstruct) {
246		t.Errorf("got %+v, want %+v", got.Payload, jstruct)
247	}
248
249	// No payload.
250	logEntry = logpb.LogEntry{
251		LogName:   "projects/PROJECT_ID/logs/LOG_ID",
252		Resource:  res,
253		Timestamp: ts,
254	}
255	got, err = fromLogEntry(&logEntry)
256	if err != nil {
257		t.Fatal(err)
258	}
259	if !ltesting.PayloadEqual(got.Payload, nil) {
260		t.Errorf("got %+v, want %+v", got.Payload, nil)
261	}
262}
263
264func TestListLogEntriesRequest(t *testing.T) {
265	dayAgo := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)
266	for _, test := range []struct {
267		opts          []EntriesOption
268		resourceNames []string
269		filter        string
270		orderBy       string
271	}{
272		// Default is client's project ID, 24 hour lookback, and orderBy.
273		{nil, []string{"projects/PROJECT_ID"}, `timestamp >= "` + dayAgo + `"`, ""},
274		// Timestamp default does not override user's filter
275		{[]EntriesOption{NewestFirst(), Filter(`timestamp > "2020-10-30T15:39:09Z"`)},
276			[]string{"projects/PROJECT_ID"}, `timestamp > "2020-10-30T15:39:09Z"`, "timestamp desc"},
277		{[]EntriesOption{NewestFirst(), Filter("f")},
278			[]string{"projects/PROJECT_ID"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"},
279		{[]EntriesOption{ProjectIDs([]string{"foo"})},
280			[]string{"projects/foo"}, `timestamp >= "` + dayAgo + `"`, ""},
281		{[]EntriesOption{ResourceNames([]string{"folders/F", "organizations/O"})},
282			[]string{"folders/F", "organizations/O"}, `timestamp >= "` + dayAgo + `"`, ""},
283		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
284			[]string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"},
285		{[]EntriesOption{NewestFirst(), Filter("f"), ProjectIDs([]string{"foo"})},
286			[]string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"},
287		// If there are repeats, last one wins.
288		{[]EntriesOption{NewestFirst(), Filter("no"), ProjectIDs([]string{"foo"}), Filter("f")},
289			[]string{"projects/foo"}, `f AND timestamp >= "` + dayAgo + `"`, "timestamp desc"},
290	} {
291		got := listLogEntriesRequest("projects/PROJECT_ID", test.opts)
292		want := &logpb.ListLogEntriesRequest{
293			ResourceNames: test.resourceNames,
294			Filter:        test.filter,
295			OrderBy:       test.orderBy,
296		}
297		if !proto.Equal(got, want) {
298			t.Errorf("%v:\ngot  %v\nwant %v", test.opts, got, want)
299		}
300	}
301}
302