Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
✅ Skip e2e tests on Bitbucket Server
  • Loading branch information
NatoBoram committed Feb 27, 2025
commit beb99269d36485f6a7ef11b1f68aa4b272b9cb13
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ BITBUCKET_SERVER_URL=
BITBUCKET_SERVER_TOKEN=
BITBUCKET_SERVER_TEST_PROJECT_KEY=
BITBUCKET_SERVER_TEST_PROJECT_NAME=

SKIP_BITBUCKET_CLOUD=false
SKIP_BITBUCKET_SERVER=true
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@natoboram/load_env": "^1.0.0",
"@types/node": "^22.13.5",
"dotenv": "^16.4.7",
"eslint": "^9.21.0",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tests/cloud/repositories.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { test } from "vitest"
import { SKIP_BITBUCKET_CLOUD } from "../env.ts"
import { client } from "./client.ts"

test("GET /repositories", async ({ expect }) => {
test.skipIf(SKIP_BITBUCKET_CLOUD)("GET /repositories", async ({ expect }) => {
const got = await client.GET("/repositories")

expect(got.data?.next).toBeTypeOf("string")
Expand Down
86 changes: 22 additions & 64 deletions tests/env.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,21 @@
import { config } from "dotenv"
import path from "path"
import { envBool, envString, envUrl, loadEnv } from "@natoboram/load_env"

/**
* @see https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production
* @see https://vitest.dev/guide/migration.html#envs
*/
export type NodeEnv = (typeof nodeEnvs)[keyof typeof nodeEnvs]
export type ProcessEnv = typeof process.env
type NodeEnv = (typeof NodeEnv)[keyof typeof NodeEnv]

interface LoadedEnv extends ProcessEnv {
readonly NODE_ENV: NodeEnv
function isNodeEnv(value: unknown): value is NodeEnv {
return Object.values<unknown>(NodeEnv).includes(value)
}

function envString(key: string) {
const value = parsed[key]
if (!value) throw new Error(`$${key} is missing`)
return value
}

function envUrl(key: string) {
const str = envString(key)
try {
return new URL(str)
} catch (error) {
throw new Error(`$${key} is not a URL: ${str}`, { cause: error })
}
}

export function isNodeEnv(value: unknown): value is NodeEnv {
return Object.values<unknown>(nodeEnvs).includes(value)
}

/** Loads environment variables from the `.env` files. `NODE_ENV` has to be
* set in the environment and will not be picked up from there.
*
* If `NODE_ENV` is not set, it will default to `development`.
*
* Environment variables are loaded in the following order:
*
* 1. `.env.development.local`
* 2. `.env.development`
* 3. `.env.local`
* 4. `.env`
*/
function loadEnv(): LoadedEnv {
const cwd = process.cwd()
const NODE_ENV = toNodeEnv(process.env.NODE_ENV?.trim())

const { parsed, error } = config({
path: [
path.resolve(cwd, `.env.${NODE_ENV}.local`),
path.resolve(cwd, `.env.${NODE_ENV}`),
path.resolve(cwd, ".env.local"),
path.resolve(cwd, ".env"),
],
})

if (!parsed)
throw new Error("Environment variables could not be loaded.", {
cause: error,
})

const merged = Object.assign(parsed, process.env, { NODE_ENV })
process.env = merged
return merged
}

export function toNodeEnv(value: unknown): NodeEnv {
function toNodeEnv(value: unknown): NodeEnv {
if (isNodeEnv(value)) return value
return nodeEnvs.development
return NodeEnv.development
}

const nodeEnvs = {
const NodeEnv = {
development: "development",
production: "production",
/**
Expand All @@ -80,18 +24,32 @@ const nodeEnvs = {
*/
test: "test",
} as const

const parsed = loadEnv()

export const NODE_ENV = toNodeEnv(parsed.NODE_ENV)

export const BITBUCKET_CLOUD_URL = envUrl("BITBUCKET_CLOUD_URL")
export const BITBUCKET_CLOUD_USERNAME = envString("BITBUCKET_CLOUD_USERNAME")
export const BITBUCKET_CLOUD_APP_PASSWORD = envString(
"BITBUCKET_CLOUD_APP_PASSWORD",
)

export const BITBUCKET_SERVER_URL = envUrl("BITBUCKET_SERVER_URL")
export const BITBUCKET_SERVER_TOKEN = envString("BITBUCKET_SERVER_TOKEN")
export const NODE_ENV = parsed.NODE_ENV
export const BITBUCKET_SERVER_TEST_PROJECT_KEY = envString(
"BITBUCKET_SERVER_TEST_PROJECT_KEY",
)
export const BITBUCKET_SERVER_TEST_PROJECT_NAME = envString(
"BITBUCKET_SERVER_TEST_PROJECT_NAME",
)

export const SKIP_BITBUCKET_CLOUD = envBool("SKIP_BITBUCKET_CLOUD")

/** Considering that single instance for a single user costs 2300 USD annually,
* most people aren't going to have a Bitbucket Data Center instance to test on.
* Therefore, end-to-end tests for Bitbucket Data Center are skipped by default.
*
* @see https://www.atlassian.com/software/bitbucket/enterprise
*/
export const SKIP_BITBUCKET_SERVER = envBool("SKIP_BITBUCKET_SERVER")
3 changes: 2 additions & 1 deletion tests/server/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { describe, test } from "vitest"
import {
BITBUCKET_SERVER_TEST_PROJECT_KEY,
BITBUCKET_SERVER_TEST_PROJECT_NAME,
SKIP_BITBUCKET_SERVER,
} from "../env.ts"
import { client } from "./client.ts"

describe("Projects", () => {
describe.skipIf(SKIP_BITBUCKET_SERVER)("Projects", () => {
const key = BITBUCKET_SERVER_TEST_PROJECT_KEY
const name = BITBUCKET_SERVER_TEST_PROJECT_NAME

Expand Down
96 changes: 52 additions & 44 deletions tests/server/repositories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,69 @@ import { describe, test } from "vitest"
import {
BITBUCKET_SERVER_TEST_PROJECT_KEY,
BITBUCKET_SERVER_TEST_PROJECT_NAME,
SKIP_BITBUCKET_SERVER,
} from "../env.ts"
import { client } from "./client.ts"

describe("Repositories", { concurrent: false, sequential: true }, () => {
const projectKey = BITBUCKET_SERVER_TEST_PROJECT_KEY
const projectName = BITBUCKET_SERVER_TEST_PROJECT_NAME
const slug = "test-repository"
const name = "Test Repository"
describe.skipIf(SKIP_BITBUCKET_SERVER)(
"Repositories",
{ concurrent: false, sequential: true },
() => {
const projectKey = BITBUCKET_SERVER_TEST_PROJECT_KEY
const projectName = BITBUCKET_SERVER_TEST_PROJECT_NAME
const slug = "test-repository"
const name = "Test Repository"

test("Create repository", async ({ expect }) => {
const created = await client.POST(
"/api/latest/projects/{projectKey}/repos",
{ params: { path: { projectKey } }, body: { name, scmId: "git", slug } },
)
test("Create repository", async ({ expect }) => {
const created = await client.POST(
"/api/latest/projects/{projectKey}/repos",
{
params: { path: { projectKey } },
body: { name, scmId: "git", slug },
},
)

if (created.error)
console.error("Failed to create a repository", created.error)
if (created.error)
console.error("Failed to create a repository", created.error)

expect(created).toMatchObject({
data: {
expect(created).toMatchObject({
data: {
slug,
name,
project: { key: projectKey, name: projectName },
scmId: "git",
},
response: { status: 201 },
})
})

test("Get a repository", async ({ expect }) => {
const repository = await client.GET(
"/api/latest/projects/{projectKey}/repos/{repositorySlug}",
{ params: { path: { projectKey, repositorySlug: slug } } },
)

if (repository.error)
console.error("Failed to get a repository", repository.error)

expect(repository.data).toMatchObject({
slug,
name,
project: { key: projectKey, name: projectName },
scmId: "git",
},
response: { status: 201 },
})
})

test("Get a repository", async ({ expect }) => {
const repository = await client.GET(
"/api/latest/projects/{projectKey}/repos/{repositorySlug}",
{ params: { path: { projectKey, repositorySlug: slug } } },
)

if (repository.error)
console.error("Failed to get a repository", repository.error)

expect(repository.data).toMatchObject({
slug,
name,
project: { key: projectKey, name: projectName },
scmId: "git",
})
})
})

test("Delete a repository", async ({ expect }) => {
const deleted = await client.DELETE(
"/api/latest/projects/{projectKey}/repos/{repositorySlug}",
{ params: { path: { projectKey, repositorySlug: slug } } },
)
test("Delete a repository", async ({ expect }) => {
const deleted = await client.DELETE(
"/api/latest/projects/{projectKey}/repos/{repositorySlug}",
{ params: { path: { projectKey, repositorySlug: slug } } },
)

if (deleted.error)
console.error("Failed to delete a repository", deleted.error)
if (deleted.error)
console.error("Failed to delete a repository", deleted.error)

expect(deleted.response.status).toBe(202)
})
})
expect(deleted.response.status).toBe(202)
})
},
)
1 change: 1 addition & 0 deletions tsconfig.eslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"isolatedModules": true,
"verbatimModuleSyntax": true,
"isolatedDeclarations": true,
"erasableSyntaxOnly": true,
"forceConsistentCasingInFileNames": true,

/* Type Checking */
Expand Down
Loading