diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index ce6950890fbc..d43af660221c 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 04c4e938a280..a2928e1f9572 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 e307c00fe293..2d5a3c241c0d 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 2e89cd0c9b05..8c761ab63aa0 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 6b0e4df98d29..021b97a03bfb 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 5058929eca46..956c67dde5d6 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 ee99769f8e83..813bbdbedd42 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 a28032ac0cbc..35eb1f77719c 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 4dffa6da7319..b62be44d961e 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 6f01332c50f6..4c95384a9a0b 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 1cfc6f631d5c..bd1c617dd76a 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 d93e1e32e3fd..2a39c774bd2b 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 71a43c6229f9..1f8c9f52fa80 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)) } }