diff --git a/Makefile b/Makefile index f70200a..cf3856c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,14 @@ .DEFAULT_GOAL := help +CONFIG_PATH ?= ./local/config.yaml + +PLANS_ENDPOINT ?= `yq .subscriptions.plans_endpoint $(CONFIG_PATH)` +USERS_ENDPOINT ?= `yq .subscriptions.users_endpoint $(CONFIG_PATH)` +SUBSCRIPTIONS_ENDPOINT ?= `yq .payments.subscriptions_endpoint $(CONFIG_PATH)` +PAYMENTS_ENDPOINT ?= `yq .server.endpoint.http $(CONFIG_PATH)` + +CMD_K6_RUN ?= K6_WEB_DASHBOARD=true k6 run -e PLANS_ENDPOINT=$(PLANS_ENDPOINT) -e USERS_ENDPOINT=$(USERS_ENDPOINT) -e SUBSCRIPTIONS_ENDPOINT=$(SUBSCRIPTIONS_ENDPOINT) -e PAYMENTS_ENDPOINT=$(PAYMENTS_ENDPOINT) ./tests/k6/k6s.js + .PHONY: help help: @echo Para construir o projeto, execute: @@ -42,6 +51,12 @@ build-subscriptions: build-plans: @goreleaser build -f=.goreleaser.yaml --snapshot --clean --single-target --id plans +.PHONY: test-load +test-load: + @echo Run K6 to test services + @echo "$(CMD_K6_RUN)" + $(CMD_K6_RUN) + .PHONY: install-tools install-tools: @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest diff --git a/tests/k6/k6s.js b/tests/k6/k6s.js new file mode 100644 index 0000000..f704dc7 --- /dev/null +++ b/tests/k6/k6s.js @@ -0,0 +1,42 @@ +// Testes básicos acessando rotas dos serviços +// VU = virtual user +import { testAddUsers} from "./svc-users.js"; +import { testAddPlans} from "./svc-plans.js"; +import { testAddSubscriptions} from "./svc-subscriptions.js"; +import { testMakePayments} from "./svc-payments.js"; + +// https://grafana.com/docs/k6/latest/using-k6/k6-options/reference/ +export const options = { + // Key configurations for spike in this section + // stages: [ + // // A list of VU { target: ..., duration: ... } objects that specify the target number of VUs to ramp up or down to for a specific period. + // { duration: '2m', target: 20 }, + // { duration: '1m', target: 1 }, + // { duration: '2m', target: 20 }, + // ], + // noConnectionReuse: true, + insecureSkipTLSVerify: true, + userAgent: 'k6-otel/1.0', + + httpDebug: '', + + summaryTimeUnit: 's', + + iterations: 200, + duration: '1m', +}; + +// Global variables should be initialized. + +export default function() { + + testAddUsers(20); + + testAddPlans(20); + + testAddSubscriptions(20); + + testMakePayments(10); +} + + diff --git a/tests/k6/svc-payments.js b/tests/k6/svc-payments.js new file mode 100644 index 0000000..3f62f12 --- /dev/null +++ b/tests/k6/svc-payments.js @@ -0,0 +1,84 @@ +import http from "k6/http"; +import { group, check } from "k6"; +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.6.0/index.js'; +import { listSubscriptions } from "./svc-subscriptions.js"; + +const SVC_PATH = "/payments"; +const BASE_URL = `${__ENV.PAYMENTS_ENDPOINT}`; + +const getURL = () => { + return fixEndpoint(BASE_URL, SVC_PATH); +} + +function fixEndpoint(endpoint, suffixPath) { + if (!endpoint.startsWith("http")) { + if (endpoint.startsWith(":")) { + return `http://localhost${endpoint}${!endpoint.endsWith(suffixPath) ? suffixPath : ""}`; + } else { + return `http://localhost:8080/${endpoint}${!endpoint.endsWith(suffixPath) ? suffixPath : ""}`; + } + } + return endpoint; +} + +const listPayments = () => { + const res = http.get(getURL(), { headers: { "Content-Type": "application/json", "Accept": "application/json" } }); + return res; +}; + +const paymentStatus = () => Math.random() < 0.5 ? "SUCCESS" : "FAILED"; + +const generatePayment = (subscriptionId, amount) => ({ + id: uuidv4(), + subscription_id: subscriptionId, + amount, + status: paymentStatus(), +}); + +const makePayment = (subscriptionId, amount) => { + const payload = JSON.stringify(generatePayment(subscriptionId, amount)); + const res = http.post(getURL(), payload, { headers: { "Content-Type": "application/json" } }); + return res; +}; + +export function testListPayments() { + group("list payments", () => { + const res = listPayments(); + check(res, { "Successful listing": (r) => r.status === 200 }); + }); +} + +export function testMakePayments(qty) { + group("make payments", () => { + for (let i = 0; i < qty; i++) { + const response = listSubscriptions(); + if (response.status !== 200) { + console.error("Failed to list subscriptions:", response.error_code); + continue; + } + + try { + const subscriptions = response.json(); + const randomSubscription = subscriptions[Math.floor(Math.random() * subscriptions.length)]; + const subscriptionId = randomSubscription.id; + + const amount = Math.floor(Math.random() * 10); + + const paymentResponse = makePayment(subscriptionId, amount); + + check(paymentResponse, { // Corrected status code check (201) + "Payment created": (r) => r.status === 201, + }); + + if (paymentResponse.status !== 201) { + console.error("Failed to make a payment:", paymentResponse.error_code); // Include error for failed payment creation + } + + } catch (error) { + console.error("Error processing subscriptions:", error); + } + } + }); +} + + diff --git a/tests/k6/svc-plans.js b/tests/k6/svc-plans.js new file mode 100644 index 0000000..91aaea7 --- /dev/null +++ b/tests/k6/svc-plans.js @@ -0,0 +1,56 @@ +import http from "k6/http"; +import { group, check } from "k6"; +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.6.0/index.js'; + +const BASE_URL = `${__ENV.PLANS_ENDPOINT}`; + +const getURL = () => BASE_URL; + +export const listPlans = () => { + const req = http.get(getURL(), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +export function testListPlans() { + group("list plans", () => { + let req = listPlans(); + check(req, { "Successful listing.": (r) => r.status === 200 }); + }); +} + +export const generatePlan = () => { + const randomU = Math.floor(Math.random() * 101); + + const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const randomLetters = letters[Math.floor(Math.random() * 26)] + letters[Math.floor(Math.random() * 26)]; + + const obj = { + id: uuidv4(), + name: randomLetters + randomU, + price: randomU * randomU , + description: "Plan" + }; + + return obj +}; + +function addPlan() { + const req = http.post(getURL(), JSON.stringify(generatePlan()), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +export function testAddPlan() { + group("add plan", () => { + let req = addPlan(); + check(req, { "Plan created.": (r) => r.status === 200 });// bug: service should return 201 + }); +} + +export function testAddPlans(qty){ + group("add " + qty + " plans", () => { + for (let index = 0; index < qty; index++) { + let req = addPlan(); + check(req, { "Plan created.": (r) => r.status === 200 }); // bug: service return should 201 + } + }); +} \ No newline at end of file diff --git a/tests/k6/svc-subscriptions.js b/tests/k6/svc-subscriptions.js new file mode 100644 index 0000000..6730496 --- /dev/null +++ b/tests/k6/svc-subscriptions.js @@ -0,0 +1,83 @@ +import http from "k6/http"; +import { group, check } from "k6"; +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.6.0/index.js'; +import { listUsers } from "./svc-users.js"; +import { listPlans } from "./svc-plans.js"; + +const BASE_URL = `${__ENV.SUBSCRIPTIONS_ENDPOINT}`; + +const getURL = () => BASE_URL; + +export const listSubscriptions = () => { + const req = http.get(getURL(), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +export function testListSubscriptions() { + group("list subscriptions", () => { + let req = listSubscriptions(); + check(req, { "Successful listing.": (r) => r.status === 200 }); + }); +} + +const generateSubscription = (userId, planId) => { + const obj = { + id: uuidv4(), + user_id: userId, + plan_id: planId + }; + return obj +}; + +const addSubscription = (userId, planId) => { + const req = http.post(getURL(), JSON.stringify(generateSubscription(userId, planId)), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +export function testAddSubscription(userId, planId) { + group("add Subscription", (userId, planId) => { + let req = addSubscription(userId, planId); + check(req, { "Successful listing.": (r) => r.status === 201 }); + }); +} + +export function testAddSubscriptions(qty){ + group("add " + qty + " subscriptions", () => { + for (let i = 0; i < qty; i++) { + + const usersResponse = listUsers() + if (usersResponse.status !== 200) { + console.error("Failed to list users:", usersResponse.error_code); + continue; + } + + try { + const users = usersResponse.json(); + const randomUser = users[Math.floor(Math.random() * users.length)]; + const userId = randomUser.id; + + const plansResponse = listPlans() + if (plansResponse.status !== 200) { + console.error("Failed to list plans:", plansResponse.error_code); + continue; + } + const plans = plansResponse.json(); + const randomPlan = plans[Math.floor(Math.random() * plans.length)]; + const planId = randomPlan.id; + + const response = addSubscription(userId, planId); + check(response, { + "Subscription created.": (r) => r.status === 200, // bug: service return should 201 + }); + + if (response.status !== 200) { + console.error("Failed to add a subscription:", response.error_code); + } + + } catch (error) { + console.error("Error processing subscriptions:", error); + } + } + }); +} + diff --git a/tests/k6/svc-users.js b/tests/k6/svc-users.js new file mode 100644 index 0000000..8fcbf71 --- /dev/null +++ b/tests/k6/svc-users.js @@ -0,0 +1,51 @@ +import http from "k6/http"; +import { group, check } from "k6"; +// import { faker } from "https://esm.sh/@faker-js/faker@v8.4.1" +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.6.0/index.js'; + +const BASE_URL = `${__ENV.USERS_ENDPOINT}`; + +const getURL = () => BASE_URL; + +export const listUsers = () => { + const req = http.get(getURL(), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +const generateUser = () => { + const obj = { + id: uuidv4(), + name: "teste" + uuidv4(), //faker.person.firstName(), + email: "teste" + uuidv4() + "@test.com" //faker.internet.email() + }; + return obj +}; + +const addUser = () => { + const req = http.post(getURL(), JSON.stringify(generateUser()), {headers: {"Content-Type": "application/json", "Accept": "application/json"}}); + return req; +} + +export function testListUsers() { + group("list users", () => { + const req = listUsers(); + check(req, { "Successful listing.": (r) => r.status === 200 }); + }); +} + +export function testAddUser() { + group("add user", () => { + let req = addUser(); + check(req, { "User created.": (r) => r.status === 200 }); // bug: service return should 201 + }); +} + +export function testAddUsers(qty){ + group("add " + qty + " users", () => { + for (let index = 0; index < qty; index++) { + let req = addUser(); + check(req, { "User created.": (r) => r.status === 200 }); // bug: service return should 201 + } + }); +}; +