diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1be93a08cc..4eb0f109c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,8 @@ on: jobs: unit-test: - runs-on: ubuntu-latest + #runs-on: ubuntu-latest + runs-on: self-hosted if: ${{ !github.event.pull_request.draft }} # only run on non-draft PRs steps: - name: Checkout @@ -26,9 +27,6 @@ jobs: version: '23.4' repo-token: ${{ secrets.KWIL_MACH_SECRET }} -# - name: Install swagger-codegen -# uses: swagger-api/swagger-codegen/.github/actions/generate/action.yml@main - - name: Install Taskfile uses: arduino/setup-task@v1 with: @@ -77,8 +75,9 @@ jobs: run: | task test:unit - acceptance-test: - runs-on: ubuntu-latest + integration-test: + #runs-on: ubuntu-latest + runs-on: self-hosted if: ${{ !github.event.pull_request.draft }} # only run on non-draft PRs steps: - name: Checkout @@ -150,6 +149,30 @@ jobs: run: | docker pull kwilbrennan/extensions-math:multi-arch --platform linux/amd64 + - name: pull kgw repo & create vendor + # we only pull the repo, not build the image, because we want to use the cache + # provided by the docker/build-push-action + # vendor is used to bypass private repo issues + run: | + rm -rf /tmp/kgw + git clone https://github.com/kwilteam/kgw.git /tmp/kgw + cd /tmp/kgw + go mod vendor + cd - + + - name: Build kgw image + id: docker_build_kgw + uses: docker/build-push-action@v4 + with: + context: /tmp/kgw + load: true + builder: ${{ steps.buildx.outputs.name }} + file: /tmp/kgw/Dockerfile + push: false + tags: kgw:latest + cache-from: type=local,src=/tmp/.buildx-cache-kgw + cache-to: type=local,dest=/tmp/.buildx-cache-kgw-new + - name: Build kwild image id: docker_build_kwild uses: docker/build-push-action@v4 @@ -167,6 +190,7 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache-kwild cache-to: type=local,dest=/tmp/.buildx-cache-kwild-new + # maybe no need? - name: Run acceptance test run: | echo "UID=$(id -u)" >> test/acceptance/.env @@ -175,7 +199,17 @@ jobs: echo "KACT_DOCKER_COMPOSE_OVERRIDE_FILE=docker-compose.override.yml" >> test/acceptance/.env task test:act:nb + - name: Run integration test + run: | + echo "UID=$(id -u)" >> test/integration/.env + echo "GID=$(id -g)" >> test/integration/.env + cp test/integration/docker-compose.override.yml.example test/integration/docker-compose.override.yml + echo "KIT_DOCKER_COMPOSE_OVERRIDE_FILE=docker-compose.override.yml" >> test/integration/.env + task test:it:nb:all + - name: Move cache run: | rm -rf /tmp/.buildx-cache-kwild mv /tmp/.buildx-cache-kwild-new /tmp/.buildx-cache-kwild + rm -rf /tmp/.buildx-cache-kgw + mv /tmp/.buildx-cache-kgw-new /tmp/.buildx-cache-kgw diff --git a/.github/workflows/on-main.yml b/.github/workflows/on-main.yml deleted file mode 100644 index f531a3c79f..0000000000 --- a/.github/workflows/on-main.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: on-main - -on: - workflow_dispatch: - push: - branches: - - main - -jobs: - build-push-image: - name: Build & push image - if: false # temporary disable - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: true - token: ${{ secrets.KWIL_MACH_SECRET }} - - - name: Install Protoc - uses: arduino/setup-protoc@v2 - with: - version: '23.4' - repo-token: ${{ secrets.KWIL_MACH_SECRET }} - - - name: Install Taskfile - uses: arduino/setup-task@v1 - with: - repo-token: ${{ secrets.KWIL_MACH_SECRET }} - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' - check-latest: true - - - name: Install dependencies - env: - GH_ACCESS_TOKEN: ${{ secrets.KWIL_MACH_SECRET }} - run: | - go version - git config --global url."https://${GH_ACCESS_TOKEN}:x-oauth-basic@github.com/kwilteam/".insteadOf "https://github.com/kwilteam/" - task install:deps - go mod download - - - name: Generate go vendor - #for faster builds and private repos, need to run this after pb:compile:v1 - run: | - task vendor - - - name: manual git tag - run: | - version=`echo ${{ github.sha }} | cut -c 1-7` - echo "GIT_TAG=$version" >> $GITHUB_ENV - #run: echo "GIT_TAG=`git describe --match 'v[0-9]*' --dirty --always --tags | sed 's/^v//'`" >> $GITHUB_ENV - - - name: manual build time - run: | - build_time=`TZ=UTC date -u --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +"%Y-%m-%dT%H:%M:%SZ"` - echo "BUILD_TIME=$build_time" >> $GITHUB_ENV - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Cache Docker layers for KD - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache-kwild - #key: ${{ runner.os }}-buildx-kwild-${{ github.sha }} - key: ${{ runner.os }}-buildx-kwild - restore-keys: | - ${{ runner.os }}-buildx-kwild - - - name: Build & push KD image - id: docker_build_kwild - uses: docker/build-push-action@v4 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - build-args: | - git_commit=${{ github.sha }} - version=${{ env.GIT_TAG }} - build_time=${{ env.BUILD_TIME }} - file: ./build/package/docker/kwild.dockerfile - push: true - tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ secrets.AWS_KWILD_ECR }}:${{ env.GIT_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-kwild - cache-to: type=local,dest=/tmp/.buildx-cache-kwild-new - - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache-kwild - mv /tmp/.buildx-cache-kwild-new /tmp/.buildx-cache-kwild - - deploy-to-eks: - name: Deploy to DEV cluster - runs-on: ubuntu-latest - needs: build-push-image - - steps: - - name: manual git tag - run: | - version=`echo ${{ github.sha }} | cut -c 1-7` - echo "GIT_TAG=$version" >> $GITHUB_ENV - - - name: deploy by release type to eks DEV cluster - run: | - echo "deploy ${{ env.GIT_TAG }} to eks DEV cluster" \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index b3d136a9e6..7a6795bf4d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -176,7 +176,7 @@ tasks: desc: Start the dev environment(with testnet) without rebuilding docker image dir: test # different module cmds: - - go test ./integration -timeout 12h -dev -v + - go test ./integration -run ^TestLocalDevSetup$ -timeout 12h -dev -v # ************ test ************ # test with build:docker task support passing CLI_ARGS to go test, e.g. task test:act -- -debug @@ -216,7 +216,7 @@ tasks: - go test ./... -tags=ext_test -count=1 -race test:it: - desc: Run integration tests + desc: Run integration tests ('short' mode) deps: - task: build:cli - task: build:admin @@ -225,6 +225,12 @@ tasks: - task: test:it:nb test:it:nb: + desc: Run integration tests ('short' mode) + dir: test # different module + cmds: + - go test -short -count=1 -timeout 0 ./integration -v {{.CLI_ARGS}} + + test:it:nb:all: desc: Run integration tests dir: test # different module cmds: diff --git a/core/rpc/client/user/http/client.go b/core/rpc/client/user/http/client.go index 1f6d264d2e..3c75b9d1dc 100644 --- a/core/rpc/client/user/http/client.go +++ b/core/rpc/client/user/http/client.go @@ -81,12 +81,16 @@ func (c *Client) Broadcast(ctx context.Context, tx *transactions.Transaction, sy // ErrInsufficientBalance, ErrWrongChain, etc. but how? the response // body had better have retained the response error details! if res != nil { - // fmt.Println("broadcast", res.StatusCode, res.Status) if swaggerErr, ok := err.(httpTx.GenericSwaggerError); ok { body := swaggerErr.Body() // fmt.Println(string(body)) - return nil, parseBroadcastError(body) + if ok, _err := parseBroadcastError(body); ok { + return nil, _err + } else { + return nil, err // return the original error(unparsed) + } } } + return nil, err } defer res.Body.Close() @@ -265,12 +269,14 @@ func (c *Client) Query(ctx context.Context, dbid string, query string) ([]map[st return unmarshalMapResults(decodedResult) } -func parseBroadcastError(respTxt []byte) error { +// parseBroadcastError parses the response body from a broadcast error. +// It returns true if the error was parsed successfully, false otherwise. +func parseBroadcastError(respTxt []byte) (bool, error) { var protoStatus status.Status err := protojson.Unmarshal(respTxt, &protoStatus) // jsonpb is deprecated, otherwise we could use the resp.Body directly if err != nil { if err = json.Unmarshal(respTxt, &protoStatus); err != nil { - return err + return false, err } } stat := grpcStatus.FromProto(&protoStatus) @@ -308,8 +314,7 @@ func parseBroadcastError(respTxt []byte) error { } } - return err - + return true, err } func parseErrorResponse(respTxt []byte) error { diff --git a/internal/abci/abci.go b/internal/abci/abci.go index c54a66124e..ebcf8793bc 100644 --- a/internal/abci/abci.go +++ b/internal/abci/abci.go @@ -211,7 +211,8 @@ func (a *AbciApp) CheckTx(ctx context.Context, incoming *abciTypes.RequestCheckT // Verify the correct chain ID is set, if it is set. if protected := tx.Body.ChainID != ""; protected && tx.Body.ChainID != a.cfg.ChainID { code = codeWrongChain - logger.Info("wrong chain ID", zap.String("payloadType", tx.Body.PayloadType.String())) + logger.Info("wrong chain ID", + zap.String("payloadType", tx.Body.PayloadType.String())) return &abciTypes.ResponseCheckTx{Code: code.Uint32(), Log: "wrong chain ID"}, nil } // Verify Payload type @@ -263,7 +264,7 @@ func (a *AbciApp) executeTx(ctx context.Context, rawTx []byte, logger *log.Logge Code: code.Uint32(), GasUsed: gasUsed, Events: events, - Log: "success", + Log: "", // Data, GasWanted, Info, Codespace } diff --git a/internal/services/grpc/txsvc/v1/call.go b/internal/services/grpc/txsvc/v1/call.go index 20270af771..26e3eef0b3 100644 --- a/internal/services/grpc/txsvc/v1/call.go +++ b/internal/services/grpc/txsvc/v1/call.go @@ -13,6 +13,7 @@ import ( func (s *Service) Call(ctx context.Context, req *txpb.CallRequest) (*txpb.CallResponse, error) { body, msg, err := convertActionCall(req) if err != nil { + // NOTE: http api needs to be able to get the error message return nil, status.Errorf(codes.InvalidArgument, "failed to convert action call: %s", err.Error()) } diff --git a/test/acceptance/docker-compose.yml b/test/acceptance/docker-compose.yml index 2327e9724b..f7730357ee 100644 --- a/test/acceptance/docker-compose.yml +++ b/test/acceptance/docker-compose.yml @@ -12,7 +12,7 @@ services: - "26657:26657" - "40000:40000" # debugger env_file: - - .env # docker compose by default will use this file, here just make it explicit + - .env # docker compose by default will use this file if presented, here just make it explicit volumes: - type: bind source: ${KWIL_HOME:-./.testnode} @@ -26,7 +26,7 @@ services: --root-dir=/app/kwil --log.level=${LOG_LEVEL:-info} --app.extension-endpoints=ext:50051 - --app.admin-listen-addr=unix:///var/run/kwil/admin.sock + --app.admin-listen-addr=unix:///tmp/admin.sock --app.grpc-listen-addr=:50051 --app.http-listen-addr=:8080 --chain.p2p.external-address=tcp://0.0.0.0:26656 diff --git a/test/acceptance/helper.go b/test/acceptance/helper.go index 53555a0dc8..f6b33d17db 100644 --- a/test/acceptance/helper.go +++ b/test/acceptance/helper.go @@ -40,10 +40,10 @@ const TestChainID = "kwil-test-chain" // ActTestCfg is the config for acceptance test type ActTestCfg struct { - HTTPEndpoint string - GrpcEndpoint string - P2PAddress string // cometbft p2p address - AdminClientUnixSocket string + HTTPEndpoint string + GrpcEndpoint string + P2PAddress string // cometbft p2p address + AdminRPC string // tcp or unix socket SchemaFile string DockerComposeFile string @@ -136,7 +136,7 @@ func (r *ActHelper) LoadConfig() *ActTestCfg { HTTPEndpoint: getEnv("KACT_HTTP_ENDPOINT", "http://localhost:8080"), GrpcEndpoint: getEnv("KACT_GRPC_ENDPOINT", "localhost:50051"), P2PAddress: getEnv("KACT_CHAIN_ENDPOINT", "tcp://0.0.0.0:26656"), - AdminClientUnixSocket: getEnv("KACT_ADMIN_CLIENT_UNIX_SOCKET", "tcp://localhost:50151"), + AdminRPC: getEnv("KACT_ADMIN_RPC", "unix:///tmp/admin.sock"), DockerComposeFile: getEnv("KACT_DOCKER_COMPOSE_FILE", "./docker-compose.yml"), DockerComposeOverrideFile: getEnv("KACT_DOCKER_COMPOSE_OVERRIDE_FILE", "./docker-compose.override.yml"), } @@ -323,7 +323,7 @@ func (r *ActHelper) getHTTPClientDriver(signer auth.Signer) KwilAcceptanceDriver }) require.NoError(r.t, err, "failed to create http client") - return driver.NewKwildClientDriver(kwilClt, logger) + return driver.NewKwildClientDriver(kwilClt, signer, logger) } func (r *ActHelper) getGRPCClientDriver(signer auth.Signer) KwilAcceptanceDriver { @@ -340,7 +340,7 @@ func (r *ActHelper) getGRPCClientDriver(signer auth.Signer) KwilAcceptanceDriver }) require.NoError(r.t, err, "failed to create grpc client") - return driver.NewKwildClientDriver(kwilClt, logger) + return driver.NewKwildClientDriver(kwilClt, signer, logger) } func (r *ActHelper) getCliDriver(privKey string, identifier []byte) KwilAcceptanceDriver { @@ -350,5 +350,5 @@ func (r *ActHelper) getCliDriver(privKey string, identifier []byte) KwilAcceptan cliBinPath := path.Join(path.Dir(currentFilePath), fmt.Sprintf("../../.build/kwil-cli-%s-%s", runtime.GOOS, runtime.GOARCH)) - return driver.NewKwilCliDriver(cliBinPath, r.cfg.HTTPEndpoint, privKey, TestChainID, identifier, logger) + return driver.NewKwilCliDriver(cliBinPath, r.cfg.HTTPEndpoint, privKey, TestChainID, identifier, false, logger) } diff --git a/test/acceptance/kwild_test.go b/test/acceptance/kwild_test.go index 5eef860d09..32f4f0d5cd 100644 --- a/test/acceptance/kwild_test.go +++ b/test/acceptance/kwild_test.go @@ -18,7 +18,7 @@ var dev = flag.Bool("dev", false, "run for development purpose (no tests)") var remote = flag.Bool("remote", false, "test against remote node") var noCleanup = flag.Bool("messy", false, "do not cleanup test directories or stop the docker compose when done") -var drivers = flag.String("drivers", "grpc,cli", "comma separated list of drivers to run") +var drivers = flag.String("drivers", "http,cli", "comma separated list of drivers to run") func TestKwildTransferAcceptance(t *testing.T) { if testing.Short() { diff --git a/test/acceptance/test-data/test_db.kf b/test/acceptance/test-data/test_db.kf index 07b54c43a3..52d4b0de1c 100644 --- a/test/acceptance/test-data/test_db.kf +++ b/test/acceptance/test-data/test_db.kf @@ -95,19 +95,12 @@ action get_user_posts($username) public { ); } -@kgw(authn='true') -action get_post_authenticated($id) public view { +action get_post($id) public view { SELECT * FROM posts WHERE id = $id; } -action get_post_unauthenticated($id) public view { - SELECT * - FROM "posts" - WHERE id = $id; -} - action multi_select() public { SELECT * FROM posts; @@ -121,7 +114,6 @@ action divide($numerator1, $numerator2, $denominator) public view { select $up AS upper_value, $down AS lower_value; } -@kgw(authn='true') action owner_only() public owner view { select 'owner only'; } @@ -135,4 +127,9 @@ action create_post_private($id, $title, $content) private { action create_post_nested($id, $title, $content) public { create_post_private($id, $title, $content); +} + +@kgw(authn='true') +action authn_only() public view { + select 'authn only'; } \ No newline at end of file diff --git a/test/driver/cli_driver.go b/test/driver/cli_driver.go index cc4f2a35f9..15aa9446ad 100644 --- a/test/driver/cli_driver.go +++ b/test/driver/cli_driver.go @@ -25,25 +25,28 @@ import ( // KwilCliDriver is a driver for tests using `cmd/kwil-cli` type KwilCliDriver struct { - cliBin string // kwil-cli binary path - rpcUrl string - privKey string - identity []byte - chainID string - logger log.Logger + cliBin string // kwil-cli binary path + rpcUrl string + privKey string + identity []byte + gatewayProvider bool + chainID string + logger log.Logger } -func NewKwilCliDriver(cliBin, rpcUrl, privKey, chainID string, identity []byte, logger log.Logger) *KwilCliDriver { +func NewKwilCliDriver(cliBin, rpcUrl, privKey, chainID string, identity []byte, gatewayProvider bool, logger log.Logger) *KwilCliDriver { return &KwilCliDriver{ - cliBin: cliBin, - rpcUrl: rpcUrl, - privKey: privKey, - identity: identity, - logger: logger, - chainID: chainID, + cliBin: cliBin, + rpcUrl: rpcUrl, + privKey: privKey, + identity: identity, + gatewayProvider: gatewayProvider, + logger: logger, + chainID: chainID, } } +// newKwilCliCmd returns a new exec.Cmd for kwil-cli func (d *KwilCliDriver) newKwilCliCmd(args ...string) *exec.Cmd { args = append(args, "--kwil-provider", d.rpcUrl) args = append(args, "--private-key", d.privKey) @@ -57,8 +60,27 @@ func (d *KwilCliDriver) newKwilCliCmd(args ...string) *exec.Cmd { return cmd } +// newKwilCliCmdWithYes this is a helper function to automatically answer yes to +// all prompts. This is useful for testing. +// The cmd will be executed as `yes | kwil-cli ` +func (d *KwilCliDriver) newKwilCliCmdWithYes(args ...string) *exec.Cmd { + args = append([]string{"yes |", d.cliBin}, args...) + + args = append(args, "--kwil-provider", d.rpcUrl) + args = append(args, "--private-key", d.privKey) + args = append(args, "--chain-id", d.chainID) + args = append(args, "--output", "json") + + s := strings.Join(args, " ") + + d.logger.Info("cli Cmd(with yes)", zap.String("args", + strings.Join(append([]string{"bash", "-c"}, s), " "))) + + cmd := exec.Command("bash", "-c", s) + return cmd +} + // SupportBatch -// kwil-cli does not support batched inputs. func (d *KwilCliDriver) SupportBatch() bool { return false } @@ -260,6 +282,7 @@ func (d *KwilCliDriver) prepareCliActionParams(dbid string, actionName string, a args := []string{} for i, input := range action.Inputs { + input = input[1:] // remove the leading $ args = append(args, fmt.Sprintf("%s:%v", input, actionInputs[i])) } return args, nil @@ -304,7 +327,7 @@ func (d *KwilCliDriver) QueryDatabase(_ context.Context, dbid, query string) (*c return records, nil } -func (d *KwilCliDriver) Call(_ context.Context, dbid, action string, inputs []any, withSignature bool) (*client.Records, error) { +func (d *KwilCliDriver) Call(_ context.Context, dbid, action string, inputs []any) (*client.Records, error) { // NOTE: kwil-cli does not support batched inputs actionInputs, err := d.prepareCliActionParams(dbid, action, inputs) if err != nil { @@ -314,12 +337,12 @@ func (d *KwilCliDriver) Call(_ context.Context, dbid, action string, inputs []an args := []string{"database", "call", "--dbid", dbid, "--action", action} args = append(args, actionInputs...) - if withSignature { + if d.gatewayProvider { args = append(args, "--authenticate") } - cmd := d.newKwilCliCmd(args...) - out, err := mustRun(cmd, d.logger) + cmd := d.newKwilCliCmdWithYes(args...) + out, err := mustRunCallIgnorePrompt(cmd, d.logger) if err != nil { return nil, fmt.Errorf("failed to call action: %w", err) } @@ -356,8 +379,7 @@ func (d *KwilCliDriver) ChainInfo(_ context.Context) (*types.ChainInfo, error) { // mustRun runs the give command, and parse stdout func mustRun(cmd *exec.Cmd, logger log.Logger) (*cliResponse, error) { cmd.Stderr = os.Stderr - //cmd.Stdout = os.Stdout - //// here we ignore the stdout + //// here we capture the stdout var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() @@ -381,6 +403,43 @@ func mustRun(cmd *exec.Cmd, logger log.Logger) (*cliResponse, error) { return result, nil } +// mustRunCallIgnorePrompt runs the given `kwil-cli database call` command, and +// throw away the prompt output. This is necessary for authn call, because +// kwil-cli will prompt for confirmation. +func mustRunCallIgnorePrompt(cmd *exec.Cmd, logger log.Logger) (*cliResponse, error) { + cmd.Stderr = os.Stderr + //// here we capture the stdout + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + output := out.Bytes() + // logger.Debug("cmd output", zap.String("output", string(output))) + + // This is a bit hacky, throw away the first part prompt output, if any + prompted := "Do you want to sign this message?" + delimiter := "{\n" + if strings.Contains(string(output), prompted) { + logger.Debug("throw away prompt output") + output = []byte(delimiter + strings.SplitN(string(output), delimiter, 2)[1]) + } + + var result *cliResponse + err = json.Unmarshal(output, &result) + if err != nil { + return nil, err + } + + if result.Error != "" { + return nil, errors.New(result.Error) + } + + return result, nil +} + type cliResponse struct { Result any `json:"result"` // json.RawMessage Error string `json:"error"` diff --git a/test/driver/client_driver.go b/test/driver/client_driver.go index def391722a..7a2916215a 100644 --- a/test/driver/client_driver.go +++ b/test/driver/client_driver.go @@ -8,13 +8,14 @@ import ( "math/big" "os" + "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" "github.com/kwilteam/kwil-db/core/client" + "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" rpcClient "github.com/kwilteam/kwil-db/core/rpc/client" "github.com/kwilteam/kwil-db/core/types" "github.com/kwilteam/kwil-db/core/types/transactions" "github.com/kwilteam/kwil-db/core/utils" - "go.uber.org/zap" ) @@ -28,13 +29,15 @@ func GetEnv(key, defaultValue string) string { // KwildClientDriver is driver for tests using the `client` package type KwildClientDriver struct { - clt *client.Client + clt common.Client + signer auth.Signer logger log.Logger } -func NewKwildClientDriver(clt *client.Client, logger log.Logger) *KwildClientDriver { +func NewKwildClientDriver(clt common.Client, signer auth.Signer, logger log.Logger) *KwildClientDriver { driver := &KwildClientDriver{ clt: clt, + signer: signer, logger: logger, } @@ -46,7 +49,7 @@ func (d *KwildClientDriver) SupportBatch() bool { } func (d *KwildClientDriver) GetUserPublicKey() []byte { - return d.clt.Signer.Identity() + return d.signer.Identity() } // TxSuccess checks if the transaction was successful @@ -88,7 +91,7 @@ func (d *KwildClientDriver) TransferAmt(ctx context.Context, to []byte, amt *big } func (d *KwildClientDriver) DBID(name string) string { - return utils.GenerateDBID(name, d.clt.Signer.Identity()) + return utils.GenerateDBID(name, d.signer.Identity()) } func (d *KwildClientDriver) DeployDatabase(ctx context.Context, db *transactions.Schema) ([]byte, error) { @@ -98,7 +101,7 @@ func (d *KwildClientDriver) DeployDatabase(ctx context.Context, db *transactions } d.logger.Debug("deployed database", - zap.String("name", db.Name), zap.String("owner", hex.EncodeToString(d.clt.Signer.Identity())), + zap.String("name", db.Name), zap.String("owner", hex.EncodeToString(d.signer.Identity())), zap.String("TxHash", rec.Hex())) return rec, nil } @@ -158,8 +161,7 @@ func (d *KwildClientDriver) QueryDatabase(ctx context.Context, dbid, query strin return d.clt.Query(ctx, dbid, query) } -func (d *KwildClientDriver) Call(ctx context.Context, dbid, action string, inputs []any, withSignature bool) (*client.Records, error) { - +func (d *KwildClientDriver) Call(ctx context.Context, dbid, action string, inputs []any) (*client.Records, error) { return d.clt.CallAction(ctx, dbid, action, inputs) } diff --git a/test/go.mod b/test/go.mod index 92bb97b849..2ac1d7f9fc 100644 --- a/test/go.mod +++ b/test/go.mod @@ -53,6 +53,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chzyer/readline v1.5.0 // indirect github.com/cometbft/cometbft-db v0.7.0 // indirect github.com/compose-spec/compose-go v1.19.0 // indirect github.com/consensys/bavard v0.1.13 // indirect @@ -139,6 +140,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect diff --git a/test/go.sum b/test/go.sum index 8a6704c334..73a8617ef6 100644 --- a/test/go.sum +++ b/test/go.sum @@ -145,8 +145,14 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e h1:Qux+lbuMaRzkQyTdzgtz8MgzPtzmaPQy6DXmxpdxT3U= @@ -547,6 +553,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -966,6 +974,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1013,6 +1022,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/test/integration/docker-compose.override.yml.example b/test/integration/docker-compose.override.yml.example index eaefadd718..bdcf532715 100644 --- a/test/integration/docker-compose.override.yml.example +++ b/test/integration/docker-compose.override.yml.example @@ -11,3 +11,7 @@ services: user: "${UID}:${GID}" ext1: user: "${UID}:${GID}" + ext3: + user: "${UID}:${GID}" + kgw: + user: "${UID}:${GID}" \ No newline at end of file diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml index f6cd997585..ec3b61274b 100644 --- a/test/integration/docker-compose.yml +++ b/test/integration/docker-compose.yml @@ -11,7 +11,7 @@ services: - "26656:26656" - "26657:26657" env_file: - - .env # docker compose by default will use this file, here just make it explicit + - .env # docker compose by default will use this file if presented, here just make it explicit volumes: - type: bind source: ${KWIL_HOME:-./.testnet}/node0 @@ -27,7 +27,7 @@ services: --app.extension-endpoints=ext1:50051 --app.grpc-listen-addr=:50051 --app.http-listen-addr=:8080 - --app.admin-listen-addr=unix:///var/run/kwil/admin.sock + --app.admin-listen-addr=unix:///tmp/admin.sock --chain.p2p.listen-addr=tcp://0.0.0.0:26656 --chain.rpc.listen-addr=tcp://0.0.0.0:26657 @@ -56,7 +56,7 @@ services: --app.extension-endpoints=ext1:50051 --app.grpc-listen-addr=:50051 --app.http-listen-addr=:8080 - --app.admin-listen-addr=unix:///var/run/kwil/admin.sock + --app.admin-listen-addr=unix:///tmp/admin.sock --chain.p2p.listen-addr=tcp://0.0.0.0:26656 --chain.rpc.listen-addr=tcp://0.0.0.0:26657 @@ -85,7 +85,7 @@ services: --app.extension-endpoints=ext1:50051 --app.grpc-listen-addr=:50051 --app.http-listen-addr=:8080 - --app.admin-listen-addr=unix:///var/run/kwil/admin.sock + --app.admin-listen-addr=unix:///tmp/admin.sock --chain.p2p.listen-addr=tcp://0.0.0.0:26656 --chain.rpc.listen-addr=tcp://0.0.0.0:26657 @@ -118,7 +118,7 @@ services: --app.grpc-listen-addr=:50051 --app.http-listen-addr=:8080 --chain.p2p.listen-addr=tcp://0.0.0.0:26656 - --app.admin-listen-addr=unix:///var/run/kwil/admin.sock + --app.admin-listen-addr=unix:///tmp/admin.sock --chain.rpc.listen-addr=tcp://0.0.0.0:26657 # this ext is shared by all nodes @@ -152,6 +152,31 @@ services: timeout: 6s retries: 20 + # for kgw tests, to run locally, you need to build this image in kgw repo + kgw: + container_name: kgw + image: kgw:latest + ports: + - "8090:8090" + env_file: + - .env + networks: + kwil-int-testnet: + ipv4_address: 172.10.100.10 + command: | + --log-level ${LOG_LEVEL:-debug} + --cors-allow-origins * + --backends node0:8080 node1:8080 node2:8080 node3:8080 + --domain http://localhost:8090 + --statement "Trust me ok?" + --session-secret "kgwtest" + --chain-id ${CHAIN_ID:-kwil-test-chain} + --allow-deploy-db + --allow-adhoc-query + --devmode + --schema-sync-interval 2 + # domain should not be changed, and client should use 'doamin' value as the provider, otherwise the test will fail + networks: kwil-int-testnet: name: kwil-int-testnet diff --git a/test/integration/driver.go b/test/integration/driver.go index 8a4e46ffc2..83ca6b736c 100644 --- a/test/integration/driver.go +++ b/test/integration/driver.go @@ -1,6 +1,8 @@ package integration -import "github.com/kwilteam/kwil-db/test/specifications" +import ( + "github.com/kwilteam/kwil-db/test/specifications" +) type KwilIntDriver interface { specifications.DatabaseDeployDsl diff --git a/test/integration/helper.go b/test/integration/helper.go index 7e5c441780..8b7d1f2180 100644 --- a/test/integration/helper.go +++ b/test/integration/helper.go @@ -22,23 +22,23 @@ import ( "testing" "time" - "github.com/kwilteam/kwil-db/core/adminclient" - gRPC "github.com/kwilteam/kwil-db/core/rpc/client/user/grpc" - "github.com/cometbft/cometbft/crypto/ed25519" "github.com/joho/godotenv" - "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/compose" - "github.com/testcontainers/testcontainers-go/wait" - "github.com/kwilteam/kwil-db/cmd/kwil-admin/nodecfg" + "github.com/kwilteam/kwil-db/cmd/kwil-cli/cmds/common" + "github.com/kwilteam/kwil-db/core/adminclient" "github.com/kwilteam/kwil-db/core/client" "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" + "github.com/kwilteam/kwil-db/core/gatewayclient" "github.com/kwilteam/kwil-db/core/log" + gRPC "github.com/kwilteam/kwil-db/core/rpc/client/user/grpc" "github.com/kwilteam/kwil-db/test/driver" "github.com/kwilteam/kwil-db/test/driver/operator" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/compose" + "github.com/testcontainers/testcontainers-go/wait" ) var ( @@ -56,6 +56,7 @@ var defaultWaitStrategies = map[string]string{ "node1": "Starting Node service", "node2": "Starting Node service", "node3": "Starting Node service", + "kgw": "KGW Server started", } const ( @@ -172,7 +173,7 @@ func (r *IntHelper) LoadConfig() { LogLevel: getEnv("KIT_LOG_LEVEL", "info"), HTTPEndpoint: getEnv("KIT_HTTP_ENDPOINT", "http://localhost:8080"), GrpcEndpoint: getEnv("KIT_GRPC_ENDPOINT", "localhost:50051"), - AdminRPC: getEnv("KIT_SOCKET_DIR", "unix:///var/run/kwil/admin.sock"), + AdminRPC: getEnv("KIT_ADMIN_RPC", "unix:///tmp/admin.sock"), DockerComposeFile: getEnv("KIT_DOCKER_COMPOSE_FILE", "./docker-compose.yml"), DockerComposeOverrideFile: getEnv("KIT_DOCKER_COMPOSE_OVERRIDE_FILE", "./docker-compose.override.yml"), } @@ -280,6 +281,8 @@ func (r *IntHelper) RunDockerComposeWithServices(ctx context.Context, services [ } // Use compose.Wait to wait for containers to become "healthy" according to // their defined healthchecks. + + // NOTE: services will be sorted by docker-compose here. err = stack.Up(ctx, compose.Wait(true), compose.RunServices(services...)) r.t.Log("docker compose up") require.NoError(r.t, err, "failed to start kwild cluster") @@ -293,7 +296,6 @@ func (r *IntHelper) RunDockerComposeWithServices(ctx context.Context, services [ require.NoError(r.t, err, "failed to get container for service %s", name) r.containers[name] = container } - } func (r *IntHelper) Setup(ctx context.Context, services []string) { @@ -347,29 +349,61 @@ func decodePrivateKey(pkey string) (ed25519.PrivKey, error) { return ed25519.PrivKey(privB), nil } -// GetUserDriver returns a integration driver connected to the given kwil node -// using the creator's private key -func (r *IntHelper) GetUserDriver(ctx context.Context, name string, driverType string) KwilIntDriver { - ctr := r.containers[name] +// GetUserGatewayDriver returns an integration driver connected to the given gateway node +func (r *IntHelper) GetUserGatewayDriver(ctx context.Context, driverType string, user string) KwilIntDriver { + gatewayProvider := true + + ctr := r.containers["kgw"] + gatewayURL, err := ctr.PortEndpoint(ctx, "8090", "http") + require.NoError(r.t, err, "failed to get gateway url") + r.t.Logf("gatewayURL: %s for container: %s", gatewayURL, "kgw") + // NOTE: gatewayURL should be http://localhost:8090, match the domain in docker-compose.yml + signer := r.cfg.CreatorSigner + pk := r.cfg.CreatorRawPk + + if user == "visitor" { + signer = r.cfg.VisitorSigner + pk = r.cfg.VisitorRawPK + } + + switch driverType { + case "http": + return r.getHTTPClientDriver(signer, gatewayURL, gatewayProvider) + case "cli": + return r.getCliDriver(gatewayURL, pk, signer.Identity(), gatewayProvider) + default: + panic("unsupported driver type") + } +} + +// GetUserDriver returns an integration driver connected to the given rpc node +// using the private key +func (r *IntHelper) GetUserDriver(ctx context.Context, nodeName string, driverType string) KwilIntDriver { + gatewayProvider := false + + ctr := r.containers[nodeName] // NOTE: maybe get from docker-compose.yml ? the port mapping is already there - nodeURL, err := ctr.PortEndpoint(ctx, "50051", "") + grpcURL, err := ctr.PortEndpoint(ctx, "50051", "tcp") require.NoError(r.t, err, "failed to get node url") - gatewayURL, err := ctr.PortEndpoint(ctx, "8080", "") + httpURL, err := ctr.PortEndpoint(ctx, "8080", "http") require.NoError(r.t, err, "failed to get gateway url") cometBftURL, err := ctr.PortEndpoint(ctx, "26657", "tcp") require.NoError(r.t, err, "failed to get cometBft url") - r.t.Logf("nodeURL: %s gatewayURL: %s cometBftURL: %s for container name: %s", nodeURL, gatewayURL, cometBftURL, name) + r.t.Logf("grpcURL: %s httpURL: %s cometBftURL: %s for container: %s", grpcURL, httpURL, cometBftURL, nodeName) signer := r.cfg.CreatorSigner pk := r.cfg.CreatorRawPk + switch driverType { case "http": - return r.getHTTPClientDriver(signer) + return r.getHTTPClientDriver(signer, httpURL, gatewayProvider) case "grpc": + // should use grpcURL, r.cfg.GrpcEndpoint is not correct(it's intended + // to be used for `remote` test mode, but integation tests don't use it) return r.getGRPCClientDriver(signer) case "cli": - return r.getCliDriver(pk, signer.Identity()) + return r.getCliDriver(httpURL, pk, signer.Identity(), gatewayProvider) default: panic("unsupported driver type") } @@ -412,20 +446,34 @@ func (r *IntHelper) GetOperatorDriver(ctx context.Context, nodeName string, driv } } -func (r *IntHelper) getHTTPClientDriver(signer auth.Signer) KwilIntDriver { +func (r *IntHelper) getHTTPClientDriver(signer auth.Signer, endpoint string, gatewayProvider bool) *driver.KwildClientDriver { logger := log.New(log.Config{Level: r.cfg.LogLevel}) - kwilClt, err := client.NewClient(context.TODO(), r.cfg.HTTPEndpoint, &client.ClientOptions{ - Signer: signer, - ChainID: testChainID, - Logger: logger, - }) + var kwilClt common.Client + var err error + + if gatewayProvider { + kwilClt, err = gatewayclient.NewClient(context.TODO(), endpoint, &gatewayclient.GatewayOptions{ + ClientOptions: client.ClientOptions{ + Signer: signer, + ChainID: testChainID, + Logger: logger, + }, + }) + } else { + kwilClt, err = client.NewClient(context.TODO(), endpoint, &client.ClientOptions{ + Signer: signer, + ChainID: testChainID, + Logger: logger, + }) + } + require.NoError(r.t, err, "failed to create kwil client") - return driver.NewKwildClientDriver(kwilClt, logger) + return driver.NewKwildClientDriver(kwilClt, signer, logger) } -func (r *IntHelper) getGRPCClientDriver(signer auth.Signer) KwilIntDriver { +func (r *IntHelper) getGRPCClientDriver(signer auth.Signer) *driver.KwildClientDriver { logger := log.New(log.Config{Level: r.cfg.LogLevel}) gtOptions := []gRPC.Option{gRPC.WithTlsCert("")} @@ -439,7 +487,7 @@ func (r *IntHelper) getGRPCClientDriver(signer auth.Signer) KwilIntDriver { }) require.NoError(r.t, err, "failed to create grpc client") - return driver.NewKwildClientDriver(kwilClt, logger) + return driver.NewKwildClientDriver(kwilClt, signer, logger) } // getCLIAdminClientDriver returns a kwil-admin client driver connected to the given kwil node. @@ -469,14 +517,14 @@ func (r *IntHelper) getCLIAdminClientDriver(adminSvcServer string, c *testcontai } } -func (r *IntHelper) getCliDriver(privKey string, identity []byte) KwilIntDriver { +func (r *IntHelper) getCliDriver(endpoint string, privKey string, identity []byte, gatewayProvider bool) *driver.KwilCliDriver { logger := log.New(log.Config{Level: r.cfg.LogLevel}) _, currentFilePath, _, _ := runtime.Caller(1) cliBinPath := path.Join(path.Dir(currentFilePath), fmt.Sprintf("../../.build/kwil-cli-%s-%s", runtime.GOOS, runtime.GOARCH)) - return driver.NewKwilCliDriver(cliBinPath, r.cfg.HTTPEndpoint, privKey, testChainID, identity, logger) + return driver.NewKwilCliDriver(cliBinPath, endpoint, privKey, testChainID, identity, gatewayProvider, logger) } func (r *IntHelper) NodePrivateKey(name string) ed25519.PrivKey { diff --git a/test/integration/kgw_test.go b/test/integration/kgw_test.go new file mode 100644 index 0000000000..02e37d702c --- /dev/null +++ b/test/integration/kgw_test.go @@ -0,0 +1,117 @@ +package integration_test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/kwilteam/kwil-db/test/integration" + "github.com/kwilteam/kwil-db/test/specifications" +) + +// We're testing gatewayclient and kwil-cli, so the tests are in kwil-db, not kgw. +// To test kgw, we need to run a kwil network with kgw services. +// Three ways to do this: +// 1. pull kgw repo, build kgw, run kgw services using `go` +// 2. pull kgw repo, build kgw, run kgw services using `docker-compose` +// 3. pull already built kgw image(built in kgw repo), run kgw services using `docker-compose` +// We choose 2, because it's the easiest, in CI. +// +// By default, we will skip kgw tests, so it's not a concern for local development. + +func TestKGWAPIs(t *testing.T) { + if testing.Short() { + t.Skip("skipping kgw test in short mode") + } + + ctx := context.Background() + + opts := []integration.HelperOpt{ + integration.WithBlockInterval(time.Second), + integration.WithValidators(4), + integration.WithNonValidators(0), + } + + testDrivers := strings.Split(*drivers, ",") + for _, driverType := range testDrivers { + t.Run(driverType+"_driver", func(t *testing.T) { + helper := integration.NewIntHelper(t, opts...) + helper.Setup(ctx, basicServices) + defer helper.Teardown() + + helper.RunDockerComposeWithServices(ctx, []string{"kgw"}) + + creatorDriver := helper.GetUserGatewayDriver(ctx, driverType, "creator") + + // When user deployed a database + specifications.DatabaseDeployInvalidSql1Specification(ctx, t, creatorDriver) + specifications.DatabaseDeployInvalidExtensionSpecification(ctx, t, creatorDriver) + specifications.DatabaseDeploySpecification(ctx, t, creatorDriver) + + // Then user should be able to execute database actions + visitorDriver := helper.GetUserGatewayDriver(ctx, driverType, "visitor") + // creator should be able to execute all actions + specifications.ExecuteOwnerActionSpecification(ctx, t, creatorDriver) + specifications.ExecuteDBInsertSpecification(ctx, t, creatorDriver) + specifications.ExecuteCallSpecification(ctx, t, creatorDriver, visitorDriver) + specifications.ExecuteDBUpdateSpecification(ctx, t, creatorDriver) + specifications.ExecuteDBDeleteSpecification(ctx, t, creatorDriver) + + // test that the loaded extensions works + specifications.ExecuteExtensionSpecification(ctx, t, creatorDriver) + + // and user should be able to drop database + specifications.DatabaseDropSpecification(ctx, t, creatorDriver) + }) + } +} + +func TestKGWAuthn(t *testing.T) { + if testing.Short() { + t.Skip("skipping kgw test in short mode") + } + + ctx := context.Background() + + opts := []integration.HelperOpt{ + integration.WithBlockInterval(time.Second), + integration.WithValidators(4), + integration.WithNonValidators(0), + } + + testDrivers := strings.Split(*drivers, ",") + for _, driverType := range testDrivers { + t.Run(driverType+"_driver", func(t *testing.T) { + helper := integration.NewIntHelper(t, opts...) + helper.Setup(ctx, basicServices) + defer helper.Teardown() + + helper.RunDockerComposeWithServices(ctx, []string{"kgw"}) + + creatorDriver := helper.GetUserGatewayDriver(ctx, driverType, "creator") + + // When user deployed a database + specifications.DatabaseDeploySpecification(ctx, t, creatorDriver) + + // Then user should be able to call authn action after authentication + db := specifications.SchemaLoader.Load(t, specifications.SchemaTestDB) + dbid := creatorDriver.DBID(db.Name) + visitorDriver := helper.GetUserGatewayDriver(ctx, driverType, "visitor") + + // visitor can execute authn action + // NOTE: due to the way we implemented client/cli for kgw authn, we cannot + // test that authn action fails without authentication, e.g. the behavior + // cannot be explicitly tested here + // + // successful call, bc schema on kgw is not synced yet, no authn rules are enforced + specifications.ExecuteAuthnCallSpecification(ctx, t, visitorDriver, dbid) + // sleep is necessary, longer than kgw schema sync interval + time.Sleep(time.Second * 3) + // successful call, gatewayDriver will automatically authenticate if required + specifications.ExecuteAuthnCallSpecification(ctx, t, visitorDriver, dbid) + // successful call, cookie from last call should be reused + specifications.ExecuteAuthnCallSpecification(ctx, t, visitorDriver, dbid) + }) + } +} diff --git a/test/integration/kwild_test.go b/test/integration/kwild_test.go index c019e6b4c8..382556824a 100644 --- a/test/integration/kwild_test.go +++ b/test/integration/kwild_test.go @@ -15,8 +15,34 @@ var dev = flag.Bool("dev", false, "run for development purpose (no tests)") var drivers = flag.String("drivers", "http,cli", "comma separated list of drivers to run") -var allServices = []string{integration.ExtContainer, "node0", "node1", "node2", integration.Ext3Container, "node3"} -var numServices = len(allServices) +// Here we make clear the services will be used in each stage +var basicServices = []string{integration.ExtContainer, "node0", "node1", "node2"} +var newServices = []string{integration.Ext3Container, "node3"} + +// NOTE: allServices will be sorted by docker-compose(in setup), so the order is not reliable +var allServices = []string{integration.ExtContainer, integration.Ext3Container, "node0", "node1", "node2", "node3"} + +func TestLocalDevSetup(t *testing.T) { + if !*dev { + t.Skip("skipping local dev setup") + } + + // running forever for local development + + ctx := context.Background() + + opts := []integration.HelperOpt{ + integration.WithBlockInterval(time.Second), + integration.WithValidators(4), + integration.WithNonValidators(0), + } + + helper := integration.NewIntHelper(t, opts...) + helper.Setup(ctx, allServices) + defer helper.Teardown() + + helper.WaitForSignals(t) +} func TestKwildDatabaseIntegration(t *testing.T) { ctx := context.Background() @@ -31,15 +57,9 @@ func TestKwildDatabaseIntegration(t *testing.T) { for _, driverType := range testDrivers { t.Run(driverType+"_driver", func(t *testing.T) { helper := integration.NewIntHelper(t, opts...) - helper.Setup(ctx, allServices) + helper.Setup(ctx, basicServices) defer helper.Teardown() - // running forever for local development - if *dev { - helper.WaitForSignals(t) - return - } - node0Driver := helper.GetUserDriver(ctx, "node0", driverType) node1Driver := helper.GetUserDriver(ctx, "node1", driverType) node2Driver := helper.GetUserDriver(ctx, "node2", driverType) @@ -82,12 +102,6 @@ func TestKwildValidatorRemoval(t *testing.T) { helper.Setup(ctx, allServices) defer helper.Teardown() - // running forever for local development - if *dev { - helper.WaitForSignals(t) - return - } - node0Driver := helper.GetOperatorDriver(ctx, "node0", driverType) node1Driver := helper.GetOperatorDriver(ctx, "node1", driverType) node2Driver := helper.GetOperatorDriver(ctx, "node2", driverType) @@ -133,12 +147,6 @@ func TestKwildValidatorUpdatesIntegration(t *testing.T) { helper.Setup(ctx, allServices) defer helper.Teardown() - // running forever for local development - if *dev { - helper.WaitForSignals(t) - return - } - node0Driver := helper.GetOperatorDriver(ctx, "node0", driverType) node1Driver := helper.GetOperatorDriver(ctx, "node1", driverType) joinerDriver := helper.GetOperatorDriver(ctx, "node3", driverType) @@ -199,22 +207,16 @@ func TestKwildNetworkSyncIntegration(t *testing.T) { for _, driverType := range testDrivers { t.Run(driverType+"_driver", func(t *testing.T) { helper := integration.NewIntHelper(t, opts...) - // Bringup ext1, node 0,1,2 services but not node3 or ext3 - helper.Setup(ctx, allServices[:numServices-2]) + helper.Setup(ctx, basicServices) defer helper.Teardown() - // running forever for local development - if *dev { - helper.WaitForSignals(t) - return - } - node0Driver := helper.GetUserDriver(ctx, "node0", driverType) node1Driver := helper.GetUserDriver(ctx, "node1", driverType) node2Driver := helper.GetUserDriver(ctx, "node2", driverType) // Create a new database and verify that the database exists on other nodes specifications.DatabaseDeploySpecification(ctx, t, node0Driver) + time.Sleep(time.Second * 2) // need time to sync specifications.DatabaseVerifySpecification(ctx, t, node1Driver, true) specifications.DatabaseVerifySpecification(ctx, t, node2Driver, true) @@ -228,14 +230,14 @@ func TestKwildNetworkSyncIntegration(t *testing.T) { 3. Get the node driver 4. Verify that the database exists on the new node */ - helper.RunDockerComposeWithServices(ctx, allServices[numServices-2:]) - //node3Driver := helper.GetUserDriver(ctx, helper.ServiceContainer("node3")) + helper.RunDockerComposeWithServices(ctx, newServices) node3Driver := helper.GetUserDriver(ctx, "node3", driverType) /* 1. This checks if the database exists on the new node 2. Verify if the user and posts are synced to the new node */ + time.Sleep(time.Second * 4) // need time to catch up specifications.DatabaseVerifySpecification(ctx, t, node3Driver, true) expectPosts := 1 diff --git a/test/integration/test-data/test_db.kf b/test/integration/test-data/test_db.kf index 3c9f297f55..c5ae41ece0 100644 --- a/test/integration/test-data/test_db.kf +++ b/test/integration/test-data/test_db.kf @@ -95,19 +95,12 @@ action get_user_posts($username) public { ); } -@kgw(authn='true') -action get_post_authenticated($id) public view { +action get_post($id) public view { SELECT * FROM posts WHERE id = $id; } -action get_post_unauthenticated($id) public view { - SELECT * - FROM "posts" - WHERE id = $id; -} - action multi_select() public { SELECT * FROM posts; @@ -121,7 +114,11 @@ action divide($numerator1, $numerator2, $denominator) public view { select $up AS upper_value, $down AS lower_value; } -@kgw(authn='true') action owner_only() public owner view { select 'owner only'; +} + +@kgw(authn='true') +action authn_only() public view { + select 'authn only'; } \ No newline at end of file diff --git a/test/specifications/deploy_database.go b/test/specifications/deploy_database.go index 33540b356f..ca043de90a 100644 --- a/test/specifications/deploy_database.go +++ b/test/specifications/deploy_database.go @@ -40,12 +40,12 @@ func DatabaseDeployInvalidSql1Specification(ctx context.Context, t *testing.T, d // Then i expect tx failure expectTxFail(t, deploy, ctx, txHash, defaultTxQueryTimeout)() - // read in fixed schema + // deploy fixed schema db2 := SchemaLoader.Load(t, schemaInvalidSqlSyntaxFixed) - _, err = deploy.DeployDatabase(ctx, db2) + txHash2, err := deploy.DeployDatabase(ctx, db2) require.NoError(t, err, "failed to send deploy database tx") - expectTxSuccess(t, deploy, ctx, txHash, defaultTxQueryTimeout)() + expectTxSuccess(t, deploy, ctx, txHash2, defaultTxQueryTimeout)() err = deploy.DatabaseExists(ctx, deploy.DBID(db.Name)) require.NoError(t, err) diff --git a/test/specifications/dsl.go b/test/specifications/dsl.go index 3f117e7c6a..63ec030d50 100644 --- a/test/specifications/dsl.go +++ b/test/specifications/dsl.go @@ -59,7 +59,7 @@ type DatabaseDropDsl interface { // ExecuteCallDsl is dsl for call specification type ExecuteCallDsl interface { DatabaseIdentifier - Call(ctx context.Context, dbid, action string, inputs []any, withSignature bool) (*client.Records, error) + Call(ctx context.Context, dbid, action string, inputs []any) (*client.Records, error) } // ExecuteExtensionDsl is dsl for extension specification diff --git a/test/specifications/execute_call_action.go b/test/specifications/execute_call_action.go index 3bc28f851a..5950691cf6 100644 --- a/test/specifications/execute_call_action.go +++ b/test/specifications/execute_call_action.go @@ -16,33 +16,38 @@ func ExecuteCallSpecification(ctx context.Context, t *testing.T, caller ExecuteC getPostInput := []any{1111} - res, err := caller.Call(ctx, dbID, "get_post_authenticated", getPostInput, true) + res, err := caller.Call(ctx, dbID, "get_post", getPostInput) if err != nil { t.Fatalf("error calling action: %s", err.Error()) } checkGetPostResults(t, res.Export()) - res, err = caller.Call(ctx, dbID, "get_post_unauthenticated", getPostInput, false) - if err != nil { - t.Fatalf("error calling action: %s", err.Error()) - } - checkGetPostResults(t, res.Export()) - // try calling mutable action, should fail - _, err = caller.Call(ctx, dbID, "delete_user", nil, false) + _, err = caller.Call(ctx, dbID, "delete_user", nil) assert.Error(t, err, "expected error calling mutable action") // test that modifiers "public owner view" enforces checks on the caller // the caller here is the correct owner - _, err = caller.Call(ctx, dbID, "owner_only", nil, false) + _, err = caller.Call(ctx, dbID, "owner_only", nil) assert.NoError(t, err, "calling owner only action with owner as sender should succeed") + // TODO: make this a separate specification // and test that authenticating works - _, err = visitor.Call(ctx, dbID, "owner_only", nil, false) + _, err = visitor.Call(ctx, dbID, "owner_only", nil) assert.Error(t, err, "calling owner only action with non-owner as sender should fail") } +// ExecuteAuthnCallSpecification tests that kgw authn annotation action +// accepts calls with authentication +func ExecuteAuthnCallSpecification(ctx context.Context, t *testing.T, caller ExecuteCallDsl, dbid string) { + t.Logf("Executing ExecuteAuthnCallSpecification") + + // try calling authn action, should success + _, err := caller.Call(ctx, dbid, "authn_only", nil) + assert.NoError(t, err, "expected success calling kgw authn action") +} + func checkGetPostResults(t *testing.T, results []map[string]any) { if len(results) != 1 { t.Fatalf("expected 1 statement result, got %d", len(results)) diff --git a/test/specifications/execute_extension.go b/test/specifications/execute_extension.go index e9a758f5b3..e89c4e84bd 100644 --- a/test/specifications/execute_extension.go +++ b/test/specifications/execute_extension.go @@ -24,7 +24,7 @@ func ExecuteExtensionSpecification(ctx context.Context, t *testing.T, execute Ex expectTxSuccess(t, execute, ctx, txHash, defaultTxQueryTimeout)() - records, err := execute.Call(ctx, dbID, divideActionName, []any{2, 1, 2}, false) + records, err := execute.Call(ctx, dbID, divideActionName, []any{2, 1, 2}) assert.NoError(t, err) results := records.Export() diff --git a/test/specifications/execute_owner_actions.go b/test/specifications/execute_owner_actions.go index afdf02422d..936ce5ad95 100644 --- a/test/specifications/execute_owner_actions.go +++ b/test/specifications/execute_owner_actions.go @@ -35,6 +35,6 @@ func ExecuteOwnerActionFailSpecification(ctx context.Context, t *testing.T, exec expectTxFail(t, execute, ctx, txHash, defaultTxQueryTimeout)() // call authenticated, should fail - _, err = execute.Call(ctx, dbID, ownerOnlyActionName, actionInputs, true) + _, err = execute.Call(ctx, dbID, ownerOnlyActionName, actionInputs) require.Error(t, err, "expected error calling owner only action with authentication") } diff --git a/test/stress/scheme.kf b/test/stress/scheme.kf index c819d0ebf0..3b728deea2 100644 --- a/test/stress/scheme.kf +++ b/test/stress/scheme.kf @@ -86,14 +86,7 @@ action get_user_posts($username) public view { ); } -@kgw(authn='true') -action get_post_authenticated($id) public view { - SELECT * - FROM posts - WHERE id = $id; -} - -action get_post_unauthenticated($id) public view { +action get_post($id) public view { SELECT * FROM posts WHERE id = $id;