Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tour_of_beam_backend_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions learning/tour-of-beam/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '{}'
```
16 changes: 10 additions & 6 deletions learning/tour-of-beam/backend/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "

Expand All @@ -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) {
Expand Down Expand Up @@ -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))
}
}
36 changes: 30 additions & 6 deletions learning/tour-of-beam/backend/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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, "{}")
}
26 changes: 26 additions & 0 deletions learning/tour-of-beam/backend/integration_tests/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions learning/tour-of-beam/backend/integration_tests/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 7 additions & 6 deletions learning/tour-of-beam/backend/integration_tests/function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions learning/tour-of-beam/backend/integration_tests/local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions learning/tour-of-beam/backend/internal/service/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
28 changes: 28 additions & 0 deletions learning/tour-of-beam/backend/internal/storage/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
2 changes: 2 additions & 0 deletions learning/tour-of-beam/backend/internal/storage/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions learning/tour-of-beam/backend/internal/storage/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@ func (d *Mock) SaveUserSnippetId(
) error {
return nil
}

func (d *Mock) DeleteProgress(ctx context.Context, uid string) error {
return nil
}
25 changes: 20 additions & 5 deletions learning/tour-of-beam/backend/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package tob

import (
"context"
"log"
"net/http"

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
}