diff --git a/services/gitea/gitea-common-fetch.js b/services/gitea/gitea-common-fetch.js
new file mode 100644
index 0000000000000..84bc11a94bde4
--- /dev/null
+++ b/services/gitea/gitea-common-fetch.js
@@ -0,0 +1,14 @@
+async function fetchIssue(
+ serviceInstance,
+ { user, repo, baseUrl, options, httpErrors },
+) {
+ return serviceInstance._request(
+ serviceInstance.authHelper.withBearerAuthHeader({
+ url: `${baseUrl}/api/v1/repos/${user}/${repo}/issues`,
+ options,
+ httpErrors,
+ }),
+ )
+}
+
+export { fetchIssue }
diff --git a/services/gitea/gitea-forks.service.js b/services/gitea/gitea-forks.service.js
new file mode 100644
index 0000000000000..ac830c85383dc
--- /dev/null
+++ b/services/gitea/gitea-forks.service.js
@@ -0,0 +1,76 @@
+import Joi from 'joi'
+import { pathParam, queryParam } from '../index.js'
+import { optionalUrl, nonNegativeInteger } from '../validators.js'
+import { metric } from '../text-formatters.js'
+import GiteaBase from './gitea-base.js'
+import { description, httpErrorsFor } from './gitea-helper.js'
+
+const schema = Joi.object({
+ forks_count: nonNegativeInteger,
+}).required()
+
+const queryParamSchema = Joi.object({
+ gitea_url: optionalUrl,
+}).required()
+
+export default class GiteaForks extends GiteaBase {
+ static category = 'social'
+
+ static route = {
+ base: 'gitea/forks',
+ pattern: ':user/:repo',
+ queryParamSchema,
+ }
+
+ static openApi = {
+ '/gitea/forks/{user}/{repo}': {
+ get: {
+ summary: 'Gitea Forks',
+ description,
+ parameters: [
+ pathParam({
+ name: 'user',
+ example: 'gitea',
+ }),
+ pathParam({
+ name: 'repo',
+ example: 'tea',
+ }),
+ queryParam({
+ name: 'gitea_url',
+ example: 'https://gitea.com',
+ }),
+ ],
+ },
+ },
+ }
+
+ static defaultBadgeData = { label: 'forks', namedLogo: 'gitea' }
+
+ static render({ baseUrl, user, repo, forkCount }) {
+ return {
+ message: metric(forkCount),
+ style: 'social',
+ color: 'blue',
+ link: [`${baseUrl}/${user}/${repo}`, `${baseUrl}/${user}/${repo}/forks`],
+ }
+ }
+
+ async fetch({ user, repo, baseUrl }) {
+ // https://gitea.com/api/swagger#/repository
+ return super.fetch({
+ schema,
+ url: `${baseUrl}/api/v1/repos/${user}/${repo}`,
+ httpErrors: httpErrorsFor(),
+ })
+ }
+
+ async handle({ user, repo }, { gitea_url: baseUrl = 'https://gitea.com' }) {
+ const { forks_count: forkCount } = await this.fetch({
+ user,
+ repo,
+ baseUrl,
+ })
+ return this.constructor.render({ baseUrl, user, repo, forkCount })
+ }
+}
diff --git a/services/gitea/gitea-forks.tester.js b/services/gitea/gitea-forks.tester.js
new file mode 100644
index 0000000000000..1795572231fe2
--- /dev/null
+++ b/services/gitea/gitea-forks.tester.js
@@ -0,0 +1,32 @@
+import { isMetric } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+
+export const t = await createServiceTester()
+
+t.create('Forks')
+ .get('/gitea/tea.json')
+ .expectBadge({
+ label: 'forks',
+ message: isMetric,
+ color: 'blue',
+ link: ['https://gitea.com/gitea/tea', 'https://gitea.com/gitea/tea/forks'],
+ })
+
+t.create('Forks (self-managed)')
+ .get('/Codeberg/forgejo.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'forks',
+ message: isMetric,
+ color: 'blue',
+ link: [
+ 'https://codeberg.org/Codeberg/forgejo',
+ 'https://codeberg.org/Codeberg/forgejo/forks',
+ ],
+ })
+
+t.create('Forks (project not found)')
+ .get('/CanisHelix/does-not-exist.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'forks',
+ message: 'user or repo not found',
+ })
diff --git a/services/gitea/gitea-helper.js b/services/gitea/gitea-helper.js
index 684f41d6bf44f..56bba95207891 100644
--- a/services/gitea/gitea-helper.js
+++ b/services/gitea/gitea-helper.js
@@ -1,3 +1,5 @@
+import { metric } from '../text-formatters.js'
+
const description = `
By default this badge looks for repositories on [gitea.com](https://gitea.com).
To specify another instance like [codeberg](https://codeberg.org/), [forgejo](https://forgejo.org/) or a self-hosted instance, use the \`gitea_url\` query param.
@@ -10,4 +12,24 @@ function httpErrorsFor() {
}
}
-export { description, httpErrorsFor }
+function renderIssue({ variant, labels, defaultBadgeData, count }) {
+ const state = variant.split('-')[0]
+ const raw = variant.endsWith('-raw')
+ const isMultiLabel = labels && labels.includes(',')
+ const labelText = labels ? `${isMultiLabel ? `${labels}` : labels} ` : ''
+
+ let labelPrefix = ''
+ let messageSuffix = ''
+ if (raw) {
+ labelPrefix = `${state} `
+ } else {
+ messageSuffix = state
+ }
+ return {
+ label: `${labelPrefix}${labelText}${defaultBadgeData.label}`,
+ message: `${metric(count)}${messageSuffix ? ' ' : ''}${messageSuffix}`,
+ color: count > 0 ? 'yellow' : 'brightgreen',
+ }
+}
+
+export { description, httpErrorsFor, renderIssue }
diff --git a/services/gitea/gitea-issues.service.js b/services/gitea/gitea-issues.service.js
new file mode 100644
index 0000000000000..cac75aaea869b
--- /dev/null
+++ b/services/gitea/gitea-issues.service.js
@@ -0,0 +1,96 @@
+import Joi from 'joi'
+import { pathParam, queryParam } from '../index.js'
+import { optionalUrl, nonNegativeInteger } from '../validators.js'
+import { fetchIssue } from './gitea-common-fetch.js'
+import { description, httpErrorsFor, renderIssue } from './gitea-helper.js'
+import GiteaBase from './gitea-base.js'
+
+const schema = Joi.object({ 'x-total-count': nonNegativeInteger }).required()
+
+const queryParamSchema = Joi.object({
+ labels: Joi.string(),
+ gitea_url: optionalUrl,
+}).required()
+
+export default class GiteaIssues extends GiteaBase {
+ static category = 'issue-tracking'
+
+ static route = {
+ base: 'gitea/issues',
+ pattern:
+ ':variant(all|all-raw|open|open-raw|closed|closed-raw)/:user/:repo+',
+ queryParamSchema,
+ }
+
+ static openApi = {
+ '/gitea/issues/{variant}/{user}/{repo}': {
+ get: {
+ summary: 'Gitea Issues',
+ description,
+ parameters: [
+ pathParam({
+ name: 'variant',
+ example: 'all',
+ schema: { type: 'string', enum: this.getEnum('variant') },
+ }),
+ pathParam({
+ name: 'user',
+ example: 'gitea',
+ }),
+ pathParam({
+ name: 'repo',
+ example: 'tea',
+ }),
+ queryParam({
+ name: 'gitea_url',
+ example: 'https://gitea.com',
+ }),
+ queryParam({
+ name: 'labels',
+ example: 'test,failure::new',
+ description:
+ 'If you want to use multiple labels, you can use a comma (,
) to separate them, e.g. foo,bar
',
+ }),
+ ],
+ },
+ },
+ }
+
+ static defaultBadgeData = { label: 'issues', color: 'informational' }
+ async handle(
+ { variant, user, repo },
+ { gitea_url: baseUrl = 'https://gitea.com', labels },
+ ) {
+ const options = {
+ searchParams: {
+ page: '1',
+ limit: '1',
+ type: 'issues',
+ state: variant.replace('-raw', ''),
+ },
+ }
+ if (labels) {
+ options.searchParams.labels = labels
+ }
+
+ const { res } = await fetchIssue(this, {
+ user,
+ repo,
+ baseUrl,
+ options,
+ httpErrors: httpErrorsFor(),
+ })
+
+ const data = this.constructor._validate(res.headers, schema)
+ // The total number of issues is in the `x-total-count` field in the headers.
+ // Pull requests are an issue of type pulls
+ // https://gitea.com/api/swagger#/issue
+ const count = data['x-total-count']
+ return renderIssue({
+ variant,
+ labels,
+ defaultBadgeData: this.constructor.defaultBadgeData,
+ count,
+ })
+ }
+}
diff --git a/services/gitea/gitea-issues.tester.js b/services/gitea/gitea-issues.tester.js
new file mode 100644
index 0000000000000..cfbf71133548e
--- /dev/null
+++ b/services/gitea/gitea-issues.tester.js
@@ -0,0 +1,167 @@
+import { createServiceTester } from '../tester.js'
+import {
+ isMetric,
+ isMetricOpenIssues,
+ isMetricClosedIssues,
+ isMetricWithPattern,
+} from '../test-validators.js'
+
+export const t = await createServiceTester()
+
+t.create('Issues (project not found)')
+ .get('/open/CanisHelix/do-not-exist.json')
+ .expectBadge({
+ label: 'issues',
+ message: 'user or repo not found',
+ })
+
+/**
+ * Opened issue number case
+ */
+t.create('Opened issues')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'issues',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open issues raw')
+ .get(
+ '/open-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'open issues',
+ message: isMetric,
+ })
+
+t.create('Open issues by label is > zero')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question',
+ )
+ .expectBadge({
+ label: 'question issues',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open issues by multi-word label is > zero')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question,enhancement',
+ )
+ .expectBadge({
+ label: 'question,enhancement issues',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open issues by label (raw)')
+ .get(
+ '/open-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question',
+ )
+ .expectBadge({
+ label: 'open question issues',
+ message: isMetric,
+ })
+
+t.create('Opened issues by Scoped labels')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question,enhancement/new',
+ )
+ .expectBadge({
+ label: 'question,enhancement/new issues',
+ message: isMetricOpenIssues,
+ })
+
+/**
+ * Closed issue number case
+ */
+t.create('Closed issues')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'issues',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed issues raw')
+ .get(
+ '/closed-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'closed issues',
+ message: isMetric,
+ })
+
+t.create('Closed issues by label is > zero')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug',
+ )
+ .expectBadge({
+ label: 'bug issues',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed issues by multi-word label is > zero')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug,good%20first%20issue',
+ )
+ .expectBadge({
+ label: 'bug,good first issue issues',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed issues by label (raw)')
+ .get(
+ '/closed-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug',
+ )
+ .expectBadge({
+ label: 'closed bug issues',
+ message: isMetric,
+ })
+
+/**
+ * All issue number case
+ */
+t.create('All issues')
+ .get('/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'issues',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All issues raw')
+ .get(
+ '/all-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'all issues',
+ message: isMetric,
+ })
+
+t.create('All issues by label is > zero')
+ .get(
+ '/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question',
+ )
+ .expectBadge({
+ label: 'question issues',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All issues by multi-word label is > zero')
+ .get(
+ '/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question,enhancement',
+ )
+ .expectBadge({
+ label: 'question,enhancement issues',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All issues by label (raw)')
+ .get(
+ '/all-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=question',
+ )
+ .expectBadge({
+ label: 'all question issues',
+ message: isMetric,
+ })
diff --git a/services/gitea/gitea-pull-requests.service.js b/services/gitea/gitea-pull-requests.service.js
new file mode 100644
index 0000000000000..5b00f2485e218
--- /dev/null
+++ b/services/gitea/gitea-pull-requests.service.js
@@ -0,0 +1,97 @@
+import Joi from 'joi'
+import { pathParam, queryParam } from '../index.js'
+import { optionalUrl, nonNegativeInteger } from '../validators.js'
+import { fetchIssue } from './gitea-common-fetch.js'
+import { description, httpErrorsFor, renderIssue } from './gitea-helper.js'
+import GiteaBase from './gitea-base.js'
+
+const schema = Joi.object({ 'x-total-count': nonNegativeInteger }).required()
+
+const queryParamSchema = Joi.object({
+ labels: Joi.string(),
+ gitea_url: optionalUrl,
+}).required()
+
+export default class GiteaPullRequests extends GiteaBase {
+ static category = 'issue-tracking'
+
+ static route = {
+ base: 'gitea/pull-requests',
+ pattern:
+ ':variant(all|all-raw|open|open-raw|closed|closed-raw)/:user/:repo+',
+ queryParamSchema,
+ }
+
+ static openApi = {
+ '/gitea/pull-requests/{variant}/{user}/{repo}': {
+ get: {
+ summary: 'Gitea Pull Requests',
+ description,
+ parameters: [
+ pathParam({
+ name: 'variant',
+ example: 'all',
+ schema: { type: 'string', enum: this.getEnum('variant') },
+ }),
+ pathParam({
+ name: 'user',
+ example: 'gitea',
+ }),
+ pathParam({
+ name: 'repo',
+ example: 'tea',
+ }),
+ queryParam({
+ name: 'gitea_url',
+ example: 'https://gitea.com',
+ }),
+ queryParam({
+ name: 'labels',
+ example: 'test,failure::new',
+ description:
+ 'If you want to use multiple labels, you can use a comma (,
) to separate them, e.g. foo,bar
',
+ }),
+ ],
+ },
+ },
+ }
+
+ static defaultBadgeData = { label: 'pull requests', color: 'informational' }
+
+ async handle(
+ { variant, user, repo },
+ { gitea_url: baseUrl = 'https://gitea.com', labels },
+ ) {
+ const options = {
+ searchParams: {
+ page: '1',
+ limit: '1',
+ type: 'pulls',
+ state: variant.replace('-raw', ''),
+ },
+ }
+ if (labels) {
+ options.searchParams.labels = labels
+ }
+
+ const { res } = await fetchIssue(this, {
+ user,
+ repo,
+ baseUrl,
+ options,
+ httpErrors: httpErrorsFor(),
+ })
+
+ const data = this.constructor._validate(res.headers, schema)
+ // The total number of issues is in the `x-total-count` field in the headers.
+ // Pull requests are an issue of type pulls
+ // https://gitea.com/api/swagger#/issue
+ const count = data['x-total-count']
+ return renderIssue({
+ variant,
+ labels,
+ defaultBadgeData: this.constructor.defaultBadgeData,
+ count,
+ })
+ }
+}
diff --git a/services/gitea/gitea-pull-requests.tester.js b/services/gitea/gitea-pull-requests.tester.js
new file mode 100644
index 0000000000000..a2849fbc7e9aa
--- /dev/null
+++ b/services/gitea/gitea-pull-requests.tester.js
@@ -0,0 +1,167 @@
+import { createServiceTester } from '../tester.js'
+import {
+ isMetric,
+ isMetricOpenIssues,
+ isMetricClosedIssues,
+ isMetricWithPattern,
+} from '../test-validators.js'
+
+export const t = await createServiceTester()
+
+t.create('Pulls (project not found)')
+ .get('/open/CanisHelix/do-not-exist.json')
+ .expectBadge({
+ label: 'pull requests',
+ message: 'user or repo not found',
+ })
+
+/**
+ * Opened pulls number case
+ */
+t.create('Opened pulls')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'pull requests',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open pulls raw')
+ .get(
+ '/open-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'open pull requests',
+ message: isMetric,
+ })
+
+t.create('Open pulls by label is > zero')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream',
+ )
+ .expectBadge({
+ label: 'upstream pull requests',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open pulls by multi-word label is > zero')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream,enhancement',
+ )
+ .expectBadge({
+ label: 'upstream,enhancement pull requests',
+ message: isMetricOpenIssues,
+ })
+
+t.create('Open pulls by label (raw)')
+ .get(
+ '/open-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream',
+ )
+ .expectBadge({
+ label: 'open upstream pull requests',
+ message: isMetric,
+ })
+
+t.create('Opened pulls by Scoped label')
+ .get(
+ '/open/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=failure/new',
+ )
+ .expectBadge({
+ label: 'failure/new pull requests',
+ message: isMetricOpenIssues,
+ })
+
+/**
+ * Closed pulls number case
+ */
+t.create('Closed pulls')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'pull requests',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed pulls raw')
+ .get(
+ '/closed-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'closed pull requests',
+ message: isMetric,
+ })
+
+t.create('Closed pulls by label is > zero')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug',
+ )
+ .expectBadge({
+ label: 'bug pull requests',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed pulls by multi-word label is > zero')
+ .get(
+ '/closed/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug,good%20first%20issue',
+ )
+ .expectBadge({
+ label: 'bug,good first issue pull requests',
+ message: isMetricClosedIssues,
+ })
+
+t.create('Closed pulls by label (raw)')
+ .get(
+ '/closed-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=bug',
+ )
+ .expectBadge({
+ label: 'closed bug pull requests',
+ message: isMetric,
+ })
+
+/**
+ * All pulls number case
+ */
+t.create('All pulls')
+ .get('/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'pull requests',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All pulls raw')
+ .get(
+ '/all-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org',
+ )
+ .expectBadge({
+ label: 'all pull requests',
+ message: isMetric,
+ })
+
+t.create('All pulls by label is > zero')
+ .get(
+ '/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream',
+ )
+ .expectBadge({
+ label: 'upstream pull requests',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All pulls by multi-word label is > zero')
+ .get(
+ '/all/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream,enhancement',
+ )
+ .expectBadge({
+ label: 'upstream,enhancement pull requests',
+ message: isMetricWithPattern(/ all/),
+ })
+
+t.create('All pulls by label (raw)')
+ .get(
+ '/all-raw/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&labels=upstream',
+ )
+ .expectBadge({
+ label: 'all upstream pull requests',
+ message: isMetric,
+ })
diff --git a/services/gitea/gitea-stars.service.js b/services/gitea/gitea-stars.service.js
new file mode 100644
index 0000000000000..43732b4068e59
--- /dev/null
+++ b/services/gitea/gitea-stars.service.js
@@ -0,0 +1,76 @@
+import Joi from 'joi'
+import { pathParam, queryParam } from '../index.js'
+import { optionalUrl, nonNegativeInteger } from '../validators.js'
+import { metric } from '../text-formatters.js'
+import GiteaBase from './gitea-base.js'
+import { description, httpErrorsFor } from './gitea-helper.js'
+
+const schema = Joi.object({
+ stars_count: nonNegativeInteger,
+}).required()
+
+const queryParamSchema = Joi.object({
+ gitea_url: optionalUrl,
+}).required()
+
+export default class GiteaStars extends GiteaBase {
+ static category = 'social'
+
+ static route = {
+ base: 'gitea/stars',
+ pattern: ':user/:repo',
+ queryParamSchema,
+ }
+
+ static openApi = {
+ '/gitea/stars/{user}/{repo}': {
+ get: {
+ summary: 'Gitea Stars',
+ description,
+ parameters: [
+ pathParam({
+ name: 'user',
+ example: 'gitea',
+ }),
+ pathParam({
+ name: 'repo',
+ example: 'tea',
+ }),
+ queryParam({
+ name: 'gitea_url',
+ example: 'https://gitea.com',
+ }),
+ ],
+ },
+ },
+ }
+
+ static defaultBadgeData = { label: 'stars', namedLogo: 'gitea' }
+
+ static render({ baseUrl, user, repo, starCount }) {
+ return {
+ message: metric(starCount),
+ style: 'social',
+ color: 'blue',
+ link: [`${baseUrl}/${user}/${repo}`, `${baseUrl}/${user}/${repo}/stars`],
+ }
+ }
+
+ async fetch({ user, repo, baseUrl }) {
+ // https://gitea.com/api/swagger#/repository
+ return super.fetch({
+ schema,
+ url: `${baseUrl}/api/v1/repos/${user}/${repo}`,
+ httpErrors: httpErrorsFor(),
+ })
+ }
+
+ async handle({ user, repo }, { gitea_url: baseUrl = 'https://gitea.com' }) {
+ const { stars_count: starCount } = await this.fetch({
+ user,
+ repo,
+ baseUrl,
+ })
+ return this.constructor.render({ baseUrl, user, repo, starCount })
+ }
+}
diff --git a/services/gitea/gitea-stars.tester.js b/services/gitea/gitea-stars.tester.js
new file mode 100644
index 0000000000000..bec444bfa74e3
--- /dev/null
+++ b/services/gitea/gitea-stars.tester.js
@@ -0,0 +1,32 @@
+import { isMetric } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+
+export const t = await createServiceTester()
+
+t.create('Stars')
+ .get('/gitea/tea.json')
+ .expectBadge({
+ label: 'stars',
+ message: isMetric,
+ color: 'blue',
+ link: ['https://gitea.com/gitea/tea', 'https://gitea.com/gitea/tea/stars'],
+ })
+
+t.create('Stars (self-managed)')
+ .get('/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'stars',
+ message: isMetric,
+ color: 'blue',
+ link: [
+ 'https://codeberg.org/CanisHelix/shields-badge-test',
+ 'https://codeberg.org/CanisHelix/shields-badge-test/stars',
+ ],
+ })
+
+t.create('Stars (project not found)')
+ .get('/CanisHelix/does-not-exist.json?gitea_url=https://codeberg.org')
+ .expectBadge({
+ label: 'stars',
+ message: 'user or repo not found',
+ })