This is an automated email from the ASF dual-hosted git repository.

damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 7c691b7449f [Tour Of Beam] delete user progress (#24657)
7c691b7449f is described below

commit 7c691b7449f910f29e63b1a75c8b19d5d970cbae
Author: Evgeny Antyshev <[email protected]>
AuthorDate: Fri Dec 16 17:36:36 2022 +0300

    [Tour Of Beam] delete user progress (#24657)
    
    * README
    
    * refactor
    
    * aio
    
    * log
    
    * workflow
---
 .../workflows/tour_of_beam_backend_integration.yml |  3 ++
 learning/tour-of-beam/backend/README.md            |  6 ++++
 learning/tour-of-beam/backend/auth.go              | 16 ++++++----
 learning/tour-of-beam/backend/function.go          | 36 ++++++++++++++++++----
 .../backend/integration_tests/auth_test.go         | 26 ++++++++++++++++
 .../backend/integration_tests/client.go            |  7 +++++
 .../backend/integration_tests/function_test.go     | 13 ++++----
 .../backend/integration_tests/local.sh             |  2 ++
 .../backend/internal/service/content.go            |  5 +++
 .../backend/internal/storage/datastore.go          | 28 +++++++++++++++++
 .../tour-of-beam/backend/internal/storage/iface.go |  2 ++
 .../tour-of-beam/backend/internal/storage/mock.go  |  4 +++
 learning/tour-of-beam/backend/middleware.go        | 25 ++++++++++++---
 13 files changed, 150 insertions(+), 23 deletions(-)

diff --git a/.github/workflows/tour_of_beam_backend_integration.yml 
b/.github/workflows/tour_of_beam_backend_integration.yml
index ce6950890fb..d43af660221 100644
--- a/.github/workflows/tour_of_beam_backend_integration.yml
+++ b/.github/workflows/tour_of_beam_backend_integration.yml
@@ -59,6 +59,7 @@ env:
   PORT_GET_USER_PROGRESS: 8804
   PORT_POST_UNIT_COMPLETE: 8805
   PORT_POST_USER_CODE: 8806
+  PORT_POST_DELETE_PROGRESS: 8807
 
 
 jobs:
@@ -102,6 +103,8 @@ jobs:
         run: PORT=${{ env.PORT_POST_UNIT_COMPLETE }} 
FUNCTION_TARGET=postUnitComplete ./tob_function &
       - name: Run postUserCode in background
         run: PORT=${{ env.PORT_POST_USER_CODE }} FUNCTION_TARGET=postUserCode 
./tob_function &
+      - name: Run postDeleteProgress in background
+        run: PORT=${{ env.PORT_POST_DELETE_PROGRESS }} 
FUNCTION_TARGET=postDeleteProgress ./tob_function &
 
       # 3. Load data in datastore: run CD step on samples/learning-content
       - name: Run CI/CD to populate datastore
diff --git a/learning/tour-of-beam/backend/README.md 
b/learning/tour-of-beam/backend/README.md
index 04c4e938a28..a2928e1f957 100644
--- a/learning/tour-of-beam/backend/README.md
+++ b/learning/tour-of-beam/backend/README.md
@@ -182,3 +182,9 @@ request body:
 $ curl -X POST -H "Authorization: Bearer $token" \
   
"https://$REGION-$PROJECT_ID.cloudfunctions.net/postUserCode?sdk=python&id=challenge1";
 -d @request.json
 ```
+
+### Delete user progress
+```
+$ curl -X POST -H "Authorization: Bearer $token" \
+  "https://$REGION-$PROJECT_ID.cloudfunctions.net/postDeleteProgress"; -d '{}'
+```
\ No newline at end of file
diff --git a/learning/tour-of-beam/backend/auth.go 
b/learning/tour-of-beam/backend/auth.go
index e307c00fe29..2d5a3c241c0 100644
--- a/learning/tour-of-beam/backend/auth.go
+++ b/learning/tour-of-beam/backend/auth.go
@@ -23,13 +23,16 @@ import (
        "net/http"
        "strings"
 
-       tob "beam.apache.org/learning/tour-of-beam/backend/internal"
        "beam.apache.org/learning/tour-of-beam/backend/internal/storage"
        firebase "firebase.google.com/go/v4"
 )
 
-// HandleFunc enriched with sdk and authenticated user uid.
-type HandlerFuncAuthWithSdk func(w http.ResponseWriter, r *http.Request, sdk 
tob.Sdk, uid string)
+// helper to extract uid from context
+// set by ParseAuthHeader middleware
+// panics if key is not found
+func getContextUid(r *http.Request) string {
+       return r.Context().Value(CONTEXT_KEY_UID).(string)
+}
 
 const BEARER_SCHEMA = "Bearer "
 
@@ -53,8 +56,8 @@ func MakeAuthorizer(ctx context.Context, repo storage.Iface) 
*Authorizer {
 }
 
 // middleware to parse authorization header, verify the ID token and extract 
uid.
-func (a *Authorizer) ParseAuthHeader(next HandlerFuncAuthWithSdk) 
HandlerFuncWithSdk {
-       return func(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) {
+func (a *Authorizer) ParseAuthHeader(next http.HandlerFunc) http.HandlerFunc {
+       return func(w http.ResponseWriter, r *http.Request) {
                ctx := r.Context()
                header := r.Header.Get("authorization") // returns "" if no 
header
                if !strings.HasPrefix(header, BEARER_SCHEMA) {
@@ -87,6 +90,7 @@ func (a *Authorizer) ParseAuthHeader(next 
HandlerFuncAuthWithSdk) HandlerFuncWit
                        return
                }
 
-               next(w, r, sdk, uid)
+               ctx = context.WithValue(ctx, CONTEXT_KEY_UID, uid)
+               next(w, r.WithContext(ctx))
        }
 }
diff --git a/learning/tour-of-beam/backend/function.go 
b/learning/tour-of-beam/backend/function.go
index 2e89cd0c9b0..8c761ab63aa 100644
--- a/learning/tour-of-beam/backend/function.go
+++ b/learning/tour-of-beam/backend/function.go
@@ -115,6 +115,7 @@ func init() {
        functions.HTTP("getUserProgress", 
commonGet(ParseSdkParam(auth.ParseAuthHeader(getUserProgress))))
        functions.HTTP("postUnitComplete", 
commonPost(ParseSdkParam(auth.ParseAuthHeader(postUnitComplete))))
        functions.HTTP("postUserCode", 
commonPost(ParseSdkParam(auth.ParseAuthHeader(postUserCode))))
+       functions.HTTP("postDeleteProgress", 
commonPost(auth.ParseAuthHeader(postDeleteProgress)))
 }
 
 // Get list of SDK names
@@ -132,7 +133,8 @@ func getSdkList(w http.ResponseWriter, r *http.Request) {
 
 // Get the content tree for a given SDK
 // Required to be wrapped into ParseSdkParam middleware.
-func getContentTree(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) {
+func getContentTree(w http.ResponseWriter, r *http.Request) {
+       sdk := getContextSdk(r)
        tree, err := svc.GetContentTree(r.Context(), sdk)
        if err != nil {
                log.Println("Get content tree error:", err)
@@ -152,7 +154,8 @@ func getContentTree(w http.ResponseWriter, r *http.Request, 
sdk tob.Sdk) {
 // Everything needed to render a learning unit:
 // description, hints, code snippets
 // Required to be wrapped into ParseSdkParam middleware.
-func getUnitContent(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) {
+func getUnitContent(w http.ResponseWriter, r *http.Request) {
+       sdk := getContextSdk(r)
        unitId := r.URL.Query().Get("id")
 
        unit, err := svc.GetUnitContent(r.Context(), sdk, unitId)
@@ -174,8 +177,11 @@ func getUnitContent(w http.ResponseWriter, r 
*http.Request, sdk tob.Sdk) {
        }
 }
 
-// Get user progress
-func getUserProgress(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid 
string) {
+// Get user progress by sdk and uid
+func getUserProgress(w http.ResponseWriter, r *http.Request) {
+       sdk := getContextSdk(r)
+       uid := getContextUid(r)
+
        progress, err := svc.GetUserProgress(r.Context(), sdk, uid)
 
        if err != nil {
@@ -193,7 +199,9 @@ func getUserProgress(w http.ResponseWriter, r 
*http.Request, sdk tob.Sdk, uid st
 }
 
 // Mark unit completed
-func postUnitComplete(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid 
string) {
+func postUnitComplete(w http.ResponseWriter, r *http.Request) {
+       sdk := getContextSdk(r)
+       uid := getContextUid(r)
        unitId := r.URL.Query().Get("id")
 
        err := svc.SetUnitComplete(r.Context(), sdk, unitId, uid)
@@ -211,7 +219,9 @@ func postUnitComplete(w http.ResponseWriter, r 
*http.Request, sdk tob.Sdk, uid s
 }
 
 // Save user code for unit
-func postUserCode(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid 
string) {
+func postUserCode(w http.ResponseWriter, r *http.Request) {
+       sdk := getContextSdk(r)
+       uid := getContextUid(r)
        unitId := r.URL.Query().Get("id")
 
        var userCodeRequest tob.UserCodeRequest
@@ -239,3 +249,17 @@ func postUserCode(w http.ResponseWriter, r *http.Request, 
sdk tob.Sdk, uid strin
 
        fmt.Fprint(w, "{}")
 }
+
+// Delete user progress
+func postDeleteProgress(w http.ResponseWriter, r *http.Request) {
+       uid := getContextUid(r)
+
+       err := svc.DeleteProgress(r.Context(), uid)
+       if err != nil {
+               log.Println("Delete progress error:", err)
+               finalizeErrResponse(w, http.StatusInternalServerError, 
INTERNAL_ERROR, "storage error")
+               return
+       }
+
+       fmt.Fprint(w, "{}")
+}
diff --git a/learning/tour-of-beam/backend/integration_tests/auth_test.go 
b/learning/tour-of-beam/backend/integration_tests/auth_test.go
index 6b0e4df98d2..021b97a03bf 100644
--- a/learning/tour-of-beam/backend/integration_tests/auth_test.go
+++ b/learning/tour-of-beam/backend/integration_tests/auth_test.go
@@ -84,6 +84,13 @@ func TestSaveGetProgress(t *testing.T) {
        }
        getUserProgressURL := "http://localhost:"; + port
 
+       // postDeleteProgressURL
+       port = os.Getenv(PORT_POST_DELETE_PROGRESS)
+       if port == "" {
+               t.Fatal(PORT_POST_DELETE_PROGRESS, "env not set")
+       }
+       postDeleteProgressURL := "http://localhost:"; + port
+
        t.Run("save_complete_no_unit", func(t *testing.T) {
                resp, err := PostUnitComplete(postUnitCompleteURL, "python", 
"unknown_unit_id_1", idToken)
                checkBadHttpCode(t, err, http.StatusNotFound)
@@ -142,6 +149,25 @@ func TestSaveGetProgress(t *testing.T) {
                exp.Units[1].UserSnippetId = resp.Units[1].UserSnippetId
                assert.Equal(t, exp, resp)
        })
+       t.Run("delete_progress", func(t *testing.T) {
+               _, err := PostDeleteProgress(postDeleteProgressURL, idToken)
+               if err != nil {
+                       t.Fatal(err)
+               }
+       })
+       t.Run("delete_progress_retry", func(t *testing.T) {
+               _, err := PostDeleteProgress(postDeleteProgressURL, idToken)
+               if err != nil {
+                       t.Fatal(err)
+               }
+       })
+       t.Run("get_deleted", func(t *testing.T) {
+               resp, err := GetUserProgress(getUserProgressURL, "python", 
idToken)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               assert.Equal(t, 0, len(resp.Units))
+       })
 }
 
 func TestUserCode(t *testing.T) {
diff --git a/learning/tour-of-beam/backend/integration_tests/client.go 
b/learning/tour-of-beam/backend/integration_tests/client.go
index 5058929eca4..956c67dde5d 100644
--- a/learning/tour-of-beam/backend/integration_tests/client.go
+++ b/learning/tour-of-beam/backend/integration_tests/client.go
@@ -112,6 +112,13 @@ func PostUserCode(url, sdk, unitId, token string, body 
UserCodeRequest) (ErrorRe
        return result, err
 }
 
+func PostDeleteProgress(url, token string) (ErrorResponse, error) {
+       var result ErrorResponse
+       err := Do(&result, http.MethodPost, url, nil,
+               map[string]string{"Authorization": "Bearer " + token}, nil)
+       return result, err
+}
+
 func Post(dst interface{}, url string, queryParams, headers map[string]string, 
body io.Reader) error {
        if err := Options(http.MethodPost, url, queryParams); err != nil {
                return fmt.Errorf("pre-flight request error: %w", err)
diff --git a/learning/tour-of-beam/backend/integration_tests/function_test.go 
b/learning/tour-of-beam/backend/integration_tests/function_test.go
index ee99769f8e8..813bbdbedd4 100644
--- a/learning/tour-of-beam/backend/integration_tests/function_test.go
+++ b/learning/tour-of-beam/backend/integration_tests/function_test.go
@@ -26,12 +26,13 @@ import (
 )
 
 const (
-       PORT_SDK_LIST           = "PORT_SDK_LIST"
-       PORT_GET_CONTENT_TREE   = "PORT_GET_CONTENT_TREE"
-       PORT_GET_UNIT_CONTENT   = "PORT_GET_UNIT_CONTENT"
-       PORT_GET_USER_PROGRESS  = "PORT_GET_USER_PROGRESS"
-       PORT_POST_UNIT_COMPLETE = "PORT_POST_UNIT_COMPLETE"
-       PORT_POST_USER_CODE     = "PORT_POST_USER_CODE"
+       PORT_SDK_LIST             = "PORT_SDK_LIST"
+       PORT_GET_CONTENT_TREE     = "PORT_GET_CONTENT_TREE"
+       PORT_GET_UNIT_CONTENT     = "PORT_GET_UNIT_CONTENT"
+       PORT_GET_USER_PROGRESS    = "PORT_GET_USER_PROGRESS"
+       PORT_POST_UNIT_COMPLETE   = "PORT_POST_UNIT_COMPLETE"
+       PORT_POST_USER_CODE       = "PORT_POST_USER_CODE"
+       PORT_POST_DELETE_PROGRESS = "PORT_POST_DELETE_PROGRESS"
 )
 
 // scenarios:
diff --git a/learning/tour-of-beam/backend/integration_tests/local.sh 
b/learning/tour-of-beam/backend/integration_tests/local.sh
index a28032ac0cb..35eb1f77719 100644
--- a/learning/tour-of-beam/backend/integration_tests/local.sh
+++ b/learning/tour-of-beam/backend/integration_tests/local.sh
@@ -31,6 +31,7 @@ export PORT_GET_UNIT_CONTENT=8803
 export PORT_GET_USER_PROGRESS=8804
 export PORT_POST_UNIT_COMPLETE=8805
 export PORT_POST_USER_CODE=8806
+export PORT_POST_DELETE_PROGRESS=8807
 
 mkdir "$DATASTORE_EMULATOR_DATADIR"
 
@@ -44,6 +45,7 @@ PORT=$PORT_GET_UNIT_CONTENT FUNCTION_TARGET=getUnitContent  
./tob_function &
 PORT=$PORT_GET_USER_PROGRESS FUNCTION_TARGET=getUserProgress ./tob_function &
 PORT=$PORT_POST_UNIT_COMPLETE FUNCTION_TARGET=postUnitComplete ./tob_function &
 PORT=$PORT_POST_USER_CODE FUNCTION_TARGET=postUserCode ./tob_function &
+PORT=$PORT_POST_DELETE_PROGRESS FUNCTION_TARGET=postDeleteProgress 
./tob_function &
 
 sleep 5
 
diff --git a/learning/tour-of-beam/backend/internal/service/content.go 
b/learning/tour-of-beam/backend/internal/service/content.go
index 4dffa6da731..b62be44d961 100644
--- a/learning/tour-of-beam/backend/internal/service/content.go
+++ b/learning/tour-of-beam/backend/internal/service/content.go
@@ -31,6 +31,7 @@ type IContent interface {
        GetUserProgress(ctx context.Context, sdk tob.Sdk, userId string) 
(tob.SdkProgress, error)
        SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) 
error
        SaveUserCode(ctx context.Context, sdk tob.Sdk, unitId, uid string, 
userRequest tob.UserCodeRequest) error
+       DeleteProgress(ctx context.Context, uid string) error
 }
 
 type Svc struct {
@@ -102,3 +103,7 @@ func (s *Svc) SaveUserCode(ctx context.Context, sdk 
tob.Sdk, unitId, uid string,
 
        return s.Repo.SaveUserSnippetId(ctx, sdk, unitId, uid, savePgSnippet)
 }
+
+func (s *Svc) DeleteProgress(ctx context.Context, uid string) error {
+       return s.Repo.DeleteProgress(ctx, uid)
+}
diff --git a/learning/tour-of-beam/backend/internal/storage/datastore.go 
b/learning/tour-of-beam/backend/internal/storage/datastore.go
index 6f01332c50f..4c95384a9a0 100644
--- a/learning/tour-of-beam/backend/internal/storage/datastore.go
+++ b/learning/tour-of-beam/backend/internal/storage/datastore.go
@@ -373,5 +373,33 @@ func (d *DatastoreDb) SaveUserSnippetId(
        return d.upsertUnitProgress(ctx, sdk, unitId, uid, applyChanges)
 }
 
+func (d *DatastoreDb) DeleteProgress(ctx context.Context, uid string) error {
+       userKey := pgNameKey(TbUserKind, uid, nil)
+
+       _, err := d.Client.RunInTransaction(ctx, func(tx 
*datastore.Transaction) error {
+               query := datastore.NewQuery(TbUserProgressKind).
+                       Namespace(PgNamespace).
+                       Ancestor(userKey).
+                       KeysOnly().
+                       Transaction(tx)
+               keys, err := d.Client.GetAll(ctx, query, nil)
+               if err != nil {
+                       return fmt.Errorf("query tb_user_progress: %w", err)
+               }
+               log.Printf("deleting %v tb_user_progress entities\n", len(keys))
+               if err := tx.DeleteMulti(keys); err != nil {
+                       return fmt.Errorf("delete %v enitities 
tb_user_progress: %w", len(keys), err)
+               }
+               if err := tx.Delete(userKey); err != nil {
+                       return fmt.Errorf("delete tb_user: %w", err)
+               }
+               return nil
+       })
+       if err != nil {
+               return fmt.Errorf("failed to commit: %w", err)
+       }
+       return nil
+}
+
 // check if the interface is implemented.
 var _ Iface = &DatastoreDb{}
diff --git a/learning/tour-of-beam/backend/internal/storage/iface.go 
b/learning/tour-of-beam/backend/internal/storage/iface.go
index 1cfc6f631d5..bd1c617dd76 100644
--- a/learning/tour-of-beam/backend/internal/storage/iface.go
+++ b/learning/tour-of-beam/backend/internal/storage/iface.go
@@ -37,4 +37,6 @@ type Iface interface {
                ctx context.Context, sdk tob.Sdk, unitId, uid string,
                externalSave func(string) (string, error),
        ) error
+
+       DeleteProgress(ctx context.Context, uid string) error
 }
diff --git a/learning/tour-of-beam/backend/internal/storage/mock.go 
b/learning/tour-of-beam/backend/internal/storage/mock.go
index d93e1e32e3f..2a39c774bd2 100644
--- a/learning/tour-of-beam/backend/internal/storage/mock.go
+++ b/learning/tour-of-beam/backend/internal/storage/mock.go
@@ -89,3 +89,7 @@ func (d *Mock) SaveUserSnippetId(
 ) error {
        return nil
 }
+
+func (d *Mock) DeleteProgress(ctx context.Context, uid string) error {
+       return nil
+}
diff --git a/learning/tour-of-beam/backend/middleware.go 
b/learning/tour-of-beam/backend/middleware.go
index 71a43c6229f..1f8c9f52fa8 100644
--- a/learning/tour-of-beam/backend/middleware.go
+++ b/learning/tour-of-beam/backend/middleware.go
@@ -18,6 +18,7 @@
 package tob
 
 import (
+       "context"
        "log"
        "net/http"
 
@@ -31,6 +32,22 @@ const (
        UNAUTHORIZED   = "UNAUTHORIZED"
 )
 
+// this subtypes here to pass go-staticcheck
+type _ContextKeyTypeSdk string
+type _ContextKeyTypeUid string
+
+const (
+       CONTEXT_KEY_SDK _ContextKeyTypeSdk = "sdk"
+       CONTEXT_KEY_UID _ContextKeyTypeUid = "uid"
+)
+
+// helper to extract sdk from context
+// set by ParseSdkParam middleware
+// panics if key is not found
+func getContextSdk(r *http.Request) tob.Sdk {
+       return r.Context().Value(CONTEXT_KEY_SDK).(tob.Sdk)
+}
+
 // Middleware-maker for setting a header
 // We also make this less generic: it works with HandlerFunc's
 // so that to be convertible to func(w http ResponseWriter, r *http.Request)
@@ -94,11 +111,8 @@ func Common(method string) func(http.HandlerFunc) 
http.HandlerFunc {
        }
 }
 
-// HandleFunc enriched with sdk.
-type HandlerFuncWithSdk func(w http.ResponseWriter, r *http.Request, sdk 
tob.Sdk)
-
 // middleware to parse sdk query param and pass it as additional handler param.
-func ParseSdkParam(next HandlerFuncWithSdk) http.HandlerFunc {
+func ParseSdkParam(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                sdkStr := r.URL.Query().Get("sdk")
                sdk := tob.ParseSdk(sdkStr)
@@ -109,6 +123,7 @@ func ParseSdkParam(next HandlerFuncWithSdk) 
http.HandlerFunc {
                        return
                }
 
-               next(w, r, sdk)
+               ctx := context.WithValue(r.Context(), CONTEXT_KEY_SDK, sdk)
+               next(w, r.WithContext(ctx))
        }
 }

Reply via email to