go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/service/service_test.go (about)

     1  // Copyright 2015 The LUCI Authors.
     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  package service
    16  
    17  import (
    18  	"bytes"
    19  	"compress/zlib"
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/base64"
    23  	"encoding/hex"
    24  	"encoding/json"
    25  	"fmt"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"testing"
    29  	"time"
    30  
    31  	"google.golang.org/protobuf/proto"
    32  
    33  	"go.chromium.org/luci/server/auth/service/protocol"
    34  	"go.chromium.org/luci/server/auth/signing"
    35  	"go.chromium.org/luci/server/auth/signing/signingtest"
    36  	"go.chromium.org/luci/server/caching"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  func TestPubSubWorkflow(t *testing.T) {
    43  	Convey("PubSub pull workflow works", t, func(c C) {
    44  		ctx := caching.WithEmptyProcessCache(context.Background())
    45  
    46  		fakeSigner := signingtest.NewSigner(nil)
    47  		certs, _ := fakeSigner.Certificates(ctx)
    48  		certsJSON, _ := json.Marshal(certs)
    49  
    50  		// Expected calls.
    51  		calls := []struct {
    52  			Method   string
    53  			URL      string
    54  			Code     int
    55  			Response string
    56  		}{
    57  			// Probing for existing subscription -> tell it is not found.
    58  			{
    59  				"GET",
    60  				"/pubsub/projects/p1/subscriptions/sub",
    61  				404,
    62  				`{"error": {"code": 404}}`,
    63  			},
    64  			// Authorizing access to PubSub topic.
    65  			{
    66  				"POST",
    67  				"/auth_service/api/v1/authdb/subscription/authorization",
    68  				200,
    69  				`{"topic": "projects/p2/topics/topic"}`,
    70  			},
    71  			// Creating the subscription.
    72  			{
    73  				"PUT",
    74  				"/pubsub/projects/p1/subscriptions/sub",
    75  				200,
    76  				"",
    77  			},
    78  			// Probing for existing subscription again.
    79  			{
    80  				"GET",
    81  				"/pubsub/projects/p1/subscriptions/sub",
    82  				200,
    83  				`{"pushConfig": {"pushEndpoint": "http://blah"}}`,
    84  			},
    85  			// Changing push URL.
    86  			{
    87  				"POST",
    88  				"/pubsub/projects/p1/subscriptions/sub:modifyPushConfig",
    89  				200,
    90  				"",
    91  			},
    92  			// Pulling messages from it, all bad.
    93  			{
    94  				"POST",
    95  				"/pubsub/projects/p1/subscriptions/sub:pull",
    96  				200,
    97  				`{"receivedMessages": [
    98  					{
    99  						"ackId": "ack1",
   100  						"message": {"data": "broken"}
   101  					}
   102  				]}`,
   103  			},
   104  			// Fetching certificates from auth service to authenticate messages.
   105  			{
   106  				"GET",
   107  				"/auth/api/v1/server/certificates",
   108  				200,
   109  				string(certsJSON),
   110  			},
   111  			// Bad messages are removed from the queue by ack.
   112  			{
   113  				"POST",
   114  				"/pubsub/projects/p1/subscriptions/sub:acknowledge",
   115  				200,
   116  				"",
   117  			},
   118  			// Pulling messages from the subscription, again.
   119  			{
   120  				"POST",
   121  				"/pubsub/projects/p1/subscriptions/sub:pull",
   122  				200,
   123  				fmt.Sprintf(`{"receivedMessages": [
   124  					{
   125  						"ackId": "ack1",
   126  						"message": {"data": "broken"}
   127  					},
   128  					%s,
   129  					%s
   130  				]}`, fakePubSubMessage(ctx, "ack2", 122, fakeSigner), fakePubSubMessage(ctx, "ack2", 123, fakeSigner)),
   131  			},
   132  			// Acknowledging messages.
   133  			{
   134  				"POST",
   135  				"/pubsub/projects/p1/subscriptions/sub:acknowledge",
   136  				200,
   137  				"",
   138  			},
   139  			// Removing existing subscription.
   140  			{
   141  				"DELETE",
   142  				"/pubsub/projects/p1/subscriptions/sub",
   143  				200,
   144  				"{}",
   145  			},
   146  			// Removing already deleted subscription.
   147  			{
   148  				"DELETE",
   149  				"/pubsub/projects/p1/subscriptions/sub",
   150  				404,
   151  				`{"error": {"code": 404}}`,
   152  			},
   153  		}
   154  		counter := 0
   155  
   156  		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   157  			if counter >= len(calls) {
   158  				c.So(fmt.Sprintf("%s %s is unexpected", r.Method, r.URL.Path), ShouldBeNil)
   159  			}
   160  			call := &calls[counter]
   161  			c.So(r.Method, ShouldEqual, call.Method)
   162  			c.So(r.URL.Path, ShouldEqual, call.URL)
   163  			w.WriteHeader(call.Code)
   164  			if call.Response != "" {
   165  				w.Write([]byte(call.Response))
   166  			}
   167  			counter++
   168  		}))
   169  		defer ts.Close()
   170  
   171  		// Register.
   172  		srv := AuthService{
   173  			URL:           ts.URL,
   174  			pubSubURLRoot: ts.URL + "/pubsub/",
   175  		}
   176  		So(srv.EnsureSubscription(ctx, "projects/p1/subscriptions/sub", "http://blah"), ShouldBeNil)
   177  
   178  		// Reregister with no push url. For code coverage.
   179  		So(srv.EnsureSubscription(ctx, "projects/p1/subscriptions/sub", ""), ShouldBeNil)
   180  
   181  		// First pull. No valid messages.
   182  		notify, err := srv.PullPubSub(ctx, "projects/p1/subscriptions/sub")
   183  		So(err, ShouldBeNil)
   184  		So(notify, ShouldBeNil)
   185  
   186  		// Second pull. Have something.
   187  		notify, err = srv.PullPubSub(ctx, "projects/p1/subscriptions/sub")
   188  		So(err, ShouldBeNil)
   189  		So(notify, ShouldNotBeNil)
   190  		So(notify.Revision, ShouldEqual, 123)
   191  
   192  		// Ack.
   193  		So(notify.Acknowledge(ctx), ShouldBeNil)
   194  
   195  		// Code coverage.
   196  		notify, err = srv.ProcessPubSubPush(ctx, []byte(fakePubSubMessage(ctx, "", 456, fakeSigner)))
   197  		So(err, ShouldBeNil)
   198  		So(notify, ShouldNotBeNil)
   199  		So(notify.Revision, ShouldEqual, 456)
   200  
   201  		// Killing existing subscription.
   202  		So(srv.DeleteSubscription(ctx, "projects/p1/subscriptions/sub"), ShouldBeNil)
   203  		// Killing already removed subscription.
   204  		So(srv.DeleteSubscription(ctx, "projects/p1/subscriptions/sub"), ShouldBeNil)
   205  	})
   206  }
   207  
   208  func TestGetSnapshot(t *testing.T) {
   209  	Convey("GetSnapshot works", t, func(c C) {
   210  		ctx := context.Background()
   211  
   212  		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   213  			c.So(r.URL.Path, ShouldEqual, "/auth_service/api/v1/authdb/revisions/123")
   214  			body, digest := generateSnapshot(123)
   215  			w.Write([]byte(fmt.Sprintf(`{"snapshot": {
   216  				"auth_db_rev": 123,
   217  				"sha256": "%s",
   218  				"created_ts": 1446599918304238,
   219  				"deflated_body": "%s"
   220  			}}`, digest, body)))
   221  		}))
   222  		defer ts.Close()
   223  
   224  		srv := AuthService{
   225  			URL: ts.URL,
   226  		}
   227  		snap, err := srv.GetSnapshot(ctx, 123)
   228  		So(err, ShouldBeNil)
   229  
   230  		So(snap, ShouldResemble, &Snapshot{
   231  			AuthDB:         &protocol.AuthDB{},
   232  			AuthServiceURL: ts.URL,
   233  			Rev:            123,
   234  			Created:        time.Unix(0, 1446599918304238000),
   235  		})
   236  	})
   237  }
   238  
   239  func TestDeflateInflate(t *testing.T) {
   240  	Convey("Deflate then Inflate works", t, func() {
   241  		initial := &protocol.AuthDB{
   242  			OauthClientId:     "abc",
   243  			OauthClientSecret: "def",
   244  		}
   245  		blob, err := DeflateAuthDB(initial)
   246  		So(err, ShouldBeNil)
   247  		inflated, err := InflateAuthDB(blob)
   248  		So(err, ShouldBeNil)
   249  		So(inflated, ShouldResembleProto, initial)
   250  	})
   251  }
   252  
   253  ///
   254  
   255  func generateSnapshot(rev int64) (body string, digest string) {
   256  	blob, err := proto.Marshal(&protocol.ReplicationPushRequest{
   257  		Revision: &protocol.AuthDBRevision{
   258  			AuthDbRev:  rev,
   259  			PrimaryId:  "primaryId",
   260  			ModifiedTs: 1446599918304238,
   261  		},
   262  		AuthDb: &protocol.AuthDB{},
   263  	})
   264  	if err != nil {
   265  		panic(err)
   266  	}
   267  
   268  	buf := bytes.Buffer{}
   269  	w := zlib.NewWriter(&buf)
   270  	_, err = w.Write(blob)
   271  	if err != nil {
   272  		panic(err)
   273  	}
   274  	w.Close()
   275  
   276  	body = base64.StdEncoding.EncodeToString(buf.Bytes())
   277  	hash := sha256.Sum256(blob)
   278  	digest = hex.EncodeToString(hash[:])
   279  	return
   280  }
   281  
   282  func fakePubSubMessage(ctx context.Context, ackID string, rev int64, signer signing.Signer) string {
   283  	msg := protocol.ChangeNotification{
   284  		Revision: &protocol.AuthDBRevision{
   285  			AuthDbRev:  rev,
   286  			PrimaryId:  "primaryId",
   287  			ModifiedTs: 1000,
   288  		},
   289  	}
   290  	blob, _ := proto.Marshal(&msg)
   291  	key, sig, _ := signer.SignBytes(ctx, blob)
   292  	ps := pubSubMessage{
   293  		AckID: ackID,
   294  	}
   295  	ps.Message.Data = base64.StdEncoding.EncodeToString(blob)
   296  	ps.Message.Attributes = map[string]string{
   297  		"X-AuthDB-SigKey-v1": key,
   298  		"X-AuthDB-SigVal-v1": base64.StdEncoding.EncodeToString(sig),
   299  	}
   300  	out, _ := json.Marshal(&ps)
   301  	return string(out)
   302  }